From 74d0600261706fd67257119b443733fd9e95fb05 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 01:41:57 +0800 Subject: [PATCH 01/50] feat(filesystem): add PageIndex filesystem shell --- .gitignore | 1 + examples/pifs_demo.py | 752 ++++++ pageindex/__init__.py | 14 +- pageindex/filesystem/__init__.py | 36 + pageindex/filesystem/agent.py | 514 +++++ pageindex/filesystem/cli.py | 47 + pageindex/filesystem/commands.py | 1639 +++++++++++++ pageindex/filesystem/core.py | 1771 +++++++++++++++ pageindex/filesystem/hybrid_projection.py | 662 ++++++ pageindex/filesystem/metadata.py | 152 ++ pageindex/filesystem/metadata_generation.py | 139 ++ pageindex/filesystem/projection_indexing.py | 131 ++ .../filesystem/semantic_folder_policy.py | 72 + pageindex/filesystem/semantic_index.py | 362 +++ pageindex/filesystem/store.py | 2020 +++++++++++++++++ pageindex/filesystem/structural_read.py | 40 + pageindex/filesystem/types.py | 87 + pifs-cli | 6 + pyproject.toml | 14 + tests/test_pageindex_filesystem_scope.py | 60 + tests/test_pageindex_structural_read.py | 632 ++++++ tests/test_pifs_agent_stream.py | 185 ++ tests/test_semantic_index.py | 53 + uv.lock | 1988 ++++++++++++++++ 24 files changed, 11373 insertions(+), 4 deletions(-) create mode 100644 examples/pifs_demo.py create mode 100644 pageindex/filesystem/__init__.py create mode 100644 pageindex/filesystem/agent.py create mode 100644 pageindex/filesystem/cli.py create mode 100644 pageindex/filesystem/commands.py create mode 100644 pageindex/filesystem/core.py create mode 100644 pageindex/filesystem/hybrid_projection.py create mode 100644 pageindex/filesystem/metadata.py create mode 100644 pageindex/filesystem/metadata_generation.py create mode 100644 pageindex/filesystem/projection_indexing.py create mode 100644 pageindex/filesystem/semantic_folder_policy.py create mode 100644 pageindex/filesystem/semantic_index.py create mode 100644 pageindex/filesystem/store.py create mode 100644 pageindex/filesystem/structural_read.py create mode 100644 pageindex/filesystem/types.py create mode 100755 pifs-cli create mode 100644 pyproject.toml create mode 100644 tests/test_pageindex_filesystem_scope.py create mode 100644 tests/test_pageindex_structural_read.py create mode 100644 tests/test_pifs_agent_stream.py create mode 100644 tests/test_semantic_index.py create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore index 23d6b5655..3702bdaa9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ __pycache__ .env* .venv/ logs/ +examples/pifs_workspace/ diff --git a/examples/pifs_demo.py b/examples/pifs_demo.py new file mode 100644 index 000000000..fddec042d --- /dev/null +++ b/examples/pifs_demo.py @@ -0,0 +1,752 @@ +""" +PageIndex FileSystem (PIFS) agent demo. + +This mirrors examples/agentic_vectorless_rag_demo.py, but exposes a corpus +through the PageIndex FileSystem shell instead of direct PageIndex document +tools. The agent receives one read-only bash-like PIFS tool and must retrieve +evidence through commands such as ls, tree, find, grep, search-summary, +cat --structure, cat --page, and cat --node. + +The demo uses PDFs under examples/documents. When a matching +examples/documents/results/*_structure.json file exists, it is loaded into the +PIFS workspace's PageIndexClient cache so register() does not rebuild the tree. + +Requirements: + pip install openai-agents + +Example: + python examples/pifs_demo.py --stream-mode all --verbose +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import shutil +import sys +import time +from pathlib import Path +from typing import Any + +import PyPDF2 + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +# Keep the local demo quiet in offline environments. +os.environ.setdefault("LITELLM_LOCAL_MODEL_COST_MAP", "true") + +from pageindex import PageIndexClient +from pageindex.filesystem import OpenAIMetadataGenerator, PageIndexFileSystem, PIFSCommandExecutor +from pageindex.filesystem.agent import run_pifs_agent + + +EXAMPLES_DIR = Path(__file__).parent +DOCUMENTS_DIR = EXAMPLES_DIR / "documents" +WORKSPACE = EXAMPLES_DIR / "pifs_workspace" +DEFAULT_MODEL = os.environ.get("PIFS_DEMO_MODEL", "gpt-5.4-mini") +DEFAULT_QUESTION = ( + "Use the PIFS workspace to find the Federal Reserve annual report. " + "Which section covers supervision and regulation, and what page range " + "should I inspect? Cite the document and evidence you used." +) + +PIFS_DEMO_AGENT_PROMPT = """ +You are a PageIndex FileSystem retrieval agent for a local demo workspace. + +Use only the bash tool. It is a read-only PIFS virtual shell, not a real OS +shell. The workspace contains registered example PDFs. + +Retrieval strategy: +- Start with ls or tree to understand the workspace. +- Use refs exactly as listed, such as ref_1, or use a concrete file path from + ls output. Do not invent paths like /documents/ref_1. +- Use search-summary when available to find likely documents. + Quote multi-word queries and include a path, for example: + search-summary "Federal Reserve supervision regulation" /documents +- Use find --where only with JSON metadata DSL, for example: + find /documents --where '{"file_format":"pdf"}' +- Use grep -R only for lexical evidence; do not treat semantic candidates as + literal matches. +- Run one evidence command at a time. Do not chain large commands like + cat --structure, grep, and cat --page in one bash call. +- For PDFs, use cat --structure to inspect the PageIndex tree, then + cat --page for evidence, for example: + cat --page 31-35 ref_1 +- For page-range questions, use cat --structure to identify the full section + range. Then run cat --page on the smallest useful evidence range, usually the + section start page or first 1-2 pages, before the final answer. Do not print + a broad multi-page section unless the user asks to read the whole section. +- Do not use cat --all on PDFs. +- Answer only from PIFS tool output and cite file refs or document ids. +""" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Run a PIFS document retrieval agent demo.") + parser.add_argument("--workspace", type=Path, default=WORKSPACE) + parser.add_argument("--documents-dir", type=Path, default=DOCUMENTS_DIR) + parser.add_argument( + "--document", + action="append", + default=[], + help="Specific document filename or path to register. May be repeated.", + ) + parser.add_argument( + "--max-docs", + type=int, + default=0, + help="Limit number of cached example documents to register. 0 means all.", + ) + parser.add_argument("--reset", action="store_true", help="Delete and rebuild the demo workspace.") + parser.add_argument( + "--prepare-only", + action="store_true", + help="Register documents and print PIFS smoke commands without running the agent.", + ) + parser.add_argument("--question", default=DEFAULT_QUESTION) + parser.add_argument("--model", default=DEFAULT_MODEL) + parser.add_argument( + "--metadata-model", + default=os.environ.get("PIFS_METADATA_MODEL", "gpt-5-nano"), + help="OpenAI or OpenAI-compatible model used for register-time metadata.", + ) + parser.add_argument("--stream-mode", default="all", choices=["off", "tools", "model", "all"]) + parser.add_argument("--verbose", action="store_true") + parser.add_argument("--max-turns", type=int, default=12) + parser.add_argument("--max-seconds", type=float, default=90) + parser.add_argument("--reasoning-effort", default=None) + parser.add_argument("--reasoning-summary", default="auto") + parser.add_argument( + "--embedding-model", + default=os.environ.get("PIFS_DEMO_EMBEDDING_MODEL", "text-embedding-3-small"), + help="OpenAI embedding model used for register-time summary projection.", + ) + parser.add_argument("--embedding-dimensions", type=int, default=256) + return parser.parse_args() + + +def require_openai_environment() -> None: + if os.environ.get("OPENAI_API_KEY"): + return + raise RuntimeError( + "OPENAI_API_KEY is required for this demo: register() generates real " + "PIFS metadata and the agent uses the OpenAI Agents SDK. Source your " + ".env or export OPENAI_API_KEY before running." + ) + + +def discover_cached_documents(documents_dir: Path) -> list[Path]: + results_dir = documents_dir / "results" + paths: list[Path] = [] + for structure_path in sorted(results_dir.glob("*_structure.json")): + stem = structure_path.name.removesuffix("_structure.json") + for suffix in (".pdf", ".md", ".markdown"): + candidate = documents_dir / f"{stem}{suffix}" + if candidate.exists(): + paths.append(candidate) + break + return paths + + +def resolve_requested_documents(documents_dir: Path, requested: list[str]) -> list[Path]: + if not requested: + return discover_cached_documents(documents_dir) + paths: list[Path] = [] + for item in requested: + path = Path(item).expanduser() + if not path.is_absolute(): + path = documents_dir / path + if not path.exists(): + raise FileNotFoundError(f"document not found: {path}") + paths.append(path) + return paths + + +def structure_path_for(document_path: Path, documents_dir: Path) -> Path | None: + path = documents_dir / "results" / f"{document_path.stem}_structure.json" + return path if path.exists() else None + + +def deterministic_doc_id(document_path: Path) -> str: + digest = hashlib.sha1(str(document_path.resolve()).encode("utf-8")).hexdigest()[:16] + return f"pifs_demo_{digest}" + + +def read_pdf_pages(document_path: Path) -> list[dict[str, Any]]: + pages: list[dict[str, Any]] = [] + with document_path.open("rb") as handle: + reader = PyPDF2.PdfReader(handle) + for page_num, page in enumerate(reader.pages, 1): + pages.append({"page": page_num, "content": page.extract_text() or ""}) + return pages + + +def load_structure_json(structure_path: Path) -> dict[str, Any]: + with structure_path.open("r", encoding="utf-8") as handle: + payload = json.load(handle) + if not isinstance(payload, dict) or not isinstance(payload.get("structure"), list): + raise ValueError(f"invalid PageIndex structure cache: {structure_path}") + return payload + + +def seed_pageindex_cache( + filesystem: PageIndexFileSystem, + document_path: Path, + *, + documents_dir: Path, +) -> str | None: + structure_path = structure_path_for(document_path, documents_dir) + if structure_path is None: + return None + + filesystem.pageindex_client_workspace.mkdir(parents=True, exist_ok=True) + meta_path = filesystem.pageindex_client_workspace / "_meta.json" + if not meta_path.exists(): + meta_path.write_text("{}", encoding="utf-8") + client = PageIndexClient(workspace=str(filesystem.pageindex_client_workspace)) + canonical_path = str(document_path.resolve()) + for doc_id, doc in client.documents.items(): + if Path(str(doc.get("path") or "")).resolve(strict=False) == Path(canonical_path): + return doc_id + + payload = load_structure_json(structure_path) + doc_id = deterministic_doc_id(document_path) + suffix = document_path.suffix.lower() + if suffix == ".pdf": + pages = read_pdf_pages(document_path) + client.documents[doc_id] = { + "id": doc_id, + "type": "pdf", + "path": canonical_path, + "doc_name": payload.get("doc_name") or document_path.name, + "doc_description": payload.get("doc_description") or "", + "page_count": len(pages), + "structure": payload["structure"], + "pages": pages, + } + elif suffix in {".md", ".markdown"}: + text = document_path.read_text(encoding="utf-8") + client.documents[doc_id] = { + "id": doc_id, + "type": "md", + "path": canonical_path, + "doc_name": payload.get("doc_name") or document_path.name, + "doc_description": payload.get("doc_description") or "", + "line_count": len(text.splitlines()), + "structure": payload["structure"], + } + else: + return None + client._save_doc(doc_id) + return doc_id + + +def content_type_for(path: Path) -> str: + suffix = path.suffix.lower() + if suffix == ".pdf": + return "application/pdf" + if suffix in {".md", ".markdown"}: + return "text/markdown" + return "text/plain" + + +def external_id_for(path: Path) -> str: + slug = "".join(ch.lower() if ch.isalnum() else "_" for ch in path.stem).strip("_") + slug = "_".join(part for part in slug.split("_") if part) + return f"example_{slug}" + + +def log_progress(message: str, *, indent: int = 0) -> None: + prefix = " " * indent + print(f"{prefix}{message}", flush=True) + + +def register_demo_metadata_schema(filesystem: PageIndexFileSystem) -> None: + filesystem.metadata.register_schema( + { + "fields": { + "source_collection": { + "type": "string", + "description": "Local example corpus collection.", + }, + "file_format": { + "type": "string", + "description": "Source file extension without the leading dot.", + }, + } + }, + source="demo", + ) + + +def backfill_registered_metadata_values(filesystem: PageIndexFileSystem, file_ref: str) -> None: + entry = filesystem.store.get_file(file_ref) + indexed_metadata = dict(entry.metadata or {}) + indexed_metadata.update(entry.derived_metadata or {}) + with filesystem.store.connect() as conn: + filesystem.store.replace_metadata_values(conn, file_ref, indexed_metadata) + + +def configure_summary_projection_backend( + filesystem: PageIndexFileSystem, + *, + embedding_model: str, + embedding_dimensions: int, +) -> None: + if not (filesystem.summary_projection_index_dir / "summary_only_vector.sqlite").exists(): + return + filesystem.configure_hybrid_projection_retrieval( + filesystem.summary_projection_index_dir, + embedding_provider="openai", + embedding_model=embedding_model, + embedding_dimensions=embedding_dimensions, + ) + + +def has_ready_register_outputs(filesystem: PageIndexFileSystem, external_id: str) -> bool: + try: + file_ref = filesystem.store.resolve_file_ref(external_id) + entry = filesystem.store.get_file(file_ref) + except KeyError: + return False + generation = entry.metadata_generation or {} + fields = generation.get("fields") or {} + required = ("summary", "doc_type", "domain", "topic") + if any(fields.get(field, {}).get("status") != "generated" for field in required): + return False + summary_projection = (generation.get("projection_indexes") or {}).get("summary") or {} + return summary_projection.get("status") == "ready" + + +def register_documents( + filesystem: PageIndexFileSystem, + documents: list[Path], + *, + documents_dir: Path, +) -> list[dict[str, Any]]: + registered: list[dict[str, Any]] = [] + total = len(documents) + for index, document_path in enumerate(documents, 1): + document_path = document_path.resolve() + external_id = external_id_for(document_path) + log_progress(f"[{index}/{total}] {document_path.name}") + log_progress("PageIndex tree cache: checking examples/documents/results", indent=1) + cache_started = time.perf_counter() + cached_doc_id = seed_pageindex_cache( + filesystem, + document_path, + documents_dir=documents_dir, + ) + cache_seconds = time.perf_counter() - cache_started + if cached_doc_id: + log_progress( + f"PageIndex tree cache: ready doc_id={cached_doc_id} ({cache_seconds:.2f}s)", + indent=1, + ) + else: + log_progress( + f"PageIndex tree cache: no cached structure; register() will index if supported ({cache_seconds:.2f}s)", + indent=1, + ) + if has_ready_register_outputs(filesystem, external_id): + file_ref = filesystem.store.resolve_file_ref(external_id) + backfill_registered_metadata_values(filesystem, file_ref) + log_progress( + f"PIFS register: cached file_ref={file_ref}; metadata and summary projection already ready", + indent=1, + ) + registered.append( + { + "file_ref": file_ref, + "external_id": external_id, + "path": str(document_path), + "status": "cached", + "pageindex_doc_id": cached_doc_id, + } + ) + continue + + log_progress( + "PIFS register: running register() -> metadata generation -> summary embedding -> sqlite upsert", + indent=1, + ) + register_started = time.perf_counter() + file_ref = filesystem.register( + storage_uri=document_path.as_uri(), + source_path=str(document_path), + folder_path="/documents", + external_id=external_id, + title=document_path.name, + content_type=content_type_for(document_path), + source_type="examples-documents", + metadata={ + "title": document_path.name, + "source_collection": "examples/documents", + "file_format": document_path.suffix.lower().lstrip("."), + }, + ) + register_seconds = time.perf_counter() - register_started + entry = filesystem.store.get_file(file_ref) + field_status = { + field: state.get("status") + for field, state in (entry.metadata_generation.get("fields") or {}).items() + } + summary_projection = ( + entry.metadata_generation.get("projection_indexes", {}).get("summary", {}) + ) + log_progress( + f"PIFS register: done file_ref={file_ref} ({register_seconds:.2f}s)", + indent=1, + ) + log_progress( + f"metadata: {entry.metadata_generation.get('status', 'unknown')} fields={field_status}", + indent=1, + ) + log_progress( + "summary projection: " + f"{summary_projection.get('status', 'not_requested')} " + f"index={summary_projection.get('index_path', '')}", + indent=1, + ) + registered.append( + { + "file_ref": file_ref, + "external_id": external_id, + "path": str(document_path), + "status": entry.metadata_generation.get("status", "unknown"), + "pageindex_tree_status": entry.pageindex_tree_status, + "pageindex_doc_id": entry.pageindex_doc_id, + } + ) + return registered + + +def print_section(title: str) -> None: + print("\n" + "#" * 78, flush=True) + print(f"# {title}", flush=True) + print("#" * 78, flush=True) + + +def print_step(title: str, detail: str = "") -> None: + print(f"\n>>> {title}", flush=True) + if detail: + print(f" {detail}", flush=True) + + +def sanitize_preview_text(text: str) -> str: + cleaned = str(text).replace("\r", "\n").replace("\f", "\n") + cleaned = "".join( + ch if ch == "\n" or ch == "\t" or ord(ch) >= 32 else " " + for ch in cleaned + ) + return "\n".join( + re.sub(r"[ \t]{2,}", " ", line).strip() + for line in cleaned.splitlines() + ) + + +def compact_lines(text: str, *, max_lines: int = 6, max_chars: int = 900) -> str: + lines = [line for line in sanitize_preview_text(text).splitlines() if line.strip()] + preview = "\n".join(lines[:max_lines]) + if len(preview) > max_chars: + preview = preview[:max_chars].rstrip() + "..." + omitted = len(lines) - min(len(lines), max_lines) + if omitted > 0: + preview += f"\n ... {omitted} more lines" + return preview + + +def find_structure_node(structure: Any, title_fragment: str) -> dict[str, Any] | None: + if isinstance(structure, list): + for item in structure: + found = find_structure_node(item, title_fragment) + if found: + return found + return None + if not isinstance(structure, dict): + return None + if title_fragment.lower() in str(structure.get("title", "")).lower(): + return structure + return find_structure_node(structure.get("nodes", []), title_fragment) + + +def page_range_for_node(node: dict[str, Any] | None) -> str: + if not node: + return "" + ranges: list[tuple[int, int]] = [] + + def collect(item: Any) -> None: + if not isinstance(item, dict): + return + start = item.get("start_index") + end = item.get("end_index") + if isinstance(start, int) and isinstance(end, int): + ranges.append((start, end)) + for child in item.get("nodes") or []: + collect(child) + + collect(node) + if not ranges: + return "" + start = min(item[0] for item in ranges) + end = max(item[1] for item in ranges) + return f"{start}-{end}" if start != end else str(start) + + +def opening_page_range_for_node(node: dict[str, Any] | None, *, max_pages: int = 2) -> str: + if not node: + return "" + ranges: list[tuple[int, int]] = [] + + def collect(item: Any) -> None: + if not isinstance(item, dict): + return + start = item.get("start_index") + end = item.get("end_index") + if isinstance(start, int) and isinstance(end, int): + ranges.append((start, end)) + for child in item.get("nodes") or []: + collect(child) + + collect(node) + if not ranges: + return "" + start = min(item[0] for item in ranges) + end = max(item[1] for item in ranges) + preview_end = min(end, start + max_pages - 1) + return f"{start}-{preview_end}" if start != preview_end else str(start) + + +def execute_json_command(executor: PIFSCommandExecutor, command: str) -> dict[str, Any]: + try: + return json.loads(executor.execute(command)) + except Exception as exc: + return {"ok": False, "error": str(exc), "data": None} + + +def show_capability( + *, + label: str, + command: str, + result: str, + raw: str = "", + verbose: bool = False, +) -> None: + print_step(label, command) + print(f" result: {result}", flush=True) + if verbose and raw: + print(" raw:", flush=True) + print(compact_lines(raw, max_lines=10, max_chars=1600), flush=True) + + +def show_registered_documents(registered: list[dict[str, Any]], *, verbose: bool = False) -> None: + print(f"\nRegistered {len(registered)} document(s):", flush=True) + for item in registered: + print( + " - " + f"{Path(str(item.get('path', ''))).name}: " + f"file_ref={item.get('file_ref')} | " + f"status={item.get('status')} | " + f"pageindex_doc_id={item.get('pageindex_doc_id')}", + flush=True, + ) + if verbose: + print("\nRaw registration records:", flush=True) + print(json.dumps(registered, ensure_ascii=False, indent=2), flush=True) + + +def run_smoke_commands( + filesystem: PageIndexFileSystem, + registered: list[dict[str, Any]], + *, + verbose: bool = False, +) -> None: + json_executor = PIFSCommandExecutor(filesystem, json_output=True) + shell_executor = PIFSCommandExecutor(filesystem, json_output=False) + + command = "tree / --depth 2" + tree = execute_json_command(json_executor, command) + folders = (tree.get("data") or {}).get("folders") or [] + documents_folder = next((item for item in folders if item.get("path") == "/documents"), {}) + show_capability( + label="Folder browse", + command=command, + result=f"/documents contains {documents_folder.get('file_count', len(registered))} files", + raw=shell_executor.execute(command) if verbose else "", + verbose=verbose, + ) + + command = "ls /documents" + listing = execute_json_command(json_executor, command) + files = (listing.get("data") or {}).get("files") or [] + file_titles = ", ".join(item.get("title", "") for item in files[:3]) + show_capability( + label="List registered files", + command=command, + result=f"{len(files)} files: {file_titles}", + raw=shell_executor.execute(command) if verbose else "", + verbose=verbose, + ) + + command = "stat --schema" + schema = execute_json_command(json_executor, command) + fields = sorted(((schema.get("data") or {}).get("fields") or {}).keys()) + show_capability( + label="Metadata schema", + command=command, + result=", ".join(fields), + raw=shell_executor.execute(command) if verbose else "", + verbose=verbose, + ) + + command = "find /documents --where '{\"source_collection\":\"examples/documents\"}' --limit 5" + found = execute_json_command(json_executor, command) + found_files = found.get("data") or [] + show_capability( + label="Metadata DSL filter", + command=command, + result=f"{len(found_files)} documents matched source_collection=examples/documents", + raw=shell_executor.execute(command) if verbose else "", + verbose=verbose, + ) + + command = 'search-summary "Federal Reserve annual report supervision regulation section page range" /documents' + summary = execute_json_command(json_executor, command) + summary_hits = ((summary.get("data") or {}).get("data") or []) + if summary_hits: + summary_result = f"{len(summary_hits)} summary-vector candidates; top={summary_hits[0].get('external_id')}" + else: + summary_result = "summary-vector command is available, but this tiny two-doc demo returned no candidates" + show_capability( + label="Semantic summary search", + command=command, + result=summary_result, + raw=shell_executor.execute(command) if verbose else "", + verbose=verbose, + ) + + first_ref = registered[0]["file_ref"] if registered else None + if not first_ref: + return + + command = f"stat {first_ref}" + stat = execute_json_command(json_executor, command) + stat_data = stat.get("data") or {} + show_capability( + label="File stat", + command=command, + result=( + f"{stat_data.get('title')} | tree={stat_data.get('pageindex_tree_status')} | " + f"metadata_status={(stat_data.get('metadata_generation') or {}).get('status')}" + ), + raw=shell_executor.execute(command) if verbose else "", + verbose=verbose, + ) + + command = f"cat --structure {first_ref}" + structure_payload = execute_json_command(json_executor, command) + structure_data = structure_payload.get("data") or {} + structure = structure_data.get("structure") or [] + supervision_node = find_structure_node(structure, "Supervision and Regulation") + supervision_range = page_range_for_node(supervision_node) + show_capability( + label="PageIndex document structure", + command=command, + result=( + "found section 'Supervision and Regulation'" + + (f" with page span {supervision_range}" if supervision_range else "") + ), + raw=shell_executor.execute(command) if verbose else "", + verbose=verbose, + ) + + evidence_range = opening_page_range_for_node(supervision_node) or "1-2" + command = f"cat --page {evidence_range} {first_ref}" + page = execute_json_command(json_executor, command) + page_text = str((page.get("data") or {}).get("text") or "") + show_capability( + label="Page evidence", + command=command, + result=compact_lines(page_text, max_lines=3, max_chars=420), + raw=shell_executor.execute(command) if verbose else "", + verbose=verbose, + ) + + command = 'grep -R "Supervision and Regulation" /documents' + grep = execute_json_command(json_executor, command) + grep_hits = ((grep.get("data") or {}).get("data") or []) + show_capability( + label="Lexical grep", + command=command, + result=f"{len(grep_hits)} real text matches", + raw=shell_executor.execute(command) if verbose else "", + verbose=verbose, + ) + + +def main() -> None: + args = parse_args() + require_openai_environment() + workspace = args.workspace.expanduser() + documents_dir = args.documents_dir.expanduser() + if args.reset and workspace.exists(): + shutil.rmtree(workspace) + workspace.mkdir(parents=True, exist_ok=True) + + documents = resolve_requested_documents(documents_dir, args.document) + if args.max_docs > 0: + documents = documents[: args.max_docs] + if not documents: + raise RuntimeError(f"no cached example documents found under {documents_dir}") + + filesystem = PageIndexFileSystem( + workspace, + metadata_generator=OpenAIMetadataGenerator(model=args.metadata_model), + summary_projection_embedding_provider="openai", + summary_projection_embedding_model=args.embedding_model, + summary_projection_embedding_dimensions=args.embedding_dimensions, + ) + register_demo_metadata_schema(filesystem) + + print_section("STEP 1/3 Register Documents") + print(f"Workspace: {workspace}", flush=True) + print(f"Documents: {len(documents)}", flush=True) + registered = register_documents(filesystem, documents, documents_dir=documents_dir) + configure_summary_projection_backend( + filesystem, + embedding_model=args.embedding_model, + embedding_dimensions=args.embedding_dimensions, + ) + show_registered_documents(registered, verbose=args.verbose) + + print_section("STEP 2/3 Explore PIFS Tool Surface") + run_smoke_commands(filesystem, registered, verbose=args.verbose) + + if args.prepare_only: + return + + print_section("STEP 3/3 Ask An Agent Using Only PIFS") + print(f"Question: {args.question}", flush=True) + answer = run_pifs_agent( + filesystem, + args.question, + model=args.model, + root="/", + system_prompt=PIFS_DEMO_AGENT_PROMPT, + max_turns=args.max_turns, + max_seconds=args.max_seconds, + verbose=args.verbose, + stream_mode=args.stream_mode, + reasoning_effort=args.reasoning_effort, + reasoning_summary=args.reasoning_summary, + ) + if answer: + print("\nFinal answer:", flush=True) + print(answer, flush=True) + + +if __name__ == "__main__": + main() diff --git a/pageindex/__init__.py b/pageindex/__init__.py index 658003bf5..4f05b6136 100644 --- a/pageindex/__init__.py +++ b/pageindex/__init__.py @@ -1,4 +1,10 @@ -from .page_index import * -from .page_index_md import md_to_tree -from .retrieve import get_document, get_document_structure, get_page_content -from .client import PageIndexClient +try: + from .page_index import * + from .page_index_md import md_to_tree + from .retrieve import get_document, get_document_structure, get_page_content + from .client import PageIndexClient +except ModuleNotFoundError as exc: + if exc.name != "litellm": + raise + +from .filesystem import PageIndexFileSystem diff --git a/pageindex/filesystem/__init__.py b/pageindex/filesystem/__init__.py new file mode 100644 index 000000000..2ad1c8480 --- /dev/null +++ b/pageindex/filesystem/__init__.py @@ -0,0 +1,36 @@ +from .commands import PIFSCommandExecutor +from .core import PageIndexFileSystem +from .hybrid_projection import HybridProjectionSearchBackend +from .metadata_generation import ( + MetadataGenerationError, + MetadataGenerationInput, + MetadataGenerationResult, + MetadataGenerator, + OpenAIMetadataGenerator, +) +from .projection_indexing import SummaryProjectionIndexer +from .semantic_index import ( + RebuildableSemanticIndex, + SemanticIndexRecord, + SemanticSearchResult, + SQLiteVecSemanticIndex, +) +from .types import OpenResult, SearchResult + +__all__ = [ + "OpenResult", + "HybridProjectionSearchBackend", + "MetadataGenerationError", + "MetadataGenerationInput", + "MetadataGenerationResult", + "MetadataGenerator", + "OpenAIMetadataGenerator", + "PIFSCommandExecutor", + "PageIndexFileSystem", + "RebuildableSemanticIndex", + "SearchResult", + "SemanticIndexRecord", + "SemanticSearchResult", + "SummaryProjectionIndexer", + "SQLiteVecSemanticIndex", +] diff --git a/pageindex/filesystem/agent.py b/pageindex/filesystem/agent.py new file mode 100644 index 000000000..a090b66c2 --- /dev/null +++ b/pageindex/filesystem/agent.py @@ -0,0 +1,514 @@ +from __future__ import annotations + +import asyncio +import concurrent.futures +import json +import os +import re +import sys +import time +from dataclasses import asdict, is_dataclass +from typing import Any, Mapping, TextIO + +from .commands import PIFSCommandError, PIFSCommandExecutor +from .core import PageIndexFileSystem + + +TRUTHY_ENV_VALUES = {"1", "true", "yes", "on"} +PIFS_AGENT_TRACING_ENV = "PAGEINDEX_PIFS_AGENT_TRACING" +PIFS_AGENT_RAW_REASONING_ENV = "PAGEINDEX_PIFS_AGENT_RAW_REASONING" + +AGENT_SYSTEM_PROMPT = """ +You are a PageIndex FileSystem retrieval agent. + +You can inspect the corpus only by calling the bash tool. The bash tool is a +read-only PageIndex virtual shell, not a real operating-system shell. + +Follow the task prompt for command policy, retrieval strategy, and answer +format. If the caller needs stricter behavior, pass an explicit system_prompt. +""" + +BASH_TOOL_DESCRIPTION = """ +Run a command in the PageIndex FileSystem virtual shell. This is not a real +operating-system shell. By default the tool is read-only: use ls, tree, find, +grep, cat, stat, head, tail, sed, and any dynamically available semantic search +commands described in the workspace context. grep -R is lexical evidence search; +semantic search commands return candidate documents and do not guarantee literal +text matches. Errors are returned as text prefixed with ERROR. Do not call +commands that are not listed as available. When evidence is required, inspect it +with cat or grep before answering. +""" + +AGENT_TOOL_POLICY = """ +Tool policy: +- The bash tool is a PageIndex virtual shell, not an operating-system shell. +- The default agent tool surface is read-only. +- Use only commands listed in the workspace capabilities. +- grep -R performs lexical evidence search. +- Semantic search commands are candidate-discovery tools and do not guarantee literal text matches. +- Tool errors are returned as ERROR text; recover by trying an available command. +- Use cat or grep to gather evidence before making source-backed claims. +""" + +STREAM_MODE_ALIASES = { + "": "off", + "none": "off", + "false": "off", + "0": "off", + "off": "off", + "tool": "tools", + "tools": "tools", + "model": "model", + "output": "model", + "outputs": "model", + "think": "model", + "all": "all", + "debug": "all", +} +AGENT_STREAM_MODE_CHOICES = sorted(item for item in STREAM_MODE_ALIASES if item) +REASONING_EFFORT_CHOICES = ["none", "minimal", "low", "medium", "high", "xhigh"] +REASONING_SUMMARY_CHOICES = ["none", "auto", "concise", "detailed"] + + +def should_use_openai_compatible_chat_model(base_url: str | None) -> bool: + if not base_url: + return False + normalized = base_url.strip().rstrip("/") + return normalized not in {"https://api.openai.com", "https://api.openai.com/v1"} + + +def env_flag_enabled(name: str, environ: Mapping[str, str] | None = None) -> bool: + source = os.environ if environ is None else environ + value = source.get(name, "") + return value.strip().lower() in TRUTHY_ENV_VALUES + + +def pifs_agent_tracing_enabled(environ: Mapping[str, str] | None = None) -> bool: + return env_flag_enabled(PIFS_AGENT_TRACING_ENV, environ) + + +def should_disable_pifs_agent_tracing(environ: Mapping[str, str] | None = None) -> bool: + return not pifs_agent_tracing_enabled(environ) + + +def pifs_agent_raw_reasoning_enabled(environ: Mapping[str, str] | None = None) -> bool: + return env_flag_enabled(PIFS_AGENT_RAW_REASONING_ENV, environ) + + +def normalize_reasoning_effort(reasoning_effort: str | None) -> str | None: + if reasoning_effort is None or not reasoning_effort.strip(): + return None + effort = reasoning_effort.strip().lower() + if effort not in REASONING_EFFORT_CHOICES: + allowed = ", ".join(REASONING_EFFORT_CHOICES) + raise ValueError(f"Unknown reasoning effort: {reasoning_effort!r}. Allowed: {allowed}") + return effort + + +def normalize_reasoning_summary(reasoning_summary: str | None) -> str | None: + if reasoning_summary is None or not reasoning_summary.strip(): + return None + summary = reasoning_summary.strip().lower() + if summary not in REASONING_SUMMARY_CHOICES: + allowed = ", ".join(REASONING_SUMMARY_CHOICES) + raise ValueError(f"Unknown reasoning summary: {reasoning_summary!r}. Allowed: {allowed}") + return None if summary == "none" else summary + + +def build_agent_model_settings( + *, + reasoning_effort: str | None = None, + reasoning_summary: str | None = None, +) -> Any | None: + effort = normalize_reasoning_effort(reasoning_effort) + summary = normalize_reasoning_summary(reasoning_summary) + if effort is None and summary is None: + return None + if effort not in {None, "none"} and summary is None: + summary = "auto" + + from agents import ModelSettings + from openai.types.shared import Reasoning + + reasoning_kwargs = {} + if effort is not None: + reasoning_kwargs["effort"] = effort + if summary is not None: + reasoning_kwargs["summary"] = summary + return ModelSettings(reasoning=Reasoning(**reasoning_kwargs), verbosity="low") + + +def normalize_agent_stream_mode(stream_mode: str | None) -> str: + mode = STREAM_MODE_ALIASES.get((stream_mode or "off").strip().lower()) + if mode is None: + allowed = ", ".join(sorted({"off", "tools", "model", "all"})) + raise ValueError(f"Unknown PIFS agent stream mode: {stream_mode!r}. Allowed: {allowed}") + return mode + + +def serialize_agent_final_output(value: Any) -> str: + if value is None: + return "" + if isinstance(value, str): + return value + if hasattr(value, "model_dump_json"): + return value.model_dump_json() + if is_dataclass(value): + return json.dumps(asdict(value), ensure_ascii=False) + if isinstance(value, (dict, list)): + return json.dumps(value, ensure_ascii=False) + return str(value) + + +def compact_tool_output_preview( + output: str, + *, + preview_chars: int = 700, + max_lines: int = 8, +) -> str: + cleaned = str(output).replace("\r", "\n").replace("\f", "\n") + cleaned = "".join( + ch if ch == "\n" or ch == "\t" or ord(ch) >= 32 else " " + for ch in cleaned + ) + lines = [ + re.sub(r"[ \t]{2,}", " ", line).strip() + for line in cleaned.splitlines() + if line.strip() + ] + is_large_result = len(cleaned) > preview_chars or len(lines) > max_lines + preview = "\n".join(lines[:max_lines]) + if len(preview) > preview_chars: + preview = preview[:preview_chars].rstrip() + "..." + omitted = len(lines) - min(len(lines), max_lines) + if is_large_result: + preview = f"[large PIFS result: {len(cleaned)} chars; showing compact preview]\n" + preview + if omitted > 0: + preview += f"\n... [{omitted} more lines omitted from preview]" + if len(cleaned) > preview_chars: + preview += "\n... [full result returned to agent; terminal preview shortened]" + return preview + + +def build_agent_initial_context( + filesystem: PageIndexFileSystem, + *, + root: str = "/", + executor: PIFSCommandExecutor | None = None, + query_context: str | None = None, +) -> str: + executor = executor or PIFSCommandExecutor( + filesystem, + json_output=False, + query_context=query_context, + ) + schema = filesystem._metadata_schema() + schema_fields = schema.get("fields", {}) + schema_sample = dict(list(schema_fields.items())[:50]) + return "\n".join( + [ + f"Root path: {root}", + "Top-level listing:", + executor.execute(f"ls {root}"), + "Metadata schema summary:", + json.dumps( + { + "field_count": len(schema_fields), + "sample_fields": schema_sample, + }, + ensure_ascii=False, + ), + "Workspace retrieval capabilities:", + executor.describe_available_command_surfaces(), + ] + ) + + +def build_pifs_agent_instructions( + filesystem: PageIndexFileSystem, + *, + root: str = "/", + system_prompt: str | None = None, + executor: PIFSCommandExecutor | None = None, + query_context: str | None = None, +) -> str: + initial_context = build_agent_initial_context( + filesystem, + root=root, + executor=executor, + query_context=query_context, + ) + return "\n\n".join( + [ + (system_prompt or AGENT_SYSTEM_PROMPT).strip(), + AGENT_TOOL_POLICY.strip(), + "Workspace context:\n" + initial_context, + ] + ) + + +class PIFSAgentStreamObserver: + def __init__( + self, + stream_mode: str, + *, + stream_log: list[dict[str, Any]] | None = None, + output: TextIO | None = None, + include_raw_reasoning: bool | None = None, + ) -> None: + self.stream_mode = normalize_agent_stream_mode(stream_mode) + self.stream_log = stream_log + self.output = output or sys.stdout + self.include_raw_reasoning = ( + pifs_agent_raw_reasoning_enabled() + if include_raw_reasoning is None + else include_raw_reasoning + ) + self._printed_section: str | None = None + self._buffers: dict[str, list[str]] = { + "output": [], + "think": [], + "think_summary": [], + "tool_args": [], + } + + @property + def wants_model_stream(self) -> bool: + return self.stream_mode in {"model", "all"} + + @property + def wants_tool_stream(self) -> bool: + return self.stream_mode in {"tools", "all"} + + @property + def has_output_text(self) -> bool: + return bool(self._buffers["output"]) + + def handle_event(self, event: Any) -> None: + if getattr(event, "type", None) == "raw_response_event": + self._handle_raw_response_event(getattr(event, "data", None)) + elif getattr(event, "type", None) == "run_item_stream_event": + self._handle_run_item_event(event) + + def finish(self, final_output: Any = None) -> None: + if self.wants_model_stream and not self.has_output_text and final_output: + self._emit("output", str(final_output), "[llm final output stream]") + if self._printed_section is not None: + print(file=self.output, flush=True) + self._printed_section = None + if self.stream_log is not None: + for kind, parts in self._buffers.items(): + text = "".join(parts) + if text: + self.stream_log.append({"kind": kind, "text": text}) + + def _handle_raw_response_event(self, data: Any) -> None: + event_type = getattr(data, "type", "") + delta = getattr(data, "delta", None) + if not isinstance(delta, str) or not delta: + return + if event_type == "response.output_text.delta": + self._emit("output", delta, "[llm final output stream]") + elif event_type == "response.reasoning_text.delta": + if self.include_raw_reasoning: + self._emit("think", delta, "[llm reasoning text stream]") + elif event_type == "response.reasoning_summary_text.delta": + self._emit("think_summary", delta, "[llm reasoning summary stream]") + elif event_type == "response.function_call_arguments.delta": + self._buffers["tool_args"].append(delta) + + def _handle_run_item_event(self, event: Any) -> None: + name = getattr(event, "name", "") + item = getattr(event, "item", None) + item_type = getattr(item, "type", "") + if self.stream_log is not None and name in {"message_output_created", "reasoning_item_created"}: + self.stream_log.append({"kind": "run_item", "name": name, "item_type": item_type}) + + def _emit(self, kind: str, text: str, label: str) -> None: + if kind == "tool_args": + should_print = self.wants_tool_stream + else: + should_print = self.wants_model_stream + if not should_print: + return + self._buffers[kind].append(text) + if self._printed_section != kind: + if self._printed_section is not None: + print(file=self.output, flush=True) + print(f"\n{label}", file=self.output, flush=True) + self._printed_section = kind + print(text, end="", file=self.output, flush=True) + + def emit_tool_call(self, command: str, *, force: bool = False) -> None: + if self.stream_log is not None: + self.stream_log.append({"kind": "tool_call", "command": command}) + if not (force or self.wants_tool_stream): + return + self._start_section("tool_call", "[llm -> pifs command]") + print(command, file=self.output, flush=True) + + def emit_tool_result( + self, + *, + ok: bool, + output: str, + seconds: float, + force: bool = False, + preview_chars: int = 1000, + ) -> None: + if self.stream_log is not None: + self.stream_log.append( + { + "kind": "tool_result", + "ok": ok, + "seconds": round(seconds, 4), + "output_chars": len(output), + "preview": compact_tool_output_preview(output, preview_chars=preview_chars), + } + ) + if not (force or self.wants_tool_stream): + return + preview = compact_tool_output_preview(output, preview_chars=preview_chars) + self._start_section("tool_result", "[pifs -> llm result preview]") + print( + f"ok={str(ok).lower()} seconds={seconds:.4f} output_chars={len(output)}", + file=self.output, + flush=True, + ) + print(preview, file=self.output, flush=True) + + def _start_section(self, kind: str, label: str) -> None: + if self._printed_section is not None: + print(file=self.output, flush=True) + print(f"\n{label}", file=self.output, flush=True) + self._printed_section = kind + + +def run_pifs_agent( + filesystem: PageIndexFileSystem, + question: str, + *, + model: str, + root: str = "/", + system_prompt: str | None = None, + max_turns: int = 20, + max_seconds: float | None = 60, + verbose: bool = False, + stream_mode: str = "off", + reasoning_effort: str | None = None, + reasoning_summary: str | None = None, + output_type: type[Any] | None = None, + tool_log: list[dict[str, Any]] | None = None, + agent_log: list[dict[str, Any]] | None = None, +) -> str: + try: + from agents import Agent, OpenAIChatCompletionsModel, Runner, function_tool, set_tracing_disabled + from openai import AsyncOpenAI + except ModuleNotFoundError as exc: + if exc.name == "agents": + raise RuntimeError("openai-agents is required to run the PageIndex FileSystem agent") from exc + raise + + set_tracing_disabled(should_disable_pifs_agent_tracing()) + normalized_stream_mode = normalize_agent_stream_mode(stream_mode) + executor = PIFSCommandExecutor( + filesystem, + json_output=False, + query_context=extract_agent_question_text(question), + ) + observer = PIFSAgentStreamObserver(normalized_stream_mode, stream_log=agent_log) + instructions = build_pifs_agent_instructions( + filesystem, + root=root, + system_prompt=system_prompt, + executor=executor, + ) + + @function_tool(description_override=BASH_TOOL_DESCRIPTION.strip()) + def bash(command: str) -> str: + """Run an allowed PageIndex FileSystem virtual shell command.""" + started = time.time() + ok = True + observer.emit_tool_call(command, force=verbose) + try: + output = executor.execute(command) + except PIFSCommandError as exc: + ok = False + output = f"ERROR: {exc}" + seconds = time.time() - started + if tool_log is not None: + tool_log.append( + { + "command": command, + "ok": ok, + "seconds": round(seconds, 4), + "output_chars": len(output), + "preview": output[:500], + } + ) + observer.emit_tool_result(ok=ok, output=output, seconds=seconds, force=verbose) + return output + + model_settings = build_agent_model_settings( + reasoning_effort=reasoning_effort, + reasoning_summary=reasoning_summary, + ) + base_url = os.environ.get("OPENAI_BASE_URL") + model_config = model + if should_use_openai_compatible_chat_model(base_url): + model_config = OpenAIChatCompletionsModel( + model=model, + openai_client=AsyncOpenAI( + api_key=os.environ.get("OPENAI_API_KEY"), + base_url=base_url, + ), + ) + + agent_kwargs: dict[str, Any] = { + "name": "PageIndexFileSystem", + "instructions": instructions, + "tools": [bash], + "model": model_config, + } + if model_settings is not None: + agent_kwargs["model_settings"] = model_settings + if output_type is not None: + agent_kwargs["output_type"] = output_type + agent = Agent(**agent_kwargs) + + async def _run_streamed() -> str: + streamed_run = Runner.run_streamed(agent, question, max_turns=max_turns) + final_output = "" + try: + async for event in streamed_run.stream_events(): + observer.handle_event(event) + final_output = serialize_agent_final_output(streamed_run.final_output) + return final_output + finally: + if not final_output and streamed_run.final_output: + final_output = serialize_agent_final_output(streamed_run.final_output) + observer.finish(final_output) + + async def _run() -> str: + if max_seconds is None or max_seconds <= 0: + return await _run_streamed() + try: + return await asyncio.wait_for(_run_streamed(), timeout=max_seconds) + except asyncio.TimeoutError as exc: + raise TimeoutError(f"MaxSecondsExceeded: exceeded {max_seconds:g}s") from exc + + try: + asyncio.get_running_loop() + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: + return pool.submit(asyncio.run, _run()).result() + except RuntimeError: + return asyncio.run(_run()) + + +def extract_agent_question_text(prompt: str) -> str: + for line in str(prompt or "").splitlines(): + if line.startswith("Question:"): + value = line.split(":", 1)[1].strip() + if value: + return value + return str(prompt or "").strip() diff --git a/pageindex/filesystem/cli.py b/pageindex/filesystem/cli.py new file mode 100644 index 000000000..0cdf6329d --- /dev/null +++ b/pageindex/filesystem/cli.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import argparse +import os +import shlex +import sys +from pathlib import Path + +from .commands import PIFSCommandError, PIFSCommandExecutor +from .core import PageIndexFileSystem + + +def main(argv: list[str] | None = None) -> int: + argv = list(sys.argv[1:] if argv is None else argv) + parser = argparse.ArgumentParser(description="PageIndex FileSystem CLI") + parser.add_argument("--workspace", default=os.environ.get("PIFS_WORKSPACE")) + parser.add_argument("--json", action="store_true", dest="json_output") + parser.add_argument("command", nargs=argparse.REMAINDER) + args = parser.parse_args(argv) + + command_tokens = [token for token in args.command if token != "--"] + json_output = args.json_output + if "--json" in command_tokens: + command_tokens = [token for token in command_tokens if token != "--json"] + json_output = True + + if not args.workspace: + parser.error("--workspace is required unless PIFS_WORKSPACE is set") + if not command_tokens: + parser.error("a filesystem command is required") + + filesystem = PageIndexFileSystem(Path(args.workspace).expanduser()) + executor = PIFSCommandExecutor(filesystem, json_output=json_output) + try: + command = " ".join(shlex.quote(token) for token in command_tokens) + print(executor.execute(command)) + except PIFSCommandError as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 2 + except Exception as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pageindex/filesystem/commands.py b/pageindex/filesystem/commands.py new file mode 100644 index 000000000..a741333a9 --- /dev/null +++ b/pageindex/filesystem/commands.py @@ -0,0 +1,1639 @@ +from __future__ import annotations + +import json +import re +import shlex +import subprocess +from dataclasses import asdict, is_dataclass +from pathlib import Path +from typing import Any + +from .core import SEMANTIC_GREP_CHANNELS, SEMANTIC_RETRIEVAL_CHANNELS, PageIndexFileSystem + + +class PIFSCommandError(ValueError): + pass + + +class PIFSCommandExecutor: + FORBIDDEN_SUBSTRINGS = (";", "`", "$(", "||", "\n", "\r") + FORBIDDEN_TOKENS = {"|", ">", "<", ">>", "<<", "&"} + BASE_ALLOWED_COMMANDS = { + "ls", + "tree", + "find", + "grep", + "cat", + "stat", + "head", + "tail", + "sed", + } + SEMANTIC_CHANNEL_COMMANDS = { + "summary": "search-summary", + "entity": "search-entity", + "relation": "search-relation", + } + ALLOWED_COMMANDS = ( + BASE_ALLOWED_COMMANDS + | {"semantic-grep"} + | set(SEMANTIC_CHANNEL_COMMANDS.values()) + ) + ALLOWED_PIPE_FILTERS = {"head", "tail", "grep", "sed"} + COMMAND_METHODS = { + "search-summary": "_cmd_search_summary", + "search-entity": "_cmd_search_entity", + "search-relation": "_cmd_search_relation", + "semantic-grep": "_cmd_semantic_grep", + } + MAX_TREE_DEPTH = 4 + MAX_LS_RENDER_FILES = 25 + MAX_STAT_METADATA_FIELDS = 8 + SEMANTIC_GREP_VECTOR_CANDIDATE_LIMIT = 20 + GREP_RECURSIVE_FOLDER_DEPTH_LIMIT = 2 + GREP_RECURSIVE_FOLDER_FILE_LIMIT = 10 + + def __init__( + self, + filesystem: PageIndexFileSystem, + *, + json_output: bool = False, + query_context: str | None = None, + ): + self.filesystem = filesystem + self.json_output = json_output + self.query_context = query_context + + def allowed_commands(self) -> set[str]: + commands = set(self.BASE_ALLOWED_COMMANDS) + semantic_channels = set(self.filesystem.semantic_retrieval_channels()) + for channel in SEMANTIC_RETRIEVAL_CHANNELS: + if channel in semantic_channels: + commands.add(self.SEMANTIC_CHANNEL_COMMANDS[channel]) + if any(channel in semantic_channels for channel in SEMANTIC_GREP_CHANNELS): + commands.add("semantic-grep") + return commands + + def command_capabilities(self) -> dict[str, Any]: + return { + "allowed_commands": sorted(self.allowed_commands()), + "retrieval": self.filesystem.retrieval_capabilities(), + } + + def describe_available_command_surfaces(self) -> str: + capabilities = self.filesystem.retrieval_capabilities() + semantic = capabilities["semantic"] + semantic_channels = set(semantic["channels"]) + lines = [ + "Available command surfaces for this workspace:", + "- mode: read-only inspection", + "- ls/tree: folder browsing", + "- find --where: exact/canonical metadata DSL filtering", + "- grep -R: recursive lexical/FTS search only; semantic vector prefilter is disabled", + "- cat --structure/--node/--page: cached PageIndex reads for PDF/Markdown files", + "- cat --all: full text artifact reads for txt/text files", + ] + if "entity" in semantic_channels: + lines.append("- find --name: entity semantic candidate discovery alias") + if "relation" in semantic_channels: + lines.append("- find --relation: relation semantic candidate discovery alias") + for channel in SEMANTIC_RETRIEVAL_CHANNELS: + if channel not in semantic_channels: + continue + lines.append( + f"- {self.SEMANTIC_CHANNEL_COMMANDS[channel]}: " + f"{channel} semantic vector candidate discovery" + ) + semantic_grep_channels = semantic.get("semantic_grep_channels") or [] + if semantic_grep_channels: + lines.append( + "- semantic-grep -R: semantic candidates from " + + ", ".join(semantic_grep_channels) + + " indexes followed by real line matching" + ) + if not semantic.get("commands"): + lines.append("- semantic vector commands: none available in this workspace") + lines.append("- grep , cat, stat: evidence inspection") + return "\n".join(lines) + + def execute(self, command: str) -> str: + try: + if not command.strip(): + raise PIFSCommandError("Empty command") + commands = self._split_chained_commands(command) + if len(commands) > 1: + return "\n".join(self._execute_pipeline(part) for part in commands) + return self._execute_pipeline(commands[0]) + except PIFSCommandError: + raise + except (KeyError, ValueError) as exc: + raise PIFSCommandError(self._clean_error_message(exc)) from exc + + def _execute_pipeline(self, command: str) -> str: + commands = self._split_piped_commands(command) + output = self._execute_single(commands[0]) + for pipe_command in commands[1:]: + output = self._execute_pipe_filter(output, pipe_command) + return output + + def _execute_single(self, command: str) -> str: + self._validate_raw_command(command) + try: + tokens = shlex.split(command) + except ValueError as exc: + raise PIFSCommandError(f"Invalid command syntax: {exc}") from exc + if not tokens: + raise PIFSCommandError("Empty command") + self._validate_tokens(tokens) + if "--json" in tokens: + tokens = [token for token in tokens if token != "--json"] + json_output = True + else: + json_output = self.json_output + name = tokens[0] + if name not in self.allowed_commands(): + raise PIFSCommandError(f"Unsupported command: {name}") + method_name = self.COMMAND_METHODS.get(name, f"_cmd_{name}") + data = getattr(self, method_name)(tokens[1:]) + return self._render(data, json_output=json_output, command_name=name) + + def _execute_pipe_filter(self, input_text: str, command: str) -> str: + self._validate_raw_command(command) + try: + tokens = shlex.split(command) + except ValueError as exc: + raise PIFSCommandError(f"Invalid command syntax: {exc}") from exc + if not tokens: + raise PIFSCommandError("Empty pipe command") + self._validate_tokens(tokens) + name = tokens[0] + if name not in self.ALLOWED_PIPE_FILTERS: + raise PIFSCommandError(f"Unsupported pipe command: {name}") + if name == "head": + return self._pipe_head_tail(input_text, tokens[1:], from_tail=False) + if name == "tail": + return self._pipe_head_tail(input_text, tokens[1:], from_tail=True) + if name == "grep": + return self._pipe_grep(input_text, tokens[1:]) + if name == "sed": + return self._pipe_sed(input_text, tokens[1:]) + raise PIFSCommandError(f"Unsupported pipe command: {name}") + + def _cmd_ls(self, args: list[str]) -> Any: + recursive = False + limit = 100 + path = "/" + i = 0 + while i < len(args): + arg = args[i] + if arg in {"-R", "-r", "--recursive"}: + recursive = True + elif arg == "--limit": + i += 1 + limit = int(args[i]) + elif arg.startswith("-"): + raise PIFSCommandError(f"Unsupported ls option: {arg}") + else: + path = arg + i += 1 + return self.filesystem.browse(path, recursive=recursive, limit=limit) + + def _cmd_tree(self, args: list[str]) -> Any: + path = "/" + limit = 1000 + depth = 2 + i = 0 + while i < len(args): + arg = args[i] + if arg == "--limit": + i += 1 + limit = int(args[i]) + elif arg in {"--depth", "-L"}: + i += 1 + depth = int(args[i]) + elif arg.startswith("-"): + raise PIFSCommandError(f"Unsupported tree option: {arg}") + else: + path = arg + i += 1 + if depth < 1: + raise PIFSCommandError("tree --depth must be at least 1") + if depth > self.MAX_TREE_DEPTH: + depth = self.MAX_TREE_DEPTH + listing = self.filesystem.browse(path, recursive=True, limit=limit) + return {"path": path, "depth": depth, "limit": limit, **listing} + + def _cmd_find(self, args: list[str]) -> Any: + path = "/" + where = None + name = None + relation = None + limit = 10 + file_type = None + i = 0 + while i < len(args): + arg = args[i] + if arg == "--where": + i += 1 + where = args[i] + elif arg == "--name": + i += 1 + name = args[i] + elif arg == "--relation": + i += 1 + relation = args[i] + elif arg == "--limit": + i += 1 + limit = int(args[i]) + elif arg == "-type": + i += 1 + file_type = args[i] + elif arg.startswith("-"): + raise PIFSCommandError(f"Unsupported find option: {arg}") + else: + path = arg + i += 1 + if file_type and file_type not in {"f", "d"}: + raise PIFSCommandError("find -type supports only f or d") + if name and relation: + raise PIFSCommandError("find supports only one of --name or --relation") + if file_type == "d": + if where: + return self.filesystem.find_folders(path, metadata_filter=where, limit=limit) + return self.filesystem.browse(path, recursive=True, limit=limit)["folders"] + if relation: + if not self.filesystem.has_semantic_channel("relation"): + raise PIFSCommandError( + "find --relation requires a relation semantic index in this workspace" + ) + return self.filesystem.search_semantic_channel( + "relation", + self._semantic_retrieval_query(relation), + scope={"folder_path": path, "recursive": True}, + metadata_filter=where, + limit=limit, + ) + if name and self.filesystem.has_semantic_channel("entity"): + return self.filesystem.search_semantic_channel( + "entity", + self._semantic_retrieval_query(name), + scope={"folder_path": path, "recursive": True}, + metadata_filter=where, + limit=limit, + ) + return self.filesystem.search( + query=name, + scope={"folder_path": path, "recursive": True}, + metadata_filter=where, + limit=limit, + semantic=False, + ) + + def _cmd_grep(self, args: list[str]) -> Any: + recursive = False + where = None + limit = 10 + positionals = [] + i = 0 + while i < len(args): + arg = args[i] + if arg in {"-R", "-r", "--recursive"}: + recursive = True + elif self._is_combined_grep_flag(arg): + recursive = recursive or "R" in arg or "r" in arg + elif arg in {"-n", "--line-number", "-i", "--ignore-case"}: + pass + elif arg == "--where": + i += 1 + where = args[i] + elif arg == "--limit": + i += 1 + limit = int(args[i]) + elif arg.startswith("-"): + raise PIFSCommandError(f"Unsupported grep option: {arg}") + else: + positionals.append(arg) + i += 1 + if not positionals: + raise PIFSCommandError("grep requires a query") + query = positionals[0] + path = positionals[1] if len(positionals) > 1 else "/" + if self._is_folder(path): + normalized = self._normalize_folder_path(path) + if recursive: + limit_notice = self._recursive_grep_limit_notice(normalized, query) + if limit_notice: + return limit_notice + children = self.filesystem.browse(normalized, recursive=False, limit=1000)["folders"] + if children: + direct_results = self.filesystem.search( + query=query, + scope={"folder_path": normalized, "recursive": False}, + metadata_filter=where, + limit=limit, + semantic=False, + ) + if direct_results: + return { + "mode": "files", + "query": query, + "scope": normalized, + "data": self._grep_file_hits_from_results(direct_results, query), + } + if where is None: + direct_source_hits = self._grep_source_file_hits( + normalized, + query, + limit=limit, + direct_only=True, + ) + if direct_source_hits: + return { + "mode": "files", + "query": query, + "scope": normalized, + "data": direct_source_hits, + } + ranked = self._rank_child_folders( + query=query, + children=children, + metadata_filter=where, + limit=limit, + ) + if not ranked and where is None: + ranked = self._rank_child_folders_from_source( + query=query, + parent_path=normalized, + children=children, + limit=limit, + ) + return { + "mode": "folders", + "query": query, + "scope": normalized, + "data": ranked, + "hint": "narrow into one directory, then run grep -R again", + } + results = self.filesystem.search( + query=query, + scope={"folder_path": normalized, "recursive": recursive}, + metadata_filter=where, + limit=limit, + semantic=False, + ) + if not results and where is None: + source_hits = self._grep_source_file_hits(normalized, query, limit=limit) + return { + "mode": "files", + "query": query, + "scope": normalized, + "data": source_hits, + } + return { + "mode": "files", + "query": query, + "scope": normalized, + "data": self._grep_file_hits_from_results(results, query), + } + return { + "mode": "matches", + "query": query, + "target": path, + "data": self._grep_file_matches(path, query, limit=limit), + } + + def _cmd_cat(self, args: list[str]) -> Any: + if not args: + raise PIFSCommandError("cat requires a file target") + target = None + location = "all" + structural_mode: str | None = None + node_id: str | None = None + page_range: str | None = None + i = 0 + while i < len(args): + arg = args[i] + if arg == "--range": + i += 1 + if i >= len(args): + raise PIFSCommandError("cat --range requires a range") + location = args[i] + elif arg == "--all": + location = "all" + elif arg == "--structure": + structural_mode = "structure" + elif arg == "--node": + i += 1 + if i >= len(args): + raise PIFSCommandError("cat --node requires a node id") + structural_mode = "node" + node_id = args[i] + elif arg == "--page": + i += 1 + if i >= len(args): + raise PIFSCommandError("cat --page requires a page range") + structural_mode = "page" + page_range = args[i] + elif arg.startswith("-"): + raise PIFSCommandError(f"Unsupported cat option: {arg}") + else: + target = arg + i += 1 + if not target: + raise PIFSCommandError("cat requires a file target") + if structural_mode == "structure": + return self.filesystem.pageindex_structure(target) + if structural_mode == "node": + return self.filesystem.pageindex_node(target, str(node_id)) + if structural_mode == "page": + return self.filesystem.pageindex_pages(target, str(page_range)) + return self.filesystem.cat_text_artifact(target, location) + + def _cmd_stat(self, args: list[str]) -> Any: + if args and args[0] == "--schema": + return self.filesystem._metadata_schema() + if not args: + raise PIFSCommandError("stat requires a file target or --schema") + return {"target": args[0], **self.filesystem._stat(args[0])} + + def _cmd_head(self, args: list[str]) -> Any: + count, target = self._parse_standalone_head_tail(args, default_count=10) + opened = self.filesystem.cat_text_artifact(target, "all") + lines = opened.text.splitlines() + text = "\n".join(lines[:count]) + return {**self._jsonable(opened), "text": text, "end_line": min(count, len(lines))} + + def _cmd_tail(self, args: list[str]) -> Any: + count, target = self._parse_standalone_head_tail(args, default_count=10) + opened = self.filesystem.cat_text_artifact(target, "all") + lines = opened.text.splitlines() + selected = lines[-count:] if count else [] + start_line = max(1, len(lines) - len(selected) + 1) + return { + **self._jsonable(opened), + "text": "\n".join(selected), + "start_line": start_line, + "end_line": len(lines), + } + + def _cmd_sed(self, args: list[str]) -> Any: + if len(args) < 3 or args[0] != "-n": + raise PIFSCommandError("sed supports only: sed -n ',p' ") + match = re.fullmatch(r"(\d+),(\d+)p", args[1]) + if not match: + raise PIFSCommandError("sed supports only: sed -n ',p' ") + return self.filesystem.cat_text_artifact( + args[2], + f"{match.group(1)}-{match.group(2)}", + ) + + def _cmd_search_summary(self, args: list[str]) -> Any: + return self._cmd_semantic_channel("summary", args) + + def _cmd_search_entity(self, args: list[str]) -> Any: + return self._cmd_semantic_channel("entity", args) + + def _cmd_search_relation(self, args: list[str]) -> Any: + return self._cmd_semantic_channel("relation", args) + + def _cmd_semantic_grep(self, args: list[str]) -> Any: + recursive = False + where = None + limit = 10 + positionals = [] + i = 0 + while i < len(args): + arg = args[i] + if arg in {"-R", "-r", "--recursive"}: + recursive = True + elif self._is_combined_grep_flag(arg): + recursive = recursive or "R" in arg or "r" in arg + elif arg in {"-n", "--line-number", "-i", "--ignore-case"}: + pass + elif arg == "--where": + i += 1 + where = args[i] + elif arg == "--limit": + i += 1 + limit = int(args[i]) + elif arg.startswith("-"): + raise PIFSCommandError(f"Unsupported semantic-grep option: {arg}") + else: + positionals.append(arg) + i += 1 + if not recursive: + raise PIFSCommandError("semantic-grep requires -R/--recursive") + channels = self._semantic_grep_channels() + if not channels: + raise PIFSCommandError( + "semantic-grep is not available; entity/relation semantic indexes are not configured" + ) + if not positionals: + raise PIFSCommandError("semantic-grep requires a query") + query = positionals[0] + path = positionals[1] if len(positionals) > 1 else "/" + if not self._is_folder(path): + raise PIFSCommandError("semantic-grep target must be a folder") + return self._semantic_recursive_grep( + self._normalize_folder_path(path), + query, + metadata_filter=where, + limit=limit, + channels=channels, + ) + + def _cmd_semantic_channel(self, channel: str, args: list[str]) -> Any: + if not self.filesystem.has_semantic_channel(channel): + raise PIFSCommandError( + f"search-{channel} is not available; {channel} semantic index is not configured" + ) + where = None + limit = 10 + positionals = [] + i = 0 + while i < len(args): + arg = args[i] + if arg == "--where": + i += 1 + where = args[i] + elif arg == "--limit": + i += 1 + limit = int(args[i]) + elif arg.startswith("-"): + raise PIFSCommandError(f"Unsupported search-{channel} option: {arg}") + else: + positionals.append(arg) + i += 1 + if not positionals: + raise PIFSCommandError(f"search-{channel} requires a query") + query = positionals[0] + path = positionals[1] if len(positionals) > 1 else "/" + normalized = self._normalize_folder_path(path) + results = self.filesystem.search_semantic_channel( + channel, + self._semantic_retrieval_query(query), + scope={"folder_path": normalized, "recursive": True}, + metadata_filter=where, + limit=limit, + ) + return { + "mode": "files", + "query": query, + "scope": normalized, + "retrieval": f"{channel}_vector", + "data": self._grep_file_hits_from_results(results, query), + } + + def _semantic_recursive_grep( + self, + folder_path: str, + query: str, + *, + metadata_filter: str | None, + limit: int, + channels: tuple[str, ...], + ) -> dict[str, Any]: + vector_query = str(query or "").strip() + candidate_debug: dict[str, Any] = {} + for channel in channels: + channel_results = self.filesystem.search_semantic_channel( + channel, + vector_query, + scope={"folder_path": folder_path, "recursive": True}, + metadata_filter=metadata_filter, + limit=self.SEMANTIC_GREP_VECTOR_CANDIDATE_LIMIT, + ) + matches = self._grep_file_hits_from_results( + channel_results, + query, + require_match=True, + limit=limit, + ) + candidate_debug[channel] = { + "candidates": len(channel_results), + "line_matches": len(matches), + "candidate_doc_ids": [ + getattr(result, "external_id", None) + for result in channel_results[:5] + ], + } + if matches: + return { + "mode": "files", + "query": query, + "scope": folder_path, + "retrieval": "semantic_grep_" + "_then_".join(channels), + "candidate_limit_per_channel": self.SEMANTIC_GREP_VECTOR_CANDIDATE_LIMIT, + "matched_channel": channel, + "candidate_debug": candidate_debug, + "data": matches, + } + return { + "mode": "files", + "query": query, + "scope": folder_path, + "retrieval": "semantic_grep_" + "_then_".join(channels), + "candidate_limit_per_channel": self.SEMANTIC_GREP_VECTOR_CANDIDATE_LIMIT, + "matched_channel": "", + "candidate_debug": candidate_debug, + "data": [], + } + + def _semantic_grep_channels(self) -> tuple[str, ...]: + available = set(self.filesystem.semantic_retrieval_channels()) + return tuple(channel for channel in SEMANTIC_GREP_CHANNELS if channel in available) + + def _render(self, data: Any, *, json_output: bool, command_name: str) -> str: + jsonable = self._jsonable(data) + if json_output: + return json.dumps({"ok": True, "data": jsonable}, ensure_ascii=False) + return self._render_shell(command_name, jsonable) + + def _render_shell(self, command_name: str, data: Any) -> str: + if command_name == "cat": + return self._render_cat(data) + if command_name == "ls": + return self._render_listing(data) + if command_name == "tree": + return self._render_tree(data) + if command_name in {"grep", "semantic-grep"}: + return self._render_grep(data) + if command_name in {"search-summary", "search-entity", "search-relation"}: + return self._render_grep(data) + if command_name == "find": + return self._render_find(data) + if command_name == "stat": + return self._render_stat(data) + if command_name in {"head", "tail", "sed"}: + return str(data.get("text", "")) if isinstance(data, dict) else str(data) + if isinstance(data, dict): + return "\n".join(f"{key}: {value}" for key, value in data.items()) + if isinstance(data, list): + return "\n".join(str(item) for item in data) + return str(data) + + def _render_cat(self, data: Any) -> str: + if not isinstance(data, dict): + return str(data) + if data.get("available") is False: + return f"# {data.get('message', 'PageIndex structural content is unavailable')}" + if data.get("mode") == "structure": + return json.dumps(data.get("structure", {}), ensure_ascii=False, indent=2) + return str(data.get("text", "")) + + def _render_listing(self, data: Any) -> str: + if not isinstance(data, dict): + return str(data) + lines: list[str] = [] + for folder in data.get("folders", []): + name = folder["path"] if folder.get("path", "").startswith("/") else folder["name"] + if not name.endswith("/"): + name = f"{name}/" + lines.append( + f"{name} folders={folder.get('children_count', 0)} files={folder.get('file_count', 0)}" + ) + files = data.get("files", []) + for file in files[: self.MAX_LS_RENDER_FILES]: + lines.append(self._file_row_text(file)) + if len(files) > self.MAX_LS_RENDER_FILES: + remaining = len(files) - self.MAX_LS_RENDER_FILES + lines.append( + f"# ... {remaining} more files omitted from ls output; use grep/find to search this folder" + ) + return "\n".join(lines) + + def _render_tree(self, data: Any) -> str: + if not isinstance(data, dict): + return str(data) + root = self._normalize_folder_path(data.get("path", "/")) + max_depth = int(data.get("depth", 2)) + lines = [root] + folders = [ + folder + for folder in data.get("folders", []) + if self._relative_depth(root, folder["path"]) <= max_depth + ] + for folder in folders: + depth = self._relative_depth(root, folder["path"]) + indent = " " * max(depth - 1, 0) + lines.append( + f"{indent}{folder['name']}/ folders={folder.get('children_count', 0)} " + f"files={folder.get('file_count', 0)}" + ) + if len(folders) < len(data.get("folders", [])): + lines.append(f"# truncated at depth={max_depth}") + return "\n".join(lines) + + def _render_grep(self, data: Any) -> str: + if not isinstance(data, dict): + return str(data) + mode = data.get("mode") + if mode == "folders": + lines = [f"# folder matches for: {data.get('query', '')}"] + for folder in data.get("data", []): + path = folder["path"] + if not path.endswith("/"): + path = f"{path}/" + lines.append( + f"{path} matched_files={folder.get('matched_files', 0)} " + f"files={folder.get('files', 0)}" + ) + lines.append(f"# {data.get('hint', 'narrow into one directory, then run grep -R again')}") + return "\n".join(lines) + if mode == "limited": + query = str(data.get("query") or "") + scope = str(data.get("scope") or "/") + suggested_commands = list(data.get("suggested_commands") or []) + lines = [ + f"# grep -R skipped for broad folder: {scope}", + ( + "# reason: recursive lexical grep is limited when a folder is deeper " + f"than {data.get('folder_depth_limit', self.GREP_RECURSIVE_FOLDER_DEPTH_LIMIT)} " + f"levels or has more than {data.get('file_count_limit', self.GREP_RECURSIVE_FOLDER_FILE_LIMIT)} files" + ), + ] + if suggested_commands: + lines.extend(f"# suggested: {command}" for command in suggested_commands) + lines.append("# also try: narrow with ls/tree/find --where") + else: + lines.append("# suggested: narrow with ls/tree/find --where") + if data.get("sample_deep_folder_path"): + lines.append(f"# deep descendant example: {data['sample_deep_folder_path']}/") + return "\n".join(lines) + if mode == "files": + if not data.get("data", []): + return f"# no matches for: {data.get('query', '')}" + return "\n".join( + self._grep_file_hit_text(item) + for item in data.get("data", []) + ) + if mode == "matches": + return "\n".join( + f"{item['reference_id']}:{item['line']}: " + f"{self._compact_text(item['text'], max_chars=220)}" + for item in data.get("data", []) + ) + return str(data) + + def _render_find(self, data: Any) -> str: + if not isinstance(data, list): + return str(data) + if data and isinstance(data[0], dict) and "path" in data[0] and "file_ref" not in data[0]: + return "\n".join( + ( + f"{item['path']}/ matched_files={item['matched_files']} " + f"files={item.get('file_count', 0)}" + if item.get("matched_files") + else f"{item['path']}/ folders={item.get('children_count', 0)} " + f"files={item.get('file_count', 0)}" + ) + for item in data + ) + return "\n".join(self._file_row_text(item) for item in data) + + def _render_stat(self, data: Any) -> str: + if not isinstance(data, dict): + return str(data) + if "fields" in data: + lines = ["metadata schema:"] + for name, field in sorted(data["fields"].items()): + lines.append(f"{name}: {field.get('type', 'string')}") + return "\n".join(lines) + lines = [ + f"ref: {data.get('target') or data.get('file_ref')}", + f"file_ref: {data.get('file_ref')}", + f"document_id: {data.get('external_id') or data.get('document_id') or '-'}", + f"source_path: {data.get('source_path') or '-'}", + f"storage_uri: {data.get('storage_uri') or '-'}", + ] + folders = data.get("folders") or [] + if folders: + lines.append("folders:") + lines.extend(f" {folder['path']}" for folder in folders) + metadata = data.get("metadata") or {} + if metadata: + lines.append("metadata:") + metadata_items = sorted(metadata.items())[: self.MAX_STAT_METADATA_FIELDS] + for key, value in metadata_items: + lines.append(f" {key}: {self._compact_value(value)}") + if len(metadata) > self.MAX_STAT_METADATA_FIELDS: + lines.append(f" ... {len(metadata) - self.MAX_STAT_METADATA_FIELDS} more fields") + return "\n".join(lines) + + def _file_row_text(self, item: dict[str, Any]) -> str: + file_ref = item.get("file_ref") + ref = item.get("reference_id") or (self.filesystem._reference_for(file_ref) if file_ref else "-") + doc_id = item.get("external_id") or item.get("document_id") or "-" + title = self._compact_text(item.get("title") or item.get("name") or "", max_chars=80) + source_path = item.get("source_path") or "-" + folder_paths = item.get("folder_paths") or self._folder_paths_for_file(file_ref) + folders = f" folders={','.join(folder_paths)}" if folder_paths else "" + return f"{ref} {doc_id} {title} {source_path}{folders}".strip() + + def _grep_file_hit_text(self, item: dict[str, Any]) -> str: + doc_id = item.get("external_id") or "-" + source_path = item.get("source_path") or "-" + line = item.get("line") or 1 + return ( + f"{item['reference_id']} {doc_id} {source_path}:{line}: " + f"{self._compact_text(item.get('text') or '', max_chars=180)}" + ) + + def _semantic_retrieval_query(self, query: str) -> str: + query = str(query or "").strip() + context = str(self.query_context or "").strip() + if context and query and query.lower() not in context.lower(): + return f"{context}\nSearch phrase: {query}" + return context or query + + def _recursive_grep_limit_notice(self, folder_path: str, query: str) -> dict[str, Any] | None: + stats = self.filesystem.store.folder_subtree_thresholds( + folder_path, + depth_limit=self.GREP_RECURSIVE_FOLDER_DEPTH_LIMIT, + file_limit=self.GREP_RECURSIVE_FOLDER_FILE_LIMIT, + ) + if not ( + stats["folder_depth_exceeds_limit"] + or stats["file_count_exceeds_limit"] + ): + return None + suggested_commands = self._semantic_alternative_commands(query, folder_path) + semantic_hint = ( + "Use " + "; ".join(suggested_commands) + " to discover candidates. " + if suggested_commands + else "" + ) + return { + "mode": "limited", + "query": query, + "scope": folder_path, + "folder_depth_limit": stats["depth_limit"], + "file_count_limit": stats["file_limit"], + "folder_depth_exceeds_limit": stats["folder_depth_exceeds_limit"], + "file_count_exceeds_limit": stats["file_count_exceeds_limit"], + "sampled_file_count": stats["sampled_file_count"], + "sample_deep_folder_path": stats["sample_deep_folder_path"], + "suggested_commands": suggested_commands, + "hint": ( + "Default grep -R remains lexical and is intentionally limited for broad deep folders " + "because the SQLite FTS path cannot guarantee fast recursive search at this scope. " + f"{semantic_hint}Use ls/tree or find --where to narrow first." + ), + } + + def _semantic_alternative_commands(self, query: str, folder_path: str) -> list[str]: + commands = [] + quoted_query = shlex.quote(query) + quoted_folder = shlex.quote(folder_path) + if self._semantic_grep_channels(): + commands.append(f"semantic-grep -R {quoted_query} {quoted_folder}") + for channel in SEMANTIC_RETRIEVAL_CHANNELS: + if self.filesystem.has_semantic_channel(channel): + command = self.SEMANTIC_CHANNEL_COMMANDS[channel] + commands.append(f"{command} {quoted_query} {quoted_folder}") + return commands + + def _rank_child_folders( + self, + *, + query: str, + children: list[dict[str, Any]], + metadata_filter: str | None, + limit: int, + ) -> list[dict[str, Any]]: + ranked: list[dict[str, Any]] = [] + for child in children: + results = self.filesystem.search( + query=query, + scope={"folder_path": child["path"], "recursive": True}, + metadata_filter=metadata_filter, + limit=max(limit, 50), + semantic=False, + ) + if not results: + continue + ranked.append( + { + "path": child["path"], + "name": child["name"], + "matched_files": len(results), + "files": self.filesystem.store.count_files_in_folder(child["path"], recursive=True), + "children_count": child.get("children_count", 0), + } + ) + ranked.sort(key=lambda item: (-item["matched_files"], item["path"])) + return ranked[:limit] + + def _grep_file_hits_from_results( + self, + results: list[Any], + query: str, + *, + require_match: bool = False, + limit: int | None = None, + ) -> list[dict[str, Any]]: + hits = [] + for result in results: + line, text = self._first_matching_line(result.file_ref, query) + if require_match and not text: + continue + hits.append( + { + "reference_id": result.reference_id, + "file_ref": result.file_ref, + "external_id": result.external_id, + "title": result.title, + "source_path": result.source_path, + "folder_paths": result.folder_paths, + "line": line, + "text": text or result.snippet, + } + ) + if limit is not None and len(hits) >= limit: + break + return hits + + def _rank_child_folders_from_source( + self, + *, + query: str, + parent_path: str, + children: list[dict[str, Any]], + limit: int, + ) -> list[dict[str, Any]]: + source_dir = self._source_dir_for_folder(parent_path) + source_root = self._source_root() + if source_dir is None or source_root is None: + return [] + child_paths = {child["path"]: child for child in children} + counts: dict[str, int] = {} + for path in self._rg_candidate_files(query, source_dir, max_files=5000): + source_path = self._source_path_from_storage(path, source_root) + folder_path = "/" + str(Path(source_path).parent).strip("/") + child_path = self._matching_child_path(parent_path, folder_path, child_paths) + if child_path: + counts[child_path] = counts.get(child_path, 0) + 1 + ranked = [ + { + "path": path, + "name": child_paths[path]["name"], + "matched_files": matched, + "files": self.filesystem.store.count_files_in_folder(path, recursive=True), + "children_count": child_paths[path].get("children_count", 0), + } + for path, matched in counts.items() + ] + ranked.sort(key=lambda item: (-item["matched_files"], item["path"])) + return ranked[:limit] + + def _grep_source_file_hits( + self, + folder_path: str, + query: str, + *, + limit: int, + direct_only: bool = False, + ) -> list[dict[str, Any]]: + source_dir = self._source_dir_for_folder(folder_path) + source_root = self._source_root() + if source_dir is None or source_root is None: + return [] + hits = [] + for path in self._rg_candidate_files(query, source_dir, max_files=max(limit * 10, 50)): + file_row = self._file_row_for_storage(path) + if not file_row: + continue + if direct_only and self._folder_path_for_source_path(file_row["source_path"]) != folder_path: + continue + reference_id = self.filesystem._reference_for(file_row["file_ref"]) + line_number, text = self._first_matching_source_line(path, query) + hits.append( + { + "reference_id": reference_id, + "file_ref": file_row["file_ref"], + "external_id": file_row["external_id"], + "title": file_row["title"], + "source_path": file_row["source_path"], + "folder_paths": self._folder_paths_for_file(file_row["file_ref"]), + "line": line_number, + "text": text or file_row["title"], + } + ) + if len(hits) >= limit: + break + return hits + + def _grep_file_matches(self, target: str, query: str, *, limit: int) -> list[dict[str, Any]]: + file_ref = self.filesystem._resolve_reference(target) + reference_id = self.filesystem._reference_for(file_ref) + entry = self.filesystem.store.get_file(file_ref) + matches = [] + for line_number, line in enumerate(self.filesystem.store.read_text(file_ref).splitlines(), 1): + if self._line_matches(line, query): + matches.append( + { + "reference_id": reference_id, + "file_ref": file_ref, + "external_id": entry.external_id, + "source_path": entry.source_path, + "line": line_number, + "text": self._compact_text(line, max_chars=220), + } + ) + if len(matches) >= limit: + break + return matches + + def _first_matching_line(self, file_ref: str, query: str) -> tuple[int, str]: + for line_number, line in enumerate(self.filesystem.store.read_text(file_ref).splitlines(), 1): + if self._line_matches(line, query): + return line_number, self._compact_text(line, max_chars=220) + return 1, "" + + def _line_matches(self, line: str, query: str) -> bool: + haystack = line.lower() + needle = query.lower().strip() + if needle and needle in haystack: + return True + terms = [term for term in re.findall(r"[A-Za-z0-9_]+", needle) if term] + return bool(terms) and all(term in haystack for term in terms) + + @staticmethod + def _is_combined_grep_flag(arg: str) -> bool: + return bool(re.fullmatch(r"-[Rrni]+", arg)) and len(arg) > 2 + + def _rg_candidate_files(self, query: str, directory: Path, *, max_files: int) -> list[Path]: + if not directory.exists(): + return [] + terms = [term.lower() for term in re.findall(r"[A-Za-z0-9_]{3,}", query)] + if not terms: + return [] + primary = max(terms, key=len) + try: + completed = subprocess.run( + [ + "rg", + "-l", + "-i", + "-F", + primary, + str(directory), + "--glob", + "*.json", + "--no-messages", + ], + check=False, + capture_output=True, + text=True, + timeout=20, + ) + except (OSError, subprocess.TimeoutExpired): + return [] + candidates = [Path(line) for line in completed.stdout.splitlines() if line.strip()] + filtered = [] + for path in candidates[: max(max_files * 20, max_files)]: + try: + text = path.read_text(encoding="utf-8", errors="ignore").lower() + except OSError: + continue + if all(term in text for term in terms): + filtered.append(path) + if len(filtered) >= max_files: + break + return filtered + + def _first_matching_source_line(self, path: Path, query: str) -> tuple[int, str]: + try: + lines = path.read_text(encoding="utf-8", errors="ignore").splitlines() + except OSError: + return 1, "" + for line_number, line in enumerate(lines, 1): + if self._line_matches(line, query): + return line_number, self._compact_text(line, max_chars=220) + return 1, self._compact_text(lines[0], max_chars=220) if lines else "" + + def _source_root(self) -> Path | None: + with self.filesystem.store.connect() as conn: + row = conn.execute( + """ + SELECT storage_uri, source_path + FROM files + WHERE deleted_at IS NULL + LIMIT 1 + """ + ).fetchone() + if row is None: + return None + storage_path = Path(row["storage_uri"]) + source_path = Path(row["source_path"]) + root = storage_path + for _part in source_path.parts: + root = root.parent + return root + + def _source_dir_for_folder(self, folder_path: str) -> Path | None: + source_root = self._source_root() + if source_root is None: + return None + stripped = folder_path.strip("/") + return source_root / stripped if stripped else source_root + + @staticmethod + def _source_path_from_storage(path: Path, source_root: Path) -> str: + try: + return path.relative_to(source_root).as_posix() + except ValueError: + return path.name + + @staticmethod + def _matching_child_path( + parent_path: str, + folder_path: str, + child_paths: dict[str, dict[str, Any]], + ) -> str | None: + normalized_parent = parent_path.rstrip("/") + if normalized_parent == "": + normalized_parent = "/" + if normalized_parent == "/": + parts = [part for part in folder_path.strip("/").split("/") if part] + candidate = "/" + parts[0] if parts else "/" + return candidate if candidate in child_paths else None + prefix = normalized_parent + "/" + if not folder_path.startswith(prefix): + return None + remainder = folder_path[len(prefix):] + first = remainder.split("/", 1)[0] + candidate = prefix + first + return candidate if candidate in child_paths else None + + def _file_row_for_storage(self, path: Path) -> dict[str, Any] | None: + storage_uri = str(path) + with self.filesystem.store.connect() as conn: + row = conn.execute( + """ + SELECT file_ref, external_id, title, source_path + FROM files + WHERE storage_uri = ? AND deleted_at IS NULL + LIMIT 1 + """, + (storage_uri,), + ).fetchone() + if row is None: + return None + return { + "file_ref": row["file_ref"], + "external_id": row["external_id"], + "title": row["title"], + "source_path": row["source_path"], + } + + @staticmethod + def _folder_path_for_source_path(source_path: str) -> str: + parent = str(Path(source_path).parent).strip(".") + return "/" + parent.strip("/") if parent and parent != "." else "/" + + def _folder_paths_for_file(self, file_ref: str | None) -> list[str]: + if not file_ref: + return [] + try: + return [folder["path"] for folder in self.filesystem.store.folder_memberships(file_ref)] + except KeyError: + return [] + + def _is_folder(self, path: str) -> bool: + try: + self.filesystem.browse(path, recursive=False, limit=1) + return True + except KeyError: + return False + + @staticmethod + def _normalize_folder_path(path: str) -> str: + value = str(path or "/").strip() + if not value or value == "/": + return "/" + return "/" + value.strip("/") + + @classmethod + def _relative_depth(cls, root: str, path: str) -> int: + root = cls._normalize_folder_path(root).rstrip("/") + path = cls._normalize_folder_path(path).rstrip("/") + if root == "": + root = "/" + if root == "/": + rel = path.strip("/") + else: + rel = path[len(root):].strip("/") + return 0 if not rel else len(rel.split("/")) + + @classmethod + def _compact_value(cls, value: Any) -> str: + if isinstance(value, list): + rendered = ", ".join(cls._compact_text(str(item), max_chars=40) for item in value[:3]) + if len(value) > 3: + rendered += f", ... {len(value) - 3} more" + return rendered + if isinstance(value, dict): + return cls._compact_text(json.dumps(value, ensure_ascii=False, sort_keys=True), max_chars=120) + return cls._compact_text(str(value), max_chars=120) + + @staticmethod + def _compact_text(text: str, *, max_chars: int) -> str: + collapsed = re.sub(r"\s+", " ", text or "").strip() + if len(collapsed) <= max_chars: + return collapsed + return collapsed[: max_chars - 3].rstrip() + "..." + + @staticmethod + def _clean_error_message(exc: BaseException) -> str: + message = str(exc) + if isinstance(exc, KeyError) and len(exc.args) == 1: + message = str(exc.args[0]) + return message or exc.__class__.__name__ + + @classmethod + def _jsonable(cls, value: Any) -> Any: + if is_dataclass(value): + return asdict(value) + if isinstance(value, list): + return [cls._jsonable(item) for item in value] + if isinstance(value, dict): + return {key: cls._jsonable(item) for key, item in value.items()} + return value + + @classmethod + def _validate_raw_command(cls, command: str) -> None: + if any(token in command for token in cls.FORBIDDEN_SUBSTRINGS): + raise PIFSCommandError("Only PageIndex FileSystem commands are allowed") + + @classmethod + def _validate_tokens(cls, tokens: list[str]) -> None: + if any(token in cls.FORBIDDEN_TOKENS for token in tokens): + raise PIFSCommandError("Only PageIndex FileSystem commands are allowed") + + @classmethod + def _split_chained_commands(cls, command: str) -> list[str]: + return cls._split_unquoted_operator(command, "&&", reject_single_amp=True) + + @classmethod + def _split_piped_commands(cls, command: str) -> list[str]: + return cls._split_unquoted_operator(command, "|") + + @classmethod + def _split_unquoted_operator( + cls, + command: str, + operator: str, + *, + reject_single_amp: bool = False, + ) -> list[str]: + cls._validate_raw_command(command) + parts: list[str] = [] + current: list[str] = [] + quote: str | None = None + escaped = False + i = 0 + while i < len(command): + char = command[i] + if escaped: + current.append(char) + escaped = False + i += 1 + continue + if char == "\\" and quote != "'": + current.append(char) + escaped = True + i += 1 + continue + if quote: + current.append(char) + if char == quote: + quote = None + i += 1 + continue + if char in {"'", '"'}: + quote = char + current.append(char) + i += 1 + continue + if command.startswith(operator, i): + part = "".join(current).strip() + if not part: + raise PIFSCommandError("Invalid command syntax") + parts.append(part) + current = [] + i += len(operator) + continue + if reject_single_amp and char == "&": + raise PIFSCommandError("Only PageIndex FileSystem commands are allowed") + current.append(char) + i += 1 + part = "".join(current).strip() + if quote: + raise PIFSCommandError("Invalid command syntax: No closing quotation") + if not part: + raise PIFSCommandError("Invalid command syntax") + parts.append(part) + return parts + + def _pipe_head_tail(self, input_text: str, args: list[str], *, from_tail: bool) -> str: + count = self._parse_head_tail_count(args) + payload = self._try_json_loads(input_text) + if payload is not None: + return self._render_json_payload(self._slice_payload(payload, count, from_tail=from_tail)) + lines = input_text.splitlines() + selected = [] if count == 0 else lines[-count:] if from_tail else lines[:count] + return "\n".join(selected) + + def _pipe_grep(self, input_text: str, args: list[str]) -> str: + ignore_case = False + invert = False + regex = False + patterns: list[str] = [] + for arg in args: + if arg in {"-i", "--ignore-case"}: + ignore_case = True + elif arg in {"-v", "--invert-match"}: + invert = True + elif arg in {"-E", "--extended-regexp"}: + regex = True + elif arg.startswith("-"): + raise PIFSCommandError(f"Unsupported pipe grep option: {arg}") + else: + patterns.append(arg) + if len(patterns) != 1: + raise PIFSCommandError("pipe grep requires exactly one pattern") + pattern = patterns[0] + payload = self._try_json_loads(input_text) + if payload is not None: + return self._render_json_payload( + self._filter_payload( + payload, + pattern, + ignore_case=ignore_case, + invert=invert, + regex=regex, + ) + ) + filtered = [ + line + for line in input_text.splitlines() + if self._text_matches(line, pattern, ignore_case=ignore_case, invert=invert, regex=regex) + ] + return "\n".join(filtered) + + def _pipe_sed(self, input_text: str, args: list[str]) -> str: + if not args: + raise PIFSCommandError("pipe sed requires an expression") + if args[0] == "-n": + args = args[1:] + if len(args) != 1: + raise PIFSCommandError("pipe sed supports only -n ',p'") + match = re.fullmatch(r"(\d+)(?:,(\d+))?p", args[0]) + if not match: + raise PIFSCommandError("pipe sed supports only -n ',p'") + start = int(match.group(1)) + end = int(match.group(2) or match.group(1)) + if start < 1 or end < start: + raise PIFSCommandError("Invalid sed line range") + payload = self._try_json_loads(input_text) + if payload is not None: + return self._render_json_payload(self._slice_text_payload(payload, start, end)) + lines = input_text.splitlines() + return "\n".join(lines[start - 1 : end]) + + @staticmethod + def _parse_head_tail_count(args: list[str]) -> int: + count = 10 + i = 0 + while i < len(args): + arg = args[i] + if arg == "-n": + i += 1 + if i >= len(args): + raise PIFSCommandError("head/tail -n requires a count") + count = PIFSCommandExecutor._parse_non_negative_int(args[i], "head/tail count") + elif re.fullmatch(r"-\d+", arg): + count = PIFSCommandExecutor._parse_non_negative_int(arg[1:], "head/tail count") + elif arg.startswith("-"): + raise PIFSCommandError(f"Unsupported head/tail option: {arg}") + else: + count = PIFSCommandExecutor._parse_non_negative_int(arg, "head/tail count") + i += 1 + return count + + @staticmethod + def _parse_standalone_head_tail(args: list[str], *, default_count: int) -> tuple[int, str]: + count = default_count + target = "" + i = 0 + while i < len(args): + arg = args[i] + if arg == "-n": + i += 1 + if i >= len(args): + raise PIFSCommandError("head/tail -n requires a count") + count = PIFSCommandExecutor._parse_non_negative_int(args[i], "head/tail count") + elif re.fullmatch(r"-\d+", arg): + count = PIFSCommandExecutor._parse_non_negative_int(arg[1:], "head/tail count") + elif arg.startswith("-"): + raise PIFSCommandError(f"Unsupported head/tail option: {arg}") + else: + target = arg + i += 1 + if not target: + raise PIFSCommandError("head/tail requires a file target") + return count, target + + @staticmethod + def _parse_non_negative_int(value: str, label: str) -> int: + try: + parsed = int(value) + except ValueError as exc: + raise PIFSCommandError(f"{label} must be an integer") from exc + if parsed < 0: + raise PIFSCommandError(f"{label} must be non-negative") + return parsed + + @staticmethod + def _try_json_loads(input_text: str) -> Any | None: + try: + return json.loads(input_text) + except json.JSONDecodeError: + return None + + @staticmethod + def _render_json_payload(payload: Any) -> str: + return json.dumps(payload, ensure_ascii=False) + + @classmethod + def _slice_payload(cls, payload: Any, count: int, *, from_tail: bool) -> Any: + if isinstance(payload, list): + return payload[-count:] if from_tail and count else payload[:count] + if not isinstance(payload, dict): + return payload + sliced = dict(payload) + if "data" in sliced: + sliced["data"] = cls._slice_data(sliced["data"], count, from_tail=from_tail) + else: + sliced = cls._slice_mapping_lists(sliced, count, from_tail=from_tail) + return sliced + + @classmethod + def _slice_data(cls, data: Any, count: int, *, from_tail: bool) -> Any: + if isinstance(data, list): + return data[-count:] if from_tail and count else data[:count] + if isinstance(data, dict): + if isinstance(data.get("text"), str): + copied = dict(data) + lines = copied["text"].splitlines() + copied["text"] = "\n".join(lines[-count:] if from_tail and count else lines[:count]) + return copied + return cls._slice_mapping_lists(data, count, from_tail=from_tail) + return data + + @classmethod + def _slice_mapping_lists(cls, data: dict[str, Any], count: int, *, from_tail: bool) -> dict[str, Any]: + copied = dict(data) + for key, value in copied.items(): + if isinstance(value, list): + copied[key] = value[-count:] if from_tail and count else value[:count] + return copied + + @classmethod + def _filter_payload( + cls, + payload: Any, + pattern: str, + *, + ignore_case: bool, + invert: bool, + regex: bool, + ) -> Any: + if isinstance(payload, list): + return [ + item + for item in payload + if cls._json_matches(item, pattern, ignore_case=ignore_case, invert=invert, regex=regex) + ] + if not isinstance(payload, dict): + return payload + filtered = dict(payload) + if "data" in filtered: + filtered["data"] = cls._filter_data( + filtered["data"], + pattern, + ignore_case=ignore_case, + invert=invert, + regex=regex, + ) + else: + filtered = cls._filter_mapping_lists( + filtered, + pattern, + ignore_case=ignore_case, + invert=invert, + regex=regex, + ) + return filtered + + @classmethod + def _filter_data( + cls, + data: Any, + pattern: str, + *, + ignore_case: bool, + invert: bool, + regex: bool, + ) -> Any: + if isinstance(data, list): + return [ + item + for item in data + if cls._json_matches(item, pattern, ignore_case=ignore_case, invert=invert, regex=regex) + ] + if isinstance(data, dict): + return cls._filter_mapping_lists( + data, + pattern, + ignore_case=ignore_case, + invert=invert, + regex=regex, + ) + if isinstance(data, str): + return "\n".join( + line + for line in data.splitlines() + if cls._text_matches(line, pattern, ignore_case=ignore_case, invert=invert, regex=regex) + ) + return data + + @classmethod + def _filter_mapping_lists( + cls, + data: dict[str, Any], + pattern: str, + *, + ignore_case: bool, + invert: bool, + regex: bool, + ) -> dict[str, Any]: + filtered = dict(data) + for key, value in filtered.items(): + if isinstance(value, list): + filtered[key] = [ + item + for item in value + if cls._json_matches(item, pattern, ignore_case=ignore_case, invert=invert, regex=regex) + ] + return filtered + + @classmethod + def _json_matches( + cls, + value: Any, + pattern: str, + *, + ignore_case: bool, + invert: bool, + regex: bool, + ) -> bool: + text = json.dumps(value, ensure_ascii=False, sort_keys=True) + return cls._text_matches(text, pattern, ignore_case=ignore_case, invert=invert, regex=regex) + + @staticmethod + def _text_matches( + text: str, + pattern: str, + *, + ignore_case: bool, + invert: bool, + regex: bool, + ) -> bool: + flags = re.IGNORECASE if ignore_case else 0 + if regex: + try: + matched = re.search(pattern, text, flags) is not None + except re.error as exc: + raise PIFSCommandError(f"Invalid grep regex: {exc}") from exc + elif ignore_case: + matched = pattern.lower() in text.lower() + else: + matched = pattern in text + return not matched if invert else matched + + @classmethod + def _slice_text_payload(cls, payload: Any, start: int, end: int) -> Any: + if not isinstance(payload, dict): + return payload + sliced = dict(payload) + data = sliced.get("data") + if isinstance(data, dict) and isinstance(data.get("text"), str): + copied_data = dict(data) + lines = copied_data["text"].splitlines() + copied_data["text"] = "\n".join(lines[start - 1 : end]) + copied_data["start_line"] = start + copied_data["end_line"] = min(end, len(lines)) + sliced["data"] = copied_data + return sliced diff --git a/pageindex/filesystem/core.py b/pageindex/filesystem/core.py new file mode 100644 index 000000000..314a53279 --- /dev/null +++ b/pageindex/filesystem/core.py @@ -0,0 +1,1771 @@ +from __future__ import annotations + +import json +import os +from pathlib import Path +from typing import Any, Optional, Union +from urllib.parse import unquote, urlparse + +from ..client import PageIndexClient +from .metadata import MetadataQueryEngine +from .metadata_generation import ( + MetadataGenerationError, + MetadataGenerationInput, + MetadataGenerationResult, + MetadataGenerator, + OpenAIMetadataGenerator, +) +from .projection_indexing import SummaryProjectionIndexer +from .semantic_folder_policy import ( + SEMANTIC_FOLDER_BASE_FIELDS, + SEMANTIC_FOLDER_ROOT, + SEMANTIC_FOLDER_SYSTEM_FIELDS, + canonical_semantic_folder_field_name, + is_semantic_folder_forbidden_field, + semantic_folder_allowed_extension_fields, +) +from .store import ( + SQLiteFileSystemStore, + fingerprint, + make_file_ref, + metadata_text, + normalize_path, +) +from .structural_read import ( + first_node_location, + find_pageindex_node, + strip_pageindex_text_fields, +) +from .types import OpenResult, SearchResult + +DEFAULT_METADATA_GENERATION_FIELDS = { + "summary": True, + "doc_type": True, + "domain": True, + "topic": True, + "entity": False, + "relation": False, +} + +DEFAULT_DERIVED_METADATA_FIELD_TYPES = { + "summary": "string", + "doc_type": "string", + "domain": "string", + "topic": "string", + "entity": "string", + "relation": "string", +} + +METADATA_GENERATION_STATUSES = { + "pending_submit", + "pending_generate", + "generated", + "failed", +} + +PROJECTION_INDEX_STATUSES = { + "not_indexed", + "pending_index", + "generated", + "ready", + "failed", +} + +SEMANTIC_RETRIEVAL_CHANNELS = ("summary", "entity", "relation") +SEMANTIC_GREP_CHANNELS = ("entity", "relation") +PAGEINDEX_DOCUMENT_SUFFIXES = {".pdf", ".md", ".markdown"} +PAGEINDEX_DOCUMENT_CONTENT_TYPES = { + "application/pdf", + "text/markdown", + "text/x-markdown", + "application/markdown", +} +TEXT_ARTIFACT_SUFFIXES = {".txt", ".text"} +TEXT_ARTIFACT_CONTENT_TYPES = {"text/plain"} + + +class PageIndexFileSystem: + def __init__( + self, + workspace: Union[str, Path], + *, + semantic_retrieval_backend: Any | None = None, + metadata_generator: MetadataGenerator | None = None, + summary_projection_indexer: SummaryProjectionIndexer | None = None, + summary_projection_index: bool = True, + summary_projection_index_dir: Union[str, Path, None] = None, + summary_projection_embedding_provider: str = "openai", + summary_projection_embedding_model: str = "text-embedding-3-small", + summary_projection_embedding_dimensions: int = 256, + summary_projection_embedding_timeout: float = 60, + ): + self.workspace = Path(workspace).expanduser() + self.store = SQLiteFileSystemStore(self.workspace) + self.metadata = MetadataQueryEngine(self.store) + self._references: dict[str, str] = {} + self.semantic_retrieval_backend = semantic_retrieval_backend + self.metadata_generator = metadata_generator + self.summary_projection_indexer = summary_projection_indexer + self.summary_projection_index = summary_projection_index + self.summary_projection_index_dir = ( + Path(summary_projection_index_dir).expanduser() + if summary_projection_index_dir is not None + else self.workspace / "artifacts" / "projection_indexes" + ) + self.summary_projection_embedding_provider = summary_projection_embedding_provider + self.summary_projection_embedding_model = summary_projection_embedding_model + self.summary_projection_embedding_dimensions = summary_projection_embedding_dimensions + self.summary_projection_embedding_timeout = summary_projection_embedding_timeout + + def register_file( + self, + *, + storage_uri: str, + source_path: str, + folder_path: Optional[str] = None, + metadata: Optional[dict[str, Any]] = None, + external_id: Optional[str] = None, + title: Optional[str] = None, + content: str = "", + content_type: str = "text/plain", + source_type: Optional[str] = None, + derived_metadata: Optional[dict[str, Any]] = None, + metadata_generation_policy: Optional[dict[str, Any]] = None, + metadata_generation_status: Optional[str] = None, + ) -> str: + return self.register_files( + [ + { + "storage_uri": storage_uri, + "source_path": source_path, + "folder_path": folder_path, + "metadata": metadata, + "external_id": external_id, + "title": title, + "content": content, + "content_type": content_type, + "source_type": source_type, + "derived_metadata": derived_metadata, + "metadata_generation_policy": metadata_generation_policy, + "metadata_generation_status": metadata_generation_status, + } + ] + )[0] + + def register(self, **kwargs: Any) -> str: + if not self._register_uses_deferred_metadata(kwargs.get("metadata_generation_policy")): + self._ensure_register_completion_defaults() + return self.register_file(**kwargs) + + def register_files(self, files: list[dict[str, Any]]) -> list[str]: + records = [self._prepare_file_record(file) for file in files] + for record in records: + self._generate_register_metadata(record) + self._complete_summary_projection_index(record) + self._sync_owned_raw_artifact(record) + self._register_generation_policy_schema(records) + self.store.insert_files(records) + return [record["file_ref"] for record in records] + + def batch_generate(self, *, limit: int | None = None) -> dict[str, Any]: + if self.metadata_generator is None: + raise MetadataGenerationError( + "metadata_generator is required to generate pending PIFS metadata" + ) + rows = self.store.list_pending_metadata_generation(limit=limit) + generated = 0 + failed = 0 + file_refs: list[str] = [] + for row in rows: + record = self._record_from_file_entry(row) + self._generate_register_metadata(record, force=True) + self._complete_summary_projection_index(record) + self._register_generation_policy_schema([record]) + self.store.update_file_metadata_generation( + record["file_ref"], + derived_metadata=record["derived_metadata"], + metadata_generation=record["metadata_generation"], + ) + self._sync_owned_raw_artifact(record) + file_refs.append(record["file_ref"]) + if record["metadata_generation"]["status"] == "failed": + failed += 1 + else: + generated += 1 + return { + "processed": len(rows), + "generated": generated, + "failed": failed, + "file_refs": file_refs, + } + + def _ensure_register_completion_defaults(self) -> None: + if self.metadata_generator is None: + self.metadata_generator = OpenAIMetadataGenerator() + if self.summary_projection_index and self.summary_projection_indexer is None: + self.summary_projection_indexer = SummaryProjectionIndexer.from_provider( + self.summary_projection_index_dir, + embedding_provider=self.summary_projection_embedding_provider, + embedding_model=self.summary_projection_embedding_model, + embedding_dimensions=self.summary_projection_embedding_dimensions, + embedding_timeout=self.summary_projection_embedding_timeout, + ) + if self.summary_projection_index and self.semantic_retrieval_backend is None: + self.configure_hybrid_projection_retrieval( + self.summary_projection_index_dir, + embedding_provider=self.summary_projection_embedding_provider, + embedding_model=self.summary_projection_embedding_model, + embedding_dimensions=self.summary_projection_embedding_dimensions, + embedding_timeout=self.summary_projection_embedding_timeout, + ) + + @staticmethod + def _register_uses_deferred_metadata(policy: Any) -> bool: + if not isinstance(policy, dict): + return False + return bool(policy.get("batch")) or policy.get("mode") == "batch" + + @classmethod + def default_metadata_generation_policy(cls) -> dict[str, Any]: + return { + "fields": dict(DEFAULT_METADATA_GENERATION_FIELDS), + "projection_indexes": {"summary": True}, + "batch": False, + } + + def browse( + self, + path: str = "/", + recursive: bool = False, + limit: int = 100, + ) -> dict[str, list[dict[str, Any]]]: + return self.store.list_folder(path, recursive=recursive, limit=limit) + + def find_folders( + self, + path: str = "/", + metadata_filter: Optional[dict[str, Any] | str] = None, + limit: int = 100, + ) -> list[dict[str, Any]]: + parsed_filter = self.metadata.parse_filter(metadata_filter) + return self.store.find_folders(path, metadata_filter=parsed_filter, limit=limit) + + def create_folder( + self, + path: str, + kind: str = "manual", + description: str = "", + metadata: Optional[dict[str, Any]] = None, + ) -> str: + return self.store.create_folder( + path, + kind=kind, + description=description, + metadata=metadata, + ) + + def attach_file_to_folder( + self, + file_ref: str, + folder_path_or_id: str, + metadata: Optional[dict[str, Any]] = None, + ) -> None: + self.store.attach_file_to_folder(file_ref, folder_path_or_id, metadata=metadata) + + def attach_files_to_folders(self, items: list[dict[str, Any]]) -> None: + self.store.attach_files_to_folders(items) + + def apply_semantic_folder_projection( + self, + projection_plan: dict[str, Any], + *, + file_ref_by_document_id: Optional[dict[str, str]] = None, + ) -> dict[str, Any]: + """Attach registered files to a Semantic Folder Projection. + + Registration remains the explicit folder placement step. This method is + the separate product API for adding derived `/semantic/...` memberships. + """ + folders = list(projection_plan.get("folders") or []) + memberships = list(projection_plan.get("memberships") or []) + policy_raw = projection_plan.get("policy") + policy = policy_raw if isinstance(policy_raw, dict) else {} + allowed_extension_fields = semantic_folder_allowed_extension_fields( + policy.get("allowed_extension_fields", []) + ) + for folder in folders: + self._validate_semantic_folder_projection_item(folder, allowed_extension_fields) + for membership in memberships: + self._validate_semantic_folder_projection_item(membership, allowed_extension_fields) + + for folder in folders: + folder_metadata = folder.get("metadata") + self.create_folder( + self._validate_semantic_folder_projection_path(str(folder["path"])), + kind=str(folder.get("kind") or "semantic_projection"), + description=str(folder.get("description") or ""), + metadata=folder_metadata if isinstance(folder_metadata, dict) else {}, + ) + + items: list[dict[str, Any]] = [] + file_ref_by_document_id = file_ref_by_document_id or {} + for membership in memberships: + document_id = self._semantic_folder_projection_document_id(membership) + file_ref = file_ref_by_document_id.get(document_id) + if not file_ref: + file_ref = self.store.resolve_file_ref(document_id) + metadata = ( + dict(membership.get("folder_metadata")) + if isinstance(membership.get("folder_metadata"), dict) + else {} + ) + metadata.update( + { + "projection": "Semantic Folder Projection", + "field": membership.get("field", ""), + "value": membership.get("value", ""), + "mount_kind": membership.get( + "mount_kind", + "semantic_folder_projection", + ), + } + ) + items.append( + { + "file_ref": file_ref, + "folder": self._validate_semantic_folder_projection_path( + str(membership["folder_path"]) + ), + "metadata": metadata, + } + ) + self.attach_files_to_folders(items) + return { + "projection": "Semantic Folder Projection", + "folders_applied": len(folders), + "memberships_attached": len(items), + } + + def search( + self, + query: Union[str, list[str], None] = None, + scope: Optional[dict[str, Any]] = None, + metadata_filter: Optional[dict[str, Any] | str] = None, + limit: int = 10, + semantic: bool = True, + ) -> list[SearchResult]: + parsed_filter = self.metadata.parse_filter(metadata_filter) + if semantic and self._should_use_semantic_retrieval(query, scope): + semantic_results = self._semantic_search( + query, + scope=scope, + metadata_filter=parsed_filter, + limit=limit, + ) + if semantic_results: + return semantic_results + rows = self.store.search_files( + query, + scope=scope, + metadata_filter=parsed_filter, + limit=limit, + ) + results = [] + scope_path = self._scope_folder_path(scope) + for row in rows: + reference_id = self._reference_for(row["file_ref"]) + folder_paths = [ + folder["path"] + for folder in self.store.folder_memberships(row["file_ref"]) + ] + folder_path = self._preferred_folder_path(folder_paths, scope_path, row["folder_path"]) + results.append( + SearchResult( + reference_id=reference_id, + file_ref=row["file_ref"], + external_id=row["external_id"], + title=row["title"], + snippet=row["snippet"], + folder_path=folder_path, + folder_paths=folder_paths, + metadata=row["metadata"], + derived_metadata=row["derived_metadata"], + metadata_generation=row["metadata_generation"], + source_path=row["source_path"], + id=row["id"], + document_id=row["document_id"], + name=row["name"], + description=row["description"], + status=row["status"], + pageNum=row["pageNum"], + createdAt=row["createdAt"], + folderId=row["folderId"], + ) + ) + return results + + def search_semantic_channel( + self, + channel: str, + query: Union[str, list[str], None], + *, + scope: Optional[dict[str, Any]] = None, + metadata_filter: Optional[dict[str, Any] | str] = None, + limit: int = 10, + ) -> list[SearchResult]: + parsed_filter = self.metadata.parse_filter(metadata_filter) + if ( + self.semantic_retrieval_backend is None + or not self.has_semantic_channel(channel) + or not self._query_text(query) + ): + return [] + return self._semantic_search( + query, + scope=scope, + metadata_filter=parsed_filter, + limit=limit, + channel=channel, + ) + + def configure_hybrid_projection_retrieval( + self, + index_dir: Union[str, Path], + *, + embedding_provider: str = "openai", + embedding_model: str = "text-embedding-3-small", + embedding_dimensions: int = 256, + embedding_timeout: float = 60, + per_channel_limit: int = 100, + fetch_multiplier: int = 100, + ) -> Any: + from .hybrid_projection import HybridProjectionSearchBackend + + self.semantic_retrieval_backend = HybridProjectionSearchBackend.from_provider( + index_dir, + embedding_provider=embedding_provider, + embedding_model=embedding_model, + embedding_dimensions=embedding_dimensions, + embedding_timeout=embedding_timeout, + per_channel_limit=per_channel_limit, + fetch_multiplier=fetch_multiplier, + ) + return self.semantic_retrieval_backend + + @property + def has_semantic_retrieval_backend(self) -> bool: + return self.semantic_retrieval_backend is not None + + def semantic_retrieval_channels(self) -> tuple[str, ...]: + backend = self.semantic_retrieval_backend + if backend is None: + return () + available_channels = getattr(backend, "available_channels", None) + if callable(available_channels): + raw_channels = available_channels() + else: + raw_channels = getattr(backend, "semantic_tool_channels", ()) + available = set(raw_channels or ()) + return tuple(channel for channel in SEMANTIC_RETRIEVAL_CHANNELS if channel in available) + + def has_semantic_channel(self, channel: str) -> bool: + return channel in self.semantic_retrieval_channels() + + def retrieval_capabilities(self) -> dict[str, Any]: + semantic_channels = self.semantic_retrieval_channels() + semantic_commands = [f"search-{channel}" for channel in semantic_channels] + semantic_grep_channels = [ + channel for channel in SEMANTIC_GREP_CHANNELS if channel in semantic_channels + ] + if semantic_grep_channels: + semantic_commands.append("semantic-grep") + return { + "lexical": { + "grep_recursive": True, + "grep_recursive_semantic_prefilter": False, + "grep_recursive_guard": "bounded broad-folder notice", + }, + "semantic": { + "backend_configured": self.semantic_retrieval_backend is not None, + "channels": list(semantic_channels), + "commands": semantic_commands, + "semantic_grep_channels": semantic_grep_channels, + }, + } + + def find( + self, + reference_id: str, + patterns: Union[str, list[str]], + limit: int = 20, + ) -> list[OpenResult]: + file_ref = self._resolve_reference(reference_id) + patterns = [patterns] if isinstance(patterns, str) else list(patterns) + lowered_patterns = [pattern.lower() for pattern in patterns if pattern] + if not lowered_patterns: + return [] + text = self.store.read_text(file_ref) + lines = text.splitlines() + matches = [] + for i, line in enumerate(lines, 1): + haystack = line.lower() + if any(pattern in haystack for pattern in lowered_patterns): + start = max(1, i - 1) + end = min(len(lines), i + 1) + matches.append(self._open_lines(reference_id, file_ref, start, end)) + if len(matches) >= limit: + break + return matches + + def open(self, reference_id: str, location: str = "all") -> OpenResult: + file_ref = self._resolve_reference(reference_id) + entry = self.store.get_file(file_ref) + if self._file_format(entry) in {"pdf", "markdown", "pageindex"}: + raise ValueError( + "open() text artifact reads are not supported for PDF/Markdown PageIndex files; " + "use pageindex_structure(), pageindex_pages(), or pageindex_node()." + ) + if str(location).strip().lower() in {"all", "full", "*"}: + return self._open_all(reference_id, file_ref) + start, end = self._parse_line_range(location) + return self._open_lines(reference_id, file_ref, start, end) + + def cat_text_artifact(self, reference_id: str, location: str = "all") -> OpenResult: + file_ref = self._resolve_reference(reference_id) + entry = self.store.get_file(file_ref) + self._require_text_artifact_file(entry, "cat --all") + if str(location).strip().lower() in {"all", "full", "*"}: + return self._open_all(reference_id, file_ref) + start, end = self._parse_line_range(location) + return self._open_lines(reference_id, file_ref, start, end) + + def pageindex_structure(self, reference_id: str) -> dict[str, Any]: + file_ref = self._resolve_reference(reference_id) + entry = self.store.get_file(file_ref) + self._require_pageindex_document_file(entry, "cat --structure") + client, doc_id = self._pageindex_client_doc_for_entry(entry) + if doc_id is None: + return self._structural_unavailable( + "structure", + entry, + message=( + "PageIndex structure is not cached for this file in the " + "PageIndexClient workspace." + ), + ) + structure = self._client_json(client.get_document_structure(doc_id)) + if isinstance(structure, dict) and structure.get("error"): + return self._structural_unavailable( + "structure", + entry, + message=str(structure["error"]), + ) + return { + "mode": "structure", + "file_ref": file_ref, + "external_id": entry.external_id, + "source_path": entry.source_path, + "status": entry.pageindex_tree_status, + "available": True, + "pageindex_doc_id": doc_id, + "structure": strip_pageindex_text_fields(structure), + } + + def pageindex_node(self, reference_id: str, node_id: str) -> dict[str, Any]: + file_ref = self._resolve_reference(reference_id) + entry = self.store.get_file(file_ref) + self._require_pageindex_document_file(entry, "cat --node") + client, doc_id = self._pageindex_client_doc_for_entry(entry) + if doc_id is None: + return self._structural_unavailable( + "node", + entry, + node_id=node_id, + message=( + "PageIndex structure is not cached for this file in the " + "PageIndexClient workspace." + ), + ) + client._ensure_doc_loaded(doc_id) + doc = client.documents.get(doc_id, {}) + node = find_pageindex_node(doc.get("structure", []), node_id) + if node is None: + return self._structural_unavailable( + "node", + entry, + node_id=node_id, + message="PageIndex node was not found in the cached structure.", + ) + text = str(node.get("text") or "") + if not text: + location = first_node_location(node) + if location: + content = self._client_json(client.get_page_content(doc_id, location)) + if isinstance(content, list): + text = "\n\n".join(str(page.get("content") or "") for page in content) + if not text: + return self._structural_unavailable( + "node", + entry, + node_id=node_id, + message="Cached PageIndex node has no text content.", + ) + return { + "mode": "node", + "file_ref": file_ref, + "external_id": entry.external_id, + "source_path": entry.source_path, + "status": entry.pageindex_tree_status, + "available": True, + "pageindex_doc_id": doc_id, + "node_id": node_id, + "node": strip_pageindex_text_fields(node), + "text": text, + } + + def pageindex_pages(self, reference_id: str, pages: str) -> dict[str, Any]: + file_ref = self._resolve_reference(reference_id) + entry = self.store.get_file(file_ref) + self._require_pageindex_document_file(entry, "cat --page") + client, doc_id = self._pageindex_client_doc_for_entry(entry) + if doc_id is None: + return self._structural_unavailable( + "page", + entry, + pages=pages, + message=( + "PageIndex page content is not cached for this file in the " + "PageIndexClient workspace." + ), + ) + page_entries = self._client_json(client.get_page_content(doc_id, pages)) + if isinstance(page_entries, dict) and page_entries.get("error"): + return self._structural_unavailable( + "page", + entry, + pages=pages, + message=str(page_entries["error"]), + ) + if not isinstance(page_entries, list) or not page_entries: + return self._structural_unavailable( + "page", + entry, + pages=pages, + message="Requested PageIndex page content is not cached for this file.", + ) + text = "\n\n".join(str(page.get("content") or "") for page in page_entries) + return { + "mode": "page", + "file_ref": file_ref, + "external_id": entry.external_id, + "source_path": entry.source_path, + "status": entry.pageindex_tree_status, + "available": True, + "pageindex_doc_id": doc_id, + "pages": pages, + "data": page_entries, + "text": text, + } + + def _stat(self, target: str) -> dict[str, Any]: + file_ref = self._resolve_reference(target) + return self.store.file_info(file_ref) + + def _require_text_artifact_file(self, entry: Any, command: str) -> None: + if self._file_format(entry) == "text": + return + raise ValueError( + f"{command} is only supported for txt/text files; " + f"got source_path={entry.source_path!r}, content_type={entry.content_type!r}. " + "Use cat --structure, cat --page, or cat --node for PDF/Markdown PageIndex files." + ) + + def _require_pageindex_document_file(self, entry: Any, command: str) -> None: + if self._file_format(entry) in {"pdf", "markdown", "pageindex"}: + return + raise ValueError( + f"{command} is only supported for PDF/Markdown PageIndex files; " + f"got source_path={entry.source_path!r}, content_type={entry.content_type!r}. " + "Use cat --all for txt/text files." + ) + + @classmethod + def _file_format(cls, entry: Any) -> str: + suffix = Path(str(entry.source_path or "")).suffix.lower() + content_type = cls._normalized_content_type(entry.content_type) + if suffix == ".pdf" or content_type == "application/pdf": + return "pdf" + if suffix in PAGEINDEX_DOCUMENT_SUFFIXES or content_type in PAGEINDEX_DOCUMENT_CONTENT_TYPES: + return "markdown" + if suffix in TEXT_ARTIFACT_SUFFIXES: + return "text" + if entry.pageindex_doc_id or entry.pageindex_tree_status != "not_built": + return "pageindex" + if content_type in TEXT_ARTIFACT_CONTENT_TYPES: + return "text" + return "unsupported" + + @classmethod + def _source_format(cls, source_path: Any, content_type: str | None) -> str: + suffix = Path(str(source_path or "")).suffix.lower() + normalized_content_type = cls._normalized_content_type(content_type) + if suffix == ".pdf" or normalized_content_type == "application/pdf": + return "pdf" + if ( + suffix in PAGEINDEX_DOCUMENT_SUFFIXES + or normalized_content_type in PAGEINDEX_DOCUMENT_CONTENT_TYPES + ): + return "markdown" + if suffix in TEXT_ARTIFACT_SUFFIXES: + return "text" + if normalized_content_type in TEXT_ARTIFACT_CONTENT_TYPES: + return "text" + return "unsupported" + + @staticmethod + def _normalized_content_type(content_type: str | None) -> str: + return str(content_type or "").split(";", 1)[0].strip().lower() + + @property + def pageindex_client_workspace(self) -> Path: + return self.workspace / "artifacts" / "pageindex_client" + + def _pageindex_client(self) -> PageIndexClient: + return PageIndexClient(workspace=str(self.pageindex_client_workspace)) + + def _pageindex_client_doc_for_entry(self, entry: Any) -> tuple[PageIndexClient, str | None]: + client = self._pageindex_client() + if not entry.pageindex_doc_id: + return client, None + if entry.pageindex_doc_id not in client.documents: + return client, None + return client, entry.pageindex_doc_id + + def _registration_pageindex_pointer( + self, + *, + storage_uri: str, + source_path: str, + content_type: str, + ) -> tuple[str | None, str]: + if self._source_format(source_path, content_type) not in {"pdf", "markdown"}: + return None, "not_built" + client = self._pageindex_client() + source = self._canonical_source_path(storage_uri=storage_uri, source_path=source_path) + cached_doc_id = self._find_cached_pageindex_doc_id(client, source) + if cached_doc_id: + return cached_doc_id, "built" + if source is None: + return None, "failed" + try: + doc_id = client.index(source) + return doc_id, "built" + except Exception: + return None, "failed" + + def _find_cached_pageindex_doc_id( + self, + client: PageIndexClient, + source_path: str | None, + ) -> str | None: + if source_path is None: + return None + for doc_id, doc in client.documents.items(): + if self._canonical_path(doc.get("path")) == source_path: + return doc_id + return None + + def _canonical_source_path(self, *, storage_uri: str, source_path: str) -> str | None: + parsed = urlparse(storage_uri) + if parsed.scheme == "file": + return self._canonical_path(unquote(parsed.path)) + if storage_uri and not parsed.scheme: + return self._canonical_path(storage_uri) + if Path(source_path).expanduser().is_absolute(): + return self._canonical_path(source_path) + return None + + @staticmethod + def _canonical_path(path: Any) -> str | None: + if not path: + return None + return str(Path(os.path.expanduser(str(path))).resolve(strict=False)) + + @staticmethod + def _client_json(payload: str) -> Any: + try: + return json.loads(payload) + except json.JSONDecodeError: + return {"error": f"Invalid PageIndexClient JSON response: {payload}"} + + def _metadata_schema(self) -> dict[str, Any]: + return self.metadata.export_schema() + + def _register_metadata_schema(self, schema: dict[str, Any]) -> None: + self.metadata.register_schema(schema) + + def _create_folder(self, path: str) -> str: + return self.create_folder(path) + + def _prepare_file_record(self, file: dict[str, Any]) -> dict[str, Any]: + storage_uri = file["storage_uri"] + raw_source_path = str(file["source_path"]) + source_path = raw_source_path.strip("/") + metadata = file.get("metadata") or {} + derived_metadata = file.get("derived_metadata") or {} + if not isinstance(metadata, dict): + raise ValueError("metadata must be a JSON object") + if not isinstance(derived_metadata, dict): + raise ValueError("derived_metadata must be a JSON object") + external_id = file.get("external_id") + content = file.get("content") or "" + content_type = file.get("content_type") or "text/plain" + pageindex_doc_id, pageindex_tree_status = self._registration_pageindex_pointer( + storage_uri=storage_uri, + source_path=raw_source_path, + content_type=content_type, + ) + artifact_content = self._registration_text_artifact_content( + source_path=raw_source_path, + content_type=content_type, + pageindex_doc_id=pageindex_doc_id, + pageindex_tree_status=pageindex_tree_status, + fallback_content=content, + ) + fts_content = file.get("fts_content", artifact_content) + source_type = file.get("source_type") or self._infer_source_type(source_path) + generation_policy = self._normalize_metadata_generation_policy( + file.get("metadata_generation_policy"), + derived_metadata=derived_metadata, + ) + generation_state = self._metadata_generation_state( + generation_policy, + derived_metadata=derived_metadata, + status=file.get("metadata_generation_status"), + ) + indexed_metadata = SQLiteFileSystemStore.indexed_metadata_values( + metadata, + derived_metadata, + generation_state, + ) + searchable_metadata = self._merge_metadata_values(metadata, derived_metadata) + folder_path = normalize_path(file.get("folder_path") or "/") + title = file.get("title") or metadata.get("title") or Path(source_path).stem + file_ref = make_file_ref(external_id or source_path) + text_artifact_path = file.get("text_artifact_path") or self.store.write_text_artifact( + file_ref, + artifact_content, + ) + raw_artifact_path = file.get("raw_artifact_path") + if raw_artifact_path is None and file.get("write_raw_artifact", True): + raw_artifact_path = self.store.write_raw_artifact( + file_ref, + self._raw_artifact_payload( + storage_uri=storage_uri, + source_path=source_path, + folder_path=folder_path, + metadata=metadata, + derived_metadata=derived_metadata, + metadata_generation=generation_state, + ), + ) + descriptor = self._build_descriptor(title, metadata) + return { + "file_ref": file_ref, + "external_id": external_id, + "storage_uri": storage_uri, + "source_path": source_path, + "title": title, + "descriptor": descriptor, + "content_type": content_type, + "source_type": source_type, + "fingerprint": fingerprint(artifact_content), + "text_artifact_path": str(text_artifact_path), + "raw_artifact_path": str(raw_artifact_path) if raw_artifact_path is not None else None, + "pageindex_doc_id": pageindex_doc_id, + "pageindex_tree_status": pageindex_tree_status, + "metadata": metadata, + "metadata_json": json.dumps(metadata, ensure_ascii=False), + "derived_metadata": derived_metadata, + "derived_metadata_json": json.dumps(derived_metadata, ensure_ascii=False), + "metadata_generation": generation_state, + "metadata_generation_json": json.dumps(generation_state, ensure_ascii=False), + "indexed_metadata": indexed_metadata, + "metadata_text": metadata_text(searchable_metadata), + "folder_path": folder_path, + "content": fts_content, + "skip_fts": bool(file.get("skip_fts", False)), + } + + def _registration_text_artifact_content( + self, + *, + source_path: str, + content_type: str, + pageindex_doc_id: str | None, + pageindex_tree_status: str, + fallback_content: str, + ) -> str: + if self._source_format(source_path, content_type) not in {"pdf", "markdown"}: + return fallback_content + if pageindex_tree_status != "built" or not pageindex_doc_id: + return fallback_content + return self._pageindex_extracted_text(pageindex_doc_id) + + def _pageindex_extracted_text(self, doc_id: str) -> str: + client = self._pageindex_client() + if doc_id not in client.documents: + return "" + client._ensure_doc_loaded(doc_id) + doc = client.documents.get(doc_id) or {} + page_text = self._pageindex_pages_text(doc.get("pages")) + if page_text: + return page_text + return self._pageindex_structure_text(doc.get("structure", [])) + + @staticmethod + def _pageindex_pages_text(pages: Any) -> str: + if not isinstance(pages, list): + return "" + parts: list[str] = [] + for page in pages: + if not isinstance(page, dict): + continue + content = str(page.get("content") or "").strip() + if content: + parts.append(content) + return "\n\n".join(parts) + + @classmethod + def _pageindex_structure_text(cls, structure: Any) -> str: + parts: list[str] = [] + cls._collect_pageindex_node_text(structure, parts) + return "\n\n".join(parts) + + @classmethod + def _collect_pageindex_node_text(cls, node: Any, parts: list[str]) -> None: + if isinstance(node, list): + for item in node: + cls._collect_pageindex_node_text(item, parts) + return + if not isinstance(node, dict): + return + text = str(node.get("text") or "").strip() + if text: + parts.append(text) + cls._collect_pageindex_node_text(node.get("nodes", []), parts) + + @staticmethod + def _raw_artifact_payload( + *, + storage_uri: str, + source_path: str, + folder_path: str, + metadata: dict[str, Any], + derived_metadata: dict[str, Any], + metadata_generation: dict[str, Any], + ) -> dict[str, Any]: + return { + "storage_uri": storage_uri, + "source_path": source_path, + "folder_path": folder_path, + "metadata": metadata, + "derived_metadata": derived_metadata, + "metadata_generation": metadata_generation, + } + + def _sync_owned_raw_artifact(self, record: dict[str, Any]) -> None: + raw_artifact_path = record.get("raw_artifact_path") + if not raw_artifact_path: + return + default_raw_artifact_path = self.store.raw_dir / f"{record['file_ref']}.json" + if Path(raw_artifact_path).expanduser().resolve(strict=False) != ( + default_raw_artifact_path.resolve(strict=False) + ): + return + record["raw_artifact_path"] = str( + self.store.write_raw_artifact( + record["file_ref"], + self._raw_artifact_payload( + storage_uri=record["storage_uri"], + source_path=record["source_path"], + folder_path=record["folder_path"], + metadata=record["metadata"], + derived_metadata=record["derived_metadata"], + metadata_generation=record["metadata_generation"], + ), + ) + ) + + def _record_from_file_entry(self, entry: Any) -> dict[str, Any]: + content = self.store.read_text(entry.file_ref) + generation_policy = self._normalize_metadata_generation_policy( + entry.metadata_generation.get("policy", {}), + derived_metadata=entry.derived_metadata, + ) + generation_state = self._metadata_generation_state( + generation_policy, + derived_metadata=entry.derived_metadata, + status=entry.metadata_generation.get("status"), + ) + return { + "file_ref": entry.file_ref, + "external_id": entry.external_id, + "storage_uri": entry.storage_uri, + "source_path": entry.source_path, + "title": entry.title, + "descriptor": entry.descriptor, + "content_type": entry.content_type, + "source_type": entry.source_type, + "fingerprint": entry.fingerprint, + "text_artifact_path": entry.text_artifact_path, + "raw_artifact_path": entry.raw_artifact_path, + "pageindex_doc_id": entry.pageindex_doc_id, + "pageindex_tree_status": entry.pageindex_tree_status, + "metadata": dict(entry.metadata), + "metadata_json": json.dumps(entry.metadata, ensure_ascii=False), + "derived_metadata": dict(entry.derived_metadata), + "derived_metadata_json": json.dumps(entry.derived_metadata, ensure_ascii=False), + "metadata_generation": generation_state, + "metadata_generation_json": json.dumps(generation_state, ensure_ascii=False), + "indexed_metadata": SQLiteFileSystemStore.indexed_metadata_values( + entry.metadata, + entry.derived_metadata, + generation_state, + ), + "metadata_text": metadata_text(self._merge_metadata_values(entry.metadata, entry.derived_metadata)), + "folder_path": entry.folder_path, + "content": content, + "skip_fts": False, + } + + def _generate_register_metadata(self, record: dict[str, Any], *, force: bool = False) -> None: + generation = record["metadata_generation"] + policy = generation.get("policy", {}) + if self._metadata_generation_is_batch(policy) and not force: + self._mark_requested_generation_status(record, "pending_submit") + return + fields = self._metadata_fields_to_generate(record) + if not fields: + return + if self.metadata_generator is None: + if self._metadata_generation_requires_sync(policy): + raise MetadataGenerationError( + "metadata_generator is required for synchronous PIFS metadata generation; " + "set metadata_generation_policy batch=true to defer" + ) + return + try: + result = self.metadata_generator.generate( + MetadataGenerationInput( + file_ref=record["file_ref"], + external_id=record.get("external_id"), + title=record["title"], + source_path=record["source_path"], + content_type=record["content_type"], + source_type=record.get("source_type"), + text=Path(record["text_artifact_path"]).read_text(encoding="utf-8"), + metadata=dict(record.get("metadata") or {}), + text_artifact_path=record.get("text_artifact_path"), + ), + fields=fields, + ) + if isinstance(result, dict): + result = MetadataGenerationResult(values=result) + except Exception as exc: + self._apply_metadata_generation_failures(record, fields, str(exc)) + return + failures = dict(result.failures) + for field in fields: + if field in result.values: + record["derived_metadata"][field] = result.values[field] + generation["fields"][field] = {"requested": True, "status": "generated"} + else: + failures.setdefault(field, "metadata generator did not return field") + for field, reason in failures.items(): + generation["fields"][field] = { + "requested": True, + "status": "failed", + "error": str(reason), + } + self._refresh_record_metadata_generation(record) + + def _complete_summary_projection_index(self, record: dict[str, Any]) -> None: + generation = record["metadata_generation"] + summary_index = generation.get("projection_indexes", {}).get("summary") + if not summary_index or not summary_index.get("requested"): + return + summary = str(record.get("derived_metadata", {}).get("summary") or "").strip() + if not summary: + return + if self.summary_projection_indexer is None: + self._refresh_record_metadata_generation(record) + return + try: + result = self.summary_projection_indexer.upsert_summary(record) + except Exception as exc: + summary_index["status"] = "failed" + summary_index["error"] = str(exc) + self._refresh_record_metadata_generation(record) + return + summary_index.clear() + summary_index.update({"requested": True, **result}) + if summary_index.get("status") != "ready": + summary_index["status"] = "ready" + self._refresh_record_metadata_generation(record) + + @staticmethod + def _metadata_generation_is_batch(policy: dict[str, Any]) -> bool: + return bool(policy.get("batch")) or policy.get("mode") == "batch" + + @staticmethod + def _metadata_generation_requires_sync(policy: dict[str, Any]) -> bool: + return policy.get("batch") is False or policy.get("mode") == "sync" + + def _metadata_fields_to_generate(self, record: dict[str, Any]) -> list[str]: + fields: list[str] = [] + for name, state in record["metadata_generation"].get("fields", {}).items(): + if not state.get("requested"): + continue + if state.get("status") == "generated" and name in record["derived_metadata"]: + continue + fields.append(name) + return fields + + def _mark_requested_generation_status(self, record: dict[str, Any], status: str) -> None: + for name, field in record["metadata_generation"].get("fields", {}).items(): + if field.get("requested") and field.get("status") != "generated": + record["metadata_generation"]["fields"][name] = { + "requested": True, + "status": status, + } + self._refresh_record_metadata_generation(record, explicit_status=status) + + def _apply_metadata_generation_failures( + self, + record: dict[str, Any], + fields: list[str], + reason: str, + ) -> None: + for field in fields: + record["metadata_generation"]["fields"][field] = { + "requested": True, + "status": "failed", + "error": reason, + } + self._refresh_record_metadata_generation(record, explicit_status="failed") + + def _refresh_record_metadata_generation( + self, + record: dict[str, Any], + *, + explicit_status: str | None = None, + ) -> None: + generation = record["metadata_generation"] + statuses = [ + field.get("status") + for field in generation.get("fields", {}).values() + if field.get("requested") and field.get("status") + ] + generation["status"] = explicit_status or self._aggregate_generation_status(statuses) + self._refresh_projection_index_statuses(generation, record["derived_metadata"]) + record["derived_metadata_json"] = json.dumps(record["derived_metadata"], ensure_ascii=False) + record["metadata_generation_json"] = json.dumps(generation, ensure_ascii=False) + record["indexed_metadata"] = SQLiteFileSystemStore.indexed_metadata_values( + record["metadata"], + record["derived_metadata"], + generation, + ) + record["metadata_text"] = metadata_text( + self._merge_metadata_values(record["metadata"], record["derived_metadata"]) + ) + + def _open_lines(self, reference_id: str, file_ref: str, start: int, end: int) -> OpenResult: + entry = self.store.get_file(file_ref) + lines = self.store.read_text(file_ref).splitlines() + start = max(1, start) + end = min(max(start, end), len(lines)) + text = "\n".join(lines[start - 1:end]) + return OpenResult( + reference_id=reference_id, + file_ref=file_ref, + start_line=start, + end_line=end, + text=text, + external_id=entry.external_id, + folder_path=entry.folder_path, + source_path=entry.source_path, + ) + + def _open_all(self, reference_id: str, file_ref: str) -> OpenResult: + entry = self.store.get_file(file_ref) + text = self.store.read_text(file_ref) + line_count = len(text.splitlines()) + return OpenResult( + reference_id=reference_id, + file_ref=file_ref, + start_line=1, + end_line=line_count, + text=text, + external_id=entry.external_id, + folder_path=entry.folder_path, + source_path=entry.source_path, + ) + + @staticmethod + def _structural_unavailable( + mode: str, + entry: Any, + *, + message: str, + node_id: str | None = None, + pages: str | None = None, + ) -> dict[str, Any]: + result = { + "mode": mode, + "file_ref": entry.file_ref, + "external_id": entry.external_id, + "source_path": entry.source_path, + "status": entry.pageindex_tree_status, + "available": False, + "message": message, + } + if node_id is not None: + result["node_id"] = node_id + if pages is not None: + result["pages"] = pages + return result + + def _resolve_reference(self, reference_id: str) -> str: + if reference_id in self._references: + return self._references[reference_id] + return self.store.resolve_file_ref(reference_id) + + def _should_use_semantic_retrieval( + self, + query: Union[str, list[str], None], + scope: Optional[dict[str, Any]], + ) -> bool: + if self.semantic_retrieval_backend is None: + return False + if not self._query_text(query): + return False + if not scope: + return True + return bool(scope.get("recursive", True)) + + def _semantic_search( + self, + query: Union[str, list[str], None], + *, + scope: Optional[dict[str, Any]], + metadata_filter: Optional[dict[str, Any]], + limit: int, + channel: str | None = None, + ) -> list[SearchResult]: + if self.semantic_retrieval_backend is None: + return [] + filters = self._semantic_filters_for_scope(scope) + fetch_limit = max(limit * 10, 50) + query_text = self._query_text(query) + if channel: + search_channel = getattr(self.semantic_retrieval_backend, "search_channel", None) + if search_channel is None: + return [] + candidates = search_channel( + channel, + query_text, + limit=fetch_limit, + filters=filters, + ) + else: + candidates = self.semantic_retrieval_backend.search( + query_text, + limit=fetch_limit, + filters=filters, + ) + results: list[SearchResult] = [] + seen: set[str] = set() + scope_path = self._scope_folder_path(scope) + for candidate in candidates: + try: + file_ref = self.store.resolve_file_ref(candidate.document_id) + except KeyError: + continue + if file_ref in seen: + continue + if not self.store.file_matches(file_ref, scope=scope, metadata_filter=metadata_filter): + continue + seen.add(file_ref) + entry = self.store.get_file(file_ref) + reference_id = self._reference_for(file_ref) + folder_paths = [ + folder["path"] + for folder in self.store.folder_memberships(file_ref) + ] + folder_path = self._preferred_folder_path(folder_paths, scope_path, entry.folder_path) + results.append( + SearchResult( + reference_id=reference_id, + file_ref=file_ref, + external_id=entry.external_id, + title=entry.title, + snippet=candidate.snippet or entry.descriptor, + folder_path=folder_path, + folder_paths=folder_paths, + metadata=entry.metadata, + derived_metadata=entry.derived_metadata, + metadata_generation=entry.metadata_generation, + source_path=entry.source_path, + id=entry.external_id or file_ref, + document_id=entry.external_id, + name=entry.title, + description=entry.descriptor, + status=entry.pageindex_tree_status, + pageNum=None, + createdAt=None, + folderId=None, + ) + ) + if len(results) >= limit: + break + return results + + def _reference_for(self, file_ref: str) -> str: + for reference_id, existing in self._references.items(): + if existing == file_ref: + return reference_id + reference_id = f"ref_{len(self._references) + 1}" + self._references[reference_id] = file_ref + return reference_id + + @staticmethod + def _build_descriptor(title: str, metadata: dict[str, Any]) -> str: + source = metadata.get("source_type") or metadata.get("repo") or metadata.get("channel") + return f"{title} ({source})" if source else title + + def _register_generation_policy_schema(self, records: list[dict[str, Any]]) -> None: + fields: dict[str, dict[str, str]] = {} + for record in records: + policy_fields = record["metadata_generation"]["policy"]["fields"] + for name, requested in policy_fields.items(): + if requested: + fields[name] = { + "type": DEFAULT_DERIVED_METADATA_FIELD_TYPES.get( + name, + self._infer_metadata_field_type( + record.get("derived_metadata", {}).get(name) + ), + ) + } + for name, value in record.get("derived_metadata", {}).items(): + fields.setdefault(name, {"type": self._infer_metadata_field_type(value)}) + if fields: + self.metadata.register_schema({"fields": fields}, source="derived") + + @classmethod + def _normalize_metadata_generation_policy( + cls, + policy: Optional[dict[str, Any]], + *, + derived_metadata: dict[str, Any], + ) -> dict[str, Any]: + fields = dict(DEFAULT_METADATA_GENERATION_FIELDS) + field_statuses: dict[str, str] = {} + projection_indexes: dict[str, bool] | None = None + projection_index_statuses: dict[str, str] = {} + mode = None + batch = None + top_level_status = None + if policy is not None: + if not isinstance(policy, dict): + raise ValueError("metadata_generation_policy must be a JSON object") + raw_fields = policy.get("fields") + if raw_fields is None: + raw_fields = { + name: declaration + for name, declaration in policy.items() + if name not in {"batch", "mode", "status", "projection_indexes"} + } + if not isinstance(raw_fields, dict): + raise ValueError("metadata_generation_policy fields must be a JSON object") + for name, declaration in raw_fields.items(): + name = str(name) + if isinstance(declaration, bool): + fields[name] = declaration + continue + if isinstance(declaration, dict): + fields[name] = bool( + declaration.get("enabled", declaration.get("requested", True)) + ) + field_status = declaration.get("status") + if field_status is not None: + cls._validate_metadata_generation_status(str(field_status)) + field_statuses[name] = str(field_status) + continue + raise ValueError(f"Invalid metadata generation policy for field: {name}") + mode = policy.get("mode") + if "batch" in policy: + batch = bool(policy["batch"]) + elif mode == "batch": + batch = True + top_level_status = policy.get("status") + if top_level_status is not None: + cls._validate_metadata_generation_status(str(top_level_status)) + if "projection_indexes" in policy: + projection_indexes, projection_index_statuses = ( + cls._normalize_projection_index_policy(policy["projection_indexes"]) + ) + for name in derived_metadata: + fields.setdefault(name, True) + normalized: dict[str, Any] = { + "fields": fields, + "projection_indexes": ( + projection_indexes + if projection_indexes is not None + else {"summary": bool(fields.get("summary", False))} + ), + } + if field_statuses: + normalized["field_statuses"] = field_statuses + if projection_index_statuses: + normalized["projection_index_statuses"] = projection_index_statuses + if mode: + normalized["mode"] = str(mode) + if batch is not None: + normalized["batch"] = batch + if top_level_status: + normalized["status"] = str(top_level_status) + return normalized + + @classmethod + def _metadata_generation_state( + cls, + policy: dict[str, Any], + *, + derived_metadata: dict[str, Any], + status: Optional[str], + ) -> dict[str, Any]: + explicit_status = status or policy.get("status") + if explicit_status is not None: + explicit_status = str(explicit_status) + cls._validate_metadata_generation_status(explicit_status) + field_statuses = policy.get("field_statuses", {}) + fields: dict[str, dict[str, Any]] = {} + for name, requested in policy["fields"].items(): + if not requested: + fields[name] = {"requested": False} + continue + field_status = field_statuses.get(name) + if field_status is None: + field_status = explicit_status + if field_status is None: + field_status = "generated" if name in derived_metadata else "pending_generate" + fields[name] = {"requested": True, "status": field_status} + + requested_statuses = [ + item["status"] + for item in fields.values() + if item.get("requested") and item.get("status") + ] + aggregate_status = explicit_status or cls._aggregate_generation_status(requested_statuses) + policy_summary = { + "fields": dict(policy["fields"]), + "projection_indexes": dict(policy.get("projection_indexes", {})), + } + if "mode" in policy: + policy_summary["mode"] = policy["mode"] + if "batch" in policy: + policy_summary["batch"] = policy["batch"] + state = { + "status": aggregate_status, + "policy": policy_summary, + "fields": fields, + "projection_indexes": {}, + } + projection_statuses = policy.get("projection_index_statuses", {}) + for name, requested in policy.get("projection_indexes", {}).items(): + if not requested: + continue + state["projection_indexes"][name] = { + "requested": True, + "status": projection_statuses.get(name, "not_indexed"), + } + cls._refresh_projection_index_statuses(state, derived_metadata) + return state + + @staticmethod + def _aggregate_generation_status(statuses: list[str]) -> str: + if not statuses: + return "generated" + for status in ("failed", "pending_submit", "pending_generate"): + if status in statuses: + return status + return "generated" + + @staticmethod + def _validate_metadata_generation_status(status: str) -> None: + if status not in METADATA_GENERATION_STATUSES: + raise ValueError(f"Unsupported metadata generation status: {status}") + + @classmethod + def _normalize_projection_index_policy( + cls, + projection_policy: Any, + ) -> tuple[dict[str, bool], dict[str, str]]: + if projection_policy is None: + return {}, {} + if not isinstance(projection_policy, dict): + raise ValueError("metadata_generation_policy projection_indexes must be a JSON object") + projection_indexes: dict[str, bool] = {} + projection_index_statuses: dict[str, str] = {} + for name, declaration in projection_policy.items(): + name = str(name) + if isinstance(declaration, bool): + projection_indexes[name] = declaration + continue + if isinstance(declaration, dict): + projection_indexes[name] = bool( + declaration.get("enabled", declaration.get("requested", True)) + ) + status = declaration.get("status") + if status is not None: + status = str(status) + cls._validate_projection_index_status(status) + projection_index_statuses[name] = status + continue + raise ValueError(f"Invalid projection index policy for index: {name}") + return projection_indexes, projection_index_statuses + + @staticmethod + def _validate_projection_index_status(status: str) -> None: + if status not in PROJECTION_INDEX_STATUSES: + raise ValueError(f"Unsupported projection index status: {status}") + + @classmethod + def _refresh_projection_index_statuses( + cls, + generation: dict[str, Any], + derived_metadata: dict[str, Any], + ) -> None: + summary_index = generation.get("projection_indexes", {}).get("summary") + if not summary_index or not summary_index.get("requested"): + return + if "summary" not in derived_metadata: + return + if summary_index.get("status", "not_indexed") == "not_indexed": + summary_index["status"] = "pending_index" + + @classmethod + def _merge_metadata_values( + cls, + metadata: dict[str, Any], + derived_metadata: dict[str, Any], + ) -> dict[str, Any]: + merged = dict(metadata) + for name, value in derived_metadata.items(): + if name not in merged: + merged[name] = value + continue + if merged[name] == value: + continue + merged[name] = cls._merge_metadata_value(merged[name], value) + return merged + + @staticmethod + def _merge_metadata_value(raw_value: Any, derived_value: Any) -> Any: + values = raw_value if isinstance(raw_value, list) else [raw_value] + derived_values = derived_value if isinstance(derived_value, list) else [derived_value] + merged = list(values) + for item in derived_values: + if item not in merged: + merged.append(item) + return merged + + @staticmethod + def _infer_metadata_field_type(value: Any) -> str: + if isinstance(value, bool): + return "boolean" + if isinstance(value, (int, float)): + return "number" + return "string" + + @staticmethod + def _infer_source_type(source_path: str) -> Optional[str]: + parts = [part for part in Path(source_path).parts if part not in ("", ".")] + return parts[0] if parts else None + + @staticmethod + def _scope_folder_path(scope: Optional[dict[str, Any]]) -> Optional[str]: + if not scope: + return None + path = scope.get("folder_path") or scope.get("path") + return normalize_path(path) if path else None + + @classmethod + def _semantic_filters_for_scope(cls, scope: Optional[dict[str, Any]]) -> dict[str, Any]: + path = cls._scope_folder_path(scope) + if not path or path == "/": + return {} + source_type = cls._source_type_filter_from_path(path) + return {"source_type": source_type} if source_type else {} + + @staticmethod + def _source_type_filter_from_path(path: str) -> str: + segments = [segment for segment in path.strip("/").split("/") if segment] + if not segments: + return "" + if segments[0] == SEMANTIC_FOLDER_ROOT.strip("/"): + segments = segments[1:] + if not segments: + return "" + first_segment = segments[0] + if first_segment.startswith("source_type="): + return first_segment.split("=", 1)[1].replace("-", "_") + if path.startswith(f"{SEMANTIC_FOLDER_ROOT}/"): + return "" + return "" + + @classmethod + def _validate_semantic_folder_projection_item( + cls, + item: dict[str, Any], + allowed_extension_fields: set[str], + ) -> None: + path = item.get("folder_path") or item.get("path") + if not path: + raise ValueError("Semantic Folder Projection items must include a folder path") + cls._validate_semantic_folder_projection_path(str(path)) + allowed_fields = ( + SEMANTIC_FOLDER_BASE_FIELDS + | SEMANTIC_FOLDER_SYSTEM_FIELDS + | allowed_extension_fields + ) + if item.get("dataset_doc_uuid"): + raise ValueError( + "dataset_doc_uuid is not allowed in Semantic Folder Projection memberships; " + "use file_key or file_ref" + ) + fields = [] + explicit_field = cls._canonical_semantic_folder_field_name(item.get("field")) + if explicit_field: + fields.append(explicit_field) + fields.extend(cls._semantic_folder_projection_fields_from_path(str(path))) + for payload_key in ("metadata", "folder_metadata"): + cls._validate_semantic_folder_projection_metadata_payload( + item.get(payload_key), + allowed_fields, + ) + for field in fields: + if is_semantic_folder_forbidden_field(field) or field not in allowed_fields: + raise ValueError(f"Field is not allowed for Semantic Folder Projection: {field}") + + @staticmethod + def _validate_semantic_folder_projection_path(path: str) -> str: + normalized = normalize_path(path) + if normalized != SEMANTIC_FOLDER_ROOT and not normalized.startswith( + f"{SEMANTIC_FOLDER_ROOT}/" + ): + raise ValueError("Semantic Folder Projection paths must be under /semantic") + return normalized + + @classmethod + def _semantic_folder_projection_fields_from_path(cls, path: str) -> list[str]: + normalized = cls._validate_semantic_folder_projection_path(path) + fields: list[str] = [] + for segment in normalized.strip("/").split("/")[1:]: + if "=" not in segment: + continue + field = cls._canonical_semantic_folder_field_name( + segment.split("=", 1)[0] + ) + if field: + fields.append(field) + return fields + + @classmethod + def _validate_semantic_folder_projection_metadata_payload( + cls, + payload: Any, + allowed_fields: set[str], + ) -> None: + if isinstance(payload, dict): + for key, value in payload.items(): + key_text = str(key) + key_field = cls._canonical_semantic_folder_field_name(key) + if is_semantic_folder_forbidden_field(key_field): + raise ValueError( + "Forbidden metadata field in Semantic Folder Projection payload: " + f"{key_text}" + ) + if key_field in {"field", "source_field", "metadata_field"}: + field = cls._canonical_semantic_folder_field_name(value) + if field and ( + is_semantic_folder_forbidden_field(field) + or field not in allowed_fields + ): + raise ValueError( + f"Field is not allowed for Semantic Folder Projection: {field}" + ) + cls._validate_semantic_folder_projection_metadata_payload(value, allowed_fields) + elif isinstance(payload, list): + for item in payload: + cls._validate_semantic_folder_projection_metadata_payload(item, allowed_fields) + elif isinstance(payload, str): + field = cls._canonical_semantic_folder_field_name(payload) + if is_semantic_folder_forbidden_field(field): + raise ValueError( + "Forbidden metadata field label in Semantic Folder Projection payload: " + f"{payload}" + ) + + @staticmethod + def _canonical_semantic_folder_field_name(value: Any) -> str: + return canonical_semantic_folder_field_name(value) + + @staticmethod + def _semantic_folder_projection_document_id(membership: dict[str, Any]) -> str: + for key in ("file_key", "file_ref", "document_ref"): + value = str(membership.get(key) or "").strip() + if value: + return value + raise ValueError("Semantic Folder Projection membership is missing file_key or file_ref") + + @staticmethod + def _query_text(query: Union[str, list[str], None]) -> str: + if query is None: + return "" + if isinstance(query, list): + return " ".join(str(item) for item in query) + return str(query) + + @staticmethod + def _preferred_folder_path( + folder_paths: list[str], + scope_path: Optional[str], + fallback: str, + ) -> str: + if scope_path: + scoped = [ + path + for path in folder_paths + if path == scope_path or path.startswith(f"{scope_path.rstrip('/')}/") + ] + if scoped: + return sorted(scoped, key=lambda item: (len(item), item))[0] + non_root = [path for path in folder_paths if path != "/"] + if non_root: + return sorted(non_root, key=lambda item: (len(item), item))[0] + return fallback + + @staticmethod + def _parse_line_range(location: str) -> tuple[int, int]: + value = str(location).strip() + if "-" in value: + left, right = value.split("-", 1) + start, end = int(left), int(right) + else: + start = end = int(value) + if start < 1 or end < start: + raise ValueError(f"Invalid line range: {location}") + return start, end diff --git a/pageindex/filesystem/hybrid_projection.py b/pageindex/filesystem/hybrid_projection.py new file mode 100644 index 000000000..30df5911d --- /dev/null +++ b/pageindex/filesystem/hybrid_projection.py @@ -0,0 +1,662 @@ +from __future__ import annotations + +import hashlib +import json +import os +import re +import sqlite3 +import struct +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from .semantic_index import SQLiteVecSemanticIndex, SemanticIndexError, SemanticSearchResult + + +INDEX_BY_CHANNEL = { + "metadata": "metadata_composite_vector", + "summary": "summary_only_vector", + "entity": "entity_vectors", + "constraint": "constraint_vectors", + "relation": "relation_vectors", +} +HYBRID_ENTITY_RELATION_CHANNELS = ("metadata", "entity", "constraint", "relation") +SEMANTIC_TOOL_CHANNELS = ("summary", "entity", "relation") +HYBRID_ENTITY_RELATION_WEIGHTS = { + "metadata": 0.25, + "entity": 0.25, + "relation": 0.30, + "constraint": 0.20, +} + + +@dataclass(frozen=True) +class QueryProjection: + entities: list[str] + relations: list[str] + constraints: list[str] + expected_answer_type: str = "" + + +@dataclass(frozen=True) +class HybridProjectionCandidate: + document_id: str + score: float + sources: list[dict[str, Any]] + source_type: str + source_path: str + title: str + metadata: dict[str, Any] + snippet: str + + +class HybridProjectionSearchBackend: + """Hybrid entity/relation/vector retrieval over rebuildable projection indexes. + + The SQLite catalog remains the source of truth. This backend only reads + external sqlite-vec projection indexes and returns candidate document ids + for the catalog to resolve and filter. + """ + + def __init__( + self, + index_dir: str | Path, + *, + embedder: Any, + embedding_provider: str, + embedding_model: str, + embedding_dimensions: int = 256, + embedding_cache_path: str | Path | None = None, + per_channel_limit: int = 100, + fetch_multiplier: int = 100, + ) -> None: + self.index_dir = Path(index_dir).expanduser() + self.embedder = embedder + self.embedding_provider = embedding_provider + self.embedding_model = embedding_model + self.embedding_dimensions = embedding_dimensions + self.cache_model = embedding_cache_model_key(embedding_model, embedding_dimensions) + self.embedding_cache = EmbeddingCache( + Path(embedding_cache_path).expanduser() + if embedding_cache_path is not None + else self.index_dir / "embedding_cache.sqlite" + ) + self.per_channel_limit = per_channel_limit + self.fetch_multiplier = fetch_multiplier + self.indexes = { + channel: SQLiteVecSemanticIndex(self.index_dir / f"{index_name}.sqlite") + for channel, index_name in INDEX_BY_CHANNEL.items() + } + + @classmethod + def from_provider( + cls, + index_dir: str | Path, + *, + embedding_provider: str = "openai", + embedding_model: str = "text-embedding-3-small", + embedding_dimensions: int = 256, + embedding_timeout: float = 60, + **kwargs: Any, + ) -> "HybridProjectionSearchBackend": + return cls( + index_dir, + embedder=make_embedder( + embedding_provider, + embedding_model, + dimensions=embedding_dimensions, + timeout=embedding_timeout, + ), + embedding_provider=embedding_provider, + embedding_model=embedding_model, + embedding_dimensions=embedding_dimensions, + **kwargs, + ) + + def search( + self, + query: str, + *, + limit: int = 10, + filters: dict[str, Any] | None = None, + ) -> list[HybridProjectionCandidate]: + query = normalize_text(query) + if not query: + return [] + projection = heuristic_query_projection(query) + channels = tuple( + channel + for channel in HYBRID_ENTITY_RELATION_CHANNELS + if self._channel_document_count(channel) > 0 + ) + if not channels: + return [] + channel_hits = self._search_channels( + query=query, + projection=projection, + limit=max(limit, self.per_channel_limit), + filters=filters, + channels=channels, + ) + return aggregate_hybrid_entity_relation(channel_hits, projection)[:limit] + + def search_channel( + self, + channel: str, + query: str, + *, + limit: int = 10, + filters: dict[str, Any] | None = None, + ) -> list[HybridProjectionCandidate]: + if channel not in SEMANTIC_TOOL_CHANNELS: + raise ValueError(f"unsupported semantic channel: {channel}") + if channel not in self.available_channels(): + return [] + query = normalize_text(query) + if not query: + return [] + projection = heuristic_query_projection(query) + vector = self.embedding_cache.embed_texts( + [query_text_for_channel(channel, query, projection)], + provider=self.embedding_provider, + model=self.cache_model, + embedder=self.embedder, + batch_size=1, + )[0] + results = self.indexes[channel].search( + vector, + limit=limit, + filters=filters, + fetch_multiplier=self.fetch_multiplier, + ) + return rank_single_semantic_channel(channel, results) + + def available_channels(self) -> tuple[str, ...]: + return tuple( + channel + for channel in SEMANTIC_TOOL_CHANNELS + if self._channel_document_count(channel) > 0 + ) + + def info(self) -> dict[str, Any]: + return { + "index_dir": str(self.index_dir), + "embedding_provider": self.embedding_provider, + "embedding_model": self.embedding_model, + "embedding_dimensions": self.embedding_dimensions, + "strategy": "hybrid_entity_relation_vector", + "available_channels": list(self.available_channels()), + "channels": { + channel: self._safe_channel_info(channel) + for channel in self.indexes + }, + } + + def _channel_document_count(self, channel: str) -> int: + info = self._safe_channel_info(channel) + if not info.get("available"): + return 0 + return int(info.get("document_count") or 0) + + def _safe_channel_info(self, channel: str) -> dict[str, Any]: + index = self.indexes[channel] + if not index.db_path.exists(): + return { + "db_path": str(index.db_path), + "available": False, + "document_count": 0, + "error": "index file is missing", + } + try: + info = index.info() + except (OSError, sqlite3.Error, SemanticIndexError) as exc: + return { + "db_path": str(index.db_path), + "available": False, + "document_count": 0, + "error": str(exc), + } + return {**info, "available": int(info.get("document_count") or 0) > 0} + + def _search_channels( + self, + *, + query: str, + projection: QueryProjection, + limit: int, + filters: dict[str, Any] | None, + channels: tuple[str, ...], + ) -> dict[str, list[SemanticSearchResult]]: + query_texts = { + channel: query_text_for_channel(channel, query, projection) + for channel in channels + } + vectors = self.embedding_cache.embed_texts( + [query_texts[channel] for channel in channels], + provider=self.embedding_provider, + model=self.cache_model, + embedder=self.embedder, + batch_size=1, + ) + return { + channel: self.indexes[channel].search( + vector, + limit=limit, + filters=filters, + fetch_multiplier=self.fetch_multiplier, + ) + for channel, vector in zip(channels, vectors) + } + + +class EmbeddingCache: + def __init__(self, db_path: Path): + self.db_path = db_path + self.db_path.parent.mkdir(parents=True, exist_ok=True) + with self.connect() as conn: + conn.execute( + """ + CREATE TABLE IF NOT EXISTS embedding_cache ( + provider TEXT NOT NULL, + model TEXT NOT NULL, + text_hash TEXT NOT NULL, + dimension INTEGER NOT NULL, + vector_blob BLOB, + vector_json TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY(provider, model, text_hash) + ) + """ + ) + conn.commit() + + def connect(self) -> sqlite3.Connection: + conn = sqlite3.connect(self.db_path) + conn.row_factory = sqlite3.Row + return conn + + def embed_texts( + self, + texts: list[str], + *, + provider: str, + model: str, + embedder: Any, + batch_size: int, + ) -> list[list[float]]: + hashes = [SQLiteVecSemanticIndex.text_hash(text) for text in texts] + cached: dict[str, list[float]] = {} + with self.connect() as conn: + for text_hash in sorted(set(hashes)): + row = conn.execute( + """ + SELECT vector_blob, vector_json + FROM embedding_cache + WHERE provider = ? AND model = ? AND text_hash = ? + """, + (provider, model, text_hash), + ).fetchone() + if row is not None: + cached[text_hash] = decode_vector(row["vector_blob"], row["vector_json"]) + missing_positions = [ + index for index, text_hash in enumerate(hashes) if text_hash not in cached + ] + for start in range(0, len(missing_positions), max(1, batch_size)): + positions = missing_positions[start : start + max(1, batch_size)] + batch_texts = [texts[index] for index in positions] + vectors = embed_with_retry(embedder, batch_texts) + with self.connect() as conn: + conn.executemany( + """ + INSERT OR REPLACE INTO embedding_cache( + provider, model, text_hash, dimension, vector_blob, vector_json + ) + VALUES (?, ?, ?, ?, ?, '') + """, + [ + ( + provider, + model, + hashes[index], + len(vector), + encode_vector(vector), + ) + for index, vector in zip(positions, vectors) + ], + ) + conn.commit() + for index, vector in zip(positions, vectors): + cached[hashes[index]] = vector + return [cached[text_hash] for text_hash in hashes] + + +class OpenAIEmbeddingClient: + def __init__(self, model: str, *, dimensions: int, timeout: float): + from openai import OpenAI + + self.model = model + self.dimensions = dimensions + self.client = OpenAI( + api_key=os.environ.get("OPENAI_API_KEY"), + base_url=os.environ.get("OPENAI_BASE_URL") or None, + timeout=timeout, + ) + + def embed(self, texts: list[str]) -> list[list[float]]: + kwargs: dict[str, Any] = {"model": self.model, "input": texts} + if self.dimensions > 0: + kwargs["dimensions"] = self.dimensions + response = self.client.embeddings.create(**kwargs) + return [list(item.embedding) for item in sorted(response.data, key=lambda item: item.index)] + + +class HashEmbeddingClient: + def __init__(self, dimensions: int = 256): + self.dimensions = dimensions + + def embed(self, texts: list[str]) -> list[list[float]]: + return [self._embed_one(text) for text in texts] + + def _embed_one(self, text: str) -> list[float]: + vector = [0.0] * self.dimensions + for term in keyword_terms(text)[:256]: + digest = hashlib.blake2b(term.encode("utf-8"), digest_size=8).digest() + bucket = int.from_bytes(digest[:4], "little") % self.dimensions + sign = 1.0 if digest[4] % 2 == 0 else -1.0 + vector[bucket] += sign + norm = sum(value * value for value in vector) ** 0.5 + if norm: + vector = [value / norm for value in vector] + return vector + + +def make_embedder(provider: str, model: str, *, dimensions: int, timeout: float) -> Any: + if provider == "openai": + return OpenAIEmbeddingClient(model, dimensions=dimensions, timeout=timeout) + if provider == "hash": + return HashEmbeddingClient(dimensions=dimensions if dimensions > 0 else 256) + raise ValueError(f"unknown embedding provider: {provider}") + + +def query_text_for_channel(channel: str, query: str, projection: QueryProjection) -> str: + if channel in {"metadata", "summary"}: + return query + if channel == "entity": + return compact_join(projection.entities, limit=24) or query + if channel == "constraint": + return compact_join(projection.constraints, limit=24) or query + if channel == "relation": + return "\n".join(projection.relations) or query + raise ValueError(f"unknown semantic channel: {channel}") + + +def rank_single_semantic_channel( + channel: str, + results: list[SemanticSearchResult], +) -> list[HybridProjectionCandidate]: + rows: list[HybridProjectionCandidate] = [] + seen: set[str] = set() + for rank, result in enumerate(results, 1): + doc_id = str(result.external_id or result.file_ref) + if doc_id in seen: + continue + seen.add(doc_id) + rows.append( + HybridProjectionCandidate( + document_id=doc_id, + score=1 / (60 + rank), + sources=[{"channel": channel, "rank": rank, "distance": result.distance}], + source_type=result.source_type, + source_path=result.source_path, + title=result.title, + metadata=result.metadata, + snippet=f"{channel}_vector rank={rank}", + ) + ) + return rows + + +def aggregate_hybrid_entity_relation( + channel_hits: dict[str, list[SemanticSearchResult]], + projection: QueryProjection, +) -> list[HybridProjectionCandidate]: + by_doc: dict[str, dict[str, Any]] = {} + for channel, results in channel_hits.items(): + weight = HYBRID_ENTITY_RELATION_WEIGHTS[channel] + seen_in_channel = set() + for rank, result in enumerate(results, 1): + doc_id = str(result.external_id or result.file_ref) + if doc_id in seen_in_channel: + continue + seen_in_channel.add(doc_id) + item = by_doc.setdefault( + doc_id, + { + "document_id": doc_id, + "score": 0.0, + "sources": [], + "source_type": result.source_type, + "source_path": result.source_path, + "title": result.title, + "metadata": result.metadata, + }, + ) + item["score"] += weight * (1 / (60 + rank)) + item["sources"].append({"channel": channel, "rank": rank, "distance": result.distance}) + candidates = [] + for item in by_doc.values(): + item["score"] += exact_match_bonus(item, projection) + candidates.append( + HybridProjectionCandidate( + document_id=item["document_id"], + score=float(item["score"]), + sources=item["sources"], + source_type=item["source_type"], + source_path=item["source_path"], + title=item["title"], + metadata=item["metadata"], + snippet=hybrid_snippet(item), + ) + ) + return sorted( + candidates, + key=lambda item: ( + -item.score, + min(source["rank"] for source in item.sources), + item.document_id, + ), + ) + + +def exact_match_bonus(item: dict[str, Any], projection: QueryProjection) -> float: + haystack = json.dumps( + { + "title": item.get("title", ""), + "source_path": item.get("source_path", ""), + "metadata": item.get("metadata", {}), + }, + ensure_ascii=False, + ).lower() + terms = [*projection.entities[:8], *projection.constraints[:6]] + matched = 0 + for term in terms: + normalized = str(term).lower().strip() + if len(normalized) >= 3 and normalized in haystack: + matched += 1 + return min(0.02, matched * 0.004) + + +def hybrid_snippet(item: dict[str, Any]) -> str: + channels = ", ".join( + f"{source['channel']}@{source['rank']}" for source in item.get("sources", [])[:4] + ) + topic = str((item.get("metadata") or {}).get("topic") or "").strip() + parts = [f"hybrid_entity_relation_vector {channels}"] + if topic: + parts.append(f"topic: {topic}") + return "; ".join(parts) + + +def heuristic_query_projection(question: str) -> QueryProjection: + entities = dedupe( + [ + *identifier_terms(question), + *keyword_terms(question)[:16], + ] + )[:16] + constraints = dedupe( + [ + *extract_constraint_terms(question), + *numeric_terms(question), + ] + )[:12] + predicate = infer_query_predicate(question) + subject = entities[0] if entities else "question" + return QueryProjection( + entities=entities, + relations=[f"{subject} | {predicate} | {question}"], + constraints=constraints, + expected_answer_type=infer_answer_type(question), + ) + + +def compact_join(values: list[str], *, limit: int) -> str: + return " | ".join(values[:limit]) + + +def identifier_terms(text: str) -> list[str]: + patterns = [ + r"\b[A-Z]{2,12}-\d{2,}\b", + r"\b[A-Za-z_][A-Za-z0-9_]{2,}\b\s*(?:=|:)\s*[A-Za-z0-9_.:/-]+", + r"\b[A-Za-z][A-Za-z0-9_+-]+(?:[-_+][A-Za-z0-9]+)+\b", + r"\b[A-Z]{2,}[A-Za-z0-9_-]*\b", + ] + found: list[str] = [] + for pattern in patterns: + found.extend(match.strip() for match in re.findall(pattern, text)) + return found + + +def keyword_terms(text: str) -> list[str]: + stopwords = { + "about", + "after", + "also", + "and", + "are", + "for", + "from", + "how", + "into", + "the", + "this", + "that", + "what", + "when", + "where", + "which", + "with", + } + terms = [ + term.lower() + for term in re.findall(r"[A-Za-z][A-Za-z0-9_+-]{2,}", text) + if term.lower() not in stopwords + ] + return dedupe(terms) + + +def extract_constraint_terms(text: str) -> list[str]: + constraints = [] + for pattern in [ + r"\b(?:must|should|required|requires?|default(?:s)?|limit(?:s)?|maximum|minimum)\b[^.!?\n]{0,120}", + r"\b[A-Za-z_][A-Za-z0-9_]{2,}\s*(?:=|:)\s*[A-Za-z0-9_.:/-]+", + ]: + constraints.extend(match.strip() for match in re.findall(pattern, text, flags=re.IGNORECASE)) + return dedupe(constraints) + + +def numeric_terms(text: str) -> list[str]: + return re.findall( + r"\b\d+(?:\.\d+)?\s*(?:MiB|GiB|MB|GB|ms|sec|seconds|minutes|hours|days|%|tokens?|req/s|rps)\b", + text, + flags=re.IGNORECASE, + ) + + +def infer_query_predicate(question: str) -> str: + lowered = question.lower() + rules = [ + ("asks_default", ["default", "defaults"]), + ("asks_limit", ["limit", "maximum", "minimum", "size"]), + ("asks_cause", ["caused", "cause", "why"]), + ("asks_owner", ["who", "owner", "assigned"]), + ("asks_deadline", ["when", "deadline", "date"]), + ("asks_status", ["status", "state"]), + ("asks_requirement", ["required", "requirement", "must"]), + ] + for predicate, needles in rules: + if any(needle in lowered for needle in needles): + return predicate + return "asks_about" + + +def infer_answer_type(question: str) -> str: + lowered = question.lower() + if "how many" in lowered or "limit" in lowered or "size" in lowered: + return "number_or_limit" + if lowered.startswith("who"): + return "person_or_team" + if lowered.startswith("when"): + return "date_or_time" + if "why" in lowered or "caused" in lowered: + return "cause" + return "fact" + + +def dedupe(values: Any) -> list[str]: + seen = set() + result = [] + for value in values: + normalized = re.sub(r"\s+", " ", str(value)).strip() + key = normalized.lower() + if not normalized or key in seen: + continue + seen.add(key) + result.append(normalized) + return result + + +def normalize_text(text: str) -> str: + return re.sub(r"\s+", " ", str(text or "")).strip() + + +def embedding_cache_model_key(model: str, dimensions: int) -> str: + return f"{model}:dimensions={dimensions}" if dimensions > 0 else model + + +def embed_with_retry(embedder: Any, texts: list[str], *, max_attempts: int = 8) -> list[list[float]]: + for attempt in range(1, max_attempts + 1): + try: + return embedder.embed(texts) + except Exception: + if attempt >= max_attempts: + raise + time.sleep(min(120.0, 2.0 ** (attempt - 1))) + raise RuntimeError("unreachable embedding retry state") + + +def encode_vector(vector: list[float]) -> bytes: + return struct.pack(f"<{len(vector)}f", *vector) + + +def decode_vector(blob: bytes | None, vector_json: str | None) -> list[float]: + if blob: + if len(blob) % 4 != 0: + raise ValueError("invalid cached vector blob length") + return list(struct.unpack(f"<{len(blob) // 4}f", blob)) + if vector_json: + value = json.loads(vector_json) + if isinstance(value, list): + return [float(item) for item in value] + raise ValueError("cached embedding row does not contain a vector") diff --git a/pageindex/filesystem/metadata.py b/pageindex/filesystem/metadata.py new file mode 100644 index 000000000..2766282ec --- /dev/null +++ b/pageindex/filesystem/metadata.py @@ -0,0 +1,152 @@ +from __future__ import annotations + +import json +import re +from typing import Any + +from .types import MetadataField + + +class MetadataQueryError(ValueError): + pass + + +class MetadataQueryEngine: + FIELD_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_]*$") + OPERATORS = {"$eq", "$ne", "$in", "$gt", "$gte", "$lt", "$lte", "$contains"} + LOGICAL_OPERATORS = {"$and", "$or"} + MAX_DEPTH = 5 + + def __init__(self, store: Any): + self.store = store + + def register_schema(self, schema: dict[str, Any], source: str = "manual") -> None: + fields = [] + raw_fields = schema.get("fields", schema) + if not isinstance(raw_fields, dict): + raise MetadataQueryError("metadata schema must contain a fields object") + for name, declaration in raw_fields.items(): + name = str(name) + self.validate_field_name(name) + if isinstance(declaration, str): + field_type = declaration + description = "" + elif isinstance(declaration, dict): + field_type = str(declaration.get("type", "")) + description = str(declaration.get("description", "")) + else: + raise MetadataQueryError(f"Invalid schema declaration for field: {name}") + if field_type not in {"string", "number", "boolean"}: + raise MetadataQueryError(f"Unsupported metadata field type for {name}: {field_type}") + fields.append( + MetadataField( + name=name, + field_type=field_type, + description=description, + source=source, + ) + ) + if fields: + self.store.upsert_metadata_fields(fields) + + def parse_filter(self, value: str | dict[str, Any] | None) -> dict[str, Any] | None: + if value is None or value == "": + return None + if isinstance(value, str): + value = self.parse_dsl(value) + if not isinstance(value, dict): + raise MetadataQueryError("metadata_filter must be a JSON object") + self.validate_filter(value) + return value + + def parse_dsl(self, dsl: str) -> dict[str, Any]: + try: + parsed = json.loads(dsl) + except json.JSONDecodeError as exc: + raise MetadataQueryError( + "metadata DSL must be a JSON object, for example " + '\'{"$and":[{"repo":"redwood"},{"year":{"$gte":2024}}]}\'' + ) from exc + if not isinstance(parsed, dict): + raise MetadataQueryError("metadata DSL must be a JSON object") + return parsed + + def validate_filter(self, metadata_filter: dict[str, Any], depth: int = 1) -> None: + if depth > self.MAX_DEPTH: + raise MetadataQueryError(f"metadata_filter nesting depth exceeds {self.MAX_DEPTH}") + if not metadata_filter: + return + for key, condition in metadata_filter.items(): + if key in self.LOGICAL_OPERATORS: + self._validate_logical(key, condition, depth) + continue + self.validate_field(key) + self._validate_field_condition(key, condition) + + def _validate_logical(self, operator: str, condition: Any, depth: int) -> None: + if not isinstance(condition, list) or not condition: + raise MetadataQueryError(f"{operator} requires a non-empty list") + for item in condition: + if not isinstance(item, dict): + raise MetadataQueryError(f"{operator} items must be metadata filter objects") + self.validate_filter(item, depth + 1) + + def _validate_field_condition(self, field: str, condition: Any) -> None: + if not isinstance(condition, dict) or not any( + str(key).startswith("$") for key in condition + ): + self._validate_scalar(condition, context=field) + return + if len(condition) != 1: + raise MetadataQueryError( + f"Field {field} condition must contain exactly one metadata operator" + ) + operator, expected = next(iter(condition.items())) + if operator not in self.OPERATORS: + raise MetadataQueryError(f"Unsupported metadata operator: {operator}") + if operator == "$in": + if not isinstance(expected, list): + raise MetadataQueryError(f"{field} $in requires a list") + for item in expected: + self._validate_scalar(item, context=f"{field} $in") + return + if operator == "$contains": + self._validate_scalar(expected, context=f"{field} $contains") + return + if operator in {"$gt", "$gte", "$lt", "$lte"}: + self._validate_range_value(expected, context=f"{field} {operator}") + return + self._validate_scalar(expected, context=f"{field} {operator}") + + def validate_field(self, field: str) -> None: + self.validate_field_name(field) + if not self.store.metadata_field_exists(field): + raise MetadataQueryError(f"Unknown metadata field: {field}") + + def validate_field_name(self, field: str) -> None: + if not self.FIELD_RE.match(field): + raise MetadataQueryError(f"Invalid metadata field: {field}") + + def export_schema(self) -> dict[str, Any]: + fields = {} + for field in self.store.list_metadata_fields(): + fields[field.name] = { + "type": field.field_type, + "description": field.description, + } + return {"fields": fields} + + @staticmethod + def _validate_scalar(value: Any, *, context: str) -> None: + if isinstance(value, bool): + return + if isinstance(value, (int, float)): + return + if isinstance(value, str): + return + raise MetadataQueryError(f"{context} must be a string, number, or boolean") + + @staticmethod + def _validate_range_value(value: Any, *, context: str) -> None: + if isinstance(value, bool) or not isinstance(value, (int, float, str)): + raise MetadataQueryError(f"{context} must be a string or number") diff --git a/pageindex/filesystem/metadata_generation.py b/pageindex/filesystem/metadata_generation.py new file mode 100644 index 000000000..19354556e --- /dev/null +++ b/pageindex/filesystem/metadata_generation.py @@ -0,0 +1,139 @@ +from __future__ import annotations + +import json +import os +from dataclasses import dataclass, field +from typing import Any, Protocol + + +GENERATED_METADATA_FIELDS = ("summary", "doc_type", "domain", "topic", "entity", "relation") + + +class MetadataGenerationError(RuntimeError): + pass + + +@dataclass(frozen=True) +class MetadataGenerationInput: + file_ref: str + external_id: str | None + title: str + source_path: str + content_type: str + source_type: str | None + text: str + metadata: dict[str, Any] = field(default_factory=dict) + text_artifact_path: str | None = None + + +@dataclass(frozen=True) +class MetadataGenerationResult: + values: dict[str, Any] = field(default_factory=dict) + failures: dict[str, str] = field(default_factory=dict) + + +class MetadataGenerator(Protocol): + def generate( + self, + request: MetadataGenerationInput, + *, + fields: list[str], + ) -> MetadataGenerationResult | dict[str, Any]: + ... + + +class OpenAIMetadataGenerator: + """Default product generator for retrieval metadata. + + This intentionally lives under pageindex.filesystem instead of benchmark + paths. It uses registered text today; callers can pass PageIndex-extracted + text through the same MetadataGenerationInput without changing the API. + """ + + def __init__( + self, + *, + model: str | None = None, + base_url: str | None = None, + max_text_chars: int = 24000, + ): + self.model = model or os.environ.get("PIFS_METADATA_MODEL", "gpt-5-nano") + self.base_url = base_url if base_url is not None else os.environ.get("OPENAI_BASE_URL") + self.max_text_chars = max_text_chars + + def generate( + self, + request: MetadataGenerationInput, + *, + fields: list[str], + ) -> MetadataGenerationResult: + api_key = os.environ.get("OPENAI_API_KEY") + if not api_key: + raise MetadataGenerationError("OPENAI_API_KEY is required for PIFS metadata generation") + + from openai import OpenAI + + client = OpenAI(api_key=api_key, base_url=self.base_url or None) + response = client.chat.completions.create( + model=self.model, + messages=[ + { + "role": "system", + "content": ( + "Generate grounded retrieval metadata for one document. " + "Use only the provided document text and ordinary source metadata. " + "The summary must be a retrieval summary, not a title rewrite. " + "Do not use filenames, paths, URLs, storage URIs, or outside knowledge. " + "Return strict JSON matching the requested fields." + ), + }, + { + "role": "user", + "content": json.dumps( + { + "requested_fields": fields, + "document": { + "title": request.title, + "source_type": request.source_type, + "content_type": request.content_type, + "metadata": request.metadata, + "text": request.text[: self.max_text_chars], + }, + }, + ensure_ascii=False, + ), + }, + ], + response_format=self._response_format(fields), + ) + content = response.choices[0].message.content or "{}" + values = json.loads(content) + return MetadataGenerationResult( + values={field: values[field] for field in fields if field in values}, + ) + + @staticmethod + def _response_format(fields: list[str]) -> dict[str, Any]: + properties: dict[str, Any] = {} + for field in fields: + if field in {"summary", "doc_type", "domain", "topic"}: + properties[field] = {"type": "string"} + elif field in {"entity", "relation"}: + properties[field] = {"type": "string"} + else: + raise MetadataGenerationError( + f"OpenAIMetadataGenerator does not support generated metadata field: {field}" + ) + return { + "type": "json_schema", + "json_schema": { + "name": "pifs_metadata_generation", + "strict": True, + "schema": { + "type": "object", + "additionalProperties": False, + "required": fields, + "properties": properties, + }, + }, + } diff --git a/pageindex/filesystem/projection_indexing.py b/pageindex/filesystem/projection_indexing.py new file mode 100644 index 000000000..5c07ca0b6 --- /dev/null +++ b/pageindex/filesystem/projection_indexing.py @@ -0,0 +1,131 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from .hybrid_projection import ( + EmbeddingCache, + INDEX_BY_CHANNEL, + embedding_cache_model_key, + make_embedder, +) +from .semantic_index import SQLiteVecSemanticIndex, SemanticIndexRecord + + +class SummaryProjectionIndexer: + """Synchronous register-time summary projection indexer.""" + + def __init__( + self, + index_dir: str | Path, + *, + embedder: Any, + embedding_provider: str, + embedding_model: str, + embedding_dimensions: int = 256, + embedding_cache_path: str | Path | None = None, + ) -> None: + self.index_dir = Path(index_dir).expanduser() + self.index_dir.mkdir(parents=True, exist_ok=True) + self.embedder = embedder + self.embedding_provider = embedding_provider + self.embedding_model = embedding_model + self.embedding_dimensions = embedding_dimensions + self.cache_model = embedding_cache_model_key(embedding_model, embedding_dimensions) + self.embedding_cache = EmbeddingCache( + Path(embedding_cache_path).expanduser() + if embedding_cache_path is not None + else self.index_dir / "embedding_cache.sqlite" + ) + self.index = SQLiteVecSemanticIndex( + self.index_dir / f"{INDEX_BY_CHANNEL['summary']}.sqlite" + ) + self._ensure_index() + + @classmethod + def from_provider( + cls, + index_dir: str | Path, + *, + embedding_provider: str = "openai", + embedding_model: str = "text-embedding-3-small", + embedding_dimensions: int = 256, + embedding_timeout: float = 60, + **kwargs: Any, + ) -> "SummaryProjectionIndexer": + return cls( + index_dir, + embedder=make_embedder( + embedding_provider, + embedding_model, + dimensions=embedding_dimensions, + timeout=embedding_timeout, + ), + embedding_provider=embedding_provider, + embedding_model=embedding_model, + embedding_dimensions=embedding_dimensions, + **kwargs, + ) + + def upsert_summary(self, record: dict[str, Any]) -> dict[str, Any]: + summary = str((record.get("derived_metadata") or {}).get("summary") or "").strip() + if not summary: + return {"status": "skipped", "reason": "missing_summary"} + vector = self.embedding_cache.embed_texts( + [summary], + provider=self.embedding_provider, + model=self.cache_model, + embedder=self.embedder, + batch_size=1, + )[0] + metadata = dict(record.get("metadata") or {}) + metadata.update(record.get("derived_metadata") or {}) + count = self.index.upsert_many( + [ + SemanticIndexRecord( + file_ref=str(record["file_ref"]), + vector=vector, + text=summary, + external_id=record.get("external_id"), + source_type=str(record.get("source_type") or ""), + source_path=str(record.get("source_path") or ""), + title=str(record.get("title") or ""), + metadata=metadata, + ) + ] + ) + return { + "status": "ready", + "indexed_rows": count, + "index_path": str(self.index.db_path), + "embedding_provider": self.embedding_provider, + "embedding_model": self.embedding_model, + "embedding_dimensions": self.embedding_dimensions, + } + + def _ensure_index(self) -> None: + if not self.index.db_path.exists(): + self.index.reset( + dimension=self.embedding_dimensions, + metadata=self._index_metadata(), + ) + return + try: + if self.index.dimension() != self.embedding_dimensions: + self.index.reset( + dimension=self.embedding_dimensions, + metadata=self._index_metadata(), + ) + except Exception: + self.index.reset( + dimension=self.embedding_dimensions, + metadata=self._index_metadata(), + ) + + def _index_metadata(self) -> dict[str, Any]: + return { + "channel": "summary", + "embedding_provider": self.embedding_provider, + "embedding_model": self.embedding_model, + "embedding_dimensions": self.embedding_dimensions, + } diff --git a/pageindex/filesystem/semantic_folder_policy.py b/pageindex/filesystem/semantic_folder_policy.py new file mode 100644 index 000000000..8e81d5f9a --- /dev/null +++ b/pageindex/filesystem/semantic_folder_policy.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import re +from typing import Any, Iterable + + +SEMANTIC_FOLDER_ROOT = "/semantic" +SEMANTIC_FOLDER_BASE_FIELDS = {"doc_type", "domain", "topic"} +SEMANTIC_FOLDER_SYSTEM_FIELDS = {"source_type"} +SEMANTIC_FOLDER_FORBIDDEN_FIELDS = { + "summary", + "entities", + "relations", + "constraints", + "retrieval_cues", + "dataset_doc_uuid", + "path", + "uri", + "source_path", + "storage_uri", + "title", + "content_type", + "created_at", + "updated_at", +} + + +def canonical_semantic_folder_field_name(value: Any) -> str: + text = str(value or "").strip() + if not text: + return "" + text = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", text) + text = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", text) + return re.sub(r"[^A-Za-z0-9]+", "_", text).strip("_").casefold() + + +def compact_semantic_folder_field_name(value: Any) -> str: + return re.sub(r"[^a-z0-9]+", "", canonical_semantic_folder_field_name(value)) + + +def semantic_folder_field_identity_keys(value: Any) -> frozenset[str]: + canonical = canonical_semantic_folder_field_name(value) + compact = compact_semantic_folder_field_name(value) + return frozenset(key for key in (canonical, compact) if key) + + +def semantic_folder_field_identity_set(fields: Iterable[Any]) -> frozenset[str]: + keys: set[str] = set() + for field in fields: + keys.update(semantic_folder_field_identity_keys(field)) + return frozenset(keys) + + +SEMANTIC_FOLDER_FORBIDDEN_FIELD_IDENTITIES = semantic_folder_field_identity_set( + SEMANTIC_FOLDER_FORBIDDEN_FIELDS +) + + +def is_semantic_folder_forbidden_field(value: Any) -> bool: + return bool( + semantic_folder_field_identity_keys(value) + & SEMANTIC_FOLDER_FORBIDDEN_FIELD_IDENTITIES + ) + + +def semantic_folder_allowed_extension_fields(fields: Iterable[Any]) -> set[str]: + allowed = set() + for field in fields: + name = canonical_semantic_folder_field_name(field) + if name and not is_semantic_folder_forbidden_field(field): + allowed.add(name) + return allowed diff --git a/pageindex/filesystem/semantic_index.py b/pageindex/filesystem/semantic_index.py new file mode 100644 index 000000000..2453e1f35 --- /dev/null +++ b/pageindex/filesystem/semantic_index.py @@ -0,0 +1,362 @@ +from __future__ import annotations + +import hashlib +import json +import sqlite3 +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Protocol + +import sqlite_vec + + +class SemanticIndexError(RuntimeError): + pass + + +@dataclass(frozen=True) +class SemanticIndexRecord: + file_ref: str + vector: list[float] + text: str + external_id: str | None = None + source_type: str = "" + source_path: str = "" + title: str = "" + metadata: dict[str, Any] | None = None + + +@dataclass(frozen=True) +class SemanticSearchResult: + file_ref: str + distance: float + external_id: str | None + source_type: str + source_path: str + title: str + text_hash: str + metadata: dict[str, Any] + + +class RebuildableSemanticIndex(Protocol): + def reset(self, *, dimension: int, metadata: dict[str, Any] | None = None) -> None: + ... + + def upsert_many(self, records: list[SemanticIndexRecord]) -> int: + ... + + def search( + self, + vector: list[float], + *, + limit: int = 10, + filters: dict[str, Any] | None = None, + fetch_multiplier: int = 20, + ) -> list[SemanticSearchResult]: + ... + + def info(self) -> dict[str, Any]: + ... + + +class SQLiteVecSemanticIndex: + """Rebuildable local semantic index backed by sqlite-vec. + + This is intentionally separate from the PIFS catalog tables. The catalog + remains source of truth; this file is a rebuildable recall index. + """ + + def __init__(self, db_path: str | Path): + self.db_path = Path(db_path).expanduser() + self.db_path.parent.mkdir(parents=True, exist_ok=True) + + def reset(self, *, dimension: int, metadata: dict[str, Any] | None = None) -> None: + if dimension <= 0: + raise SemanticIndexError("semantic index dimension must be positive") + with self.connect() as conn: + conn.executescript( + """ + DROP TABLE IF EXISTS semantic_index_vec; + DROP TABLE IF EXISTS semantic_index_docs; + DROP TABLE IF EXISTS semantic_index_config; + CREATE TABLE semantic_index_config ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ); + CREATE TABLE semantic_index_docs ( + rowid INTEGER PRIMARY KEY, + file_ref TEXT NOT NULL UNIQUE, + external_id TEXT, + source_type TEXT NOT NULL DEFAULT '', + source_path TEXT NOT NULL DEFAULT '', + title TEXT NOT NULL DEFAULT '', + text_hash TEXT NOT NULL, + text_chars INTEGER NOT NULL DEFAULT 0, + metadata_json TEXT NOT NULL DEFAULT '{}', + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP + ); + CREATE INDEX idx_semantic_index_docs_file_ref + ON semantic_index_docs(file_ref); + CREATE INDEX idx_semantic_index_docs_external_id + ON semantic_index_docs(external_id); + CREATE INDEX idx_semantic_index_docs_source_type + ON semantic_index_docs(source_type); + """ + ) + conn.execute( + "CREATE VIRTUAL TABLE semantic_index_vec USING " + f"vec0(source_type TEXT partition key, embedding float[{dimension}])" + ) + config = { + "dimension": str(dimension), + "adapter": "sqlite-vec", + "adapter_version": sqlite_vec.__version__, + "metadata": json.dumps(metadata or {}, ensure_ascii=False, sort_keys=True), + } + conn.executemany( + "INSERT INTO semantic_index_config(key, value) VALUES (?, ?)", + sorted(config.items()), + ) + conn.commit() + + def upsert_many(self, records: list[SemanticIndexRecord]) -> int: + if not records: + return 0 + dimension = self.dimension() + with self.connect() as conn: + inserted = 0 + for record in records: + if len(record.vector) != dimension: + raise SemanticIndexError( + f"vector dimension mismatch for {record.file_ref}: " + f"expected {dimension}, got {len(record.vector)}" + ) + rowid = self._upsert_doc(conn, record) + conn.execute("DELETE FROM semantic_index_vec WHERE rowid = ?", (rowid,)) + conn.execute( + "INSERT INTO semantic_index_vec(rowid, source_type, embedding) VALUES (?, ?, ?)", + ( + rowid, + record.source_type, + sqlite_vec.serialize_float32(record.vector), + ), + ) + inserted += 1 + conn.commit() + return inserted + + def search( + self, + vector: list[float], + *, + limit: int = 10, + filters: dict[str, Any] | None = None, + fetch_multiplier: int = 20, + ) -> list[SemanticSearchResult]: + dimension = self.dimension() + if len(vector) != dimension: + raise SemanticIndexError( + f"query vector dimension mismatch: expected {dimension}, got {len(vector)}" + ) + fetch_k = min(4096, max(limit, limit * max(fetch_multiplier, 1))) + source_types = _source_type_filters(filters or {}) + with self.connect() as conn: + rows = [] + if source_types: + for source_type in source_types: + rows.extend( + conn.execute( + """ + SELECT + d.file_ref, + d.external_id, + d.source_type, + d.source_path, + d.title, + d.text_hash, + d.metadata_json, + v.distance + FROM semantic_index_vec v + JOIN semantic_index_docs d ON d.rowid = v.rowid + WHERE v.embedding MATCH ? AND k = ? AND v.source_type = ? + ORDER BY v.distance + """, + (sqlite_vec.serialize_float32(vector), fetch_k, source_type), + ).fetchall() + ) + rows.sort(key=lambda row: float(row["distance"])) + else: + rows = conn.execute( + """ + SELECT + d.file_ref, + d.external_id, + d.source_type, + d.source_path, + d.title, + d.text_hash, + d.metadata_json, + v.distance + FROM semantic_index_vec v + JOIN semantic_index_docs d ON d.rowid = v.rowid + WHERE v.embedding MATCH ? AND k = ? + ORDER BY v.distance + """, + (sqlite_vec.serialize_float32(vector), fetch_k), + ).fetchall() + results: list[SemanticSearchResult] = [] + for row in rows: + metadata = _json_obj(row["metadata_json"]) + if not _matches_filters(row, metadata, filters or {}): + continue + results.append( + SemanticSearchResult( + file_ref=row["file_ref"], + distance=float(row["distance"]), + external_id=row["external_id"], + source_type=row["source_type"], + source_path=row["source_path"], + title=row["title"], + text_hash=row["text_hash"], + metadata=metadata, + ) + ) + if len(results) >= limit: + break + return results + + def info(self) -> dict[str, Any]: + with self.connect() as conn: + config = { + row["key"]: row["value"] + for row in conn.execute( + "SELECT key, value FROM semantic_index_config ORDER BY key" + ).fetchall() + } + count = conn.execute("SELECT COUNT(*) FROM semantic_index_docs").fetchone()[0] + parsed_metadata: dict[str, Any] + try: + parsed_metadata = json.loads(config.get("metadata", "{}")) + except json.JSONDecodeError: + parsed_metadata = {} + return { + "db_path": str(self.db_path), + "adapter": config.get("adapter", "sqlite-vec"), + "adapter_version": config.get("adapter_version", ""), + "dimension": int(config.get("dimension", "0") or 0), + "document_count": count, + "metadata": parsed_metadata, + } + + def dimension(self) -> int: + with self.connect() as conn: + row = conn.execute( + "SELECT value FROM semantic_index_config WHERE key = 'dimension'" + ).fetchone() + if row is None: + raise SemanticIndexError( + f"semantic index is not initialized; call reset() first: {self.db_path}" + ) + return int(row["value"]) + + def connect(self) -> sqlite3.Connection: + conn = sqlite3.connect(self.db_path) + conn.row_factory = sqlite3.Row + conn.enable_load_extension(True) + sqlite_vec.load(conn) + conn.enable_load_extension(False) + return conn + + @staticmethod + def text_hash(text: str) -> str: + return hashlib.sha256(text.encode("utf-8")).hexdigest() + + @staticmethod + def _upsert_doc(conn: sqlite3.Connection, record: SemanticIndexRecord) -> int: + existing = conn.execute( + "SELECT rowid FROM semantic_index_docs WHERE file_ref = ?", + (record.file_ref,), + ).fetchone() + metadata_json = json.dumps(record.metadata or {}, ensure_ascii=False, sort_keys=True) + text_hash = SQLiteVecSemanticIndex.text_hash(record.text) + if existing is None: + cursor = conn.execute( + """ + INSERT INTO semantic_index_docs( + file_ref, external_id, source_type, source_path, title, + text_hash, text_chars, metadata_json + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + record.file_ref, + record.external_id, + record.source_type, + record.source_path, + record.title, + text_hash, + len(record.text), + metadata_json, + ), + ) + return int(cursor.lastrowid) + rowid = int(existing["rowid"]) + conn.execute( + """ + UPDATE semantic_index_docs + SET external_id = ?, + source_type = ?, + source_path = ?, + title = ?, + text_hash = ?, + text_chars = ?, + metadata_json = ?, + updated_at = CURRENT_TIMESTAMP + WHERE rowid = ? + """, + ( + record.external_id, + record.source_type, + record.source_path, + record.title, + text_hash, + len(record.text), + metadata_json, + rowid, + ), + ) + return rowid + + +def _json_obj(text: str | None) -> dict[str, Any]: + if not text: + return {} + try: + value = json.loads(text) + except json.JSONDecodeError: + return {} + return value if isinstance(value, dict) else {} + + +def _matches_filters( + row: sqlite3.Row, + metadata: dict[str, Any], + filters: dict[str, Any], +) -> bool: + for key, expected in filters.items(): + actual = row[key] if key in row.keys() else metadata.get(key) + if isinstance(expected, list): + if str(actual) not in {str(item) for item in expected}: + return False + elif str(actual) != str(expected): + return False + return True + + +def _source_type_filters(filters: dict[str, Any]) -> list[str]: + value = filters.get("source_type") + if value is None: + return [] + if isinstance(value, list): + return [str(item) for item in value if str(item)] + return [str(value)] if str(value) else [] diff --git a/pageindex/filesystem/store.py b/pageindex/filesystem/store.py new file mode 100644 index 000000000..d164eb4fa --- /dev/null +++ b/pageindex/filesystem/store.py @@ -0,0 +1,2020 @@ +from __future__ import annotations + +import hashlib +import json +import re +import sqlite3 +from pathlib import Path +from typing import Any, Iterable, Optional + +from .types import FileEntry, MetadataField + +SCHEMA_VERSION = 4 + + +class SQLiteFileSystemStore: + def __init__(self, workspace: str | Path): + self.workspace = Path(workspace).expanduser() + self.workspace.mkdir(parents=True, exist_ok=True) + self.db_path = self.workspace / "filesystem.sqlite" + self.text_dir = self.workspace / "artifacts" / "text" + self.raw_dir = self.workspace / "artifacts" / "raw" + self.pageindex_client_dir = self.workspace / "artifacts" / "pageindex_client" + for path in (self.text_dir, self.raw_dir, self.pageindex_client_dir): + path.mkdir(parents=True, exist_ok=True) + self.migrate() + + def connect(self) -> sqlite3.Connection: + conn = sqlite3.connect(self.db_path) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA foreign_keys = ON") + return conn + + def migrate(self) -> None: + with self.connect() as conn: + version = conn.execute("PRAGMA user_version").fetchone()[0] + if version < 1: + self._migrate_to_v1(conn) + conn.execute("PRAGMA user_version = 1") + version = 1 + if version < 2: + self._migrate_to_v2(conn) + conn.execute("PRAGMA user_version = 2") + version = 2 + if version < 3: + self._migrate_to_v3(conn) + conn.execute("PRAGMA user_version = 3") + version = 3 + if version < 4: + self._migrate_to_v4(conn) + conn.execute(f"PRAGMA user_version = {SCHEMA_VERSION}") + + def _migrate_to_v1(self, conn: sqlite3.Connection) -> None: + self._migrate_legacy_tables(conn) + conn.executescript( + """ + CREATE TABLE IF NOT EXISTS files ( + file_ref TEXT PRIMARY KEY, + external_id TEXT, + storage_uri TEXT NOT NULL, + source_path TEXT NOT NULL, + title TEXT NOT NULL, + descriptor TEXT NOT NULL, + content_type TEXT NOT NULL, + source_type TEXT, + fingerprint TEXT NOT NULL, + text_artifact_path TEXT NOT NULL, + raw_artifact_path TEXT, + pageindex_doc_id TEXT, + pageindex_tree_status TEXT NOT NULL DEFAULT 'not_built', + metadata_json TEXT NOT NULL DEFAULT '{}', + derived_metadata_json TEXT NOT NULL DEFAULT '{}', + metadata_generation_json TEXT NOT NULL DEFAULT '{}', + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP, + deleted_at TEXT + ); + + CREATE TABLE IF NOT EXISTS folders ( + folder_id TEXT PRIMARY KEY, + parent_id TEXT, + name TEXT NOT NULL, + path TEXT NOT NULL UNIQUE, + description TEXT NOT NULL DEFAULT '', + kind TEXT NOT NULL DEFAULT 'manual', + metadata_json TEXT NOT NULL DEFAULT '{}', + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(parent_id) REFERENCES folders(folder_id) + ); + + CREATE TABLE IF NOT EXISTS file_folders ( + file_ref TEXT NOT NULL, + folder_id TEXT NOT NULL, + metadata_json TEXT NOT NULL DEFAULT '{}', + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (file_ref, folder_id), + FOREIGN KEY(file_ref) REFERENCES files(file_ref) ON DELETE CASCADE, + FOREIGN KEY(folder_id) REFERENCES folders(folder_id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS metadata_schema ( + schema_id TEXT PRIMARY KEY, + scope_path TEXT, + version INTEGER NOT NULL DEFAULT 1, + status TEXT NOT NULL DEFAULT 'active', + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP + ); + + CREATE TABLE IF NOT EXISTS metadata_fields ( + field_id TEXT PRIMARY KEY, + schema_id TEXT NOT NULL DEFAULT 'default', + name TEXT NOT NULL, + type TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + indexed INTEGER NOT NULL DEFAULT 1, + faceted INTEGER NOT NULL DEFAULT 0, + sortable INTEGER NOT NULL DEFAULT 0, + source TEXT NOT NULL DEFAULT 'manual', + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP, + UNIQUE(schema_id, name), + FOREIGN KEY(schema_id) REFERENCES metadata_schema(schema_id) + ); + + CREATE TABLE IF NOT EXISTS metadata_values ( + file_ref TEXT NOT NULL, + field_id TEXT NOT NULL, + value_text TEXT, + value_number REAL, + value_bool INTEGER, + value_json TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(file_ref) REFERENCES files(file_ref) ON DELETE CASCADE, + FOREIGN KEY(field_id) REFERENCES metadata_fields(field_id) ON DELETE CASCADE + ); + + CREATE VIRTUAL TABLE IF NOT EXISTS file_fts + USING fts5(file_ref UNINDEXED, title, body, metadata_text); + + CREATE INDEX IF NOT EXISTS idx_files_external_id ON files(external_id); + CREATE INDEX IF NOT EXISTS idx_files_source_path ON files(source_path); + CREATE INDEX IF NOT EXISTS idx_files_source_type ON files(source_type); + CREATE INDEX IF NOT EXISTS idx_folders_path ON folders(path); + CREATE INDEX IF NOT EXISTS idx_folders_parent_id ON folders(parent_id); + CREATE INDEX IF NOT EXISTS idx_file_folders_folder ON file_folders(folder_id); + CREATE INDEX IF NOT EXISTS idx_metadata_fields_name ON metadata_fields(name); + CREATE INDEX IF NOT EXISTS idx_metadata_values_field_text ON metadata_values(field_id, value_text); + CREATE INDEX IF NOT EXISTS idx_metadata_values_field_number ON metadata_values(field_id, value_number); + """ + ) + conn.execute( + """ + INSERT OR IGNORE INTO metadata_schema(schema_id, scope_path, version, status) + VALUES ('default', NULL, 1, 'active') + """ + ) + self.ensure_folder(conn, "/") + self._backfill_legacy_memberships(conn) + self._backfill_metadata_values(conn) + + def _migrate_to_v2(self, conn: sqlite3.Connection) -> None: + if "folders" in self._tables(conn): + columns = self._columns(conn, "folders") + if "description" not in columns: + conn.execute("ALTER TABLE folders ADD COLUMN description TEXT NOT NULL DEFAULT ''") + if "metadata_fields" in self._tables(conn): + conn.execute( + """ + UPDATE metadata_fields + SET type = 'string' + WHERE type NOT IN ('string', 'number', 'boolean') + """ + ) + + def _migrate_to_v3(self, conn: sqlite3.Connection) -> None: + if "folders" in self._tables(conn): + columns = self._columns(conn, "folders") + if "metadata_json" not in columns: + conn.execute("ALTER TABLE folders ADD COLUMN metadata_json TEXT NOT NULL DEFAULT '{}'") + if "file_folders" in self._tables(conn): + columns = self._columns(conn, "file_folders") + if "membership_kind" in columns or "metadata_json" not in columns: + conn.execute("DROP INDEX IF EXISTS idx_file_folders_kind") + conn.execute("DROP INDEX IF EXISTS idx_file_folders_folder") + conn.execute( + """ + CREATE TABLE file_folders_v3 ( + file_ref TEXT NOT NULL, + folder_id TEXT NOT NULL, + metadata_json TEXT NOT NULL DEFAULT '{}', + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (file_ref, folder_id), + FOREIGN KEY(file_ref) REFERENCES files(file_ref) ON DELETE CASCADE, + FOREIGN KEY(folder_id) REFERENCES folders(folder_id) ON DELETE CASCADE + ) + """ + ) + conn.execute( + """ + INSERT OR IGNORE INTO file_folders_v3(file_ref, folder_id, metadata_json, created_at) + SELECT file_ref, folder_id, '{}', MIN(created_at) + FROM file_folders + GROUP BY file_ref, folder_id + """ + ) + conn.execute("DROP TABLE file_folders") + conn.execute("ALTER TABLE file_folders_v3 RENAME TO file_folders") + elif "metadata_json" not in columns: + conn.execute("ALTER TABLE file_folders ADD COLUMN metadata_json TEXT NOT NULL DEFAULT '{}'") + else: + conn.execute( + """ + CREATE TABLE file_folders ( + file_ref TEXT NOT NULL, + folder_id TEXT NOT NULL, + metadata_json TEXT NOT NULL DEFAULT '{}', + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (file_ref, folder_id), + FOREIGN KEY(file_ref) REFERENCES files(file_ref) ON DELETE CASCADE, + FOREIGN KEY(folder_id) REFERENCES folders(folder_id) ON DELETE CASCADE + ) + """ + ) + conn.execute("CREATE INDEX IF NOT EXISTS idx_file_folders_folder ON file_folders(folder_id)") + + def _migrate_to_v4(self, conn: sqlite3.Connection) -> None: + if "files" not in self._tables(conn): + return + columns = self._columns(conn, "files") + if "derived_metadata_json" not in columns: + conn.execute("ALTER TABLE files ADD COLUMN derived_metadata_json TEXT NOT NULL DEFAULT '{}'") + if "metadata_generation_json" not in columns: + conn.execute("ALTER TABLE files ADD COLUMN metadata_generation_json TEXT NOT NULL DEFAULT '{}'") + self._backfill_metadata_values(conn) + + def _migrate_legacy_tables(self, conn: sqlite3.Connection) -> None: + tables = self._tables(conn) + if "folders" in tables and "folder_id" not in self._columns(conn, "folders"): + conn.execute("ALTER TABLE folders RENAME TO folders_legacy_v0") + if "files" in tables: + columns = self._columns(conn, "files") + for name, ddl in { + "raw_artifact_path": "ALTER TABLE files ADD COLUMN raw_artifact_path TEXT", + "pageindex_doc_id": "ALTER TABLE files ADD COLUMN pageindex_doc_id TEXT", + "pageindex_tree_status": ( + "ALTER TABLE files ADD COLUMN pageindex_tree_status TEXT " + "NOT NULL DEFAULT 'not_built'" + ), + "deleted_at": "ALTER TABLE files ADD COLUMN deleted_at TEXT", + }.items(): + if name not in columns: + conn.execute(ddl) + + def _backfill_legacy_memberships(self, conn: sqlite3.Connection) -> None: + if "files" not in self._tables(conn) or "folder_path" not in self._columns(conn, "files"): + return + rows = conn.execute( + "SELECT file_ref, folder_path FROM files WHERE deleted_at IS NULL" + ).fetchall() + for row in rows: + folder_id = self.ensure_folder(conn, row["folder_path"] or "/") + conn.execute( + """ + INSERT OR IGNORE INTO file_folders(file_ref, folder_id, metadata_json) + VALUES (?, ?, '{}') + """, + (row["file_ref"], folder_id), + ) + + def _backfill_metadata_values(self, conn: sqlite3.Connection) -> None: + if "files" not in self._tables(conn): + return + columns = self._columns(conn, "files") + derived_select = ( + "derived_metadata_json" + if "derived_metadata_json" in columns + else "'{}' AS derived_metadata_json" + ) + generation_select = ( + "metadata_generation_json" + if "metadata_generation_json" in columns + else "'{}' AS metadata_generation_json" + ) + rows = conn.execute( + f""" + SELECT file_ref, metadata_json, {derived_select}, {generation_select} + FROM files + WHERE deleted_at IS NULL + """ + ).fetchall() + for row in rows: + try: + metadata = json.loads(row["metadata_json"] or "{}") + except json.JSONDecodeError: + metadata = {} + try: + derived_metadata = json.loads(row["derived_metadata_json"] or "{}") + except json.JSONDecodeError: + derived_metadata = {} + try: + metadata_generation = json.loads(row["metadata_generation_json"] or "{}") + except json.JSONDecodeError: + metadata_generation = {} + self.replace_metadata_values( + conn, + row["file_ref"], + self.indexed_metadata_values(metadata, derived_metadata, metadata_generation), + ) + + @staticmethod + def _tables(conn: sqlite3.Connection) -> set[str]: + rows = conn.execute("SELECT name FROM sqlite_master WHERE type IN ('table', 'virtual table')").fetchall() + return {row["name"] for row in rows} + + @staticmethod + def _columns(conn: sqlite3.Connection, table: str) -> set[str]: + return {row["name"] for row in conn.execute(f"PRAGMA table_info({table})").fetchall()} + + def insert_file(self, record: dict[str, Any]) -> None: + self.insert_files([record]) + + def insert_files(self, records: list[dict[str, Any]]) -> None: + if not records: + return + with self.connect() as conn: + conn.execute("PRAGMA synchronous = OFF") + conn.execute("PRAGMA temp_store = MEMORY") + folder_cache: dict[tuple[str, str], str] = {} + file_rows = [] + membership_rows = [] + file_ref_rows = [] + fts_file_ref_rows = [] + fts_rows = [] + metadata_rows = [] + metadata_field_ids = { + row["name"]: row["field_id"] + for row in conn.execute( + "SELECT name, field_id FROM metadata_fields WHERE schema_id = 'default'" + ).fetchall() + } + include_folder_path = "folder_path" in self._columns(conn, "files") + for record in records: + folder_cache_key = (record["folder_path"], record.get("folder_kind", "manual")) + folder_id = folder_cache.get(folder_cache_key) + if folder_id is None: + folder_id = self.ensure_folder( + conn, + record["folder_path"], + kind=record.get("folder_kind", "manual"), + ) + folder_cache[folder_cache_key] = folder_id + file_rows.append(self._file_insert_values(record, include_folder_path=include_folder_path)) + membership_rows.append( + ( + record["file_ref"], + folder_id, + json.dumps(record.get("folder_metadata") or {}, ensure_ascii=False), + ) + ) + file_ref_rows.append((record["file_ref"],)) + if not record.get("skip_fts", False): + fts_file_ref_rows.append((record["file_ref"],)) + fts_rows.append( + ( + record["file_ref"], + record["title"], + record["content"], + record["metadata_text"], + ) + ) + metadata_rows.extend( + self._metadata_insert_values( + record["file_ref"], + record.get("indexed_metadata", record["metadata"]), + metadata_field_ids, + ) + ) + conn.executemany(self._file_insert_sql(include_folder_path=include_folder_path), file_rows) + conn.executemany( + """ + INSERT OR REPLACE INTO file_folders(file_ref, folder_id, metadata_json) + VALUES (?, ?, ?) + """, + membership_rows, + ) + conn.executemany("DELETE FROM metadata_values WHERE file_ref = ?", file_ref_rows) + if metadata_rows: + conn.executemany( + """ + INSERT INTO metadata_values( + file_ref, field_id, value_text, value_number, value_bool, value_json + ) VALUES (?, ?, ?, ?, ?, ?) + """, + metadata_rows, + ) + if fts_file_ref_rows: + conn.executemany("DELETE FROM file_fts WHERE file_ref = ?", fts_file_ref_rows) + conn.executemany( + """ + INSERT INTO file_fts(file_ref, title, body, metadata_text) + VALUES (?, ?, ?, ?) + """, + fts_rows, + ) + + @staticmethod + def _file_insert_sql(*, include_folder_path: bool) -> str: + columns = [ + "file_ref", + "external_id", + "storage_uri", + "source_path", + "title", + "descriptor", + "content_type", + "source_type", + "fingerprint", + "text_artifact_path", + "raw_artifact_path", + "pageindex_doc_id", + "pageindex_tree_status", + "metadata_json", + "derived_metadata_json", + "metadata_generation_json", + ] + if include_folder_path: + columns.append("folder_path") + columns.extend(["deleted_at", "updated_at"]) + placeholders = ", ".join(["?"] * (len(columns) - 2) + ["NULL", "CURRENT_TIMESTAMP"]) + return f""" + INSERT OR REPLACE INTO files ({", ".join(columns)}) + VALUES ({placeholders}) + """ + + @staticmethod + def _file_insert_values(record: dict[str, Any], *, include_folder_path: bool) -> tuple[Any, ...]: + values: list[Any] = [ + record["file_ref"], + record["external_id"], + record["storage_uri"], + record["source_path"], + record["title"], + record["descriptor"], + record["content_type"], + record["source_type"], + record["fingerprint"], + record["text_artifact_path"], + record["raw_artifact_path"], + record.get("pageindex_doc_id"), + record.get("pageindex_tree_status", "not_built"), + record["metadata_json"], + record.get("derived_metadata_json", "{}"), + record.get("metadata_generation_json", "{}"), + ] + if include_folder_path: + values.append(record["folder_path"]) + return tuple(values) + + def _metadata_insert_values( + self, + file_ref: str, + metadata: dict[str, Any], + metadata_field_ids: dict[str, str], + ) -> list[tuple[Any, ...]]: + values = [] + for name, value in metadata.items(): + if not self._valid_field_name(name): + continue + field_id = metadata_field_ids.get(name) + if field_id is None: + continue + for item in self._metadata_value_items(value): + values.append( + ( + file_ref, + field_id, + item["value_text"], + item["value_number"], + item["value_bool"], + item["value_json"], + ) + ) + return values + + def create_folder( + self, + path: str, + *, + kind: str = "manual", + description: str = "", + metadata: dict[str, Any] | None = None, + ) -> str: + with self.connect() as conn: + return self.ensure_folder( + conn, + path, + kind=kind, + description=description, + metadata=metadata, + ) + + def attach_file_to_folder( + self, + file_ref: str, + folder_path_or_id: str, + *, + metadata: dict[str, Any] | None = None, + ) -> None: + with self.connect() as conn: + resolved_file_ref = self._resolve_file_ref(conn, file_ref) + folder_id = self._resolve_or_create_folder(conn, folder_path_or_id) + conn.execute( + """ + INSERT INTO file_folders(file_ref, folder_id, metadata_json) + VALUES (?, ?, ?) + ON CONFLICT(file_ref, folder_id) DO UPDATE SET + metadata_json = excluded.metadata_json + """, + ( + resolved_file_ref, + folder_id, + json.dumps(metadata or {}, ensure_ascii=False), + ), + ) + + def attach_files_to_folders(self, items: list[dict[str, Any]]) -> None: + with self.connect() as conn: + for item in items: + resolved_file_ref = self._resolve_file_ref(conn, item["file_ref"]) + folder_id = self._resolve_or_create_folder(conn, item["folder"]) + conn.execute( + """ + INSERT INTO file_folders(file_ref, folder_id, metadata_json) + VALUES (?, ?, ?) + ON CONFLICT(file_ref, folder_id) DO UPDATE SET + metadata_json = excluded.metadata_json + """, + ( + resolved_file_ref, + folder_id, + json.dumps(item.get("metadata") or {}, ensure_ascii=False), + ), + ) + + def _insert_file_row(self, conn: sqlite3.Connection, record: dict[str, Any]) -> None: + current_timestamp = object() + columns = [ + "file_ref", + "external_id", + "storage_uri", + "source_path", + "title", + "descriptor", + "content_type", + "source_type", + "fingerprint", + "text_artifact_path", + "raw_artifact_path", + "pageindex_doc_id", + "pageindex_tree_status", + "metadata_json", + "derived_metadata_json", + "metadata_generation_json", + "deleted_at", + "updated_at", + ] + values: list[Any] = [ + record["file_ref"], + record["external_id"], + record["storage_uri"], + record["source_path"], + record["title"], + record["descriptor"], + record["content_type"], + record["source_type"], + record["fingerprint"], + record["text_artifact_path"], + record["raw_artifact_path"], + record.get("pageindex_doc_id"), + record.get("pageindex_tree_status", "not_built"), + record["metadata_json"], + record.get("derived_metadata_json", "{}"), + record.get("metadata_generation_json", "{}"), + None, + current_timestamp, + ] + if "folder_path" in self._columns(conn, "files"): + columns.insert(-2, "folder_path") + values.insert(-2, record["folder_path"]) + placeholders = ", ".join("CURRENT_TIMESTAMP" if value is current_timestamp else "?" for value in values) + bound_values = [value for value in values if value is not current_timestamp] + conn.execute( + f""" + INSERT OR REPLACE INTO files ({", ".join(columns)}) + VALUES ({placeholders}) + """, + bound_values, + ) + + def replace_metadata_values( + self, + conn: sqlite3.Connection, + file_ref: str, + metadata: dict[str, Any], + ) -> None: + conn.execute("DELETE FROM metadata_values WHERE file_ref = ?", (file_ref,)) + for name, value in metadata.items(): + if not self._valid_field_name(name): + continue + field_id = self._registered_field_id(conn, name) + if field_id is None: + continue + for item in self._metadata_value_items(value): + conn.execute( + """ + INSERT INTO metadata_values( + file_ref, field_id, value_text, value_number, value_bool, value_json + ) VALUES (?, ?, ?, ?, ?, ?) + """, + ( + file_ref, + field_id, + item["value_text"], + item["value_number"], + item["value_bool"], + item["value_json"], + ), + ) + + @staticmethod + def _registered_field_id(conn: sqlite3.Connection, name: str) -> str | None: + row = conn.execute( + """ + SELECT field_id + FROM metadata_fields + WHERE schema_id = 'default' AND name = ? + """, + (name,), + ).fetchone() + return None if row is None else row["field_id"] + + def replace_fts(self, conn: sqlite3.Connection, record: dict[str, Any]) -> None: + conn.execute("DELETE FROM file_fts WHERE file_ref = ?", (record["file_ref"],)) + conn.execute( + """ + INSERT INTO file_fts(file_ref, title, body, metadata_text) + VALUES (?, ?, ?, ?) + """, + ( + record["file_ref"], + record["title"], + record["content"], + record["metadata_text"], + ), + ) + + def upsert_metadata_fields( + self, + fields: Iterable[MetadataField], + *, + conn: sqlite3.Connection | None = None, + ) -> None: + owns_connection = conn is None + if conn is None: + conn = self.connect() + try: + conn.execute( + """ + INSERT OR IGNORE INTO metadata_schema(schema_id, scope_path, version, status) + VALUES ('default', NULL, 1, 'active') + """ + ) + for field in fields: + conn.execute( + """ + INSERT INTO metadata_fields( + field_id, schema_id, name, type, description, + indexed, faceted, sortable, source, updated_at + ) VALUES (?, 'default', ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(schema_id, name) DO UPDATE SET + type = excluded.type, + source = excluded.source, + updated_at = CURRENT_TIMESTAMP + """, + ( + self.field_id(field.name), + field.name, + field.field_type, + field.description, + int(field.indexed), + int(field.faceted), + int(field.sortable), + field.source, + ), + ) + if owns_connection: + conn.commit() + finally: + if owns_connection: + conn.close() + + def metadata_field_exists(self, name: str) -> bool: + with self.connect() as conn: + row = conn.execute( + "SELECT 1 FROM metadata_fields WHERE schema_id = 'default' AND name = ?", + (name,), + ).fetchone() + return row is not None + + def list_metadata_fields(self) -> list[MetadataField]: + with self.connect() as conn: + rows = conn.execute( + """ + SELECT name, type, description, indexed, faceted, sortable, source + FROM metadata_fields + WHERE schema_id = 'default' + ORDER BY name + """ + ).fetchall() + return [ + MetadataField( + name=row["name"], + field_type=row["type"], + description=row["description"], + indexed=bool(row["indexed"]), + faceted=bool(row["faceted"]), + sortable=bool(row["sortable"]), + source=row["source"], + ) + for row in rows + ] + + def list_folder(self, path: str = "/", recursive: bool = False, limit: int = 100) -> dict[str, Any]: + path = normalize_path(path) + with self.connect() as conn: + folder = self._folder_by_path(conn, path) + if folder is None: + raise KeyError(f"Unknown folder path: {path}") + if recursive: + folder_rows = conn.execute( + """ + SELECT + fo.folder_id, + fo.parent_id, + fo.name, + fo.path, + fo.description, + fo.kind, + fo.metadata_json, + fo.created_at, + fo.updated_at, + ( + SELECT COUNT(DISTINCT child_ff.file_ref) + FROM file_folders child_ff + JOIN files child_file + ON child_file.file_ref = child_ff.file_ref + AND child_file.deleted_at IS NULL + WHERE child_ff.folder_id = fo.folder_id + ) AS file_count, + ( + SELECT COUNT(*) + FROM folders child_folder + WHERE child_folder.parent_id = fo.folder_id + ) AS children_count + FROM folders fo + WHERE fo.path != ? AND (fo.path LIKE ?) + ORDER BY fo.path + LIMIT ? + """, + (path, self._descendant_like(path), limit), + ).fetchall() + file_rows = self._file_rows_for_scope(conn, path, True, limit) + else: + folder_rows = conn.execute( + """ + SELECT + fo.folder_id, + fo.parent_id, + fo.name, + fo.path, + fo.description, + fo.kind, + fo.metadata_json, + fo.created_at, + fo.updated_at, + ( + SELECT COUNT(DISTINCT child_ff.file_ref) + FROM file_folders child_ff + JOIN files child_file + ON child_file.file_ref = child_ff.file_ref + AND child_file.deleted_at IS NULL + WHERE child_ff.folder_id = fo.folder_id + ) AS file_count, + ( + SELECT COUNT(*) + FROM folders child_folder + WHERE child_folder.parent_id = fo.folder_id + ) AS children_count + FROM folders fo + WHERE fo.parent_id = ? + ORDER BY fo.kind, fo.name + LIMIT ? + """, + (folder["folder_id"], limit), + ).fetchall() + file_rows = self._file_rows_for_scope(conn, path, False, limit) + return { + "folders": [self._folder_row_to_dict(row) for row in folder_rows], + "files": [self._file_summary(row) for row in file_rows], + } + + def find_folders( + self, + path: str = "/", + *, + metadata_filter: Optional[dict[str, Any]] = None, + limit: int = 100, + ) -> list[dict[str, Any]]: + path = normalize_path(path) + metadata_sql, metadata_params = self._metadata_filter_sql(metadata_filter) + metadata_clause = f"AND {' AND '.join(metadata_sql)}" if metadata_sql else "" + sql = f""" + SELECT * + FROM ( + SELECT + fo.folder_id, + fo.parent_id, + fo.name, + fo.path, + fo.description, + fo.kind, + fo.metadata_json, + fo.created_at, + fo.updated_at, + ( + SELECT COUNT(DISTINCT child_ff.file_ref) + FROM file_folders child_ff + JOIN files child_file + ON child_file.file_ref = child_ff.file_ref + AND child_file.deleted_at IS NULL + WHERE child_ff.folder_id = fo.folder_id + ) AS file_count, + ( + SELECT COUNT(*) + FROM folders child_folder + WHERE child_folder.parent_id = fo.folder_id + ) AS children_count, + ( + SELECT COUNT(DISTINCT f.file_ref) + FROM files f + JOIN file_folders matched_ff + ON matched_ff.file_ref = f.file_ref + JOIN folders matched_folder + ON matched_folder.folder_id = matched_ff.folder_id + WHERE f.deleted_at IS NULL + AND ( + matched_folder.folder_id = fo.folder_id + OR matched_folder.path LIKE CASE + WHEN fo.path = '/' THEN '/%' + ELSE fo.path || '/%' + END + ) + {metadata_clause} + ) AS matched_files + FROM folders fo + WHERE fo.path != ? AND fo.path LIKE ? + ) + WHERE matched_files > 0 + ORDER BY path + LIMIT ? + """ + params = [*metadata_params, path, self._descendant_like(path), limit] + with self.connect() as conn: + folder = self._folder_by_path(conn, path) + if folder is None: + raise KeyError(f"Unknown folder path: {path}") + rows = conn.execute(sql, params).fetchall() + return [self._folder_row_to_dict(row) for row in rows] + + def search_files( + self, + query: str | list[str] | None, + *, + scope: Optional[dict[str, Any]] = None, + metadata_filter: Optional[dict[str, Any]] = None, + limit: int = 10, + ) -> list[dict[str, Any]]: + query_text = self._query_text(query) + match_queries = self._fts_match_queries(query_text) if query_text else [None] + results: list[dict[str, Any]] = [] + seen: set[str] = set() + for match_query in match_queries: + rows = self._search_once(match_query, scope, metadata_filter, max(limit * 25, limit)) + for row in rows: + if row["file_ref"] in seen: + continue + seen.add(row["file_ref"]) + results.append(self._search_row_to_dict(row)) + if len(results) >= limit: + return results + if results: + return results + return results + + def _search_once( + self, + match_query: str | None, + scope: Optional[dict[str, Any]], + metadata_filter: Optional[dict[str, Any]], + limit: int, + ) -> list[sqlite3.Row]: + joins = [] + selects = [ + "f.file_ref", + "f.external_id", + "f.source_path", + "f.title", + "f.descriptor", + "f.pageindex_tree_status", + "f.metadata_json", + "f.derived_metadata_json", + "f.metadata_generation_json", + "f.created_at", + """ + ( + SELECT display_folder.folder_id + FROM file_folders display_ff + JOIN folders display_folder + ON display_folder.folder_id = display_ff.folder_id + WHERE display_ff.file_ref = f.file_ref + ORDER BY display_folder.path + LIMIT 1 + ) AS folder_id + """, + """ + ( + SELECT display_folder.path + FROM file_folders display_ff + JOIN folders display_folder + ON display_folder.folder_id = display_ff.folder_id + WHERE display_ff.file_ref = f.file_ref + ORDER BY display_folder.path + LIMIT 1 + ) AS folder_path + """, + ] + where = ["f.deleted_at IS NULL"] + params: list[Any] = [] + if match_query: + joins.append("JOIN file_fts ON file_fts.file_ref = f.file_ref") + selects.append("snippet(file_fts, 2, '', '', '...', 16) AS snippet") + selects.append("bm25(file_fts) AS rank") + where.append("file_fts MATCH ?") + params.append(match_query) + order_by = "rank" + else: + selects.append("f.descriptor AS snippet") + selects.append("0 AS rank") + order_by = "f.created_at DESC, f.title" + scope_sql, scope_params = self._scope_sql(scope) + if scope_sql: + where.append(scope_sql) + params.extend(scope_params) + metadata_sql, metadata_params = self._metadata_filter_sql(metadata_filter) + where.extend(metadata_sql) + params.extend(metadata_params) + sql = f""" + SELECT {", ".join(selects)} + FROM files f + {" ".join(joins)} + WHERE {" AND ".join(where)} + ORDER BY {order_by} + LIMIT ? + """ + params.append(limit) + with self.connect() as conn: + return conn.execute(sql, params).fetchall() + + def _metadata_filter_sql(self, metadata_filter: Optional[dict[str, Any]]) -> tuple[list[str], list[Any]]: + if not metadata_filter: + return [], [] + clause, params = self._compile_metadata_filter(metadata_filter) + return [clause] if clause else [], params + + def _compile_metadata_filter(self, metadata_filter: dict[str, Any]) -> tuple[str, list[Any]]: + clauses = [] + params: list[Any] = [] + for key, condition in metadata_filter.items(): + if key in {"$and", "$or"}: + child_clauses = [] + child_params: list[Any] = [] + for item in condition: + child_clause, item_params = self._compile_metadata_filter(item) + if child_clause: + child_clauses.append(f"({child_clause})") + child_params.extend(item_params) + if child_clauses: + joiner = " AND " if key == "$and" else " OR " + clauses.append(joiner.join(child_clauses)) + params.extend(child_params) + continue + field_clause, field_params = self._compile_metadata_field_filter(key, condition) + clauses.append(field_clause) + params.extend(field_params) + return " AND ".join(f"({clause})" for clause in clauses), params + + def _compile_metadata_field_filter(self, field: str, condition: Any) -> tuple[str, list[Any]]: + if not isinstance(condition, dict) or not any(str(key).startswith("$") for key in condition): + condition = {"$eq": condition} + operator, expected = next(iter(condition.items())) + field_id = self.field_id(field) + if operator == "$eq": + return ( + """ + EXISTS ( + SELECT 1 FROM metadata_values mv + WHERE mv.file_ref = f.file_ref + AND mv.field_id = ? + AND mv.value_text = ? + ) + """, + [field_id, self._metadata_compare_text(expected)], + ) + if operator == "$ne": + return ( + """ + NOT EXISTS ( + SELECT 1 FROM metadata_values mv + WHERE mv.file_ref = f.file_ref + AND mv.field_id = ? + AND mv.value_text = ? + ) + """, + [field_id, self._metadata_compare_text(expected)], + ) + if operator == "$in": + values = [self._metadata_compare_text(item) for item in expected] + if not values: + return "0", [] + placeholders = ", ".join("?" for _ in values) + return ( + f""" + EXISTS ( + SELECT 1 FROM metadata_values mv + WHERE mv.file_ref = f.file_ref + AND mv.field_id = ? + AND mv.value_text IN ({placeholders}) + ) + """, + [field_id, *values], + ) + if operator == "$contains": + return ( + """ + EXISTS ( + SELECT 1 FROM metadata_values mv + WHERE mv.file_ref = f.file_ref + AND mv.field_id = ? + AND lower(mv.value_text) LIKE '%' || lower(?) || '%' + ) + """, + [field_id, self._metadata_compare_text(expected)], + ) + if operator in {"$gt", "$gte", "$lt", "$lte"}: + comparator = { + "$gt": ">", + "$gte": ">=", + "$lt": "<", + "$lte": "<=", + }[operator] + if isinstance(expected, (int, float)) and not isinstance(expected, bool): + return ( + f""" + EXISTS ( + SELECT 1 FROM metadata_values mv + WHERE mv.file_ref = f.file_ref + AND mv.field_id = ? + AND mv.value_number IS NOT NULL + AND mv.value_number {comparator} ? + ) + """, + [field_id, float(expected)], + ) + return ( + f""" + EXISTS ( + SELECT 1 FROM metadata_values mv + WHERE mv.file_ref = f.file_ref + AND mv.field_id = ? + AND mv.value_text {comparator} ? + ) + """, + [field_id, self._metadata_compare_text(expected)], + ) + raise ValueError(f"Unsupported metadata operator: {operator}") + + def get_file(self, file_ref: str) -> FileEntry: + with self.connect() as conn: + row = self._file_entry_row(conn, file_ref) + if row is None: + raise KeyError(f"Unknown file_ref: {file_ref}") + return self._file_entry(row) + + def list_pending_metadata_generation(self, *, limit: int | None = None) -> list[FileEntry]: + sql = """ + SELECT + f.file_ref, + f.external_id, + f.storage_uri, + f.source_path, + f.title, + f.descriptor, + f.content_type, + f.source_type, + f.fingerprint, + f.text_artifact_path, + f.raw_artifact_path, + f.pageindex_doc_id, + f.pageindex_tree_status, + f.metadata_json, + f.derived_metadata_json, + f.metadata_generation_json, + COALESCE(primary_folder.path, '/') AS folder_path + FROM files f + LEFT JOIN file_folders ff ON ff.file_ref = f.file_ref + LEFT JOIN folders primary_folder ON primary_folder.folder_id = ff.folder_id + WHERE f.deleted_at IS NULL + AND ( + f.metadata_generation_json LIKE '%pending_generate%' + OR f.metadata_generation_json LIKE '%pending_submit%' + ) + GROUP BY f.file_ref + ORDER BY f.created_at, f.file_ref + """ + params: list[Any] = [] + if limit is not None: + sql += " LIMIT ?" + params.append(int(limit)) + with self.connect() as conn: + rows = conn.execute(sql, params).fetchall() + return [self._file_entry(row) for row in rows] + + def update_file_metadata_generation( + self, + file_ref: str, + *, + derived_metadata: dict[str, Any], + metadata_generation: dict[str, Any], + ) -> None: + with self.connect() as conn: + row = self._file_entry_row(conn, file_ref) + if row is None: + raise KeyError(f"Unknown file_ref: {file_ref}") + metadata = json.loads(row["metadata_json"] or "{}") + metadata_text_value = metadata_text( + self._merge_metadata_values(metadata, derived_metadata) + ) + conn.execute( + """ + UPDATE files + SET derived_metadata_json = ?, + metadata_generation_json = ?, + updated_at = CURRENT_TIMESTAMP + WHERE file_ref = ? AND deleted_at IS NULL + """, + ( + json.dumps(derived_metadata, ensure_ascii=False), + json.dumps(metadata_generation, ensure_ascii=False), + file_ref, + ), + ) + self.replace_metadata_values( + conn, + file_ref, + self.indexed_metadata_values(metadata, derived_metadata, metadata_generation), + ) + conn.execute( + """ + UPDATE file_fts + SET metadata_text = ? + WHERE file_ref = ? + """, + (metadata_text_value, file_ref), + ) + + def resolve_file_ref(self, target: str) -> str: + with self.connect() as conn: + return self._resolve_file_ref(conn, target) + + def _resolve_file_ref(self, conn: sqlite3.Connection, target: str) -> str: + target = str(target).strip() + if not target: + raise KeyError("Empty file target") + row = conn.execute( + "SELECT file_ref FROM files WHERE file_ref = ? AND deleted_at IS NULL", + (target,), + ).fetchone() + if row: + return row["file_ref"] + row = conn.execute( + "SELECT file_ref FROM files WHERE external_id = ? AND deleted_at IS NULL", + (target,), + ).fetchone() + if row: + return row["file_ref"] + stripped = target.strip("/") + row = conn.execute( + "SELECT file_ref FROM files WHERE source_path = ? AND deleted_at IS NULL", + (stripped,), + ).fetchone() + if row: + return row["file_ref"] + row = conn.execute( + """ + SELECT f.file_ref + FROM files f + JOIN file_folders ff ON ff.file_ref = f.file_ref + JOIN folders pf ON pf.folder_id = ff.folder_id + WHERE (pf.path || '/' || f.title) = ? + OR (pf.path || '/' || f.source_path) = ? + LIMIT 1 + """, + (target, target), + ).fetchone() + if row: + return row["file_ref"] + raise KeyError(f"Unknown file target: {target}") + + def ensure_folder( + self, + conn: sqlite3.Connection | None, + path: str, + *, + kind: str = "manual", + description: str = "", + metadata: dict[str, Any] | None = None, + ) -> str: + owns_connection = conn is None + if conn is None: + conn = self.connect() + try: + normalized = normalize_path(path) + metadata_json = json.dumps(metadata or {}, ensure_ascii=False) + if normalized == "/": + folder_id = self.folder_id("/") + existing = conn.execute( + "SELECT folder_id FROM folders WHERE path = '/'" + ).fetchone() + if existing is not None and not description and metadata_json == "{}": + if owns_connection: + conn.commit() + return folder_id + self._upsert_folder_row( + conn, + folder_id=folder_id, + parent_id=None, + name="/", + path="/", + kind=kind, + description=description, + metadata_json=metadata_json, + ) + if owns_connection: + conn.commit() + return folder_id + parent_id = self.ensure_folder(conn, str(Path(normalized).parent), kind=kind) + name = normalized.rsplit("/", 1)[-1] + folder_id = self.folder_id(normalized) + self._upsert_folder_row( + conn, + folder_id=folder_id, + parent_id=parent_id, + name=name, + path=normalized, + kind=kind, + description=description, + metadata_json=metadata_json, + ) + if owns_connection: + conn.commit() + return folder_id + finally: + if owns_connection: + conn.close() + + def _upsert_folder_row( + self, + conn: sqlite3.Connection, + *, + folder_id: str, + parent_id: str | None, + name: str, + path: str, + kind: str, + description: str, + metadata_json: str, + ) -> None: + columns = self._columns(conn, "folders") + insert_columns = ["folder_id", "parent_id", "name", "path", "description", "kind", "metadata_json"] + values: list[Any] = [folder_id, parent_id, name, path, description, kind, metadata_json] + if "source" in columns: + insert_columns.append("source") + values.append("system") + if "sort_order" in columns: + insert_columns.append("sort_order") + values.append(0) + placeholders = ", ".join("?" for _ in values) + update_assignments = [ + "parent_id = excluded.parent_id", + "name = excluded.name", + "kind = excluded.kind", + "updated_at = CURRENT_TIMESTAMP", + ] + if description: + update_assignments.append("description = excluded.description") + if metadata_json != "{}": + update_assignments.append("metadata_json = excluded.metadata_json") + conn.execute( + f""" + INSERT INTO folders({", ".join(insert_columns)}) + VALUES ({placeholders}) + ON CONFLICT(path) DO UPDATE SET + {", ".join(update_assignments)} + """, + values, + ) + + def _resolve_or_create_folder(self, conn: sqlite3.Connection, folder_path_or_id: str) -> str: + target = str(folder_path_or_id).strip() + if not target: + raise KeyError("Empty folder target") + row = conn.execute( + "SELECT folder_id FROM folders WHERE folder_id = ?", + (target,), + ).fetchone() + if row: + return row["folder_id"] + row = conn.execute( + "SELECT folder_id FROM folders WHERE path = ?", + (normalize_path(target),), + ).fetchone() + if row: + return row["folder_id"] + return self.ensure_folder(conn, target) + + def read_text(self, file_ref: str) -> str: + entry = self.get_file(file_ref) + return Path(entry.text_artifact_path).read_text(encoding="utf-8") + + def write_text_artifact(self, file_ref: str, content: str) -> Path: + path = self.text_dir / f"{file_ref}.txt" + path.write_text(content, encoding="utf-8") + return path + + def update_pageindex_pointer( + self, + file_ref: str, + *, + pageindex_doc_id: str | None, + pageindex_tree_status: str, + ) -> None: + with self.connect() as conn: + resolved = self._resolve_file_ref(conn, file_ref) + conn.execute( + """ + UPDATE files + SET pageindex_doc_id = ?, + pageindex_tree_status = ?, + updated_at = CURRENT_TIMESTAMP + WHERE file_ref = ? AND deleted_at IS NULL + """, + (pageindex_doc_id, pageindex_tree_status, resolved), + ) + + def write_raw_artifact(self, file_ref: str, metadata: dict[str, Any]) -> Path: + path = self.raw_dir / f"{file_ref}.json" + path.write_text(json.dumps(metadata, ensure_ascii=False, indent=2), encoding="utf-8") + return path + + def file_info(self, target: str) -> dict[str, Any]: + file_ref = self.resolve_file_ref(target) + entry = self.get_file(file_ref) + info = self._file_entry_to_dict(entry) + info["folders"] = self.folder_memberships(file_ref) + return info + + def file_matches( + self, + file_ref: str, + *, + scope: Optional[dict[str, Any]] = None, + metadata_filter: Optional[dict[str, Any]] = None, + ) -> bool: + where = ["f.file_ref = ?", "f.deleted_at IS NULL"] + params: list[Any] = [file_ref] + scope_sql, scope_params = self._scope_sql(scope) + if scope_sql: + where.append(scope_sql) + params.extend(scope_params) + metadata_sql, metadata_params = self._metadata_filter_sql(metadata_filter) + where.extend(metadata_sql) + params.extend(metadata_params) + with self.connect() as conn: + row = conn.execute( + f""" + SELECT 1 + FROM files f + WHERE {" AND ".join(where)} + LIMIT 1 + """, + params, + ).fetchone() + return row is not None + + def folder_memberships(self, file_ref: str) -> list[dict[str, Any]]: + with self.connect() as conn: + rows = conn.execute( + """ + SELECT + fo.folder_id, + fo.parent_id, + fo.name, + fo.path, + fo.description, + fo.kind, + fo.metadata_json AS folder_metadata_json, + ff.metadata_json AS membership_metadata_json, + ff.created_at + FROM file_folders ff + JOIN folders fo ON fo.folder_id = ff.folder_id + WHERE ff.file_ref = ? + ORDER BY fo.path + """, + (file_ref,), + ).fetchall() + return [ + { + "folder_id": row["folder_id"], + "id": row["folder_id"], + "parent_id": row["parent_id"], + "parent_folder_id": row["parent_id"], + "name": row["name"], + "path": row["path"], + "kind": row["kind"], + "description": row["description"], + "folder_metadata": json.loads(row["folder_metadata_json"] or "{}"), + "metadata": json.loads(row["membership_metadata_json"] or "{}"), + "created_at": row["created_at"], + } + for row in rows + ] + + def count_files_in_folder(self, path: str, *, recursive: bool = True) -> int: + path = normalize_path(path) + with self.connect() as conn: + folder = self._folder_by_path(conn, path) + if folder is None: + raise KeyError(f"Unknown folder path: {path}") + if recursive: + row = conn.execute( + """ + SELECT COUNT(DISTINCT f.file_ref) AS count + FROM files f + JOIN file_folders ff ON ff.file_ref = f.file_ref + JOIN folders fo ON fo.folder_id = ff.folder_id + WHERE f.deleted_at IS NULL + AND (fo.path = ? OR fo.path LIKE ?) + """, + (path, self._descendant_like(path)), + ).fetchone() + else: + row = conn.execute( + """ + SELECT COUNT(DISTINCT f.file_ref) AS count + FROM files f + JOIN file_folders ff ON ff.file_ref = f.file_ref + JOIN folders fo ON fo.folder_id = ff.folder_id + WHERE f.deleted_at IS NULL + AND fo.path = ? + """, + (path,), + ).fetchone() + return int(row["count"] or 0) + + def folder_subtree_thresholds( + self, + path: str, + *, + depth_limit: int, + file_limit: int, + ) -> dict[str, Any]: + path = normalize_path(path) + with self.connect() as conn: + folder = self._folder_by_path(conn, path) + if folder is None: + raise KeyError(f"Unknown folder path: {path}") + base_depth = self._folder_depth(path) + deep_folder = conn.execute( + """ + SELECT path + FROM folders + WHERE path != ? + AND path LIKE ? + AND ( + CASE + WHEN TRIM(path, '/') = '' THEN 0 + ELSE LENGTH(TRIM(path, '/')) - LENGTH(REPLACE(TRIM(path, '/'), '/', '')) + 1 + END + ) - ? > ? + LIMIT 1 + """, + (path, self._descendant_like(path), base_depth, depth_limit), + ).fetchone() + file_rows = conn.execute( + """ + SELECT DISTINCT f.file_ref + FROM files f + JOIN file_folders ff ON ff.file_ref = f.file_ref + JOIN folders fo ON fo.folder_id = ff.folder_id + WHERE f.deleted_at IS NULL + AND (fo.path = ? OR fo.path LIKE ?) + LIMIT ? + """, + (path, self._descendant_like(path), file_limit + 1), + ).fetchall() + return { + "depth_limit": depth_limit, + "file_limit": file_limit, + "folder_depth_exceeds_limit": deep_folder is not None, + "file_count_exceeds_limit": len(file_rows) > file_limit, + "sampled_file_count": len(file_rows), + "sample_deep_folder_path": deep_folder["path"] if deep_folder is not None else "", + } + + def _file_entry_row(self, conn: sqlite3.Connection, file_ref: str) -> sqlite3.Row | None: + return conn.execute( + """ + SELECT + f.file_ref, + f.external_id, + f.storage_uri, + f.source_path, + f.title, + f.descriptor, + f.content_type, + f.source_type, + f.fingerprint, + f.text_artifact_path, + f.raw_artifact_path, + f.pageindex_doc_id, + f.pageindex_tree_status, + f.metadata_json, + f.derived_metadata_json, + f.metadata_generation_json, + COALESCE( + ( + SELECT display_folder.path + FROM file_folders display_ff + JOIN folders display_folder + ON display_folder.folder_id = display_ff.folder_id + WHERE display_ff.file_ref = f.file_ref + ORDER BY display_folder.path + LIMIT 1 + ), + '/' + ) AS folder_path + FROM files f + WHERE f.file_ref = ? AND f.deleted_at IS NULL + """, + (file_ref,), + ).fetchone() + + def _file_rows_for_scope( + self, + conn: sqlite3.Connection, + path: str, + recursive: bool, + limit: int, + ) -> list[sqlite3.Row]: + sql = """ + SELECT + f.file_ref, + f.external_id, + f.title, + f.descriptor, + f.source_path, + f.pageindex_tree_status, + f.metadata_json, + f.derived_metadata_json, + f.metadata_generation_json, + f.created_at, + MIN(pf.folder_id) AS folder_id, + MIN(pf.path) AS folder_path + FROM files f + JOIN file_folders ff ON ff.file_ref = f.file_ref + JOIN folders pf ON pf.folder_id = ff.folder_id + WHERE f.deleted_at IS NULL + """ + params: list[Any] + if recursive: + sql += " AND (pf.path = ? OR pf.path LIKE ?)" + params = [path, self._descendant_like(path)] + else: + sql += " AND pf.path = ?" + params = [path] + sql += " GROUP BY f.file_ref ORDER BY f.created_at DESC, f.title LIMIT ?" + params.append(limit) + return conn.execute(sql, params).fetchall() + + def _scope_sql(self, scope: Optional[dict[str, Any]]) -> tuple[str, list[Any]]: + if not scope: + return "", [] + recursive = scope.get("recursive", True) + folder_id = scope.get("folder_id") + if folder_id: + if folder_id == "root": + folder_path = "/" + else: + if recursive: + return ( + """ + EXISTS ( + SELECT 1 + FROM file_folders scope_ff + JOIN folders scope_folder + ON scope_folder.folder_id = scope_ff.folder_id + JOIN folders base_folder + ON base_folder.folder_id = ? + WHERE scope_ff.file_ref = f.file_ref + AND ( + scope_folder.folder_id = base_folder.folder_id + OR scope_folder.path LIKE CASE + WHEN base_folder.path = '/' THEN '/%' + ELSE base_folder.path || '/%' + END + ) + ) + """, + [folder_id], + ) + return ( + """ + EXISTS ( + SELECT 1 + FROM file_folders scope_ff + WHERE scope_ff.file_ref = f.file_ref + AND scope_ff.folder_id = ? + ) + """, + [folder_id], + ) + elif scope.get("folder_path") or scope.get("path"): + folder_path = normalize_path(scope.get("folder_path") or scope.get("path")) + else: + return "", [] + path_clause = ( + "(scope_folder.path = ? OR scope_folder.path LIKE ?)" + if recursive + else "scope_folder.path = ?" + ) + params = [folder_path, self._descendant_like(folder_path)] if recursive else [folder_path] + return ( + f""" + EXISTS ( + SELECT 1 + FROM file_folders scope_ff + JOIN folders scope_folder + ON scope_folder.folder_id = scope_ff.folder_id + WHERE scope_ff.file_ref = f.file_ref + AND {path_clause} + ) + """, + params, + ) + + def _folder_by_path(self, conn: sqlite3.Connection, path: str) -> sqlite3.Row | None: + return conn.execute( + """ + SELECT + folder_id, + parent_id, + name, + path, + description, + kind, + metadata_json, + created_at, + updated_at + FROM folders + WHERE path = ? + """, + (path,), + ).fetchone() + + @staticmethod + def _descendant_like(path: str) -> str: + return "/%" if path == "/" else f"{path}/%" + + @staticmethod + def _folder_depth(path: str) -> int: + stripped = normalize_path(path).strip("/") + return 0 if not stripped else len(stripped.split("/")) + + @classmethod + def _folder_row_to_dict(cls, row: sqlite3.Row) -> dict[str, Any]: + return { + "folder_id": row["folder_id"], + "id": row["folder_id"], + "parent_id": row["parent_id"], + "parent_folder_id": row["parent_id"], + "name": row["name"], + "description": cls._row_value(row, "description", ""), + "path": row["path"], + "kind": row["kind"], + "metadata": json.loads(cls._row_value(row, "metadata_json", "{}") or "{}"), + "created_at": cls._row_value(row, "created_at"), + "updated_at": cls._row_value(row, "updated_at"), + "file_count": cls._row_value(row, "file_count", 0), + "children_count": cls._row_value(row, "children_count", 0), + "matched_files": cls._row_value(row, "matched_files", 0), + } + + @classmethod + def _file_summary(cls, row: sqlite3.Row) -> dict[str, Any]: + external_id = row["external_id"] + return { + "file_ref": row["file_ref"], + "id": external_id or row["file_ref"], + "document_id": external_id, + "external_id": external_id, + "name": row["title"], + "title": row["title"], + "description": cls._row_value(row, "descriptor", row["title"]), + "status": cls._row_value(row, "pageindex_tree_status", "not_built"), + "pageNum": None, + "createdAt": cls._row_value(row, "created_at"), + "folderId": cls._row_value(row, "folder_id"), + "source_path": row["source_path"], + "folder_path": row["folder_path"], + "metadata": json.loads(row["metadata_json"] or "{}"), + "derived_metadata": json.loads(cls._row_value(row, "derived_metadata_json", "{}") or "{}"), + "metadata_generation": json.loads( + cls._row_value(row, "metadata_generation_json", "{}") or "{}" + ), + } + + @classmethod + def _search_row_to_dict(cls, row: sqlite3.Row) -> dict[str, Any]: + external_id = row["external_id"] + return { + "file_ref": row["file_ref"], + "id": external_id or row["file_ref"], + "document_id": external_id, + "external_id": external_id, + "name": row["title"], + "title": row["title"], + "description": cls._row_value(row, "descriptor", row["title"]), + "status": cls._row_value(row, "pageindex_tree_status", "not_built"), + "pageNum": None, + "createdAt": cls._row_value(row, "created_at"), + "folderId": cls._row_value(row, "folder_id"), + "source_path": row["source_path"], + "snippet": row["snippet"] or row["title"], + "folder_path": row["folder_path"], + "metadata": json.loads(row["metadata_json"] or "{}"), + "derived_metadata": json.loads(cls._row_value(row, "derived_metadata_json", "{}") or "{}"), + "metadata_generation": json.loads( + cls._row_value(row, "metadata_generation_json", "{}") or "{}" + ), + } + + @staticmethod + def _row_value(row: sqlite3.Row, key: str, default: Any = None) -> Any: + return row[key] if key in row.keys() else default + + @staticmethod + def _file_entry(row: sqlite3.Row) -> FileEntry: + return FileEntry( + file_ref=row["file_ref"], + external_id=row["external_id"], + storage_uri=row["storage_uri"], + source_path=row["source_path"], + title=row["title"], + descriptor=row["descriptor"], + content_type=row["content_type"], + source_type=row["source_type"], + fingerprint=row["fingerprint"], + text_artifact_path=row["text_artifact_path"], + raw_artifact_path=row["raw_artifact_path"], + pageindex_doc_id=row["pageindex_doc_id"], + pageindex_tree_status=row["pageindex_tree_status"], + metadata=json.loads(row["metadata_json"] or "{}"), + folder_path=row["folder_path"], + derived_metadata=json.loads( + SQLiteFileSystemStore._row_value(row, "derived_metadata_json", "{}") or "{}" + ), + metadata_generation=json.loads( + SQLiteFileSystemStore._row_value(row, "metadata_generation_json", "{}") or "{}" + ), + ) + + @classmethod + def _file_entry_to_dict(cls, entry: FileEntry) -> dict[str, Any]: + return { + "file_ref": entry.file_ref, + "id": entry.external_id or entry.file_ref, + "document_id": entry.external_id, + "external_id": entry.external_id, + "name": entry.title, + "storage_uri": entry.storage_uri, + "source_path": entry.source_path, + "title": entry.title, + "description": entry.descriptor, + "status": entry.pageindex_tree_status, + "pageNum": None, + "descriptor": entry.descriptor, + "content_type": entry.content_type, + "source_type": entry.source_type, + "fingerprint": entry.fingerprint, + "text_artifact_path": entry.text_artifact_path, + "raw_artifact_path": entry.raw_artifact_path, + "pageindex_doc_id": entry.pageindex_doc_id, + "pageindex_tree_status": entry.pageindex_tree_status, + "metadata": entry.metadata, + "derived_metadata": entry.derived_metadata, + "metadata_generation": entry.metadata_generation, + "folder_path": entry.folder_path, + } + + @staticmethod + def _query_text(query: str | list[str] | None) -> str: + if query is None: + return "" + if isinstance(query, list): + return " ".join(str(item) for item in query) + return str(query) + + @classmethod + def _fts_match_queries(cls, query: str) -> list[str]: + terms = cls._fts_terms(query) + if not terms: + return [] + queries = [" ".join(terms)] + if len(terms) > 1: + queries.append(" OR ".join(terms)) + return queries + + @staticmethod + def _fts_terms(query: str) -> list[str]: + stopwords = { + "a", + "an", + "and", + "are", + "as", + "at", + "be", + "by", + "did", + "do", + "does", + "for", + "from", + "how", + "in", + "is", + "it", + "of", + "on", + "or", + "that", + "the", + "to", + "was", + "were", + "what", + "when", + "where", + "which", + "who", + "why", + "with", + } + terms = re.findall(r"[A-Za-z0-9_]+", query.lower()) + unique_terms = [] + seen = set() + for term in terms: + if term in stopwords or term in seen: + continue + seen.add(term) + unique_terms.append(term) + return unique_terms + + @staticmethod + def _metadata_value_items(value: Any) -> list[dict[str, Any]]: + if value is None: + return [] + if isinstance(value, list): + items = [] + for item in value: + items.extend(SQLiteFileSystemStore._metadata_value_items(item)) + return items + value_json = json.dumps(value, ensure_ascii=False, sort_keys=True) + value_text = SQLiteFileSystemStore._metadata_compare_text(value) + return [ + { + "value_text": value_text, + "value_number": float(value) if isinstance(value, (int, float)) and not isinstance(value, bool) else None, + "value_bool": int(value) if isinstance(value, bool) else None, + "value_json": value_json, + } + ] + + @staticmethod + def _metadata_compare_text(value: Any) -> str: + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, (dict, list)): + return json.dumps(value, ensure_ascii=False, sort_keys=True) + return "" if value is None else str(value) + + @classmethod + def _merge_metadata_values( + cls, + metadata: dict[str, Any], + derived_metadata: dict[str, Any], + ) -> dict[str, Any]: + merged = dict(metadata) + for name, value in derived_metadata.items(): + if name not in merged: + merged[name] = value + continue + if merged[name] == value: + continue + merged[name] = cls._merge_metadata_value(merged[name], value) + return merged + + @staticmethod + def _merge_metadata_value(raw_value: Any, derived_value: Any) -> Any: + values = raw_value if isinstance(raw_value, list) else [raw_value] + derived_values = derived_value if isinstance(derived_value, list) else [derived_value] + merged = list(values) + for item in derived_values: + if item not in merged: + merged.append(item) + return merged + + @classmethod + def indexed_metadata_values( + cls, + metadata: dict[str, Any], + derived_metadata: dict[str, Any], + metadata_generation: dict[str, Any] | None, + ) -> dict[str, Any]: + generated_fields = set(derived_metadata) + if isinstance(metadata_generation, dict): + policy = metadata_generation.get("policy", {}) + if isinstance(policy, dict): + fields = policy.get("fields", {}) + if isinstance(fields, dict): + generated_fields.update( + str(name) + for name in fields + ) + + indexed = { + name: value + for name, value in metadata.items() + if name not in generated_fields + } + indexed.update(derived_metadata) + return indexed + + @staticmethod + def _valid_field_name(name: str) -> bool: + return re.match(r"^[A-Za-z][A-Za-z0-9_]*$", str(name)) is not None + + @staticmethod + def folder_id(path: str) -> str: + normalized = normalize_path(path) + if normalized == "/": + return "folder_root" + digest = hashlib.sha1(normalized.encode("utf-8")).hexdigest()[:16] + return f"folder_{digest}" + + @staticmethod + def field_id(name: str) -> str: + digest = hashlib.sha1(name.encode("utf-8")).hexdigest()[:16] + return f"field_{digest}" + + +def normalize_path(path: str | Path | None) -> str: + if path is None: + return "/" + if str(path).strip().lower() == "root": + return "/" + parts = [part for part in str(path).replace("\\", "/").split("/") if part and part != "."] + return "/" + "/".join(parts) if parts else "/" + + +def make_file_ref(seed: str) -> str: + digest = hashlib.sha1(seed.encode("utf-8")).hexdigest()[:16] + return f"file_{digest}" + + +def fingerprint(content: str) -> str: + return hashlib.sha256(content.encode("utf-8")).hexdigest() + + +def metadata_text(metadata: dict[str, Any]) -> str: + values = [] + for value in metadata.values(): + if isinstance(value, list): + values.extend(str(item) for item in value) + elif isinstance(value, dict): + values.append(json.dumps(value, ensure_ascii=False, sort_keys=True)) + elif value is not None: + values.append(str(value)) + return " ".join(values) diff --git a/pageindex/filesystem/structural_read.py b/pageindex/filesystem/structural_read.py new file mode 100644 index 000000000..b61df43b3 --- /dev/null +++ b/pageindex/filesystem/structural_read.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from copy import deepcopy +from typing import Any + + +def strip_pageindex_text_fields(value: Any) -> Any: + if isinstance(value, list): + return [strip_pageindex_text_fields(item) for item in value] + if isinstance(value, dict): + return { + key: strip_pageindex_text_fields(item) + for key, item in value.items() + if key != "text" + } + return value + + +def find_pageindex_node(structure: Any, node_id: str) -> dict[str, Any] | None: + if isinstance(structure, dict): + if str(structure.get("node_id", "")) == str(node_id): + return deepcopy(structure) + for child_key in ("nodes", "children"): + found = find_pageindex_node(structure.get(child_key), node_id) + if found is not None: + return found + if isinstance(structure, list): + for item in structure: + found = find_pageindex_node(item, node_id) + if found is not None: + return found + return None + + +def first_node_location(node: dict[str, Any]) -> str | None: + for key in ("line_num", "physical_index", "start_index"): + value = node.get(key) + if value is not None and value != "": + return str(value) + return None diff --git a/pageindex/filesystem/types.py b/pageindex/filesystem/types.py new file mode 100644 index 000000000..eac0a15ce --- /dev/null +++ b/pageindex/filesystem/types.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Optional + + +@dataclass(frozen=True) +class SearchResult: + reference_id: str + file_ref: str + external_id: Optional[str] + title: str + snippet: str + folder_path: str + folder_paths: list[str] + metadata: dict[str, Any] + source_path: str = "" + id: Optional[str] = None + document_id: Optional[str] = None + name: str = "" + description: str = "" + status: str = "" + pageNum: Optional[int] = None + createdAt: Optional[str] = None + folderId: Optional[str] = None + derived_metadata: dict[str, Any] = field(default_factory=dict) + metadata_generation: dict[str, Any] = field(default_factory=dict) + + +@dataclass(frozen=True) +class OpenResult: + reference_id: str + file_ref: str + start_line: int + end_line: int + text: str + external_id: Optional[str] = None + folder_path: str = "" + source_path: str = "" + + +@dataclass(frozen=True) +class FolderEntry: + folder_id: str + parent_id: Optional[str] + name: str + path: str + kind: str + + +@dataclass(frozen=True) +class FileEntry: + file_ref: str + external_id: Optional[str] + storage_uri: str + source_path: str + title: str + descriptor: str + content_type: str + source_type: Optional[str] + fingerprint: str + text_artifact_path: str + raw_artifact_path: Optional[str] + pageindex_doc_id: Optional[str] + pageindex_tree_status: str + metadata: dict[str, Any] + folder_path: str + derived_metadata: dict[str, Any] = field(default_factory=dict) + metadata_generation: dict[str, Any] = field(default_factory=dict) + + +@dataclass(frozen=True) +class MetadataField: + name: str + field_type: str + description: str = "" + indexed: bool = True + faceted: bool = False + sortable: bool = False + source: str = "manual" + + +@dataclass(frozen=True) +class CommandResult: + command: str + data: Any + text: str diff --git a/pifs-cli b/pifs-cli new file mode 100755 index 000000000..46b6e0e3a --- /dev/null +++ b/pifs-cli @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from pageindex.filesystem.cli import main + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..ee374805d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "pageindex" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "litellm==1.83.7", + "openai-agents>=0.17.2", + "pymupdf==1.26.4", + "pypdf2==3.0.1", + "pytest>=9.0.3", + "python-dotenv==1.1.0", + "pyyaml==6.0.2", + "sqlite-vec>=0.1.9", +] diff --git a/tests/test_pageindex_filesystem_scope.py b/tests/test_pageindex_filesystem_scope.py new file mode 100644 index 000000000..e08dabd97 --- /dev/null +++ b/tests/test_pageindex_filesystem_scope.py @@ -0,0 +1,60 @@ +import json +from types import SimpleNamespace + + +class SummaryBackend: + def __init__(self, document_id): + self.document_id = document_id + self.calls = [] + + def available_channels(self): + return ("summary",) + + def search_channel(self, channel, query, *, limit=10, filters=None): + self.calls.append((channel, query, filters)) + return [ + SimpleNamespace( + document_id=self.document_id, + snippet=f"summary candidate: {query}", + ) + ] + + +def test_semantic_search_scope_keeps_ordinary_folders_out_of_source_type_filters(tmp_path): + from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem + + filesystem = PageIndexFileSystem(workspace=tmp_path / "workspace") + filesystem.register_file( + storage_uri="file:///tmp/report.pdf", + source_path="examples/documents/report.pdf", + folder_path="/documents", + external_id="dsid_report", + title="Annual report", + metadata={"source_type": "examples-documents"}, + content="Federal Reserve supervision and regulation annual report.", + ) + backend = SummaryBackend("dsid_report") + filesystem.semantic_retrieval_backend = backend + executor = PIFSCommandExecutor(filesystem, json_output=True) + + result = json.loads( + executor.execute('search-summary "Federal Reserve annual report" /documents') + ) + + assert backend.calls[0][2] == {} + assert result["data"]["data"][0]["external_id"] == "dsid_report" + + +def test_semantic_search_scope_filters_explicit_source_type_facets(): + from pageindex.filesystem import PageIndexFileSystem + + assert PageIndexFileSystem._semantic_filters_for_scope( + {"folder_path": "/source_type=google-drive"} + ) == {"source_type": "google_drive"} + assert PageIndexFileSystem._semantic_filters_for_scope( + {"folder_path": "/semantic/source_type=google-drive"} + ) == {"source_type": "google_drive"} + assert PageIndexFileSystem._semantic_filters_for_scope( + {"folder_path": "/documents"} + ) == {} + diff --git a/tests/test_pageindex_structural_read.py b/tests/test_pageindex_structural_read.py new file mode 100644 index 000000000..fcb2725b1 --- /dev/null +++ b/tests/test_pageindex_structural_read.py @@ -0,0 +1,632 @@ +import json +import tempfile +from pathlib import Path + +import pytest + + +def write_pageindex_client_doc(workspace: Path, doc_id: str, doc: dict) -> None: + workspace.mkdir(parents=True, exist_ok=True) + (workspace / f"{doc_id}.json").write_text( + json.dumps(doc, ensure_ascii=False, indent=2), + encoding="utf-8", + ) + meta = { + doc_id: { + "type": doc.get("type", ""), + "doc_name": doc.get("doc_name", ""), + "doc_description": doc.get("doc_description", ""), + "path": doc.get("path", ""), + } + } + if doc.get("type") == "pdf": + meta[doc_id]["page_count"] = doc.get("page_count") + elif doc.get("type") == "md": + meta[doc_id]["line_count"] = doc.get("line_count") + (workspace / "_meta.json").write_text( + json.dumps(meta, ensure_ascii=False, indent=2), + encoding="utf-8", + ) + + +class RecordingMetadataGenerator: + values = { + "summary": "Generated retrieval summary.", + "doc_type": "technical_note", + "domain": "documentation", + "topic": "pageindex extraction", + } + + def __init__(self): + self.calls = [] + + def generate(self, request, *, fields): + self.calls.append((request, list(fields))) + return {field: self.values[field] for field in fields if field in self.values} + + +def test_pageindex_structure_options_report_failed_register_build(monkeypatch): + from pageindex import PageIndexClient + from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem + + with tempfile.TemporaryDirectory() as tmp: + source = Path(tmp) / "report.md" + source.write_text("# Report\n\nCached structure is not built yet.", encoding="utf-8") + filesystem = PageIndexFileSystem(workspace=Path(tmp) / "workspace") + + def fail_index(*args, **kwargs): + raise RuntimeError("index failed") + + monkeypatch.setattr(PageIndexClient, "index", fail_index) + filesystem.register_file( + storage_uri=source.as_uri(), + source_path="docs/report.md", + external_id="dsid_structural_missing", + title="Structural report", + content=source.read_text(encoding="utf-8"), + ) + executor = PIFSCommandExecutor(filesystem, json_output=True) + + structure = json.loads(executor.execute("cat --structure dsid_structural_missing")) + node = json.loads(executor.execute("cat --node 0001 dsid_structural_missing")) + pages = json.loads(executor.execute("cat --page 1-2 dsid_structural_missing")) + stat = json.loads(executor.execute("stat dsid_structural_missing")) + + assert structure["data"]["mode"] == "structure" + assert structure["data"]["available"] is False + assert structure["data"]["status"] == "failed" + assert "PageIndexClient workspace" in structure["data"]["message"] + assert stat["data"]["pageindex_tree_status"] == "failed" + + assert node["data"]["mode"] == "node" + assert node["data"]["available"] is False + assert node["data"]["node_id"] == "0001" + + assert pages["data"]["mode"] == "page" + assert pages["data"]["available"] is False + assert pages["data"]["pages"] == "1-2" + + assert "cp" not in executor.allowed_commands() + assert "mkdir" not in executor.allowed_commands() + + +def test_register_pdf_markdown_uses_pageindex_extracted_text_for_metadata_and_fts(monkeypatch): + from pageindex import PageIndexClient + from pageindex.filesystem import PageIndexFileSystem + + def fake_index(self, file_path, mode="auto"): + suffix = Path(file_path).suffix.lower() + doc_id = f"doc_{suffix.lstrip('.')}" + if suffix == ".pdf": + doc = { + "id": doc_id, + "type": "pdf", + "path": str(Path(file_path).resolve()), + "doc_name": "report.pdf", + "doc_description": "", + "page_count": 2, + "structure": [{"title": "Report", "node_id": "0001", "nodes": []}], + "pages": [ + {"page": 1, "content": "PageIndex PDF extracted alpha text."}, + {"page": 2, "content": "Second PageIndex PDF extracted beta text."}, + ], + } + else: + doc = { + "id": doc_id, + "type": "md", + "path": str(Path(file_path).resolve()), + "doc_name": "notes", + "doc_description": "", + "line_count": 3, + "structure": [ + { + "title": "Notes", + "node_id": "0001", + "line_num": 1, + "text": "# Notes\n\nPageIndex Markdown extracted gamma text.", + "nodes": [], + } + ], + } + write_pageindex_client_doc(self.workspace, doc_id, doc) + self.documents[doc_id] = doc + return doc_id + + monkeypatch.setattr(PageIndexClient, "index", fake_index) + with tempfile.TemporaryDirectory() as tmp: + source_pdf = Path(tmp) / "report.pdf" + source_md = Path(tmp) / "notes.md" + source_pdf.write_bytes(b"%PDF-1.4\n% test fixture\n") + source_md.write_text("# Notes\n\nCaller markdown content", encoding="utf-8") + generator = RecordingMetadataGenerator() + filesystem = PageIndexFileSystem( + workspace=Path(tmp) / "workspace", + metadata_generator=generator, + ) + + filesystem.register_file( + storage_uri=source_pdf.as_uri(), + source_path="docs/report.pdf", + external_id="dsid_pdf_extracted", + title="PDF extracted", + content="CALLER PDF CONTENT MUST NOT REACH GENERATOR", + ) + filesystem.register_file( + storage_uri=source_md.as_uri(), + source_path="docs/notes.md", + external_id="dsid_md_extracted", + title="Markdown extracted", + content="CALLER MD CONTENT MUST NOT REACH GENERATOR", + ) + + pdf_request = generator.calls[0][0] + md_request = generator.calls[1][0] + pdf_stat = filesystem.store.file_info("dsid_pdf_extracted") + md_stat = filesystem.store.file_info("dsid_md_extracted") + + assert "PageIndex PDF extracted alpha text" in pdf_request.text + assert "Second PageIndex PDF extracted beta text" in pdf_request.text + assert "CALLER PDF CONTENT" not in pdf_request.text + assert "PageIndex Markdown extracted gamma text" in md_request.text + assert "CALLER MD CONTENT" not in md_request.text + assert "PageIndex PDF extracted alpha text" in Path( + pdf_stat["text_artifact_path"] + ).read_text(encoding="utf-8") + assert "PageIndex Markdown extracted gamma text" in Path( + md_stat["text_artifact_path"] + ).read_text(encoding="utf-8") + assert [r.external_id for r in filesystem.search("alpha beta", limit=5)] == [ + "dsid_pdf_extracted" + ] + assert [r.external_id for r in filesystem.search("gamma", limit=5)] == [ + "dsid_md_extracted" + ] + assert filesystem.search("CALLER", limit=5) == [] + + +def test_register_text_metadata_generation_keeps_caller_content_without_pageindex(monkeypatch): + from pageindex import PageIndexClient + from pageindex.filesystem import PageIndexFileSystem + + def fail_index(*args, **kwargs): + raise AssertionError("PageIndexClient.index should not be called for text files") + + monkeypatch.setattr(PageIndexClient, "index", fail_index) + with tempfile.TemporaryDirectory() as tmp: + generator = RecordingMetadataGenerator() + filesystem = PageIndexFileSystem( + workspace=Path(tmp) / "workspace", + metadata_generator=generator, + ) + + filesystem.register_file( + storage_uri="file:///tmp/readme.txt", + source_path="docs/readme.txt", + external_id="dsid_text_generation", + title="Text generation", + content="Plain text caller content stays authoritative.", + content_type="text/plain", + ) + + stat = filesystem.store.file_info("dsid_text_generation") + + assert generator.calls[0][0].text == "Plain text caller content stays authoritative." + assert stat["pageindex_doc_id"] is None + assert stat["pageindex_tree_status"] == "not_built" + assert Path(stat["text_artifact_path"]).read_text( + encoding="utf-8" + ) == "Plain text caller content stays authoritative." + + +def test_register_pdf_markdown_cache_miss_invokes_pageindex_client_index(monkeypatch): + from pageindex import PageIndexClient + from pageindex.filesystem import PageIndexFileSystem + + calls: list[str] = [] + + def fake_index(self, file_path, mode="auto"): + calls.append(str(file_path)) + doc_id = f"doc_{Path(file_path).suffix.lstrip('.')}" + doc_type = "pdf" if Path(file_path).suffix == ".pdf" else "md" + doc = { + "id": doc_id, + "type": doc_type, + "path": str(Path(file_path).resolve()), + "doc_name": Path(file_path).name, + "doc_description": "", + "structure": [{"title": Path(file_path).stem, "node_id": "0001", "nodes": []}], + } + if doc_type == "pdf": + doc["page_count"] = 1 + doc["pages"] = [{"page": 1, "content": "Page one text"}] + else: + doc["line_count"] = 1 + write_pageindex_client_doc(self.workspace, doc_id, doc) + self.documents[doc_id] = doc + return doc_id + + monkeypatch.setattr(PageIndexClient, "index", fake_index) + with tempfile.TemporaryDirectory() as tmp: + source_pdf = Path(tmp) / "report.pdf" + source_md = Path(tmp) / "notes.md" + source_pdf.write_bytes(b"%PDF-1.4\n% test fixture\n") + source_md.write_text("# Notes", encoding="utf-8") + filesystem = PageIndexFileSystem(workspace=Path(tmp) / "workspace") + + filesystem.register_file( + storage_uri=str(source_pdf), + source_path="docs/report.pdf", + external_id="dsid_pdf_build", + title="PDF build", + content="pdf text", + ) + filesystem.register_file( + storage_uri=source_md.as_uri(), + source_path="docs/notes.md", + external_id="dsid_md_build", + title="Markdown build", + content=source_md.read_text(encoding="utf-8"), + ) + + pdf_stat = filesystem.store.file_info("dsid_pdf_build") + md_stat = filesystem.store.file_info("dsid_md_build") + + assert calls == [str(source_pdf.resolve()), str(source_md.resolve())] + assert pdf_stat["pageindex_doc_id"] == "doc_pdf" + assert pdf_stat["pageindex_tree_status"] == "built" + assert md_stat["pageindex_doc_id"] == "doc_md" + assert md_stat["pageindex_tree_status"] == "built" + + +def test_cat_structure_page_reuses_pageindex_client_cache_without_indexing(monkeypatch): + from pageindex import PageIndexClient + from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem + + with tempfile.TemporaryDirectory() as tmp: + source = Path(tmp) / "report.pdf" + source.write_bytes(b"%PDF-1.4\n% test fixture\n") + workspace = Path(tmp) / "workspace" + filesystem = PageIndexFileSystem(workspace=workspace) + write_pageindex_client_doc( + filesystem.pageindex_client_workspace, + "doc_cached_pdf", + { + "id": "doc_cached_pdf", + "type": "pdf", + "path": str(source.resolve()), + "doc_name": "report.pdf", + "doc_description": "", + "page_count": 2, + "structure": [ + { + "title": "Introduction", + "node_id": "0001", + "text": "Intro section text", + "nodes": [ + { + "title": "Findings", + "node_id": "0002", + "physical_index": 2, + "nodes": [], + } + ], + } + ], + "pages": [ + {"page": 1, "content": "Page one text"}, + {"page": 2, "content": "Page two text"}, + ], + }, + ) + + def fail_index(*args, **kwargs): + raise AssertionError("PageIndexClient.index should not be called on cache hit") + + monkeypatch.setattr(PageIndexClient, "index", fail_index) + filesystem.register_file( + storage_uri=source.as_uri(), + source_path="docs/report.pdf", + external_id="dsid_structural_cached", + title="Cached structural report", + content="text artifact remains available for grep, not cat --all", + ) + executor = PIFSCommandExecutor(filesystem, json_output=True) + + structure = json.loads(executor.execute("cat --structure dsid_structural_cached")) + pages = json.loads(executor.execute("cat --page 1-2 dsid_structural_cached")) + stat = json.loads(executor.execute("stat dsid_structural_cached")) + + assert structure["data"]["available"] is True + assert structure["data"]["pageindex_doc_id"] == "doc_cached_pdf" + assert structure["data"]["structure"][0]["title"] == "Introduction" + assert "text" not in structure["data"]["structure"][0] + assert "text" not in structure["data"]["structure"][0]["nodes"][0] + + assert pages["data"]["available"] is True + assert pages["data"]["text"] == "Page one text\n\nPage two text" + + assert stat["data"]["pageindex_doc_id"] == "doc_cached_pdf" + assert stat["data"]["pageindex_tree_status"] == "built" + + +def test_cat_node_reads_pageindex_client_structure_without_custom_pifs_artifact(): + from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem + + with tempfile.TemporaryDirectory() as tmp: + source = Path(tmp) / "notes.md" + source.write_text("# Notes\n\nBody", encoding="utf-8") + filesystem = PageIndexFileSystem(workspace=Path(tmp) / "workspace") + write_pageindex_client_doc( + filesystem.pageindex_client_workspace, + "doc_cached_md", + { + "id": "doc_cached_md", + "type": "md", + "path": str(source.resolve()), + "doc_name": "notes", + "doc_description": "", + "line_count": 3, + "structure": [ + { + "title": "Notes", + "node_id": "0001", + "line_num": 1, + "text": "# Notes\n\nBody", + "nodes": [], + } + ], + }, + ) + filesystem.register_file( + storage_uri=source.as_uri(), + source_path="docs/notes.md", + external_id="dsid_md_cached", + title="Cached markdown notes", + content=source.read_text(encoding="utf-8"), + ) + executor = PIFSCommandExecutor(filesystem, json_output=True) + + node = json.loads(executor.execute("cat --node 0001 dsid_md_cached")) + + assert node["data"]["available"] is True + assert node["data"]["pageindex_doc_id"] == "doc_cached_md" + assert node["data"]["node"]["title"] == "Notes" + assert node["data"]["text"] == "# Notes\n\nBody" + assert "text" not in node["data"]["node"] + + +def test_tree_folder_behavior_is_preserved(): + from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem + + with tempfile.TemporaryDirectory() as tmp: + filesystem = PageIndexFileSystem(workspace=Path(tmp) / "workspace") + filesystem.register_file( + storage_uri="file:///tmp/report.txt", + source_path="docs/report.txt", + folder_path="/docs/reports", + external_id="dsid_folder_tree", + title="Folder report", + content="folder tree behavior remains intact", + ) + executor = PIFSCommandExecutor(filesystem, json_output=True) + + folder_tree = json.loads(executor.execute("tree /docs --depth 2")) + + assert folder_tree["data"]["path"] == "/docs" + assert folder_tree["data"]["folders"][0]["path"] == "/docs/reports" + + +def test_tree_does_not_read_file_internal_pageindex_structure(): + from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem + from pageindex.filesystem.commands import PIFSCommandError + + with tempfile.TemporaryDirectory() as tmp: + source = Path(tmp) / "report.pdf" + source.write_bytes(b"%PDF-1.4\n% test fixture\n") + filesystem = PageIndexFileSystem(workspace=Path(tmp) / "workspace") + write_pageindex_client_doc( + filesystem.pageindex_client_workspace, + "doc_tree_is_folder_only", + { + "id": "doc_tree_is_folder_only", + "type": "pdf", + "path": str(source.resolve()), + "doc_name": "report.pdf", + "doc_description": "", + "page_count": 1, + "structure": [ + {"title": "Introduction", "node_id": "0001", "nodes": []} + ], + "pages": [{"page": 1, "content": "Page one text"}], + }, + ) + filesystem.register_file( + storage_uri=source.as_uri(), + source_path="docs/report.pdf", + external_id="dsid_tree_is_folder_only", + title="Cached structural report", + content="text artifact remains available", + ) + executor = PIFSCommandExecutor(filesystem, json_output=True) + + with pytest.raises(PIFSCommandError): + executor.execute("tree dsid_tree_is_folder_only") + + structure = json.loads(executor.execute("cat --structure dsid_tree_is_folder_only")) + assert structure["data"]["structure"][0]["title"] == "Introduction" + + +def test_cat_all_is_limited_to_text_files(): + from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem + from pageindex.filesystem.commands import PIFSCommandError + + with tempfile.TemporaryDirectory() as tmp: + filesystem = PageIndexFileSystem(workspace=Path(tmp) / "workspace") + filesystem.register_file( + storage_uri="file:///tmp/readme.txt", + source_path="docs/readme.txt", + external_id="dsid_text_file", + title="Text readme", + content="plain text body", + ) + filesystem.register_file( + storage_uri="file:///tmp/report.pdf", + source_path="docs/report.pdf", + external_id="dsid_pdf_file", + title="PDF report", + content="extracted text should not be served through cat --all", + ) + filesystem.register_file( + storage_uri="file:///tmp/notes.md", + source_path="docs/notes.md", + external_id="dsid_md_file", + title="Markdown notes", + content="markdown text should use PageIndex structure reads", + ) + filesystem.register_file( + storage_uri="file:///tmp/data.json", + source_path="docs/data.json", + external_id="dsid_json_file", + title="JSON record", + content='{"body":"json"}', + content_type="application/json", + ) + executor = PIFSCommandExecutor(filesystem, json_output=True) + + text = json.loads(executor.execute("cat --all dsid_text_file")) + assert text["data"]["text"] == "plain text body" + + with pytest.raises(PIFSCommandError, match="only supported for txt/text files"): + executor.execute("cat --all dsid_pdf_file") + with pytest.raises(ValueError, match="not supported for PDF/Markdown"): + filesystem.open("dsid_pdf_file") + with pytest.raises(PIFSCommandError, match="only supported for txt/text files"): + executor.execute("cat --all dsid_md_file") + with pytest.raises(ValueError, match="not supported for PDF/Markdown"): + filesystem.open("dsid_md_file") + with pytest.raises(PIFSCommandError, match="only supported for txt/text files"): + executor.execute("cat --all dsid_json_file") + assert filesystem.open("dsid_json_file").text == '{"body":"json"}' + for command in ( + "head dsid_pdf_file", + "tail dsid_pdf_file", + "sed -n 1,1p dsid_pdf_file", + "head dsid_md_file", + "tail dsid_md_file", + "sed -n 1,1p dsid_md_file", + ): + with pytest.raises(PIFSCommandError, match="only supported for txt/text files"): + executor.execute(command) + + +def test_pageindex_structure_commands_are_limited_to_pdf_and_markdown(): + from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem + from pageindex.filesystem.commands import PIFSCommandError + + with tempfile.TemporaryDirectory() as tmp: + filesystem = PageIndexFileSystem(workspace=Path(tmp) / "workspace") + filesystem.register_file( + storage_uri="file:///tmp/readme.txt", + source_path="docs/readme.txt", + external_id="dsid_text_only", + title="Text readme", + content="plain text body", + ) + executor = PIFSCommandExecutor(filesystem, json_output=True) + + for command in ( + "cat --structure dsid_text_only", + "cat --page 1 dsid_text_only", + "cat --node 0001 dsid_text_only", + ): + with pytest.raises(PIFSCommandError, match="only supported for PDF/Markdown"): + executor.execute(command) + + +def test_existing_pageindex_status_allows_legacy_record_without_format_suffix(): + from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem + from pageindex.filesystem.commands import PIFSCommandError + + with tempfile.TemporaryDirectory() as tmp: + source = Path(tmp) / "uploaded" + source.write_text("# Uploaded\n\nBody", encoding="utf-8") + filesystem = PageIndexFileSystem(workspace=Path(tmp) / "workspace") + file_ref = filesystem.register_file( + storage_uri=source.as_uri(), + source_path="uploads/uploaded", + external_id="dsid_legacy_pageindex", + title="Legacy PageIndex record", + content="text/plain is only a weak default here", + ) + write_pageindex_client_doc( + filesystem.pageindex_client_workspace, + "doc_legacy_pageindex", + { + "id": "doc_legacy_pageindex", + "type": "md", + "path": str(source.resolve()), + "doc_name": "uploaded", + "doc_description": "", + "line_count": 3, + "structure": [ + {"title": "Uploaded", "node_id": "0001", "text": "Body", "nodes": []} + ], + }, + ) + filesystem.store.update_pageindex_pointer( + file_ref, + pageindex_doc_id="doc_legacy_pageindex", + pageindex_tree_status="built", + ) + executor = PIFSCommandExecutor(filesystem, json_output=True) + + structure = json.loads(executor.execute("cat --structure dsid_legacy_pageindex")) + assert structure["data"]["structure"][0]["title"] == "Uploaded" + with pytest.raises(PIFSCommandError, match="only supported for txt/text files"): + executor.execute("cat --all dsid_legacy_pageindex") + + +def test_read_commands_do_not_link_pageindex_cache_when_pointer_is_missing(monkeypatch): + from pageindex import PageIndexClient + from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem + + with tempfile.TemporaryDirectory() as tmp: + source = Path(tmp) / "late.md" + source.write_text("# Late\n\nBody", encoding="utf-8") + filesystem = PageIndexFileSystem(workspace=Path(tmp) / "workspace") + + def fail_index(*args, **kwargs): + raise RuntimeError("index failed") + + monkeypatch.setattr(PageIndexClient, "index", fail_index) + filesystem.register_file( + storage_uri=source.as_uri(), + source_path="docs/late.md", + external_id="dsid_late_cache", + title="Late cache", + content=source.read_text(encoding="utf-8"), + ) + write_pageindex_client_doc( + filesystem.pageindex_client_workspace, + "doc_late_cache", + { + "id": "doc_late_cache", + "type": "md", + "path": str(source.resolve()), + "doc_name": "late", + "doc_description": "", + "line_count": 3, + "structure": [ + {"title": "Late", "node_id": "0001", "text": "Body", "nodes": []} + ], + }, + ) + executor = PIFSCommandExecutor(filesystem, json_output=True) + + structure = json.loads(executor.execute("cat --structure dsid_late_cache")) + stat = json.loads(executor.execute("stat dsid_late_cache")) + + assert structure["data"]["available"] is False + assert stat["data"]["pageindex_doc_id"] is None + assert stat["data"]["pageindex_tree_status"] == "failed" diff --git a/tests/test_pifs_agent_stream.py b/tests/test_pifs_agent_stream.py new file mode 100644 index 000000000..1b7c9d120 --- /dev/null +++ b/tests/test_pifs_agent_stream.py @@ -0,0 +1,185 @@ +import io +import os +import unittest +from types import SimpleNamespace + +from pydantic import BaseModel, ConfigDict + +from pageindex.filesystem.agent import ( + PIFSAgentStreamObserver, + build_agent_model_settings, + normalize_agent_stream_mode, + normalize_reasoning_effort, + normalize_reasoning_summary, + pifs_agent_raw_reasoning_enabled, + serialize_agent_final_output, + should_disable_pifs_agent_tracing, + should_use_openai_compatible_chat_model, +) + + +class StructuredAnswer(BaseModel): + model_config = ConfigDict(extra="forbid") + + answer: str + document_ids: list[str] + + +class PIFSAgentStreamTest(unittest.TestCase): + def raw_event(self, event_type, delta): + return SimpleNamespace( + type="raw_response_event", + data=SimpleNamespace(type=event_type, delta=delta), + ) + + def test_model_stream_prints_output_and_think_deltas(self): + output = io.StringIO() + stream_log = [] + observer = PIFSAgentStreamObserver("model", stream_log=stream_log, output=output) + + observer.handle_event(self.raw_event("response.reasoning_summary_text.delta", "look up folder")) + observer.handle_event(self.raw_event("response.output_text.delta", '{"answer":')) + observer.handle_event(self.raw_event("response.output_text.delta", '"done"}')) + observer.finish() + + printed = output.getvalue() + self.assertIn("[llm reasoning summary stream]", printed) + self.assertIn("look up folder", printed) + self.assertIn("[llm final output stream]", printed) + self.assertIn('{"answer":"done"}', printed.replace("\n", "")) + self.assertEqual( + stream_log, + [ + {"kind": "output", "text": '{"answer":"done"}'}, + {"kind": "think_summary", "text": "look up folder"}, + ], + ) + + def test_tools_mode_does_not_print_model_text(self): + output = io.StringIO() + stream_log = [] + observer = PIFSAgentStreamObserver("tools", stream_log=stream_log, output=output) + + observer.handle_event(self.raw_event("response.output_text.delta", "hidden from tools mode")) + observer.handle_event(self.raw_event("response.function_call_arguments.delta", '{"command":"ls /"}')) + observer.emit_tool_call("ls /") + observer.emit_tool_result(ok=True, output='{"ok": true}', seconds=0.001) + observer.finish() + + printed = output.getvalue() + self.assertNotIn("hidden from tools mode", printed) + self.assertIn("[llm -> pifs command]", printed) + self.assertIn("ls /", printed) + self.assertIn("[pifs -> llm result preview]", printed) + self.assertIn('{"ok": true}', printed) + self.assertEqual(stream_log[0], {"kind": "tool_call", "command": "ls /"}) + self.assertEqual(stream_log[1]["kind"], "tool_result") + self.assertEqual(stream_log[2], {"kind": "tool_args", "text": '{"command":"ls /"}'}) + + def test_tool_result_preview_compacts_large_outputs(self): + output = io.StringIO() + observer = PIFSAgentStreamObserver("tools", output=output) + + observer.emit_tool_result( + ok=True, + output="\n".join(f"line {index}" for index in range(50)), + seconds=0.001, + ) + + printed = output.getvalue() + self.assertIn("[large PIFS result", printed) + self.assertIn("line 0", printed) + self.assertIn("more lines omitted from preview", printed) + self.assertNotIn("line 49", printed) + + def test_raw_reasoning_is_not_logged_by_default_but_summary_is(self): + output = io.StringIO() + stream_log = [] + previous = os.environ.pop("PAGEINDEX_PIFS_AGENT_RAW_REASONING", None) + try: + observer = PIFSAgentStreamObserver("model", stream_log=stream_log, output=output) + observer.handle_event(self.raw_event("response.reasoning_text.delta", "private chain")) + observer.handle_event( + self.raw_event("response.reasoning_summary_text.delta", "visible summary") + ) + observer.finish() + finally: + if previous is not None: + os.environ["PAGEINDEX_PIFS_AGENT_RAW_REASONING"] = previous + + printed = output.getvalue() + self.assertNotIn("private chain", printed) + self.assertIn("visible summary", printed) + self.assertEqual(stream_log, [{"kind": "think_summary", "text": "visible summary"}]) + + def test_raw_reasoning_requires_debug_env_flag(self): + self.assertFalse(pifs_agent_raw_reasoning_enabled({})) + self.assertTrue( + pifs_agent_raw_reasoning_enabled({"PAGEINDEX_PIFS_AGENT_RAW_REASONING": "on"}) + ) + self.assertTrue( + pifs_agent_raw_reasoning_enabled({"PAGEINDEX_PIFS_AGENT_RAW_REASONING": "TRUE"}) + ) + self.assertFalse( + pifs_agent_raw_reasoning_enabled({"PAGEINDEX_PIFS_AGENT_RAW_REASONING": "0"}) + ) + + def test_stream_mode_aliases(self): + self.assertEqual(normalize_agent_stream_mode("think"), "model") + self.assertEqual(normalize_agent_stream_mode("debug"), "all") + self.assertEqual(normalize_agent_stream_mode(""), "off") + with self.assertRaises(ValueError): + normalize_agent_stream_mode("nope") + + def test_reasoning_settings_enable_effort_and_summary(self): + settings = build_agent_model_settings( + reasoning_effort="medium", + reasoning_summary="detailed", + ) + + self.assertIsNotNone(settings) + self.assertEqual(settings.reasoning.effort, "medium") + self.assertEqual(settings.reasoning.summary, "detailed") + self.assertEqual(settings.verbosity, "low") + + def test_reasoning_effort_defaults_to_visible_summary(self): + settings = build_agent_model_settings(reasoning_effort="low") + + self.assertIsNotNone(settings) + self.assertEqual(settings.reasoning.effort, "low") + self.assertEqual(settings.reasoning.summary, "auto") + + def test_reasoning_and_base_url_normalization(self): + self.assertEqual(normalize_reasoning_effort("xhigh"), "xhigh") + self.assertIsNone(normalize_reasoning_summary("none")) + self.assertFalse(should_use_openai_compatible_chat_model(None)) + self.assertFalse(should_use_openai_compatible_chat_model("https://api.openai.com/v1/")) + self.assertTrue(should_use_openai_compatible_chat_model("https://example.test/v1")) + with self.assertRaises(ValueError): + normalize_reasoning_effort("maximum") + + def test_tracing_is_disabled_by_default_unless_env_enables_it(self): + self.assertTrue(should_disable_pifs_agent_tracing({})) + self.assertFalse( + should_disable_pifs_agent_tracing({"PAGEINDEX_PIFS_AGENT_TRACING": "1"}) + ) + self.assertFalse( + should_disable_pifs_agent_tracing({"PAGEINDEX_PIFS_AGENT_TRACING": "true"}) + ) + self.assertFalse( + should_disable_pifs_agent_tracing({"PAGEINDEX_PIFS_AGENT_TRACING": "on"}) + ) + self.assertTrue( + should_disable_pifs_agent_tracing({"PAGEINDEX_PIFS_AGENT_TRACING": "0"}) + ) + + def test_structured_agent_output_serializes_to_json(self): + output = serialize_agent_final_output( + StructuredAnswer(answer="done", document_ids=["dsid_1"]) + ) + + self.assertEqual(output, '{"answer":"done","document_ids":["dsid_1"]}') + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_semantic_index.py b/tests/test_semantic_index.py new file mode 100644 index 000000000..a500d9be8 --- /dev/null +++ b/tests/test_semantic_index.py @@ -0,0 +1,53 @@ +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from pageindex.filesystem.semantic_index import ( + SemanticIndexRecord, + SQLiteVecSemanticIndex, +) + + +def test_sqlite_vec_semantic_index_round_trip(tmp_path): + index = SQLiteVecSemanticIndex(tmp_path / "semantic.sqlite") + index.reset(dimension=3, metadata={"field_mode": "summary"}) + + index.upsert_many( + [ + SemanticIndexRecord( + file_ref="file_a", + external_id="doc_a", + source_type="github", + source_path="github/a.json", + title="Multipart upload limits", + text="multipart upload limits", + vector=[1.0, 0.0, 0.0], + metadata={"topic": "uploads"}, + ), + SemanticIndexRecord( + file_ref="file_b", + external_id="doc_b", + source_type="slack", + source_path="slack/b.json", + title="GPU cache issue", + text="gpu cache issue", + vector=[0.0, 1.0, 0.0], + metadata={"topic": "runtime"}, + ), + ] + ) + + assert index.info()["document_count"] == 2 + + results = index.search([0.9, 0.1, 0.0], limit=2) + assert [item.external_id for item in results] == ["doc_a", "doc_b"] + + filtered = index.search( + [0.9, 0.1, 0.0], + limit=2, + filters={"source_type": "slack"}, + ) + assert [item.external_id for item in filtered] == ["doc_b"] diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..a2161f852 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1988 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, + { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, + { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, + { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, + { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, + { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, + { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, + { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, + { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, + { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, + { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, + { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, + { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, + { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, + { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, + { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, + { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, + { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, + { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, + { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, + { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, + { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, + { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, + { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, + { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, + { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, + { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, + { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "48.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, + { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, + { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, + { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, + { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, + { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, + { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, + { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, + { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, + { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, + { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, + { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, + { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, + { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, + { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, + { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, + { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, + { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, + { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "fastuuid" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232, upload-time = "2025-10-19T22:19:22.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/a2/e78fcc5df65467f0d207661b7ef86c5b7ac62eea337c0c0fcedbeee6fb13/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", size = 510164, upload-time = "2025-10-19T22:31:45.635Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b3/c846f933f22f581f558ee63f81f29fa924acd971ce903dab1a9b6701816e/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", size = 261837, upload-time = "2025-10-19T22:38:38.53Z" }, + { url = "https://files.pythonhosted.org/packages/54/ea/682551030f8c4fa9a769d9825570ad28c0c71e30cf34020b85c1f7ee7382/fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", size = 251370, upload-time = "2025-10-19T22:40:26.07Z" }, + { url = "https://files.pythonhosted.org/packages/14/dd/5927f0a523d8e6a76b70968e6004966ee7df30322f5fc9b6cdfb0276646a/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", size = 277766, upload-time = "2025-10-19T22:37:23.779Z" }, + { url = "https://files.pythonhosted.org/packages/16/6e/c0fb547eef61293153348f12e0f75a06abb322664b34a1573a7760501336/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", size = 278105, upload-time = "2025-10-19T22:26:56.821Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b1/b9c75e03b768f61cf2e84ee193dc18601aeaf89a4684b20f2f0e9f52b62c/fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", size = 301564, upload-time = "2025-10-19T22:30:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fa/f7395fdac07c7a54f18f801744573707321ca0cee082e638e36452355a9d/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", size = 459659, upload-time = "2025-10-19T22:31:32.341Z" }, + { url = "https://files.pythonhosted.org/packages/66/49/c9fd06a4a0b1f0f048aacb6599e7d96e5d6bc6fa680ed0d46bf111929d1b/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", size = 478430, upload-time = "2025-10-19T22:26:22.962Z" }, + { url = "https://files.pythonhosted.org/packages/be/9c/909e8c95b494e8e140e8be6165d5fc3f61fdc46198c1554df7b3e1764471/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", size = 450894, upload-time = "2025-10-19T22:27:01.647Z" }, + { url = "https://files.pythonhosted.org/packages/90/eb/d29d17521976e673c55ef7f210d4cdd72091a9ec6755d0fd4710d9b3c871/fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176", size = 154374, upload-time = "2025-10-19T22:29:19.879Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fc/f5c799a6ea6d877faec0472d0b27c079b47c86b1cdc577720a5386483b36/fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397", size = 156550, upload-time = "2025-10-19T22:27:49.658Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021", size = 510720, upload-time = "2025-10-19T22:42:34.633Z" }, + { url = "https://files.pythonhosted.org/packages/53/b0/a4b03ff5d00f563cc7546b933c28cb3f2a07344b2aec5834e874f7d44143/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc", size = 262024, upload-time = "2025-10-19T22:30:25.482Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6d/64aee0a0f6a58eeabadd582e55d0d7d70258ffdd01d093b30c53d668303b/fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5", size = 251679, upload-time = "2025-10-19T22:36:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/a7e9cda8369e4f7919d36552db9b2ae21db7915083bc6336f1b0082c8b2e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f", size = 277862, upload-time = "2025-10-19T22:36:23.302Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/8ce11827c783affffd5bd4d6378b28eb6cc6d2ddf41474006b8d62e7448e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87", size = 278278, upload-time = "2025-10-19T22:29:43.809Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/680fb6352d0bbade04036da46264a8001f74b7484e2fd1f4da9e3db1c666/fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b", size = 301788, upload-time = "2025-10-19T22:36:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7c/2014b5785bd8ebdab04ec857635ebd84d5ee4950186a577db9eff0fb8ff6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022", size = 459819, upload-time = "2025-10-19T22:35:31.623Z" }, + { url = "https://files.pythonhosted.org/packages/01/d2/524d4ceeba9160e7a9bc2ea3e8f4ccf1ad78f3bde34090ca0c51f09a5e91/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995", size = 478546, upload-time = "2025-10-19T22:26:03.023Z" }, + { url = "https://files.pythonhosted.org/packages/bc/17/354d04951ce114bf4afc78e27a18cfbd6ee319ab1829c2d5fb5e94063ac6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab", size = 450921, upload-time = "2025-10-19T22:31:02.151Z" }, + { url = "https://files.pythonhosted.org/packages/fb/be/d7be8670151d16d88f15bb121c5b66cdb5ea6a0c2a362d0dcf30276ade53/fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad", size = 154559, upload-time = "2025-10-19T22:36:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/22/1d/5573ef3624ceb7abf4a46073d3554e37191c868abc3aecd5289a72f9810a/fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed", size = 156539, upload-time = "2025-10-19T22:33:35.898Z" }, + { url = "https://files.pythonhosted.org/packages/16/c9/8c7660d1fe3862e3f8acabd9be7fc9ad71eb270f1c65cce9a2b7a31329ab/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad", size = 510600, upload-time = "2025-10-19T22:43:44.17Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f4/a989c82f9a90d0ad995aa957b3e572ebef163c5299823b4027986f133dfb/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b", size = 262069, upload-time = "2025-10-19T22:43:38.38Z" }, + { url = "https://files.pythonhosted.org/packages/da/6c/a1a24f73574ac995482b1326cf7ab41301af0fabaa3e37eeb6b3df00e6e2/fastuuid-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714", size = 251543, upload-time = "2025-10-19T22:32:22.537Z" }, + { url = "https://files.pythonhosted.org/packages/1a/20/2a9b59185ba7a6c7b37808431477c2d739fcbdabbf63e00243e37bd6bf49/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f", size = 277798, upload-time = "2025-10-19T22:33:53.821Z" }, + { url = "https://files.pythonhosted.org/packages/ef/33/4105ca574f6ded0af6a797d39add041bcfb468a1255fbbe82fcb6f592da2/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f", size = 278283, upload-time = "2025-10-19T22:29:02.812Z" }, + { url = "https://files.pythonhosted.org/packages/fe/8c/fca59f8e21c4deb013f574eae05723737ddb1d2937ce87cb2a5d20992dc3/fastuuid-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75", size = 301627, upload-time = "2025-10-19T22:35:54.985Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e2/f78c271b909c034d429218f2798ca4e89eeda7983f4257d7865976ddbb6c/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4", size = 459778, upload-time = "2025-10-19T22:28:00.999Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f0/5ff209d865897667a2ff3e7a572267a9ced8f7313919f6d6043aed8b1caa/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad", size = 478605, upload-time = "2025-10-19T22:36:21.764Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c8/2ce1c78f983a2c4987ea865d9516dbdfb141a120fd3abb977ae6f02ba7ca/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8", size = 450837, upload-time = "2025-10-19T22:34:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/df/60/dad662ec9a33b4a5fe44f60699258da64172c39bd041da2994422cdc40fe/fastuuid-0.14.0-cp314-cp314-win32.whl", hash = "sha256:e23fc6a83f112de4be0cc1990e5b127c27663ae43f866353166f87df58e73d06", size = 154532, upload-time = "2025-10-19T22:35:18.217Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/da4db31001e854025ffd26bc9ba0740a9cbba2c3259695f7c5834908b336/fastuuid-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:df61342889d0f5e7a32f7284e55ef95103f2110fee433c2ae7c2c0956d76ac8a", size = 156457, upload-time = "2025-10-19T22:33:44.579Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/8d/1c51c094345df128ca4a990d633fe1a0ff28726c9e6b3c41ba65087bba1d/fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4", size = 312760, upload-time = "2026-04-29T20:42:38.635Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, +] + +[[package]] +name = "griffelib" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" }, + { url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" }, + { url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" }, + { url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" }, + { url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" }, + { url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" }, + { url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" }, + { url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" }, + { url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" }, + { url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" }, + { url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" }, + { url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" }, + { url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/40/43109e943fd718b0ccd0cd61eb4f1c347df22bf81f5874c6f22adf44bcff/huggingface_hub-1.14.0.tar.gz", hash = "sha256:d6d2c9cd6be1d02ae9ec6672d5587d10a427f377db688e82528f426a041622c2", size = 782365, upload-time = "2026-05-06T14:14:34.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/a5/33b49ba7bea7c41bb37f74ec0f8beea0831e052330196633fe2c77516ea6/huggingface_hub-1.14.0-py3-none-any.whl", hash = "sha256:efe075535c62e130b30e836b138e13785f6f043d1f0539e0a39aa411a99e90b8", size = 661479, upload-time = "2026-05-06T14:14:32.029Z" }, +] + +[[package]] +name = "idna" +version = "3.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/c1/0cddc6eb17d4c53a99840953f95dd3accdc5cfc7a337b0e9b26476276be9/jiter-0.14.0.tar.gz", hash = "sha256:e8a39e66dac7153cf3f964a12aad515afa8d74938ec5cc0018adcdae5367c79e", size = 165725, upload-time = "2026-04-10T14:28:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/68/7390a418f10897da93b158f2d5a8bd0bcd73a0f9ec3bb36917085bb759ef/jiter-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2fb2ce3a7bc331256dfb14cefc34832366bb28a9aca81deaf43bbf2a5659e607", size = 316295, upload-time = "2026-04-10T14:26:24.887Z" }, + { url = "https://files.pythonhosted.org/packages/60/a0/5854ac00ff63551c52c6c89534ec6aba4b93474e7924d64e860b1c94165b/jiter-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5252a7ca23785cef5d02d4ece6077a1b556a410c591b379f82091c3001e14844", size = 315898, upload-time = "2026-04-10T14:26:26.601Z" }, + { url = "https://files.pythonhosted.org/packages/41/a1/4f44832650a16b18e8391f1bf1d6ca4909bc738351826bcc198bba4357f4/jiter-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c409578cbd77c338975670ada777add4efd53379667edf0aceea730cabede6fb", size = 343730, upload-time = "2026-04-10T14:26:28.326Z" }, + { url = "https://files.pythonhosted.org/packages/48/64/a329e9d469f86307203594b1707e11ae51c3348d03bfd514a5f997870012/jiter-0.14.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ede4331a1899d604463369c730dbb961ffdc5312bc7f16c41c2896415b1304a", size = 370102, upload-time = "2026-04-10T14:26:30.089Z" }, + { url = "https://files.pythonhosted.org/packages/94/c1/5e3dfc59635aa4d4c7bd20a820ac1d09b8ed851568356802cf1c08edb3cf/jiter-0.14.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92cd8b6025981a041f5310430310b55b25ca593972c16407af8837d3d7d2ca01", size = 461335, upload-time = "2026-04-10T14:26:31.911Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1b/dd157009dbc058f7b00108f545ccb72a2d56461395c4fc7b9cfdccb00af4/jiter-0.14.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:351bf6eda4e3a7ceb876377840c702e9a3e4ecc4624dbfb2d6463c67ae52637d", size = 378536, upload-time = "2026-04-10T14:26:33.595Z" }, + { url = "https://files.pythonhosted.org/packages/91/78/256013667b7c10b8834f8e6e54cd3e562d4c6e34227a1596addccc05e38c/jiter-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dcfbeb93d9ecd9ca128bbf8910120367777973fa193fb9a39c31237d8df165", size = 353859, upload-time = "2026-04-10T14:26:35.098Z" }, + { url = "https://files.pythonhosted.org/packages/de/d9/137d65ade9093a409fe80955ce60b12bb753722c986467aeda47faf450ad/jiter-0.14.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ae039aaef8de3f8157ecc1fdd4d85043ac4f57538c245a0afaecb8321ec951c3", size = 357626, upload-time = "2026-04-10T14:26:36.685Z" }, + { url = "https://files.pythonhosted.org/packages/2e/48/76750835b87029342727c1a268bea8878ab988caf81ee4e7b880900eeb5a/jiter-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7d9d51eb96c82a9652933bd769fe6de66877d6eb2b2440e281f2938c51b5643e", size = 393172, upload-time = "2026-04-10T14:26:38.097Z" }, + { url = "https://files.pythonhosted.org/packages/a6/60/456c4e81d5c8045279aefe60e9e483be08793828800a4e64add8fdde7f2a/jiter-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d824ca4148b705970bf4e120924a212fdfca9859a73e42bd7889a63a4ea6bb98", size = 520300, upload-time = "2026-04-10T14:26:39.532Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9f/2020e0984c235f678dced38fe4eec3058cf528e6af36ebf969b410305941/jiter-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff3a6465b3a0f54b1a430f45c3c0ba7d61ceb45cbc3e33f9e1a7f638d690baf3", size = 553059, upload-time = "2026-04-10T14:26:40.991Z" }, + { url = "https://files.pythonhosted.org/packages/ef/32/e2d298e1a22a4bbe6062136d1c7192db7dba003a6975e51d9a9eecabc4c2/jiter-0.14.0-cp312-cp312-win32.whl", hash = "sha256:5dec7c0a3e98d2a3f8a2e67382d0d7c3ac60c69103a4b271da889b4e8bb1e129", size = 206030, upload-time = "2026-04-10T14:26:42.517Z" }, + { url = "https://files.pythonhosted.org/packages/36/ac/96369141b3d8a4a8e4590e983085efe1c436f35c0cda940dd76d942e3e40/jiter-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:fc7e37b4b8bc7e80a63ad6cfa5fc11fab27dbfea4cc4ae644b1ab3f273dc348f", size = 201603, upload-time = "2026-04-10T14:26:44.328Z" }, + { url = "https://files.pythonhosted.org/packages/01/c3/75d847f264647017d7e3052bbcc8b1e24b95fa139c320c5f5066fa7a0bdd/jiter-0.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:ee4a72f12847ef29b072aee9ad5474041ab2924106bdca9fcf5d7d965853e057", size = 191525, upload-time = "2026-04-10T14:26:46Z" }, + { url = "https://files.pythonhosted.org/packages/97/2a/09f70020898507a89279659a1afe3364d57fc1b2c89949081975d135f6f5/jiter-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:af72f204cf4d44258e5b4c1745130ac45ddab0e71a06333b01de660ab4187a94", size = 315502, upload-time = "2026-04-10T14:26:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/d6/be/080c96a45cd74f9fce5db4fd68510b88087fb37ffe2541ff73c12db92535/jiter-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4b77da71f6e819be5fbcec11a453fde5b1d0267ef6ed487e2a392fd8e14e4e3a", size = 314870, upload-time = "2026-04-10T14:26:49.149Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5e/2d0fee155826a968a832cc32438de5e2a193292c8721ca70d0b53e58245b/jiter-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f4ea612fe8b84b8b04e51d0e78029ecf3466348e25973f953de6e6a59aa4c1", size = 343406, upload-time = "2026-04-10T14:26:50.762Z" }, + { url = "https://files.pythonhosted.org/packages/70/af/bf9ee0d3a4f8dc0d679fc1337f874fe60cdbf841ebbb304b374e1c9aaceb/jiter-0.14.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62fe2451f8fcc0240261e6a4df18ecbcd58327857e61e625b2393ea3b468aac9", size = 369415, upload-time = "2026-04-10T14:26:52.188Z" }, + { url = "https://files.pythonhosted.org/packages/0f/83/8e8561eadba31f4d3948a5b712fb0447ec71c3560b57a855449e7b8ddc98/jiter-0.14.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6112f26f5afc75bcb475787d29da3aa92f9d09c7858f632f4be6ffe607be82e9", size = 461456, upload-time = "2026-04-10T14:26:53.611Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c9/c5299e826a5fe6108d172b344033f61c69b1bb979dd8d9ddd4278a160971/jiter-0.14.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:215a6cb8fb7dc702aa35d475cc00ddc7f970e5c0b1417fb4b4ac5d82fa2a29db", size = 378488, upload-time = "2026-04-10T14:26:55.211Z" }, + { url = "https://files.pythonhosted.org/packages/5d/37/c16d9d15c0a471b8644b1abe3c82668092a707d9bedcf076f24ff2e380cd/jiter-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ab96a30fb3cb2c7e0cd33f7616c8860da5f5674438988a54ac717caccdbaa", size = 353242, upload-time = "2026-04-10T14:26:56.705Z" }, + { url = "https://files.pythonhosted.org/packages/58/ea/8050cb0dc654e728e1bfacbc0c640772f2181af5dedd13ae70145743a439/jiter-0.14.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3a99c1387b1f2928f799a9de899193484d66206a50e98233b6b088a7f0c1edb2", size = 356823, upload-time = "2026-04-10T14:26:58.281Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/cf71506d270e5f84d97326bf220e47aed9b95e9a4a060758fb07772170ab/jiter-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ab18d11074485438695f8d34a1b6da61db9754248f96d51341956607a8f39985", size = 392564, upload-time = "2026-04-10T14:27:00.018Z" }, + { url = "https://files.pythonhosted.org/packages/b0/cc/8c6c74a3efb5bd671bfd14f51e8a73375464ca914b1551bc3b40e26ac2c9/jiter-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:801028dcfc26ac0895e4964cbc0fd62c73be9fd4a7d7b1aaf6e5790033a719b7", size = 520322, upload-time = "2026-04-10T14:27:01.664Z" }, + { url = "https://files.pythonhosted.org/packages/41/24/68d7b883ec959884ddf00d019b2e0e82ba81b167e1253684fa90519ce33c/jiter-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ad425b087aafb4a1c7e1e98a279200743b9aaf30c3e0ba723aec93f061bd9bc8", size = 552619, upload-time = "2026-04-10T14:27:03.316Z" }, + { url = "https://files.pythonhosted.org/packages/b6/89/b1a0985223bbf3150ff9e8f46f98fc9360c1de94f48abe271bbe1b465682/jiter-0.14.0-cp313-cp313-win32.whl", hash = "sha256:882bcb9b334318e233950b8be366fe5f92c86b66a7e449e76975dfd6d776a01f", size = 205699, upload-time = "2026-04-10T14:27:04.662Z" }, + { url = "https://files.pythonhosted.org/packages/4c/19/3f339a5a7f14a11730e67f6be34f9d5105751d547b615ef593fa122a5ded/jiter-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:9b8c571a5dba09b98bd3462b5a53f27209a5cbbe85670391692ede71974e979f", size = 201323, upload-time = "2026-04-10T14:27:06.139Z" }, + { url = "https://files.pythonhosted.org/packages/50/56/752dd89c84be0e022a8ea3720bcfa0a8431db79a962578544812ce061739/jiter-0.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:34f19dcc35cb1abe7c369b3756babf8c7f04595c0807a848df8f26ef8298ef92", size = 191099, upload-time = "2026-04-10T14:27:07.564Z" }, + { url = "https://files.pythonhosted.org/packages/91/28/292916f354f25a1fe8cf2c918d1415c699a4a659ae00be0430e1c5d9ffea/jiter-0.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e89bcd7d426a75bb4952c696b267075790d854a07aad4c9894551a82c5b574ab", size = 320880, upload-time = "2026-04-10T14:27:09.326Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c7/b002a7d8b8957ac3d469bd59c18ef4b1595a5216ae0de639a287b9816023/jiter-0.14.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b25beaa0d4447ea8c7ae0c18c688905d34840d7d0b937f2f7bdd52162c98a40", size = 346563, upload-time = "2026-04-10T14:27:11.287Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3b/f8d07580d8706021d255a6356b8fab13ee4c869412995550ce6ed4ddf97d/jiter-0.14.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:651a8758dd413c51e3b7f6557cdc6921faf70b14106f45f969f091f5cda990ea", size = 357928, upload-time = "2026-04-10T14:27:12.729Z" }, + { url = "https://files.pythonhosted.org/packages/47/5b/ac1a974da29e35507230383110ffec59998b290a8732585d04e19a9eb5ba/jiter-0.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e1a7eead856a5038a8d291f1447176ab0b525c77a279a058121b5fccee257f6f", size = 203519, upload-time = "2026-04-10T14:27:14.125Z" }, + { url = "https://files.pythonhosted.org/packages/96/6d/9fc8433d667d2454271378a79747d8c76c10b51b482b454e6190e511f244/jiter-0.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e692633a12cda97e352fdcd1c4acc971b1c28707e1e33aeef782b0cbf051975", size = 190113, upload-time = "2026-04-10T14:27:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/4f/1e/354ed92461b165bd581f9ef5150971a572c873ec3b68a916d5aa91da3cc2/jiter-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:6f396837fc7577871ca8c12edaf239ed9ccef3bbe39904ae9b8b63ce0a48b140", size = 315277, upload-time = "2026-04-10T14:27:18.109Z" }, + { url = "https://files.pythonhosted.org/packages/a6/95/8c7c7028aa8636ac21b7a55faef3e34215e6ed0cbf5ae58258427f621aa3/jiter-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a4d50ea3d8ba4176f79754333bd35f1bbcd28e91adc13eb9b7ca91bc52a6cef9", size = 315923, upload-time = "2026-04-10T14:27:19.603Z" }, + { url = "https://files.pythonhosted.org/packages/47/40/e2a852a44c4a089f2681a16611b7ce113224a80fd8504c46d78491b47220/jiter-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce17f8a050447d1b4153bda4fb7d26e6a9e74eb4f4a41913f30934c5075bf615", size = 344943, upload-time = "2026-04-10T14:27:21.262Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1f/670f92adee1e9895eac41e8a4d623b6da68c4d46249d8b556b60b63f949e/jiter-0.14.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4f1c4b125e1652aefbc2e2c1617b60a160ab789d180e3d423c41439e5f32850", size = 369725, upload-time = "2026-04-10T14:27:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/01/2f/541c9ba567d05de1c4874a0f8f8c5e3fd78e2b874266623da9a775cf46e0/jiter-0.14.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be808176a6a3a14321d18c603f2d40741858a7c4fc982f83232842689fe86dd9", size = 461210, upload-time = "2026-04-10T14:27:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/c31cbec09627e0d5de7aeaec7690dba03e090caa808fefd8133137cf45bc/jiter-0.14.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26679d58ba816f88c3849306dd58cb863a90a1cf352cdd4ef67e30ccf8a77994", size = 380002, upload-time = "2026-04-10T14:27:26.155Z" }, + { url = "https://files.pythonhosted.org/packages/50/02/3c05c1666c41904a2f607475a73e7a4763d1cbde2d18229c4f85b22dc253/jiter-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80381f5a19af8fa9aef743f080e34f6b25ebd89656475f8cf0470ec6157052aa", size = 354678, upload-time = "2026-04-10T14:27:27.701Z" }, + { url = "https://files.pythonhosted.org/packages/7d/97/e15b33545c2b13518f560d695f974b9891b311641bdcf178d63177e8801e/jiter-0.14.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:004df5fdb8ecbd6d99f3227df18ba1a259254c4359736a2e6f036c944e02d7c5", size = 358920, upload-time = "2026-04-10T14:27:29.256Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d2/8b1461def6b96ba44530df20d07ef7a1c7da22f3f9bf1727e2d611077bf1/jiter-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cff5708f7ed0fa098f2b53446c6fa74c48469118e5cd7497b4f1cd569ab06928", size = 394512, upload-time = "2026-04-10T14:27:31.344Z" }, + { url = "https://files.pythonhosted.org/packages/e3/88/837566dd6ed6e452e8d3205355afd484ce44b2533edfa4ed73a298ea893e/jiter-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:2492e5f06c36a976d25c7cc347a60e26d5470178d44cde1b9b75e60b4e519f28", size = 521120, upload-time = "2026-04-10T14:27:33.299Z" }, + { url = "https://files.pythonhosted.org/packages/89/6b/b00b45c4d1b4c031777fe161d620b755b5b02cdade1e316dcb46e4471d63/jiter-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7609cfbe3a03d37bfdbf5052012d5a879e72b83168a363deae7b3a26564d57de", size = 553668, upload-time = "2026-04-10T14:27:34.868Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d8/6fe5b42011d19397433d345716eac16728ac241862a2aac9c91923c7509a/jiter-0.14.0-cp314-cp314-win32.whl", hash = "sha256:7282342d32e357543565286b6450378c3cd402eea333fc1ebe146f1fabb306fc", size = 207001, upload-time = "2026-04-10T14:27:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/e5/43/5c2e08da1efad5e410f0eaaabeadd954812612c33fbbd8fd5328b489139d/jiter-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:bd77945f38866a448e73b0b7637366afa814d4617790ecd88a18ca74377e6c02", size = 202187, upload-time = "2026-04-10T14:27:38Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1f/6e39ac0b4cdfa23e606af5b245df5f9adaa76f35e0c5096790da430ca506/jiter-0.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:f2d4c61da0821ee42e0cdf5489da60a6d074306313a377c2b35af464955a3611", size = 192257, upload-time = "2026-04-10T14:27:39.504Z" }, + { url = "https://files.pythonhosted.org/packages/05/57/7dbc0ffbbb5176a27e3518716608aa464aee2e2887dc938f0b900a120449/jiter-0.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1bf7ff85517dd2f20a5750081d2b75083c1b269cf75afc7511bdf1f9548beb3b", size = 323441, upload-time = "2026-04-10T14:27:41.039Z" }, + { url = "https://files.pythonhosted.org/packages/83/6e/7b3314398d8983f06b557aa21b670511ec72d3b79a68ee5e4d9bff972286/jiter-0.14.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8ef8791c3e78d6c6b157c6d360fbb5c715bebb8113bc6a9303c5caff012754a", size = 348109, upload-time = "2026-04-10T14:27:42.552Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4f/8dc674bcd7db6dba566de73c08c763c337058baff1dbeb34567045b27cdc/jiter-0.14.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e74663b8b10da1fe0f4e4703fd7980d24ad17174b6bb35d8498d6e3ebce2ae6a", size = 368328, upload-time = "2026-04-10T14:27:44.574Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5f/188e09a1f20906f98bbdec44ed820e19f4e8eb8aff88b9d1a5a497587ff3/jiter-0.14.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1aca29ba52913f78362ec9c2da62f22cdc4c3083313403f90c15460979b84d9b", size = 463301, upload-time = "2026-04-10T14:27:46.717Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f0/19046ef965ed8f349e8554775bb12ff4352f443fbe12b95d31f575891256/jiter-0.14.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b39b7d87a952b79949af5fef44d2544e58c21a28da7f1bae3ef166455c61746", size = 378891, upload-time = "2026-04-10T14:27:48.32Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c3/da43bd8431ee175695777ee78cf0e93eacbb47393ff493f18c45231b427d/jiter-0.14.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d918a68b26e9fab068c2b5453577ef04943ab2807b9a6275df2a812599a310", size = 360749, upload-time = "2026-04-10T14:27:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/72/26/e054771be889707c6161dbdec9c23d33a9ec70945395d70f07cfea1e9a6f/jiter-0.14.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:b08997c35aee1201c1a5361466a8fb9162d03ae7bf6568df70b6c859f1e654a4", size = 358526, upload-time = "2026-04-10T14:27:51.504Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0f/7bea65ea2a6d91f2bf989ff11a18136644392bf2b0497a1fa50934c30a9c/jiter-0.14.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:260bf7ca20704d58d41f669e5e9fe7fe2fa72901a6b324e79056f5d52e9c9be2", size = 393926, upload-time = "2026-04-10T14:27:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/b1ff7d70deef61ac0b7c6c2f12d2ace950cdeecb4fdc94500a0926802857/jiter-0.14.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:37826e3df29e60f30a382f9294348d0238ef127f4b5d7f5f8da78b5b9e050560", size = 521052, upload-time = "2026-04-10T14:27:55.058Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7b/3b0649983cbaf15eda26a414b5b1982e910c67bd6f7b1b490f3cfc76896a/jiter-0.14.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:645be49c46f2900937ba0eaf871ad5183c96858c0af74b6becc7f4e367e36e06", size = 553716, upload-time = "2026-04-10T14:27:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/97/f8/33d78c83bd93ae0c0af05293a6660f88a1977caef39a6d72a84afab94ce0/jiter-0.14.0-cp314-cp314t-win32.whl", hash = "sha256:2f7877ed45118de283786178eceaf877110abacd04fde31efff3940ae9672674", size = 207957, upload-time = "2026-04-10T14:27:59.285Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ac/2b760516c03e2227826d1f7025d89bf6bf6357a28fe75c2a2800873c50bf/jiter-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:14c0cb10337c49f5eafe8e7364daca5e29a020ea03580b8f8e6c597fed4e1588", size = 204690, upload-time = "2026-04-10T14:28:00.962Z" }, + { url = "https://files.pythonhosted.org/packages/dc/2e/a44c20c58aeed0355f2d326969a181696aeb551a25195f47563908a815be/jiter-0.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5419d4aa2024961da9fe12a9cfe7484996735dca99e8e090b5c88595ef1951ff", size = 191338, upload-time = "2026-04-10T14:28:02.853Z" }, + { url = "https://files.pythonhosted.org/packages/21/42/9042c3f3019de4adcb8c16591c325ec7255beea9fcd33a42a43f3b0b1000/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:fbd9e482663ca9d005d051330e4d2d8150bb208a209409c10f7e7dfdf7c49da9", size = 308810, upload-time = "2026-04-10T14:28:34.673Z" }, + { url = "https://files.pythonhosted.org/packages/60/cf/a7e19b308bd86bb04776803b1f01a5f9a287a4c55205f4708827ee487fbf/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:33a20d838b91ef376b3a56896d5b04e725c7df5bc4864cc6569cf046a8d73b6d", size = 308443, upload-time = "2026-04-10T14:28:36.658Z" }, + { url = "https://files.pythonhosted.org/packages/ca/44/e26ede3f0caeff93f222559cb0cc4ca68579f07d009d7b6010c5b586f9b1/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:432c4db5255d86a259efde91e55cb4c8d18c0521d844c9e2e7efcce3899fb016", size = 343039, upload-time = "2026-04-10T14:28:38.356Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/1f9ada30cef7b05e74bb06f52127e7a724976c225f46adb65c37b1dadfb6/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f00d94b281174144d6532a04b66a12cb866cbdc47c3af3bfe2973677f9861a", size = 349613, upload-time = "2026-04-10T14:28:40.066Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778, upload-time = "2024-07-08T18:40:05.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462, upload-time = "2024-07-08T18:40:00.165Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "litellm" +version = "1.83.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "fastuuid" }, + { name = "httpx" }, + { name = "importlib-metadata" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "tiktoken" }, + { name = "tokenizers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/92/6ce9737554994ca8e536e5f4f6a87cc7c4774b656c9eb9add071caf7d54b/litellm-1.83.0.tar.gz", hash = "sha256:860bebc76c4bb27b4cf90b4a77acd66dba25aced37e3db98750de8a1766bfb7a", size = 17333062, upload-time = "2026-03-31T05:08:25.331Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/2c/a670cc050fcd6f45c6199eb99e259c73aea92edba8d5c2fc1b3686d36217/litellm-1.83.0-py3-none-any.whl", hash = "sha256:88c536d339248f3987571493015784671ba3f193a328e1ea6780dbebaa2094a8", size = 15610306, upload-time = "2026-03-31T05:08:21.987Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mcp" +version = "1.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/83/d1efe7c2980d8a3afa476f4e3d42d53dd54c0ab94c27bee5d755b45c8b73/mcp-1.27.1.tar.gz", hash = "sha256:0f47e1820f8f8f941466b39749eb1d1839a04caddca2bc60e9d46e8a99914924", size = 608458, upload-time = "2026-05-08T16:50:12.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/73/42d9596facebdb533b7f0b86c1b0364ef350d1f8ba78b1052e8a58b48b65/mcp-1.27.1-py3-none-any.whl", hash = "sha256:1af3c4203b329430fde7a87b4fcb6392a041f5cb851fd68fc674016ab4e7c06f", size = 216260, upload-time = "2026-05-08T16:50:10.547Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "openai" +version = "2.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/15/52580c8fbc16d0675d516e8749806eda679b16de1e4434ea06fb6feaa610/openai-2.30.0.tar.gz", hash = "sha256:92f7661c990bda4b22a941806c83eabe4896c3094465030dd882a71abe80c885", size = 676084, upload-time = "2026-03-25T22:08:59.96Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/9e/5bfa2270f902d5b92ab7d41ce0475b8630572e71e349b2a4996d14bdda93/openai-2.30.0-py3-none-any.whl", hash = "sha256:9a5ae616888eb2748ec5e0c5b955a51592e0b201a11f4262db920f2a78c5231d", size = 1146656, upload-time = "2026-03-25T22:08:58.2Z" }, +] + +[[package]] +name = "openai-agents" +version = "0.17.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffelib" }, + { name = "mcp" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "types-requests" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/cd/14acaf94c6a438cfe72c5ea043bfbc77d7fbb9514ab7796d82f2180d1518/openai_agents-0.17.2.tar.gz", hash = "sha256:5e11414bdd8c20c8e9192d21f78265d30e65992f4537f940309eca9255804449", size = 5403689, upload-time = "2026-05-12T03:14:57.433Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/71/5ba9afa4b6a0d250bfdbdd1cf9a5255ae131a7f82915c634ac50c0633c3a/openai_agents-0.17.2-py3-none-any.whl", hash = "sha256:1b3560c1690bcee635a487f77ebfb8b4fb2dd52a653e045a86e51974ab87faf3", size = 838225, upload-time = "2026-05-12T03:14:55.149Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pageindex" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "litellm" }, + { name = "openai-agents" }, + { name = "pymupdf" }, + { name = "pypdf2" }, + { name = "pytest" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "sqlite-vec" }, +] + +[package.metadata] +requires-dist = [ + { name = "litellm", specifier = "==1.83.0" }, + { name = "openai-agents", specifier = ">=0.17.2" }, + { name = "pymupdf", specifier = "==1.26.4" }, + { name = "pypdf2", specifier = "==3.0.1" }, + { name = "pytest", specifier = ">=9.0.3" }, + { name = "python-dotenv", specifier = "==1.1.0" }, + { name = "pyyaml", specifier = "==6.0.2" }, + { name = "sqlite-vec", specifier = ">=0.1.9" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "propcache" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pymupdf" +version = "1.26.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/35/031556dfc0d332d8e9ed9b61ca105138606d3f8971b9eb02e20118629334/pymupdf-1.26.4.tar.gz", hash = "sha256:be13a066d42bfaed343a488168656637c4d9843ddc63b768dc827c9dfc6b9989", size = 83077563, upload-time = "2025-08-25T14:20:29.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/ae/3be722886cc7be2093585cd94f466db1199133ab005645a7a567b249560f/pymupdf-1.26.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cb95562a0a63ce906fd788bdad5239063b63068cf4a991684f43acb09052cb99", size = 23061974, upload-time = "2025-08-25T14:16:58.811Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b0/9a451d837e1fe18ecdbfbc34a6499f153c8a008763229cc634725383a93f/pymupdf-1.26.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:67e9e6b45832c33726651c2a031e9a20108fd9e759140b9e843f934de813a7ff", size = 22410112, upload-time = "2025-08-25T14:17:24.511Z" }, + { url = "https://files.pythonhosted.org/packages/d8/13/0916e8e02cb5453161fb9d9167c747d0a20d58633e30728645374153f815/pymupdf-1.26.4-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2604f687dd02b6a1b98c81bd8becfc0024899a2d2085adfe3f9e91607721fd22", size = 23454948, upload-time = "2025-08-25T21:20:07.71Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c6/d3cfafc75d383603884edeabe4821a549345df954a88d79e6764e2c87601/pymupdf-1.26.4-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:973a6dda61ebd34040e4df3753bf004b669017663fbbfdaa294d44eceba98de0", size = 24060686, upload-time = "2025-08-25T14:17:56.536Z" }, + { url = "https://files.pythonhosted.org/packages/72/08/035e9d22c801e801bba50c6745bc90ba8696a042fe2c68793e28bf0c3b07/pymupdf-1.26.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:299a49797df5b558e695647fa791329ba3911cbbb31ed65f24a6266c118ef1a7", size = 24265046, upload-time = "2025-08-25T14:18:21.238Z" }, + { url = "https://files.pythonhosted.org/packages/28/8c/c201e4846ec0fb6ae5d52aa3a5d66f9355f0c69fb94230265714df0de65e/pymupdf-1.26.4-cp39-abi3-win32.whl", hash = "sha256:51b38379aad8c71bd7a8dd24d93fbe7580c2a5d9d7e1f9cd29ebbba315aa1bd1", size = 17127332, upload-time = "2025-08-25T14:18:39.132Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c4/87d27b108c2f6d773aa5183c5ae367b2a99296ea4bc16eb79f453c679e30/pymupdf-1.26.4-cp39-abi3-win_amd64.whl", hash = "sha256:0b6345a93a9afd28de2567e433055e873205c52e6b920b129ca50e836a3aeec6", size = 18743491, upload-time = "2025-08-25T14:19:01.104Z" }, +] + +[[package]] +name = "pypdf2" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/bb/18dc3062d37db6c491392007dfd1a7f524bb95886eb956569ac38a23a784/PyPDF2-3.0.1.tar.gz", hash = "sha256:a74408f69ba6271f71b9352ef4ed03dc53a31aa404d29b5d31f53bfecfee1440", size = 227419, upload-time = "2022-12-31T10:36:13.13Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/5e/c86a5643653825d3c913719e788e41386bee415c2b87b4f955432f2de6b2/pypdf2-3.0.1-py3-none-any.whl", hash = "sha256:d16e4205cfee272fbdc0568b68d82be796540b1537508cef59388f839c191928", size = 232572, upload-time = "2022-12-31T10:36:10.327Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/54/a85eb421fbdd5007bc5af39d0f4ed9fa609e0fedbfdc2adcf0b34526870e/python_multipart-0.0.28.tar.gz", hash = "sha256:8550da197eac0f7ab748961fc9509b999fa2662ea25cef857f05249f6893c0f8", size = 45314, upload-time = "2026-05-10T11:05:16.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/a2/43bbc5860b5034e2af4ef99a0e04d726ff329c43e192ef3abaa8d7ecfce5/python_multipart-0.0.28-py3-none-any.whl", hash = "sha256:10faac07eb966c3f48dc415f9dee46c04cb10d58d30a35677db8027c825ed9b6", size = 29438, upload-time = "2026-05-10T11:05:15.052Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "regex" +version = "2026.5.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270", size = 416074, upload-time = "2026-05-09T23:15:19.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06", size = 490451, upload-time = "2026-05-09T23:12:34.72Z" }, + { url = "https://files.pythonhosted.org/packages/1e/95/fc7ba4303b5a0f92446a12ee6778ef2c6c799233f5060042a31bf390cfe9/regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6", size = 292112, upload-time = "2026-05-09T23:12:36.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225", size = 289599, upload-time = "2026-05-09T23:12:38.089Z" }, + { url = "https://files.pythonhosted.org/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0", size = 796732, upload-time = "2026-05-09T23:12:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107", size = 865440, upload-time = "2026-05-09T23:12:42.059Z" }, + { url = "https://files.pythonhosted.org/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309", size = 912329, upload-time = "2026-05-09T23:12:44.373Z" }, + { url = "https://files.pythonhosted.org/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8", size = 801239, upload-time = "2026-05-09T23:12:46.268Z" }, + { url = "https://files.pythonhosted.org/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66", size = 777054, upload-time = "2026-05-09T23:12:48.051Z" }, + { url = "https://files.pythonhosted.org/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026", size = 785098, upload-time = "2026-05-09T23:12:49.851Z" }, + { url = "https://files.pythonhosted.org/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962", size = 860095, upload-time = "2026-05-09T23:12:51.666Z" }, + { url = "https://files.pythonhosted.org/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621", size = 765762, upload-time = "2026-05-09T23:12:53.413Z" }, + { url = "https://files.pythonhosted.org/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d", size = 852100, upload-time = "2026-05-09T23:12:55.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce", size = 789479, upload-time = "2026-05-09T23:12:57.573Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1c/bdcc98f9a4af4fdd166c74941174619ccff4726d3ce32faa8e9a2ecd38dd/regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e", size = 266699, upload-time = "2026-05-09T23:12:59.14Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/240d36864f9e48ace85f72e79ced97ceb7f27ce87739a947dcb834b4e6bc/regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e", size = 277783, upload-time = "2026-05-09T23:13:00.789Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b5/7b30f312b0669dff5beebe5b0989dc2d1a312b1a44fab852199c387a5b96/regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070", size = 270513, upload-time = "2026-05-09T23:13:02.426Z" }, + { url = "https://files.pythonhosted.org/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb", size = 490303, upload-time = "2026-05-09T23:13:04.382Z" }, + { url = "https://files.pythonhosted.org/packages/44/da/bf30abaaa737b58f4a4b8c4a03659e02fd92092c822e0197ed9e0daab917/regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f", size = 292019, upload-time = "2026-05-09T23:13:06.022Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c", size = 289468, upload-time = "2026-05-09T23:13:08.214Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed", size = 796749, upload-time = "2026-05-09T23:13:10.573Z" }, + { url = "https://files.pythonhosted.org/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020", size = 865445, upload-time = "2026-05-09T23:13:12.523Z" }, + { url = "https://files.pythonhosted.org/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2", size = 912322, upload-time = "2026-05-09T23:13:14.281Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2", size = 801269, upload-time = "2026-05-09T23:13:16.569Z" }, + { url = "https://files.pythonhosted.org/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04", size = 777085, upload-time = "2026-05-09T23:13:18.675Z" }, + { url = "https://files.pythonhosted.org/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c", size = 785153, upload-time = "2026-05-09T23:13:20.525Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f", size = 860164, upload-time = "2026-05-09T23:13:22.263Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8", size = 765731, upload-time = "2026-05-09T23:13:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6", size = 852062, upload-time = "2026-05-09T23:13:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21", size = 789577, upload-time = "2026-05-09T23:13:27.814Z" }, + { url = "https://files.pythonhosted.org/packages/05/a4/018e71f7d2ad48c1ebe6d3ae0026f9b7cb4802fd15c7cc02fdf724355102/regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127", size = 266691, upload-time = "2026-05-09T23:13:29.549Z" }, + { url = "https://files.pythonhosted.org/packages/e6/1d/861a93719fb9ee7dbfc3761b3797b7a3e112a5d42c6129459d2d741be9b5/regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca", size = 277747, upload-time = "2026-05-09T23:13:31.859Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c6/0a2436ae4da1ba76e51cb98943c6838a9a721faa40ebe2dce07694ae34e3/regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6", size = 270500, upload-time = "2026-05-09T23:13:33.525Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3", size = 494172, upload-time = "2026-05-09T23:13:35.935Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/fd1177a2032037c681baecdb3422ee4e1424aec4e4f470ef47793d325274/regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6", size = 293952, upload-time = "2026-05-09T23:13:38.307Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff", size = 292314, upload-time = "2026-05-09T23:13:40.353Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88", size = 811681, upload-time = "2026-05-09T23:13:42.543Z" }, + { url = "https://files.pythonhosted.org/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178", size = 871135, upload-time = "2026-05-09T23:13:44.326Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100", size = 917265, upload-time = "2026-05-09T23:13:47.295Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e", size = 816311, upload-time = "2026-05-09T23:13:49.885Z" }, + { url = "https://files.pythonhosted.org/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2", size = 785498, upload-time = "2026-05-09T23:13:52.25Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b", size = 801348, upload-time = "2026-05-09T23:13:54.127Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e", size = 866493, upload-time = "2026-05-09T23:13:56.067Z" }, + { url = "https://files.pythonhosted.org/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041", size = 772811, upload-time = "2026-05-09T23:13:57.867Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0", size = 856584, upload-time = "2026-05-09T23:13:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081", size = 803453, upload-time = "2026-05-09T23:14:01.978Z" }, + { url = "https://files.pythonhosted.org/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5", size = 269951, upload-time = "2026-05-09T23:14:03.661Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4", size = 281240, upload-time = "2026-05-09T23:14:05.56Z" }, + { url = "https://files.pythonhosted.org/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de", size = 272383, upload-time = "2026-05-09T23:14:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/13/3e/9c3cd292d8808b3645a2ce517e200179b6d0e903f176300bd8b542e14de5/regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a", size = 490376, upload-time = "2026-05-09T23:14:09.64Z" }, + { url = "https://files.pythonhosted.org/packages/60/70/d43ee8a2ca0a8b68d167f21658b85520ac0574617c7f320367c5047f7556/regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4", size = 291964, upload-time = "2026-05-09T23:14:11.424Z" }, + { url = "https://files.pythonhosted.org/packages/21/91/9d50b433828d8e74196904e168a43abf1e6e88b2a15d47ed742456720c37/regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c", size = 289682, upload-time = "2026-05-09T23:14:13.123Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/b835e3cafbb9d977736912436259ff551d60919f7d7b3d37d46659c63564/regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9", size = 796996, upload-time = "2026-05-09T23:14:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a6/9f992d00019166b9de01c546dd4549bc679f2a68df11b877740b0760b7c2/regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af", size = 866089, upload-time = "2026-05-09T23:14:17.757Z" }, + { url = "https://files.pythonhosted.org/packages/e0/08/4d32af657e049b19cb62b02e46e38fe1518797bfb2203ee93a510b21b0dc/regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0", size = 911530, upload-time = "2026-05-09T23:14:20.353Z" }, + { url = "https://files.pythonhosted.org/packages/d9/27/2af43dd1dc201d1fecefda64a45f4ad0995855b92724f795a777b402ee69/regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4", size = 800643, upload-time = "2026-05-09T23:14:22.265Z" }, + { url = "https://files.pythonhosted.org/packages/a4/dd/23a249047013b5321d4a60c4d2437462086f601b061776a525e5fba2a59f/regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf", size = 777223, upload-time = "2026-05-09T23:14:24.179Z" }, + { url = "https://files.pythonhosted.org/packages/94/6a/e85ed9538cd19586d0465076a4578a12e093ce776d15f3f8ce92733a8dd6/regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346", size = 785760, upload-time = "2026-05-09T23:14:26.065Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c4/f25473209438638e947c55f9156fd8f236f74169229028cc99116380868e/regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676", size = 860891, upload-time = "2026-05-09T23:14:28.17Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f7/f4f86e3c74419c37370e91f150ae0c2ef7d34b2e0e4cdd5da046a02e4022/regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14", size = 765891, upload-time = "2026-05-09T23:14:30.06Z" }, + { url = "https://files.pythonhosted.org/packages/26/70/704d8e13765939146b1cd0ef4e2feb71d7929727d2290f026eed10095955/regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd", size = 851380, upload-time = "2026-05-09T23:14:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/26/29/1a13582a8460038edc38e49f64ceb0dd7c60f5caba77571f4bf6601965d9/regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e", size = 789350, upload-time = "2026-05-09T23:14:34.799Z" }, + { url = "https://files.pythonhosted.org/packages/73/56/3dcafe34fc72e271d62ad9a291801e88a1457bb251c132f15fcc2e5aad1a/regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad", size = 272130, upload-time = "2026-05-09T23:14:36.729Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/02eebf0be95efe416c664db7fb8b6b05b7a0b06a7544f2884f2558b0526f/regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763", size = 280999, upload-time = "2026-05-09T23:14:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/70/5a/1dd1abee76cb7a846a0bcf42fdc87e5720c3c33c24f3e37814310a513d9f/regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372", size = 273500, upload-time = "2026-05-09T23:14:41.059Z" }, + { url = "https://files.pythonhosted.org/packages/86/c1/c5f619b0057a7965cb78ec559c1d7a45ce8c99a35bea95483d64959a93d9/regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499", size = 494269, upload-time = "2026-05-09T23:14:42.869Z" }, + { url = "https://files.pythonhosted.org/packages/05/2c/5d01f1aee33de4bbe60c8452945bfc8477ca7c5ae4450f6bfe711036cb36/regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1", size = 293954, upload-time = "2026-05-09T23:14:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fe/e8988b2ae2108c6ef71bd4aa8d87fbe257976dd0810e826cd75f701c68b6/regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d", size = 292405, upload-time = "2026-05-09T23:14:47.211Z" }, + { url = "https://files.pythonhosted.org/packages/79/34/d2b0937faa7859263f7f0a3c6b103a1296306be6952dc173d0154e9a2f49/regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c", size = 811855, upload-time = "2026-05-09T23:14:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/80/fe/daf53a47457a8486db66c66c01ceb9c2303eecee3f87197f1e77eb1a736d/regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5", size = 871189, upload-time = "2026-05-09T23:14:51.555Z" }, + { url = "https://files.pythonhosted.org/packages/1c/75/058fc4470cbfbf57d800aff1a0022b929a3f9fa553ee10a0cdf2070eb31f/regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20", size = 917485, upload-time = "2026-05-09T23:14:53.633Z" }, + { url = "https://files.pythonhosted.org/packages/88/e7/179cfda3a28bc843b5c6cfe7f79f23489c791ed95f151083803660878432/regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0", size = 816369, upload-time = "2026-05-09T23:14:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/41/90/6f0cc422071688266d344fca8462d787cba0a2c144acb25721f9a61ec265/regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d", size = 785869, upload-time = "2026-05-09T23:14:58.602Z" }, + { url = "https://files.pythonhosted.org/packages/02/67/a31f1760f09c27b251ef39e9beb541f462cf977381d067faa764c2c0e393/regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b", size = 801427, upload-time = "2026-05-09T23:15:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c4/1a80654597b6bc1e1ea0494824c31200e8a956abe290afae9b19a166a148/regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a", size = 866482, upload-time = "2026-05-09T23:15:03.384Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/960724e06482c08466ff5611e242e86f80062949cdf6b4b9cc317b9dd93d/regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415", size = 773022, upload-time = "2026-05-09T23:15:05.625Z" }, + { url = "https://files.pythonhosted.org/packages/50/a8/a9979c3e7918280e93159ebcab5ef1a65116dd4f3bd6091be0eae4a126e8/regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2", size = 856642, upload-time = "2026-05-09T23:15:07.966Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d4/a9b732f2f0072c0ab12227483abb24fffcb9f73f8a2b203df0a6d0434735/regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41", size = 803552, upload-time = "2026-05-09T23:15:10.215Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fe/1b3113817447a1d4155e4ac76d2e072f42c0bcba2f43fa8a0e756ea2cd91/regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58", size = 275746, upload-time = "2026-05-09T23:15:12.609Z" }, + { url = "https://files.pythonhosted.org/packages/92/73/93d42045302636c91f2e5ef588b65b84b01428f28ec77de256b1dfdfbe5c/regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77", size = 285685, upload-time = "2026-05-09T23:15:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/da/80/35b4c33c804a165a7f55289afda3ea9e3eb6d15800341a2d66455c0f1f30/regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa", size = 275713, upload-time = "2026-05-09T23:15:16.98Z" }, +] + +[[package]] +name = "requests" +version = "2.34.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/36/7180e7f077c38108945dbbdf60fe04db681c3feb6e96419f8c6dc8723741/requests-2.34.1.tar.gz", hash = "sha256:0fc5669f2b69704449fe1552360bd2a73a54512dfd03e65529157f1513322beb", size = 142783, upload-time = "2026-05-13T19:20:24.662Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/5a/4a949d170476de3c04ac036b5466422fbcbf348a917d8042eedf2cac7d1b/requests-2.34.1-py3-none-any.whl", hash = "sha256:bf38a3ff993960d3dd819c08862c40b3c703306eb7c744fcd9f4ddbb95b548f0", size = 73085, upload-time = "2026-05-13T19:20:22.827Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sqlite-vec" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/85/9fad0045d8e7c8df3e0fa5a56c630e8e15ad6e5ca2e6106fceb666aa6638/sqlite_vec-0.1.9-py3-none-macosx_10_6_x86_64.whl", hash = "sha256:1b62a7f0a060d9475575d4e599bbf94a13d85af896bc1ce86ee80d1b5b48e5fb", size = 131171, upload-time = "2026-03-31T08:02:31.717Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3d/3677e0cd2f92e5ebc43cd29fbf565b75582bff1ccfa0b8327c7508e1084f/sqlite_vec-0.1.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d52e30513bae4cc9778ddbf6145610434081be4c3afe57cd877893bad9f6b6c", size = 165434, upload-time = "2026-03-31T08:02:32.712Z" }, + { url = "https://files.pythonhosted.org/packages/00/d4/f2b936d3bdc38eadcbd2a87875815db36430fab0363182ba5d12cd8e0b51/sqlite_vec-0.1.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e921e592f24a5f9a18f590b6ddd530eb637e2d474e3b1972f9bbeb773aa3cb9", size = 160076, upload-time = "2026-03-31T08:02:33.796Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ad/6afd073b0f817b3e03f9e37ad626ae341805891f23c74b5292818f49ac63/sqlite_vec-0.1.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux1_x86_64.whl", hash = "sha256:1515727990b49e79bcaf75fdee2ffc7d461f8b66905013231251f1c8938e7786", size = 163388, upload-time = "2026-03-31T08:02:34.888Z" }, + { url = "https://files.pythonhosted.org/packages/42/89/81b2907cda14e566b9bf215e2ad82fc9b349edf07d2010756ffdb902f328/sqlite_vec-0.1.9-py3-none-win_amd64.whl", hash = "sha256:4a28dc12fa4b53d7b1dced22da2488fade444e96b5d16fd2d698cd670675cf32", size = 292804, upload-time = "2026-03-31T08:02:36.035Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/2b/58abc2d1fd397e7dde08e947e05c884d8ef2f78d5e2588c17a12d42d6994/sse_starlette-3.4.4.tar.gz", hash = "sha256:07e0fa0460138baf25cdd5fb28683472c3995dc1642225191b3832d62526bcb0", size = 31819, upload-time = "2026-05-12T17:37:17.019Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/67/805710444ea8cc75fbf70b920ed431a560c4bf9c57f7d5a3117213189399/sse_starlette-3.4.4-py3-none-any.whl", hash = "sha256:3f4dd50d8aed2771a091f3a83000323fc3844541c16b4fe585ae2420cc6df973", size = 16514, upload-time = "2026-05-12T17:37:15.601Z" }, +] + +[[package]] +name = "starlette" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, + { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, + { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "typer" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/07/b822e1b307d40e263e8253d2384cf98c51aa2368cc7ba9a07e523a1d964b/typer-0.23.1.tar.gz", hash = "sha256:2070374e4d31c83e7b61362fd859aa683576432fd5b026b060ad6b4cd3b86134", size = 120047, upload-time = "2026-02-13T10:04:30.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/91/9b286ab899c008c2cb05e8be99814807e7fbbd33f0c0c960470826e5ac82/typer-0.23.1-py3-none-any.whl", hash = "sha256:3291ad0d3c701cbf522012faccfbb29352ff16ad262db2139e6b01f15781f14e", size = 56813, upload-time = "2026-02-13T10:04:32.008Z" }, +] + +[[package]] +name = "types-requests" +version = "2.33.0.20260513" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/f7/3228dd3794941bcb92ca6ca2045a6671a828ec0b47becbef23310bc45559/types_requests-2.33.0.20260513.tar.gz", hash = "sha256:bd845450e954e751373d5d33526742592f298808a3ee3bda7e858e46b839b57f", size = 24714, upload-time = "2026-05-13T05:39:23.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/f5/233a78be8367a9888de718f002fb27b1ea4be39471cd88aedeafceed872e/types_requests-2.33.0.20260513-py3-none-any.whl", hash = "sha256:d5a965f9d18b6e06b72039a69565de9027e58f36a7f709857da747fbe7521122", size = 21390, upload-time = "2026-05-13T05:39:22.262Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.46.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/93/041fca8274050e40e6791f267d82e0e2e27dd165627bd640d3e0e378d877/uvicorn-0.46.0.tar.gz", hash = "sha256:fb9da0926999cc6cb22dc7cd71a94a632f078e6ae47ff683c5c420750fb7413d", size = 88758, upload-time = "2026-04-23T07:16:00.151Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl", hash = "sha256:bbebbcbed972d162afca128605223022bedd345b7bc7855ce66deb31487a9048", size = 70926, upload-time = "2026-04-23T07:15:58.355Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "yarl" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, + { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, + { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, + { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, + { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, + { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, + { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, + { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, + { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, + { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, + { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, + { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, +] From 574125d7dd468f318e9f4e330e9005e3209b33e1 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 14:14:45 +0800 Subject: [PATCH 02/50] fix(filesystem): add pifs ask and chat commands --- pageindex/filesystem/cli.py | 144 +++++++++++++++++++++++++++++++++--- pyproject.toml | 3 + tests/test_pifs_cli.py | 113 ++++++++++++++++++++++++++++ 3 files changed, 251 insertions(+), 9 deletions(-) create mode 100644 tests/test_pifs_cli.py diff --git a/pageindex/filesystem/cli.py b/pageindex/filesystem/cli.py index 0cdf6329d..04ff8c936 100644 --- a/pageindex/filesystem/cli.py +++ b/pageindex/filesystem/cli.py @@ -6,10 +6,128 @@ import sys from pathlib import Path +from .agent import REASONING_EFFORT_CHOICES, REASONING_SUMMARY_CHOICES, run_pifs_agent from .commands import PIFSCommandError, PIFSCommandExecutor from .core import PageIndexFileSystem +AGENT_STREAM_MODE_CHOICES = ("off", "tools", "model", "all") +DEFAULT_AGENT_MODEL = "gpt-5.4-mini" +EXIT_COMMANDS = {"exit", "quit", ":q"} + + +def _agent_model_default() -> str: + return ( + os.environ.get("PIFS_AGENT_MODEL") + or os.environ.get("PIFS_MODEL") + or DEFAULT_AGENT_MODEL + ) + + +def _add_agent_arguments( + parser: argparse.ArgumentParser, + *, + workspace_default: str | None, +) -> None: + parser.add_argument("--workspace", default=workspace_default) + parser.add_argument("--model", default=_agent_model_default()) + parser.add_argument( + "--stream-mode", + default="off", + choices=AGENT_STREAM_MODE_CHOICES, + ) + parser.add_argument("--max-turns", type=int, default=20) + parser.add_argument("--max-seconds", type=float, default=60) + parser.add_argument( + "--reasoning-effort", + choices=REASONING_EFFORT_CHOICES, + default=None, + ) + parser.add_argument( + "--reasoning-summary", + choices=REASONING_SUMMARY_CHOICES, + default=None, + ) + + +def _parse_agent_command( + command_name: str, + argv: list[str], + *, + workspace_default: str | None, +) -> argparse.Namespace: + parser = argparse.ArgumentParser( + prog=f"pifs {command_name}", + description=f"PageIndex FileSystem {command_name}", + ) + _add_agent_arguments(parser, workspace_default=workspace_default) + if command_name == "ask": + parser.add_argument("question", nargs=argparse.REMAINDER) + args = parser.parse_args(argv) + if not args.workspace: + parser.error("--workspace is required unless PIFS_WORKSPACE is set") + return args + + +def _filesystem_from_workspace(workspace: str) -> PageIndexFileSystem: + return PageIndexFileSystem(Path(workspace).expanduser()) + + +def _agent_kwargs(args: argparse.Namespace) -> dict[str, object]: + return { + "model": args.model, + "stream_mode": args.stream_mode, + "max_turns": args.max_turns, + "max_seconds": args.max_seconds, + "reasoning_effort": args.reasoning_effort, + "reasoning_summary": args.reasoning_summary, + } + + +def _run_ask(argv: list[str], *, workspace_default: str | None) -> int: + args = _parse_agent_command("ask", argv, workspace_default=workspace_default) + question_tokens = [token for token in args.question if token != "--"] + question = " ".join(question_tokens).strip() + if not question: + raise ValueError("ask requires a question") + + filesystem = _filesystem_from_workspace(args.workspace) + print(run_pifs_agent(filesystem, question, **_agent_kwargs(args))) + return 0 + + +def _run_chat(argv: list[str], *, workspace_default: str | None) -> int: + args = _parse_agent_command("chat", argv, workspace_default=workspace_default) + filesystem = _filesystem_from_workspace(args.workspace) + while True: + try: + question = input("pifs> ").strip() + except EOFError: + break + except KeyboardInterrupt: + print() + break + if not question: + continue + if question.lower() in EXIT_COMMANDS: + break + print(run_pifs_agent(filesystem, question, **_agent_kwargs(args))) + return 0 + + +def _run_passthrough( + command_tokens: list[str], + *, + workspace: str, + json_output: bool, +) -> int: + filesystem = _filesystem_from_workspace(workspace) + executor = PIFSCommandExecutor(filesystem, json_output=json_output) + command = " ".join(shlex.quote(token) for token in command_tokens) + print(executor.execute(command)) + return 0 + + def main(argv: list[str] | None = None) -> int: argv = list(sys.argv[1:] if argv is None else argv) parser = argparse.ArgumentParser(description="PageIndex FileSystem CLI") @@ -20,20 +138,28 @@ def main(argv: list[str] | None = None) -> int: command_tokens = [token for token in args.command if token != "--"] json_output = args.json_output - if "--json" in command_tokens: - command_tokens = [token for token in command_tokens if token != "--json"] - json_output = True - if not args.workspace: - parser.error("--workspace is required unless PIFS_WORKSPACE is set") if not command_tokens: parser.error("a filesystem command is required") - filesystem = PageIndexFileSystem(Path(args.workspace).expanduser()) - executor = PIFSCommandExecutor(filesystem, json_output=json_output) try: - command = " ".join(shlex.quote(token) for token in command_tokens) - print(executor.execute(command)) + command_name = command_tokens[0] + command_args = command_tokens[1:] + if command_name == "ask": + return _run_ask(command_args, workspace_default=args.workspace) + if command_name == "chat": + return _run_chat(command_args, workspace_default=args.workspace) + + if "--json" in command_tokens: + command_tokens = [token for token in command_tokens if token != "--json"] + json_output = True + if not args.workspace: + parser.error("--workspace is required unless PIFS_WORKSPACE is set") + return _run_passthrough( + command_tokens, + workspace=args.workspace, + json_output=json_output, + ) except PIFSCommandError as exc: print(f"ERROR: {exc}", file=sys.stderr) return 2 diff --git a/pyproject.toml b/pyproject.toml index ee374805d..745824785 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,3 +12,6 @@ dependencies = [ "pyyaml==6.0.2", "sqlite-vec>=0.1.9", ] + +[project.scripts] +pifs = "pageindex.filesystem.cli:main" diff --git a/tests/test_pifs_cli.py b/tests/test_pifs_cli.py new file mode 100644 index 000000000..d08c053b7 --- /dev/null +++ b/tests/test_pifs_cli.py @@ -0,0 +1,113 @@ +from pathlib import Path + + +class FakeFileSystem: + def __init__(self, workspace): + self.workspace = Path(workspace) + + +def test_cli_passthrough_invokes_pifs_command_executor(monkeypatch, capsys, tmp_path): + from pageindex.filesystem import cli + + workspace = tmp_path / "workspace" + executor_instances = [] + + class FakeExecutor: + def __init__(self, filesystem, *, json_output=False): + self.filesystem = filesystem + self.json_output = json_output + self.commands = [] + executor_instances.append(self) + + def execute(self, command): + self.commands.append(command) + return f"executed:{command}" + + monkeypatch.setattr(cli, "PageIndexFileSystem", FakeFileSystem) + monkeypatch.setattr(cli, "PIFSCommandExecutor", FakeExecutor) + + status = cli.main(["--workspace", str(workspace), "ls", "/documents", "--json"]) + + assert status == 0 + assert capsys.readouterr().out == "executed:ls /documents\n" + assert len(executor_instances) == 1 + assert executor_instances[0].filesystem.workspace == workspace + assert executor_instances[0].json_output is True + assert executor_instances[0].commands == ["ls /documents"] + + +def test_cli_ask_invokes_agent_with_question(monkeypatch, capsys, tmp_path): + from pageindex.filesystem import cli + + workspace = tmp_path / "workspace" + agent_calls = [] + + def fake_run_pifs_agent(filesystem, question, **kwargs): + agent_calls.append((filesystem, question, kwargs)) + return "agent answer" + + monkeypatch.setattr(cli, "PageIndexFileSystem", FakeFileSystem) + monkeypatch.setattr(cli, "run_pifs_agent", fake_run_pifs_agent) + + status = cli.main( + [ + "ask", + "--workspace", + str(workspace), + "--model", + "test-model", + "--stream-mode", + "tools", + "--max-turns", + "7", + "--max-seconds", + "3.5", + "--reasoning-effort", + "low", + "--reasoning-summary", + "concise", + "What", + "is", + "inside?", + ] + ) + + assert status == 0 + assert capsys.readouterr().out == "agent answer\n" + filesystem, question, kwargs = agent_calls[0] + assert filesystem.workspace == workspace + assert question == "What is inside?" + assert kwargs == { + "model": "test-model", + "stream_mode": "tools", + "max_turns": 7, + "max_seconds": 3.5, + "reasoning_effort": "low", + "reasoning_summary": "concise", + } + + +def test_cli_chat_runs_one_question_and_exits(monkeypatch, capsys, tmp_path): + from pageindex.filesystem import cli + + workspace = tmp_path / "workspace" + inputs = iter(["", "Summarize the workspace", "exit"]) + agent_calls = [] + + def fake_run_pifs_agent(filesystem, question, **kwargs): + agent_calls.append((filesystem, question, kwargs)) + return f"answer:{question}" + + monkeypatch.setattr(cli, "PageIndexFileSystem", FakeFileSystem) + monkeypatch.setattr(cli, "run_pifs_agent", fake_run_pifs_agent) + monkeypatch.setattr("builtins.input", lambda prompt="": next(inputs)) + + status = cli.main(["chat", "--workspace", str(workspace), "--model", "test-model"]) + + assert status == 0 + assert capsys.readouterr().out == "answer:Summarize the workspace\n" + assert len(agent_calls) == 1 + filesystem, question, kwargs = agent_calls[0] + assert filesystem.workspace == workspace + assert question == "Summarize the workspace" + assert kwargs["model"] == "test-model" From e074bf17652382c32815e73192138e66a658b7dd Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 14:38:39 +0800 Subject: [PATCH 03/50] fix(filesystem): package pifs cli entrypoint --- pageindex/__init__.py | 4 ++++ pageindex/filesystem/cli.py | 42 +++++++++++++++++++++++++++++++++++++ pyproject.toml | 7 +++++++ tests/test_pifs_cli.py | 35 +++++++++++++++++++++++++++++++ uv.lock | 2 +- 5 files changed, 89 insertions(+), 1 deletion(-) diff --git a/pageindex/__init__.py b/pageindex/__init__.py index 4f05b6136..97c37815e 100644 --- a/pageindex/__init__.py +++ b/pageindex/__init__.py @@ -1,3 +1,7 @@ +import os + +os.environ.setdefault("LITELLM_LOCAL_MODEL_COST_MAP", "true") + try: from .page_index import * from .page_index_md import md_to_tree diff --git a/pageindex/filesystem/cli.py b/pageindex/filesystem/cli.py index 04ff8c936..9791e6aec 100644 --- a/pageindex/filesystem/cli.py +++ b/pageindex/filesystem/cli.py @@ -16,6 +16,39 @@ EXIT_COMMANDS = {"exit", "quit", ":q"} +def _load_env_file(path: str | None = None, *, workspace: str | None = None) -> Path | None: + from dotenv import load_dotenv + + if path: + env_path = Path(path).expanduser() + if not env_path.exists(): + raise FileNotFoundError(f"env file not found: {env_path}") + load_dotenv(env_path, override=True) + return env_path + + env_override = os.environ.get("PIFS_ENV_FILE") + if env_override: + return _load_env_file(env_override) + + starts = [Path.cwd()] + if workspace: + starts.append(Path(workspace).expanduser()) + seen: set[Path] = set() + for start in starts: + current = start.resolve() if start.exists() else start.resolve(strict=False) + if current.is_file(): + current = current.parent + for parent in (current, *current.parents): + candidate = parent / ".env" + if candidate in seen: + continue + seen.add(candidate) + if candidate.exists(): + load_dotenv(candidate, override=False) + return candidate + return None + + def _agent_model_default() -> str: return ( os.environ.get("PIFS_AGENT_MODEL") @@ -30,6 +63,7 @@ def _add_agent_arguments( workspace_default: str | None, ) -> None: parser.add_argument("--workspace", default=workspace_default) + parser.add_argument("--env-file", default=None) parser.add_argument("--model", default=_agent_model_default()) parser.add_argument( "--stream-mode", @@ -64,6 +98,9 @@ def _parse_agent_command( if command_name == "ask": parser.add_argument("question", nargs=argparse.REMAINDER) args = parser.parse_args(argv) + _load_env_file(args.env_file, workspace=args.workspace) + if not args.workspace: + args.workspace = os.environ.get("PIFS_WORKSPACE") if not args.workspace: parser.error("--workspace is required unless PIFS_WORKSPACE is set") return args @@ -130,11 +167,16 @@ def _run_passthrough( def main(argv: list[str] | None = None) -> int: argv = list(sys.argv[1:] if argv is None else argv) + _load_env_file() parser = argparse.ArgumentParser(description="PageIndex FileSystem CLI") parser.add_argument("--workspace", default=os.environ.get("PIFS_WORKSPACE")) + parser.add_argument("--env-file", default=None) parser.add_argument("--json", action="store_true", dest="json_output") parser.add_argument("command", nargs=argparse.REMAINDER) args = parser.parse_args(argv) + _load_env_file(args.env_file, workspace=args.workspace) + if not args.workspace: + args.workspace = os.environ.get("PIFS_WORKSPACE") command_tokens = [token for token in args.command if token != "--"] json_output = args.json_output diff --git a/pyproject.toml b/pyproject.toml index 745824785..aaea9f881 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,3 +15,10 @@ dependencies = [ [project.scripts] pifs = "pageindex.filesystem.cli:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["pageindex"] diff --git a/tests/test_pifs_cli.py b/tests/test_pifs_cli.py index d08c053b7..554ad43e8 100644 --- a/tests/test_pifs_cli.py +++ b/tests/test_pifs_cli.py @@ -1,3 +1,4 @@ +import os from pathlib import Path @@ -87,6 +88,40 @@ def fake_run_pifs_agent(filesystem, question, **kwargs): } +def test_cli_ask_loads_env_file_before_running_agent(monkeypatch, capsys, tmp_path): + from pageindex.filesystem import cli + + workspace = tmp_path / "workspace" + env_file = tmp_path / ".env" + env_file.write_text("OPENAI_API_KEY=from-dotenv\n", encoding="utf-8") + agent_keys = [] + + def fake_run_pifs_agent(filesystem, question, **kwargs): + agent_keys.append(os.environ.get("OPENAI_API_KEY")) + return "agent answer" + + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + monkeypatch.setattr(cli, "PageIndexFileSystem", FakeFileSystem) + monkeypatch.setattr(cli, "run_pifs_agent", fake_run_pifs_agent) + + status = cli.main( + [ + "ask", + "--workspace", + str(workspace), + "--env-file", + str(env_file), + "What", + "is", + "inside?", + ] + ) + + assert status == 0 + assert capsys.readouterr().out == "agent answer\n" + assert agent_keys == ["from-dotenv"] + + def test_cli_chat_runs_one_question_and_exits(monkeypatch, capsys, tmp_path): from pageindex.filesystem import cli diff --git a/uv.lock b/uv.lock index a2161f852..41ed9e4fe 100644 --- a/uv.lock +++ b/uv.lock @@ -1048,7 +1048,7 @@ wheels = [ [[package]] name = "pageindex" version = "0.1.0" -source = { virtual = "." } +source = { editable = "." } dependencies = [ { name = "litellm" }, { name = "openai-agents" }, From 3625a592f2bd0f85695a53408e2a507366242b75 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 14:46:43 +0800 Subject: [PATCH 04/50] fix(filesystem): stream pifs chat by default --- pageindex/filesystem/cli.py | 24 ++++++++++++++++++++---- tests/test_pifs_cli.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/pageindex/filesystem/cli.py b/pageindex/filesystem/cli.py index 9791e6aec..4d6d8eb8c 100644 --- a/pageindex/filesystem/cli.py +++ b/pageindex/filesystem/cli.py @@ -61,13 +61,14 @@ def _add_agent_arguments( parser: argparse.ArgumentParser, *, workspace_default: str | None, + default_stream_mode: str, ) -> None: parser.add_argument("--workspace", default=workspace_default) parser.add_argument("--env-file", default=None) parser.add_argument("--model", default=_agent_model_default()) parser.add_argument( "--stream-mode", - default="off", + default=default_stream_mode, choices=AGENT_STREAM_MODE_CHOICES, ) parser.add_argument("--max-turns", type=int, default=20) @@ -89,12 +90,17 @@ def _parse_agent_command( argv: list[str], *, workspace_default: str | None, + default_stream_mode: str, ) -> argparse.Namespace: parser = argparse.ArgumentParser( prog=f"pifs {command_name}", description=f"PageIndex FileSystem {command_name}", ) - _add_agent_arguments(parser, workspace_default=workspace_default) + _add_agent_arguments( + parser, + workspace_default=workspace_default, + default_stream_mode=default_stream_mode, + ) if command_name == "ask": parser.add_argument("question", nargs=argparse.REMAINDER) args = parser.parse_args(argv) @@ -122,7 +128,12 @@ def _agent_kwargs(args: argparse.Namespace) -> dict[str, object]: def _run_ask(argv: list[str], *, workspace_default: str | None) -> int: - args = _parse_agent_command("ask", argv, workspace_default=workspace_default) + args = _parse_agent_command( + "ask", + argv, + workspace_default=workspace_default, + default_stream_mode="off", + ) question_tokens = [token for token in args.question if token != "--"] question = " ".join(question_tokens).strip() if not question: @@ -134,7 +145,12 @@ def _run_ask(argv: list[str], *, workspace_default: str | None) -> int: def _run_chat(argv: list[str], *, workspace_default: str | None) -> int: - args = _parse_agent_command("chat", argv, workspace_default=workspace_default) + args = _parse_agent_command( + "chat", + argv, + workspace_default=workspace_default, + default_stream_mode="all", + ) filesystem = _filesystem_from_workspace(args.workspace) while True: try: diff --git a/tests/test_pifs_cli.py b/tests/test_pifs_cli.py index 554ad43e8..70f510e50 100644 --- a/tests/test_pifs_cli.py +++ b/tests/test_pifs_cli.py @@ -146,3 +146,33 @@ def fake_run_pifs_agent(filesystem, question, **kwargs): assert filesystem.workspace == workspace assert question == "Summarize the workspace" assert kwargs["model"] == "test-model" + assert kwargs["stream_mode"] == "all" + + +def test_cli_chat_stream_mode_can_be_overridden(monkeypatch, tmp_path): + from pageindex.filesystem import cli + + workspace = tmp_path / "workspace" + inputs = iter(["Summarize the workspace", "exit"]) + agent_calls = [] + + def fake_run_pifs_agent(filesystem, question, **kwargs): + agent_calls.append((filesystem, question, kwargs)) + return f"answer:{question}" + + monkeypatch.setattr(cli, "PageIndexFileSystem", FakeFileSystem) + monkeypatch.setattr(cli, "run_pifs_agent", fake_run_pifs_agent) + monkeypatch.setattr("builtins.input", lambda prompt="": next(inputs)) + + status = cli.main( + [ + "chat", + "--workspace", + str(workspace), + "--stream-mode", + "tools", + ] + ) + + assert status == 0 + assert agent_calls[0][2]["stream_mode"] == "tools" From 3d62293a1e18862113554cae16bbcb22461add34 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 14:48:53 +0800 Subject: [PATCH 05/50] fix(filesystem): avoid duplicate pifs chat output --- pageindex/filesystem/cli.py | 8 ++++++-- tests/test_pifs_cli.py | 35 ++++++++++++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/pageindex/filesystem/cli.py b/pageindex/filesystem/cli.py index 4d6d8eb8c..9625fe875 100644 --- a/pageindex/filesystem/cli.py +++ b/pageindex/filesystem/cli.py @@ -140,7 +140,9 @@ def _run_ask(argv: list[str], *, workspace_default: str | None) -> int: raise ValueError("ask requires a question") filesystem = _filesystem_from_workspace(args.workspace) - print(run_pifs_agent(filesystem, question, **_agent_kwargs(args))) + answer = run_pifs_agent(filesystem, question, **_agent_kwargs(args)) + if args.stream_mode == "off": + print(answer) return 0 @@ -164,7 +166,9 @@ def _run_chat(argv: list[str], *, workspace_default: str | None) -> int: continue if question.lower() in EXIT_COMMANDS: break - print(run_pifs_agent(filesystem, question, **_agent_kwargs(args))) + answer = run_pifs_agent(filesystem, question, **_agent_kwargs(args)) + if args.stream_mode == "off": + print(answer) return 0 diff --git a/tests/test_pifs_cli.py b/tests/test_pifs_cli.py index 70f510e50..74e95b499 100644 --- a/tests/test_pifs_cli.py +++ b/tests/test_pifs_cli.py @@ -58,7 +58,7 @@ def fake_run_pifs_agent(filesystem, question, **kwargs): "--model", "test-model", "--stream-mode", - "tools", + "off", "--max-turns", "7", "--max-seconds", @@ -80,7 +80,7 @@ def fake_run_pifs_agent(filesystem, question, **kwargs): assert question == "What is inside?" assert kwargs == { "model": "test-model", - "stream_mode": "tools", + "stream_mode": "off", "max_turns": 7, "max_seconds": 3.5, "reasoning_effort": "low", @@ -140,7 +140,7 @@ def fake_run_pifs_agent(filesystem, question, **kwargs): status = cli.main(["chat", "--workspace", str(workspace), "--model", "test-model"]) assert status == 0 - assert capsys.readouterr().out == "answer:Summarize the workspace\n" + assert capsys.readouterr().out == "" assert len(agent_calls) == 1 filesystem, question, kwargs = agent_calls[0] assert filesystem.workspace == workspace @@ -149,6 +149,35 @@ def fake_run_pifs_agent(filesystem, question, **kwargs): assert kwargs["stream_mode"] == "all" +def test_cli_ask_does_not_reprint_streamed_agent_output(monkeypatch, capsys, tmp_path): + from pageindex.filesystem import cli + + workspace = tmp_path / "workspace" + + def fake_run_pifs_agent(filesystem, question, **kwargs): + print("streamed answer") + return "returned answer" + + monkeypatch.setattr(cli, "PageIndexFileSystem", FakeFileSystem) + monkeypatch.setattr(cli, "run_pifs_agent", fake_run_pifs_agent) + + status = cli.main( + [ + "ask", + "--workspace", + str(workspace), + "--stream-mode", + "all", + "What", + "is", + "inside?", + ] + ) + + assert status == 0 + assert capsys.readouterr().out == "streamed answer\n" + + def test_cli_chat_stream_mode_can_be_overridden(monkeypatch, tmp_path): from pageindex.filesystem import cli From b9ee7110873f52a50ced30ef7010a78851112810 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 15:00:23 +0800 Subject: [PATCH 06/50] fix(filesystem): require target-first cat syntax --- examples/pifs_demo.py | 18 ++++++------ pageindex/filesystem/agent.py | 5 +++- pageindex/filesystem/commands.py | 27 ++++++++++++----- pageindex/filesystem/core.py | 5 ++-- tests/test_pageindex_structural_read.py | 39 ++++++++++++++----------- 5 files changed, 57 insertions(+), 37 deletions(-) diff --git a/examples/pifs_demo.py b/examples/pifs_demo.py index fddec042d..839d37c5f 100644 --- a/examples/pifs_demo.py +++ b/examples/pifs_demo.py @@ -5,7 +5,7 @@ through the PageIndex FileSystem shell instead of direct PageIndex document tools. The agent receives one read-only bash-like PIFS tool and must retrieve evidence through commands such as ls, tree, find, grep, search-summary, -cat --structure, cat --page, and cat --node. +cat --structure, cat --page, and cat --node. The demo uses PDFs under examples/documents. When a matching examples/documents/results/*_structure.json file exists, it is loaded into the @@ -71,12 +71,12 @@ - Use grep -R only for lexical evidence; do not treat semantic candidates as literal matches. - Run one evidence command at a time. Do not chain large commands like - cat --structure, grep, and cat --page in one bash call. -- For PDFs, use cat --structure to inspect the PageIndex tree, then - cat --page for evidence, for example: - cat --page 31-35 ref_1 -- For page-range questions, use cat --structure to identify the full section - range. Then run cat --page on the smallest useful evidence range, usually the + cat --structure, grep, and cat --page in one bash call. +- For PDFs, use cat --structure to inspect the PageIndex tree, then + cat --page for evidence, for example: + cat ref_1 --page 31-35 +- For page-range questions, use cat --structure to identify the full section + range. Then run cat --page on the smallest useful evidence range, usually the section start page or first 1-2 pages, before the final answer. Do not print a broad multi-page section unless the user asks to read the whole section. - Do not use cat --all on PDFs. @@ -646,7 +646,7 @@ def run_smoke_commands( verbose=verbose, ) - command = f"cat --structure {first_ref}" + command = f"cat {first_ref} --structure" structure_payload = execute_json_command(json_executor, command) structure_data = structure_payload.get("data") or {} structure = structure_data.get("structure") or [] @@ -664,7 +664,7 @@ def run_smoke_commands( ) evidence_range = opening_page_range_for_node(supervision_node) or "1-2" - command = f"cat --page {evidence_range} {first_ref}" + command = f"cat {first_ref} --page {evidence_range}" page = execute_json_command(json_executor, command) page_text = str((page.get("data") or {}).get("text") or "") show_capability( diff --git a/pageindex/filesystem/agent.py b/pageindex/filesystem/agent.py index a090b66c2..15e95a555 100644 --- a/pageindex/filesystem/agent.py +++ b/pageindex/filesystem/agent.py @@ -36,7 +36,8 @@ semantic search commands return candidate documents and do not guarantee literal text matches. Errors are returned as text prefixed with ERROR. Do not call commands that are not listed as available. When evidence is required, inspect it -with cat or grep before answering. +with cat or grep before answering. Prefer shell-like target-first cat syntax: +cat --structure, cat --page 31-59, and cat --node 0009. """ AGENT_TOOL_POLICY = """ @@ -48,6 +49,8 @@ - Semantic search commands are candidate-discovery tools and do not guarantee literal text matches. - Tool errors are returned as ERROR text; recover by trying an available command. - Use cat or grep to gather evidence before making source-backed claims. +- Prefer target-first cat syntax: cat --structure, cat --page 31-59, cat --node . +- Do not call cat --page ; if you need a page span, use cat --page -. """ STREAM_MODE_ALIASES = { diff --git a/pageindex/filesystem/commands.py b/pageindex/filesystem/commands.py index a741333a9..124825dae 100644 --- a/pageindex/filesystem/commands.py +++ b/pageindex/filesystem/commands.py @@ -90,8 +90,8 @@ def describe_available_command_surfaces(self) -> str: "- ls/tree: folder browsing", "- find --where: exact/canonical metadata DSL filtering", "- grep -R: recursive lexical/FTS search only; semantic vector prefilter is disabled", - "- cat --structure/--node/--page: cached PageIndex reads for PDF/Markdown files", - "- cat --all: full text artifact reads for txt/text files", + "- cat --structure/--node/--page: cached PageIndex reads for PDF/Markdown files", + "- cat --all: full text artifact reads for txt/text files", ] if "entity" in semantic_channels: lines.append("- find --name: entity semantic candidate discovery alias") @@ -405,12 +405,17 @@ def _cmd_grep(self, args: list[str]) -> Any: def _cmd_cat(self, args: list[str]) -> Any: if not args: raise PIFSCommandError("cat requires a file target") - target = None + target = args[0] + if target.startswith("-"): + raise PIFSCommandError( + "cat syntax is target-first: cat --structure, " + "cat --page 31-59, or cat --node 0009" + ) location = "all" structural_mode: str | None = None node_id: str | None = None page_range: str | None = None - i = 0 + i = 1 while i < len(args): arg = args[i] if arg == "--range": @@ -437,16 +442,22 @@ def _cmd_cat(self, args: list[str]) -> Any: elif arg.startswith("-"): raise PIFSCommandError(f"Unsupported cat option: {arg}") else: - target = arg + raise PIFSCommandError( + "cat accepts one file target. Use: cat --page , " + "for example: cat ref_1 --page 31-59" + ) i += 1 - if not target: - raise PIFSCommandError("cat requires a file target") if structural_mode == "structure": return self.filesystem.pageindex_structure(target) if structural_mode == "node": return self.filesystem.pageindex_node(target, str(node_id)) if structural_mode == "page": - return self.filesystem.pageindex_pages(target, str(page_range)) + if not page_range or not re.fullmatch(r"\d+(?:-\d+)?", page_range): + raise PIFSCommandError( + "cat --page requires one page selector like 31 or 31-59. " + "Use: cat --page " + ) + return self.filesystem.pageindex_pages(target, page_range) return self.filesystem.cat_text_artifact(target, location) def _cmd_stat(self, args: list[str]) -> Any: diff --git a/pageindex/filesystem/core.py b/pageindex/filesystem/core.py index 314a53279..5fddfe357 100644 --- a/pageindex/filesystem/core.py +++ b/pageindex/filesystem/core.py @@ -677,7 +677,8 @@ def _require_text_artifact_file(self, entry: Any, command: str) -> None: raise ValueError( f"{command} is only supported for txt/text files; " f"got source_path={entry.source_path!r}, content_type={entry.content_type!r}. " - "Use cat --structure, cat --page, or cat --node for PDF/Markdown PageIndex files." + "Use cat --structure, cat --page, or cat --node " + "for PDF/Markdown PageIndex files." ) def _require_pageindex_document_file(self, entry: Any, command: str) -> None: @@ -686,7 +687,7 @@ def _require_pageindex_document_file(self, entry: Any, command: str) -> None: raise ValueError( f"{command} is only supported for PDF/Markdown PageIndex files; " f"got source_path={entry.source_path!r}, content_type={entry.content_type!r}. " - "Use cat --all for txt/text files." + "Use cat --all for txt/text files." ) @classmethod diff --git a/tests/test_pageindex_structural_read.py b/tests/test_pageindex_structural_read.py index fcb2725b1..5b0629085 100644 --- a/tests/test_pageindex_structural_read.py +++ b/tests/test_pageindex_structural_read.py @@ -67,9 +67,9 @@ def fail_index(*args, **kwargs): ) executor = PIFSCommandExecutor(filesystem, json_output=True) - structure = json.loads(executor.execute("cat --structure dsid_structural_missing")) - node = json.loads(executor.execute("cat --node 0001 dsid_structural_missing")) - pages = json.loads(executor.execute("cat --page 1-2 dsid_structural_missing")) + structure = json.loads(executor.execute("cat dsid_structural_missing --structure")) + node = json.loads(executor.execute("cat dsid_structural_missing --node 0001")) + pages = json.loads(executor.execute("cat dsid_structural_missing --page 1-2")) stat = json.loads(executor.execute("stat dsid_structural_missing")) assert structure["data"]["mode"] == "structure" @@ -282,6 +282,7 @@ def fake_index(self, file_path, mode="auto"): def test_cat_structure_page_reuses_pageindex_client_cache_without_indexing(monkeypatch): from pageindex import PageIndexClient from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem + from pageindex.filesystem.commands import PIFSCommandError with tempfile.TemporaryDirectory() as tmp: source = Path(tmp) / "report.pdf" @@ -333,8 +334,8 @@ def fail_index(*args, **kwargs): ) executor = PIFSCommandExecutor(filesystem, json_output=True) - structure = json.loads(executor.execute("cat --structure dsid_structural_cached")) - pages = json.loads(executor.execute("cat --page 1-2 dsid_structural_cached")) + structure = json.loads(executor.execute("cat dsid_structural_cached --structure")) + pages = json.loads(executor.execute("cat dsid_structural_cached --page 1-2")) stat = json.loads(executor.execute("stat dsid_structural_cached")) assert structure["data"]["available"] is True @@ -345,6 +346,10 @@ def fail_index(*args, **kwargs): assert pages["data"]["available"] is True assert pages["data"]["text"] == "Page one text\n\nPage two text" + with pytest.raises(PIFSCommandError, match="target-first"): + executor.execute("cat --page 1-2 dsid_structural_cached") + with pytest.raises(PIFSCommandError, match="one file target"): + executor.execute("cat dsid_structural_cached --page 1 2") assert stat["data"]["pageindex_doc_id"] == "doc_cached_pdf" assert stat["data"]["pageindex_tree_status"] == "built" @@ -387,7 +392,7 @@ def test_cat_node_reads_pageindex_client_structure_without_custom_pifs_artifact( ) executor = PIFSCommandExecutor(filesystem, json_output=True) - node = json.loads(executor.execute("cat --node 0001 dsid_md_cached")) + node = json.loads(executor.execute("cat dsid_md_cached --node 0001")) assert node["data"]["available"] is True assert node["data"]["pageindex_doc_id"] == "doc_cached_md" @@ -453,7 +458,7 @@ def test_tree_does_not_read_file_internal_pageindex_structure(): with pytest.raises(PIFSCommandError): executor.execute("tree dsid_tree_is_folder_only") - structure = json.loads(executor.execute("cat --structure dsid_tree_is_folder_only")) + structure = json.loads(executor.execute("cat dsid_tree_is_folder_only --structure")) assert structure["data"]["structure"][0]["title"] == "Introduction" @@ -494,19 +499,19 @@ def test_cat_all_is_limited_to_text_files(): ) executor = PIFSCommandExecutor(filesystem, json_output=True) - text = json.loads(executor.execute("cat --all dsid_text_file")) + text = json.loads(executor.execute("cat dsid_text_file --all")) assert text["data"]["text"] == "plain text body" with pytest.raises(PIFSCommandError, match="only supported for txt/text files"): - executor.execute("cat --all dsid_pdf_file") + executor.execute("cat dsid_pdf_file --all") with pytest.raises(ValueError, match="not supported for PDF/Markdown"): filesystem.open("dsid_pdf_file") with pytest.raises(PIFSCommandError, match="only supported for txt/text files"): - executor.execute("cat --all dsid_md_file") + executor.execute("cat dsid_md_file --all") with pytest.raises(ValueError, match="not supported for PDF/Markdown"): filesystem.open("dsid_md_file") with pytest.raises(PIFSCommandError, match="only supported for txt/text files"): - executor.execute("cat --all dsid_json_file") + executor.execute("cat dsid_json_file --all") assert filesystem.open("dsid_json_file").text == '{"body":"json"}' for command in ( "head dsid_pdf_file", @@ -536,9 +541,9 @@ def test_pageindex_structure_commands_are_limited_to_pdf_and_markdown(): executor = PIFSCommandExecutor(filesystem, json_output=True) for command in ( - "cat --structure dsid_text_only", - "cat --page 1 dsid_text_only", - "cat --node 0001 dsid_text_only", + "cat dsid_text_only --structure", + "cat dsid_text_only --page 1", + "cat dsid_text_only --node 0001", ): with pytest.raises(PIFSCommandError, match="only supported for PDF/Markdown"): executor.execute(command) @@ -581,10 +586,10 @@ def test_existing_pageindex_status_allows_legacy_record_without_format_suffix(): ) executor = PIFSCommandExecutor(filesystem, json_output=True) - structure = json.loads(executor.execute("cat --structure dsid_legacy_pageindex")) + structure = json.loads(executor.execute("cat dsid_legacy_pageindex --structure")) assert structure["data"]["structure"][0]["title"] == "Uploaded" with pytest.raises(PIFSCommandError, match="only supported for txt/text files"): - executor.execute("cat --all dsid_legacy_pageindex") + executor.execute("cat dsid_legacy_pageindex --all") def test_read_commands_do_not_link_pageindex_cache_when_pointer_is_missing(monkeypatch): @@ -624,7 +629,7 @@ def fail_index(*args, **kwargs): ) executor = PIFSCommandExecutor(filesystem, json_output=True) - structure = json.loads(executor.execute("cat --structure dsid_late_cache")) + structure = json.loads(executor.execute("cat dsid_late_cache --structure")) stat = json.loads(executor.execute("stat dsid_late_cache")) assert structure["data"]["available"] is False From 5a78131509cf4ea5a75c026c68a7c3e6d6d9eaa1 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 15:04:24 +0800 Subject: [PATCH 07/50] fix(filesystem): suppress chat input echo while streaming --- pageindex/filesystem/cli.py | 51 +++++++++++++++++++++++++++++++++++-- tests/test_pifs_cli.py | 22 ++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/pageindex/filesystem/cli.py b/pageindex/filesystem/cli.py index 9625fe875..ab749e298 100644 --- a/pageindex/filesystem/cli.py +++ b/pageindex/filesystem/cli.py @@ -1,10 +1,13 @@ from __future__ import annotations import argparse +import contextlib import os +import re import shlex import sys from pathlib import Path +from typing import Iterator, TextIO from .agent import REASONING_EFFORT_CHOICES, REASONING_SUMMARY_CHOICES, run_pifs_agent from .commands import PIFSCommandError, PIFSCommandExecutor @@ -14,6 +17,7 @@ AGENT_STREAM_MODE_CHOICES = ("off", "tools", "model", "all") DEFAULT_AGENT_MODEL = "gpt-5.4-mini" EXIT_COMMANDS = {"exit", "quit", ":q"} +ANSI_ESCAPE_RE = re.compile(r"\x1b(?:\[[0-?]*[ -/]*[@-~]|.)") def _load_env_file(path: str | None = None, *, workspace: str | None = None) -> Path | None: @@ -127,6 +131,48 @@ def _agent_kwargs(args: argparse.Namespace) -> dict[str, object]: } +def _sanitize_chat_question(raw: str) -> str: + text = ANSI_ESCAPE_RE.sub("", raw) + chars: list[str] = [] + for char in text: + if char in {"\b", "\x7f"}: + if chars: + chars.pop() + continue + if char in {"\r", "\n"}: + continue + if ord(char) < 32 or ord(char) == 127: + continue + chars.append(char) + return "".join(chars).strip() + + +@contextlib.contextmanager +def _suppress_tty_input_echo(stdin: TextIO | None = None) -> Iterator[None]: + stream = sys.stdin if stdin is None else stdin + if not hasattr(stream, "isatty") or not stream.isatty(): + yield + return + try: + import termios + + fd = stream.fileno() + original = termios.tcgetattr(fd) + muted = original[:] + muted[3] = muted[3] & ~termios.ECHO + termios.tcsetattr(fd, termios.TCSADRAIN, muted) + except Exception: + yield + return + try: + yield + finally: + with contextlib.suppress(Exception): + termios.tcflush(fd, termios.TCIFLUSH) + with contextlib.suppress(Exception): + termios.tcsetattr(fd, termios.TCSADRAIN, original) + + def _run_ask(argv: list[str], *, workspace_default: str | None) -> int: args = _parse_agent_command( "ask", @@ -156,7 +202,7 @@ def _run_chat(argv: list[str], *, workspace_default: str | None) -> int: filesystem = _filesystem_from_workspace(args.workspace) while True: try: - question = input("pifs> ").strip() + question = _sanitize_chat_question(input("pifs> ")) except EOFError: break except KeyboardInterrupt: @@ -166,7 +212,8 @@ def _run_chat(argv: list[str], *, workspace_default: str | None) -> int: continue if question.lower() in EXIT_COMMANDS: break - answer = run_pifs_agent(filesystem, question, **_agent_kwargs(args)) + with _suppress_tty_input_echo(): + answer = run_pifs_agent(filesystem, question, **_agent_kwargs(args)) if args.stream_mode == "off": print(answer) return 0 diff --git a/tests/test_pifs_cli.py b/tests/test_pifs_cli.py index 74e95b499..85e2b6d08 100644 --- a/tests/test_pifs_cli.py +++ b/tests/test_pifs_cli.py @@ -149,6 +149,28 @@ def fake_run_pifs_agent(filesystem, question, **kwargs): assert kwargs["stream_mode"] == "all" +def test_cli_chat_sanitizes_control_input(monkeypatch, capsys, tmp_path): + from pageindex.filesystem import cli + + workspace = tmp_path / "workspace" + inputs = iter(["\x12", "he\x7fllo\x1b[A", "exit"]) + agent_calls = [] + + def fake_run_pifs_agent(filesystem, question, **kwargs): + agent_calls.append(question) + return f"answer:{question}" + + monkeypatch.setattr(cli, "PageIndexFileSystem", FakeFileSystem) + monkeypatch.setattr(cli, "run_pifs_agent", fake_run_pifs_agent) + monkeypatch.setattr("builtins.input", lambda prompt="": next(inputs)) + + status = cli.main(["chat", "--workspace", str(workspace), "--stream-mode", "off"]) + + assert status == 0 + assert agent_calls == ["hllo"] + assert capsys.readouterr().out == "answer:hllo\n" + + def test_cli_ask_does_not_reprint_streamed_agent_output(monkeypatch, capsys, tmp_path): from pageindex.filesystem import cli From 144e8ba3255306411cd44138ddf453afa629db5e Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 15:17:35 +0800 Subject: [PATCH 08/50] feat(pifs): add find maxdepth traversal limit --- pageindex/filesystem/commands.py | 45 +++++++++-- pageindex/filesystem/core.py | 20 ++++- pageindex/filesystem/store.py | 135 +++++++++++++++++++++++++++++-- tests/test_pifs_find_maxdepth.py | 117 +++++++++++++++++++++++++++ 4 files changed, 303 insertions(+), 14 deletions(-) create mode 100644 tests/test_pifs_find_maxdepth.py diff --git a/pageindex/filesystem/commands.py b/pageindex/filesystem/commands.py index 124825dae..2e36179ba 100644 --- a/pageindex/filesystem/commands.py +++ b/pageindex/filesystem/commands.py @@ -89,6 +89,7 @@ def describe_available_command_surfaces(self) -> str: "- mode: read-only inspection", "- ls/tree: folder browsing", "- find --where: exact/canonical metadata DSL filtering", + "- find -maxdepth N -type f|d: bounded folder traversal for find", "- grep -R: recursive lexical/FTS search only; semantic vector prefilter is disabled", "- cat --structure/--node/--page: cached PageIndex reads for PDF/Markdown files", "- cat --all: full text artifact reads for txt/text files", @@ -230,6 +231,7 @@ def _cmd_find(self, args: list[str]) -> Any: relation = None limit = 10 file_type = None + max_depth = None i = 0 while i < len(args): arg = args[i] @@ -248,6 +250,9 @@ def _cmd_find(self, args: list[str]) -> Any: elif arg == "-type": i += 1 file_type = args[i] + elif arg == "-maxdepth": + i += 1 + max_depth = self._parse_find_maxdepth(args[i] if i < len(args) else None) elif arg.startswith("-"): raise PIFSCommandError(f"Unsupported find option: {arg}") else: @@ -259,8 +264,26 @@ def _cmd_find(self, args: list[str]) -> Any: raise PIFSCommandError("find supports only one of --name or --relation") if file_type == "d": if where: - return self.filesystem.find_folders(path, metadata_filter=where, limit=limit) - return self.filesystem.browse(path, recursive=True, limit=limit)["folders"] + return self.filesystem.find_folders( + path, + metadata_filter=where, + limit=limit, + max_depth=max_depth, + ) + folders = self.filesystem.browse( + path, + recursive=True, + limit=limit, + max_depth=max_depth, + )["folders"] + if max_depth is not None and limit != 0: + return [self.filesystem.folder_info(path), *folders][:limit] + return folders + scope = {"folder_path": path, "recursive": True} + if max_depth is not None: + if max_depth == 0: + return [] + scope["max_depth"] = max_depth if relation: if not self.filesystem.has_semantic_channel("relation"): raise PIFSCommandError( @@ -269,7 +292,7 @@ def _cmd_find(self, args: list[str]) -> Any: return self.filesystem.search_semantic_channel( "relation", self._semantic_retrieval_query(relation), - scope={"folder_path": path, "recursive": True}, + scope=scope, metadata_filter=where, limit=limit, ) @@ -277,13 +300,13 @@ def _cmd_find(self, args: list[str]) -> Any: return self.filesystem.search_semantic_channel( "entity", self._semantic_retrieval_query(name), - scope={"folder_path": path, "recursive": True}, + scope=scope, metadata_filter=where, limit=limit, ) return self.filesystem.search( query=name, - scope={"folder_path": path, "recursive": True}, + scope=scope, metadata_filter=where, limit=limit, semantic=False, @@ -1466,6 +1489,18 @@ def _parse_non_negative_int(value: str, label: str) -> int: raise PIFSCommandError(f"{label} must be non-negative") return parsed + @staticmethod + def _parse_find_maxdepth(value: str | None) -> int: + if value is None: + raise PIFSCommandError("find -maxdepth requires an integer >= 0") + try: + parsed = int(value) + except ValueError as exc: + raise PIFSCommandError("find -maxdepth requires an integer >= 0") from exc + if parsed < 0: + raise PIFSCommandError("find -maxdepth requires an integer >= 0") + return parsed + @staticmethod def _try_json_loads(input_text: str) -> Any | None: try: diff --git a/pageindex/filesystem/core.py b/pageindex/filesystem/core.py index 5fddfe357..d45e6791c 100644 --- a/pageindex/filesystem/core.py +++ b/pageindex/filesystem/core.py @@ -238,17 +238,32 @@ def browse( path: str = "/", recursive: bool = False, limit: int = 100, + max_depth: int | None = None, ) -> dict[str, list[dict[str, Any]]]: - return self.store.list_folder(path, recursive=recursive, limit=limit) + return self.store.list_folder( + path, + recursive=recursive, + limit=limit, + max_depth=max_depth, + ) + + def folder_info(self, path: str = "/") -> dict[str, Any]: + return self.store.folder_info(path) def find_folders( self, path: str = "/", metadata_filter: Optional[dict[str, Any] | str] = None, limit: int = 100, + max_depth: int | None = None, ) -> list[dict[str, Any]]: parsed_filter = self.metadata.parse_filter(metadata_filter) - return self.store.find_folders(path, metadata_filter=parsed_filter, limit=limit) + return self.store.find_folders( + path, + metadata_filter=parsed_filter, + limit=limit, + max_depth=max_depth, + ) def create_folder( self, @@ -484,6 +499,7 @@ def retrieval_capabilities(self) -> dict[str, Any]: "grep_recursive": True, "grep_recursive_semantic_prefilter": False, "grep_recursive_guard": "bounded broad-folder notice", + "find_maxdepth": True, }, "semantic": { "backend_configured": self.semantic_retrieval_backend is not None, diff --git a/pageindex/filesystem/store.py b/pageindex/filesystem/store.py index d164eb4fa..9ef90adda 100644 --- a/pageindex/filesystem/store.py +++ b/pageindex/filesystem/store.py @@ -731,15 +731,33 @@ def list_metadata_fields(self) -> list[MetadataField]: for row in rows ] - def list_folder(self, path: str = "/", recursive: bool = False, limit: int = 100) -> dict[str, Any]: + def list_folder( + self, + path: str = "/", + recursive: bool = False, + limit: int = 100, + max_depth: int | None = None, + ) -> dict[str, Any]: path = normalize_path(path) + if max_depth is not None and max_depth < 0: + raise ValueError("max_depth must be non-negative") with self.connect() as conn: folder = self._folder_by_path(conn, path) if folder is None: raise KeyError(f"Unknown folder path: {path}") if recursive: + folder_depth_clause = "" + folder_depth_params: list[Any] = [] + if max_depth is not None: + if max_depth == 0: + folder_depth_clause = "AND 0" + else: + folder_depth_clause = ( + f"AND ({self._folder_depth_sql('fo.path')} - ?) <= ?" + ) + folder_depth_params = [self._folder_depth(path), max_depth] folder_rows = conn.execute( - """ + f""" SELECT fo.folder_id, fo.parent_id, @@ -765,12 +783,19 @@ def list_folder(self, path: str = "/", recursive: bool = False, limit: int = 100 ) AS children_count FROM folders fo WHERE fo.path != ? AND (fo.path LIKE ?) + {folder_depth_clause} ORDER BY fo.path LIMIT ? """, - (path, self._descendant_like(path), limit), + (path, self._descendant_like(path), *folder_depth_params, limit), ).fetchall() - file_rows = self._file_rows_for_scope(conn, path, True, limit) + file_rows = self._file_rows_for_scope( + conn, + path, + True, + limit, + max_depth=max_depth, + ) else: folder_rows = conn.execute( """ @@ -810,16 +835,64 @@ def list_folder(self, path: str = "/", recursive: bool = False, limit: int = 100 "files": [self._file_summary(row) for row in file_rows], } + def folder_info(self, path: str = "/") -> dict[str, Any]: + path = normalize_path(path) + with self.connect() as conn: + row = conn.execute( + """ + SELECT + fo.folder_id, + fo.parent_id, + fo.name, + fo.path, + fo.description, + fo.kind, + fo.metadata_json, + fo.created_at, + fo.updated_at, + ( + SELECT COUNT(DISTINCT child_ff.file_ref) + FROM file_folders child_ff + JOIN files child_file + ON child_file.file_ref = child_ff.file_ref + AND child_file.deleted_at IS NULL + WHERE child_ff.folder_id = fo.folder_id + ) AS file_count, + ( + SELECT COUNT(*) + FROM folders child_folder + WHERE child_folder.parent_id = fo.folder_id + ) AS children_count + FROM folders fo + WHERE fo.path = ? + """, + (path,), + ).fetchone() + if row is None: + raise KeyError(f"Unknown folder path: {path}") + return self._folder_row_to_dict(row) + def find_folders( self, path: str = "/", *, metadata_filter: Optional[dict[str, Any]] = None, limit: int = 100, + max_depth: int | None = None, ) -> list[dict[str, Any]]: path = normalize_path(path) + if max_depth is not None and max_depth < 0: + raise ValueError("max_depth must be non-negative") metadata_sql, metadata_params = self._metadata_filter_sql(metadata_filter) metadata_clause = f"AND {' AND '.join(metadata_sql)}" if metadata_sql else "" + folder_depth_clause = "" + folder_depth_params: list[Any] = [] + if max_depth is not None: + if max_depth == 0: + folder_depth_clause = "AND 0" + else: + folder_depth_clause = f"AND ({self._folder_depth_sql('fo.path')} - ?) <= ?" + folder_depth_params = [self._folder_depth(path), max_depth] sql = f""" SELECT * FROM ( @@ -865,12 +938,19 @@ def find_folders( ) AS matched_files FROM folders fo WHERE fo.path != ? AND fo.path LIKE ? + {folder_depth_clause} ) WHERE matched_files > 0 ORDER BY path LIMIT ? """ - params = [*metadata_params, path, self._descendant_like(path), limit] + params = [ + *metadata_params, + path, + self._descendant_like(path), + *folder_depth_params, + limit, + ] with self.connect() as conn: folder = self._folder_by_path(conn, path) if folder is None: @@ -1577,6 +1657,7 @@ def _file_rows_for_scope( path: str, recursive: bool, limit: int, + max_depth: int | None = None, ) -> list[sqlite3.Row]: sql = """ SELECT @@ -1601,6 +1682,12 @@ def _file_rows_for_scope( if recursive: sql += " AND (pf.path = ? OR pf.path LIKE ?)" params = [path, self._descendant_like(path)] + if max_depth is not None: + if max_depth <= 0: + sql += " AND 0" + else: + sql += f" AND ({self._folder_depth_sql('pf.path')} - ?) <= ?" + params.extend([self._folder_depth(path), max_depth - 1]) else: sql += " AND pf.path = ?" params = [path] @@ -1612,14 +1699,30 @@ def _scope_sql(self, scope: Optional[dict[str, Any]]) -> tuple[str, list[Any]]: if not scope: return "", [] recursive = scope.get("recursive", True) + max_depth = scope.get("max_depth") + if max_depth is not None: + max_depth = int(max_depth) + if max_depth < 0: + raise ValueError("max_depth must be non-negative") folder_id = scope.get("folder_id") if folder_id: if folder_id == "root": folder_path = "/" else: if recursive: + if max_depth == 0: + return "0", [] + depth_clause = "" + depth_params: list[Any] = [] + if max_depth is not None: + depth_clause = ( + "AND " + f"({self._folder_depth_sql('scope_folder.path')} - " + f"{self._folder_depth_sql('base_folder.path')}) <= ?" + ) + depth_params = [max_depth - 1] return ( - """ + f""" EXISTS ( SELECT 1 FROM file_folders scope_ff @@ -1635,9 +1738,10 @@ def _scope_sql(self, scope: Optional[dict[str, Any]]) -> tuple[str, list[Any]]: ELSE base_folder.path || '/%' END ) + {depth_clause} ) """, - [folder_id], + [folder_id, *depth_params], ) return ( """ @@ -1654,12 +1758,18 @@ def _scope_sql(self, scope: Optional[dict[str, Any]]) -> tuple[str, list[Any]]: folder_path = normalize_path(scope.get("folder_path") or scope.get("path")) else: return "", [] + if recursive and max_depth == 0: + return "0", [] path_clause = ( "(scope_folder.path = ? OR scope_folder.path LIKE ?)" if recursive else "scope_folder.path = ?" ) params = [folder_path, self._descendant_like(folder_path)] if recursive else [folder_path] + depth_clause = "" + if recursive and max_depth is not None: + depth_clause = f"AND ({self._folder_depth_sql('scope_folder.path')} - ?) <= ?" + params.extend([self._folder_depth(folder_path), max_depth - 1]) return ( f""" EXISTS ( @@ -1669,6 +1779,7 @@ def _scope_sql(self, scope: Optional[dict[str, Any]]) -> tuple[str, list[Any]]: ON scope_folder.folder_id = scope_ff.folder_id WHERE scope_ff.file_ref = f.file_ref AND {path_clause} + {depth_clause} ) """, params, @@ -1702,6 +1813,16 @@ def _folder_depth(path: str) -> int: stripped = normalize_path(path).strip("/") return 0 if not stripped else len(stripped.split("/")) + @staticmethod + def _folder_depth_sql(path_expr: str) -> str: + return ( + "(CASE " + f"WHEN TRIM({path_expr}, '/') = '' THEN 0 " + f"ELSE LENGTH(TRIM({path_expr}, '/')) " + f"- LENGTH(REPLACE(TRIM({path_expr}, '/'), '/', '')) + 1 " + "END)" + ) + @classmethod def _folder_row_to_dict(cls, row: sqlite3.Row) -> dict[str, Any]: return { diff --git a/tests/test_pifs_find_maxdepth.py b/tests/test_pifs_find_maxdepth.py new file mode 100644 index 000000000..56c32e492 --- /dev/null +++ b/tests/test_pifs_find_maxdepth.py @@ -0,0 +1,117 @@ +import json +from pathlib import Path + +import pytest + + +def _register_find_fixture(tmp_path: Path): + from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem + + source_dir = tmp_path / "source" + source_dir.mkdir() + filesystem = PageIndexFileSystem(workspace=tmp_path / "workspace") + filesystem.metadata.register_schema({"fields": {"department": "string"}}) + + def add_file( + filename: str, + *, + folder_path: str, + external_id: str, + title: str, + domain: str, + ) -> None: + source = source_dir / filename + source.write_text(f"{title} fixture text", encoding="utf-8") + filesystem.register_file( + storage_uri=source.as_uri(), + source_path=f"docs/{filename}", + folder_path=folder_path, + external_id=external_id, + title=title, + content=source.read_text(encoding="utf-8"), + metadata={"department": domain}, + ) + + add_file( + "root.txt", + folder_path="/documents", + external_id="doc_root", + title="Root document", + domain="ops", + ) + add_file( + "child.txt", + folder_path="/documents/team", + external_id="doc_child", + title="Child document", + domain="ops", + ) + add_file( + "deep.txt", + folder_path="/documents/team/deep", + external_id="doc_deep", + title="Deep document", + domain="ops", + ) + add_file( + "other.txt", + folder_path="/documents/team", + external_id="doc_other", + title="Other document", + domain="finance", + ) + return PIFSCommandExecutor(filesystem, json_output=True) + + +def _data(output: str): + return json.loads(output)["data"] + + +def test_find_maxdepth_one_returns_direct_files_only(tmp_path): + executor = _register_find_fixture(tmp_path) + + rows = _data(executor.execute("find /documents -maxdepth 1 -type f")) + + assert [row["external_id"] for row in rows] == ["doc_root"] + + +def test_find_maxdepth_zero_type_directory_returns_start_folder(tmp_path): + executor = _register_find_fixture(tmp_path) + + rows = _data(executor.execute("find /documents -maxdepth 0 -type d")) + + assert [row["path"] for row in rows] == ["/documents"] + + +def test_find_maxdepth_combines_with_where_and_limit(tmp_path): + executor = _register_find_fixture(tmp_path) + + rows = _data( + executor.execute( + """find /documents -maxdepth 2 -type f --where '{"department":"ops"}' --limit 1""" + ) + ) + + assert len(rows) == 1 + assert rows[0]["metadata"]["department"] == "ops" + assert rows[0]["folder_path"] in {"/documents", "/documents/team"} + + +def test_find_maxdepth_rejects_invalid_values_and_unsupported_options(tmp_path): + from pageindex.filesystem.commands import PIFSCommandError + + executor = _register_find_fixture(tmp_path) + + with pytest.raises(PIFSCommandError, match="find -maxdepth requires an integer >= 0"): + executor.execute("find /documents -maxdepth nope -type f") + with pytest.raises(PIFSCommandError, match="find -maxdepth requires an integer >= 0"): + executor.execute("find /documents -maxdepth -1 -type f") + with pytest.raises(PIFSCommandError, match="Unsupported find option: -exec"): + executor.execute("find /documents -maxdepth 1 -type f -exec") + + +def test_find_maxdepth_is_advertised_to_agents(tmp_path): + executor = _register_find_fixture(tmp_path) + + assert "-maxdepth N -type f|d" in executor.describe_available_command_surfaces() + assert executor.command_capabilities()["retrieval"]["lexical"]["find_maxdepth"] is True From 06d8553a0a6eb69efcdcf3aef96e7253c97c6789 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 15:25:37 +0800 Subject: [PATCH 09/50] fix(filesystem): clarify folder path metadata errors --- examples/pifs_demo.py | 2 ++ pageindex/filesystem/agent.py | 2 ++ pageindex/filesystem/commands.py | 3 ++- pageindex/filesystem/metadata.py | 7 +++++++ tests/test_pifs_find_maxdepth.py | 14 ++++++++++++++ 5 files changed, 27 insertions(+), 1 deletion(-) diff --git a/examples/pifs_demo.py b/examples/pifs_demo.py index 839d37c5f..fa610ad9a 100644 --- a/examples/pifs_demo.py +++ b/examples/pifs_demo.py @@ -63,6 +63,8 @@ - Start with ls or tree to understand the workspace. - Use refs exactly as listed, such as ref_1, or use a concrete file path from ls output. Do not invent paths like /documents/ref_1. +- Folder paths such as /documents are positional command targets; do not put + folder paths inside --where. - Use search-summary when available to find likely documents. Quote multi-word queries and include a path, for example: search-summary "Federal Reserve supervision regulation" /documents diff --git a/pageindex/filesystem/agent.py b/pageindex/filesystem/agent.py index 15e95a555..dce9aca97 100644 --- a/pageindex/filesystem/agent.py +++ b/pageindex/filesystem/agent.py @@ -45,6 +45,8 @@ - The bash tool is a PageIndex virtual shell, not an operating-system shell. - The default agent tool surface is read-only. - Use only commands listed in the workspace capabilities. +- Folder paths such as /documents are positional command targets; never put folder paths in --where. +- Use --where only with metadata fields shown by stat --schema. - grep -R performs lexical evidence search. - Semantic search commands are candidate-discovery tools and do not guarantee literal text matches. - Tool errors are returned as ERROR text; recover by trying an available command. diff --git a/pageindex/filesystem/commands.py b/pageindex/filesystem/commands.py index 2e36179ba..730deeea8 100644 --- a/pageindex/filesystem/commands.py +++ b/pageindex/filesystem/commands.py @@ -88,7 +88,8 @@ def describe_available_command_surfaces(self) -> str: "Available command surfaces for this workspace:", "- mode: read-only inspection", "- ls/tree: folder browsing", - "- find --where: exact/canonical metadata DSL filtering", + "- find : folder path is positional; do not put paths in --where", + "- find --where: exact/canonical metadata DSL filtering using stat --schema fields only", "- find -maxdepth N -type f|d: bounded folder traversal for find", "- grep -R: recursive lexical/FTS search only; semantic vector prefilter is disabled", "- cat --structure/--node/--page: cached PageIndex reads for PDF/Markdown files", diff --git a/pageindex/filesystem/metadata.py b/pageindex/filesystem/metadata.py index 2766282ec..60d7beb97 100644 --- a/pageindex/filesystem/metadata.py +++ b/pageindex/filesystem/metadata.py @@ -15,6 +15,7 @@ class MetadataQueryEngine: FIELD_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_]*$") OPERATORS = {"$eq", "$ne", "$in", "$gt", "$gte", "$lt", "$lte", "$contains"} LOGICAL_OPERATORS = {"$and", "$or"} + FOLDER_SCOPE_FIELD_HINTS = {"path", "folder", "folders", "folder_path", "folder_paths"} MAX_DEPTH = 5 def __init__(self, store: Any): @@ -121,6 +122,12 @@ def _validate_field_condition(self, field: str, condition: Any) -> None: def validate_field(self, field: str) -> None: self.validate_field_name(field) if not self.store.metadata_field_exists(field): + if field in self.FOLDER_SCOPE_FIELD_HINTS: + raise MetadataQueryError( + f"Unknown metadata field: {field}. Folder paths are positional PIFS paths, " + "not metadata fields; use `ls /documents` or `find /documents -type f`. " + "Use --where only with fields from `stat --schema`." + ) raise MetadataQueryError(f"Unknown metadata field: {field}") def validate_field_name(self, field: str) -> None: diff --git a/tests/test_pifs_find_maxdepth.py b/tests/test_pifs_find_maxdepth.py index 56c32e492..2635f2729 100644 --- a/tests/test_pifs_find_maxdepth.py +++ b/tests/test_pifs_find_maxdepth.py @@ -115,3 +115,17 @@ def test_find_maxdepth_is_advertised_to_agents(tmp_path): assert "-maxdepth N -type f|d" in executor.describe_available_command_surfaces() assert executor.command_capabilities()["retrieval"]["lexical"]["find_maxdepth"] is True + + +def test_where_path_error_points_to_folder_scope(tmp_path): + from pageindex.filesystem.commands import PIFSCommandError + + executor = _register_find_fixture(tmp_path) + + with pytest.raises(PIFSCommandError) as exc_info: + executor.execute("""find --where '{"path":"/documents"}'""") + + message = str(exc_info.value) + assert "Folder paths are positional PIFS paths" in message + assert "find /documents -type f" in message + assert "stat --schema" in message From a80b84dae432912f6cf5a1cf18bdf332676216c2 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 15:39:16 +0800 Subject: [PATCH 10/50] fix(filesystem): remove session ref aliases from cli output --- examples/pifs_demo.py | 28 +++++++++--------- pageindex/filesystem/agent.py | 10 ++++--- pageindex/filesystem/commands.py | 51 +++++++++++++++++++++----------- pageindex/filesystem/core.py | 24 ++++----------- tests/test_pifs_find_maxdepth.py | 23 ++++++++++++++ 5 files changed, 82 insertions(+), 54 deletions(-) diff --git a/examples/pifs_demo.py b/examples/pifs_demo.py index fa610ad9a..230d58673 100644 --- a/examples/pifs_demo.py +++ b/examples/pifs_demo.py @@ -5,7 +5,7 @@ through the PageIndex FileSystem shell instead of direct PageIndex document tools. The agent receives one read-only bash-like PIFS tool and must retrieve evidence through commands such as ls, tree, find, grep, search-summary, -cat --structure, cat --page, and cat --node. +cat --structure, cat --page, and cat --node. The demo uses PDFs under examples/documents. When a matching examples/documents/results/*_structure.json file exists, it is loaded into the @@ -61,8 +61,8 @@ Retrieval strategy: - Start with ls or tree to understand the workspace. -- Use refs exactly as listed, such as ref_1, or use a concrete file path from - ls output. Do not invent paths like /documents/ref_1. +- Use concrete PIFS paths from ls/find output, such as /documents/report.pdf, + or stable file_ref/document ids. Do not invent temporary ref_N aliases. - Folder paths such as /documents are positional command targets; do not put folder paths inside --where. - Use search-summary when available to find likely documents. @@ -73,12 +73,12 @@ - Use grep -R only for lexical evidence; do not treat semantic candidates as literal matches. - Run one evidence command at a time. Do not chain large commands like - cat --structure, grep, and cat --page in one bash call. -- For PDFs, use cat --structure to inspect the PageIndex tree, then - cat --page for evidence, for example: - cat ref_1 --page 31-35 -- For page-range questions, use cat --structure to identify the full section - range. Then run cat --page on the smallest useful evidence range, usually the + cat --structure, grep, and cat --page in one bash call. +- For PDFs, use cat --structure to inspect the PageIndex tree, then + cat --page for evidence, for example: + cat /documents/2023-annual-report.pdf --page 31-35 +- For page-range questions, use cat --structure to identify the full section + range. Then run cat --page on the smallest useful evidence range, usually the section start page or first 1-2 pages, before the final answer. Do not print a broad multi-page section unless the user asks to read the whole section. - Do not use cat --all on PDFs. @@ -630,11 +630,11 @@ def run_smoke_commands( verbose=verbose, ) - first_ref = registered[0]["file_ref"] if registered else None - if not first_ref: + first_target = f"/documents/{Path(str(registered[0]['path'])).name}" if registered else None + if not first_target: return - command = f"stat {first_ref}" + command = f"stat {first_target}" stat = execute_json_command(json_executor, command) stat_data = stat.get("data") or {} show_capability( @@ -648,7 +648,7 @@ def run_smoke_commands( verbose=verbose, ) - command = f"cat {first_ref} --structure" + command = f"cat {first_target} --structure" structure_payload = execute_json_command(json_executor, command) structure_data = structure_payload.get("data") or {} structure = structure_data.get("structure") or [] @@ -666,7 +666,7 @@ def run_smoke_commands( ) evidence_range = opening_page_range_for_node(supervision_node) or "1-2" - command = f"cat {first_ref} --page {evidence_range}" + command = f"cat {first_target} --page {evidence_range}" page = execute_json_command(json_executor, command) page_text = str((page.get("data") or {}).get("text") or "") show_capability( diff --git a/pageindex/filesystem/agent.py b/pageindex/filesystem/agent.py index dce9aca97..2fbe03460 100644 --- a/pageindex/filesystem/agent.py +++ b/pageindex/filesystem/agent.py @@ -36,8 +36,10 @@ semantic search commands return candidate documents and do not guarantee literal text matches. Errors are returned as text prefixed with ERROR. Do not call commands that are not listed as available. When evidence is required, inspect it -with cat or grep before answering. Prefer shell-like target-first cat syntax: -cat --structure, cat --page 31-59, and cat --node 0009. +with cat or grep before answering. Prefer shell-like target-first cat syntax +with stable targets: cat --structure, cat --page 31-59, and +cat --node 0009. You may also use file_ref or document_id when a path is +ambiguous. """ AGENT_TOOL_POLICY = """ @@ -51,8 +53,8 @@ - Semantic search commands are candidate-discovery tools and do not guarantee literal text matches. - Tool errors are returned as ERROR text; recover by trying an available command. - Use cat or grep to gather evidence before making source-backed claims. -- Prefer target-first cat syntax: cat --structure, cat --page 31-59, cat --node . -- Do not call cat --page ; if you need a page span, use cat --page -. +- Prefer target-first cat syntax with stable targets: cat --structure, cat --page 31-59, cat --node . +- Do not call cat --page ; if you need a page span, use cat --page -. """ STREAM_MODE_ALIASES = { diff --git a/pageindex/filesystem/commands.py b/pageindex/filesystem/commands.py index 730deeea8..860e1b7f9 100644 --- a/pageindex/filesystem/commands.py +++ b/pageindex/filesystem/commands.py @@ -92,8 +92,8 @@ def describe_available_command_surfaces(self) -> str: "- find --where: exact/canonical metadata DSL filtering using stat --schema fields only", "- find -maxdepth N -type f|d: bounded folder traversal for find", "- grep -R: recursive lexical/FTS search only; semantic vector prefilter is disabled", - "- cat --structure/--node/--page: cached PageIndex reads for PDF/Markdown files", - "- cat --all: full text artifact reads for txt/text files", + "- cat --structure/--node/--page: cached PageIndex reads for PDF/Markdown files", + "- cat --all: full text artifact reads for txt/text files", ] if "entity" in semantic_channels: lines.append("- find --name: entity semantic candidate discovery alias") @@ -115,7 +115,7 @@ def describe_available_command_surfaces(self) -> str: ) if not semantic.get("commands"): lines.append("- semantic vector commands: none available in this workspace") - lines.append("- grep , cat, stat: evidence inspection") + lines.append("- grep , cat, stat: evidence inspection") return "\n".join(lines) def execute(self, command: str) -> str: @@ -432,8 +432,9 @@ def _cmd_cat(self, args: list[str]) -> Any: target = args[0] if target.startswith("-"): raise PIFSCommandError( - "cat syntax is target-first: cat --structure, " - "cat --page 31-59, or cat --node 0009" + "cat syntax is target-first: cat --structure, " + "cat --page 31-59, or " + "cat --node 0009" ) location = "all" structural_mode: str | None = None @@ -467,8 +468,8 @@ def _cmd_cat(self, args: list[str]) -> Any: raise PIFSCommandError(f"Unsupported cat option: {arg}") else: raise PIFSCommandError( - "cat accepts one file target. Use: cat --page , " - "for example: cat ref_1 --page 31-59" + "cat accepts one file target. Use: cat --page , " + "for example: cat /documents/report.pdf --page 31-59" ) i += 1 if structural_mode == "structure": @@ -479,7 +480,7 @@ def _cmd_cat(self, args: list[str]) -> Any: if not page_range or not re.fullmatch(r"\d+(?:-\d+)?", page_range): raise PIFSCommandError( "cat --page requires one page selector like 31 or 31-59. " - "Use: cat --page " + "Use: cat --page " ) return self.filesystem.pageindex_pages(target, page_range) return self.filesystem.cat_text_artifact(target, location) @@ -804,7 +805,7 @@ def _render_grep(self, data: Any) -> str: ) if mode == "matches": return "\n".join( - f"{item['reference_id']}:{item['line']}: " + f"{self._file_target_path(item)}:{item['line']}: " f"{self._compact_text(item['text'], max_chars=220)}" for item in data.get("data", []) ) @@ -835,7 +836,7 @@ def _render_stat(self, data: Any) -> str: lines.append(f"{name}: {field.get('type', 'string')}") return "\n".join(lines) lines = [ - f"ref: {data.get('target') or data.get('file_ref')}", + f"target: {data.get('target') or data.get('file_ref')}", f"file_ref: {data.get('file_ref')}", f"document_id: {data.get('external_id') or data.get('document_id') or '-'}", f"source_path: {data.get('source_path') or '-'}", @@ -857,23 +858,37 @@ def _render_stat(self, data: Any) -> str: def _file_row_text(self, item: dict[str, Any]) -> str: file_ref = item.get("file_ref") - ref = item.get("reference_id") or (self.filesystem._reference_for(file_ref) if file_ref else "-") doc_id = item.get("external_id") or item.get("document_id") or "-" title = self._compact_text(item.get("title") or item.get("name") or "", max_chars=80) source_path = item.get("source_path") or "-" folder_paths = item.get("folder_paths") or self._folder_paths_for_file(file_ref) folders = f" folders={','.join(folder_paths)}" if folder_paths else "" - return f"{ref} {doc_id} {title} {source_path}{folders}".strip() + target = self._file_target_path(item) + return f"{target} id={doc_id} file_ref={file_ref or '-'} title={title} source={source_path}{folders}".strip() def _grep_file_hit_text(self, item: dict[str, Any]) -> str: doc_id = item.get("external_id") or "-" - source_path = item.get("source_path") or "-" line = item.get("line") or 1 + target = self._file_target_path(item) return ( - f"{item['reference_id']} {doc_id} {source_path}:{line}: " + f"{target}:{line}: id={doc_id} " f"{self._compact_text(item.get('text') or '', max_chars=180)}" ) + def _file_target_path(self, item: dict[str, Any]) -> str: + file_ref = item.get("file_ref") + title = str(item.get("title") or item.get("name") or "").strip() + folder_paths = item.get("folder_paths") or [] + folder_path = item.get("folder_path") + if not folder_paths and folder_path: + folder_paths = [folder_path] + if not folder_paths: + folder_paths = self._folder_paths_for_file(file_ref) + if folder_paths and title: + folder = str(folder_paths[0] or "/").rstrip("/") + return f"{folder}/{title}" if folder else f"/{title}" + return str(item.get("source_path") or item.get("external_id") or file_ref or "-") + def _semantic_retrieval_query(self, query: str) -> str: query = str(query or "").strip() context = str(self.query_context or "").strip() @@ -1040,11 +1055,10 @@ def _grep_source_file_hits( continue if direct_only and self._folder_path_for_source_path(file_row["source_path"]) != folder_path: continue - reference_id = self.filesystem._reference_for(file_row["file_ref"]) line_number, text = self._first_matching_source_line(path, query) hits.append( { - "reference_id": reference_id, + "reference_id": file_row["external_id"] or file_row["file_ref"], "file_ref": file_row["file_ref"], "external_id": file_row["external_id"], "title": file_row["title"], @@ -1060,17 +1074,18 @@ def _grep_source_file_hits( def _grep_file_matches(self, target: str, query: str, *, limit: int) -> list[dict[str, Any]]: file_ref = self.filesystem._resolve_reference(target) - reference_id = self.filesystem._reference_for(file_ref) entry = self.filesystem.store.get_file(file_ref) matches = [] for line_number, line in enumerate(self.filesystem.store.read_text(file_ref).splitlines(), 1): if self._line_matches(line, query): matches.append( { - "reference_id": reference_id, + "reference_id": entry.external_id or file_ref, "file_ref": file_ref, "external_id": entry.external_id, + "title": entry.title, "source_path": entry.source_path, + "folder_paths": self._folder_paths_for_file(file_ref), "line": line_number, "text": self._compact_text(line, max_chars=220), } diff --git a/pageindex/filesystem/core.py b/pageindex/filesystem/core.py index d45e6791c..7110fa6a0 100644 --- a/pageindex/filesystem/core.py +++ b/pageindex/filesystem/core.py @@ -102,7 +102,6 @@ def __init__( self.workspace = Path(workspace).expanduser() self.store = SQLiteFileSystemStore(self.workspace) self.metadata = MetadataQueryEngine(self.store) - self._references: dict[str, str] = {} self.semantic_retrieval_backend = semantic_retrieval_backend self.metadata_generator = metadata_generator self.summary_projection_indexer = summary_projection_indexer @@ -388,7 +387,6 @@ def search( results = [] scope_path = self._scope_folder_path(scope) for row in rows: - reference_id = self._reference_for(row["file_ref"]) folder_paths = [ folder["path"] for folder in self.store.folder_memberships(row["file_ref"]) @@ -396,7 +394,7 @@ def search( folder_path = self._preferred_folder_path(folder_paths, scope_path, row["folder_path"]) results.append( SearchResult( - reference_id=reference_id, + reference_id=row["external_id"] or row["file_ref"], file_ref=row["file_ref"], external_id=row["external_id"], title=row["title"], @@ -693,8 +691,9 @@ def _require_text_artifact_file(self, entry: Any, command: str) -> None: raise ValueError( f"{command} is only supported for txt/text files; " f"got source_path={entry.source_path!r}, content_type={entry.content_type!r}. " - "Use cat --structure, cat --page, or cat --node " - "for PDF/Markdown PageIndex files." + "Use cat --structure, " + "cat --page, or " + "cat --node for PDF/Markdown PageIndex files." ) def _require_pageindex_document_file(self, entry: Any, command: str) -> None: @@ -703,7 +702,7 @@ def _require_pageindex_document_file(self, entry: Any, command: str) -> None: raise ValueError( f"{command} is only supported for PDF/Markdown PageIndex files; " f"got source_path={entry.source_path!r}, content_type={entry.content_type!r}. " - "Use cat --all for txt/text files." + "Use cat --all for txt/text files." ) @classmethod @@ -1254,8 +1253,6 @@ def _structural_unavailable( return result def _resolve_reference(self, reference_id: str) -> str: - if reference_id in self._references: - return self._references[reference_id] return self.store.resolve_file_ref(reference_id) def _should_use_semantic_retrieval( @@ -1315,7 +1312,6 @@ def _semantic_search( continue seen.add(file_ref) entry = self.store.get_file(file_ref) - reference_id = self._reference_for(file_ref) folder_paths = [ folder["path"] for folder in self.store.folder_memberships(file_ref) @@ -1323,7 +1319,7 @@ def _semantic_search( folder_path = self._preferred_folder_path(folder_paths, scope_path, entry.folder_path) results.append( SearchResult( - reference_id=reference_id, + reference_id=entry.external_id or file_ref, file_ref=file_ref, external_id=entry.external_id, title=entry.title, @@ -1348,14 +1344,6 @@ def _semantic_search( break return results - def _reference_for(self, file_ref: str) -> str: - for reference_id, existing in self._references.items(): - if existing == file_ref: - return reference_id - reference_id = f"ref_{len(self._references) + 1}" - self._references[reference_id] = file_ref - return reference_id - @staticmethod def _build_descriptor(title: str, metadata: dict[str, Any]) -> str: source = metadata.get("source_type") or metadata.get("repo") or metadata.get("channel") diff --git a/tests/test_pifs_find_maxdepth.py b/tests/test_pifs_find_maxdepth.py index 2635f2729..580633e30 100644 --- a/tests/test_pifs_find_maxdepth.py +++ b/tests/test_pifs_find_maxdepth.py @@ -75,6 +75,29 @@ def test_find_maxdepth_one_returns_direct_files_only(tmp_path): assert [row["external_id"] for row in rows] == ["doc_root"] +def test_find_output_is_path_first_without_session_refs(tmp_path): + executor = _register_find_fixture(tmp_path) + executor.json_output = False + + output = executor.execute("find /documents -maxdepth 1 -type f") + + assert output.startswith("/documents/Root document id=doc_root file_ref=file_") + assert "ref_1" not in output + assert "title=Root document" in output + + +def test_stable_path_targets_work_without_session_refs(tmp_path): + executor = _register_find_fixture(tmp_path) + executor.json_output = False + + stat = executor.execute("stat '/documents/Root document'") + text = executor.execute("cat '/documents/Root document' --all") + + assert "target: /documents/Root document" in stat + assert "document_id: doc_root" in stat + assert "Root document fixture text" in text + + def test_find_maxdepth_zero_type_directory_returns_start_folder(tmp_path): executor = _register_find_fixture(tmp_path) From b2089712b3682a140dd5af2ea2c249b0dbf41877 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 15:53:25 +0800 Subject: [PATCH 11/50] refactor(filesystem): remove reference id concept --- pageindex/filesystem/commands.py | 5 +--- pageindex/filesystem/core.py | 48 +++++++++++++++----------------- pageindex/filesystem/types.py | 2 -- 3 files changed, 23 insertions(+), 32 deletions(-) diff --git a/pageindex/filesystem/commands.py b/pageindex/filesystem/commands.py index 860e1b7f9..d56df792a 100644 --- a/pageindex/filesystem/commands.py +++ b/pageindex/filesystem/commands.py @@ -989,7 +989,6 @@ def _grep_file_hits_from_results( continue hits.append( { - "reference_id": result.reference_id, "file_ref": result.file_ref, "external_id": result.external_id, "title": result.title, @@ -1058,7 +1057,6 @@ def _grep_source_file_hits( line_number, text = self._first_matching_source_line(path, query) hits.append( { - "reference_id": file_row["external_id"] or file_row["file_ref"], "file_ref": file_row["file_ref"], "external_id": file_row["external_id"], "title": file_row["title"], @@ -1073,14 +1071,13 @@ def _grep_source_file_hits( return hits def _grep_file_matches(self, target: str, query: str, *, limit: int) -> list[dict[str, Any]]: - file_ref = self.filesystem._resolve_reference(target) + file_ref = self.filesystem._resolve_target(target) entry = self.filesystem.store.get_file(file_ref) matches = [] for line_number, line in enumerate(self.filesystem.store.read_text(file_ref).splitlines(), 1): if self._line_matches(line, query): matches.append( { - "reference_id": entry.external_id or file_ref, "file_ref": file_ref, "external_id": entry.external_id, "title": entry.title, diff --git a/pageindex/filesystem/core.py b/pageindex/filesystem/core.py index 7110fa6a0..9a0f88b3d 100644 --- a/pageindex/filesystem/core.py +++ b/pageindex/filesystem/core.py @@ -394,7 +394,6 @@ def search( folder_path = self._preferred_folder_path(folder_paths, scope_path, row["folder_path"]) results.append( SearchResult( - reference_id=row["external_id"] or row["file_ref"], file_ref=row["file_ref"], external_id=row["external_id"], title=row["title"], @@ -509,11 +508,11 @@ def retrieval_capabilities(self) -> dict[str, Any]: def find( self, - reference_id: str, + target: str, patterns: Union[str, list[str]], limit: int = 20, ) -> list[OpenResult]: - file_ref = self._resolve_reference(reference_id) + file_ref = self._resolve_target(target) patterns = [patterns] if isinstance(patterns, str) else list(patterns) lowered_patterns = [pattern.lower() for pattern in patterns if pattern] if not lowered_patterns: @@ -526,13 +525,13 @@ def find( if any(pattern in haystack for pattern in lowered_patterns): start = max(1, i - 1) end = min(len(lines), i + 1) - matches.append(self._open_lines(reference_id, file_ref, start, end)) + matches.append(self._open_lines(file_ref, start, end)) if len(matches) >= limit: break return matches - def open(self, reference_id: str, location: str = "all") -> OpenResult: - file_ref = self._resolve_reference(reference_id) + def open(self, target: str, location: str = "all") -> OpenResult: + file_ref = self._resolve_target(target) entry = self.store.get_file(file_ref) if self._file_format(entry) in {"pdf", "markdown", "pageindex"}: raise ValueError( @@ -540,21 +539,21 @@ def open(self, reference_id: str, location: str = "all") -> OpenResult: "use pageindex_structure(), pageindex_pages(), or pageindex_node()." ) if str(location).strip().lower() in {"all", "full", "*"}: - return self._open_all(reference_id, file_ref) + return self._open_all(file_ref) start, end = self._parse_line_range(location) - return self._open_lines(reference_id, file_ref, start, end) + return self._open_lines(file_ref, start, end) - def cat_text_artifact(self, reference_id: str, location: str = "all") -> OpenResult: - file_ref = self._resolve_reference(reference_id) + def cat_text_artifact(self, target: str, location: str = "all") -> OpenResult: + file_ref = self._resolve_target(target) entry = self.store.get_file(file_ref) self._require_text_artifact_file(entry, "cat --all") if str(location).strip().lower() in {"all", "full", "*"}: - return self._open_all(reference_id, file_ref) + return self._open_all(file_ref) start, end = self._parse_line_range(location) - return self._open_lines(reference_id, file_ref, start, end) + return self._open_lines(file_ref, start, end) - def pageindex_structure(self, reference_id: str) -> dict[str, Any]: - file_ref = self._resolve_reference(reference_id) + def pageindex_structure(self, target: str) -> dict[str, Any]: + file_ref = self._resolve_target(target) entry = self.store.get_file(file_ref) self._require_pageindex_document_file(entry, "cat --structure") client, doc_id = self._pageindex_client_doc_for_entry(entry) @@ -585,8 +584,8 @@ def pageindex_structure(self, reference_id: str) -> dict[str, Any]: "structure": strip_pageindex_text_fields(structure), } - def pageindex_node(self, reference_id: str, node_id: str) -> dict[str, Any]: - file_ref = self._resolve_reference(reference_id) + def pageindex_node(self, target: str, node_id: str) -> dict[str, Any]: + file_ref = self._resolve_target(target) entry = self.store.get_file(file_ref) self._require_pageindex_document_file(entry, "cat --node") client, doc_id = self._pageindex_client_doc_for_entry(entry) @@ -637,8 +636,8 @@ def pageindex_node(self, reference_id: str, node_id: str) -> dict[str, Any]: "text": text, } - def pageindex_pages(self, reference_id: str, pages: str) -> dict[str, Any]: - file_ref = self._resolve_reference(reference_id) + def pageindex_pages(self, target: str, pages: str) -> dict[str, Any]: + file_ref = self._resolve_target(target) entry = self.store.get_file(file_ref) self._require_pageindex_document_file(entry, "cat --page") client, doc_id = self._pageindex_client_doc_for_entry(entry) @@ -682,7 +681,7 @@ def pageindex_pages(self, reference_id: str, pages: str) -> dict[str, Any]: } def _stat(self, target: str) -> dict[str, Any]: - file_ref = self._resolve_reference(target) + file_ref = self._resolve_target(target) return self.store.file_info(file_ref) def _require_text_artifact_file(self, entry: Any, command: str) -> None: @@ -1196,14 +1195,13 @@ def _refresh_record_metadata_generation( self._merge_metadata_values(record["metadata"], record["derived_metadata"]) ) - def _open_lines(self, reference_id: str, file_ref: str, start: int, end: int) -> OpenResult: + def _open_lines(self, file_ref: str, start: int, end: int) -> OpenResult: entry = self.store.get_file(file_ref) lines = self.store.read_text(file_ref).splitlines() start = max(1, start) end = min(max(start, end), len(lines)) text = "\n".join(lines[start - 1:end]) return OpenResult( - reference_id=reference_id, file_ref=file_ref, start_line=start, end_line=end, @@ -1213,12 +1211,11 @@ def _open_lines(self, reference_id: str, file_ref: str, start: int, end: int) -> source_path=entry.source_path, ) - def _open_all(self, reference_id: str, file_ref: str) -> OpenResult: + def _open_all(self, file_ref: str) -> OpenResult: entry = self.store.get_file(file_ref) text = self.store.read_text(file_ref) line_count = len(text.splitlines()) return OpenResult( - reference_id=reference_id, file_ref=file_ref, start_line=1, end_line=line_count, @@ -1252,8 +1249,8 @@ def _structural_unavailable( result["pages"] = pages return result - def _resolve_reference(self, reference_id: str) -> str: - return self.store.resolve_file_ref(reference_id) + def _resolve_target(self, target: str) -> str: + return self.store.resolve_file_ref(target) def _should_use_semantic_retrieval( self, @@ -1319,7 +1316,6 @@ def _semantic_search( folder_path = self._preferred_folder_path(folder_paths, scope_path, entry.folder_path) results.append( SearchResult( - reference_id=entry.external_id or file_ref, file_ref=file_ref, external_id=entry.external_id, title=entry.title, diff --git a/pageindex/filesystem/types.py b/pageindex/filesystem/types.py index eac0a15ce..4cd573fcb 100644 --- a/pageindex/filesystem/types.py +++ b/pageindex/filesystem/types.py @@ -6,7 +6,6 @@ @dataclass(frozen=True) class SearchResult: - reference_id: str file_ref: str external_id: Optional[str] title: str @@ -29,7 +28,6 @@ class SearchResult: @dataclass(frozen=True) class OpenResult: - reference_id: str file_ref: str start_line: int end_line: int From cb1bfb00fafa9ab249bde5c3577c33adedf47451 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 16:04:46 +0800 Subject: [PATCH 12/50] fix(filesystem): reuse agent session in pifs chat --- pageindex/filesystem/agent.py | 242 +++++++++++++++++++++----------- pageindex/filesystem/cli.py | 10 +- tests/test_pifs_agent_stream.py | 10 ++ tests/test_pifs_cli.py | 83 ++++++++--- 4 files changed, 241 insertions(+), 104 deletions(-) diff --git a/pageindex/filesystem/agent.py b/pageindex/filesystem/agent.py index 2fbe03460..a1a384747 100644 --- a/pageindex/filesystem/agent.py +++ b/pageindex/filesystem/agent.py @@ -354,6 +354,12 @@ def emit_tool_call(self, command: str, *, force: bool = False) -> None: self._start_section("tool_call", "[llm -> pifs command]") print(command, file=self.output, flush=True) + def emit_request_started(self) -> None: + if not (self.wants_model_stream or self.wants_tool_stream): + return + self._start_section("request_started", "[llm request started]") + print("waiting for first model token or PIFS tool call...", file=self.output, flush=True) + def emit_tool_result( self, *, @@ -408,43 +414,167 @@ def run_pifs_agent( tool_log: list[dict[str, Any]] | None = None, agent_log: list[dict[str, Any]] | None = None, ) -> str: - try: - from agents import Agent, OpenAIChatCompletionsModel, Runner, function_tool, set_tracing_disabled - from openai import AsyncOpenAI - except ModuleNotFoundError as exc: - if exc.name == "agents": - raise RuntimeError("openai-agents is required to run the PageIndex FileSystem agent") from exc - raise - - set_tracing_disabled(should_disable_pifs_agent_tracing()) - normalized_stream_mode = normalize_agent_stream_mode(stream_mode) - executor = PIFSCommandExecutor( - filesystem, - json_output=False, - query_context=extract_agent_question_text(question), - ) - observer = PIFSAgentStreamObserver(normalized_stream_mode, stream_log=agent_log) - instructions = build_pifs_agent_instructions( + session = PIFSAgentSession( filesystem, + model=model, root=root, system_prompt=system_prompt, - executor=executor, + max_turns=max_turns, + max_seconds=max_seconds, + verbose=verbose, + stream_mode=stream_mode, + reasoning_effort=reasoning_effort, + reasoning_summary=reasoning_summary, + output_type=output_type, + tool_log=tool_log, + agent_log=agent_log, + persist_conversation=False, ) + return session.run(question) + + +class PIFSAgentSession: + def __init__( + self, + filesystem: PageIndexFileSystem, + *, + model: str, + root: str = "/", + system_prompt: str | None = None, + max_turns: int = 20, + max_seconds: float | None = 60, + verbose: bool = False, + stream_mode: str = "off", + reasoning_effort: str | None = None, + reasoning_summary: str | None = None, + output_type: type[Any] | None = None, + tool_log: list[dict[str, Any]] | None = None, + agent_log: list[dict[str, Any]] | None = None, + persist_conversation: bool = True, + ) -> None: + self.filesystem = filesystem + self.max_turns = max_turns + self.max_seconds = max_seconds + self.verbose = verbose + self.tool_log = tool_log + self.agent_log = agent_log + self.normalized_stream_mode = normalize_agent_stream_mode(stream_mode) + self.observer: PIFSAgentStreamObserver | None = None + + try: + from agents import ( + Agent, + OpenAIChatCompletionsModel, + function_tool, + set_tracing_disabled, + ) + from agents.memory import SQLiteSession + from openai import AsyncOpenAI + except ModuleNotFoundError as exc: + if exc.name == "agents": + raise RuntimeError( + "openai-agents is required to run the PageIndex FileSystem agent" + ) from exc + raise + + set_tracing_disabled(should_disable_pifs_agent_tracing()) + self.executor = PIFSCommandExecutor(filesystem, json_output=False) + instructions = build_pifs_agent_instructions( + filesystem, + root=root, + system_prompt=system_prompt, + executor=self.executor, + ) + + @function_tool(description_override=BASH_TOOL_DESCRIPTION.strip()) + def bash(command: str) -> str: + """Run an allowed PageIndex FileSystem virtual shell command.""" + return self._run_bash(command) + + model_settings = build_agent_model_settings( + reasoning_effort=reasoning_effort, + reasoning_summary=reasoning_summary, + ) + base_url = os.environ.get("OPENAI_BASE_URL") + model_config = model + if should_use_openai_compatible_chat_model(base_url): + model_config = OpenAIChatCompletionsModel( + model=model, + openai_client=AsyncOpenAI( + api_key=os.environ.get("OPENAI_API_KEY"), + base_url=base_url, + ), + ) - @function_tool(description_override=BASH_TOOL_DESCRIPTION.strip()) - def bash(command: str) -> str: - """Run an allowed PageIndex FileSystem virtual shell command.""" + agent_kwargs: dict[str, Any] = { + "name": "PageIndexFileSystem", + "instructions": instructions, + "tools": [bash], + "model": model_config, + } + if model_settings is not None: + agent_kwargs["model_settings"] = model_settings + if output_type is not None: + agent_kwargs["output_type"] = output_type + self.agent = Agent(**agent_kwargs) + self.session = SQLiteSession("pifs-chat") if persist_conversation else None + + def run(self, question: str) -> str: + self.executor.query_context = extract_agent_question_text(question) + self.observer = PIFSAgentStreamObserver( + self.normalized_stream_mode, + stream_log=self.agent_log, + ) + self.observer.emit_request_started() + + async def _run_streamed() -> str: + from agents import Runner + + streamed_run = Runner.run_streamed( + self.agent, + question, + max_turns=self.max_turns, + session=self.session, + ) + final_output = "" + try: + async for event in streamed_run.stream_events(): + self.observer.handle_event(event) + final_output = serialize_agent_final_output(streamed_run.final_output) + return final_output + finally: + if not final_output and streamed_run.final_output: + final_output = serialize_agent_final_output(streamed_run.final_output) + self.observer.finish(final_output) + + async def _run() -> str: + if self.max_seconds is None or self.max_seconds <= 0: + return await _run_streamed() + try: + return await asyncio.wait_for(_run_streamed(), timeout=self.max_seconds) + except asyncio.TimeoutError as exc: + raise TimeoutError(f"MaxSecondsExceeded: exceeded {self.max_seconds:g}s") from exc + + try: + asyncio.get_running_loop() + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: + return pool.submit(asyncio.run, _run()).result() + except RuntimeError: + return asyncio.run(_run()) + + def _run_bash(self, command: str) -> str: started = time.time() ok = True - observer.emit_tool_call(command, force=verbose) + assert self.observer is not None + self.observer.emit_tool_call(command, force=self.verbose) try: - output = executor.execute(command) + output = self.executor.execute(command) except PIFSCommandError as exc: ok = False output = f"ERROR: {exc}" seconds = time.time() - started - if tool_log is not None: - tool_log.append( + if self.tool_log is not None: + self.tool_log.append( { "command": command, "ok": ok, @@ -453,63 +583,13 @@ def bash(command: str) -> str: "preview": output[:500], } ) - observer.emit_tool_result(ok=ok, output=output, seconds=seconds, force=verbose) - return output - - model_settings = build_agent_model_settings( - reasoning_effort=reasoning_effort, - reasoning_summary=reasoning_summary, - ) - base_url = os.environ.get("OPENAI_BASE_URL") - model_config = model - if should_use_openai_compatible_chat_model(base_url): - model_config = OpenAIChatCompletionsModel( - model=model, - openai_client=AsyncOpenAI( - api_key=os.environ.get("OPENAI_API_KEY"), - base_url=base_url, - ), + self.observer.emit_tool_result( + ok=ok, + output=output, + seconds=seconds, + force=self.verbose, ) - - agent_kwargs: dict[str, Any] = { - "name": "PageIndexFileSystem", - "instructions": instructions, - "tools": [bash], - "model": model_config, - } - if model_settings is not None: - agent_kwargs["model_settings"] = model_settings - if output_type is not None: - agent_kwargs["output_type"] = output_type - agent = Agent(**agent_kwargs) - - async def _run_streamed() -> str: - streamed_run = Runner.run_streamed(agent, question, max_turns=max_turns) - final_output = "" - try: - async for event in streamed_run.stream_events(): - observer.handle_event(event) - final_output = serialize_agent_final_output(streamed_run.final_output) - return final_output - finally: - if not final_output and streamed_run.final_output: - final_output = serialize_agent_final_output(streamed_run.final_output) - observer.finish(final_output) - - async def _run() -> str: - if max_seconds is None or max_seconds <= 0: - return await _run_streamed() - try: - return await asyncio.wait_for(_run_streamed(), timeout=max_seconds) - except asyncio.TimeoutError as exc: - raise TimeoutError(f"MaxSecondsExceeded: exceeded {max_seconds:g}s") from exc - - try: - asyncio.get_running_loop() - with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: - return pool.submit(asyncio.run, _run()).result() - except RuntimeError: - return asyncio.run(_run()) + return output def extract_agent_question_text(prompt: str) -> str: diff --git a/pageindex/filesystem/cli.py b/pageindex/filesystem/cli.py index ab749e298..aa443a52e 100644 --- a/pageindex/filesystem/cli.py +++ b/pageindex/filesystem/cli.py @@ -9,7 +9,12 @@ from pathlib import Path from typing import Iterator, TextIO -from .agent import REASONING_EFFORT_CHOICES, REASONING_SUMMARY_CHOICES, run_pifs_agent +from .agent import ( + PIFSAgentSession, + REASONING_EFFORT_CHOICES, + REASONING_SUMMARY_CHOICES, + run_pifs_agent, +) from .commands import PIFSCommandError, PIFSCommandExecutor from .core import PageIndexFileSystem @@ -200,6 +205,7 @@ def _run_chat(argv: list[str], *, workspace_default: str | None) -> int: default_stream_mode="all", ) filesystem = _filesystem_from_workspace(args.workspace) + session = PIFSAgentSession(filesystem, **_agent_kwargs(args)) while True: try: question = _sanitize_chat_question(input("pifs> ")) @@ -213,7 +219,7 @@ def _run_chat(argv: list[str], *, workspace_default: str | None) -> int: if question.lower() in EXIT_COMMANDS: break with _suppress_tty_input_echo(): - answer = run_pifs_agent(filesystem, question, **_agent_kwargs(args)) + answer = session.run(question) if args.stream_mode == "off": print(answer) return 0 diff --git a/tests/test_pifs_agent_stream.py b/tests/test_pifs_agent_stream.py index 1b7c9d120..0f56f3601 100644 --- a/tests/test_pifs_agent_stream.py +++ b/tests/test_pifs_agent_stream.py @@ -92,6 +92,16 @@ def test_tool_result_preview_compacts_large_outputs(self): self.assertIn("more lines omitted from preview", printed) self.assertNotIn("line 49", printed) + def test_request_started_prints_immediate_stream_feedback(self): + output = io.StringIO() + observer = PIFSAgentStreamObserver("all", output=output) + + observer.emit_request_started() + + printed = output.getvalue() + self.assertIn("[llm request started]", printed) + self.assertIn("waiting for first model token or PIFS tool call", printed) + def test_raw_reasoning_is_not_logged_by_default_but_summary_is(self): output = io.StringIO() stream_log = [] diff --git a/tests/test_pifs_cli.py b/tests/test_pifs_cli.py index 85e2b6d08..04717c420 100644 --- a/tests/test_pifs_cli.py +++ b/tests/test_pifs_cli.py @@ -127,26 +127,32 @@ def test_cli_chat_runs_one_question_and_exits(monkeypatch, capsys, tmp_path): workspace = tmp_path / "workspace" inputs = iter(["", "Summarize the workspace", "exit"]) - agent_calls = [] + session_instances = [] + session_questions = [] - def fake_run_pifs_agent(filesystem, question, **kwargs): - agent_calls.append((filesystem, question, kwargs)) - return f"answer:{question}" + class FakeSession: + def __init__(self, filesystem, **kwargs): + self.filesystem = filesystem + self.kwargs = kwargs + session_instances.append(self) + + def run(self, question): + session_questions.append((self, question)) + return f"answer:{question}" monkeypatch.setattr(cli, "PageIndexFileSystem", FakeFileSystem) - monkeypatch.setattr(cli, "run_pifs_agent", fake_run_pifs_agent) + monkeypatch.setattr(cli, "PIFSAgentSession", FakeSession) monkeypatch.setattr("builtins.input", lambda prompt="": next(inputs)) status = cli.main(["chat", "--workspace", str(workspace), "--model", "test-model"]) assert status == 0 assert capsys.readouterr().out == "" - assert len(agent_calls) == 1 - filesystem, question, kwargs = agent_calls[0] - assert filesystem.workspace == workspace - assert question == "Summarize the workspace" - assert kwargs["model"] == "test-model" - assert kwargs["stream_mode"] == "all" + assert len(session_instances) == 1 + assert session_instances[0].filesystem.workspace == workspace + assert session_questions == [(session_instances[0], "Summarize the workspace")] + assert session_instances[0].kwargs["model"] == "test-model" + assert session_instances[0].kwargs["stream_mode"] == "all" def test_cli_chat_sanitizes_control_input(monkeypatch, capsys, tmp_path): @@ -156,12 +162,16 @@ def test_cli_chat_sanitizes_control_input(monkeypatch, capsys, tmp_path): inputs = iter(["\x12", "he\x7fllo\x1b[A", "exit"]) agent_calls = [] - def fake_run_pifs_agent(filesystem, question, **kwargs): - agent_calls.append(question) - return f"answer:{question}" + class FakeSession: + def __init__(self, filesystem, **kwargs): + pass + + def run(self, question): + agent_calls.append(question) + return f"answer:{question}" monkeypatch.setattr(cli, "PageIndexFileSystem", FakeFileSystem) - monkeypatch.setattr(cli, "run_pifs_agent", fake_run_pifs_agent) + monkeypatch.setattr(cli, "PIFSAgentSession", FakeSession) monkeypatch.setattr("builtins.input", lambda prompt="": next(inputs)) status = cli.main(["chat", "--workspace", str(workspace), "--stream-mode", "off"]) @@ -205,14 +215,17 @@ def test_cli_chat_stream_mode_can_be_overridden(monkeypatch, tmp_path): workspace = tmp_path / "workspace" inputs = iter(["Summarize the workspace", "exit"]) - agent_calls = [] + session_kwargs = [] - def fake_run_pifs_agent(filesystem, question, **kwargs): - agent_calls.append((filesystem, question, kwargs)) - return f"answer:{question}" + class FakeSession: + def __init__(self, filesystem, **kwargs): + session_kwargs.append(kwargs) + + def run(self, question): + return f"answer:{question}" monkeypatch.setattr(cli, "PageIndexFileSystem", FakeFileSystem) - monkeypatch.setattr(cli, "run_pifs_agent", fake_run_pifs_agent) + monkeypatch.setattr(cli, "PIFSAgentSession", FakeSession) monkeypatch.setattr("builtins.input", lambda prompt="": next(inputs)) status = cli.main( @@ -226,4 +239,32 @@ def fake_run_pifs_agent(filesystem, question, **kwargs): ) assert status == 0 - assert agent_calls[0][2]["stream_mode"] == "tools" + assert session_kwargs[0]["stream_mode"] == "tools" + + +def test_cli_chat_reuses_one_agent_session_for_multiple_questions(monkeypatch, capsys, tmp_path): + from pageindex.filesystem import cli + + workspace = tmp_path / "workspace" + inputs = iter(["first", "second", "exit"]) + sessions = [] + + class FakeSession: + def __init__(self, filesystem, **kwargs): + self.questions = [] + sessions.append(self) + + def run(self, question): + self.questions.append(question) + return f"answer:{question}" + + monkeypatch.setattr(cli, "PageIndexFileSystem", FakeFileSystem) + monkeypatch.setattr(cli, "PIFSAgentSession", FakeSession) + monkeypatch.setattr("builtins.input", lambda prompt="": next(inputs)) + + status = cli.main(["chat", "--workspace", str(workspace), "--stream-mode", "off"]) + + assert status == 0 + assert len(sessions) == 1 + assert sessions[0].questions == ["first", "second"] + assert capsys.readouterr().out == "answer:first\nanswer:second\n" From beed21647f3d5494174b6a7d1fa91c6d34cea1c9 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 16:09:29 +0800 Subject: [PATCH 13/50] fix(filesystem): remove chat request started noise --- pageindex/filesystem/agent.py | 7 ------- tests/test_pifs_agent_stream.py | 10 ---------- 2 files changed, 17 deletions(-) diff --git a/pageindex/filesystem/agent.py b/pageindex/filesystem/agent.py index a1a384747..315fdce92 100644 --- a/pageindex/filesystem/agent.py +++ b/pageindex/filesystem/agent.py @@ -354,12 +354,6 @@ def emit_tool_call(self, command: str, *, force: bool = False) -> None: self._start_section("tool_call", "[llm -> pifs command]") print(command, file=self.output, flush=True) - def emit_request_started(self) -> None: - if not (self.wants_model_stream or self.wants_tool_stream): - return - self._start_section("request_started", "[llm request started]") - print("waiting for first model token or PIFS tool call...", file=self.output, flush=True) - def emit_tool_result( self, *, @@ -525,7 +519,6 @@ def run(self, question: str) -> str: self.normalized_stream_mode, stream_log=self.agent_log, ) - self.observer.emit_request_started() async def _run_streamed() -> str: from agents import Runner diff --git a/tests/test_pifs_agent_stream.py b/tests/test_pifs_agent_stream.py index 0f56f3601..1b7c9d120 100644 --- a/tests/test_pifs_agent_stream.py +++ b/tests/test_pifs_agent_stream.py @@ -92,16 +92,6 @@ def test_tool_result_preview_compacts_large_outputs(self): self.assertIn("more lines omitted from preview", printed) self.assertNotIn("line 49", printed) - def test_request_started_prints_immediate_stream_feedback(self): - output = io.StringIO() - observer = PIFSAgentStreamObserver("all", output=output) - - observer.emit_request_started() - - printed = output.getvalue() - self.assertIn("[llm request started]", printed) - self.assertIn("waiting for first model token or PIFS tool call", printed) - def test_raw_reasoning_is_not_logged_by_default_but_summary_is(self): output = io.StringIO() stream_log = [] From c12c2de65b56f16a650f81e5d73b0e1354b7c825 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 16:13:31 +0800 Subject: [PATCH 14/50] fix(filesystem): guide structural node reads --- pageindex/filesystem/agent.py | 7 ++++++- tests/test_pifs_agent_stream.py | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pageindex/filesystem/agent.py b/pageindex/filesystem/agent.py index 315fdce92..9bddb1603 100644 --- a/pageindex/filesystem/agent.py +++ b/pageindex/filesystem/agent.py @@ -39,7 +39,9 @@ with cat or grep before answering. Prefer shell-like target-first cat syntax with stable targets: cat --structure, cat --page 31-59, and cat --node 0009. You may also use file_ref or document_id when a path is -ambiguous. +ambiguous. After structure identifies a relevant section node, prefer +cat --node ; use cat --page when the user asks +for page-level evidence, no suitable node exists, or exact page text is needed. """ AGENT_TOOL_POLICY = """ @@ -54,6 +56,9 @@ - Tool errors are returned as ERROR text; recover by trying an available command. - Use cat or grep to gather evidence before making source-backed claims. - Prefer target-first cat syntax with stable targets: cat --structure, cat --page 31-59, cat --node . +- After cat --structure finds a relevant section/subsection with a node_id, prefer cat --node for content from that semantic unit. +- Use cat --page - when the user explicitly asks for pages/page ranges, when no suitable node_id exists, or when you need exact page text to verify page-level evidence. +- Avoid fetching a broad page span after a matching node is available unless page-level citation or verification is required. - Do not call cat --page ; if you need a page span, use cat --page -. """ diff --git a/tests/test_pifs_agent_stream.py b/tests/test_pifs_agent_stream.py index 1b7c9d120..25de771c3 100644 --- a/tests/test_pifs_agent_stream.py +++ b/tests/test_pifs_agent_stream.py @@ -6,6 +6,8 @@ from pydantic import BaseModel, ConfigDict from pageindex.filesystem.agent import ( + AGENT_TOOL_POLICY, + BASH_TOOL_DESCRIPTION, PIFSAgentStreamObserver, build_agent_model_settings, normalize_agent_stream_mode, @@ -180,6 +182,11 @@ def test_structured_agent_output_serializes_to_json(self): self.assertEqual(output, '{"answer":"done","document_ids":["dsid_1"]}') + def test_prompt_tells_agent_when_to_choose_node_or_page(self): + self.assertIn("prefer cat --node ", AGENT_TOOL_POLICY) + self.assertIn("page-level evidence", AGENT_TOOL_POLICY) + self.assertIn("prefer\ncat --node ", BASH_TOOL_DESCRIPTION) + if __name__ == "__main__": unittest.main() From ec96812e6bec67d5653a40bb72d49038bd67d3f3 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 16:19:32 +0800 Subject: [PATCH 15/50] fix(filesystem): scope pifs chat identity --- pageindex/filesystem/agent.py | 11 ++++++++++- tests/test_pifs_agent_stream.py | 8 ++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pageindex/filesystem/agent.py b/pageindex/filesystem/agent.py index 9bddb1603..291fb9cb9 100644 --- a/pageindex/filesystem/agent.py +++ b/pageindex/filesystem/agent.py @@ -19,11 +19,20 @@ PIFS_AGENT_RAW_REASONING_ENV = "PAGEINDEX_PIFS_AGENT_RAW_REASONING" AGENT_SYSTEM_PROMPT = """ -You are a PageIndex FileSystem retrieval agent. +You are the PageIndex FileSystem Demo Agent, developed by the VectifyAI Team. +Your job is to answer questions about the caller's current PageIndex FileSystem +workspace. You can inspect the corpus only by calling the bash tool. The bash tool is a read-only PageIndex virtual shell, not a real operating-system shell. +If the user asks who you are, answer with this identity and mention that you can +help inspect and answer questions about the current PIFS workspace. If the user +asks a general question unrelated to the current workspace, do not answer it as +a general-purpose assistant; briefly say that you can only answer workspace- +related questions and invite them to ask about files, folders, metadata, or +document contents in the workspace. + Follow the task prompt for command policy, retrieval strategy, and answer format. If the caller needs stricter behavior, pass an explicit system_prompt. """ diff --git a/tests/test_pifs_agent_stream.py b/tests/test_pifs_agent_stream.py index 25de771c3..5e0787a72 100644 --- a/tests/test_pifs_agent_stream.py +++ b/tests/test_pifs_agent_stream.py @@ -7,6 +7,7 @@ from pageindex.filesystem.agent import ( AGENT_TOOL_POLICY, + AGENT_SYSTEM_PROMPT, BASH_TOOL_DESCRIPTION, PIFSAgentStreamObserver, build_agent_model_settings, @@ -187,6 +188,13 @@ def test_prompt_tells_agent_when_to_choose_node_or_page(self): self.assertIn("page-level evidence", AGENT_TOOL_POLICY) self.assertIn("prefer\ncat --node ", BASH_TOOL_DESCRIPTION) + def test_system_prompt_sets_workspace_identity_and_scope(self): + self.assertIn("PageIndex FileSystem Demo Agent", AGENT_SYSTEM_PROMPT) + self.assertIn("VectifyAI Team", AGENT_SYSTEM_PROMPT) + self.assertIn("current PageIndex FileSystem\nworkspace", AGENT_SYSTEM_PROMPT) + self.assertIn("unrelated to the current workspace", AGENT_SYSTEM_PROMPT) + self.assertIn("do not answer it as\na general-purpose assistant", AGENT_SYSTEM_PROMPT) + if __name__ == "__main__": unittest.main() From 8ae94ade19fa35fc36104d37e2324816d0fbb0d9 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 16:21:57 +0800 Subject: [PATCH 16/50] fix(filesystem): render root folder path correctly --- pageindex/filesystem/commands.py | 8 ++++++-- tests/test_pifs_find_maxdepth.py | 11 +++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/pageindex/filesystem/commands.py b/pageindex/filesystem/commands.py index d56df792a..6615da25b 100644 --- a/pageindex/filesystem/commands.py +++ b/pageindex/filesystem/commands.py @@ -817,16 +817,20 @@ def _render_find(self, data: Any) -> str: if data and isinstance(data[0], dict) and "path" in data[0] and "file_ref" not in data[0]: return "\n".join( ( - f"{item['path']}/ matched_files={item['matched_files']} " + f"{self._folder_row_path(item['path'])} matched_files={item['matched_files']} " f"files={item.get('file_count', 0)}" if item.get("matched_files") - else f"{item['path']}/ folders={item.get('children_count', 0)} " + else f"{self._folder_row_path(item['path'])} folders={item.get('children_count', 0)} " f"files={item.get('file_count', 0)}" ) for item in data ) return "\n".join(self._file_row_text(item) for item in data) + def _folder_row_path(self, path: str) -> str: + normalized = self._normalize_folder_path(path) + return "/" if normalized == "/" else f"{normalized}/" + def _render_stat(self, data: Any) -> str: if not isinstance(data, dict): return str(data) diff --git a/tests/test_pifs_find_maxdepth.py b/tests/test_pifs_find_maxdepth.py index 580633e30..8a7fe50c0 100644 --- a/tests/test_pifs_find_maxdepth.py +++ b/tests/test_pifs_find_maxdepth.py @@ -106,6 +106,17 @@ def test_find_maxdepth_zero_type_directory_returns_start_folder(tmp_path): assert [row["path"] for row in rows] == ["/documents"] +def test_find_directory_output_renders_root_without_double_slash(tmp_path): + executor = _register_find_fixture(tmp_path) + executor.json_output = False + + output = executor.execute("find / -maxdepth 1 -type d") + + assert output.splitlines()[0] == "/ folders=1 files=0" + assert "//" not in output + assert "/documents/ folders=1 files=1" in output + + def test_find_maxdepth_combines_with_where_and_limit(tmp_path): executor = _register_find_fixture(tmp_path) From 5c948d084087fb7ff2ac0abdd7b8831d8a77fcbc Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 16:25:51 +0800 Subject: [PATCH 17/50] fix(filesystem): require metadata evidence in chat --- pageindex/filesystem/agent.py | 6 ++++++ tests/test_pifs_agent_stream.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/pageindex/filesystem/agent.py b/pageindex/filesystem/agent.py index 291fb9cb9..1b342eb27 100644 --- a/pageindex/filesystem/agent.py +++ b/pageindex/filesystem/agent.py @@ -51,6 +51,8 @@ ambiguous. After structure identifies a relevant section node, prefer cat --node ; use cat --page when the user asks for page-level evidence, no suitable node exists, or exact page text is needed. +For questions about metadata fields, available summaries, or whether metadata +was provided, inspect stat --schema and stat before making claims. """ AGENT_TOOL_POLICY = """ @@ -69,6 +71,8 @@ - Use cat --page - when the user explicitly asks for pages/page ranges, when no suitable node_id exists, or when you need exact page text to verify page-level evidence. - Avoid fetching a broad page span after a matching node is available unless page-level citation or verification is required. - Do not call cat --page ; if you need a page span, use cat --page -. +- For metadata or summary-field questions, run stat --schema and stat for relevant files before answering; do not infer metadata presence or absence from ls/find output alone. +- Distinguish default/register metadata from caller-provided custom metadata when the evidence supports it. """ STREAM_MODE_ALIASES = { @@ -361,6 +365,8 @@ def _emit(self, kind: str, text: str, label: str) -> None: print(text, end="", file=self.output, flush=True) def emit_tool_call(self, command: str, *, force: bool = False) -> None: + if not command.strip(): + return if self.stream_log is not None: self.stream_log.append({"kind": "tool_call", "command": command}) if not (force or self.wants_tool_stream): diff --git a/tests/test_pifs_agent_stream.py b/tests/test_pifs_agent_stream.py index 5e0787a72..4beaf6c0b 100644 --- a/tests/test_pifs_agent_stream.py +++ b/tests/test_pifs_agent_stream.py @@ -79,6 +79,17 @@ def test_tools_mode_does_not_print_model_text(self): self.assertEqual(stream_log[1]["kind"], "tool_result") self.assertEqual(stream_log[2], {"kind": "tool_args", "text": '{"command":"ls /"}'}) + def test_empty_tool_command_is_not_printed_or_logged(self): + output = io.StringIO() + stream_log = [] + observer = PIFSAgentStreamObserver("tools", stream_log=stream_log, output=output) + + observer.emit_tool_call("") + observer.emit_tool_call(" ") + + self.assertEqual(output.getvalue(), "") + self.assertEqual(stream_log, []) + def test_tool_result_preview_compacts_large_outputs(self): output = io.StringIO() observer = PIFSAgentStreamObserver("tools", output=output) @@ -188,6 +199,11 @@ def test_prompt_tells_agent_when_to_choose_node_or_page(self): self.assertIn("page-level evidence", AGENT_TOOL_POLICY) self.assertIn("prefer\ncat --node ", BASH_TOOL_DESCRIPTION) + def test_prompt_requires_stat_for_metadata_questions(self): + self.assertIn("stat --schema and stat ", AGENT_TOOL_POLICY) + self.assertIn("do not infer metadata presence or absence", AGENT_TOOL_POLICY) + self.assertIn("questions about metadata fields", BASH_TOOL_DESCRIPTION) + def test_system_prompt_sets_workspace_identity_and_scope(self): self.assertIn("PageIndex FileSystem Demo Agent", AGENT_SYSTEM_PROMPT) self.assertIn("VectifyAI Team", AGENT_SYSTEM_PROMPT) From 7e70b580f067c4cf8324339679df3a4b1cda031a Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 16:31:29 +0800 Subject: [PATCH 18/50] fix(filesystem): show generated metadata in stat --- pageindex/filesystem/commands.py | 13 ++++++++++++ tests/test_pifs_find_maxdepth.py | 35 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/pageindex/filesystem/commands.py b/pageindex/filesystem/commands.py index 6615da25b..416acec06 100644 --- a/pageindex/filesystem/commands.py +++ b/pageindex/filesystem/commands.py @@ -858,6 +858,19 @@ def _render_stat(self, data: Any) -> str: lines.append(f" {key}: {self._compact_value(value)}") if len(metadata) > self.MAX_STAT_METADATA_FIELDS: lines.append(f" ... {len(metadata) - self.MAX_STAT_METADATA_FIELDS} more fields") + derived_metadata = data.get("derived_metadata") or {} + if derived_metadata: + lines.append("generated_metadata:") + derived_items = sorted(derived_metadata.items())[: self.MAX_STAT_METADATA_FIELDS] + for key, value in derived_items: + lines.append(f" {key}: {self._compact_value(value)}") + if len(derived_metadata) > self.MAX_STAT_METADATA_FIELDS: + lines.append( + f" ... {len(derived_metadata) - self.MAX_STAT_METADATA_FIELDS} more fields" + ) + generation = data.get("metadata_generation") or {} + if generation: + lines.append(f"metadata_generation_status: {generation.get('status', '-')}") return "\n".join(lines) def _file_row_text(self, item: dict[str, Any]) -> str: diff --git a/tests/test_pifs_find_maxdepth.py b/tests/test_pifs_find_maxdepth.py index 8a7fe50c0..304964fbf 100644 --- a/tests/test_pifs_find_maxdepth.py +++ b/tests/test_pifs_find_maxdepth.py @@ -98,6 +98,41 @@ def test_stable_path_targets_work_without_session_refs(tmp_path): assert "Root document fixture text" in text +def test_stat_shell_output_includes_generated_metadata(tmp_path): + from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem + + source = tmp_path / "source.txt" + source.write_text("fixture text", encoding="utf-8") + filesystem = PageIndexFileSystem(workspace=tmp_path / "workspace") + filesystem.register_file( + storage_uri=source.as_uri(), + source_path="docs/source.txt", + folder_path="/documents", + external_id="doc_generated", + title="Generated metadata document", + content=source.read_text(encoding="utf-8"), + metadata={"department": "ops"}, + derived_metadata={"summary": "Generated summary for retrieval."}, + metadata_generation_policy={ + "fields": { + "summary": True, + "doc_type": False, + "domain": False, + "topic": False, + } + }, + ) + executor = PIFSCommandExecutor(filesystem, json_output=False) + + stat = executor.execute("stat /documents/'Generated metadata document'") + + assert "metadata:" in stat + assert " department: ops" in stat + assert "generated_metadata:" in stat + assert " summary: Generated summary for retrieval." in stat + assert "metadata_generation_status: generated" in stat + + def test_find_maxdepth_zero_type_directory_returns_start_folder(tmp_path): executor = _register_find_fixture(tmp_path) From fc0be1aeeea41b51659c39697862f3f6ffd81c68 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 16:40:14 +0800 Subject: [PATCH 19/50] fix(filesystem): restore summary vector search in cli --- pageindex/filesystem/agent.py | 7 ++- pageindex/filesystem/cli.py | 5 +- pageindex/filesystem/core.py | 58 ++++++++++++++++++++++++ tests/test_pageindex_filesystem_scope.py | 57 +++++++++++++++++++++++ tests/test_pifs_agent_stream.py | 5 ++ tests/test_pifs_cli.py | 18 ++++++++ 6 files changed, 147 insertions(+), 3 deletions(-) diff --git a/pageindex/filesystem/agent.py b/pageindex/filesystem/agent.py index 1b342eb27..ba8645df1 100644 --- a/pageindex/filesystem/agent.py +++ b/pageindex/filesystem/agent.py @@ -42,8 +42,10 @@ operating-system shell. By default the tool is read-only: use ls, tree, find, grep, cat, stat, head, tail, sed, and any dynamically available semantic search commands described in the workspace context. grep -R is lexical evidence search; -semantic search commands return candidate documents and do not guarantee literal -text matches. Errors are returned as text prefixed with ERROR. Do not call +semantic search commands such as search-summary return candidate documents and +do not guarantee literal text matches. Use search-summary when the user asks for +summary search, semantic search, or vector search and the command is listed as +available. Errors are returned as text prefixed with ERROR. Do not call commands that are not listed as available. When evidence is required, inspect it with cat or grep before answering. Prefer shell-like target-first cat syntax with stable targets: cat --structure, cat --page 31-59, and @@ -64,6 +66,7 @@ - Use --where only with metadata fields shown by stat --schema. - grep -R performs lexical evidence search. - Semantic search commands are candidate-discovery tools and do not guarantee literal text matches. +- If search-summary is available and the user asks for summary search, semantic search, vector search, or "用 summary 搜", use search-summary ; do not translate that request into find --where. - Tool errors are returned as ERROR text; recover by trying an available command. - Use cat or grep to gather evidence before making source-backed claims. - Prefer target-first cat syntax with stable targets: cat --structure, cat --page 31-59, cat --node . diff --git a/pageindex/filesystem/cli.py b/pageindex/filesystem/cli.py index aa443a52e..24a78f4de 100644 --- a/pageindex/filesystem/cli.py +++ b/pageindex/filesystem/cli.py @@ -122,7 +122,10 @@ def _parse_agent_command( def _filesystem_from_workspace(workspace: str) -> PageIndexFileSystem: - return PageIndexFileSystem(Path(workspace).expanduser()) + filesystem = PageIndexFileSystem(Path(workspace).expanduser()) + with contextlib.suppress(Exception): + filesystem.configure_existing_projection_retrieval() + return filesystem def _agent_kwargs(args: argparse.Namespace) -> dict[str, object]: diff --git a/pageindex/filesystem/core.py b/pageindex/filesystem/core.py index 9a0f88b3d..35af51300 100644 --- a/pageindex/filesystem/core.py +++ b/pageindex/filesystem/core.py @@ -218,6 +218,64 @@ def _ensure_register_completion_defaults(self) -> None: embedding_timeout=self.summary_projection_embedding_timeout, ) + def configure_existing_projection_retrieval(self) -> bool: + """Attach semantic retrieval to already-built projection indexes. + + Register-time generation owns building the index files. Opening an + existing workspace should still expose the corresponding read commands, + such as search-summary, without forcing a re-register step. + """ + if self.semantic_retrieval_backend is not None: + return bool(self.semantic_retrieval_channels()) + index_config = self._existing_projection_index_config() + if index_config is None: + return False + metadata = dict(index_config.get("metadata") or {}) + embedding_provider = str( + metadata.get("embedding_provider") + or self.summary_projection_embedding_provider + ) + embedding_model = str( + metadata.get("embedding_model") + or self.summary_projection_embedding_model + ) + embedding_dimensions = int( + metadata.get("embedding_dimensions") + or index_config.get("dimension") + or self.summary_projection_embedding_dimensions + ) + self.configure_hybrid_projection_retrieval( + self.summary_projection_index_dir, + embedding_provider=embedding_provider, + embedding_model=embedding_model, + embedding_dimensions=embedding_dimensions, + embedding_timeout=self.summary_projection_embedding_timeout, + ) + return bool(self.semantic_retrieval_channels()) + + def _existing_projection_index_config(self) -> dict[str, Any] | None: + from .hybrid_projection import INDEX_BY_CHANNEL + from .semantic_index import SQLiteVecSemanticIndex + + for channel in SEMANTIC_RETRIEVAL_CHANNELS: + index_name = INDEX_BY_CHANNEL.get(channel) + if not index_name: + continue + index_path = self.summary_projection_index_dir / f"{index_name}.sqlite" + if not index_path.exists(): + continue + try: + info = SQLiteVecSemanticIndex(index_path).info() + except Exception: + continue + if int(info.get("document_count") or 0) <= 0: + continue + metadata = dict(info.get("metadata") or {}) + if metadata.get("channel") and metadata.get("channel") != channel: + continue + return info + return None + @staticmethod def _register_uses_deferred_metadata(policy: Any) -> bool: if not isinstance(policy, dict): diff --git a/tests/test_pageindex_filesystem_scope.py b/tests/test_pageindex_filesystem_scope.py index e08dabd97..b421714ab 100644 --- a/tests/test_pageindex_filesystem_scope.py +++ b/tests/test_pageindex_filesystem_scope.py @@ -58,3 +58,60 @@ def test_semantic_search_scope_filters_explicit_source_type_facets(): {"folder_path": "/documents"} ) == {} + +def test_existing_summary_projection_index_configures_retrieval_backend(tmp_path, monkeypatch): + from pageindex.filesystem import PageIndexFileSystem + from pageindex.filesystem.semantic_index import SemanticIndexRecord, SQLiteVecSemanticIndex + + workspace = tmp_path / "workspace" + index_dir = workspace / "artifacts" / "projection_indexes" + summary_index = SQLiteVecSemanticIndex(index_dir / "summary_only_vector.sqlite") + summary_index.reset( + dimension=3, + metadata={ + "channel": "summary", + "embedding_provider": "openai", + "embedding_model": "test-embedding", + "embedding_dimensions": 3, + }, + ) + summary_index.upsert_many( + [ + SemanticIndexRecord( + file_ref="file_a", + external_id="doc_a", + source_type="documents", + source_path="documents/a.pdf", + title="A", + text="summary", + vector=[1.0, 0.0, 0.0], + ) + ] + ) + filesystem = PageIndexFileSystem(workspace) + calls = [] + + def fake_configure(index_dir_arg, **kwargs): + calls.append((index_dir_arg, kwargs)) + filesystem.semantic_retrieval_backend = SummaryBackend("doc_a") + return filesystem.semantic_retrieval_backend + + monkeypatch.setattr( + filesystem, + "configure_hybrid_projection_retrieval", + fake_configure, + ) + + assert filesystem.configure_existing_projection_retrieval() is True + assert calls == [ + ( + filesystem.summary_projection_index_dir, + { + "embedding_provider": "openai", + "embedding_model": "test-embedding", + "embedding_dimensions": 3, + "embedding_timeout": 60, + }, + ) + ] + assert filesystem.semantic_retrieval_channels() == ("summary",) diff --git a/tests/test_pifs_agent_stream.py b/tests/test_pifs_agent_stream.py index 4beaf6c0b..e16085023 100644 --- a/tests/test_pifs_agent_stream.py +++ b/tests/test_pifs_agent_stream.py @@ -204,6 +204,11 @@ def test_prompt_requires_stat_for_metadata_questions(self): self.assertIn("do not infer metadata presence or absence", AGENT_TOOL_POLICY) self.assertIn("questions about metadata fields", BASH_TOOL_DESCRIPTION) + def test_prompt_routes_summary_search_to_search_summary(self): + self.assertIn("search-summary when the user asks for", BASH_TOOL_DESCRIPTION) + self.assertIn("use search-summary ", AGENT_TOOL_POLICY) + self.assertIn("do not translate that request into find --where", AGENT_TOOL_POLICY) + def test_system_prompt_sets_workspace_identity_and_scope(self): self.assertIn("PageIndex FileSystem Demo Agent", AGENT_SYSTEM_PROMPT) self.assertIn("VectifyAI Team", AGENT_SYSTEM_PROMPT) diff --git a/tests/test_pifs_cli.py b/tests/test_pifs_cli.py index 04717c420..74832b859 100644 --- a/tests/test_pifs_cli.py +++ b/tests/test_pifs_cli.py @@ -5,6 +5,24 @@ class FakeFileSystem: def __init__(self, workspace): self.workspace = Path(workspace) + self.projection_retrieval_configured = False + + def configure_existing_projection_retrieval(self): + self.projection_retrieval_configured = True + return True + + +def test_cli_workspace_configures_existing_projection_retrieval(monkeypatch, tmp_path): + from pageindex.filesystem import cli + + workspace = tmp_path / "workspace" + + monkeypatch.setattr(cli, "PageIndexFileSystem", FakeFileSystem) + + filesystem = cli._filesystem_from_workspace(str(workspace)) + + assert filesystem.workspace == workspace + assert filesystem.projection_retrieval_configured is True def test_cli_passthrough_invokes_pifs_command_executor(monkeypatch, capsys, tmp_path): From 7c021a7dd0c62a1b2d7f7908aebfdf7aff2e7d1e Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 17:12:59 +0800 Subject: [PATCH 20/50] refactor(filesystem): unify pifs metadata state --- examples/pifs_demo.py | 17 +- pageindex/filesystem/commands.py | 19 +- pageindex/filesystem/core.py | 325 ++++++++++---------- pageindex/filesystem/projection_indexing.py | 3 +- pageindex/filesystem/store.py | 258 ++++++++-------- pageindex/filesystem/types.py | 6 +- tests/test_pifs_find_maxdepth.py | 139 ++++++++- tests/test_semantic_index.py | 36 +++ 8 files changed, 479 insertions(+), 324 deletions(-) diff --git a/examples/pifs_demo.py b/examples/pifs_demo.py index 230d58673..f6f9b511d 100644 --- a/examples/pifs_demo.py +++ b/examples/pifs_demo.py @@ -287,7 +287,6 @@ def register_demo_metadata_schema(filesystem: PageIndexFileSystem) -> None: def backfill_registered_metadata_values(filesystem: PageIndexFileSystem, file_ref: str) -> None: entry = filesystem.store.get_file(file_ref) indexed_metadata = dict(entry.metadata or {}) - indexed_metadata.update(entry.derived_metadata or {}) with filesystem.store.connect() as conn: filesystem.store.replace_metadata_values(conn, file_ref, indexed_metadata) @@ -314,12 +313,12 @@ def has_ready_register_outputs(filesystem: PageIndexFileSystem, external_id: str entry = filesystem.store.get_file(file_ref) except KeyError: return False - generation = entry.metadata_generation or {} - fields = generation.get("fields") or {} + status = entry.metadata_status or {} + fields = status.get("fields") or {} required = ("summary", "doc_type", "domain", "topic") if any(fields.get(field, {}).get("status") != "generated" for field in required): return False - summary_projection = (generation.get("projection_indexes") or {}).get("summary") or {} + summary_projection = (status.get("projection_indexes") or {}).get("summary") or {} return summary_projection.get("status") == "ready" @@ -394,17 +393,17 @@ def register_documents( entry = filesystem.store.get_file(file_ref) field_status = { field: state.get("status") - for field, state in (entry.metadata_generation.get("fields") or {}).items() + for field, state in (entry.metadata_status.get("fields") or {}).items() } summary_projection = ( - entry.metadata_generation.get("projection_indexes", {}).get("summary", {}) + entry.metadata_status.get("projection_indexes", {}).get("summary", {}) ) log_progress( f"PIFS register: done file_ref={file_ref} ({register_seconds:.2f}s)", indent=1, ) log_progress( - f"metadata: {entry.metadata_generation.get('status', 'unknown')} fields={field_status}", + f"metadata: {entry.metadata_status.get('status', 'unknown')} fields={field_status}", indent=1, ) log_progress( @@ -418,7 +417,7 @@ def register_documents( "file_ref": file_ref, "external_id": external_id, "path": str(document_path), - "status": entry.metadata_generation.get("status", "unknown"), + "status": entry.metadata_status.get("status", "unknown"), "pageindex_tree_status": entry.pageindex_tree_status, "pageindex_doc_id": entry.pageindex_doc_id, } @@ -642,7 +641,7 @@ def run_smoke_commands( command=command, result=( f"{stat_data.get('title')} | tree={stat_data.get('pageindex_tree_status')} | " - f"metadata_status={(stat_data.get('metadata_generation') or {}).get('status')}" + f"metadata_status={(stat_data.get('metadata_status') or {}).get('status')}" ), raw=shell_executor.execute(command) if verbose else "", verbose=verbose, diff --git a/pageindex/filesystem/commands.py b/pageindex/filesystem/commands.py index 416acec06..fc2f47c0a 100644 --- a/pageindex/filesystem/commands.py +++ b/pageindex/filesystem/commands.py @@ -858,19 +858,16 @@ def _render_stat(self, data: Any) -> str: lines.append(f" {key}: {self._compact_value(value)}") if len(metadata) > self.MAX_STAT_METADATA_FIELDS: lines.append(f" ... {len(metadata) - self.MAX_STAT_METADATA_FIELDS} more fields") - derived_metadata = data.get("derived_metadata") or {} - if derived_metadata: - lines.append("generated_metadata:") - derived_items = sorted(derived_metadata.items())[: self.MAX_STAT_METADATA_FIELDS] - for key, value in derived_items: - lines.append(f" {key}: {self._compact_value(value)}") - if len(derived_metadata) > self.MAX_STAT_METADATA_FIELDS: + metadata_status = data.get("metadata_status") or {} + if metadata_status: + lines.append(f"metadata_status: {metadata_status.get('status', '-')}") + summary_projection = ( + metadata_status.get("projection_indexes", {}).get("summary", {}) + ) + if summary_projection: lines.append( - f" ... {len(derived_metadata) - self.MAX_STAT_METADATA_FIELDS} more fields" + f"summary_projection_status: {summary_projection.get('status', '-')}" ) - generation = data.get("metadata_generation") or {} - if generation: - lines.append(f"metadata_generation_status: {generation.get('status', '-')}") return "\n".join(lines) def _file_row_text(self, item: dict[str, Any]) -> str: diff --git a/pageindex/filesystem/core.py b/pageindex/filesystem/core.py index 35af51300..bbf81f19f 100644 --- a/pageindex/filesystem/core.py +++ b/pageindex/filesystem/core.py @@ -47,7 +47,7 @@ "relation": False, } -DEFAULT_DERIVED_METADATA_FIELD_TYPES = { +DEFAULT_METADATA_FIELD_TYPES = { "summary": "string", "doc_type": "string", "domain": "string", @@ -56,7 +56,8 @@ "relation": "string", } -METADATA_GENERATION_STATUSES = { +METADATA_STATUSES = { + "skipped", "pending_submit", "pending_generate", "generated", @@ -128,9 +129,8 @@ def register_file( content: str = "", content_type: str = "text/plain", source_type: Optional[str] = None, - derived_metadata: Optional[dict[str, Any]] = None, - metadata_generation_policy: Optional[dict[str, Any]] = None, - metadata_generation_status: Optional[str] = None, + metadata_policy: Optional[dict[str, Any]] = None, + metadata_status: Optional[str] = None, ) -> str: return self.register_files( [ @@ -144,15 +144,14 @@ def register_file( "content": content, "content_type": content_type, "source_type": source_type, - "derived_metadata": derived_metadata, - "metadata_generation_policy": metadata_generation_policy, - "metadata_generation_status": metadata_generation_status, + "metadata_policy": metadata_policy, + "metadata_status": metadata_status, } ] )[0] def register(self, **kwargs: Any) -> str: - if not self._register_uses_deferred_metadata(kwargs.get("metadata_generation_policy")): + if not self._register_uses_deferred_metadata(kwargs.get("metadata_policy")): self._ensure_register_completion_defaults() return self.register_file(**kwargs) @@ -171,7 +170,7 @@ def batch_generate(self, *, limit: int | None = None) -> dict[str, Any]: raise MetadataGenerationError( "metadata_generator is required to generate pending PIFS metadata" ) - rows = self.store.list_pending_metadata_generation(limit=limit) + rows = self.store.list_pending_metadata_status(limit=limit) generated = 0 failed = 0 file_refs: list[str] = [] @@ -180,14 +179,14 @@ def batch_generate(self, *, limit: int | None = None) -> dict[str, Any]: self._generate_register_metadata(record, force=True) self._complete_summary_projection_index(record) self._register_generation_policy_schema([record]) - self.store.update_file_metadata_generation( + self.store.update_file_metadata_status( record["file_ref"], - derived_metadata=record["derived_metadata"], - metadata_generation=record["metadata_generation"], + metadata=record["metadata"], + metadata_status=record["metadata_status"], ) self._sync_owned_raw_artifact(record) file_refs.append(record["file_ref"]) - if record["metadata_generation"]["status"] == "failed": + if record["metadata_status"]["status"] == "failed": failed += 1 else: generated += 1 @@ -283,7 +282,7 @@ def _register_uses_deferred_metadata(policy: Any) -> bool: return bool(policy.get("batch")) or policy.get("mode") == "batch" @classmethod - def default_metadata_generation_policy(cls) -> dict[str, Any]: + def default_metadata_policy(cls) -> dict[str, Any]: return { "fields": dict(DEFAULT_METADATA_GENERATION_FIELDS), "projection_indexes": {"summary": True}, @@ -459,8 +458,7 @@ def search( folder_path=folder_path, folder_paths=folder_paths, metadata=row["metadata"], - derived_metadata=row["derived_metadata"], - metadata_generation=row["metadata_generation"], + metadata_status=row["metadata_status"], source_path=row["source_path"], id=row["id"], document_id=row["document_id"], @@ -885,11 +883,18 @@ def _prepare_file_record(self, file: dict[str, Any]) -> dict[str, Any]: raw_source_path = str(file["source_path"]) source_path = raw_source_path.strip("/") metadata = file.get("metadata") or {} - derived_metadata = file.get("derived_metadata") or {} if not isinstance(metadata, dict): raise ValueError("metadata must be a JSON object") - if not isinstance(derived_metadata, dict): - raise ValueError("derived_metadata must be a JSON object") + legacy_value_key = "derived_" + "metadata" + legacy_policy_key = "metadata_" + "generation_policy" + legacy_status_key = "metadata_" + "generation_status" + if legacy_value_key in file: + raise ValueError("legacy generated metadata map has been removed; put values in metadata") + if legacy_policy_key in file: + raise ValueError("legacy metadata policy key has been renamed to metadata_policy") + if legacy_status_key in file: + raise ValueError("legacy metadata status key has been renamed to metadata_status") + self._validate_register_metadata(metadata) external_id = file.get("external_id") content = file.get("content") or "" content_type = file.get("content_type") or "text/plain" @@ -907,21 +912,17 @@ def _prepare_file_record(self, file: dict[str, Any]) -> dict[str, Any]: ) fts_content = file.get("fts_content", artifact_content) source_type = file.get("source_type") or self._infer_source_type(source_path) - generation_policy = self._normalize_metadata_generation_policy( - file.get("metadata_generation_policy"), - derived_metadata=derived_metadata, - ) - generation_state = self._metadata_generation_state( - generation_policy, - derived_metadata=derived_metadata, - status=file.get("metadata_generation_status"), + metadata_policy = self._normalize_metadata_policy( + file.get("metadata_policy"), + metadata=metadata, ) - indexed_metadata = SQLiteFileSystemStore.indexed_metadata_values( - metadata, - derived_metadata, - generation_state, + metadata_status = self._metadata_status_state( + metadata_policy, + metadata=metadata, + status=file.get("metadata_status"), ) - searchable_metadata = self._merge_metadata_values(metadata, derived_metadata) + indexed_metadata = SQLiteFileSystemStore.indexed_metadata_values(metadata) + searchable_metadata = dict(metadata) folder_path = normalize_path(file.get("folder_path") or "/") title = file.get("title") or metadata.get("title") or Path(source_path).stem file_ref = make_file_ref(external_id or source_path) @@ -938,8 +939,7 @@ def _prepare_file_record(self, file: dict[str, Any]) -> dict[str, Any]: source_path=source_path, folder_path=folder_path, metadata=metadata, - derived_metadata=derived_metadata, - metadata_generation=generation_state, + metadata_status=metadata_status, ), ) descriptor = self._build_descriptor(title, metadata) @@ -959,10 +959,8 @@ def _prepare_file_record(self, file: dict[str, Any]) -> dict[str, Any]: "pageindex_tree_status": pageindex_tree_status, "metadata": metadata, "metadata_json": json.dumps(metadata, ensure_ascii=False), - "derived_metadata": derived_metadata, - "derived_metadata_json": json.dumps(derived_metadata, ensure_ascii=False), - "metadata_generation": generation_state, - "metadata_generation_json": json.dumps(generation_state, ensure_ascii=False), + "metadata_status": metadata_status, + "metadata_status_json": json.dumps(metadata_status, ensure_ascii=False), "indexed_metadata": indexed_metadata, "metadata_text": metadata_text(searchable_metadata), "folder_path": folder_path, @@ -1035,16 +1033,14 @@ def _raw_artifact_payload( source_path: str, folder_path: str, metadata: dict[str, Any], - derived_metadata: dict[str, Any], - metadata_generation: dict[str, Any], + metadata_status: dict[str, Any], ) -> dict[str, Any]: return { "storage_uri": storage_uri, "source_path": source_path, "folder_path": folder_path, "metadata": metadata, - "derived_metadata": derived_metadata, - "metadata_generation": metadata_generation, + "metadata_status": metadata_status, } def _sync_owned_raw_artifact(self, record: dict[str, Any]) -> None: @@ -1064,22 +1060,21 @@ def _sync_owned_raw_artifact(self, record: dict[str, Any]) -> None: source_path=record["source_path"], folder_path=record["folder_path"], metadata=record["metadata"], - derived_metadata=record["derived_metadata"], - metadata_generation=record["metadata_generation"], + metadata_status=record["metadata_status"], ), ) ) def _record_from_file_entry(self, entry: Any) -> dict[str, Any]: content = self.store.read_text(entry.file_ref) - generation_policy = self._normalize_metadata_generation_policy( - entry.metadata_generation.get("policy", {}), - derived_metadata=entry.derived_metadata, + metadata_policy = self._normalize_metadata_policy( + entry.metadata_status.get("policy", {}), + metadata=entry.metadata, ) - generation_state = self._metadata_generation_state( - generation_policy, - derived_metadata=entry.derived_metadata, - status=entry.metadata_generation.get("status"), + metadata_status = self._metadata_status_state( + metadata_policy, + metadata=entry.metadata, + status=entry.metadata_status.get("status"), ) return { "file_ref": entry.file_ref, @@ -1097,35 +1092,29 @@ def _record_from_file_entry(self, entry: Any) -> dict[str, Any]: "pageindex_tree_status": entry.pageindex_tree_status, "metadata": dict(entry.metadata), "metadata_json": json.dumps(entry.metadata, ensure_ascii=False), - "derived_metadata": dict(entry.derived_metadata), - "derived_metadata_json": json.dumps(entry.derived_metadata, ensure_ascii=False), - "metadata_generation": generation_state, - "metadata_generation_json": json.dumps(generation_state, ensure_ascii=False), - "indexed_metadata": SQLiteFileSystemStore.indexed_metadata_values( - entry.metadata, - entry.derived_metadata, - generation_state, - ), - "metadata_text": metadata_text(self._merge_metadata_values(entry.metadata, entry.derived_metadata)), + "metadata_status": metadata_status, + "metadata_status_json": json.dumps(metadata_status, ensure_ascii=False), + "indexed_metadata": SQLiteFileSystemStore.indexed_metadata_values(entry.metadata), + "metadata_text": metadata_text(entry.metadata), "folder_path": entry.folder_path, "content": content, "skip_fts": False, } def _generate_register_metadata(self, record: dict[str, Any], *, force: bool = False) -> None: - generation = record["metadata_generation"] - policy = generation.get("policy", {}) - if self._metadata_generation_is_batch(policy) and not force: + status = record["metadata_status"] + policy = status.get("policy", {}) + if self._metadata_policy_is_batch(policy) and not force: self._mark_requested_generation_status(record, "pending_submit") return fields = self._metadata_fields_to_generate(record) if not fields: return if self.metadata_generator is None: - if self._metadata_generation_requires_sync(policy): + if self._metadata_policy_requires_sync(policy): raise MetadataGenerationError( "metadata_generator is required for synchronous PIFS metadata generation; " - "set metadata_generation_policy batch=true to defer" + "set metadata_policy batch=true to defer" ) return try: @@ -1146,112 +1135,117 @@ def _generate_register_metadata(self, record: dict[str, Any], *, force: bool = F if isinstance(result, dict): result = MetadataGenerationResult(values=result) except Exception as exc: - self._apply_metadata_generation_failures(record, fields, str(exc)) + self._apply_metadata_status_failures(record, fields, str(exc)) return failures = dict(result.failures) for field in fields: if field in result.values: - record["derived_metadata"][field] = result.values[field] - generation["fields"][field] = {"requested": True, "status": "generated"} + record["metadata"][field] = result.values[field] + status["fields"][field] = { + "requested": True, + "status": "generated", + "owner": "pifs", + "source": "llm", + } else: failures.setdefault(field, "metadata generator did not return field") for field, reason in failures.items(): - generation["fields"][field] = { + status["fields"][field] = { "requested": True, "status": "failed", + "owner": "pifs", + "source": "llm", "error": str(reason), } - self._refresh_record_metadata_generation(record) + self._refresh_record_metadata_status(record) def _complete_summary_projection_index(self, record: dict[str, Any]) -> None: - generation = record["metadata_generation"] - summary_index = generation.get("projection_indexes", {}).get("summary") + metadata_status = record["metadata_status"] + summary_index = metadata_status.get("projection_indexes", {}).get("summary") if not summary_index or not summary_index.get("requested"): return - summary = str(record.get("derived_metadata", {}).get("summary") or "").strip() + summary = str(record.get("metadata", {}).get("summary") or "").strip() if not summary: return if self.summary_projection_indexer is None: - self._refresh_record_metadata_generation(record) + self._refresh_record_metadata_status(record) return try: result = self.summary_projection_indexer.upsert_summary(record) except Exception as exc: summary_index["status"] = "failed" summary_index["error"] = str(exc) - self._refresh_record_metadata_generation(record) + self._refresh_record_metadata_status(record) return summary_index.clear() summary_index.update({"requested": True, **result}) if summary_index.get("status") != "ready": summary_index["status"] = "ready" - self._refresh_record_metadata_generation(record) + self._refresh_record_metadata_status(record) @staticmethod - def _metadata_generation_is_batch(policy: dict[str, Any]) -> bool: + def _metadata_policy_is_batch(policy: dict[str, Any]) -> bool: return bool(policy.get("batch")) or policy.get("mode") == "batch" @staticmethod - def _metadata_generation_requires_sync(policy: dict[str, Any]) -> bool: + def _metadata_policy_requires_sync(policy: dict[str, Any]) -> bool: return policy.get("batch") is False or policy.get("mode") == "sync" def _metadata_fields_to_generate(self, record: dict[str, Any]) -> list[str]: fields: list[str] = [] - for name, state in record["metadata_generation"].get("fields", {}).items(): + for name, state in record["metadata_status"].get("fields", {}).items(): if not state.get("requested"): continue - if state.get("status") == "generated" and name in record["derived_metadata"]: + if state.get("status") == "generated" and name in record["metadata"]: continue fields.append(name) return fields def _mark_requested_generation_status(self, record: dict[str, Any], status: str) -> None: - for name, field in record["metadata_generation"].get("fields", {}).items(): + for name, field in record["metadata_status"].get("fields", {}).items(): if field.get("requested") and field.get("status") != "generated": - record["metadata_generation"]["fields"][name] = { + record["metadata_status"]["fields"][name] = { "requested": True, "status": status, + "owner": "pifs", + "source": "llm", } - self._refresh_record_metadata_generation(record, explicit_status=status) + self._refresh_record_metadata_status(record, explicit_status=status) - def _apply_metadata_generation_failures( + def _apply_metadata_status_failures( self, record: dict[str, Any], fields: list[str], reason: str, ) -> None: for field in fields: - record["metadata_generation"]["fields"][field] = { + record["metadata_status"]["fields"][field] = { "requested": True, "status": "failed", + "owner": "pifs", + "source": "llm", "error": reason, } - self._refresh_record_metadata_generation(record, explicit_status="failed") + self._refresh_record_metadata_status(record, explicit_status="failed") - def _refresh_record_metadata_generation( + def _refresh_record_metadata_status( self, record: dict[str, Any], *, explicit_status: str | None = None, ) -> None: - generation = record["metadata_generation"] + metadata_status = record["metadata_status"] statuses = [ field.get("status") - for field in generation.get("fields", {}).values() + for field in metadata_status.get("fields", {}).values() if field.get("requested") and field.get("status") ] - generation["status"] = explicit_status or self._aggregate_generation_status(statuses) - self._refresh_projection_index_statuses(generation, record["derived_metadata"]) - record["derived_metadata_json"] = json.dumps(record["derived_metadata"], ensure_ascii=False) - record["metadata_generation_json"] = json.dumps(generation, ensure_ascii=False) - record["indexed_metadata"] = SQLiteFileSystemStore.indexed_metadata_values( - record["metadata"], - record["derived_metadata"], - generation, - ) - record["metadata_text"] = metadata_text( - self._merge_metadata_values(record["metadata"], record["derived_metadata"]) - ) + metadata_status["status"] = explicit_status or self._aggregate_metadata_status(statuses) + self._refresh_projection_index_statuses(metadata_status, record["metadata"]) + record["metadata_json"] = json.dumps(record["metadata"], ensure_ascii=False) + record["metadata_status_json"] = json.dumps(metadata_status, ensure_ascii=False) + record["indexed_metadata"] = SQLiteFileSystemStore.indexed_metadata_values(record["metadata"]) + record["metadata_text"] = metadata_text(record["metadata"]) def _open_lines(self, file_ref: str, start: int, end: int) -> OpenResult: entry = self.store.get_file(file_ref) @@ -1381,8 +1375,7 @@ def _semantic_search( folder_path=folder_path, folder_paths=folder_paths, metadata=entry.metadata, - derived_metadata=entry.derived_metadata, - metadata_generation=entry.metadata_generation, + metadata_status=entry.metadata_status, source_path=entry.source_path, id=entry.external_id or file_ref, document_id=entry.external_id, @@ -1403,31 +1396,49 @@ def _build_descriptor(title: str, metadata: dict[str, Any]) -> str: source = metadata.get("source_type") or metadata.get("repo") or metadata.get("channel") return f"{title} ({source})" if source else title + @staticmethod + def _validate_register_metadata(metadata: dict[str, Any]) -> None: + pifs_owned_fields = set(DEFAULT_METADATA_GENERATION_FIELDS) + conflicts = sorted(pifs_owned_fields.intersection(metadata)) + if conflicts: + raise ValueError( + "metadata contains PIFS-owned generated field(s): " + + ", ".join(conflicts) + + "; configure metadata_policy instead of passing generated fields" + ) + def _register_generation_policy_schema(self, records: list[dict[str, Any]]) -> None: - fields: dict[str, dict[str, str]] = {} + pifs_fields: dict[str, dict[str, str]] = {} + user_fields: dict[str, dict[str, str]] = {} for record in records: - policy_fields = record["metadata_generation"]["policy"]["fields"] + policy_fields = record["metadata_status"]["policy"]["fields"] + generated_names = {str(name) for name, requested in policy_fields.items() if requested} for name, requested in policy_fields.items(): if requested: - fields[name] = { - "type": DEFAULT_DERIVED_METADATA_FIELD_TYPES.get( + pifs_fields[name] = { + "type": DEFAULT_METADATA_FIELD_TYPES.get( name, self._infer_metadata_field_type( - record.get("derived_metadata", {}).get(name) + record.get("metadata", {}).get(name) ), ) } - for name, value in record.get("derived_metadata", {}).items(): - fields.setdefault(name, {"type": self._infer_metadata_field_type(value)}) - if fields: - self.metadata.register_schema({"fields": fields}, source="derived") + for name, value in record.get("metadata", {}).items(): + if name in generated_names: + pifs_fields.setdefault(name, {"type": self._infer_metadata_field_type(value)}) + else: + user_fields.setdefault(name, {"type": self._infer_metadata_field_type(value)}) + if pifs_fields: + self.metadata.register_schema({"fields": pifs_fields}, source="pifs") + if user_fields: + self.metadata.register_schema({"fields": user_fields}, source="user") @classmethod - def _normalize_metadata_generation_policy( + def _normalize_metadata_policy( cls, policy: Optional[dict[str, Any]], *, - derived_metadata: dict[str, Any], + metadata: dict[str, Any], ) -> dict[str, Any]: fields = dict(DEFAULT_METADATA_GENERATION_FIELDS) field_statuses: dict[str, str] = {} @@ -1438,7 +1449,7 @@ def _normalize_metadata_generation_policy( top_level_status = None if policy is not None: if not isinstance(policy, dict): - raise ValueError("metadata_generation_policy must be a JSON object") + raise ValueError("metadata_policy must be a JSON object") raw_fields = policy.get("fields") if raw_fields is None: raw_fields = { @@ -1447,7 +1458,7 @@ def _normalize_metadata_generation_policy( if name not in {"batch", "mode", "status", "projection_indexes"} } if not isinstance(raw_fields, dict): - raise ValueError("metadata_generation_policy fields must be a JSON object") + raise ValueError("metadata_policy fields must be a JSON object") for name, declaration in raw_fields.items(): name = str(name) if isinstance(declaration, bool): @@ -1459,7 +1470,7 @@ def _normalize_metadata_generation_policy( ) field_status = declaration.get("status") if field_status is not None: - cls._validate_metadata_generation_status(str(field_status)) + cls._validate_metadata_status(str(field_status)) field_statuses[name] = str(field_status) continue raise ValueError(f"Invalid metadata generation policy for field: {name}") @@ -1470,13 +1481,11 @@ def _normalize_metadata_generation_policy( batch = True top_level_status = policy.get("status") if top_level_status is not None: - cls._validate_metadata_generation_status(str(top_level_status)) + cls._validate_metadata_status(str(top_level_status)) if "projection_indexes" in policy: projection_indexes, projection_index_statuses = ( cls._normalize_projection_index_policy(policy["projection_indexes"]) ) - for name in derived_metadata: - fields.setdefault(name, True) normalized: dict[str, Any] = { "fields": fields, "projection_indexes": ( @@ -1498,36 +1507,46 @@ def _normalize_metadata_generation_policy( return normalized @classmethod - def _metadata_generation_state( + def _metadata_status_state( cls, policy: dict[str, Any], *, - derived_metadata: dict[str, Any], + metadata: dict[str, Any], status: Optional[str], ) -> dict[str, Any]: explicit_status = status or policy.get("status") if explicit_status is not None: explicit_status = str(explicit_status) - cls._validate_metadata_generation_status(explicit_status) + cls._validate_metadata_status(explicit_status) field_statuses = policy.get("field_statuses", {}) fields: dict[str, dict[str, Any]] = {} for name, requested in policy["fields"].items(): if not requested: - fields[name] = {"requested": False} + fields[name] = { + "requested": False, + "status": "skipped", + "owner": "pifs", + "source": "llm", + } continue field_status = field_statuses.get(name) if field_status is None: field_status = explicit_status if field_status is None: - field_status = "generated" if name in derived_metadata else "pending_generate" - fields[name] = {"requested": True, "status": field_status} + field_status = "generated" if name in metadata else "pending_generate" + fields[name] = { + "requested": True, + "status": field_status, + "owner": "pifs", + "source": "llm", + } requested_statuses = [ item["status"] for item in fields.values() if item.get("requested") and item.get("status") ] - aggregate_status = explicit_status or cls._aggregate_generation_status(requested_statuses) + aggregate_status = explicit_status or cls._aggregate_metadata_status(requested_statuses) policy_summary = { "fields": dict(policy["fields"]), "projection_indexes": dict(policy.get("projection_indexes", {})), @@ -1549,12 +1568,14 @@ def _metadata_generation_state( state["projection_indexes"][name] = { "requested": True, "status": projection_statuses.get(name, "not_indexed"), + "owner": "pifs", + "source": "index", } - cls._refresh_projection_index_statuses(state, derived_metadata) + cls._refresh_projection_index_statuses(state, metadata) return state @staticmethod - def _aggregate_generation_status(statuses: list[str]) -> str: + def _aggregate_metadata_status(statuses: list[str]) -> str: if not statuses: return "generated" for status in ("failed", "pending_submit", "pending_generate"): @@ -1563,9 +1584,9 @@ def _aggregate_generation_status(statuses: list[str]) -> str: return "generated" @staticmethod - def _validate_metadata_generation_status(status: str) -> None: - if status not in METADATA_GENERATION_STATUSES: - raise ValueError(f"Unsupported metadata generation status: {status}") + def _validate_metadata_status(status: str) -> None: + if status not in METADATA_STATUSES: + raise ValueError(f"Unsupported metadata status: {status}") @classmethod def _normalize_projection_index_policy( @@ -1575,7 +1596,7 @@ def _normalize_projection_index_policy( if projection_policy is None: return {}, {} if not isinstance(projection_policy, dict): - raise ValueError("metadata_generation_policy projection_indexes must be a JSON object") + raise ValueError("metadata_policy projection_indexes must be a JSON object") projection_indexes: dict[str, bool] = {} projection_index_statuses: dict[str, str] = {} for name, declaration in projection_policy.items(): @@ -1604,43 +1625,17 @@ def _validate_projection_index_status(status: str) -> None: @classmethod def _refresh_projection_index_statuses( cls, - generation: dict[str, Any], - derived_metadata: dict[str, Any], + metadata_status: dict[str, Any], + metadata: dict[str, Any], ) -> None: - summary_index = generation.get("projection_indexes", {}).get("summary") + summary_index = metadata_status.get("projection_indexes", {}).get("summary") if not summary_index or not summary_index.get("requested"): return - if "summary" not in derived_metadata: + if "summary" not in metadata: return if summary_index.get("status", "not_indexed") == "not_indexed": summary_index["status"] = "pending_index" - @classmethod - def _merge_metadata_values( - cls, - metadata: dict[str, Any], - derived_metadata: dict[str, Any], - ) -> dict[str, Any]: - merged = dict(metadata) - for name, value in derived_metadata.items(): - if name not in merged: - merged[name] = value - continue - if merged[name] == value: - continue - merged[name] = cls._merge_metadata_value(merged[name], value) - return merged - - @staticmethod - def _merge_metadata_value(raw_value: Any, derived_value: Any) -> Any: - values = raw_value if isinstance(raw_value, list) else [raw_value] - derived_values = derived_value if isinstance(derived_value, list) else [derived_value] - merged = list(values) - for item in derived_values: - if item not in merged: - merged.append(item) - return merged - @staticmethod def _infer_metadata_field_type(value: Any) -> str: if isinstance(value, bool): diff --git a/pageindex/filesystem/projection_indexing.py b/pageindex/filesystem/projection_indexing.py index 5c07ca0b6..ed77465ef 100644 --- a/pageindex/filesystem/projection_indexing.py +++ b/pageindex/filesystem/projection_indexing.py @@ -68,7 +68,7 @@ def from_provider( ) def upsert_summary(self, record: dict[str, Any]) -> dict[str, Any]: - summary = str((record.get("derived_metadata") or {}).get("summary") or "").strip() + summary = str((record.get("metadata") or {}).get("summary") or "").strip() if not summary: return {"status": "skipped", "reason": "missing_summary"} vector = self.embedding_cache.embed_texts( @@ -79,7 +79,6 @@ def upsert_summary(self, record: dict[str, Any]) -> dict[str, Any]: batch_size=1, )[0] metadata = dict(record.get("metadata") or {}) - metadata.update(record.get("derived_metadata") or {}) count = self.index.upsert_many( [ SemanticIndexRecord( diff --git a/pageindex/filesystem/store.py b/pageindex/filesystem/store.py index 9ef90adda..a0dbe6aed 100644 --- a/pageindex/filesystem/store.py +++ b/pageindex/filesystem/store.py @@ -9,7 +9,7 @@ from .types import FileEntry, MetadataField -SCHEMA_VERSION = 4 +SCHEMA_VERSION = 5 class SQLiteFileSystemStore: @@ -47,6 +47,10 @@ def migrate(self) -> None: version = 3 if version < 4: self._migrate_to_v4(conn) + conn.execute("PRAGMA user_version = 4") + version = 4 + if version < 5: + self._migrate_to_v5(conn) conn.execute(f"PRAGMA user_version = {SCHEMA_VERSION}") def _migrate_to_v1(self, conn: sqlite3.Connection) -> None: @@ -68,8 +72,7 @@ def _migrate_to_v1(self, conn: sqlite3.Connection) -> None: pageindex_doc_id TEXT, pageindex_tree_status TEXT NOT NULL DEFAULT 'not_built', metadata_json TEXT NOT NULL DEFAULT '{}', - derived_metadata_json TEXT NOT NULL DEFAULT '{}', - metadata_generation_json TEXT NOT NULL DEFAULT '{}', + metadata_status_json TEXT NOT NULL DEFAULT '{}', created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP, deleted_at TEXT @@ -228,10 +231,58 @@ def _migrate_to_v4(self, conn: sqlite3.Connection) -> None: if "files" not in self._tables(conn): return columns = self._columns(conn, "files") - if "derived_metadata_json" not in columns: - conn.execute("ALTER TABLE files ADD COLUMN derived_metadata_json TEXT NOT NULL DEFAULT '{}'") - if "metadata_generation_json" not in columns: - conn.execute("ALTER TABLE files ADD COLUMN metadata_generation_json TEXT NOT NULL DEFAULT '{}'") + if "metadata_status_json" not in columns: + conn.execute("ALTER TABLE files ADD COLUMN metadata_status_json TEXT NOT NULL DEFAULT '{}'") + self._backfill_metadata_values(conn) + + def _migrate_to_v5(self, conn: sqlite3.Connection) -> None: + if "files" not in self._tables(conn): + return + columns = self._columns(conn, "files") + if "metadata_status_json" not in columns: + conn.execute("ALTER TABLE files ADD COLUMN metadata_status_json TEXT NOT NULL DEFAULT '{}'") + columns = self._columns(conn, "files") + derived_select = ( + "derived_metadata_json" + if "derived_metadata_json" in columns + else "'{}' AS derived_metadata_json" + ) + legacy_status_select = ( + "metadata_generation_json" + if "metadata_generation_json" in columns + else "'{}' AS metadata_generation_json" + ) + rows = conn.execute( + f""" + SELECT file_ref, metadata_json, metadata_status_json, {derived_select}, {legacy_status_select} + FROM files + WHERE deleted_at IS NULL + """ + ).fetchall() + for row in rows: + metadata = self._json_object(row["metadata_json"]) + legacy_generated_values = self._json_object(row["derived_metadata_json"]) + metadata_status = self._json_object(row["metadata_status_json"]) + legacy_status = self._json_object(row["metadata_generation_json"]) + if not metadata_status and legacy_status: + metadata_status = legacy_status + metadata_status = self._normalize_metadata_status(metadata_status) + if legacy_generated_values: + metadata.update(legacy_generated_values) + conn.execute( + """ + UPDATE files + SET metadata_json = ?, + metadata_status_json = ?, + updated_at = CURRENT_TIMESTAMP + WHERE file_ref = ? AND deleted_at IS NULL + """, + ( + json.dumps(metadata, ensure_ascii=False), + json.dumps(metadata_status, ensure_ascii=False), + row["file_ref"], + ), + ) self._backfill_metadata_values(conn) def _migrate_legacy_tables(self, conn: sqlite3.Connection) -> None: @@ -271,43 +322,61 @@ def _backfill_legacy_memberships(self, conn: sqlite3.Connection) -> None: def _backfill_metadata_values(self, conn: sqlite3.Connection) -> None: if "files" not in self._tables(conn): return - columns = self._columns(conn, "files") - derived_select = ( - "derived_metadata_json" - if "derived_metadata_json" in columns - else "'{}' AS derived_metadata_json" - ) - generation_select = ( - "metadata_generation_json" - if "metadata_generation_json" in columns - else "'{}' AS metadata_generation_json" - ) rows = conn.execute( - f""" - SELECT file_ref, metadata_json, {derived_select}, {generation_select} + """ + SELECT file_ref, metadata_json FROM files WHERE deleted_at IS NULL """ ).fetchall() for row in rows: - try: - metadata = json.loads(row["metadata_json"] or "{}") - except json.JSONDecodeError: - metadata = {} - try: - derived_metadata = json.loads(row["derived_metadata_json"] or "{}") - except json.JSONDecodeError: - derived_metadata = {} - try: - metadata_generation = json.loads(row["metadata_generation_json"] or "{}") - except json.JSONDecodeError: - metadata_generation = {} self.replace_metadata_values( conn, row["file_ref"], - self.indexed_metadata_values(metadata, derived_metadata, metadata_generation), + self.indexed_metadata_values(self._json_object(row["metadata_json"])), ) + @staticmethod + def _json_object(value: Any) -> dict[str, Any]: + try: + parsed = json.loads(value or "{}") if isinstance(value, str) else value + except json.JSONDecodeError: + return {} + return parsed if isinstance(parsed, dict) else {} + + @staticmethod + def _normalize_metadata_status(metadata_status: dict[str, Any]) -> dict[str, Any]: + normalized = dict(metadata_status) + fields = normalized.get("fields") + if isinstance(fields, dict): + normalized["fields"] = { + name: ( + { + **state, + "owner": state.get("owner", "pifs"), + "source": state.get("source", "llm"), + } + if isinstance(state, dict) + else state + ) + for name, state in fields.items() + } + projection_indexes = normalized.get("projection_indexes") + if isinstance(projection_indexes, dict): + normalized["projection_indexes"] = { + name: ( + { + **state, + "owner": state.get("owner", "pifs"), + "source": state.get("source", "index"), + } + if isinstance(state, dict) + else state + ) + for name, state in projection_indexes.items() + } + return normalized + @staticmethod def _tables(conn: sqlite3.Connection) -> set[str]: rows = conn.execute("SELECT name FROM sqlite_master WHERE type IN ('table', 'virtual table')").fetchall() @@ -421,8 +490,7 @@ def _file_insert_sql(*, include_folder_path: bool) -> str: "pageindex_doc_id", "pageindex_tree_status", "metadata_json", - "derived_metadata_json", - "metadata_generation_json", + "metadata_status_json", ] if include_folder_path: columns.append("folder_path") @@ -450,8 +518,7 @@ def _file_insert_values(record: dict[str, Any], *, include_folder_path: bool) -> record.get("pageindex_doc_id"), record.get("pageindex_tree_status", "not_built"), record["metadata_json"], - record.get("derived_metadata_json", "{}"), - record.get("metadata_generation_json", "{}"), + record.get("metadata_status_json", "{}"), ] if include_folder_path: values.append(record["folder_path"]) @@ -560,8 +627,7 @@ def _insert_file_row(self, conn: sqlite3.Connection, record: dict[str, Any]) -> "pageindex_doc_id", "pageindex_tree_status", "metadata_json", - "derived_metadata_json", - "metadata_generation_json", + "metadata_status_json", "deleted_at", "updated_at", ] @@ -580,8 +646,7 @@ def _insert_file_row(self, conn: sqlite3.Connection, record: dict[str, Any]) -> record.get("pageindex_doc_id"), record.get("pageindex_tree_status", "not_built"), record["metadata_json"], - record.get("derived_metadata_json", "{}"), - record.get("metadata_generation_json", "{}"), + record.get("metadata_status_json", "{}"), None, current_timestamp, ] @@ -999,8 +1064,7 @@ def _search_once( "f.descriptor", "f.pageindex_tree_status", "f.metadata_json", - "f.derived_metadata_json", - "f.metadata_generation_json", + "f.metadata_status_json", "f.created_at", """ ( @@ -1182,7 +1246,7 @@ def get_file(self, file_ref: str) -> FileEntry: raise KeyError(f"Unknown file_ref: {file_ref}") return self._file_entry(row) - def list_pending_metadata_generation(self, *, limit: int | None = None) -> list[FileEntry]: + def list_pending_metadata_status(self, *, limit: int | None = None) -> list[FileEntry]: sql = """ SELECT f.file_ref, @@ -1199,16 +1263,15 @@ def list_pending_metadata_generation(self, *, limit: int | None = None) -> list[ f.pageindex_doc_id, f.pageindex_tree_status, f.metadata_json, - f.derived_metadata_json, - f.metadata_generation_json, + f.metadata_status_json, COALESCE(primary_folder.path, '/') AS folder_path FROM files f LEFT JOIN file_folders ff ON ff.file_ref = f.file_ref LEFT JOIN folders primary_folder ON primary_folder.folder_id = ff.folder_id WHERE f.deleted_at IS NULL AND ( - f.metadata_generation_json LIKE '%pending_generate%' - OR f.metadata_generation_json LIKE '%pending_submit%' + f.metadata_status_json LIKE '%pending_generate%' + OR f.metadata_status_json LIKE '%pending_submit%' ) GROUP BY f.file_ref ORDER BY f.created_at, f.file_ref @@ -1221,39 +1284,36 @@ def list_pending_metadata_generation(self, *, limit: int | None = None) -> list[ rows = conn.execute(sql, params).fetchall() return [self._file_entry(row) for row in rows] - def update_file_metadata_generation( + def update_file_metadata_status( self, file_ref: str, *, - derived_metadata: dict[str, Any], - metadata_generation: dict[str, Any], + metadata: dict[str, Any], + metadata_status: dict[str, Any], ) -> None: with self.connect() as conn: row = self._file_entry_row(conn, file_ref) if row is None: raise KeyError(f"Unknown file_ref: {file_ref}") - metadata = json.loads(row["metadata_json"] or "{}") - metadata_text_value = metadata_text( - self._merge_metadata_values(metadata, derived_metadata) - ) + metadata_text_value = metadata_text(metadata) conn.execute( """ UPDATE files - SET derived_metadata_json = ?, - metadata_generation_json = ?, + SET metadata_json = ?, + metadata_status_json = ?, updated_at = CURRENT_TIMESTAMP WHERE file_ref = ? AND deleted_at IS NULL """, ( - json.dumps(derived_metadata, ensure_ascii=False), - json.dumps(metadata_generation, ensure_ascii=False), + json.dumps(metadata, ensure_ascii=False), + json.dumps(metadata_status, ensure_ascii=False), file_ref, ), ) self.replace_metadata_values( conn, file_ref, - self.indexed_metadata_values(metadata, derived_metadata, metadata_generation), + self.indexed_metadata_values(metadata), ) conn.execute( """ @@ -1631,8 +1691,7 @@ def _file_entry_row(self, conn: sqlite3.Connection, file_ref: str) -> sqlite3.Ro f.pageindex_doc_id, f.pageindex_tree_status, f.metadata_json, - f.derived_metadata_json, - f.metadata_generation_json, + f.metadata_status_json, COALESCE( ( SELECT display_folder.path @@ -1668,8 +1727,7 @@ def _file_rows_for_scope( f.source_path, f.pageindex_tree_status, f.metadata_json, - f.derived_metadata_json, - f.metadata_generation_json, + f.metadata_status_json, f.created_at, MIN(pf.folder_id) AS folder_id, MIN(pf.path) AS folder_path @@ -1860,9 +1918,8 @@ def _file_summary(cls, row: sqlite3.Row) -> dict[str, Any]: "source_path": row["source_path"], "folder_path": row["folder_path"], "metadata": json.loads(row["metadata_json"] or "{}"), - "derived_metadata": json.loads(cls._row_value(row, "derived_metadata_json", "{}") or "{}"), - "metadata_generation": json.loads( - cls._row_value(row, "metadata_generation_json", "{}") or "{}" + "metadata_status": json.loads( + cls._row_value(row, "metadata_status_json", "{}") or "{}" ), } @@ -1885,9 +1942,8 @@ def _search_row_to_dict(cls, row: sqlite3.Row) -> dict[str, Any]: "snippet": row["snippet"] or row["title"], "folder_path": row["folder_path"], "metadata": json.loads(row["metadata_json"] or "{}"), - "derived_metadata": json.loads(cls._row_value(row, "derived_metadata_json", "{}") or "{}"), - "metadata_generation": json.loads( - cls._row_value(row, "metadata_generation_json", "{}") or "{}" + "metadata_status": json.loads( + cls._row_value(row, "metadata_status_json", "{}") or "{}" ), } @@ -1913,11 +1969,8 @@ def _file_entry(row: sqlite3.Row) -> FileEntry: pageindex_tree_status=row["pageindex_tree_status"], metadata=json.loads(row["metadata_json"] or "{}"), folder_path=row["folder_path"], - derived_metadata=json.loads( - SQLiteFileSystemStore._row_value(row, "derived_metadata_json", "{}") or "{}" - ), - metadata_generation=json.loads( - SQLiteFileSystemStore._row_value(row, "metadata_generation_json", "{}") or "{}" + metadata_status=json.loads( + SQLiteFileSystemStore._row_value(row, "metadata_status_json", "{}") or "{}" ), ) @@ -1944,8 +1997,7 @@ def _file_entry_to_dict(cls, entry: FileEntry) -> dict[str, Any]: "pageindex_doc_id": entry.pageindex_doc_id, "pageindex_tree_status": entry.pageindex_tree_status, "metadata": entry.metadata, - "derived_metadata": entry.derived_metadata, - "metadata_generation": entry.metadata_generation, + "metadata_status": entry.metadata_status, "folder_path": entry.folder_path, } @@ -2041,57 +2093,9 @@ def _metadata_compare_text(value: Any) -> str: return json.dumps(value, ensure_ascii=False, sort_keys=True) return "" if value is None else str(value) - @classmethod - def _merge_metadata_values( - cls, - metadata: dict[str, Any], - derived_metadata: dict[str, Any], - ) -> dict[str, Any]: - merged = dict(metadata) - for name, value in derived_metadata.items(): - if name not in merged: - merged[name] = value - continue - if merged[name] == value: - continue - merged[name] = cls._merge_metadata_value(merged[name], value) - return merged - @staticmethod - def _merge_metadata_value(raw_value: Any, derived_value: Any) -> Any: - values = raw_value if isinstance(raw_value, list) else [raw_value] - derived_values = derived_value if isinstance(derived_value, list) else [derived_value] - merged = list(values) - for item in derived_values: - if item not in merged: - merged.append(item) - return merged - - @classmethod - def indexed_metadata_values( - cls, - metadata: dict[str, Any], - derived_metadata: dict[str, Any], - metadata_generation: dict[str, Any] | None, - ) -> dict[str, Any]: - generated_fields = set(derived_metadata) - if isinstance(metadata_generation, dict): - policy = metadata_generation.get("policy", {}) - if isinstance(policy, dict): - fields = policy.get("fields", {}) - if isinstance(fields, dict): - generated_fields.update( - str(name) - for name in fields - ) - - indexed = { - name: value - for name, value in metadata.items() - if name not in generated_fields - } - indexed.update(derived_metadata) - return indexed + def indexed_metadata_values(metadata: dict[str, Any]) -> dict[str, Any]: + return dict(metadata) @staticmethod def _valid_field_name(name: str) -> bool: diff --git a/pageindex/filesystem/types.py b/pageindex/filesystem/types.py index 4cd573fcb..103d28dd6 100644 --- a/pageindex/filesystem/types.py +++ b/pageindex/filesystem/types.py @@ -22,8 +22,7 @@ class SearchResult: pageNum: Optional[int] = None createdAt: Optional[str] = None folderId: Optional[str] = None - derived_metadata: dict[str, Any] = field(default_factory=dict) - metadata_generation: dict[str, Any] = field(default_factory=dict) + metadata_status: dict[str, Any] = field(default_factory=dict) @dataclass(frozen=True) @@ -63,8 +62,7 @@ class FileEntry: pageindex_tree_status: str metadata: dict[str, Any] folder_path: str - derived_metadata: dict[str, Any] = field(default_factory=dict) - metadata_generation: dict[str, Any] = field(default_factory=dict) + metadata_status: dict[str, Any] = field(default_factory=dict) @dataclass(frozen=True) diff --git a/tests/test_pifs_find_maxdepth.py b/tests/test_pifs_find_maxdepth.py index 304964fbf..e30124835 100644 --- a/tests/test_pifs_find_maxdepth.py +++ b/tests/test_pifs_find_maxdepth.py @@ -98,12 +98,23 @@ def test_stable_path_targets_work_without_session_refs(tmp_path): assert "Root document fixture text" in text -def test_stat_shell_output_includes_generated_metadata(tmp_path): +def test_stat_shell_output_includes_unified_metadata_status(tmp_path): from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem + from pageindex.filesystem.metadata_generation import MetadataGenerationResult source = tmp_path / "source.txt" source.write_text("fixture text", encoding="utf-8") - filesystem = PageIndexFileSystem(workspace=tmp_path / "workspace") + + class SummaryGenerator: + def generate(self, document, *, fields): + return MetadataGenerationResult( + values={field: "Generated summary for retrieval." for field in fields} + ) + + filesystem = PageIndexFileSystem( + workspace=tmp_path / "workspace", + metadata_generator=SummaryGenerator(), + ) filesystem.register_file( storage_uri=source.as_uri(), source_path="docs/source.txt", @@ -112,8 +123,7 @@ def test_stat_shell_output_includes_generated_metadata(tmp_path): title="Generated metadata document", content=source.read_text(encoding="utf-8"), metadata={"department": "ops"}, - derived_metadata={"summary": "Generated summary for retrieval."}, - metadata_generation_policy={ + metadata_policy={ "fields": { "summary": True, "doc_type": False, @@ -128,9 +138,126 @@ def test_stat_shell_output_includes_generated_metadata(tmp_path): assert "metadata:" in stat assert " department: ops" in stat - assert "generated_metadata:" in stat assert " summary: Generated summary for retrieval." in stat - assert "metadata_generation_status: generated" in stat + assert "metadata_status: generated" in stat + + +def test_register_rejects_pifs_owned_metadata_fields(tmp_path): + from pageindex.filesystem import PageIndexFileSystem + + source = tmp_path / "source.txt" + source.write_text("fixture text", encoding="utf-8") + filesystem = PageIndexFileSystem(workspace=tmp_path / "workspace") + + with pytest.raises(ValueError, match="PIFS-owned generated field"): + filesystem.register_file( + storage_uri=source.as_uri(), + source_path="docs/source.txt", + folder_path="/documents", + external_id="doc_conflict", + title="Conflict document", + content=source.read_text(encoding="utf-8"), + metadata={"summary": "caller summary"}, + ) + + +def test_batch_metadata_status_generates_into_unified_metadata(tmp_path): + from pageindex.filesystem import PageIndexFileSystem + from pageindex.filesystem.metadata_generation import MetadataGenerationResult + + source = tmp_path / "source.txt" + source.write_text("fixture text", encoding="utf-8") + + class SummaryGenerator: + def generate(self, document, *, fields): + return MetadataGenerationResult(values={"summary": "Batch generated summary."}) + + filesystem = PageIndexFileSystem( + workspace=tmp_path / "workspace", + metadata_generator=SummaryGenerator(), + ) + file_ref = filesystem.register_file( + storage_uri=source.as_uri(), + source_path="docs/source.txt", + folder_path="/documents", + external_id="doc_batch", + title="Batch document", + content=source.read_text(encoding="utf-8"), + metadata={"department": "ops"}, + metadata_policy={ + "batch": True, + "fields": { + "summary": True, + "doc_type": False, + "domain": False, + "topic": False, + }, + }, + ) + + before = filesystem.store.get_file(file_ref) + assert "summary" not in before.metadata + assert before.metadata_status["fields"]["summary"]["status"] == "pending_submit" + + result = filesystem.batch_generate() + after = filesystem.store.get_file(file_ref) + + assert result["generated"] == 1 + assert after.metadata["summary"] == "Batch generated summary." + assert after.metadata["department"] == "ops" + assert after.metadata_status["fields"]["summary"]["status"] == "generated" + + +def test_v4_metadata_columns_migrate_to_unified_metadata_status(tmp_path): + from pageindex.filesystem import PageIndexFileSystem + + source = tmp_path / "source.txt" + source.write_text("fixture text", encoding="utf-8") + workspace = tmp_path / "workspace" + filesystem = PageIndexFileSystem(workspace=workspace) + file_ref = filesystem.register_file( + storage_uri=source.as_uri(), + source_path="docs/source.txt", + folder_path="/documents", + external_id="doc_migrate", + title="Migrated document", + content=source.read_text(encoding="utf-8"), + metadata={"department": "ops"}, + ) + + with filesystem.store.connect() as conn: + conn.execute( + "ALTER TABLE files ADD COLUMN derived_metadata_json TEXT NOT NULL DEFAULT '{}'" + ) + conn.execute( + "ALTER TABLE files ADD COLUMN metadata_generation_json TEXT NOT NULL DEFAULT '{}'" + ) + conn.execute( + """ + UPDATE files + SET metadata_json = ?, + derived_metadata_json = ?, + metadata_generation_json = ?, + metadata_status_json = '{}' + WHERE file_ref = ? + """, + ( + '{"department":"ops","summary":"raw summary"}', + '{"summary":"generated summary"}', + '{"status":"generated","fields":{"summary":{"requested":true,"status":"generated"}}}', + file_ref, + ), + ) + conn.execute("PRAGMA user_version = 4") + + migrated = PageIndexFileSystem(workspace=workspace) + entry = migrated.store.get_file(file_ref) + + assert entry.metadata["department"] == "ops" + assert entry.metadata["summary"] == "generated summary" + assert entry.metadata_status["status"] == "generated" + assert entry.metadata_status["fields"]["summary"]["owner"] == "pifs" + assert entry.metadata_status["fields"]["summary"]["source"] == "llm" def test_find_maxdepth_zero_type_directory_returns_start_folder(tmp_path): diff --git a/tests/test_semantic_index.py b/tests/test_semantic_index.py index a500d9be8..a06641ee4 100644 --- a/tests/test_semantic_index.py +++ b/tests/test_semantic_index.py @@ -51,3 +51,39 @@ def test_sqlite_vec_semantic_index_round_trip(tmp_path): filters={"source_type": "slack"}, ) assert [item.external_id for item in filtered] == ["doc_b"] + + +def test_summary_projection_indexes_unified_metadata_summary(tmp_path): + from pageindex.filesystem.projection_indexing import SummaryProjectionIndexer + + class FakeEmbedder: + def embed(self, texts): + return [[1.0, 0.0, 0.0] for _ in texts] + + indexer = SummaryProjectionIndexer( + tmp_path / "projection", + embedder=FakeEmbedder(), + embedding_provider="test", + embedding_model="fake", + embedding_dimensions=3, + ) + + result = indexer.upsert_summary( + { + "file_ref": "file_a", + "external_id": "doc_a", + "source_type": "documents", + "source_path": "docs/a.pdf", + "title": "A", + "metadata": { + "summary": "Unified metadata summary.", + "department": "ops", + }, + } + ) + + assert result["status"] == "ready" + hits = indexer.index.search([1.0, 0.0, 0.0], limit=1) + assert hits[0].external_id == "doc_a" + assert hits[0].metadata["summary"] == "Unified metadata summary." + assert hits[0].metadata["department"] == "ops" From de1992def10b0cc70fb76a519a5eb1f76cf930bf Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 17:21:44 +0800 Subject: [PATCH 21/50] refactor(filesystem): make pifs providers configurable --- examples/pifs_demo.py | 66 ++++++++++++++++----- pageindex/filesystem/__init__.py | 4 +- pageindex/filesystem/core.py | 19 +++++- pageindex/filesystem/hybrid_projection.py | 55 +++++++---------- pageindex/filesystem/metadata_generation.py | 32 ++++++++-- tests/test_metadata_generation.py | 30 ++++++++++ tests/test_semantic_index.py | 9 +++ 7 files changed, 154 insertions(+), 61 deletions(-) create mode 100644 tests/test_metadata_generation.py diff --git a/examples/pifs_demo.py b/examples/pifs_demo.py index f6f9b511d..cf5bf82da 100644 --- a/examples/pifs_demo.py +++ b/examples/pifs_demo.py @@ -39,7 +39,7 @@ os.environ.setdefault("LITELLM_LOCAL_MODEL_COST_MAP", "true") from pageindex import PageIndexClient -from pageindex.filesystem import OpenAIMetadataGenerator, PageIndexFileSystem, PIFSCommandExecutor +from pageindex.filesystem import MetadataGenerator, PageIndexFileSystem, PIFSCommandExecutor from pageindex.filesystem.agent import run_pifs_agent @@ -47,6 +47,12 @@ DOCUMENTS_DIR = EXAMPLES_DIR / "documents" WORKSPACE = EXAMPLES_DIR / "pifs_workspace" DEFAULT_MODEL = os.environ.get("PIFS_DEMO_MODEL", "gpt-5.4-mini") +DEFAULT_METADATA_PROVIDER = os.environ.get("PIFS_DEMO_METADATA_PROVIDER") or os.environ.get( + "PIFS_METADATA_PROVIDER", "openai" +) +DEFAULT_EMBEDDING_PROVIDER = os.environ.get("PIFS_DEMO_EMBEDDING_PROVIDER") or os.environ.get( + "PIFS_EMBEDDING_PROVIDER", "openai" +) DEFAULT_QUESTION = ( "Use the PIFS workspace to find the Federal Reserve annual report. " "Which section covers supervision and regulation, and what page range " @@ -110,10 +116,15 @@ def parse_args() -> argparse.Namespace: ) parser.add_argument("--question", default=DEFAULT_QUESTION) parser.add_argument("--model", default=DEFAULT_MODEL) + parser.add_argument( + "--metadata-provider", + default=DEFAULT_METADATA_PROVIDER, + help="Provider used for register-time metadata generation.", + ) parser.add_argument( "--metadata-model", default=os.environ.get("PIFS_METADATA_MODEL", "gpt-5-nano"), - help="OpenAI or OpenAI-compatible model used for register-time metadata.", + help="Model used for register-time metadata generation.", ) parser.add_argument("--stream-mode", default="all", choices=["off", "tools", "model", "all"]) parser.add_argument("--verbose", action="store_true") @@ -121,23 +132,40 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--max-seconds", type=float, default=90) parser.add_argument("--reasoning-effort", default=None) parser.add_argument("--reasoning-summary", default="auto") + parser.add_argument( + "--embedding-provider", + default=DEFAULT_EMBEDDING_PROVIDER, + help="Provider used for register-time summary projection embeddings.", + ) parser.add_argument( "--embedding-model", default=os.environ.get("PIFS_DEMO_EMBEDDING_MODEL", "text-embedding-3-small"), - help="OpenAI embedding model used for register-time summary projection.", + help="Embedding model used for register-time summary projection.", ) parser.add_argument("--embedding-dimensions", type=int, default=256) return parser.parse_args() -def require_openai_environment() -> None: - if os.environ.get("OPENAI_API_KEY"): - return - raise RuntimeError( - "OPENAI_API_KEY is required for this demo: register() generates real " - "PIFS metadata and the agent uses the OpenAI Agents SDK. Source your " - ".env or export OPENAI_API_KEY before running." - ) +def require_runtime_environment(*, metadata_provider: str, embedding_provider: str) -> None: + metadata_provider = metadata_provider.lower() + embedding_provider = embedding_provider.lower() + missing: list[str] = [] + if not os.environ.get("OPENAI_API_KEY"): + missing.append("OPENAI_API_KEY for the OpenAI Agents SDK demo agent") + if metadata_provider == "openai" and not ( + os.environ.get("PIFS_METADATA_API_KEY") or os.environ.get("OPENAI_API_KEY") + ): + missing.append("PIFS_METADATA_API_KEY or OPENAI_API_KEY for metadata generation") + if embedding_provider == "openai" and not ( + os.environ.get("PIFS_EMBEDDING_API_KEY") or os.environ.get("OPENAI_API_KEY") + ): + missing.append("PIFS_EMBEDDING_API_KEY or OPENAI_API_KEY for summary embeddings") + if missing: + raise RuntimeError( + "Missing required environment variable(s): " + + "; ".join(missing) + + ". Source your .env or export the required key before running." + ) def discover_cached_documents(documents_dir: Path) -> list[Path]: @@ -294,6 +322,7 @@ def backfill_registered_metadata_values(filesystem: PageIndexFileSystem, file_re def configure_summary_projection_backend( filesystem: PageIndexFileSystem, *, + embedding_provider: str, embedding_model: str, embedding_dimensions: int, ) -> None: @@ -301,7 +330,7 @@ def configure_summary_projection_backend( return filesystem.configure_hybrid_projection_retrieval( filesystem.summary_projection_index_dir, - embedding_provider="openai", + embedding_provider=embedding_provider, embedding_model=embedding_model, embedding_dimensions=embedding_dimensions, ) @@ -690,7 +719,10 @@ def run_smoke_commands( def main() -> None: args = parse_args() - require_openai_environment() + require_runtime_environment( + metadata_provider=args.metadata_provider, + embedding_provider=args.embedding_provider, + ) workspace = args.workspace.expanduser() documents_dir = args.documents_dir.expanduser() if args.reset and workspace.exists(): @@ -705,8 +737,11 @@ def main() -> None: filesystem = PageIndexFileSystem( workspace, - metadata_generator=OpenAIMetadataGenerator(model=args.metadata_model), - summary_projection_embedding_provider="openai", + metadata_generator=MetadataGenerator( + provider=args.metadata_provider, + model=args.metadata_model, + ), + summary_projection_embedding_provider=args.embedding_provider, summary_projection_embedding_model=args.embedding_model, summary_projection_embedding_dimensions=args.embedding_dimensions, ) @@ -718,6 +753,7 @@ def main() -> None: registered = register_documents(filesystem, documents, documents_dir=documents_dir) configure_summary_projection_backend( filesystem, + embedding_provider=args.embedding_provider, embedding_model=args.embedding_model, embedding_dimensions=args.embedding_dimensions, ) diff --git a/pageindex/filesystem/__init__.py b/pageindex/filesystem/__init__.py index 2ad1c8480..a6cde1614 100644 --- a/pageindex/filesystem/__init__.py +++ b/pageindex/filesystem/__init__.py @@ -2,11 +2,11 @@ from .core import PageIndexFileSystem from .hybrid_projection import HybridProjectionSearchBackend from .metadata_generation import ( + MetadataGenerationBackend, MetadataGenerationError, MetadataGenerationInput, MetadataGenerationResult, MetadataGenerator, - OpenAIMetadataGenerator, ) from .projection_indexing import SummaryProjectionIndexer from .semantic_index import ( @@ -20,11 +20,11 @@ __all__ = [ "OpenResult", "HybridProjectionSearchBackend", + "MetadataGenerationBackend", "MetadataGenerationError", "MetadataGenerationInput", "MetadataGenerationResult", "MetadataGenerator", - "OpenAIMetadataGenerator", "PIFSCommandExecutor", "PageIndexFileSystem", "RebuildableSemanticIndex", diff --git a/pageindex/filesystem/core.py b/pageindex/filesystem/core.py index bbf81f19f..fc096e3be 100644 --- a/pageindex/filesystem/core.py +++ b/pageindex/filesystem/core.py @@ -9,11 +9,11 @@ from ..client import PageIndexClient from .metadata import MetadataQueryEngine from .metadata_generation import ( + MetadataGenerationBackend, MetadataGenerationError, MetadataGenerationInput, MetadataGenerationResult, MetadataGenerator, - OpenAIMetadataGenerator, ) from .projection_indexing import SummaryProjectionIndexer from .semantic_folder_policy import ( @@ -91,7 +91,11 @@ def __init__( workspace: Union[str, Path], *, semantic_retrieval_backend: Any | None = None, - metadata_generator: MetadataGenerator | None = None, + metadata_generator: MetadataGenerationBackend | None = None, + metadata_provider: str = "openai", + metadata_model: str | None = None, + metadata_base_url: str | None = None, + metadata_max_text_chars: int = 24000, summary_projection_indexer: SummaryProjectionIndexer | None = None, summary_projection_index: bool = True, summary_projection_index_dir: Union[str, Path, None] = None, @@ -105,6 +109,10 @@ def __init__( self.metadata = MetadataQueryEngine(self.store) self.semantic_retrieval_backend = semantic_retrieval_backend self.metadata_generator = metadata_generator + self.metadata_provider = metadata_provider + self.metadata_model = metadata_model + self.metadata_base_url = metadata_base_url + self.metadata_max_text_chars = metadata_max_text_chars self.summary_projection_indexer = summary_projection_indexer self.summary_projection_index = summary_projection_index self.summary_projection_index_dir = ( @@ -199,7 +207,12 @@ def batch_generate(self, *, limit: int | None = None) -> dict[str, Any]: def _ensure_register_completion_defaults(self) -> None: if self.metadata_generator is None: - self.metadata_generator = OpenAIMetadataGenerator() + self.metadata_generator = MetadataGenerator( + provider=self.metadata_provider, + model=self.metadata_model, + base_url=self.metadata_base_url, + max_text_chars=self.metadata_max_text_chars, + ) if self.summary_projection_index and self.summary_projection_indexer is None: self.summary_projection_indexer = SummaryProjectionIndexer.from_provider( self.summary_projection_index_dir, diff --git a/pageindex/filesystem/hybrid_projection.py b/pageindex/filesystem/hybrid_projection.py index 30df5911d..e802ab38c 100644 --- a/pageindex/filesystem/hybrid_projection.py +++ b/pageindex/filesystem/hybrid_projection.py @@ -1,6 +1,5 @@ from __future__ import annotations -import hashlib import json import os import re @@ -331,17 +330,22 @@ def embed_texts( return [cached[text_hash] for text_hash in hashes] -class OpenAIEmbeddingClient: - def __init__(self, model: str, *, dimensions: int, timeout: float): - from openai import OpenAI - +class EmbeddingClient: + def __init__(self, *, provider: str, model: str, dimensions: int, timeout: float): + self.provider = provider.lower() self.model = model self.dimensions = dimensions - self.client = OpenAI( - api_key=os.environ.get("OPENAI_API_KEY"), - base_url=os.environ.get("OPENAI_BASE_URL") or None, - timeout=timeout, - ) + if self.provider != "openai": + raise ValueError(f"unknown embedding provider: {provider}") + from openai import OpenAI + + api_key = os.environ.get("PIFS_EMBEDDING_API_KEY") or os.environ.get("OPENAI_API_KEY") + base_url = os.environ.get("PIFS_EMBEDDING_BASE_URL") or os.environ.get("OPENAI_BASE_URL") + if not api_key: + raise ValueError( + "PIFS_EMBEDDING_API_KEY or OPENAI_API_KEY is required for PIFS embeddings" + ) + self.client = OpenAI(api_key=api_key, base_url=base_url or None, timeout=timeout) def embed(self, texts: list[str]) -> list[list[float]]: kwargs: dict[str, Any] = {"model": self.model, "input": texts} @@ -351,32 +355,13 @@ def embed(self, texts: list[str]) -> list[list[float]]: return [list(item.embedding) for item in sorted(response.data, key=lambda item: item.index)] -class HashEmbeddingClient: - def __init__(self, dimensions: int = 256): - self.dimensions = dimensions - - def embed(self, texts: list[str]) -> list[list[float]]: - return [self._embed_one(text) for text in texts] - - def _embed_one(self, text: str) -> list[float]: - vector = [0.0] * self.dimensions - for term in keyword_terms(text)[:256]: - digest = hashlib.blake2b(term.encode("utf-8"), digest_size=8).digest() - bucket = int.from_bytes(digest[:4], "little") % self.dimensions - sign = 1.0 if digest[4] % 2 == 0 else -1.0 - vector[bucket] += sign - norm = sum(value * value for value in vector) ** 0.5 - if norm: - vector = [value / norm for value in vector] - return vector - - def make_embedder(provider: str, model: str, *, dimensions: int, timeout: float) -> Any: - if provider == "openai": - return OpenAIEmbeddingClient(model, dimensions=dimensions, timeout=timeout) - if provider == "hash": - return HashEmbeddingClient(dimensions=dimensions if dimensions > 0 else 256) - raise ValueError(f"unknown embedding provider: {provider}") + return EmbeddingClient( + provider=provider, + model=model, + dimensions=dimensions, + timeout=timeout, + ) def query_text_for_channel(channel: str, query: str, projection: QueryProjection) -> str: diff --git a/pageindex/filesystem/metadata_generation.py b/pageindex/filesystem/metadata_generation.py index 19354556e..86b2ac6e7 100644 --- a/pageindex/filesystem/metadata_generation.py +++ b/pageindex/filesystem/metadata_generation.py @@ -32,7 +32,7 @@ class MetadataGenerationResult: failures: dict[str, str] = field(default_factory=dict) -class MetadataGenerator(Protocol): +class MetadataGenerationBackend(Protocol): def generate( self, request: MetadataGenerationInput, @@ -42,23 +42,31 @@ def generate( ... -class OpenAIMetadataGenerator: +class MetadataGenerator: """Default product generator for retrieval metadata. This intentionally lives under pageindex.filesystem instead of benchmark paths. It uses registered text today; callers can pass PageIndex-extracted text through the same MetadataGenerationInput without changing the API. + Provider selection is an instance parameter rather than a provider-specific + public class name. """ def __init__( self, *, + provider: str | None = None, model: str | None = None, base_url: str | None = None, max_text_chars: int = 24000, ): + self.provider = (provider or os.environ.get("PIFS_METADATA_PROVIDER", "openai")).lower() self.model = model or os.environ.get("PIFS_METADATA_MODEL", "gpt-5-nano") - self.base_url = base_url if base_url is not None else os.environ.get("OPENAI_BASE_URL") + self.base_url = ( + base_url + if base_url is not None + else os.environ.get("PIFS_METADATA_BASE_URL") or os.environ.get("OPENAI_BASE_URL") + ) self.max_text_chars = max_text_chars def generate( @@ -67,9 +75,21 @@ def generate( *, fields: list[str], ) -> MetadataGenerationResult: - api_key = os.environ.get("OPENAI_API_KEY") + if self.provider != "openai": + raise MetadataGenerationError(f"unsupported metadata provider: {self.provider}") + return self._generate_openai(request, fields=fields) + + def _generate_openai( + self, + request: MetadataGenerationInput, + *, + fields: list[str], + ) -> MetadataGenerationResult: + api_key = os.environ.get("PIFS_METADATA_API_KEY") or os.environ.get("OPENAI_API_KEY") if not api_key: - raise MetadataGenerationError("OPENAI_API_KEY is required for PIFS metadata generation") + raise MetadataGenerationError( + "PIFS_METADATA_API_KEY or OPENAI_API_KEY is required for PIFS metadata generation" + ) from openai import OpenAI @@ -122,7 +142,7 @@ def _response_format(fields: list[str]) -> dict[str, Any]: properties[field] = {"type": "string"} else: raise MetadataGenerationError( - f"OpenAIMetadataGenerator does not support generated metadata field: {field}" + f"MetadataGenerator does not support generated metadata field: {field}" ) return { "type": "json_schema", diff --git a/tests/test_metadata_generation.py b/tests/test_metadata_generation.py new file mode 100644 index 000000000..3e64a4b9e --- /dev/null +++ b/tests/test_metadata_generation.py @@ -0,0 +1,30 @@ +import sys +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parents[1] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + + +def test_metadata_generator_uses_provider_parameter(): + from pageindex.filesystem.metadata_generation import ( + MetadataGenerationError, + MetadataGenerationInput, + MetadataGenerator, + ) + + generator = MetadataGenerator(provider="unsupported", model="unused") + request = MetadataGenerationInput( + file_ref="file_a", + external_id="doc_a", + title="A", + source_path="docs/a.txt", + content_type="text/plain", + source_type=None, + text="hello", + ) + + with pytest.raises(MetadataGenerationError, match="unsupported metadata provider: unsupported"): + generator.generate(request, fields=["summary"]) diff --git a/tests/test_semantic_index.py b/tests/test_semantic_index.py index a06641ee4..4bd9085d0 100644 --- a/tests/test_semantic_index.py +++ b/tests/test_semantic_index.py @@ -1,6 +1,8 @@ import sys from pathlib import Path +import pytest + REPO_ROOT = Path(__file__).resolve().parents[1] if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) @@ -87,3 +89,10 @@ def embed(self, texts): assert hits[0].external_id == "doc_a" assert hits[0].metadata["summary"] == "Unified metadata summary." assert hits[0].metadata["department"] == "ops" + + +def test_hash_embedding_provider_is_not_available(): + from pageindex.filesystem.hybrid_projection import make_embedder + + with pytest.raises(ValueError, match="unknown embedding provider: hash"): + make_embedder("hash", "unused", dimensions=256, timeout=1) From 30830fc19e49c61b1ed3cc8ca2dd769dc5648954 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 17:31:03 +0800 Subject: [PATCH 22/50] refactor(filesystem): remove pre-release store migrations --- pageindex/filesystem/store.py | 315 ++----------------------------- tests/test_pifs_find_maxdepth.py | 52 ----- 2 files changed, 12 insertions(+), 355 deletions(-) diff --git a/pageindex/filesystem/store.py b/pageindex/filesystem/store.py index a0dbe6aed..1d85810e5 100644 --- a/pageindex/filesystem/store.py +++ b/pageindex/filesystem/store.py @@ -9,7 +9,7 @@ from .types import FileEntry, MetadataField -SCHEMA_VERSION = 5 +SCHEMA_VERSION = 1 class SQLiteFileSystemStore: @@ -22,7 +22,7 @@ def __init__(self, workspace: str | Path): self.pageindex_client_dir = self.workspace / "artifacts" / "pageindex_client" for path in (self.text_dir, self.raw_dir, self.pageindex_client_dir): path.mkdir(parents=True, exist_ok=True) - self.migrate() + self.initialize_schema() def connect(self) -> sqlite3.Connection: conn = sqlite3.connect(self.db_path) @@ -30,31 +30,13 @@ def connect(self) -> sqlite3.Connection: conn.execute("PRAGMA foreign_keys = ON") return conn - def migrate(self) -> None: + def initialize_schema(self) -> None: with self.connect() as conn: - version = conn.execute("PRAGMA user_version").fetchone()[0] - if version < 1: - self._migrate_to_v1(conn) - conn.execute("PRAGMA user_version = 1") - version = 1 - if version < 2: - self._migrate_to_v2(conn) - conn.execute("PRAGMA user_version = 2") - version = 2 - if version < 3: - self._migrate_to_v3(conn) - conn.execute("PRAGMA user_version = 3") - version = 3 - if version < 4: - self._migrate_to_v4(conn) - conn.execute("PRAGMA user_version = 4") - version = 4 - if version < 5: - self._migrate_to_v5(conn) - conn.execute(f"PRAGMA user_version = {SCHEMA_VERSION}") - - def _migrate_to_v1(self, conn: sqlite3.Connection) -> None: - self._migrate_legacy_tables(conn) + self._create_current_schema(conn) + self.ensure_folder(conn, "/") + conn.execute(f"PRAGMA user_version = {SCHEMA_VERSION}") + + def _create_current_schema(self, conn: sqlite3.Connection) -> None: conn.executescript( """ CREATE TABLE IF NOT EXISTS files ( @@ -158,183 +140,6 @@ def _migrate_to_v1(self, conn: sqlite3.Connection) -> None: VALUES ('default', NULL, 1, 'active') """ ) - self.ensure_folder(conn, "/") - self._backfill_legacy_memberships(conn) - self._backfill_metadata_values(conn) - - def _migrate_to_v2(self, conn: sqlite3.Connection) -> None: - if "folders" in self._tables(conn): - columns = self._columns(conn, "folders") - if "description" not in columns: - conn.execute("ALTER TABLE folders ADD COLUMN description TEXT NOT NULL DEFAULT ''") - if "metadata_fields" in self._tables(conn): - conn.execute( - """ - UPDATE metadata_fields - SET type = 'string' - WHERE type NOT IN ('string', 'number', 'boolean') - """ - ) - - def _migrate_to_v3(self, conn: sqlite3.Connection) -> None: - if "folders" in self._tables(conn): - columns = self._columns(conn, "folders") - if "metadata_json" not in columns: - conn.execute("ALTER TABLE folders ADD COLUMN metadata_json TEXT NOT NULL DEFAULT '{}'") - if "file_folders" in self._tables(conn): - columns = self._columns(conn, "file_folders") - if "membership_kind" in columns or "metadata_json" not in columns: - conn.execute("DROP INDEX IF EXISTS idx_file_folders_kind") - conn.execute("DROP INDEX IF EXISTS idx_file_folders_folder") - conn.execute( - """ - CREATE TABLE file_folders_v3 ( - file_ref TEXT NOT NULL, - folder_id TEXT NOT NULL, - metadata_json TEXT NOT NULL DEFAULT '{}', - created_at TEXT DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (file_ref, folder_id), - FOREIGN KEY(file_ref) REFERENCES files(file_ref) ON DELETE CASCADE, - FOREIGN KEY(folder_id) REFERENCES folders(folder_id) ON DELETE CASCADE - ) - """ - ) - conn.execute( - """ - INSERT OR IGNORE INTO file_folders_v3(file_ref, folder_id, metadata_json, created_at) - SELECT file_ref, folder_id, '{}', MIN(created_at) - FROM file_folders - GROUP BY file_ref, folder_id - """ - ) - conn.execute("DROP TABLE file_folders") - conn.execute("ALTER TABLE file_folders_v3 RENAME TO file_folders") - elif "metadata_json" not in columns: - conn.execute("ALTER TABLE file_folders ADD COLUMN metadata_json TEXT NOT NULL DEFAULT '{}'") - else: - conn.execute( - """ - CREATE TABLE file_folders ( - file_ref TEXT NOT NULL, - folder_id TEXT NOT NULL, - metadata_json TEXT NOT NULL DEFAULT '{}', - created_at TEXT DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (file_ref, folder_id), - FOREIGN KEY(file_ref) REFERENCES files(file_ref) ON DELETE CASCADE, - FOREIGN KEY(folder_id) REFERENCES folders(folder_id) ON DELETE CASCADE - ) - """ - ) - conn.execute("CREATE INDEX IF NOT EXISTS idx_file_folders_folder ON file_folders(folder_id)") - - def _migrate_to_v4(self, conn: sqlite3.Connection) -> None: - if "files" not in self._tables(conn): - return - columns = self._columns(conn, "files") - if "metadata_status_json" not in columns: - conn.execute("ALTER TABLE files ADD COLUMN metadata_status_json TEXT NOT NULL DEFAULT '{}'") - self._backfill_metadata_values(conn) - - def _migrate_to_v5(self, conn: sqlite3.Connection) -> None: - if "files" not in self._tables(conn): - return - columns = self._columns(conn, "files") - if "metadata_status_json" not in columns: - conn.execute("ALTER TABLE files ADD COLUMN metadata_status_json TEXT NOT NULL DEFAULT '{}'") - columns = self._columns(conn, "files") - derived_select = ( - "derived_metadata_json" - if "derived_metadata_json" in columns - else "'{}' AS derived_metadata_json" - ) - legacy_status_select = ( - "metadata_generation_json" - if "metadata_generation_json" in columns - else "'{}' AS metadata_generation_json" - ) - rows = conn.execute( - f""" - SELECT file_ref, metadata_json, metadata_status_json, {derived_select}, {legacy_status_select} - FROM files - WHERE deleted_at IS NULL - """ - ).fetchall() - for row in rows: - metadata = self._json_object(row["metadata_json"]) - legacy_generated_values = self._json_object(row["derived_metadata_json"]) - metadata_status = self._json_object(row["metadata_status_json"]) - legacy_status = self._json_object(row["metadata_generation_json"]) - if not metadata_status and legacy_status: - metadata_status = legacy_status - metadata_status = self._normalize_metadata_status(metadata_status) - if legacy_generated_values: - metadata.update(legacy_generated_values) - conn.execute( - """ - UPDATE files - SET metadata_json = ?, - metadata_status_json = ?, - updated_at = CURRENT_TIMESTAMP - WHERE file_ref = ? AND deleted_at IS NULL - """, - ( - json.dumps(metadata, ensure_ascii=False), - json.dumps(metadata_status, ensure_ascii=False), - row["file_ref"], - ), - ) - self._backfill_metadata_values(conn) - - def _migrate_legacy_tables(self, conn: sqlite3.Connection) -> None: - tables = self._tables(conn) - if "folders" in tables and "folder_id" not in self._columns(conn, "folders"): - conn.execute("ALTER TABLE folders RENAME TO folders_legacy_v0") - if "files" in tables: - columns = self._columns(conn, "files") - for name, ddl in { - "raw_artifact_path": "ALTER TABLE files ADD COLUMN raw_artifact_path TEXT", - "pageindex_doc_id": "ALTER TABLE files ADD COLUMN pageindex_doc_id TEXT", - "pageindex_tree_status": ( - "ALTER TABLE files ADD COLUMN pageindex_tree_status TEXT " - "NOT NULL DEFAULT 'not_built'" - ), - "deleted_at": "ALTER TABLE files ADD COLUMN deleted_at TEXT", - }.items(): - if name not in columns: - conn.execute(ddl) - - def _backfill_legacy_memberships(self, conn: sqlite3.Connection) -> None: - if "files" not in self._tables(conn) or "folder_path" not in self._columns(conn, "files"): - return - rows = conn.execute( - "SELECT file_ref, folder_path FROM files WHERE deleted_at IS NULL" - ).fetchall() - for row in rows: - folder_id = self.ensure_folder(conn, row["folder_path"] or "/") - conn.execute( - """ - INSERT OR IGNORE INTO file_folders(file_ref, folder_id, metadata_json) - VALUES (?, ?, '{}') - """, - (row["file_ref"], folder_id), - ) - - def _backfill_metadata_values(self, conn: sqlite3.Connection) -> None: - if "files" not in self._tables(conn): - return - rows = conn.execute( - """ - SELECT file_ref, metadata_json - FROM files - WHERE deleted_at IS NULL - """ - ).fetchall() - for row in rows: - self.replace_metadata_values( - conn, - row["file_ref"], - self.indexed_metadata_values(self._json_object(row["metadata_json"])), - ) @staticmethod def _json_object(value: Any) -> dict[str, Any]: @@ -344,44 +149,6 @@ def _json_object(value: Any) -> dict[str, Any]: return {} return parsed if isinstance(parsed, dict) else {} - @staticmethod - def _normalize_metadata_status(metadata_status: dict[str, Any]) -> dict[str, Any]: - normalized = dict(metadata_status) - fields = normalized.get("fields") - if isinstance(fields, dict): - normalized["fields"] = { - name: ( - { - **state, - "owner": state.get("owner", "pifs"), - "source": state.get("source", "llm"), - } - if isinstance(state, dict) - else state - ) - for name, state in fields.items() - } - projection_indexes = normalized.get("projection_indexes") - if isinstance(projection_indexes, dict): - normalized["projection_indexes"] = { - name: ( - { - **state, - "owner": state.get("owner", "pifs"), - "source": state.get("source", "index"), - } - if isinstance(state, dict) - else state - ) - for name, state in projection_indexes.items() - } - return normalized - - @staticmethod - def _tables(conn: sqlite3.Connection) -> set[str]: - rows = conn.execute("SELECT name FROM sqlite_master WHERE type IN ('table', 'virtual table')").fetchall() - return {row["name"] for row in rows} - @staticmethod def _columns(conn: sqlite3.Connection, table: str) -> set[str]: return {row["name"] for row in conn.execute(f"PRAGMA table_info({table})").fetchall()} @@ -408,7 +175,6 @@ def insert_files(self, records: list[dict[str, Any]]) -> None: "SELECT name, field_id FROM metadata_fields WHERE schema_id = 'default'" ).fetchall() } - include_folder_path = "folder_path" in self._columns(conn, "files") for record in records: folder_cache_key = (record["folder_path"], record.get("folder_kind", "manual")) folder_id = folder_cache.get(folder_cache_key) @@ -419,7 +185,7 @@ def insert_files(self, records: list[dict[str, Any]]) -> None: kind=record.get("folder_kind", "manual"), ) folder_cache[folder_cache_key] = folder_id - file_rows.append(self._file_insert_values(record, include_folder_path=include_folder_path)) + file_rows.append(self._file_insert_values(record)) membership_rows.append( ( record["file_ref"], @@ -445,7 +211,7 @@ def insert_files(self, records: list[dict[str, Any]]) -> None: metadata_field_ids, ) ) - conn.executemany(self._file_insert_sql(include_folder_path=include_folder_path), file_rows) + conn.executemany(self._file_insert_sql(), file_rows) conn.executemany( """ INSERT OR REPLACE INTO file_folders(file_ref, folder_id, metadata_json) @@ -474,7 +240,7 @@ def insert_files(self, records: list[dict[str, Any]]) -> None: ) @staticmethod - def _file_insert_sql(*, include_folder_path: bool) -> str: + def _file_insert_sql() -> str: columns = [ "file_ref", "external_id", @@ -492,8 +258,6 @@ def _file_insert_sql(*, include_folder_path: bool) -> str: "metadata_json", "metadata_status_json", ] - if include_folder_path: - columns.append("folder_path") columns.extend(["deleted_at", "updated_at"]) placeholders = ", ".join(["?"] * (len(columns) - 2) + ["NULL", "CURRENT_TIMESTAMP"]) return f""" @@ -502,7 +266,7 @@ def _file_insert_sql(*, include_folder_path: bool) -> str: """ @staticmethod - def _file_insert_values(record: dict[str, Any], *, include_folder_path: bool) -> tuple[Any, ...]: + def _file_insert_values(record: dict[str, Any]) -> tuple[Any, ...]: values: list[Any] = [ record["file_ref"], record["external_id"], @@ -520,8 +284,6 @@ def _file_insert_values(record: dict[str, Any], *, include_folder_path: bool) -> record["metadata_json"], record.get("metadata_status_json", "{}"), ] - if include_folder_path: - values.append(record["folder_path"]) return tuple(values) def _metadata_insert_values( @@ -610,59 +372,6 @@ def attach_files_to_folders(self, items: list[dict[str, Any]]) -> None: ), ) - def _insert_file_row(self, conn: sqlite3.Connection, record: dict[str, Any]) -> None: - current_timestamp = object() - columns = [ - "file_ref", - "external_id", - "storage_uri", - "source_path", - "title", - "descriptor", - "content_type", - "source_type", - "fingerprint", - "text_artifact_path", - "raw_artifact_path", - "pageindex_doc_id", - "pageindex_tree_status", - "metadata_json", - "metadata_status_json", - "deleted_at", - "updated_at", - ] - values: list[Any] = [ - record["file_ref"], - record["external_id"], - record["storage_uri"], - record["source_path"], - record["title"], - record["descriptor"], - record["content_type"], - record["source_type"], - record["fingerprint"], - record["text_artifact_path"], - record["raw_artifact_path"], - record.get("pageindex_doc_id"), - record.get("pageindex_tree_status", "not_built"), - record["metadata_json"], - record.get("metadata_status_json", "{}"), - None, - current_timestamp, - ] - if "folder_path" in self._columns(conn, "files"): - columns.insert(-2, "folder_path") - values.insert(-2, record["folder_path"]) - placeholders = ", ".join("CURRENT_TIMESTAMP" if value is current_timestamp else "?" for value in values) - bound_values = [value for value in values if value is not current_timestamp] - conn.execute( - f""" - INSERT OR REPLACE INTO files ({", ".join(columns)}) - VALUES ({placeholders}) - """, - bound_values, - ) - def replace_metadata_values( self, conn: sqlite3.Connection, diff --git a/tests/test_pifs_find_maxdepth.py b/tests/test_pifs_find_maxdepth.py index e30124835..fea78316b 100644 --- a/tests/test_pifs_find_maxdepth.py +++ b/tests/test_pifs_find_maxdepth.py @@ -208,58 +208,6 @@ def generate(self, document, *, fields): assert after.metadata_status["fields"]["summary"]["status"] == "generated" -def test_v4_metadata_columns_migrate_to_unified_metadata_status(tmp_path): - from pageindex.filesystem import PageIndexFileSystem - - source = tmp_path / "source.txt" - source.write_text("fixture text", encoding="utf-8") - workspace = tmp_path / "workspace" - filesystem = PageIndexFileSystem(workspace=workspace) - file_ref = filesystem.register_file( - storage_uri=source.as_uri(), - source_path="docs/source.txt", - folder_path="/documents", - external_id="doc_migrate", - title="Migrated document", - content=source.read_text(encoding="utf-8"), - metadata={"department": "ops"}, - ) - - with filesystem.store.connect() as conn: - conn.execute( - "ALTER TABLE files ADD COLUMN derived_metadata_json TEXT NOT NULL DEFAULT '{}'" - ) - conn.execute( - "ALTER TABLE files ADD COLUMN metadata_generation_json TEXT NOT NULL DEFAULT '{}'" - ) - conn.execute( - """ - UPDATE files - SET metadata_json = ?, - derived_metadata_json = ?, - metadata_generation_json = ?, - metadata_status_json = '{}' - WHERE file_ref = ? - """, - ( - '{"department":"ops","summary":"raw summary"}', - '{"summary":"generated summary"}', - '{"status":"generated","fields":{"summary":{"requested":true,"status":"generated"}}}', - file_ref, - ), - ) - conn.execute("PRAGMA user_version = 4") - - migrated = PageIndexFileSystem(workspace=workspace) - entry = migrated.store.get_file(file_ref) - - assert entry.metadata["department"] == "ops" - assert entry.metadata["summary"] == "generated summary" - assert entry.metadata_status["status"] == "generated" - assert entry.metadata_status["fields"]["summary"]["owner"] == "pifs" - assert entry.metadata_status["fields"]["summary"]["source"] == "llm" - - def test_find_maxdepth_zero_type_directory_returns_start_folder(tmp_path): executor = _register_find_fixture(tmp_path) From b6587350a3e27d90bb6077dcbd54d2995e67396f Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 19:10:56 +0800 Subject: [PATCH 23/50] fix(demo): register all example documents --- examples/pifs_demo.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/examples/pifs_demo.py b/examples/pifs_demo.py index cf5bf82da..7dcfd0d09 100644 --- a/examples/pifs_demo.py +++ b/examples/pifs_demo.py @@ -7,9 +7,10 @@ evidence through commands such as ls, tree, find, grep, search-summary, cat --structure, cat --page, and cat --node. -The demo uses PDFs under examples/documents. When a matching +The demo registers supported files under examples/documents. When a matching examples/documents/results/*_structure.json file exists, it is loaded into the -PIFS workspace's PageIndexClient cache so register() does not rebuild the tree. +PIFS workspace's PageIndexClient cache. Files without a cache exercise the +normal PageIndexClient.index() path during register(). Requirements: pip install openai-agents @@ -168,22 +169,20 @@ def require_runtime_environment(*, metadata_provider: str, embedding_provider: s ) -def discover_cached_documents(documents_dir: Path) -> list[Path]: - results_dir = documents_dir / "results" - paths: list[Path] = [] - for structure_path in sorted(results_dir.glob("*_structure.json")): - stem = structure_path.name.removesuffix("_structure.json") - for suffix in (".pdf", ".md", ".markdown"): - candidate = documents_dir / f"{stem}{suffix}" - if candidate.exists(): - paths.append(candidate) - break - return paths +SUPPORTED_DOCUMENT_SUFFIXES = {".pdf", ".md", ".markdown", ".txt", ".text"} + + +def discover_documents(documents_dir: Path) -> list[Path]: + return sorted( + path + for path in documents_dir.iterdir() + if path.is_file() and path.suffix.lower() in SUPPORTED_DOCUMENT_SUFFIXES + ) def resolve_requested_documents(documents_dir: Path, requested: list[str]) -> list[Path]: if not requested: - return discover_cached_documents(documents_dir) + return discover_documents(documents_dir) paths: list[Path] = [] for item in requested: path = Path(item).expanduser() From c42162613624a4713cb39418746d62ed4f3a9f58 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 19:25:52 +0800 Subject: [PATCH 24/50] fix(filesystem): constrain pifs agent tool identity --- pageindex/filesystem/agent.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pageindex/filesystem/agent.py b/pageindex/filesystem/agent.py index ba8645df1..67aaa6fba 100644 --- a/pageindex/filesystem/agent.py +++ b/pageindex/filesystem/agent.py @@ -33,6 +33,12 @@ related questions and invite them to ask about files, folders, metadata, or document contents in the workspace. +If the user asks what tools or capabilities you have, describe only the PIFS +virtual shell capabilities available inside this workspace: ls, tree, find, +stat, grep, cat, and semantic search commands such as search-summary when they +are available. Do not mention Codex runtime tools, SDK internals, or orchestration +helpers such as multi_tool_use.parallel. + Follow the task prompt for command policy, retrieval strategy, and answer format. If the caller needs stricter behavior, pass an explicit system_prompt. """ From 75acf5dce345b1339ee15817cc06c3c62769c5dd Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 19:29:23 +0800 Subject: [PATCH 25/50] fix(filesystem): generalize pifs tool identity prompt --- pageindex/filesystem/agent.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pageindex/filesystem/agent.py b/pageindex/filesystem/agent.py index 67aaa6fba..4375132fa 100644 --- a/pageindex/filesystem/agent.py +++ b/pageindex/filesystem/agent.py @@ -36,8 +36,8 @@ If the user asks what tools or capabilities you have, describe only the PIFS virtual shell capabilities available inside this workspace: ls, tree, find, stat, grep, cat, and semantic search commands such as search-summary when they -are available. Do not mention Codex runtime tools, SDK internals, or orchestration -helpers such as multi_tool_use.parallel. +are available. Do not mention host runtime tools, SDK internals, or orchestration +helpers that are not part of the PIFS shell. Follow the task prompt for command policy, retrieval strategy, and answer format. If the caller needs stricter behavior, pass an explicit system_prompt. From fe1c4aeb1cf3455e71fe5b5575bbf67f30c5e9ce Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 19:31:57 +0800 Subject: [PATCH 26/50] fix(filesystem): search workspace topic questions before clarifying --- pageindex/filesystem/agent.py | 6 ++++++ tests/test_pifs_agent_stream.py | 3 +++ 2 files changed, 9 insertions(+) diff --git a/pageindex/filesystem/agent.py b/pageindex/filesystem/agent.py index 4375132fa..4c2691992 100644 --- a/pageindex/filesystem/agent.py +++ b/pageindex/filesystem/agent.py @@ -39,6 +39,11 @@ are available. Do not mention host runtime tools, SDK internals, or orchestration helpers that are not part of the PIFS shell. +If the user asks a workspace-related topic question without naming a specific +file, treat it as a retrieval task. Use available PIFS discovery commands to +look for relevant files and inspect evidence before answering. Ask the user to +clarify only after a reasonable search cannot identify relevant evidence. + Follow the task prompt for command policy, retrieval strategy, and answer format. If the caller needs stricter behavior, pass an explicit system_prompt. """ @@ -75,6 +80,7 @@ - If search-summary is available and the user asks for summary search, semantic search, vector search, or "用 summary 搜", use search-summary ; do not translate that request into find --where. - Tool errors are returned as ERROR text; recover by trying an available command. - Use cat or grep to gather evidence before making source-backed claims. +- For broad topic, method, or "what solution" questions that are likely about the workspace, search for candidate documents before asking the user to choose a document. - Prefer target-first cat syntax with stable targets: cat --structure, cat --page 31-59, cat --node . - After cat --structure finds a relevant section/subsection with a node_id, prefer cat --node for content from that semantic unit. - Use cat --page - when the user explicitly asks for pages/page ranges, when no suitable node_id exists, or when you need exact page text to verify page-level evidence. diff --git a/tests/test_pifs_agent_stream.py b/tests/test_pifs_agent_stream.py index e16085023..1b6a3786f 100644 --- a/tests/test_pifs_agent_stream.py +++ b/tests/test_pifs_agent_stream.py @@ -215,6 +215,9 @@ def test_system_prompt_sets_workspace_identity_and_scope(self): self.assertIn("current PageIndex FileSystem\nworkspace", AGENT_SYSTEM_PROMPT) self.assertIn("unrelated to the current workspace", AGENT_SYSTEM_PROMPT) self.assertIn("do not answer it as\na general-purpose assistant", AGENT_SYSTEM_PROMPT) + self.assertIn("workspace-related topic question", AGENT_SYSTEM_PROMPT) + self.assertIn("clarify only after a reasonable search", AGENT_SYSTEM_PROMPT) + self.assertIn("search for candidate documents before asking", AGENT_TOOL_POLICY) if __name__ == "__main__": From 3519adfbd11536c41f38ddb7a10a3112ed710957 Mon Sep 17 00:00:00 2001 From: Bukely_ Date: Tue, 26 May 2026 20:26:58 +0800 Subject: [PATCH 27/50] fix(filesystem): preserve sqlite catalog durability Remove the synchronous=OFF pragma from PIFS catalog inserts so SQLite remains the durable source of truth. --- pageindex/filesystem/store.py | 1 - tests/test_filesystem_store.py | 45 ++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 tests/test_filesystem_store.py diff --git a/pageindex/filesystem/store.py b/pageindex/filesystem/store.py index 1d85810e5..1cca9ea25 100644 --- a/pageindex/filesystem/store.py +++ b/pageindex/filesystem/store.py @@ -160,7 +160,6 @@ def insert_files(self, records: list[dict[str, Any]]) -> None: if not records: return with self.connect() as conn: - conn.execute("PRAGMA synchronous = OFF") conn.execute("PRAGMA temp_store = MEMORY") folder_cache: dict[tuple[str, str], str] = {} file_rows = [] diff --git a/tests/test_filesystem_store.py b/tests/test_filesystem_store.py new file mode 100644 index 000000000..7f425038f --- /dev/null +++ b/tests/test_filesystem_store.py @@ -0,0 +1,45 @@ +import json + + +def test_insert_files_does_not_disable_sqlite_synchronous(tmp_path): + from pageindex.filesystem.store import SQLiteFileSystemStore + + statements = [] + + class RecordingStore(SQLiteFileSystemStore): + def connect(self): + conn = super().connect() + conn.set_trace_callback(statements.append) + return conn + + store = RecordingStore(tmp_path / "workspace") + statements.clear() + + store.insert_files( + [ + { + "file_ref": "ref_report", + "external_id": "doc_report", + "storage_uri": "file:///tmp/report.pdf", + "source_path": "documents/report.pdf", + "folder_path": "/documents", + "title": "Report", + "descriptor": "documents/report.pdf", + "content_type": "application/pdf", + "source_type": "documents", + "fingerprint": "fingerprint", + "text_artifact_path": "artifacts/text/ref_report.txt", + "raw_artifact_path": None, + "metadata": {}, + "metadata_json": json.dumps({}), + "metadata_text": "", + "content": "", + "skip_fts": True, + } + ] + ) + + assert not any( + statement.upper().replace(" ", "") == "PRAGMASYNCHRONOUS=OFF" + for statement in statements + ) From ad45f96dfa42d7015a41464f1c8c763727408246 Mon Sep 17 00:00:00 2001 From: Bukely_ Date: Tue, 26 May 2026 20:27:29 +0800 Subject: [PATCH 28/50] fix(filesystem): use summary projection for default semantic search Route default semantic search to the summary projection when summary is the only populated semantic channel. --- pageindex/filesystem/hybrid_projection.py | 2 + tests/test_pageindex_filesystem_scope.py | 65 +++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/pageindex/filesystem/hybrid_projection.py b/pageindex/filesystem/hybrid_projection.py index e802ab38c..b49d49afa 100644 --- a/pageindex/filesystem/hybrid_projection.py +++ b/pageindex/filesystem/hybrid_projection.py @@ -130,6 +130,8 @@ def search( if self._channel_document_count(channel) > 0 ) if not channels: + if self._channel_document_count("summary") > 0: + return self.search_channel("summary", query, limit=limit, filters=filters) return [] channel_hits = self._search_channels( query=query, diff --git a/tests/test_pageindex_filesystem_scope.py b/tests/test_pageindex_filesystem_scope.py index b421714ab..b5e89fd31 100644 --- a/tests/test_pageindex_filesystem_scope.py +++ b/tests/test_pageindex_filesystem_scope.py @@ -115,3 +115,68 @@ def fake_configure(index_dir_arg, **kwargs): ) ] assert filesystem.semantic_retrieval_channels() == ("summary",) + + +def test_default_semantic_search_uses_summary_projection_when_only_summary_available(tmp_path): + from pageindex.filesystem import PageIndexFileSystem + from pageindex.filesystem.hybrid_projection import HybridProjectionSearchBackend + from pageindex.filesystem.metadata_generation import MetadataGenerationResult + from pageindex.filesystem.projection_indexing import SummaryProjectionIndexer + + class FixedEmbedder: + def embed(self, texts): + return [[1.0, 0.0, 0.0] for _ in texts] + + class SummaryGenerator: + def generate(self, document, *, fields): + return MetadataGenerationResult( + values={"summary": "vendor renewal risk matrix"} + ) + + source = tmp_path / "source.txt" + source.write_text("ordinary fixture body", encoding="utf-8") + index_dir = tmp_path / "workspace" / "artifacts" / "projection_indexes" + indexer = SummaryProjectionIndexer( + index_dir, + embedder=FixedEmbedder(), + embedding_provider="test", + embedding_model="fake", + embedding_dimensions=3, + ) + backend = HybridProjectionSearchBackend( + index_dir, + embedder=FixedEmbedder(), + embedding_provider="test", + embedding_model="fake", + embedding_dimensions=3, + ) + filesystem = PageIndexFileSystem( + workspace=tmp_path / "workspace", + metadata_generator=SummaryGenerator(), + summary_projection_indexer=indexer, + semantic_retrieval_backend=backend, + ) + filesystem.register_file( + storage_uri=source.as_uri(), + source_path="docs/source.txt", + folder_path="/documents", + external_id="doc_summary_only", + title="Operations note", + content=source.read_text(encoding="utf-8"), + metadata={"department": "ops"}, + metadata_policy={ + "fields": { + "summary": True, + "doc_type": False, + "domain": False, + "topic": False, + } + }, + ) + + assert filesystem.search("purchase order exposure", semantic=False) == [] + + results = filesystem.search("purchase order exposure", semantic=True) + + assert [result.external_id for result in results] == ["doc_summary_only"] + assert results[0].snippet == "summary_vector rank=1" From 229745310300eef8b8725c6051816228b344f423 Mon Sep 17 00:00:00 2001 From: Bukely_ Date: Tue, 26 May 2026 20:27:58 +0800 Subject: [PATCH 29/50] fix(filesystem): avoid replaying failed agent runs Only use the fresh event loop fallback for missing running-loop detection, so RuntimeError from a threaded agent run is not retried. --- pageindex/filesystem/agent.py | 5 +++-- tests/test_pifs_agent_stream.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/pageindex/filesystem/agent.py b/pageindex/filesystem/agent.py index 4c2691992..0d69c73c9 100644 --- a/pageindex/filesystem/agent.py +++ b/pageindex/filesystem/agent.py @@ -585,11 +585,12 @@ async def _run() -> str: try: asyncio.get_running_loop() - with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: - return pool.submit(asyncio.run, _run()).result() except RuntimeError: return asyncio.run(_run()) + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: + return pool.submit(asyncio.run, _run()).result() + def _run_bash(self, command: str) -> str: started = time.time() ok = True diff --git a/tests/test_pifs_agent_stream.py b/tests/test_pifs_agent_stream.py index 1b6a3786f..0994b945c 100644 --- a/tests/test_pifs_agent_stream.py +++ b/tests/test_pifs_agent_stream.py @@ -1,14 +1,18 @@ import io import os +import threading import unittest +from unittest.mock import patch from types import SimpleNamespace from pydantic import BaseModel, ConfigDict +from pageindex.filesystem import agent as agent_module from pageindex.filesystem.agent import ( AGENT_TOOL_POLICY, AGENT_SYSTEM_PROMPT, BASH_TOOL_DESCRIPTION, + PIFSAgentSession, PIFSAgentStreamObserver, build_agent_model_settings, normalize_agent_stream_mode, @@ -219,6 +223,34 @@ def test_system_prompt_sets_workspace_identity_and_scope(self): self.assertIn("clarify only after a reasonable search", AGENT_SYSTEM_PROMPT) self.assertIn("search for candidate documents before asking", AGENT_TOOL_POLICY) + def test_threaded_runtime_error_is_not_retried_on_fresh_loop(self): + session = object.__new__(PIFSAgentSession) + session.executor = SimpleNamespace(query_context=None) + session.normalized_stream_mode = "off" + session.agent_log = [] + session.max_seconds = None + session.max_turns = 1 + session.session = None + session.agent = object() + + main_thread = threading.get_ident() + run_threads = [] + + def fail_asyncio_run(coro): + coro.close() + run_threads.append(threading.get_ident()) + raise RuntimeError("threaded agent failure") + + with ( + patch.object(agent_module.asyncio, "get_running_loop", return_value=object()), + patch.object(agent_module.asyncio, "run", side_effect=fail_asyncio_run), + ): + with self.assertRaisesRegex(RuntimeError, "threaded agent failure"): + session.run("Question: inspect workspace") + + self.assertEqual(len(run_threads), 1) + self.assertNotEqual(run_threads[0], main_thread) + if __name__ == "__main__": unittest.main() From cb9db0bab9c4e5bfcf77c96e980836b491d88406 Mon Sep 17 00:00:00 2001 From: Bukely_ Date: Tue, 26 May 2026 20:28:11 +0800 Subject: [PATCH 30/50] fix(filesystem): guard summary index dimensions Raise on summary projection dimension mismatch instead of resetting an existing index. --- pageindex/filesystem/projection_indexing.py | 23 ++++++---- tests/test_semantic_index.py | 47 +++++++++++++++++++++ 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/pageindex/filesystem/projection_indexing.py b/pageindex/filesystem/projection_indexing.py index ed77465ef..e5d7b829e 100644 --- a/pageindex/filesystem/projection_indexing.py +++ b/pageindex/filesystem/projection_indexing.py @@ -110,15 +110,20 @@ def _ensure_index(self) -> None: ) return try: - if self.index.dimension() != self.embedding_dimensions: - self.index.reset( - dimension=self.embedding_dimensions, - metadata=self._index_metadata(), - ) - except Exception: - self.index.reset( - dimension=self.embedding_dimensions, - metadata=self._index_metadata(), + existing_dimension = self.index.dimension() + except Exception as exc: + raise RuntimeError( + "could not validate existing summary projection index config; " + f"refusing to reset {self.index.db_path}. Move the existing index " + "aside or rebuild it intentionally before changing embedding config." + ) from exc + if existing_dimension != self.embedding_dimensions: + raise RuntimeError( + "summary projection index dimension mismatch: " + f"{self.index.db_path} was built with dimension {existing_dimension}, " + f"but configured embedding_dimensions is {self.embedding_dimensions}. " + "Use the matching embedding config, or rebuild the projection index " + "at a new path after preserving the existing data." ) def _index_metadata(self) -> dict[str, Any]: diff --git a/tests/test_semantic_index.py b/tests/test_semantic_index.py index 4bd9085d0..324ead76f 100644 --- a/tests/test_semantic_index.py +++ b/tests/test_semantic_index.py @@ -91,6 +91,53 @@ def embed(self, texts): assert hits[0].metadata["department"] == "ops" +def test_summary_projection_dimension_mismatch_preserves_existing_index(tmp_path): + from pageindex.filesystem.projection_indexing import SummaryProjectionIndexer + + class FakeEmbedder: + def embed(self, texts): + return [[1.0, 0.0, 0.0, 0.0] for _ in texts] + + index_dir = tmp_path / "projection" + index = SQLiteVecSemanticIndex(index_dir / "summary_only_vector.sqlite") + index.reset( + dimension=3, + metadata={ + "channel": "summary", + "embedding_provider": "test", + "embedding_model": "fake", + "embedding_dimensions": 3, + }, + ) + index.upsert_many( + [ + SemanticIndexRecord( + file_ref="file_a", + external_id="doc_a", + source_type="documents", + source_path="docs/a.pdf", + title="A", + text="summary", + vector=[1.0, 0.0, 0.0], + ) + ] + ) + + with pytest.raises(RuntimeError, match="summary projection index dimension mismatch"): + SummaryProjectionIndexer( + index_dir, + embedder=FakeEmbedder(), + embedding_provider="test", + embedding_model="fake", + embedding_dimensions=4, + ) + + preserved = SQLiteVecSemanticIndex(index.db_path) + assert preserved.info()["dimension"] == 3 + assert preserved.info()["document_count"] == 1 + assert preserved.search([1.0, 0.0, 0.0], limit=1)[0].external_id == "doc_a" + + def test_hash_embedding_provider_is_not_available(): from pageindex.filesystem.hybrid_projection import make_embedder From 9734bf6914eaf16a33142908c318593a6210284b Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 20:27:40 +0800 Subject: [PATCH 31/50] fix(filesystem): enforce pifs shell command limits --- pageindex/filesystem/agent.py | 26 +- pageindex/filesystem/commands.py | 471 +++++++++++++++++++++-- pageindex/filesystem/core.py | 25 +- pageindex/filesystem/structural_read.py | 37 ++ tests/test_pageindex_filesystem_scope.py | 28 ++ tests/test_pageindex_structural_read.py | 90 ++++- tests/test_pifs_agent_stream.py | 8 +- tests/test_pifs_find_maxdepth.py | 124 ++++++ 8 files changed, 780 insertions(+), 29 deletions(-) diff --git a/pageindex/filesystem/agent.py b/pageindex/filesystem/agent.py index 0d69c73c9..4f796ec26 100644 --- a/pageindex/filesystem/agent.py +++ b/pageindex/filesystem/agent.py @@ -43,6 +43,10 @@ file, treat it as a retrieval task. Use available PIFS discovery commands to look for relevant files and inspect evidence before answering. Ask the user to clarify only after a reasonable search cannot identify relevant evidence. +Do not conclude that no relevant document exists from one failed grep. If grep +returns no matches for a workspace topic, verify with available semantic +candidate discovery such as search-summary, or inspect likely document +structure, before saying that the workspace lacks evidence. Follow the task prompt for command policy, retrieval strategy, and answer format. If the caller needs stricter behavior, pass an explicit system_prompt. @@ -53,10 +57,15 @@ operating-system shell. By default the tool is read-only: use ls, tree, find, grep, cat, stat, head, tail, sed, and any dynamically available semantic search commands described in the workspace context. grep -R is lexical evidence search; +grep does not support regex alternation such as "a|b"; run multiple grep +commands or use search-summary for semantic candidate discovery instead. semantic search commands such as search-summary return candidate documents and do not guarantee literal text matches. Use search-summary when the user asks for summary search, semantic search, or vector search and the command is listed as -available. Errors are returned as text prefixed with ERROR. Do not call +available. Quote multi-word semantic queries, for example: +search-summary "Federal Reserve" /documents. Do not write +search-summary Federal Reserve /documents. Errors are returned as text prefixed +with ERROR. Do not call commands that are not listed as available. When evidence is required, inspect it with cat or grep before answering. Prefer shell-like target-first cat syntax with stable targets: cat --structure, cat --page 31-59, and @@ -64,8 +73,14 @@ ambiguous. After structure identifies a relevant section node, prefer cat --node ; use cat --page when the user asks for page-level evidence, no suitable node exists, or exact page text is needed. +cat --structure is paginated; request more with --offset if needed. Page +reads are limited to three pages at once, node reads to at most five node ids, +and text cat --all returns only the first page of text lines. For questions about metadata fields, available summaries, or whether metadata was provided, inspect stat --schema and stat before making claims. +Do not use stat as a general content/topic discovery step. For document Q&A, +prefer search-summary/find/grep for candidates, then cat --structure and +cat --node or cat --page for evidence. """ AGENT_TOOL_POLICY = """ @@ -76,12 +91,19 @@ - Folder paths such as /documents are positional command targets; never put folder paths in --where. - Use --where only with metadata fields shown by stat --schema. - grep -R performs lexical evidence search. +- grep does not support regex alternation such as "a|b"; run separate grep commands or use search-summary for semantic candidate discovery. - Semantic search commands are candidate-discovery tools and do not guarantee literal text matches. -- If search-summary is available and the user asks for summary search, semantic search, vector search, or "用 summary 搜", use search-summary ; do not translate that request into find --where. +- A single failed grep is not enough evidence to say there is no relevant document. If grep returns no matches for a workspace-topic question, verify with search-summary or another available semantic/vector candidate command, or inspect likely document structure, before answering no-evidence. +- If search-summary is available and the user asks for summary search, semantic search, vector search, or "用 summary 搜", use search-summary "" ; quote multi-word queries, for example search-summary "Federal Reserve" /documents; do not translate that request into find --where. - Tool errors are returned as ERROR text; recover by trying an available command. - Use cat or grep to gather evidence before making source-backed claims. - For broad topic, method, or "what solution" questions that are likely about the workspace, search for candidate documents before asking the user to choose a document. +- Use stat only for metadata/schema/status questions or to resolve ambiguous target identity. Do not run stat merely to understand what a document says. - Prefer target-first cat syntax with stable targets: cat --structure, cat --page 31-59, cat --node . +- cat --structure returns at most 25 nodes; use --offset and --limit for more structure pages. +- cat --page accepts at most 3 pages at once. If a larger range is needed, first inspect cat --structure and then read a smaller page range or node. +- cat --node accepts at most 5 node ids at once. Prefer one relevant node when possible. +- cat --all returns at most 100 text lines; use cat --range - for the next page. - After cat --structure finds a relevant section/subsection with a node_id, prefer cat --node for content from that semantic unit. - Use cat --page - when the user explicitly asks for pages/page ranges, when no suitable node_id exists, or when you need exact page text to verify page-level evidence. - Avoid fetching a broad page span after a matching node is available unless page-level citation or verification is required. diff --git a/pageindex/filesystem/commands.py b/pageindex/filesystem/commands.py index fc2f47c0a..c6394b97a 100644 --- a/pageindex/filesystem/commands.py +++ b/pageindex/filesystem/commands.py @@ -46,6 +46,20 @@ class PIFSCommandExecutor: "search-relation": "_cmd_search_relation", "semantic-grep": "_cmd_semantic_grep", } + MAX_CHAINED_COMMANDS = 3 + MAX_PIPE_COMMANDS = 3 + MAX_LS_LIMIT = 100 + MAX_TREE_LIMIT = 200 + MAX_FIND_LIMIT = 50 + MAX_GREP_LIMIT = 20 + MAX_SEMANTIC_LIMIT = 20 + MAX_TEXT_LINES = 100 + MAX_PAGE_SPAN = 3 + MAX_STRUCTURE_NODES = 25 + MAX_NODE_IDS = 5 + MAX_NODE_TEXT_LINES = 100 + MAX_NODE_TEXT_CHARS = 12_000 + MAX_STAT_FIELD_TARGETS = 20 MAX_TREE_DEPTH = 4 MAX_LS_RENDER_FILES = 25 MAX_STAT_METADATA_FIELDS = 8 @@ -92,8 +106,11 @@ def describe_available_command_surfaces(self) -> str: "- find --where: exact/canonical metadata DSL filtering using stat --schema fields only", "- find -maxdepth N -type f|d: bounded folder traversal for find", "- grep -R: recursive lexical/FTS search only; semantic vector prefilter is disabled", - "- cat --structure/--node/--page: cached PageIndex reads for PDF/Markdown files", - "- cat --all: full text artifact reads for txt/text files", + "- cat --structure: cached PageIndex node list, paginated at 25 nodes", + "- cat --page: cached PageIndex page reads, limited to 3 pages", + "- cat --node: cached PageIndex node reads, limited to 5 node ids", + "- cat --all: text artifact reads for txt/text files, paginated at 100 lines", + "- stat --field : one metadata field across up to 20 documents", ] if "entity" in semantic_channels: lines.append("- find --name: entity semantic candidate discovery alias") @@ -123,6 +140,12 @@ def execute(self, command: str) -> str: if not command.strip(): raise PIFSCommandError("Empty command") commands = self._split_chained_commands(command) + if len(commands) > self.MAX_CHAINED_COMMANDS: + raise PIFSCommandError( + f"Command chain supports at most {self.MAX_CHAINED_COMMANDS} commands. " + "Run fewer commands or narrow the request first; if you are unsure where " + "to inspect, use cat --structure." + ) if len(commands) > 1: return "\n".join(self._execute_pipeline(part) for part in commands) return self._execute_pipeline(commands[0]) @@ -133,6 +156,12 @@ def execute(self, command: str) -> str: def _execute_pipeline(self, command: str) -> str: commands = self._split_piped_commands(command) + if len(commands) > self.MAX_PIPE_COMMANDS: + raise PIFSCommandError( + f"Pipeline supports at most {self.MAX_PIPE_COMMANDS} commands. " + "Use a smaller command and explicit limits; if you are unsure where " + "to inspect, use cat --structure." + ) output = self._execute_single(commands[0]) for pipe_command in commands[1:]: output = self._execute_pipe_filter(output, pipe_command) @@ -170,7 +199,13 @@ def _execute_pipe_filter(self, input_text: str, command: str) -> str: self._validate_tokens(tokens) name = tokens[0] if name not in self.ALLOWED_PIPE_FILTERS: - raise PIFSCommandError(f"Unsupported pipe command: {name}") + raise PIFSCommandError( + f"Unsupported pipe command: {name}. Supported pipes are: " + f"{', '.join(sorted(self.ALLOWED_PIPE_FILTERS))}. " + "If you meant regex alternation such as a|b, PIFS grep/search " + "does not support it; run multiple grep or search-summary " + "commands with one phrase each." + ) if name == "head": return self._pipe_head_tail(input_text, tokens[1:], from_tail=False) if name == "tail": @@ -183,7 +218,7 @@ def _execute_pipe_filter(self, input_text: str, command: str) -> str: def _cmd_ls(self, args: list[str]) -> Any: recursive = False - limit = 100 + limit = self.MAX_LS_LIMIT path = "/" i = 0 while i < len(args): @@ -192,7 +227,9 @@ def _cmd_ls(self, args: list[str]) -> Any: recursive = True elif arg == "--limit": i += 1 - limit = int(args[i]) + limit = self._parse_bounded_int( + args[i], "ls --limit", max_value=self.MAX_LS_LIMIT + ) elif arg.startswith("-"): raise PIFSCommandError(f"Unsupported ls option: {arg}") else: @@ -202,17 +239,19 @@ def _cmd_ls(self, args: list[str]) -> Any: def _cmd_tree(self, args: list[str]) -> Any: path = "/" - limit = 1000 + limit = self.MAX_TREE_LIMIT depth = 2 i = 0 while i < len(args): arg = args[i] if arg == "--limit": i += 1 - limit = int(args[i]) + limit = self._parse_bounded_int( + args[i], "tree --limit", max_value=self.MAX_TREE_LIMIT + ) elif arg in {"--depth", "-L"}: i += 1 - depth = int(args[i]) + depth = self._parse_non_negative_int(args[i], "tree --depth") elif arg.startswith("-"): raise PIFSCommandError(f"Unsupported tree option: {arg}") else: @@ -247,7 +286,9 @@ def _cmd_find(self, args: list[str]) -> Any: relation = args[i] elif arg == "--limit": i += 1 - limit = int(args[i]) + limit = self._parse_bounded_int( + args[i], "find --limit", max_value=self.MAX_FIND_LIMIT + ) elif arg == "-type": i += 1 file_type = args[i] @@ -332,7 +373,9 @@ def _cmd_grep(self, args: list[str]) -> Any: where = args[i] elif arg == "--limit": i += 1 - limit = int(args[i]) + limit = self._parse_bounded_int( + args[i], "grep --limit", max_value=self.MAX_GREP_LIMIT + ) elif arg.startswith("-"): raise PIFSCommandError(f"Unsupported grep option: {arg}") else: @@ -341,6 +384,7 @@ def _cmd_grep(self, args: list[str]) -> Any: if not positionals: raise PIFSCommandError("grep requires a query") query = positionals[0] + self._reject_regex_alternation_query(query, "grep") path = positionals[1] if len(positionals) > 1 else "/" if self._is_folder(path): normalized = self._normalize_folder_path(path) @@ -438,8 +482,10 @@ def _cmd_cat(self, args: list[str]) -> Any: ) location = "all" structural_mode: str | None = None - node_id: str | None = None + node_ids: list[str] = [] page_range: str | None = None + structure_offset = 0 + structure_limit = self.MAX_STRUCTURE_NODES i = 1 while i < len(args): arg = args[i] @@ -452,12 +498,26 @@ def _cmd_cat(self, args: list[str]) -> Any: location = "all" elif arg == "--structure": structural_mode = "structure" + elif arg == "--offset": + i += 1 + if i >= len(args): + raise PIFSCommandError("cat --structure --offset requires a value") + structure_offset = self._parse_non_negative_int(args[i], "cat --structure --offset") + elif arg == "--limit": + i += 1 + if i >= len(args): + raise PIFSCommandError("cat --structure --limit requires a value") + structure_limit = self._parse_bounded_int( + args[i], + "cat --structure --limit", + max_value=self.MAX_STRUCTURE_NODES, + ) elif arg == "--node": i += 1 if i >= len(args): raise PIFSCommandError("cat --node requires a node id") structural_mode = "node" - node_id = args[i] + node_ids.extend(self._parse_node_ids(args[i])) elif arg == "--page": i += 1 if i >= len(args): @@ -473,27 +533,121 @@ def _cmd_cat(self, args: list[str]) -> Any: ) i += 1 if structural_mode == "structure": - return self.filesystem.pageindex_structure(target) + if structure_limit < 1: + raise PIFSCommandError( + "cat --structure --limit must be at least 1 and at most " + f"{self.MAX_STRUCTURE_NODES}." + ) + data = self.filesystem.pageindex_structure( + target, + offset=structure_offset, + limit=structure_limit, + ) + self._attach_structure_next_command(data, target) + return data if structural_mode == "node": - return self.filesystem.pageindex_node(target, str(node_id)) + self._require_at_most( + len(node_ids), + "cat --node node count", + self.MAX_NODE_IDS, + ) + if not node_ids: + raise PIFSCommandError("cat --node requires a node id") + node_results = [ + self._bounded_node_result( + self.filesystem.pageindex_node(target, node_id), + target=target, + node_id=node_id, + ) + for node_id in node_ids + ] + if len(node_results) == 1: + return node_results[0] + return { + "mode": "nodes", + "target": target, + "available": all(result.get("available") is not False for result in node_results), + "node_ids": node_ids, + "nodes": node_results, + "text": "\n\n".join( + f"[node {result.get('node_id') or node_id}]\n{result.get('text', '')}" + for node_id, result in zip(node_ids, node_results) + ), + } if structural_mode == "page": if not page_range or not re.fullmatch(r"\d+(?:-\d+)?", page_range): raise PIFSCommandError( "cat --page requires one page selector like 31 or 31-59. " "Use: cat --page " ) - return self.filesystem.pageindex_pages(target, page_range) - return self.filesystem.cat_text_artifact(target, location) + start, end = self._parse_numeric_range(page_range, "cat --page") + self._require_at_most( + end - start + 1, + "cat --page page count", + self.MAX_PAGE_SPAN, + ) + data = self.filesystem.pageindex_pages(target, page_range) + self._attach_page_next_command(data, target, start=start, end=end) + return data + return self._bounded_text_artifact(target, location) def _cmd_stat(self, args: list[str]) -> Any: - if args and args[0] == "--schema": + schema = False + field: str | None = None + targets: list[str] = [] + i = 0 + while i < len(args): + arg = args[i] + if arg == "--schema": + schema = True + elif arg == "--field": + i += 1 + if i >= len(args): + raise PIFSCommandError("stat --field requires a metadata field name") + field = args[i] + elif arg.startswith("-"): + raise PIFSCommandError(f"Unsupported stat option: {arg}") + else: + targets.append(arg) + i += 1 + if schema: + if field or targets: + raise PIFSCommandError("stat --schema cannot be combined with file targets or --field") return self.filesystem._metadata_schema() - if not args: + if field: + if not targets: + raise PIFSCommandError("stat --field requires at least one file target") + self._require_at_most( + len(targets), + "stat --field target count", + self.MAX_STAT_FIELD_TARGETS, + ) + self._validate_metadata_field_for_stat(field) + return { + "mode": "field_values", + "field": field, + "target_count": len(targets), + "max_targets": self.MAX_STAT_FIELD_TARGETS, + "data": [self._stat_field_row(field, target) for target in targets], + } + if not targets: raise PIFSCommandError("stat requires a file target or --schema") - return {"target": args[0], **self.filesystem._stat(args[0])} + self._require_at_most( + len(targets), + "stat target count", + self.MAX_STAT_FIELD_TARGETS, + ) + if len(targets) == 1: + return {"target": targets[0], **self.filesystem._stat(targets[0])} + return { + "mode": "files", + "target_count": len(targets), + "data": [{"target": target, **self.filesystem._stat(target)} for target in targets], + } def _cmd_head(self, args: list[str]) -> Any: count, target = self._parse_standalone_head_tail(args, default_count=10) + count = self._require_at_most(count, "head line count", self.MAX_TEXT_LINES) opened = self.filesystem.cat_text_artifact(target, "all") lines = opened.text.splitlines() text = "\n".join(lines[:count]) @@ -501,6 +655,7 @@ def _cmd_head(self, args: list[str]) -> Any: def _cmd_tail(self, args: list[str]) -> Any: count, target = self._parse_standalone_head_tail(args, default_count=10) + count = self._require_at_most(count, "tail line count", self.MAX_TEXT_LINES) opened = self.filesystem.cat_text_artifact(target, "all") lines = opened.text.splitlines() selected = lines[-count:] if count else [] @@ -518,9 +673,13 @@ def _cmd_sed(self, args: list[str]) -> Any: match = re.fullmatch(r"(\d+),(\d+)p", args[1]) if not match: raise PIFSCommandError("sed supports only: sed -n ',p' ") + start, end = int(match.group(1)), int(match.group(2)) + if start < 1 or end < start: + raise PIFSCommandError("Invalid sed line range") + self._require_at_most(end - start + 1, "sed line count", self.MAX_TEXT_LINES) return self.filesystem.cat_text_artifact( args[2], - f"{match.group(1)}-{match.group(2)}", + f"{start}-{end}", ) def _cmd_search_summary(self, args: list[str]) -> Any: @@ -551,7 +710,9 @@ def _cmd_semantic_grep(self, args: list[str]) -> Any: where = args[i] elif arg == "--limit": i += 1 - limit = int(args[i]) + limit = self._parse_bounded_int( + args[i], "semantic-grep --limit", max_value=self.MAX_SEMANTIC_LIMIT + ) elif arg.startswith("-"): raise PIFSCommandError(f"Unsupported semantic-grep option: {arg}") else: @@ -566,7 +727,9 @@ def _cmd_semantic_grep(self, args: list[str]) -> Any: ) if not positionals: raise PIFSCommandError("semantic-grep requires a query") + self._validate_search_positionals("semantic-grep", positionals) query = positionals[0] + self._reject_regex_alternation_query(query, "semantic-grep") path = positionals[1] if len(positionals) > 1 else "/" if not self._is_folder(path): raise PIFSCommandError("semantic-grep target must be a folder") @@ -594,7 +757,11 @@ def _cmd_semantic_channel(self, channel: str, args: list[str]) -> Any: where = args[i] elif arg == "--limit": i += 1 - limit = int(args[i]) + limit = self._parse_bounded_int( + args[i], + f"search-{channel} --limit", + max_value=self.MAX_SEMANTIC_LIMIT, + ) elif arg.startswith("-"): raise PIFSCommandError(f"Unsupported search-{channel} option: {arg}") else: @@ -602,7 +769,9 @@ def _cmd_semantic_channel(self, channel: str, args: list[str]) -> Any: i += 1 if not positionals: raise PIFSCommandError(f"search-{channel} requires a query") + self._validate_search_positionals(f"search-{channel}", positionals) query = positionals[0] + self._reject_regex_alternation_query(query, f"search-{channel}") path = positionals[1] if len(positionals) > 1 else "/" normalized = self._normalize_folder_path(path) results = self.filesystem.search_semantic_channel( @@ -679,6 +848,214 @@ def _semantic_grep_channels(self) -> tuple[str, ...]: available = set(self.filesystem.semantic_retrieval_channels()) return tuple(channel for channel in SEMANTIC_GREP_CHANNELS if channel in available) + def _bounded_text_artifact(self, target: str, location: str) -> dict[str, Any]: + if str(location).strip().lower() in {"all", "full", "*"}: + start, end = 1, self.MAX_TEXT_LINES + else: + start, end = self._parse_numeric_range(location, "cat --range") + self._require_at_most( + end - start + 1, + "cat --range line count", + self.MAX_TEXT_LINES, + ) + opened = self.filesystem.cat_text_artifact(target, f"{start}-{end}") + data = self._jsonable(opened) + total_lines = len(self.filesystem.store.read_text(opened.file_ref).splitlines()) + has_more = int(data.get("end_line") or end) < total_lines + pagination = { + "offset_line": start, + "limit": self.MAX_TEXT_LINES, + "returned_lines": max(0, int(data.get("end_line") or end) - start + 1), + "total_lines": total_lines, + "has_more": has_more, + "next_range": None, + "next_command": None, + } + if has_more: + next_start = int(data.get("end_line") or end) + 1 + next_end = min(total_lines, next_start + self.MAX_TEXT_LINES - 1) + next_range = f"{next_start}-{next_end}" + pagination["next_range"] = next_range + pagination["next_command"] = ( + f"cat {shlex.quote(target)} --range {shlex.quote(next_range)}" + ) + data["text"] = ( + str(data.get("text") or "").rstrip() + + "\n" + + self._pagination_footer( + "cat --all", + f"showing lines {start}-{data.get('end_line')} of {total_lines}", + str(pagination["next_command"]), + ) + ).strip() + data["pagination"] = pagination + return data + + def _bounded_node_result( + self, + data: dict[str, Any], + *, + target: str, + node_id: str, + ) -> dict[str, Any]: + if not isinstance(data, dict) or data.get("available") is False: + return data + text = str(data.get("text") or "") + lines = text.splitlines() + truncated_by_lines = len(lines) > self.MAX_NODE_TEXT_LINES + truncated_by_chars = len(text) > self.MAX_NODE_TEXT_CHARS + if not truncated_by_lines and not truncated_by_chars: + data["node_pagination"] = { + "limit_nodes": self.MAX_NODE_IDS, + "text_truncated": False, + } + return data + + selected = "\n".join(lines[: self.MAX_NODE_TEXT_LINES]) + if len(selected) > self.MAX_NODE_TEXT_CHARS: + selected = selected[: self.MAX_NODE_TEXT_CHARS].rstrip() + data["text"] = ( + selected.rstrip() + + "\n" + + self._pagination_footer( + "cat --node", + ( + f"node text limited to {self.MAX_NODE_TEXT_LINES} lines/" + f"{self.MAX_NODE_TEXT_CHARS} chars" + ), + f"cat {shlex.quote(target)} --structure", + ) + ).strip() + data["node_pagination"] = { + "limit_nodes": self.MAX_NODE_IDS, + "line_limit": self.MAX_NODE_TEXT_LINES, + "char_limit": self.MAX_NODE_TEXT_CHARS, + "original_lines": len(lines), + "original_chars": len(text), + "text_truncated": True, + "suggested_command": f"cat {shlex.quote(target)} --structure", + "node_id": node_id, + } + return data + + def _attach_structure_next_command(self, data: dict[str, Any], target: str) -> None: + pagination = data.get("structure_pagination") + if not isinstance(pagination, dict): + return + if pagination.get("has_more") and pagination.get("next_offset") is not None: + next_command = ( + f"cat {shlex.quote(target)} --structure " + f"--offset {pagination['next_offset']} --limit {pagination['limit']}" + ) + pagination["next_command"] = next_command + else: + pagination["next_command"] = None + + def _attach_page_next_command( + self, + data: dict[str, Any], + target: str, + *, + start: int, + end: int, + ) -> None: + page_count = end - start + 1 + next_command = None + if page_count == self.MAX_PAGE_SPAN: + next_start = end + 1 + next_end = next_start + self.MAX_PAGE_SPAN - 1 + next_command = f"cat {shlex.quote(target)} --page {next_start}-{next_end}" + data["page_pagination"] = { + "start": start, + "end": end, + "returned_pages": page_count, + "limit": self.MAX_PAGE_SPAN, + "next_command": next_command, + } + + @staticmethod + def _pagination_footer(command: str, reason: str, next_command: str) -> str: + return ( + f"# output limited by {command}: {reason}. " + f"Next: {next_command}. If unsure, use cat --structure." + ) + + @staticmethod + def _parse_node_ids(value: str) -> list[str]: + return [part.strip() for part in value.split(",") if part.strip()] + + @staticmethod + def _reject_regex_alternation_query(query: str, command_name: str) -> None: + if "|" not in str(query): + return + raise PIFSCommandError( + f"{command_name} does not support regex alternation '|'. " + "Run multiple grep commands or multiple search-summary commands " + "with one phrase each." + ) + + @staticmethod + def _validate_search_positionals(command_name: str, positionals: list[str]) -> None: + if len(positionals) > 2: + raise PIFSCommandError( + f"{command_name} accepts one query and an optional folder path. " + f"Quote multi-word queries, for example: {command_name} " + '"Federal Reserve" /documents' + ) + if len(positionals) == 2 and not positionals[1].startswith("/"): + raise PIFSCommandError( + f"{command_name} target must be a PIFS folder path like /documents. " + f"If your query has spaces, quote it, for example: {command_name} " + '"Federal Reserve" /documents' + ) + + @staticmethod + def _parse_numeric_range(value: str, label: str) -> tuple[int, int]: + try: + if "-" in value: + left, right = value.split("-", 1) + start, end = int(left), int(right) + else: + start = end = int(value) + except ValueError as exc: + raise PIFSCommandError(f"{label} requires a numeric range") from exc + if start < 1 or end < start: + raise PIFSCommandError(f"Invalid {label} range: {value}") + return start, end + + def _validate_metadata_field_for_stat(self, field: str) -> None: + schema = self.filesystem._metadata_schema() + fields = schema.get("fields", {}) + if field not in fields: + available = ", ".join(sorted(fields)[:20]) or "(none)" + raise PIFSCommandError( + f"Unknown metadata field: {field}. Use stat --schema to inspect fields. " + f"Available fields include: {available}" + ) + + def _stat_field_row(self, field: str, target: str) -> dict[str, Any]: + info = self.filesystem._stat(target) + folder_paths = [ + folder.get("path", "") + for folder in info.get("folders", []) + if folder.get("path") + ] + row = dict(info) + row["target"] = target + row["folder_paths"] = folder_paths + metadata = info.get("metadata") or {} + raw_value = metadata.get(field) + value_text = "" if raw_value is None else str(raw_value) + row.update( + { + "field": field, + "present": field in metadata, + "value": raw_value if field in metadata else None, + "display_target": self._file_target_path(row), + } + ) + return row + def _render(self, data: Any, *, json_output: bool, command_name: str) -> str: jsonable = self._jsonable(data) if json_output: @@ -714,7 +1091,14 @@ def _render_cat(self, data: Any) -> str: if data.get("available") is False: return f"# {data.get('message', 'PageIndex structural content is unavailable')}" if data.get("mode") == "structure": - return json.dumps(data.get("structure", {}), ensure_ascii=False, indent=2) + return json.dumps( + { + "structure": data.get("structure", []), + "pagination": data.get("structure_pagination", {}), + }, + ensure_ascii=False, + indent=2, + ) return str(data.get("text", "")) def _render_listing(self, data: Any) -> str: @@ -839,6 +1223,19 @@ def _render_stat(self, data: Any) -> str: for name, field in sorted(data["fields"].items()): lines.append(f"{name}: {field.get('type', 'string')}") return "\n".join(lines) + if data.get("mode") == "field_values": + field = data.get("field", "") + lines = [] + for item in data.get("data", []): + lines.append(f"{item.get('display_target') or item.get('target')}:") + value = item.get("value") + if value is None: + lines.append(f"{field}: -") + else: + lines.append(f"{field}: {self._one_line_value(value)}") + return "\n\n".join(lines) + if data.get("mode") == "files": + return "\n\n".join(self._render_stat(item) for item in data.get("data", [])) lines = [ f"target: {data.get('target') or data.get('file_ref')}", f"file_ref: {data.get('file_ref')}", @@ -1298,6 +1695,12 @@ def _compact_value(cls, value: Any) -> str: return cls._compact_text(json.dumps(value, ensure_ascii=False, sort_keys=True), max_chars=120) return cls._compact_text(str(value), max_chars=120) + @staticmethod + def _one_line_value(value: Any) -> str: + if isinstance(value, (dict, list)): + value = json.dumps(value, ensure_ascii=False, sort_keys=True) + return re.sub(r"\s+", " ", str(value or "")).strip() + @staticmethod def _compact_text(text: str, *, max_chars: int) -> str: collapsed = re.sub(r"\s+", " ", text or "").strip() @@ -1399,6 +1802,11 @@ def _split_unquoted_operator( def _pipe_head_tail(self, input_text: str, args: list[str], *, from_tail: bool) -> str: count = self._parse_head_tail_count(args) + count = self._require_at_most( + count, + "pipe head/tail line count", + self.MAX_TEXT_LINES, + ) payload = self._try_json_loads(input_text) if payload is not None: return self._render_json_payload(self._slice_payload(payload, count, from_tail=from_tail)) @@ -1425,6 +1833,7 @@ def _pipe_grep(self, input_text: str, args: list[str]) -> str: if len(patterns) != 1: raise PIFSCommandError("pipe grep requires exactly one pattern") pattern = patterns[0] + self._reject_regex_alternation_query(pattern, "pipe grep") payload = self._try_json_loads(input_text) if payload is not None: return self._render_json_payload( @@ -1457,6 +1866,7 @@ def _pipe_sed(self, input_text: str, args: list[str]) -> str: end = int(match.group(2) or match.group(1)) if start < 1 or end < start: raise PIFSCommandError("Invalid sed line range") + self._require_at_most(end - start + 1, "pipe sed line count", self.MAX_TEXT_LINES) payload = self._try_json_loads(input_text) if payload is not None: return self._render_json_payload(self._slice_text_payload(payload, start, end)) @@ -1516,6 +1926,21 @@ def _parse_non_negative_int(value: str, label: str) -> int: raise PIFSCommandError(f"{label} must be non-negative") return parsed + @classmethod + def _parse_bounded_int(cls, value: str, label: str, *, max_value: int) -> int: + parsed = cls._parse_non_negative_int(value, label) + return cls._require_at_most(parsed, label, max_value) + + @classmethod + def _require_at_most(cls, value: int, label: str, max_value: int) -> int: + if value > max_value: + raise PIFSCommandError( + f"{label} supports at most {max_value}; requested {value}. " + "Use a smaller value. If you are unsure where to inspect, " + "use cat --structure first." + ) + return value + @staticmethod def _parse_find_maxdepth(value: str | None) -> int: if value is None: diff --git a/pageindex/filesystem/core.py b/pageindex/filesystem/core.py index fc096e3be..dcfb72f3f 100644 --- a/pageindex/filesystem/core.py +++ b/pageindex/filesystem/core.py @@ -32,6 +32,7 @@ normalize_path, ) from .structural_read import ( + flatten_pageindex_structure_nodes, first_node_location, find_pageindex_node, strip_pageindex_text_fields, @@ -621,7 +622,13 @@ def cat_text_artifact(self, target: str, location: str = "all") -> OpenResult: start, end = self._parse_line_range(location) return self._open_lines(file_ref, start, end) - def pageindex_structure(self, target: str) -> dict[str, Any]: + def pageindex_structure( + self, + target: str, + *, + offset: int = 0, + limit: int = 25, + ) -> dict[str, Any]: file_ref = self._resolve_target(target) entry = self.store.get_file(file_ref) self._require_pageindex_document_file(entry, "cat --structure") @@ -642,6 +649,12 @@ def pageindex_structure(self, target: str) -> dict[str, Any]: entry, message=str(structure["error"]), ) + node_rows = flatten_pageindex_structure_nodes(structure) + offset = max(0, offset) + limit = max(0, limit) + window = node_rows[offset : offset + limit] if limit else [] + next_offset = offset + len(window) + has_more = next_offset < len(node_rows) return { "mode": "structure", "file_ref": file_ref, @@ -650,7 +663,15 @@ def pageindex_structure(self, target: str) -> dict[str, Any]: "status": entry.pageindex_tree_status, "available": True, "pageindex_doc_id": doc_id, - "structure": strip_pageindex_text_fields(structure), + "structure": window, + "structure_pagination": { + "offset": offset, + "limit": limit, + "returned_nodes": len(window), + "total_nodes": len(node_rows), + "has_more": has_more, + "next_offset": next_offset if has_more else None, + }, } def pageindex_node(self, target: str, node_id: str) -> dict[str, Any]: diff --git a/pageindex/filesystem/structural_read.py b/pageindex/filesystem/structural_read.py index b61df43b3..aca2bcdcd 100644 --- a/pageindex/filesystem/structural_read.py +++ b/pageindex/filesystem/structural_read.py @@ -16,6 +16,43 @@ def strip_pageindex_text_fields(value: Any) -> Any: return value +def flatten_pageindex_structure_nodes(structure: Any) -> list[dict[str, Any]]: + rows: list[dict[str, Any]] = [] + + def visit(value: Any, *, depth: int, parent_node_id: str | None) -> None: + if isinstance(value, list): + for item in value: + visit(item, depth=depth, parent_node_id=parent_node_id) + return + if not isinstance(value, dict): + return + + node_id = value.get("node_id") + child_values: list[Any] = [] + for child_key in ("nodes", "children"): + children = value.get(child_key) + if isinstance(children, list): + child_values.extend(children) + + row = { + key: strip_pageindex_text_fields(item) + for key, item in value.items() + if key not in {"text", "nodes", "children"} + } + row["depth"] = depth + row["children_count"] = len(child_values) + if parent_node_id: + row["parent_node_id"] = parent_node_id + rows.append(row) + + next_parent = str(node_id) if node_id is not None else parent_node_id + for child in child_values: + visit(child, depth=depth + 1, parent_node_id=next_parent) + + visit(structure, depth=0, parent_node_id=None) + return rows + + def find_pageindex_node(structure: Any, node_id: str) -> dict[str, Any] | None: if isinstance(structure, dict): if str(structure.get("node_id", "")) == str(node_id): diff --git a/tests/test_pageindex_filesystem_scope.py b/tests/test_pageindex_filesystem_scope.py index b5e89fd31..a07c4bb87 100644 --- a/tests/test_pageindex_filesystem_scope.py +++ b/tests/test_pageindex_filesystem_scope.py @@ -1,6 +1,8 @@ import json from types import SimpleNamespace +import pytest + class SummaryBackend: def __init__(self, document_id): @@ -45,6 +47,32 @@ def test_semantic_search_scope_keeps_ordinary_folders_out_of_source_type_filters assert result["data"]["data"][0]["external_id"] == "dsid_report" +def test_semantic_search_rejects_unquoted_multi_word_query(tmp_path): + from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem + from pageindex.filesystem.commands import PIFSCommandError + + filesystem = PageIndexFileSystem(workspace=tmp_path / "workspace") + filesystem.register_file( + storage_uri="file:///tmp/report.pdf", + source_path="examples/documents/report.pdf", + folder_path="/documents", + external_id="dsid_report", + title="Annual report", + content="Federal Reserve supervision and regulation annual report.", + ) + filesystem.semantic_retrieval_backend = SummaryBackend("dsid_report") + executor = PIFSCommandExecutor(filesystem, json_output=True) + + with pytest.raises(PIFSCommandError, match="Quote multi-word queries"): + executor.execute("search-summary Federal Reserve /documents") + + with pytest.raises(PIFSCommandError, match="quote it"): + executor.execute("search-summary Federal Reserve") + + with pytest.raises(PIFSCommandError, match="does not support regex alternation"): + executor.execute('search-summary "Federal|Reserve" /documents') + + def test_semantic_search_scope_filters_explicit_source_type_facets(): from pageindex.filesystem import PageIndexFileSystem diff --git a/tests/test_pageindex_structural_read.py b/tests/test_pageindex_structural_read.py index 5b0629085..d54d9a7d5 100644 --- a/tests/test_pageindex_structural_read.py +++ b/tests/test_pageindex_structural_read.py @@ -341,8 +341,10 @@ def fail_index(*args, **kwargs): assert structure["data"]["available"] is True assert structure["data"]["pageindex_doc_id"] == "doc_cached_pdf" assert structure["data"]["structure"][0]["title"] == "Introduction" + assert structure["data"]["structure"][1]["title"] == "Findings" + assert structure["data"]["structure_pagination"]["limit"] == 25 assert "text" not in structure["data"]["structure"][0] - assert "text" not in structure["data"]["structure"][0]["nodes"][0] + assert "text" not in structure["data"]["structure"][1] assert pages["data"]["available"] is True assert pages["data"]["text"] == "Page one text\n\nPage two text" @@ -401,6 +403,92 @@ def test_cat_node_reads_pageindex_client_structure_without_custom_pifs_artifact( assert "text" not in node["data"]["node"] +def test_cat_structure_page_node_and_text_outputs_are_hard_limited(): + from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem + from pageindex.filesystem.commands import PIFSCommandError + + with tempfile.TemporaryDirectory() as tmp: + source = Path(tmp) / "report.pdf" + source.write_bytes(b"%PDF-1.4\n% test fixture\n") + filesystem = PageIndexFileSystem(workspace=Path(tmp) / "workspace") + structure_nodes = [ + { + "title": f"Section {index}", + "node_id": f"{index:04d}", + "start_index": index, + "end_index": index, + "text": f"node {index} text", + "nodes": [], + } + for index in range(1, 31) + ] + write_pageindex_client_doc( + filesystem.pageindex_client_workspace, + "doc_limited_pdf", + { + "id": "doc_limited_pdf", + "type": "pdf", + "path": str(source.resolve()), + "doc_name": "report.pdf", + "doc_description": "", + "page_count": 10, + "structure": structure_nodes, + "pages": [ + {"page": index, "content": f"Page {index} text"} + for index in range(1, 11) + ], + }, + ) + filesystem.register_file( + storage_uri=source.as_uri(), + source_path="docs/report.pdf", + external_id="dsid_limited_pdf", + title="Limited structural report", + content="text artifact remains available for grep", + ) + text_content = "\n".join(f"line {index}" for index in range(1, 106)) + filesystem.register_file( + storage_uri="file:///tmp/long.txt", + source_path="docs/long.txt", + external_id="dsid_long_text", + title="Long text", + content=text_content, + ) + executor = PIFSCommandExecutor(filesystem, json_output=True) + + first_structure = json.loads(executor.execute("cat dsid_limited_pdf --structure")) + assert len(first_structure["data"]["structure"]) == 25 + assert first_structure["data"]["structure_pagination"]["has_more"] is True + assert first_structure["data"]["structure_pagination"]["next_offset"] == 25 + + second_structure = json.loads( + executor.execute("cat dsid_limited_pdf --structure --offset 25") + ) + assert len(second_structure["data"]["structure"]) == 5 + assert second_structure["data"]["structure"][0]["node_id"] == "0026" + + pages = json.loads(executor.execute("cat dsid_limited_pdf --page 1-3")) + assert pages["data"]["text"] == "Page 1 text\n\nPage 2 text\n\nPage 3 text" + assert pages["data"]["page_pagination"]["limit"] == 3 + with pytest.raises(PIFSCommandError, match="at most 3"): + executor.execute("cat dsid_limited_pdf --page 1-4") + + nodes = json.loads( + executor.execute("cat dsid_limited_pdf --node 0001,0002,0003,0004,0005") + ) + assert nodes["data"]["node_ids"] == ["0001", "0002", "0003", "0004", "0005"] + with pytest.raises(PIFSCommandError, match="at most 5"): + executor.execute("cat dsid_limited_pdf --node 0001,0002,0003,0004,0005,0006") + + text = json.loads(executor.execute("cat dsid_long_text --all")) + assert "line 100" in text["data"]["text"] + assert "line 101" not in text["data"]["text"] + assert text["data"]["pagination"]["has_more"] is True + assert text["data"]["pagination"]["next_range"] == "101-105" + with pytest.raises(PIFSCommandError, match="at most 100"): + executor.execute("cat dsid_long_text --range 1-101") + + def test_tree_folder_behavior_is_preserved(): from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem diff --git a/tests/test_pifs_agent_stream.py b/tests/test_pifs_agent_stream.py index 0994b945c..9fe62c101 100644 --- a/tests/test_pifs_agent_stream.py +++ b/tests/test_pifs_agent_stream.py @@ -207,10 +207,14 @@ def test_prompt_requires_stat_for_metadata_questions(self): self.assertIn("stat --schema and stat ", AGENT_TOOL_POLICY) self.assertIn("do not infer metadata presence or absence", AGENT_TOOL_POLICY) self.assertIn("questions about metadata fields", BASH_TOOL_DESCRIPTION) + self.assertIn("Use stat only for metadata/schema/status questions", AGENT_TOOL_POLICY) + self.assertIn("Do not run stat merely to understand what a document says", AGENT_TOOL_POLICY) + self.assertIn("Do not use stat as a general content/topic discovery step", BASH_TOOL_DESCRIPTION) def test_prompt_routes_summary_search_to_search_summary(self): self.assertIn("search-summary when the user asks for", BASH_TOOL_DESCRIPTION) - self.assertIn("use search-summary ", AGENT_TOOL_POLICY) + self.assertIn('use search-summary "" ', AGENT_TOOL_POLICY) + self.assertIn('search-summary "Federal Reserve" /documents', BASH_TOOL_DESCRIPTION) self.assertIn("do not translate that request into find --where", AGENT_TOOL_POLICY) def test_system_prompt_sets_workspace_identity_and_scope(self): @@ -222,6 +226,8 @@ def test_system_prompt_sets_workspace_identity_and_scope(self): self.assertIn("workspace-related topic question", AGENT_SYSTEM_PROMPT) self.assertIn("clarify only after a reasonable search", AGENT_SYSTEM_PROMPT) self.assertIn("search for candidate documents before asking", AGENT_TOOL_POLICY) + self.assertIn("Do not conclude that no relevant document exists from one failed grep", AGENT_SYSTEM_PROMPT) + self.assertIn("A single failed grep is not enough evidence", AGENT_TOOL_POLICY) def test_threaded_runtime_error_is_not_retried_on_fresh_loop(self): session = object.__new__(PIFSAgentSession) diff --git a/tests/test_pifs_find_maxdepth.py b/tests/test_pifs_find_maxdepth.py index fea78316b..c1afe9145 100644 --- a/tests/test_pifs_find_maxdepth.py +++ b/tests/test_pifs_find_maxdepth.py @@ -98,6 +98,37 @@ def test_stable_path_targets_work_without_session_refs(tmp_path): assert "Root document fixture text" in text +def test_shell_limits_reject_context_expanding_counts(tmp_path): + from pageindex.filesystem.commands import PIFSCommandError + + executor = _register_find_fixture(tmp_path) + + for command, limit in ( + ("find /documents --limit 51", 50), + ("grep --limit 21 Root /documents", 20), + ("ls /documents --limit 101", 100), + ("tree /documents --limit 201", 200), + ("head -n 101 /documents/Root\\ document", 100), + ("tail -n 101 /documents/Root\\ document", 100), + ("sed -n 1,101p /documents/Root\\ document", 100), + ): + with pytest.raises(PIFSCommandError, match=f"at most {limit}"): + executor.execute(command) + + +def test_grep_rejects_regex_alternation_patterns(tmp_path): + from pageindex.filesystem.commands import PIFSCommandError + + executor = _register_find_fixture(tmp_path) + executor.json_output = False + + with pytest.raises(PIFSCommandError, match="does not support regex alternation"): + executor.execute('grep -R "Root|Child" /documents') + + with pytest.raises(PIFSCommandError, match="multiple grep commands"): + executor.execute('find /documents -type f | grep "Root|Child"') + + def test_stat_shell_output_includes_unified_metadata_status(tmp_path): from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem from pageindex.filesystem.metadata_generation import MetadataGenerationResult @@ -142,6 +173,99 @@ def generate(self, document, *, fields): assert "metadata_status: generated" in stat +def test_stat_field_reads_one_metadata_field_across_multiple_targets(tmp_path): + from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem + from pageindex.filesystem.commands import PIFSCommandError + from pageindex.filesystem.metadata_generation import MetadataGenerationResult + + class SummaryGenerator: + def generate(self, document, *, fields): + return MetadataGenerationResult( + values={ + field: ( + f"Summary for {document.title}\n" + + "full summary token " * 80 + ) + for field in fields + } + ) + + filesystem = PageIndexFileSystem( + workspace=tmp_path / "workspace", + metadata_generator=SummaryGenerator(), + ) + for index in range(1, 3): + source = tmp_path / f"source{index}.txt" + source.write_text(f"fixture text {index}", encoding="utf-8") + filesystem.register_file( + storage_uri=source.as_uri(), + source_path=f"docs/source{index}.txt", + folder_path="/documents", + external_id=f"doc_summary_{index}", + title=f"Summary document {index}", + content=source.read_text(encoding="utf-8"), + metadata_policy={ + "fields": { + "summary": True, + "doc_type": False, + "domain": False, + "topic": False, + } + }, + ) + executor = PIFSCommandExecutor(filesystem, json_output=False) + + output = executor.execute( + "stat --field summary /documents/'Summary document 1' /documents/'Summary document 2'" + ) + + assert "/documents/Summary document 1:" in output + assert "summary: Summary for Summary document 1" in output + assert "full summary token" in output + assert "[truncated]" not in output + assert "/documents/Summary document 2:" in output + assert "summary: Summary for Summary document 2" in output + + data = json.loads( + PIFSCommandExecutor(filesystem, json_output=True).execute( + "stat --field summary /documents/'Summary document 1' /documents/'Summary document 2'" + ) + )["data"] + assert data["mode"] == "field_values" + assert data["target_count"] == 2 + assert data["data"][0]["field"] == "summary" + assert data["data"][0]["value"].startswith("Summary for Summary document 1\n") + assert data["data"][0]["value"].count("full summary token") == 80 + + with pytest.raises(PIFSCommandError, match="Unknown metadata field"): + executor.execute("stat --field missing_field /documents/'Summary document 1'") + + +def test_stat_field_rejects_more_than_twenty_targets(tmp_path): + from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem + from pageindex.filesystem.commands import PIFSCommandError + + filesystem = PageIndexFileSystem(workspace=tmp_path / "workspace") + targets = [] + for index in range(21): + source = tmp_path / f"source{index}.txt" + source.write_text(f"fixture text {index}", encoding="utf-8") + filesystem.register_file( + storage_uri=source.as_uri(), + source_path=f"docs/source{index}.txt", + folder_path="/documents", + external_id=f"doc_{index}", + title=f"Document {index}", + content=source.read_text(encoding="utf-8"), + metadata={"department": "ops"}, + ) + targets.append(f"/documents/'Document {index}'") + executor = PIFSCommandExecutor(filesystem, json_output=False) + + with pytest.raises(PIFSCommandError, match="at most 20"): + executor.execute("stat --field department " + " ".join(targets)) + + def test_register_rejects_pifs_owned_metadata_fields(tmp_path): from pageindex.filesystem import PageIndexFileSystem From 70eece52e58ee56f12866202edc14d84e582b65a Mon Sep 17 00:00:00 2001 From: Bukely_ Date: Tue, 26 May 2026 20:29:51 +0800 Subject: [PATCH 32/50] fix(filesystem): require real grep source lines Do not emit source-file grep fallback candidates unless an actual source line matches the query. --- pageindex/filesystem/commands.py | 8 +++--- tests/test_pageindex_filesystem_scope.py | 34 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/pageindex/filesystem/commands.py b/pageindex/filesystem/commands.py index c6394b97a..270de4dbe 100644 --- a/pageindex/filesystem/commands.py +++ b/pageindex/filesystem/commands.py @@ -1466,6 +1466,8 @@ def _grep_source_file_hits( if direct_only and self._folder_path_for_source_path(file_row["source_path"]) != folder_path: continue line_number, text = self._first_matching_source_line(path, query) + if line_number is None: + continue hits.append( { "file_ref": file_row["file_ref"], @@ -1560,15 +1562,15 @@ def _rg_candidate_files(self, query: str, directory: Path, *, max_files: int) -> break return filtered - def _first_matching_source_line(self, path: Path, query: str) -> tuple[int, str]: + def _first_matching_source_line(self, path: Path, query: str) -> tuple[int | None, str]: try: lines = path.read_text(encoding="utf-8", errors="ignore").splitlines() except OSError: - return 1, "" + return None, "" for line_number, line in enumerate(lines, 1): if self._line_matches(line, query): return line_number, self._compact_text(line, max_chars=220) - return 1, self._compact_text(lines[0], max_chars=220) if lines else "" + return None, "" def _source_root(self) -> Path | None: with self.filesystem.store.connect() as conn: diff --git a/tests/test_pageindex_filesystem_scope.py b/tests/test_pageindex_filesystem_scope.py index a07c4bb87..dd350277b 100644 --- a/tests/test_pageindex_filesystem_scope.py +++ b/tests/test_pageindex_filesystem_scope.py @@ -87,6 +87,40 @@ def test_semantic_search_scope_filters_explicit_source_type_facets(): ) == {} +def test_grep_source_file_requires_terms_on_same_line(tmp_path): + from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem + + source_dir = tmp_path / "source" / "documents" + source_dir.mkdir(parents=True) + source = source_dir / "split.json" + source.write_text( + '{\n "first": "alpha evidence lives here",\n' + ' "second": "omega evidence lives there"\n}\n', + encoding="utf-8", + ) + filesystem = PageIndexFileSystem(workspace=tmp_path / "workspace") + filesystem.register_file( + storage_uri=str(source), + source_path="documents/split.json", + folder_path="/documents", + external_id="doc_split_terms", + title="Split source terms", + content="registered artifact without the searched tokens", + ) + executor = PIFSCommandExecutor(filesystem, json_output=True) + + result = json.loads(executor.execute('grep -R "alpha omega" /documents')) + + assert result["data"]["mode"] == "files" + assert result["data"]["data"] == [] + + matched = json.loads(executor.execute('grep -R "alpha evidence" /documents')) + + assert matched["data"]["data"][0]["external_id"] == "doc_split_terms" + assert matched["data"]["data"][0]["line"] == 2 + assert "alpha evidence" in matched["data"]["data"][0]["text"] + + def test_existing_summary_projection_index_configures_retrieval_backend(tmp_path, monkeypatch): from pageindex.filesystem import PageIndexFileSystem from pageindex.filesystem.semantic_index import SemanticIndexRecord, SQLiteVecSemanticIndex From e9453725b6523e57f1163c1d127b10241273082f Mon Sep 17 00:00:00 2001 From: Bukely_ Date: Tue, 26 May 2026 20:30:08 +0800 Subject: [PATCH 33/50] fix(filesystem): lazy-load pifs import surface Avoid eager optional dependency imports when importing PageIndexFileSystem or filesystem semantic exports. --- pageindex/__init__.py | 12 +++++++++-- pageindex/filesystem/__init__.py | 32 +++++++++++++++++++++-------- pageindex/filesystem/core.py | 12 ++++++++--- tests/test_import_surface.py | 35 ++++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 13 deletions(-) create mode 100644 tests/test_import_surface.py diff --git a/pageindex/__init__.py b/pageindex/__init__.py index 97c37815e..c3fb0b0ae 100644 --- a/pageindex/__init__.py +++ b/pageindex/__init__.py @@ -2,13 +2,21 @@ os.environ.setdefault("LITELLM_LOCAL_MODEL_COST_MAP", "true") +_OPTIONAL_CORE_IMPORTS = {"litellm", "openai", "PyPDF2", "pymupdf"} + try: from .page_index import * from .page_index_md import md_to_tree from .retrieve import get_document, get_document_structure, get_page_content from .client import PageIndexClient except ModuleNotFoundError as exc: - if exc.name != "litellm": + if exc.name not in _OPTIONAL_CORE_IMPORTS: raise -from .filesystem import PageIndexFileSystem + +def __getattr__(name: str): + if name == "PageIndexFileSystem": + from .filesystem import PageIndexFileSystem + + return PageIndexFileSystem + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/pageindex/filesystem/__init__.py b/pageindex/filesystem/__init__.py index a6cde1614..2c8fd1b9b 100644 --- a/pageindex/filesystem/__init__.py +++ b/pageindex/filesystem/__init__.py @@ -1,6 +1,7 @@ +from importlib import import_module + from .commands import PIFSCommandExecutor from .core import PageIndexFileSystem -from .hybrid_projection import HybridProjectionSearchBackend from .metadata_generation import ( MetadataGenerationBackend, MetadataGenerationError, @@ -8,15 +9,17 @@ MetadataGenerationResult, MetadataGenerator, ) -from .projection_indexing import SummaryProjectionIndexer -from .semantic_index import ( - RebuildableSemanticIndex, - SemanticIndexRecord, - SemanticSearchResult, - SQLiteVecSemanticIndex, -) from .types import OpenResult, SearchResult +_LAZY_EXPORTS = { + "HybridProjectionSearchBackend": (".hybrid_projection", "HybridProjectionSearchBackend"), + "RebuildableSemanticIndex": (".semantic_index", "RebuildableSemanticIndex"), + "SemanticIndexRecord": (".semantic_index", "SemanticIndexRecord"), + "SemanticSearchResult": (".semantic_index", "SemanticSearchResult"), + "SQLiteVecSemanticIndex": (".semantic_index", "SQLiteVecSemanticIndex"), + "SummaryProjectionIndexer": (".projection_indexing", "SummaryProjectionIndexer"), +} + __all__ = [ "OpenResult", "HybridProjectionSearchBackend", @@ -34,3 +37,16 @@ "SummaryProjectionIndexer", "SQLiteVecSemanticIndex", ] + + +def __getattr__(name: str): + if name in _LAZY_EXPORTS: + module_name, attribute_name = _LAZY_EXPORTS[name] + value = getattr(import_module(module_name, __name__), attribute_name) + globals()[name] = value + return value + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +def __dir__() -> list[str]: + return sorted(set(globals()) | set(__all__)) diff --git a/pageindex/filesystem/core.py b/pageindex/filesystem/core.py index dcfb72f3f..b6cdef604 100644 --- a/pageindex/filesystem/core.py +++ b/pageindex/filesystem/core.py @@ -3,10 +3,9 @@ import json import os from pathlib import Path -from typing import Any, Optional, Union +from typing import TYPE_CHECKING, Any, Optional, Union from urllib.parse import unquote, urlparse -from ..client import PageIndexClient from .metadata import MetadataQueryEngine from .metadata_generation import ( MetadataGenerationBackend, @@ -15,7 +14,6 @@ MetadataGenerationResult, MetadataGenerator, ) -from .projection_indexing import SummaryProjectionIndexer from .semantic_folder_policy import ( SEMANTIC_FOLDER_BASE_FIELDS, SEMANTIC_FOLDER_ROOT, @@ -39,6 +37,10 @@ ) from .types import OpenResult, SearchResult +if TYPE_CHECKING: + from ..client import PageIndexClient + from .projection_indexing import SummaryProjectionIndexer + DEFAULT_METADATA_GENERATION_FIELDS = { "summary": True, "doc_type": True, @@ -215,6 +217,8 @@ def _ensure_register_completion_defaults(self) -> None: max_text_chars=self.metadata_max_text_chars, ) if self.summary_projection_index and self.summary_projection_indexer is None: + from .projection_indexing import SummaryProjectionIndexer + self.summary_projection_indexer = SummaryProjectionIndexer.from_provider( self.summary_projection_index_dir, embedding_provider=self.summary_projection_embedding_provider, @@ -836,6 +840,8 @@ def pageindex_client_workspace(self) -> Path: return self.workspace / "artifacts" / "pageindex_client" def _pageindex_client(self) -> PageIndexClient: + from ..client import PageIndexClient + return PageIndexClient(workspace=str(self.pageindex_client_workspace)) def _pageindex_client_doc_for_entry(self, entry: Any) -> tuple[PageIndexClient, str | None]: diff --git a/tests/test_import_surface.py b/tests/test_import_surface.py new file mode 100644 index 000000000..b4309cf05 --- /dev/null +++ b/tests/test_import_surface.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import builtins +import importlib +import sys + + +def test_filesystem_import_works_without_eager_optional_dependencies(monkeypatch): + blocked_roots = {"litellm", "openai", "PyPDF2", "pymupdf", "sqlite_vec"} + real_import = builtins.__import__ + + def clear_pageindex_modules() -> None: + for name in list(sys.modules): + if name == "pageindex" or name.startswith("pageindex."): + sys.modules.pop(name, None) + + def import_without_optional_deps(name, globals=None, locals=None, fromlist=(), level=0): + root = name.split(".", 1)[0] + if root in blocked_roots: + raise ModuleNotFoundError(f"No module named '{root}'", name=root) + return real_import(name, globals, locals, fromlist, level) + + clear_pageindex_modules() + try: + with monkeypatch.context() as patch: + patch.setattr(builtins, "__import__", import_without_optional_deps) + + filesystem_module = importlib.import_module("pageindex.filesystem") + from pageindex import PageIndexFileSystem as TopLevelPageIndexFileSystem + from pageindex.filesystem import PageIndexFileSystem + + assert filesystem_module.PageIndexFileSystem is PageIndexFileSystem + assert TopLevelPageIndexFileSystem is PageIndexFileSystem + finally: + clear_pageindex_modules() From 8e0f295464efcd3a35a9ad481246416b59870f4e Mon Sep 17 00:00:00 2001 From: Bukely_ Date: Tue, 26 May 2026 20:30:21 +0800 Subject: [PATCH 34/50] fix(filesystem): detect ambiguous virtual paths Resolve root virtual file paths correctly and raise a clear error for ambiguous file targets. --- pageindex/filesystem/store.py | 66 ++++++++++++++++++++++++------ tests/test_pifs_path_resolution.py | 44 ++++++++++++++++++++ 2 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 tests/test_pifs_path_resolution.py diff --git a/pageindex/filesystem/store.py b/pageindex/filesystem/store.py index 1cca9ea25..85de66b36 100644 --- a/pageindex/filesystem/store.py +++ b/pageindex/filesystem/store.py @@ -1059,21 +1059,61 @@ def _resolve_file_ref(self, conn: sqlite3.Connection, target: str) -> str: ).fetchone() if row: return row["file_ref"] - row = conn.execute( + virtual_file_ref = self._resolve_virtual_file_ref(conn, target) + if virtual_file_ref: + return virtual_file_ref + raise KeyError(f"Unknown file target: {target}") + + def _resolve_virtual_file_ref(self, conn: sqlite3.Connection, target: str) -> str | None: + virtual_target = normalize_path(target) + rows = conn.execute( """ - SELECT f.file_ref - FROM files f - JOIN file_folders ff ON ff.file_ref = f.file_ref - JOIN folders pf ON pf.folder_id = ff.folder_id - WHERE (pf.path || '/' || f.title) = ? - OR (pf.path || '/' || f.source_path) = ? - LIMIT 1 + WITH virtual_matches AS ( + SELECT + f.file_ref, + f.external_id, + f.title, + f.source_path, + pf.path AS folder_path, + (CASE WHEN pf.path = '/' THEN '/' ELSE pf.path || '/' END) + || ltrim(f.title, '/') AS title_virtual_path, + (CASE WHEN pf.path = '/' THEN '/' ELSE pf.path || '/' END) + || ltrim(f.source_path, '/') AS source_virtual_path + FROM files f + JOIN file_folders ff ON ff.file_ref = f.file_ref + JOIN folders pf ON pf.folder_id = ff.folder_id + WHERE f.deleted_at IS NULL + ) + SELECT + file_ref, + external_id, + title, + source_path, + MIN(folder_path) AS folder_path + FROM virtual_matches + WHERE title_virtual_path = ? + OR source_virtual_path = ? + GROUP BY file_ref, external_id, title, source_path + ORDER BY file_ref + LIMIT 2 """, - (target, target), - ).fetchone() - if row: - return row["file_ref"] - raise KeyError(f"Unknown file target: {target}") + (virtual_target, virtual_target), + ).fetchall() + if not rows: + return None + if len(rows) > 1: + matches = "; ".join(self._virtual_match_summary(row) for row in rows) + raise KeyError(f"Ambiguous file target: {target}. Matches: {matches}") + return rows[0]["file_ref"] + + @staticmethod + def _virtual_match_summary(row: sqlite3.Row) -> str: + external_id = row["external_id"] or "-" + return ( + f"file_ref={row['file_ref']} external_id={external_id} " + f"folder={row['folder_path']} title={row['title']!r} " + f"source_path={row['source_path']!r}" + ) def ensure_folder( self, diff --git a/tests/test_pifs_path_resolution.py b/tests/test_pifs_path_resolution.py new file mode 100644 index 000000000..08cf28f72 --- /dev/null +++ b/tests/test_pifs_path_resolution.py @@ -0,0 +1,44 @@ +import pytest + + +def test_root_virtual_file_path_resolves_without_double_slash(tmp_path): + from pageindex.filesystem import PageIndexFileSystem + + filesystem = PageIndexFileSystem(workspace=tmp_path / "workspace") + file_ref = filesystem.register_file( + storage_uri="file:///tmp/root-source.txt", + source_path="sources/root-source.txt", + folder_path="/", + external_id="doc_root_title", + title="Root Title", + content="root content", + ) + + assert filesystem.store.resolve_file_ref("/Root Title") == file_ref + + +def test_ambiguous_virtual_file_path_raises_clear_error(tmp_path): + from pageindex.filesystem import PageIndexFileSystem + + filesystem = PageIndexFileSystem(workspace=tmp_path / "workspace") + first_ref = filesystem.register_file( + storage_uri="file:///tmp/first.txt", + source_path="b/file.txt", + folder_path="/a", + external_id="doc_first", + title="First", + content="first content", + ) + second_ref = filesystem.register_file( + storage_uri="file:///tmp/second.txt", + source_path="second-source.txt", + folder_path="/a/b", + external_id="doc_second", + title="file.txt", + content="second content", + ) + + with pytest.raises(KeyError, match="Ambiguous file target"): + filesystem.store.resolve_file_ref("/a/b/file.txt") + + assert first_ref != second_ref From 112ef99d479ba89cdbd2f22732cc8a08c484631e Mon Sep 17 00:00:00 2001 From: Bukely_ Date: Tue, 26 May 2026 20:30:33 +0800 Subject: [PATCH 35/50] fix(filesystem): escape sqlite like wildcards Escape wildcard characters in recursive folder LIKE filters and metadata contains queries. --- pageindex/filesystem/store.py | 56 ++++++++++------ tests/test_pifs_like_escape.py | 115 +++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+), 19 deletions(-) create mode 100644 tests/test_pifs_like_escape.py diff --git a/pageindex/filesystem/store.py b/pageindex/filesystem/store.py index 85de66b36..b88b54e2e 100644 --- a/pageindex/filesystem/store.py +++ b/pageindex/filesystem/store.py @@ -555,7 +555,7 @@ def list_folder( WHERE child_folder.parent_id = fo.folder_id ) AS children_count FROM folders fo - WHERE fo.path != ? AND (fo.path LIKE ?) + WHERE fo.path != ? AND (fo.path LIKE ? ESCAPE '\\') {folder_depth_clause} ORDER BY fo.path LIMIT ? @@ -702,15 +702,12 @@ def find_folders( WHERE f.deleted_at IS NULL AND ( matched_folder.folder_id = fo.folder_id - OR matched_folder.path LIKE CASE - WHEN fo.path = '/' THEN '/%' - ELSE fo.path || '/%' - END + OR matched_folder.path LIKE {self._descendant_like_sql_expr("fo.path")} ESCAPE '\\' ) {metadata_clause} ) AS matched_files FROM folders fo - WHERE fo.path != ? AND fo.path LIKE ? + WHERE fo.path != ? AND fo.path LIKE ? ESCAPE '\\' {folder_depth_clause} ) WHERE matched_files > 0 @@ -909,10 +906,10 @@ def _compile_metadata_field_filter(self, field: str, condition: Any) -> tuple[st SELECT 1 FROM metadata_values mv WHERE mv.file_ref = f.file_ref AND mv.field_id = ? - AND lower(mv.value_text) LIKE '%' || lower(?) || '%' + AND lower(mv.value_text) LIKE lower(?) ESCAPE '\\' ) """, - [field_id, self._metadata_compare_text(expected)], + [field_id, self._contains_like(self._metadata_compare_text(expected))], ) if operator in {"$gt", "$gte", "$lt", "$lte"}: comparator = { @@ -1353,7 +1350,7 @@ def count_files_in_folder(self, path: str, *, recursive: bool = True) -> int: JOIN file_folders ff ON ff.file_ref = f.file_ref JOIN folders fo ON fo.folder_id = ff.folder_id WHERE f.deleted_at IS NULL - AND (fo.path = ? OR fo.path LIKE ?) + AND (fo.path = ? OR fo.path LIKE ? ESCAPE '\\') """, (path, self._descendant_like(path)), ).fetchone() @@ -1389,7 +1386,7 @@ def folder_subtree_thresholds( SELECT path FROM folders WHERE path != ? - AND path LIKE ? + AND path LIKE ? ESCAPE '\\' AND ( CASE WHEN TRIM(path, '/') = '' THEN 0 @@ -1407,7 +1404,7 @@ def folder_subtree_thresholds( JOIN file_folders ff ON ff.file_ref = f.file_ref JOIN folders fo ON fo.folder_id = ff.folder_id WHERE f.deleted_at IS NULL - AND (fo.path = ? OR fo.path LIKE ?) + AND (fo.path = ? OR fo.path LIKE ? ESCAPE '\\') LIMIT ? """, (path, self._descendant_like(path), file_limit + 1), @@ -1486,7 +1483,7 @@ def _file_rows_for_scope( """ params: list[Any] if recursive: - sql += " AND (pf.path = ? OR pf.path LIKE ?)" + sql += " AND (pf.path = ? OR pf.path LIKE ? ESCAPE '\\')" params = [path, self._descendant_like(path)] if max_depth is not None: if max_depth <= 0: @@ -1539,10 +1536,7 @@ def _scope_sql(self, scope: Optional[dict[str, Any]]) -> tuple[str, list[Any]]: WHERE scope_ff.file_ref = f.file_ref AND ( scope_folder.folder_id = base_folder.folder_id - OR scope_folder.path LIKE CASE - WHEN base_folder.path = '/' THEN '/%' - ELSE base_folder.path || '/%' - END + OR scope_folder.path LIKE {self._descendant_like_sql_expr("base_folder.path")} ESCAPE '\\' ) {depth_clause} ) @@ -1567,7 +1561,7 @@ def _scope_sql(self, scope: Optional[dict[str, Any]]) -> tuple[str, list[Any]]: if recursive and max_depth == 0: return "0", [] path_clause = ( - "(scope_folder.path = ? OR scope_folder.path LIKE ?)" + "(scope_folder.path = ? OR scope_folder.path LIKE ? ESCAPE '\\')" if recursive else "scope_folder.path = ?" ) @@ -1610,9 +1604,33 @@ def _folder_by_path(self, conn: sqlite3.Connection, path: str) -> sqlite3.Row | (path,), ).fetchone() + @classmethod + def _descendant_like(cls, path: str) -> str: + return "/%" if path == "/" else f"{cls._like_escape(path)}/%" + + @staticmethod + def _descendant_like_sql_expr(path_expr: str) -> str: + escaped_expr = SQLiteFileSystemStore._like_escape_sql_expr(path_expr) + return f"CASE WHEN {path_expr} = '/' THEN '/%' ELSE {escaped_expr} || '/%' END" + + @staticmethod + def _contains_like(value: str) -> str: + return f"%{SQLiteFileSystemStore._like_escape(value)}%" + @staticmethod - def _descendant_like(path: str) -> str: - return "/%" if path == "/" else f"{path}/%" + def _like_escape(value: str) -> str: + return ( + value.replace("\\", "\\\\") + .replace("%", "\\%") + .replace("_", "\\_") + ) + + @staticmethod + def _like_escape_sql_expr(value_expr: str) -> str: + return ( + f"replace(replace(replace({value_expr}, '\\', '\\\\'), " + "'%', '\\%'), '_', '\\_')" + ) @staticmethod def _folder_depth(path: str) -> int: diff --git a/tests/test_pifs_like_escape.py b/tests/test_pifs_like_escape.py new file mode 100644 index 000000000..82e7ef9dd --- /dev/null +++ b/tests/test_pifs_like_escape.py @@ -0,0 +1,115 @@ +from pathlib import Path + + +def _register_file( + filesystem, + tmp_path: Path, + filename: str, + *, + folder_path: str, + external_id: str, + metadata: dict[str, str] | None = None, +) -> None: + source = tmp_path / filename + source.write_text(f"{external_id} fixture text", encoding="utf-8") + filesystem.register_file( + storage_uri=source.as_uri(), + source_path=f"docs/{filename}", + folder_path=folder_path, + external_id=external_id, + title=external_id, + content=source.read_text(encoding="utf-8"), + metadata=metadata or {}, + ) + + +def test_descendant_folder_filter_treats_underscore_literally(tmp_path): + from pageindex.filesystem import PageIndexFileSystem + + filesystem = PageIndexFileSystem(workspace=tmp_path / "workspace") + _register_file( + filesystem, + tmp_path, + "literal.txt", + folder_path="/proj_1/docs", + external_id="literal_underscore", + ) + _register_file( + filesystem, + tmp_path, + "wildcard.txt", + folder_path="/projA1/docs", + external_id="wildcard_neighbor", + ) + + recursive = filesystem.browse("/proj_1", recursive=True, limit=10) + folder_id = filesystem.folder_info("/proj_1")["folder_id"] + scoped_results = filesystem.search( + scope={"folder_id": folder_id, "recursive": True}, + semantic=False, + limit=10, + ) + ranked_folders = { + folder["path"]: folder + for folder in filesystem.find_folders("/", max_depth=1, limit=10) + } + + assert {folder["path"] for folder in recursive["folders"]} == {"/proj_1/docs"} + assert {file["external_id"] for file in recursive["files"]} == {"literal_underscore"} + assert {result.external_id for result in scoped_results} == {"literal_underscore"} + assert ranked_folders["/proj_1"]["matched_files"] == 1 + assert ranked_folders["/projA1"]["matched_files"] == 1 + assert filesystem.store.count_files_in_folder("/proj_1", recursive=True) == 1 + + +def test_metadata_contains_treats_percent_and_underscore_literally(tmp_path): + from pageindex.filesystem import PageIndexFileSystem + + filesystem = PageIndexFileSystem(workspace=tmp_path / "workspace") + filesystem.metadata.register_schema({"fields": {"status": "string"}}) + _register_file( + filesystem, + tmp_path, + "percent.txt", + folder_path="/documents", + external_id="literal_percent", + metadata={"status": "100% done"}, + ) + _register_file( + filesystem, + tmp_path, + "percent-neighbor.txt", + folder_path="/documents", + external_id="percent_neighbor", + metadata={"status": "100X done"}, + ) + _register_file( + filesystem, + tmp_path, + "underscore.txt", + folder_path="/documents", + external_id="literal_underscore", + metadata={"status": "build_alpha"}, + ) + _register_file( + filesystem, + tmp_path, + "underscore-neighbor.txt", + folder_path="/documents", + external_id="underscore_neighbor", + metadata={"status": "buildXalpha"}, + ) + + percent_results = filesystem.search( + metadata_filter={"status": {"$contains": "100% done"}}, + semantic=False, + limit=10, + ) + underscore_results = filesystem.search( + metadata_filter={"status": {"$contains": "build_alpha"}}, + semantic=False, + limit=10, + ) + + assert {result.external_id for result in percent_results} == {"literal_percent"} + assert {result.external_id for result in underscore_results} == {"literal_underscore"} From c86d5727ed8dcec4efe1d5c22058c5e7fec1c159 Mon Sep 17 00:00:00 2001 From: Bukely_ Date: Tue, 26 May 2026 20:30:46 +0800 Subject: [PATCH 36/50] fix(filesystem): preserve pageindex registration errors Persist PageIndex tree build failure details in metadata_status and surface them through stat and structural reads. --- pageindex/filesystem/commands.py | 9 +++ pageindex/filesystem/core.py | 82 ++++++++++++++++++++++--- tests/test_pageindex_structural_read.py | 11 +++- 3 files changed, 91 insertions(+), 11 deletions(-) diff --git a/pageindex/filesystem/commands.py b/pageindex/filesystem/commands.py index 270de4dbe..cc4535fa1 100644 --- a/pageindex/filesystem/commands.py +++ b/pageindex/filesystem/commands.py @@ -1258,6 +1258,15 @@ def _render_stat(self, data: Any) -> str: metadata_status = data.get("metadata_status") or {} if metadata_status: lines.append(f"metadata_status: {metadata_status.get('status', '-')}") + pageindex_tree = metadata_status.get("pageindex_tree") or {} + if isinstance(pageindex_tree, dict) and pageindex_tree: + lines.append(f"pageindex_tree_status: {pageindex_tree.get('status', '-')}") + message = str(pageindex_tree.get("message") or "").strip() + error_type = str(pageindex_tree.get("error_type") or "").strip() + if error_type and message: + lines.append(f"pageindex_tree_error: {error_type}: {message}") + elif message or error_type: + lines.append(f"pageindex_tree_error: {message or error_type}") summary_projection = ( metadata_status.get("projection_indexes", {}).get("summary", {}) ) diff --git a/pageindex/filesystem/core.py b/pageindex/filesystem/core.py index b6cdef604..e2c6136b4 100644 --- a/pageindex/filesystem/core.py +++ b/pageindex/filesystem/core.py @@ -858,21 +858,47 @@ def _registration_pageindex_pointer( storage_uri: str, source_path: str, content_type: str, - ) -> tuple[str | None, str]: + ) -> tuple[str | None, str, dict[str, Any] | None]: if self._source_format(source_path, content_type) not in {"pdf", "markdown"}: - return None, "not_built" + return None, "not_built", None client = self._pageindex_client() source = self._canonical_source_path(storage_uri=storage_uri, source_path=source_path) cached_doc_id = self._find_cached_pageindex_doc_id(client, source) if cached_doc_id: - return cached_doc_id, "built" + return cached_doc_id, "built", None if source is None: - return None, "failed" + return None, "failed", self._pageindex_tree_failure_record( + source="PageIndexFileSystem.registration", + error_type="UnresolvableSourcePath", + message=( + "PageIndex source path must resolve to a local file path for " + "PDF/Markdown registration." + ), + ) try: doc_id = client.index(source) - return doc_id, "built" - except Exception: - return None, "failed" + return doc_id, "built", None + except Exception as exc: + return None, "failed", self._pageindex_tree_failure_record( + source="PageIndexClient.index", + error_type=exc.__class__.__name__, + message=str(exc) or exc.__class__.__name__, + ) + + @staticmethod + def _pageindex_tree_failure_record( + *, + source: str, + error_type: str, + message: str, + ) -> dict[str, Any]: + return { + "status": "failed", + "owner": "pageindex", + "source": source, + "error_type": error_type, + "message": message, + } def _find_cached_pageindex_doc_id( self, @@ -938,7 +964,11 @@ def _prepare_file_record(self, file: dict[str, Any]) -> dict[str, Any]: external_id = file.get("external_id") content = file.get("content") or "" content_type = file.get("content_type") or "text/plain" - pageindex_doc_id, pageindex_tree_status = self._registration_pageindex_pointer( + ( + pageindex_doc_id, + pageindex_tree_status, + pageindex_tree_failure, + ) = self._registration_pageindex_pointer( storage_uri=storage_uri, source_path=raw_source_path, content_type=content_type, @@ -961,6 +991,7 @@ def _prepare_file_record(self, file: dict[str, Any]) -> dict[str, Any]: metadata=metadata, status=file.get("metadata_status"), ) + self._attach_pageindex_tree_failure(metadata_status, pageindex_tree_failure) indexed_metadata = SQLiteFileSystemStore.indexed_metadata_values(metadata) searchable_metadata = dict(metadata) folder_path = normalize_path(file.get("folder_path") or "/") @@ -1116,6 +1147,10 @@ def _record_from_file_entry(self, entry: Any) -> dict[str, Any]: metadata=entry.metadata, status=entry.metadata_status.get("status"), ) + self._attach_pageindex_tree_failure( + metadata_status, + entry.metadata_status.get("pageindex_tree"), + ) return { "file_ref": entry.file_ref, "external_id": entry.external_id, @@ -1317,8 +1352,9 @@ def _open_all(self, file_ref: str) -> OpenResult: source_path=entry.source_path, ) - @staticmethod + @classmethod def _structural_unavailable( + cls, mode: str, entry: Any, *, @@ -1326,6 +1362,9 @@ def _structural_unavailable( node_id: str | None = None, pages: str | None = None, ) -> dict[str, Any]: + pageindex_tree_error = cls._pageindex_tree_failure_message(entry.metadata_status) + if pageindex_tree_error and entry.pageindex_tree_status == "failed": + message = f"PageIndex tree build failed: {pageindex_tree_error}" result = { "mode": mode, "file_ref": entry.file_ref, @@ -1335,12 +1374,37 @@ def _structural_unavailable( "available": False, "message": message, } + if pageindex_tree_error: + result["pageindex_tree_error"] = pageindex_tree_error if node_id is not None: result["node_id"] = node_id if pages is not None: result["pages"] = pages return result + @staticmethod + def _attach_pageindex_tree_failure( + metadata_status: dict[str, Any], + pageindex_tree_failure: Any, + ) -> None: + if isinstance(pageindex_tree_failure, dict) and pageindex_tree_failure: + metadata_status["pageindex_tree"] = dict(pageindex_tree_failure) + + @staticmethod + def _pageindex_tree_failure_message(metadata_status: Any) -> str | None: + if not isinstance(metadata_status, dict): + return None + pageindex_tree = metadata_status.get("pageindex_tree") + if not isinstance(pageindex_tree, dict): + return None + if pageindex_tree.get("status") != "failed": + return None + message = str(pageindex_tree.get("message") or "").strip() + error_type = str(pageindex_tree.get("error_type") or "").strip() + if error_type and message: + return f"{error_type}: {message}" + return message or error_type or None + def _resolve_target(self, target: str) -> str: return self.store.resolve_file_ref(target) diff --git a/tests/test_pageindex_structural_read.py b/tests/test_pageindex_structural_read.py index d54d9a7d5..500e4c53e 100644 --- a/tests/test_pageindex_structural_read.py +++ b/tests/test_pageindex_structural_read.py @@ -55,7 +55,7 @@ def test_pageindex_structure_options_report_failed_register_build(monkeypatch): filesystem = PageIndexFileSystem(workspace=Path(tmp) / "workspace") def fail_index(*args, **kwargs): - raise RuntimeError("index failed") + raise RuntimeError("index failed: extractor unavailable") monkeypatch.setattr(PageIndexClient, "index", fail_index) filesystem.register_file( @@ -75,8 +75,15 @@ def fail_index(*args, **kwargs): assert structure["data"]["mode"] == "structure" assert structure["data"]["available"] is False assert structure["data"]["status"] == "failed" - assert "PageIndexClient workspace" in structure["data"]["message"] + assert "RuntimeError: index failed: extractor unavailable" in structure["data"]["message"] assert stat["data"]["pageindex_tree_status"] == "failed" + assert stat["data"]["metadata_status"]["pageindex_tree"] == { + "status": "failed", + "owner": "pageindex", + "source": "PageIndexClient.index", + "error_type": "RuntimeError", + "message": "index failed: extractor unavailable", + } assert node["data"]["mode"] == "node" assert node["data"]["available"] is False From d139181c86fd92a7768dbd7f294250e0ae345a4c Mon Sep 17 00:00:00 2001 From: Bukely_ Date: Tue, 26 May 2026 20:31:13 +0800 Subject: [PATCH 37/50] fix(filesystem): delay register side effects until insert Write projection and raw side effects only after a successful catalog insert, and clean owned artifacts when registration fails. --- pageindex/filesystem/core.py | 66 +++++++++++++++--------- tests/test_pifs_register_side_effects.py | 60 +++++++++++++++++++++ 2 files changed, 103 insertions(+), 23 deletions(-) create mode 100644 tests/test_pifs_register_side_effects.py diff --git a/pageindex/filesystem/core.py b/pageindex/filesystem/core.py index e2c6136b4..72833b78a 100644 --- a/pageindex/filesystem/core.py +++ b/pageindex/filesystem/core.py @@ -168,12 +168,22 @@ def register(self, **kwargs: Any) -> str: def register_files(self, files: list[dict[str, Any]]) -> list[str]: records = [self._prepare_file_record(file) for file in files] + try: + for record in records: + self._generate_register_metadata(record) + self._register_generation_policy_schema(records) + self.store.insert_files(records) + except Exception: + self._cleanup_failed_register_artifacts(records) + raise for record in records: - self._generate_register_metadata(record) - self._complete_summary_projection_index(record) + if self._complete_summary_projection_index(record): + self.store.update_file_metadata_status( + record["file_ref"], + metadata=record["metadata"], + metadata_status=record["metadata_status"], + ) self._sync_owned_raw_artifact(record) - self._register_generation_policy_schema(records) - self.store.insert_files(records) return [record["file_ref"] for record in records] def batch_generate(self, *, limit: int | None = None) -> dict[str, Any]: @@ -997,22 +1007,15 @@ def _prepare_file_record(self, file: dict[str, Any]) -> dict[str, Any]: folder_path = normalize_path(file.get("folder_path") or "/") title = file.get("title") or metadata.get("title") or Path(source_path).stem file_ref = make_file_ref(external_id or source_path) - text_artifact_path = file.get("text_artifact_path") or self.store.write_text_artifact( - file_ref, - artifact_content, - ) + text_artifact_path = file.get("text_artifact_path") + owns_text_artifact = text_artifact_path is None + if text_artifact_path is None: + text_artifact_path = self.store.write_text_artifact(file_ref, artifact_content) raw_artifact_path = file.get("raw_artifact_path") + owns_raw_artifact = False if raw_artifact_path is None and file.get("write_raw_artifact", True): - raw_artifact_path = self.store.write_raw_artifact( - file_ref, - self._raw_artifact_payload( - storage_uri=storage_uri, - source_path=source_path, - folder_path=folder_path, - metadata=metadata, - metadata_status=metadata_status, - ), - ) + raw_artifact_path = self.store.raw_dir / f"{file_ref}.json" + owns_raw_artifact = True descriptor = self._build_descriptor(title, metadata) return { "file_ref": file_ref, @@ -1037,6 +1040,8 @@ def _prepare_file_record(self, file: dict[str, Any]) -> dict[str, Any]: "folder_path": folder_path, "content": fts_content, "skip_fts": bool(file.get("skip_fts", False)), + "_pifs_owned_text_artifact": owns_text_artifact, + "_pifs_owned_raw_artifact": owns_raw_artifact, } def _registration_text_artifact_content( @@ -1234,29 +1239,44 @@ def _generate_register_metadata(self, record: dict[str, Any], *, force: bool = F } self._refresh_record_metadata_status(record) - def _complete_summary_projection_index(self, record: dict[str, Any]) -> None: + def _complete_summary_projection_index(self, record: dict[str, Any]) -> bool: metadata_status = record["metadata_status"] summary_index = metadata_status.get("projection_indexes", {}).get("summary") if not summary_index or not summary_index.get("requested"): - return + return False summary = str(record.get("metadata", {}).get("summary") or "").strip() if not summary: - return + return False if self.summary_projection_indexer is None: self._refresh_record_metadata_status(record) - return + return True try: result = self.summary_projection_indexer.upsert_summary(record) except Exception as exc: summary_index["status"] = "failed" summary_index["error"] = str(exc) self._refresh_record_metadata_status(record) - return + return True summary_index.clear() summary_index.update({"requested": True, **result}) if summary_index.get("status") != "ready": summary_index["status"] = "ready" self._refresh_record_metadata_status(record) + return True + + @staticmethod + def _unlink_artifact(path: Any) -> None: + try: + Path(path).unlink() + except FileNotFoundError: + return + + def _cleanup_failed_register_artifacts(self, records: list[dict[str, Any]]) -> None: + for record in records: + if record.get("_pifs_owned_text_artifact"): + self._unlink_artifact(record["text_artifact_path"]) + if record.get("_pifs_owned_raw_artifact") and record.get("raw_artifact_path"): + self._unlink_artifact(record["raw_artifact_path"]) @staticmethod def _metadata_policy_is_batch(policy: dict[str, Any]) -> bool: diff --git a/tests/test_pifs_register_side_effects.py b/tests/test_pifs_register_side_effects.py new file mode 100644 index 000000000..867dd6bf4 --- /dev/null +++ b/tests/test_pifs_register_side_effects.py @@ -0,0 +1,60 @@ +from pathlib import Path + +import pytest + + +class SummaryGenerator: + def generate(self, document, *, fields): + return {field: "Generated registration summary." for field in fields} + + +class RecordingSummaryIndexer: + def __init__(self): + self.upserted = [] + + def upsert_summary(self, record): + self.upserted.append(dict(record)) + return {"status": "ready"} + + +def test_register_insert_failure_cleans_owned_artifacts_and_skips_projection( + tmp_path: Path, monkeypatch +): + from pageindex.filesystem import PageIndexFileSystem + + workspace = tmp_path / "workspace" + source = tmp_path / "source.txt" + source.write_text("Plain text content for registration.", encoding="utf-8") + indexer = RecordingSummaryIndexer() + filesystem = PageIndexFileSystem( + workspace=workspace, + metadata_generator=SummaryGenerator(), + summary_projection_indexer=indexer, + ) + + def fail_insert(records): + raise RuntimeError("catalog insert failed") + + monkeypatch.setattr(filesystem.store, "insert_files", fail_insert) + + with pytest.raises(RuntimeError, match="catalog insert failed"): + filesystem.register_file( + storage_uri=source.as_uri(), + source_path="docs/source.txt", + folder_path="/documents", + external_id="doc_insert_failure", + title="Insert failure", + content=source.read_text(encoding="utf-8"), + metadata_policy={ + "fields": { + "summary": True, + "doc_type": False, + "domain": False, + "topic": False, + } + }, + ) + + assert indexer.upserted == [] + assert list((workspace / "artifacts" / "raw").glob("*.json")) == [] + assert list((workspace / "artifacts" / "text").glob("*.txt")) == [] From 7104602a70ab1cd7604d63446565f9ca3c733069 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 20:39:25 +0800 Subject: [PATCH 38/50] refactor(filesystem): simplify semantic search result fields --- pageindex/filesystem/commands.py | 55 ++++++++++- tests/test_pageindex_filesystem_scope.py | 114 ++++++++++++++++++++++- 2 files changed, 164 insertions(+), 5 deletions(-) diff --git a/pageindex/filesystem/commands.py b/pageindex/filesystem/commands.py index cc4535fa1..533e88a03 100644 --- a/pageindex/filesystem/commands.py +++ b/pageindex/filesystem/commands.py @@ -786,7 +786,7 @@ def _cmd_semantic_channel(self, channel: str, args: list[str]) -> Any: "query": query, "scope": normalized, "retrieval": f"{channel}_vector", - "data": self._grep_file_hits_from_results(results, query), + "data": self._semantic_channel_hits_from_results(channel, results, query), } def _semantic_recursive_grep( @@ -1072,7 +1072,7 @@ def _render_shell(self, command_name: str, data: Any) -> str: if command_name in {"grep", "semantic-grep"}: return self._render_grep(data) if command_name in {"search-summary", "search-entity", "search-relation"}: - return self._render_grep(data) + return self._render_semantic_search(data) if command_name == "find": return self._render_find(data) if command_name == "stat": @@ -1195,6 +1195,26 @@ def _render_grep(self, data: Any) -> str: ) return str(data) + def _render_semantic_search(self, data: Any) -> str: + if not isinstance(data, dict): + return str(data) + if data.get("mode") != "files": + return self._render_grep(data) + if not data.get("data", []): + return f"# no matches for: {data.get('query', '')}" + lines: list[str] = [] + for item in data.get("data", []): + lines.append(str(item.get("path") or "-")) + lines.append(f"summary: {self._one_line_value(item.get('summary') or '')}") + if "entity" in item: + lines.append(f"entity: {self._one_line_value(item.get('entity') or '')}") + if "relation" in item: + lines.append(f"relation: {self._one_line_value(item.get('relation') or '')}") + line_text = self._one_line_value(item.get("line_text") or "") + lines.append(f"line_text: {line_text or '-'}") + lines.append("") + return "\n".join(lines).rstrip() + def _render_find(self, data: Any) -> str: if not isinstance(data, list): return str(data) @@ -1422,6 +1442,37 @@ def _grep_file_hits_from_results( break return hits + def _semantic_channel_hits_from_results( + self, + channel: str, + results: list[Any], + query: str, + ) -> list[dict[str, Any]]: + hits = [] + for result in results: + metadata = result.metadata or {} + line, text = self._first_matching_line(result.file_ref, query) + line_text = "" + if text: + line_text = f"{line}: {self._compact_text(text, max_chars=220)}" + hit = { + "path": self._file_target_path( + { + "file_ref": result.file_ref, + "title": result.title, + "folder_paths": result.folder_paths, + "source_path": result.source_path, + "external_id": result.external_id, + } + ), + "summary": metadata.get("summary") or "", + "line_text": line_text, + } + if channel in {"entity", "relation"}: + hit[channel] = metadata.get(channel) or "" + hits.append(hit) + return hits + def _rank_child_folders_from_source( self, *, diff --git a/tests/test_pageindex_filesystem_scope.py b/tests/test_pageindex_filesystem_scope.py index dd350277b..1ee57cc09 100644 --- a/tests/test_pageindex_filesystem_scope.py +++ b/tests/test_pageindex_filesystem_scope.py @@ -22,18 +22,53 @@ def search_channel(self, channel, query, *, limit=10, filters=None): ] +class ChannelBackend: + def __init__(self, document_id, channels=("summary", "entity", "relation")): + self.document_id = document_id + self.channels = channels + + def available_channels(self): + return self.channels + + def search_channel(self, channel, query, *, limit=10, filters=None): + return [ + SimpleNamespace( + document_id=self.document_id, + snippet=f"{channel} candidate: {query}", + ) + ] + + def test_semantic_search_scope_keeps_ordinary_folders_out_of_source_type_filters(tmp_path): from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem + from pageindex.filesystem.metadata_generation import MetadataGenerationResult - filesystem = PageIndexFileSystem(workspace=tmp_path / "workspace") + class SummaryGenerator: + def generate(self, document, *, fields): + return MetadataGenerationResult( + values={"summary": "Federal Reserve annual report summary"} + ) + + filesystem = PageIndexFileSystem( + workspace=tmp_path / "workspace", + metadata_generator=SummaryGenerator(), + ) filesystem.register_file( storage_uri="file:///tmp/report.pdf", source_path="examples/documents/report.pdf", folder_path="/documents", external_id="dsid_report", - title="Annual report", + title="report.pdf", metadata={"source_type": "examples-documents"}, content="Federal Reserve supervision and regulation annual report.", + metadata_policy={ + "fields": { + "summary": True, + "doc_type": False, + "domain": False, + "topic": False, + } + }, ) backend = SummaryBackend("dsid_report") filesystem.semantic_retrieval_backend = backend @@ -44,7 +79,80 @@ def test_semantic_search_scope_keeps_ordinary_folders_out_of_source_type_filters ) assert backend.calls[0][2] == {} - assert result["data"]["data"][0]["external_id"] == "dsid_report" + assert result["data"]["data"][0] == { + "path": "/documents/report.pdf", + "summary": "Federal Reserve annual report summary", + "line_text": "1: Federal Reserve supervision and regulation annual report.", + } + + executor.json_output = False + rendered = executor.execute('search-summary "Federal Reserve annual report" /documents') + assert "/documents/report.pdf" in rendered + assert "summary: Federal Reserve annual report summary" in rendered + assert "line_text: 1: Federal Reserve supervision and regulation annual report." in rendered + assert "id=dsid_report" not in rendered + assert "file_ref=" not in rendered + + +def test_entity_relation_search_return_minimal_fields_with_summary(tmp_path): + from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem + from pageindex.filesystem.metadata_generation import MetadataGenerationResult + + class MetadataGenerator: + def generate(self, document, *, fields): + values = { + "summary": "Risk and compliance summary", + "entity": "Federal Reserve; Disney", + "relation": "Federal Reserve affects Disney valuation", + } + return MetadataGenerationResult(values={field: values[field] for field in fields}) + + filesystem = PageIndexFileSystem( + workspace=tmp_path / "workspace", + metadata_generator=MetadataGenerator(), + ) + filesystem.register_file( + storage_uri="file:///tmp/market-note.pdf", + source_path="examples/documents/market-note.pdf", + folder_path="/documents", + external_id="dsid_market_note", + title="market-note.pdf", + content="Federal Reserve policy affects Disney valuation.", + metadata_policy={ + "fields": { + "summary": True, + "doc_type": False, + "domain": False, + "topic": False, + "entity": True, + "relation": True, + } + }, + ) + filesystem.semantic_retrieval_backend = ChannelBackend("dsid_market_note") + executor = PIFSCommandExecutor(filesystem, json_output=True) + + entity = json.loads(executor.execute('search-entity "Federal Reserve" /documents')) + assert entity["data"]["data"][0] == { + "path": "/documents/market-note.pdf", + "summary": "Risk and compliance summary", + "line_text": "1: Federal Reserve policy affects Disney valuation.", + "entity": "Federal Reserve; Disney", + } + + relation = json.loads(executor.execute('search-relation "Disney valuation" /documents')) + assert relation["data"]["data"][0] == { + "path": "/documents/market-note.pdf", + "summary": "Risk and compliance summary", + "line_text": "1: Federal Reserve policy affects Disney valuation.", + "relation": "Federal Reserve affects Disney valuation", + } + + executor.json_output = False + rendered = executor.execute('search-entity "Federal Reserve" /documents') + assert "summary: Risk and compliance summary" in rendered + assert "entity: Federal Reserve; Disney" in rendered + assert "file_ref=" not in rendered def test_semantic_search_rejects_unquoted_multi_word_query(tmp_path): From 4a158f9e5fb13d6be06599b731c57d7a03d35991 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 20:44:28 +0800 Subject: [PATCH 39/50] fix(filesystem): label semantic search paths in cli output --- pageindex/filesystem/commands.py | 2 +- tests/test_pageindex_filesystem_scope.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pageindex/filesystem/commands.py b/pageindex/filesystem/commands.py index 533e88a03..ece7aa06e 100644 --- a/pageindex/filesystem/commands.py +++ b/pageindex/filesystem/commands.py @@ -1204,7 +1204,7 @@ def _render_semantic_search(self, data: Any) -> str: return f"# no matches for: {data.get('query', '')}" lines: list[str] = [] for item in data.get("data", []): - lines.append(str(item.get("path") or "-")) + lines.append(f"path: {item.get('path') or '-'}") lines.append(f"summary: {self._one_line_value(item.get('summary') or '')}") if "entity" in item: lines.append(f"entity: {self._one_line_value(item.get('entity') or '')}") diff --git a/tests/test_pageindex_filesystem_scope.py b/tests/test_pageindex_filesystem_scope.py index 1ee57cc09..5bd730c11 100644 --- a/tests/test_pageindex_filesystem_scope.py +++ b/tests/test_pageindex_filesystem_scope.py @@ -87,7 +87,7 @@ def generate(self, document, *, fields): executor.json_output = False rendered = executor.execute('search-summary "Federal Reserve annual report" /documents') - assert "/documents/report.pdf" in rendered + assert "path: /documents/report.pdf" in rendered assert "summary: Federal Reserve annual report summary" in rendered assert "line_text: 1: Federal Reserve supervision and regulation annual report." in rendered assert "id=dsid_report" not in rendered @@ -150,6 +150,7 @@ def generate(self, document, *, fields): executor.json_output = False rendered = executor.execute('search-entity "Federal Reserve" /documents') + assert "path: /documents/market-note.pdf" in rendered assert "summary: Risk and compliance summary" in rendered assert "entity: Federal Reserve; Disney" in rendered assert "file_ref=" not in rendered From 2b69719f603dcc4c3f8693eafe06e9733dadcb65 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 20:49:28 +0800 Subject: [PATCH 40/50] feat(filesystem): support space-separated cat node ids --- pageindex/filesystem/commands.py | 11 ++++++++--- tests/test_pageindex_structural_read.py | 11 +++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/pageindex/filesystem/commands.py b/pageindex/filesystem/commands.py index ece7aa06e..29ea46a8d 100644 --- a/pageindex/filesystem/commands.py +++ b/pageindex/filesystem/commands.py @@ -517,7 +517,10 @@ def _cmd_cat(self, args: list[str]) -> Any: if i >= len(args): raise PIFSCommandError("cat --node requires a node id") structural_mode = "node" - node_ids.extend(self._parse_node_ids(args[i])) + while i < len(args) and not args[i].startswith("-"): + node_ids.extend(self._parse_node_ids(args[i])) + i += 1 + i -= 1 elif arg == "--page": i += 1 if i >= len(args): @@ -528,8 +531,10 @@ def _cmd_cat(self, args: list[str]) -> Any: raise PIFSCommandError(f"Unsupported cat option: {arg}") else: raise PIFSCommandError( - "cat accepts one file target. Use: cat --page , " - "for example: cat /documents/report.pdf --page 31-59" + "cat accepts one file target. Use target-first syntax: " + "cat --structure, " + "cat --node 0002 0004, or " + "cat --page 31-33" ) i += 1 if structural_mode == "structure": diff --git a/tests/test_pageindex_structural_read.py b/tests/test_pageindex_structural_read.py index 500e4c53e..cd104c516 100644 --- a/tests/test_pageindex_structural_read.py +++ b/tests/test_pageindex_structural_read.py @@ -481,11 +481,18 @@ def test_cat_structure_page_node_and_text_outputs_are_hard_limited(): executor.execute("cat dsid_limited_pdf --page 1-4") nodes = json.loads( - executor.execute("cat dsid_limited_pdf --node 0001,0002,0003,0004,0005") + executor.execute("cat dsid_limited_pdf --node 0001 0002 0003 0004 0005") ) assert nodes["data"]["node_ids"] == ["0001", "0002", "0003", "0004", "0005"] + comma_nodes = json.loads( + executor.execute("cat dsid_limited_pdf --node 0001,0002") + ) + assert comma_nodes["data"]["node_ids"] == ["0001", "0002"] with pytest.raises(PIFSCommandError, match="at most 5"): - executor.execute("cat dsid_limited_pdf --node 0001,0002,0003,0004,0005,0006") + executor.execute("cat dsid_limited_pdf --node 0001 0002 0003 0004 0005 0006") + + with pytest.raises(PIFSCommandError, match="cat accepts one file target"): + executor.execute("cat dsid_limited_pdf 0001") text = json.loads(executor.execute("cat dsid_long_text --all")) assert "line 100" in text["data"]["text"] From cd571ddbaf0d7ce84856c320d88f1b844ca0d298 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Tue, 26 May 2026 20:57:08 +0800 Subject: [PATCH 41/50] fix(filesystem): relax structural read limits --- pageindex/filesystem/agent.py | 11 ++++--- pageindex/filesystem/commands.py | 14 ++++---- tests/test_pageindex_structural_read.py | 44 ++++++++++++++++++++----- tests/test_pifs_agent_stream.py | 2 ++ 4 files changed, 52 insertions(+), 19 deletions(-) diff --git a/pageindex/filesystem/agent.py b/pageindex/filesystem/agent.py index 4f796ec26..facaceb6f 100644 --- a/pageindex/filesystem/agent.py +++ b/pageindex/filesystem/agent.py @@ -74,8 +74,10 @@ cat --node ; use cat --page when the user asks for page-level evidence, no suitable node exists, or exact page text is needed. cat --structure is paginated; request more with --offset if needed. Page -reads are limited to three pages at once, node reads to at most five node ids, -and text cat --all returns only the first page of text lines. +reads are limited to five pages at once, node reads to at most ten node ids, +and text cat --all returns only the first page of text lines. If a cat limit +error requires a smaller call, stop when the evidence is sufficient; otherwise +continue with another chunk before answering. For questions about metadata fields, available summaries, or whether metadata was provided, inspect stat --schema and stat before making claims. Do not use stat as a general content/topic discovery step. For document Q&A, @@ -101,8 +103,9 @@ - Use stat only for metadata/schema/status questions or to resolve ambiguous target identity. Do not run stat merely to understand what a document says. - Prefer target-first cat syntax with stable targets: cat --structure, cat --page 31-59, cat --node . - cat --structure returns at most 25 nodes; use --offset and --limit for more structure pages. -- cat --page accepts at most 3 pages at once. If a larger range is needed, first inspect cat --structure and then read a smaller page range or node. -- cat --node accepts at most 5 node ids at once. Prefer one relevant node when possible. +- cat --page accepts at most 5 pages at once. If a larger range is needed, first inspect cat --structure and then read a smaller page range or node. +- cat --node accepts at most 10 node ids at once. Prefer relevant nodes from structure when possible. +- When recovering from cat page/node/text limit errors, stop if the evidence is sufficient; if it is not sufficient, make another smaller call before answering. - cat --all returns at most 100 text lines; use cat --range - for the next page. - After cat --structure finds a relevant section/subsection with a node_id, prefer cat --node for content from that semantic unit. - Use cat --page - when the user explicitly asks for pages/page ranges, when no suitable node_id exists, or when you need exact page text to verify page-level evidence. diff --git a/pageindex/filesystem/commands.py b/pageindex/filesystem/commands.py index 29ea46a8d..6341b8baf 100644 --- a/pageindex/filesystem/commands.py +++ b/pageindex/filesystem/commands.py @@ -54,9 +54,9 @@ class PIFSCommandExecutor: MAX_GREP_LIMIT = 20 MAX_SEMANTIC_LIMIT = 20 MAX_TEXT_LINES = 100 - MAX_PAGE_SPAN = 3 + MAX_PAGE_SPAN = 5 MAX_STRUCTURE_NODES = 25 - MAX_NODE_IDS = 5 + MAX_NODE_IDS = 10 MAX_NODE_TEXT_LINES = 100 MAX_NODE_TEXT_CHARS = 12_000 MAX_STAT_FIELD_TARGETS = 20 @@ -107,8 +107,8 @@ def describe_available_command_surfaces(self) -> str: "- find -maxdepth N -type f|d: bounded folder traversal for find", "- grep -R: recursive lexical/FTS search only; semantic vector prefilter is disabled", "- cat --structure: cached PageIndex node list, paginated at 25 nodes", - "- cat --page: cached PageIndex page reads, limited to 3 pages", - "- cat --node: cached PageIndex node reads, limited to 5 node ids", + "- cat --page: cached PageIndex page reads, limited to 5 pages", + "- cat --node: cached PageIndex node reads, limited to 10 node ids", "- cat --all: text artifact reads for txt/text files, paginated at 100 lines", "- stat --field : one metadata field across up to 20 documents", ] @@ -2003,8 +2003,10 @@ def _require_at_most(cls, value: int, label: str, max_value: int) -> int: if value > max_value: raise PIFSCommandError( f"{label} supports at most {max_value}; requested {value}. " - "Use a smaller value. If you are unsure where to inspect, " - "use cat --structure first." + "Split it into a smaller call. If the evidence is sufficient, " + "stop; if not, continue with additional chunks before " + "answering. If you are unsure where to inspect, use cat " + "--structure first." ) return value diff --git a/tests/test_pageindex_structural_read.py b/tests/test_pageindex_structural_read.py index cd104c516..f5b6deae9 100644 --- a/tests/test_pageindex_structural_read.py +++ b/tests/test_pageindex_structural_read.py @@ -474,22 +474,48 @@ def test_cat_structure_page_node_and_text_outputs_are_hard_limited(): assert len(second_structure["data"]["structure"]) == 5 assert second_structure["data"]["structure"][0]["node_id"] == "0026" - pages = json.loads(executor.execute("cat dsid_limited_pdf --page 1-3")) - assert pages["data"]["text"] == "Page 1 text\n\nPage 2 text\n\nPage 3 text" - assert pages["data"]["page_pagination"]["limit"] == 3 - with pytest.raises(PIFSCommandError, match="at most 3"): - executor.execute("cat dsid_limited_pdf --page 1-4") + pages = json.loads(executor.execute("cat dsid_limited_pdf --page 1-5")) + assert pages["data"]["text"] == ( + "Page 1 text\n\nPage 2 text\n\nPage 3 text\n\nPage 4 text\n\nPage 5 text" + ) + assert pages["data"]["page_pagination"]["limit"] == 5 + with pytest.raises(PIFSCommandError, match="at most 5"): + executor.execute("cat dsid_limited_pdf --page 1-6") + with pytest.raises(PIFSCommandError, match="evidence is sufficient"): + executor.execute("cat dsid_limited_pdf --page 1-6") nodes = json.loads( - executor.execute("cat dsid_limited_pdf --node 0001 0002 0003 0004 0005") + executor.execute( + "cat dsid_limited_pdf --node 0001 0002 0003 0004 0005 " + "0006 0007 0008 0009 0010" + ) ) - assert nodes["data"]["node_ids"] == ["0001", "0002", "0003", "0004", "0005"] + assert nodes["data"]["node_ids"] == [ + "0001", + "0002", + "0003", + "0004", + "0005", + "0006", + "0007", + "0008", + "0009", + "0010", + ] comma_nodes = json.loads( executor.execute("cat dsid_limited_pdf --node 0001,0002") ) assert comma_nodes["data"]["node_ids"] == ["0001", "0002"] - with pytest.raises(PIFSCommandError, match="at most 5"): - executor.execute("cat dsid_limited_pdf --node 0001 0002 0003 0004 0005 0006") + with pytest.raises(PIFSCommandError, match="at most 10"): + executor.execute( + "cat dsid_limited_pdf --node 0001 0002 0003 0004 0005 " + "0006 0007 0008 0009 0010 0011" + ) + with pytest.raises(PIFSCommandError, match="continue with additional chunks"): + executor.execute( + "cat dsid_limited_pdf --node 0001 0002 0003 0004 0005 " + "0006 0007 0008 0009 0010 0011" + ) with pytest.raises(PIFSCommandError, match="cat accepts one file target"): executor.execute("cat dsid_limited_pdf 0001") diff --git a/tests/test_pifs_agent_stream.py b/tests/test_pifs_agent_stream.py index 9fe62c101..5dae40bd7 100644 --- a/tests/test_pifs_agent_stream.py +++ b/tests/test_pifs_agent_stream.py @@ -202,6 +202,8 @@ def test_prompt_tells_agent_when_to_choose_node_or_page(self): self.assertIn("prefer cat --node ", AGENT_TOOL_POLICY) self.assertIn("page-level evidence", AGENT_TOOL_POLICY) self.assertIn("prefer\ncat --node ", BASH_TOOL_DESCRIPTION) + self.assertIn("stop if the evidence is sufficient", AGENT_TOOL_POLICY) + self.assertIn("continue with another chunk before answering", BASH_TOOL_DESCRIPTION) def test_prompt_requires_stat_for_metadata_questions(self): self.assertIn("stat --schema and stat ", AGENT_TOOL_POLICY) From 311c7b5f1c553836d3949ce846c3f136271988cd Mon Sep 17 00:00:00 2001 From: BukeLy Date: Wed, 27 May 2026 01:53:46 +0800 Subject: [PATCH 42/50] fix(filesystem): preserve lazy filesystem exports --- pageindex/filesystem/__init__.py | 13 +++++++++- pageindex/filesystem/cli.py | 1 - pageindex/filesystem/commands.py | 3 +-- tests/test_pageindex_filesystem_scope.py | 30 ++++++++++++++++++++++++ tests/test_pageindex_structural_read.py | 3 ++- 5 files changed, 45 insertions(+), 5 deletions(-) diff --git a/pageindex/filesystem/__init__.py b/pageindex/filesystem/__init__.py index 2c8fd1b9b..7908393d8 100644 --- a/pageindex/filesystem/__init__.py +++ b/pageindex/filesystem/__init__.py @@ -1,4 +1,5 @@ from importlib import import_module +from typing import TYPE_CHECKING from .commands import PIFSCommandExecutor from .core import PageIndexFileSystem @@ -11,6 +12,16 @@ ) from .types import OpenResult, SearchResult +if TYPE_CHECKING: + from .hybrid_projection import HybridProjectionSearchBackend + from .projection_indexing import SummaryProjectionIndexer + from .semantic_index import ( + RebuildableSemanticIndex, + SemanticIndexRecord, + SemanticSearchResult, + SQLiteVecSemanticIndex, + ) + _LAZY_EXPORTS = { "HybridProjectionSearchBackend": (".hybrid_projection", "HybridProjectionSearchBackend"), "RebuildableSemanticIndex": (".semantic_index", "RebuildableSemanticIndex"), @@ -49,4 +60,4 @@ def __getattr__(name: str): def __dir__() -> list[str]: - return sorted(set(globals()) | set(__all__)) + return sorted(set(globals()) | set(__all__) | set(_LAZY_EXPORTS)) diff --git a/pageindex/filesystem/cli.py b/pageindex/filesystem/cli.py index 24a78f4de..8af12e649 100644 --- a/pageindex/filesystem/cli.py +++ b/pageindex/filesystem/cli.py @@ -284,7 +284,6 @@ def main(argv: list[str] | None = None) -> int: except Exception as exc: print(f"ERROR: {exc}", file=sys.stderr) return 1 - return 0 if __name__ == "__main__": diff --git a/pageindex/filesystem/commands.py b/pageindex/filesystem/commands.py index 6341b8baf..73470a625 100644 --- a/pageindex/filesystem/commands.py +++ b/pageindex/filesystem/commands.py @@ -1050,7 +1050,6 @@ def _stat_field_row(self, field: str, target: str) -> dict[str, Any]: row["folder_paths"] = folder_paths metadata = info.get("metadata") or {} raw_value = metadata.get(field) - value_text = "" if raw_value is None else str(raw_value) row.update( { "field": field, @@ -1652,7 +1651,7 @@ def _source_root(self) -> Path | None: storage_path = Path(row["storage_uri"]) source_path = Path(row["source_path"]) root = storage_path - for _part in source_path.parts: + for _ in range(len(source_path.parts)): root = root.parent return root diff --git a/tests/test_pageindex_filesystem_scope.py b/tests/test_pageindex_filesystem_scope.py index 5bd730c11..7c9e31bd1 100644 --- a/tests/test_pageindex_filesystem_scope.py +++ b/tests/test_pageindex_filesystem_scope.py @@ -4,6 +4,36 @@ import pytest +def test_filesystem_lazy_exports_remain_public(): + import pageindex.filesystem as filesystem + from pageindex.filesystem import ( + HybridProjectionSearchBackend, + RebuildableSemanticIndex, + SemanticIndexRecord, + SemanticSearchResult, + SQLiteVecSemanticIndex, + SummaryProjectionIndexer, + ) + + for name in ( + "HybridProjectionSearchBackend", + "RebuildableSemanticIndex", + "SemanticIndexRecord", + "SemanticSearchResult", + "SQLiteVecSemanticIndex", + "SummaryProjectionIndexer", + ): + assert name in filesystem.__all__ + assert name in dir(filesystem) + + assert HybridProjectionSearchBackend.__name__ == "HybridProjectionSearchBackend" + assert RebuildableSemanticIndex.__name__ == "RebuildableSemanticIndex" + assert SemanticIndexRecord.__name__ == "SemanticIndexRecord" + assert SemanticSearchResult.__name__ == "SemanticSearchResult" + assert SQLiteVecSemanticIndex.__name__ == "SQLiteVecSemanticIndex" + assert SummaryProjectionIndexer.__name__ == "SummaryProjectionIndexer" + + class SummaryBackend: def __init__(self, document_id): self.document_id = document_id diff --git a/tests/test_pageindex_structural_read.py b/tests/test_pageindex_structural_read.py index f5b6deae9..a78f1d15a 100644 --- a/tests/test_pageindex_structural_read.py +++ b/tests/test_pageindex_structural_read.py @@ -640,7 +640,8 @@ def test_cat_all_is_limited_to_text_files(): filesystem.open("dsid_md_file") with pytest.raises(PIFSCommandError, match="only supported for txt/text files"): executor.execute("cat dsid_json_file --all") - assert filesystem.open("dsid_json_file").text == '{"body":"json"}' + opened_json = filesystem.open("dsid_json_file") + assert opened_json.text == '{"body":"json"}' for command in ( "head dsid_pdf_file", "tail dsid_pdf_file", From 346eb0aa4fc74996f4433200327ce90bcd5f09ab Mon Sep 17 00:00:00 2001 From: BukeLy Date: Wed, 27 May 2026 02:13:05 +0800 Subject: [PATCH 43/50] chore(filesystem): remove uv project files --- pyproject.toml | 24 - uv.lock | 1988 ------------------------------------------------ 2 files changed, 2012 deletions(-) delete mode 100644 pyproject.toml delete mode 100644 uv.lock diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index aaea9f881..000000000 --- a/pyproject.toml +++ /dev/null @@ -1,24 +0,0 @@ -[project] -name = "pageindex" -version = "0.1.0" -requires-python = ">=3.12" -dependencies = [ - "litellm==1.83.7", - "openai-agents>=0.17.2", - "pymupdf==1.26.4", - "pypdf2==3.0.1", - "pytest>=9.0.3", - "python-dotenv==1.1.0", - "pyyaml==6.0.2", - "sqlite-vec>=0.1.9", -] - -[project.scripts] -pifs = "pageindex.filesystem.cli:main" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel] -packages = ["pageindex"] diff --git a/uv.lock b/uv.lock deleted file mode 100644 index 41ed9e4fe..000000000 --- a/uv.lock +++ /dev/null @@ -1,1988 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.12" - -[[package]] -name = "aiohappyeyeballs" -version = "2.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, -] - -[[package]] -name = "aiohttp" -version = "3.13.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohappyeyeballs" }, - { name = "aiosignal" }, - { name = "attrs" }, - { name = "frozenlist" }, - { name = "multidict" }, - { name = "propcache" }, - { name = "yarl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, - { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, - { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, - { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, - { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, - { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, - { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, - { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, - { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, - { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, - { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, - { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, - { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, - { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, - { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, - { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, - { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, - { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, - { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, - { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, - { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, - { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, - { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, - { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, - { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, - { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, - { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, - { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, - { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, - { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, - { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, - { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, - { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, - { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, - { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, - { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, - { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, - { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, - { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, - { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, - { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, - { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, - { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, - { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, - { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, - { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, - { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, - { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, - { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, - { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, - { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, - { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, - { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, - { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, - { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, - { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, - { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, -] - -[[package]] -name = "aiosignal" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "frozenlist" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, -] - -[[package]] -name = "annotated-doc" -version = "0.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, -] - -[[package]] -name = "anyio" -version = "4.13.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, -] - -[[package]] -name = "attrs" -version = "26.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, -] - -[[package]] -name = "certifi" -version = "2026.4.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, -] - -[[package]] -name = "cffi" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, - { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, - { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, - { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, - { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, - { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, - { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, - { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, - { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, - { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, - { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, - { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, - { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, - { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, - { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, - { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, - { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, - { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, - { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, - { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, - { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, - { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, - { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, - { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, - { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, - { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, - { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, - { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, - { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, - { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, - { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, - { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, - { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, - { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, - { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, - { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, - { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, - { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, - { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, - { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, - { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, - { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, - { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, -] - -[[package]] -name = "click" -version = "8.1.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "cryptography" -version = "48.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, - { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, - { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, - { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, - { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, - { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, - { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, - { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, - { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, - { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, - { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, - { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, - { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, - { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, - { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, - { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, - { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, - { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, - { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, - { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, - { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, - { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, - { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, - { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, - { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, - { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, - { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, - { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, - { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, - { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, - { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, - { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, - { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, - { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, -] - -[[package]] -name = "distro" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, -] - -[[package]] -name = "fastuuid" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232, upload-time = "2025-10-19T22:19:22.402Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/a2/e78fcc5df65467f0d207661b7ef86c5b7ac62eea337c0c0fcedbeee6fb13/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", size = 510164, upload-time = "2025-10-19T22:31:45.635Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b3/c846f933f22f581f558ee63f81f29fa924acd971ce903dab1a9b6701816e/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", size = 261837, upload-time = "2025-10-19T22:38:38.53Z" }, - { url = "https://files.pythonhosted.org/packages/54/ea/682551030f8c4fa9a769d9825570ad28c0c71e30cf34020b85c1f7ee7382/fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", size = 251370, upload-time = "2025-10-19T22:40:26.07Z" }, - { url = "https://files.pythonhosted.org/packages/14/dd/5927f0a523d8e6a76b70968e6004966ee7df30322f5fc9b6cdfb0276646a/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", size = 277766, upload-time = "2025-10-19T22:37:23.779Z" }, - { url = "https://files.pythonhosted.org/packages/16/6e/c0fb547eef61293153348f12e0f75a06abb322664b34a1573a7760501336/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", size = 278105, upload-time = "2025-10-19T22:26:56.821Z" }, - { url = "https://files.pythonhosted.org/packages/2d/b1/b9c75e03b768f61cf2e84ee193dc18601aeaf89a4684b20f2f0e9f52b62c/fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", size = 301564, upload-time = "2025-10-19T22:30:31.604Z" }, - { url = "https://files.pythonhosted.org/packages/fc/fa/f7395fdac07c7a54f18f801744573707321ca0cee082e638e36452355a9d/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", size = 459659, upload-time = "2025-10-19T22:31:32.341Z" }, - { url = "https://files.pythonhosted.org/packages/66/49/c9fd06a4a0b1f0f048aacb6599e7d96e5d6bc6fa680ed0d46bf111929d1b/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", size = 478430, upload-time = "2025-10-19T22:26:22.962Z" }, - { url = "https://files.pythonhosted.org/packages/be/9c/909e8c95b494e8e140e8be6165d5fc3f61fdc46198c1554df7b3e1764471/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", size = 450894, upload-time = "2025-10-19T22:27:01.647Z" }, - { url = "https://files.pythonhosted.org/packages/90/eb/d29d17521976e673c55ef7f210d4cdd72091a9ec6755d0fd4710d9b3c871/fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176", size = 154374, upload-time = "2025-10-19T22:29:19.879Z" }, - { url = "https://files.pythonhosted.org/packages/cc/fc/f5c799a6ea6d877faec0472d0b27c079b47c86b1cdc577720a5386483b36/fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397", size = 156550, upload-time = "2025-10-19T22:27:49.658Z" }, - { url = "https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021", size = 510720, upload-time = "2025-10-19T22:42:34.633Z" }, - { url = "https://files.pythonhosted.org/packages/53/b0/a4b03ff5d00f563cc7546b933c28cb3f2a07344b2aec5834e874f7d44143/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc", size = 262024, upload-time = "2025-10-19T22:30:25.482Z" }, - { url = "https://files.pythonhosted.org/packages/9c/6d/64aee0a0f6a58eeabadd582e55d0d7d70258ffdd01d093b30c53d668303b/fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5", size = 251679, upload-time = "2025-10-19T22:36:14.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/f5/a7e9cda8369e4f7919d36552db9b2ae21db7915083bc6336f1b0082c8b2e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f", size = 277862, upload-time = "2025-10-19T22:36:23.302Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d3/8ce11827c783affffd5bd4d6378b28eb6cc6d2ddf41474006b8d62e7448e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87", size = 278278, upload-time = "2025-10-19T22:29:43.809Z" }, - { url = "https://files.pythonhosted.org/packages/a2/51/680fb6352d0bbade04036da46264a8001f74b7484e2fd1f4da9e3db1c666/fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b", size = 301788, upload-time = "2025-10-19T22:36:06.825Z" }, - { url = "https://files.pythonhosted.org/packages/fa/7c/2014b5785bd8ebdab04ec857635ebd84d5ee4950186a577db9eff0fb8ff6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022", size = 459819, upload-time = "2025-10-19T22:35:31.623Z" }, - { url = "https://files.pythonhosted.org/packages/01/d2/524d4ceeba9160e7a9bc2ea3e8f4ccf1ad78f3bde34090ca0c51f09a5e91/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995", size = 478546, upload-time = "2025-10-19T22:26:03.023Z" }, - { url = "https://files.pythonhosted.org/packages/bc/17/354d04951ce114bf4afc78e27a18cfbd6ee319ab1829c2d5fb5e94063ac6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab", size = 450921, upload-time = "2025-10-19T22:31:02.151Z" }, - { url = "https://files.pythonhosted.org/packages/fb/be/d7be8670151d16d88f15bb121c5b66cdb5ea6a0c2a362d0dcf30276ade53/fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad", size = 154559, upload-time = "2025-10-19T22:36:36.011Z" }, - { url = "https://files.pythonhosted.org/packages/22/1d/5573ef3624ceb7abf4a46073d3554e37191c868abc3aecd5289a72f9810a/fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed", size = 156539, upload-time = "2025-10-19T22:33:35.898Z" }, - { url = "https://files.pythonhosted.org/packages/16/c9/8c7660d1fe3862e3f8acabd9be7fc9ad71eb270f1c65cce9a2b7a31329ab/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad", size = 510600, upload-time = "2025-10-19T22:43:44.17Z" }, - { url = "https://files.pythonhosted.org/packages/4c/f4/a989c82f9a90d0ad995aa957b3e572ebef163c5299823b4027986f133dfb/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b", size = 262069, upload-time = "2025-10-19T22:43:38.38Z" }, - { url = "https://files.pythonhosted.org/packages/da/6c/a1a24f73574ac995482b1326cf7ab41301af0fabaa3e37eeb6b3df00e6e2/fastuuid-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714", size = 251543, upload-time = "2025-10-19T22:32:22.537Z" }, - { url = "https://files.pythonhosted.org/packages/1a/20/2a9b59185ba7a6c7b37808431477c2d739fcbdabbf63e00243e37bd6bf49/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f", size = 277798, upload-time = "2025-10-19T22:33:53.821Z" }, - { url = "https://files.pythonhosted.org/packages/ef/33/4105ca574f6ded0af6a797d39add041bcfb468a1255fbbe82fcb6f592da2/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f", size = 278283, upload-time = "2025-10-19T22:29:02.812Z" }, - { url = "https://files.pythonhosted.org/packages/fe/8c/fca59f8e21c4deb013f574eae05723737ddb1d2937ce87cb2a5d20992dc3/fastuuid-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75", size = 301627, upload-time = "2025-10-19T22:35:54.985Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e2/f78c271b909c034d429218f2798ca4e89eeda7983f4257d7865976ddbb6c/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4", size = 459778, upload-time = "2025-10-19T22:28:00.999Z" }, - { url = "https://files.pythonhosted.org/packages/1e/f0/5ff209d865897667a2ff3e7a572267a9ced8f7313919f6d6043aed8b1caa/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad", size = 478605, upload-time = "2025-10-19T22:36:21.764Z" }, - { url = "https://files.pythonhosted.org/packages/e0/c8/2ce1c78f983a2c4987ea865d9516dbdfb141a120fd3abb977ae6f02ba7ca/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8", size = 450837, upload-time = "2025-10-19T22:34:37.178Z" }, - { url = "https://files.pythonhosted.org/packages/df/60/dad662ec9a33b4a5fe44f60699258da64172c39bd041da2994422cdc40fe/fastuuid-0.14.0-cp314-cp314-win32.whl", hash = "sha256:e23fc6a83f112de4be0cc1990e5b127c27663ae43f866353166f87df58e73d06", size = 154532, upload-time = "2025-10-19T22:35:18.217Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f6/da4db31001e854025ffd26bc9ba0740a9cbba2c3259695f7c5834908b336/fastuuid-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:df61342889d0f5e7a32f7284e55ef95103f2110fee433c2ae7c2c0956d76ac8a", size = 156457, upload-time = "2025-10-19T22:33:44.579Z" }, -] - -[[package]] -name = "filelock" -version = "3.29.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, -] - -[[package]] -name = "frozenlist" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, - { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, - { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, - { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, - { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, - { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, - { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, - { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, - { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, - { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, - { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, - { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, - { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, - { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, - { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, - { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, - { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, - { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, - { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, - { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, - { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, - { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, - { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, - { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, - { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, - { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, - { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, - { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, - { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, - { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, - { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, - { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, - { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, - { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, - { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, - { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, - { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, - { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, - { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, - { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, - { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, - { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, - { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, - { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, - { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, - { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, - { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, - { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, - { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, - { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, - { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, - { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, - { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, - { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, - { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, - { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, - { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, - { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, - { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, - { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, -] - -[[package]] -name = "fsspec" -version = "2026.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d5/8d/1c51c094345df128ca4a990d633fe1a0ff28726c9e6b3c41ba65087bba1d/fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4", size = 312760, upload-time = "2026-04-29T20:42:38.635Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, -] - -[[package]] -name = "griffelib" -version = "2.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, -] - -[[package]] -name = "h11" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, -] - -[[package]] -name = "hf-xet" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" }, - { url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" }, - { url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" }, - { url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" }, - { url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" }, - { url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" }, - { url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" }, - { url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" }, - { url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" }, - { url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" }, - { url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" }, - { url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" }, - { url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" }, - { url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" }, - { url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" }, - { url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" }, - { url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" }, - { url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" }, - { url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" }, - { url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" }, - { url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" }, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, -] - -[[package]] -name = "httpx-sse" -version = "0.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, -] - -[[package]] -name = "huggingface-hub" -version = "1.14.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "filelock" }, - { name = "fsspec" }, - { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, - { name = "httpx" }, - { name = "packaging" }, - { name = "pyyaml" }, - { name = "tqdm" }, - { name = "typer" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/39/40/43109e943fd718b0ccd0cd61eb4f1c347df22bf81f5874c6f22adf44bcff/huggingface_hub-1.14.0.tar.gz", hash = "sha256:d6d2c9cd6be1d02ae9ec6672d5587d10a427f377db688e82528f426a041622c2", size = 782365, upload-time = "2026-05-06T14:14:34.278Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/a5/33b49ba7bea7c41bb37f74ec0f8beea0831e052330196633fe2c77516ea6/huggingface_hub-1.14.0-py3-none-any.whl", hash = "sha256:efe075535c62e130b30e836b138e13785f6f043d1f0539e0a39aa411a99e90b8", size = 661479, upload-time = "2026-05-06T14:14:32.029Z" }, -] - -[[package]] -name = "idna" -version = "3.15" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, -] - -[[package]] -name = "importlib-metadata" -version = "8.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, -] - -[[package]] -name = "jiter" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6e/c1/0cddc6eb17d4c53a99840953f95dd3accdc5cfc7a337b0e9b26476276be9/jiter-0.14.0.tar.gz", hash = "sha256:e8a39e66dac7153cf3f964a12aad515afa8d74938ec5cc0018adcdae5367c79e", size = 165725, upload-time = "2026-04-10T14:28:42.01Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/68/7390a418f10897da93b158f2d5a8bd0bcd73a0f9ec3bb36917085bb759ef/jiter-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2fb2ce3a7bc331256dfb14cefc34832366bb28a9aca81deaf43bbf2a5659e607", size = 316295, upload-time = "2026-04-10T14:26:24.887Z" }, - { url = "https://files.pythonhosted.org/packages/60/a0/5854ac00ff63551c52c6c89534ec6aba4b93474e7924d64e860b1c94165b/jiter-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5252a7ca23785cef5d02d4ece6077a1b556a410c591b379f82091c3001e14844", size = 315898, upload-time = "2026-04-10T14:26:26.601Z" }, - { url = "https://files.pythonhosted.org/packages/41/a1/4f44832650a16b18e8391f1bf1d6ca4909bc738351826bcc198bba4357f4/jiter-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c409578cbd77c338975670ada777add4efd53379667edf0aceea730cabede6fb", size = 343730, upload-time = "2026-04-10T14:26:28.326Z" }, - { url = "https://files.pythonhosted.org/packages/48/64/a329e9d469f86307203594b1707e11ae51c3348d03bfd514a5f997870012/jiter-0.14.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ede4331a1899d604463369c730dbb961ffdc5312bc7f16c41c2896415b1304a", size = 370102, upload-time = "2026-04-10T14:26:30.089Z" }, - { url = "https://files.pythonhosted.org/packages/94/c1/5e3dfc59635aa4d4c7bd20a820ac1d09b8ed851568356802cf1c08edb3cf/jiter-0.14.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92cd8b6025981a041f5310430310b55b25ca593972c16407af8837d3d7d2ca01", size = 461335, upload-time = "2026-04-10T14:26:31.911Z" }, - { url = "https://files.pythonhosted.org/packages/e3/1b/dd157009dbc058f7b00108f545ccb72a2d56461395c4fc7b9cfdccb00af4/jiter-0.14.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:351bf6eda4e3a7ceb876377840c702e9a3e4ecc4624dbfb2d6463c67ae52637d", size = 378536, upload-time = "2026-04-10T14:26:33.595Z" }, - { url = "https://files.pythonhosted.org/packages/91/78/256013667b7c10b8834f8e6e54cd3e562d4c6e34227a1596addccc05e38c/jiter-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dcfbeb93d9ecd9ca128bbf8910120367777973fa193fb9a39c31237d8df165", size = 353859, upload-time = "2026-04-10T14:26:35.098Z" }, - { url = "https://files.pythonhosted.org/packages/de/d9/137d65ade9093a409fe80955ce60b12bb753722c986467aeda47faf450ad/jiter-0.14.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ae039aaef8de3f8157ecc1fdd4d85043ac4f57538c245a0afaecb8321ec951c3", size = 357626, upload-time = "2026-04-10T14:26:36.685Z" }, - { url = "https://files.pythonhosted.org/packages/2e/48/76750835b87029342727c1a268bea8878ab988caf81ee4e7b880900eeb5a/jiter-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7d9d51eb96c82a9652933bd769fe6de66877d6eb2b2440e281f2938c51b5643e", size = 393172, upload-time = "2026-04-10T14:26:38.097Z" }, - { url = "https://files.pythonhosted.org/packages/a6/60/456c4e81d5c8045279aefe60e9e483be08793828800a4e64add8fdde7f2a/jiter-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d824ca4148b705970bf4e120924a212fdfca9859a73e42bd7889a63a4ea6bb98", size = 520300, upload-time = "2026-04-10T14:26:39.532Z" }, - { url = "https://files.pythonhosted.org/packages/a8/9f/2020e0984c235f678dced38fe4eec3058cf528e6af36ebf969b410305941/jiter-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff3a6465b3a0f54b1a430f45c3c0ba7d61ceb45cbc3e33f9e1a7f638d690baf3", size = 553059, upload-time = "2026-04-10T14:26:40.991Z" }, - { url = "https://files.pythonhosted.org/packages/ef/32/e2d298e1a22a4bbe6062136d1c7192db7dba003a6975e51d9a9eecabc4c2/jiter-0.14.0-cp312-cp312-win32.whl", hash = "sha256:5dec7c0a3e98d2a3f8a2e67382d0d7c3ac60c69103a4b271da889b4e8bb1e129", size = 206030, upload-time = "2026-04-10T14:26:42.517Z" }, - { url = "https://files.pythonhosted.org/packages/36/ac/96369141b3d8a4a8e4590e983085efe1c436f35c0cda940dd76d942e3e40/jiter-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:fc7e37b4b8bc7e80a63ad6cfa5fc11fab27dbfea4cc4ae644b1ab3f273dc348f", size = 201603, upload-time = "2026-04-10T14:26:44.328Z" }, - { url = "https://files.pythonhosted.org/packages/01/c3/75d847f264647017d7e3052bbcc8b1e24b95fa139c320c5f5066fa7a0bdd/jiter-0.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:ee4a72f12847ef29b072aee9ad5474041ab2924106bdca9fcf5d7d965853e057", size = 191525, upload-time = "2026-04-10T14:26:46Z" }, - { url = "https://files.pythonhosted.org/packages/97/2a/09f70020898507a89279659a1afe3364d57fc1b2c89949081975d135f6f5/jiter-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:af72f204cf4d44258e5b4c1745130ac45ddab0e71a06333b01de660ab4187a94", size = 315502, upload-time = "2026-04-10T14:26:47.697Z" }, - { url = "https://files.pythonhosted.org/packages/d6/be/080c96a45cd74f9fce5db4fd68510b88087fb37ffe2541ff73c12db92535/jiter-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4b77da71f6e819be5fbcec11a453fde5b1d0267ef6ed487e2a392fd8e14e4e3a", size = 314870, upload-time = "2026-04-10T14:26:49.149Z" }, - { url = "https://files.pythonhosted.org/packages/7d/5e/2d0fee155826a968a832cc32438de5e2a193292c8721ca70d0b53e58245b/jiter-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f4ea612fe8b84b8b04e51d0e78029ecf3466348e25973f953de6e6a59aa4c1", size = 343406, upload-time = "2026-04-10T14:26:50.762Z" }, - { url = "https://files.pythonhosted.org/packages/70/af/bf9ee0d3a4f8dc0d679fc1337f874fe60cdbf841ebbb304b374e1c9aaceb/jiter-0.14.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62fe2451f8fcc0240261e6a4df18ecbcd58327857e61e625b2393ea3b468aac9", size = 369415, upload-time = "2026-04-10T14:26:52.188Z" }, - { url = "https://files.pythonhosted.org/packages/0f/83/8e8561eadba31f4d3948a5b712fb0447ec71c3560b57a855449e7b8ddc98/jiter-0.14.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6112f26f5afc75bcb475787d29da3aa92f9d09c7858f632f4be6ffe607be82e9", size = 461456, upload-time = "2026-04-10T14:26:53.611Z" }, - { url = "https://files.pythonhosted.org/packages/f6/c9/c5299e826a5fe6108d172b344033f61c69b1bb979dd8d9ddd4278a160971/jiter-0.14.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:215a6cb8fb7dc702aa35d475cc00ddc7f970e5c0b1417fb4b4ac5d82fa2a29db", size = 378488, upload-time = "2026-04-10T14:26:55.211Z" }, - { url = "https://files.pythonhosted.org/packages/5d/37/c16d9d15c0a471b8644b1abe3c82668092a707d9bedcf076f24ff2e380cd/jiter-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ab96a30fb3cb2c7e0cd33f7616c8860da5f5674438988a54ac717caccdbaa", size = 353242, upload-time = "2026-04-10T14:26:56.705Z" }, - { url = "https://files.pythonhosted.org/packages/58/ea/8050cb0dc654e728e1bfacbc0c640772f2181af5dedd13ae70145743a439/jiter-0.14.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3a99c1387b1f2928f799a9de899193484d66206a50e98233b6b088a7f0c1edb2", size = 356823, upload-time = "2026-04-10T14:26:58.281Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3b/cf71506d270e5f84d97326bf220e47aed9b95e9a4a060758fb07772170ab/jiter-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ab18d11074485438695f8d34a1b6da61db9754248f96d51341956607a8f39985", size = 392564, upload-time = "2026-04-10T14:27:00.018Z" }, - { url = "https://files.pythonhosted.org/packages/b0/cc/8c6c74a3efb5bd671bfd14f51e8a73375464ca914b1551bc3b40e26ac2c9/jiter-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:801028dcfc26ac0895e4964cbc0fd62c73be9fd4a7d7b1aaf6e5790033a719b7", size = 520322, upload-time = "2026-04-10T14:27:01.664Z" }, - { url = "https://files.pythonhosted.org/packages/41/24/68d7b883ec959884ddf00d019b2e0e82ba81b167e1253684fa90519ce33c/jiter-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ad425b087aafb4a1c7e1e98a279200743b9aaf30c3e0ba723aec93f061bd9bc8", size = 552619, upload-time = "2026-04-10T14:27:03.316Z" }, - { url = "https://files.pythonhosted.org/packages/b6/89/b1a0985223bbf3150ff9e8f46f98fc9360c1de94f48abe271bbe1b465682/jiter-0.14.0-cp313-cp313-win32.whl", hash = "sha256:882bcb9b334318e233950b8be366fe5f92c86b66a7e449e76975dfd6d776a01f", size = 205699, upload-time = "2026-04-10T14:27:04.662Z" }, - { url = "https://files.pythonhosted.org/packages/4c/19/3f339a5a7f14a11730e67f6be34f9d5105751d547b615ef593fa122a5ded/jiter-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:9b8c571a5dba09b98bd3462b5a53f27209a5cbbe85670391692ede71974e979f", size = 201323, upload-time = "2026-04-10T14:27:06.139Z" }, - { url = "https://files.pythonhosted.org/packages/50/56/752dd89c84be0e022a8ea3720bcfa0a8431db79a962578544812ce061739/jiter-0.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:34f19dcc35cb1abe7c369b3756babf8c7f04595c0807a848df8f26ef8298ef92", size = 191099, upload-time = "2026-04-10T14:27:07.564Z" }, - { url = "https://files.pythonhosted.org/packages/91/28/292916f354f25a1fe8cf2c918d1415c699a4a659ae00be0430e1c5d9ffea/jiter-0.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e89bcd7d426a75bb4952c696b267075790d854a07aad4c9894551a82c5b574ab", size = 320880, upload-time = "2026-04-10T14:27:09.326Z" }, - { url = "https://files.pythonhosted.org/packages/ad/c7/b002a7d8b8957ac3d469bd59c18ef4b1595a5216ae0de639a287b9816023/jiter-0.14.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b25beaa0d4447ea8c7ae0c18c688905d34840d7d0b937f2f7bdd52162c98a40", size = 346563, upload-time = "2026-04-10T14:27:11.287Z" }, - { url = "https://files.pythonhosted.org/packages/f9/3b/f8d07580d8706021d255a6356b8fab13ee4c869412995550ce6ed4ddf97d/jiter-0.14.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:651a8758dd413c51e3b7f6557cdc6921faf70b14106f45f969f091f5cda990ea", size = 357928, upload-time = "2026-04-10T14:27:12.729Z" }, - { url = "https://files.pythonhosted.org/packages/47/5b/ac1a974da29e35507230383110ffec59998b290a8732585d04e19a9eb5ba/jiter-0.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e1a7eead856a5038a8d291f1447176ab0b525c77a279a058121b5fccee257f6f", size = 203519, upload-time = "2026-04-10T14:27:14.125Z" }, - { url = "https://files.pythonhosted.org/packages/96/6d/9fc8433d667d2454271378a79747d8c76c10b51b482b454e6190e511f244/jiter-0.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e692633a12cda97e352fdcd1c4acc971b1c28707e1e33aeef782b0cbf051975", size = 190113, upload-time = "2026-04-10T14:27:16.638Z" }, - { url = "https://files.pythonhosted.org/packages/4f/1e/354ed92461b165bd581f9ef5150971a572c873ec3b68a916d5aa91da3cc2/jiter-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:6f396837fc7577871ca8c12edaf239ed9ccef3bbe39904ae9b8b63ce0a48b140", size = 315277, upload-time = "2026-04-10T14:27:18.109Z" }, - { url = "https://files.pythonhosted.org/packages/a6/95/8c7c7028aa8636ac21b7a55faef3e34215e6ed0cbf5ae58258427f621aa3/jiter-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a4d50ea3d8ba4176f79754333bd35f1bbcd28e91adc13eb9b7ca91bc52a6cef9", size = 315923, upload-time = "2026-04-10T14:27:19.603Z" }, - { url = "https://files.pythonhosted.org/packages/47/40/e2a852a44c4a089f2681a16611b7ce113224a80fd8504c46d78491b47220/jiter-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce17f8a050447d1b4153bda4fb7d26e6a9e74eb4f4a41913f30934c5075bf615", size = 344943, upload-time = "2026-04-10T14:27:21.262Z" }, - { url = "https://files.pythonhosted.org/packages/fc/1f/670f92adee1e9895eac41e8a4d623b6da68c4d46249d8b556b60b63f949e/jiter-0.14.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4f1c4b125e1652aefbc2e2c1617b60a160ab789d180e3d423c41439e5f32850", size = 369725, upload-time = "2026-04-10T14:27:22.766Z" }, - { url = "https://files.pythonhosted.org/packages/01/2f/541c9ba567d05de1c4874a0f8f8c5e3fd78e2b874266623da9a775cf46e0/jiter-0.14.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be808176a6a3a14321d18c603f2d40741858a7c4fc982f83232842689fe86dd9", size = 461210, upload-time = "2026-04-10T14:27:24.315Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a9/c31cbec09627e0d5de7aeaec7690dba03e090caa808fefd8133137cf45bc/jiter-0.14.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26679d58ba816f88c3849306dd58cb863a90a1cf352cdd4ef67e30ccf8a77994", size = 380002, upload-time = "2026-04-10T14:27:26.155Z" }, - { url = "https://files.pythonhosted.org/packages/50/02/3c05c1666c41904a2f607475a73e7a4763d1cbde2d18229c4f85b22dc253/jiter-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80381f5a19af8fa9aef743f080e34f6b25ebd89656475f8cf0470ec6157052aa", size = 354678, upload-time = "2026-04-10T14:27:27.701Z" }, - { url = "https://files.pythonhosted.org/packages/7d/97/e15b33545c2b13518f560d695f974b9891b311641bdcf178d63177e8801e/jiter-0.14.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:004df5fdb8ecbd6d99f3227df18ba1a259254c4359736a2e6f036c944e02d7c5", size = 358920, upload-time = "2026-04-10T14:27:29.256Z" }, - { url = "https://files.pythonhosted.org/packages/ad/d2/8b1461def6b96ba44530df20d07ef7a1c7da22f3f9bf1727e2d611077bf1/jiter-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cff5708f7ed0fa098f2b53446c6fa74c48469118e5cd7497b4f1cd569ab06928", size = 394512, upload-time = "2026-04-10T14:27:31.344Z" }, - { url = "https://files.pythonhosted.org/packages/e3/88/837566dd6ed6e452e8d3205355afd484ce44b2533edfa4ed73a298ea893e/jiter-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:2492e5f06c36a976d25c7cc347a60e26d5470178d44cde1b9b75e60b4e519f28", size = 521120, upload-time = "2026-04-10T14:27:33.299Z" }, - { url = "https://files.pythonhosted.org/packages/89/6b/b00b45c4d1b4c031777fe161d620b755b5b02cdade1e316dcb46e4471d63/jiter-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7609cfbe3a03d37bfdbf5052012d5a879e72b83168a363deae7b3a26564d57de", size = 553668, upload-time = "2026-04-10T14:27:34.868Z" }, - { url = "https://files.pythonhosted.org/packages/ad/d8/6fe5b42011d19397433d345716eac16728ac241862a2aac9c91923c7509a/jiter-0.14.0-cp314-cp314-win32.whl", hash = "sha256:7282342d32e357543565286b6450378c3cd402eea333fc1ebe146f1fabb306fc", size = 207001, upload-time = "2026-04-10T14:27:36.455Z" }, - { url = "https://files.pythonhosted.org/packages/e5/43/5c2e08da1efad5e410f0eaaabeadd954812612c33fbbd8fd5328b489139d/jiter-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:bd77945f38866a448e73b0b7637366afa814d4617790ecd88a18ca74377e6c02", size = 202187, upload-time = "2026-04-10T14:27:38Z" }, - { url = "https://files.pythonhosted.org/packages/aa/1f/6e39ac0b4cdfa23e606af5b245df5f9adaa76f35e0c5096790da430ca506/jiter-0.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:f2d4c61da0821ee42e0cdf5489da60a6d074306313a377c2b35af464955a3611", size = 192257, upload-time = "2026-04-10T14:27:39.504Z" }, - { url = "https://files.pythonhosted.org/packages/05/57/7dbc0ffbbb5176a27e3518716608aa464aee2e2887dc938f0b900a120449/jiter-0.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1bf7ff85517dd2f20a5750081d2b75083c1b269cf75afc7511bdf1f9548beb3b", size = 323441, upload-time = "2026-04-10T14:27:41.039Z" }, - { url = "https://files.pythonhosted.org/packages/83/6e/7b3314398d8983f06b557aa21b670511ec72d3b79a68ee5e4d9bff972286/jiter-0.14.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8ef8791c3e78d6c6b157c6d360fbb5c715bebb8113bc6a9303c5caff012754a", size = 348109, upload-time = "2026-04-10T14:27:42.552Z" }, - { url = "https://files.pythonhosted.org/packages/ae/4f/8dc674bcd7db6dba566de73c08c763c337058baff1dbeb34567045b27cdc/jiter-0.14.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e74663b8b10da1fe0f4e4703fd7980d24ad17174b6bb35d8498d6e3ebce2ae6a", size = 368328, upload-time = "2026-04-10T14:27:44.574Z" }, - { url = "https://files.pythonhosted.org/packages/3b/5f/188e09a1f20906f98bbdec44ed820e19f4e8eb8aff88b9d1a5a497587ff3/jiter-0.14.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1aca29ba52913f78362ec9c2da62f22cdc4c3083313403f90c15460979b84d9b", size = 463301, upload-time = "2026-04-10T14:27:46.717Z" }, - { url = "https://files.pythonhosted.org/packages/ac/f0/19046ef965ed8f349e8554775bb12ff4352f443fbe12b95d31f575891256/jiter-0.14.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b39b7d87a952b79949af5fef44d2544e58c21a28da7f1bae3ef166455c61746", size = 378891, upload-time = "2026-04-10T14:27:48.32Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c3/da43bd8431ee175695777ee78cf0e93eacbb47393ff493f18c45231b427d/jiter-0.14.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d918a68b26e9fab068c2b5453577ef04943ab2807b9a6275df2a812599a310", size = 360749, upload-time = "2026-04-10T14:27:49.88Z" }, - { url = "https://files.pythonhosted.org/packages/72/26/e054771be889707c6161dbdec9c23d33a9ec70945395d70f07cfea1e9a6f/jiter-0.14.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:b08997c35aee1201c1a5361466a8fb9162d03ae7bf6568df70b6c859f1e654a4", size = 358526, upload-time = "2026-04-10T14:27:51.504Z" }, - { url = "https://files.pythonhosted.org/packages/c3/0f/7bea65ea2a6d91f2bf989ff11a18136644392bf2b0497a1fa50934c30a9c/jiter-0.14.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:260bf7ca20704d58d41f669e5e9fe7fe2fa72901a6b324e79056f5d52e9c9be2", size = 393926, upload-time = "2026-04-10T14:27:53.368Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/b1ff7d70deef61ac0b7c6c2f12d2ace950cdeecb4fdc94500a0926802857/jiter-0.14.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:37826e3df29e60f30a382f9294348d0238ef127f4b5d7f5f8da78b5b9e050560", size = 521052, upload-time = "2026-04-10T14:27:55.058Z" }, - { url = "https://files.pythonhosted.org/packages/0b/7b/3b0649983cbaf15eda26a414b5b1982e910c67bd6f7b1b490f3cfc76896a/jiter-0.14.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:645be49c46f2900937ba0eaf871ad5183c96858c0af74b6becc7f4e367e36e06", size = 553716, upload-time = "2026-04-10T14:27:57.269Z" }, - { url = "https://files.pythonhosted.org/packages/97/f8/33d78c83bd93ae0c0af05293a6660f88a1977caef39a6d72a84afab94ce0/jiter-0.14.0-cp314-cp314t-win32.whl", hash = "sha256:2f7877ed45118de283786178eceaf877110abacd04fde31efff3940ae9672674", size = 207957, upload-time = "2026-04-10T14:27:59.285Z" }, - { url = "https://files.pythonhosted.org/packages/d6/ac/2b760516c03e2227826d1f7025d89bf6bf6357a28fe75c2a2800873c50bf/jiter-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:14c0cb10337c49f5eafe8e7364daca5e29a020ea03580b8f8e6c597fed4e1588", size = 204690, upload-time = "2026-04-10T14:28:00.962Z" }, - { url = "https://files.pythonhosted.org/packages/dc/2e/a44c20c58aeed0355f2d326969a181696aeb551a25195f47563908a815be/jiter-0.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5419d4aa2024961da9fe12a9cfe7484996735dca99e8e090b5c88595ef1951ff", size = 191338, upload-time = "2026-04-10T14:28:02.853Z" }, - { url = "https://files.pythonhosted.org/packages/21/42/9042c3f3019de4adcb8c16591c325ec7255beea9fcd33a42a43f3b0b1000/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:fbd9e482663ca9d005d051330e4d2d8150bb208a209409c10f7e7dfdf7c49da9", size = 308810, upload-time = "2026-04-10T14:28:34.673Z" }, - { url = "https://files.pythonhosted.org/packages/60/cf/a7e19b308bd86bb04776803b1f01a5f9a287a4c55205f4708827ee487fbf/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:33a20d838b91ef376b3a56896d5b04e725c7df5bc4864cc6569cf046a8d73b6d", size = 308443, upload-time = "2026-04-10T14:28:36.658Z" }, - { url = "https://files.pythonhosted.org/packages/ca/44/e26ede3f0caeff93f222559cb0cc4ca68579f07d009d7b6010c5b586f9b1/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:432c4db5255d86a259efde91e55cb4c8d18c0521d844c9e2e7efcce3899fb016", size = 343039, upload-time = "2026-04-10T14:28:38.356Z" }, - { url = "https://files.pythonhosted.org/packages/da/e9/1f9ada30cef7b05e74bb06f52127e7a724976c225f46adb65c37b1dadfb6/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f00d94b281174144d6532a04b66a12cb866cbdc47c3af3bfe2973677f9861a", size = 349613, upload-time = "2026-04-10T14:28:40.066Z" }, -] - -[[package]] -name = "jsonschema" -version = "4.23.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "jsonschema-specifications" }, - { name = "referencing" }, - { name = "rpds-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778, upload-time = "2024-07-08T18:40:05.546Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462, upload-time = "2024-07-08T18:40:00.165Z" }, -] - -[[package]] -name = "jsonschema-specifications" -version = "2025.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "referencing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, -] - -[[package]] -name = "litellm" -version = "1.83.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "click" }, - { name = "fastuuid" }, - { name = "httpx" }, - { name = "importlib-metadata" }, - { name = "jinja2" }, - { name = "jsonschema" }, - { name = "openai" }, - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "tiktoken" }, - { name = "tokenizers" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/22/92/6ce9737554994ca8e536e5f4f6a87cc7c4774b656c9eb9add071caf7d54b/litellm-1.83.0.tar.gz", hash = "sha256:860bebc76c4bb27b4cf90b4a77acd66dba25aced37e3db98750de8a1766bfb7a", size = 17333062, upload-time = "2026-03-31T05:08:25.331Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/19/2c/a670cc050fcd6f45c6199eb99e259c73aea92edba8d5c2fc1b3686d36217/litellm-1.83.0-py3-none-any.whl", hash = "sha256:88c536d339248f3987571493015784671ba3f193a328e1ea6780dbebaa2094a8", size = 15610306, upload-time = "2026-03-31T05:08:21.987Z" }, -] - -[[package]] -name = "markdown-it-py" -version = "4.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, -] - -[[package]] -name = "markupsafe" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, -] - -[[package]] -name = "mcp" -version = "1.27.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "httpx" }, - { name = "httpx-sse" }, - { name = "jsonschema" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "pyjwt", extra = ["crypto"] }, - { name = "python-multipart" }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "sse-starlette" }, - { name = "starlette" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, - { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/83/d1efe7c2980d8a3afa476f4e3d42d53dd54c0ab94c27bee5d755b45c8b73/mcp-1.27.1.tar.gz", hash = "sha256:0f47e1820f8f8f941466b39749eb1d1839a04caddca2bc60e9d46e8a99914924", size = 608458, upload-time = "2026-05-08T16:50:12.601Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/73/42d9596facebdb533b7f0b86c1b0364ef350d1f8ba78b1052e8a58b48b65/mcp-1.27.1-py3-none-any.whl", hash = "sha256:1af3c4203b329430fde7a87b4fcb6392a041f5cb851fd68fc674016ab4e7c06f", size = 216260, upload-time = "2026-05-08T16:50:10.547Z" }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, -] - -[[package]] -name = "multidict" -version = "6.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, - { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, - { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, - { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, - { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, - { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, - { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, - { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, - { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, - { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, - { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, - { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, - { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, - { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, - { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, - { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, - { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, - { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, - { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, - { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, - { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, - { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, - { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, - { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, - { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, - { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, - { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, - { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, - { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, - { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, - { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, - { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, - { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, - { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, - { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, - { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, - { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, - { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, - { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, - { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, - { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, - { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, - { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, - { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, - { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, - { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, - { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, - { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, - { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, - { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, - { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, - { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, - { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, - { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, - { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, - { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, - { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, - { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, - { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, - { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, - { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, - { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, - { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, - { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, - { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, - { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, - { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, - { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, - { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, - { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, - { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, - { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, - { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, - { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, - { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, - { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, - { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, - { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, -] - -[[package]] -name = "openai" -version = "2.30.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "httpx" }, - { name = "jiter" }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "tqdm" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/88/15/52580c8fbc16d0675d516e8749806eda679b16de1e4434ea06fb6feaa610/openai-2.30.0.tar.gz", hash = "sha256:92f7661c990bda4b22a941806c83eabe4896c3094465030dd882a71abe80c885", size = 676084, upload-time = "2026-03-25T22:08:59.96Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/9e/5bfa2270f902d5b92ab7d41ce0475b8630572e71e349b2a4996d14bdda93/openai-2.30.0-py3-none-any.whl", hash = "sha256:9a5ae616888eb2748ec5e0c5b955a51592e0b201a11f4262db920f2a78c5231d", size = 1146656, upload-time = "2026-03-25T22:08:58.2Z" }, -] - -[[package]] -name = "openai-agents" -version = "0.17.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "griffelib" }, - { name = "mcp" }, - { name = "openai" }, - { name = "pydantic" }, - { name = "requests" }, - { name = "types-requests" }, - { name = "typing-extensions" }, - { name = "websockets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/11/cd/14acaf94c6a438cfe72c5ea043bfbc77d7fbb9514ab7796d82f2180d1518/openai_agents-0.17.2.tar.gz", hash = "sha256:5e11414bdd8c20c8e9192d21f78265d30e65992f4537f940309eca9255804449", size = 5403689, upload-time = "2026-05-12T03:14:57.433Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/71/5ba9afa4b6a0d250bfdbdd1cf9a5255ae131a7f82915c634ac50c0633c3a/openai_agents-0.17.2-py3-none-any.whl", hash = "sha256:1b3560c1690bcee635a487f77ebfb8b4fb2dd52a653e045a86e51974ab87faf3", size = 838225, upload-time = "2026-05-12T03:14:55.149Z" }, -] - -[[package]] -name = "packaging" -version = "26.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, -] - -[[package]] -name = "pageindex" -version = "0.1.0" -source = { editable = "." } -dependencies = [ - { name = "litellm" }, - { name = "openai-agents" }, - { name = "pymupdf" }, - { name = "pypdf2" }, - { name = "pytest" }, - { name = "python-dotenv" }, - { name = "pyyaml" }, - { name = "sqlite-vec" }, -] - -[package.metadata] -requires-dist = [ - { name = "litellm", specifier = "==1.83.0" }, - { name = "openai-agents", specifier = ">=0.17.2" }, - { name = "pymupdf", specifier = "==1.26.4" }, - { name = "pypdf2", specifier = "==3.0.1" }, - { name = "pytest", specifier = ">=9.0.3" }, - { name = "python-dotenv", specifier = "==1.1.0" }, - { name = "pyyaml", specifier = "==6.0.2" }, - { name = "sqlite-vec", specifier = ">=0.1.9" }, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, -] - -[[package]] -name = "propcache" -version = "0.5.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, - { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, - { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, - { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, - { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, - { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, - { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, - { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, - { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, - { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, - { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, - { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, - { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, - { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, - { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, - { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, - { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, - { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, - { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, - { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, - { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, - { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, - { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, - { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, - { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, - { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, - { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, - { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, - { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, - { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, - { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, - { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, - { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, - { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, - { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, - { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, - { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, - { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, - { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, - { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, - { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, - { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, - { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, - { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, - { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, - { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, - { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, - { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, - { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, - { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, - { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, - { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, - { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, - { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, - { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, - { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, - { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, - { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, - { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, - { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, - { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, - { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, - { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, - { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, - { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, - { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, - { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, - { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, - { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, - { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, - { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, - { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, - { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, - { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, - { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, - { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, - { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, - { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, -] - -[[package]] -name = "pycparser" -version = "3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, -] - -[[package]] -name = "pydantic" -version = "2.12.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, -] - -[[package]] -name = "pydantic-core" -version = "2.41.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, -] - -[[package]] -name = "pydantic-settings" -version = "2.14.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, -] - -[[package]] -name = "pygments" -version = "2.20.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, -] - -[[package]] -name = "pyjwt" -version = "2.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, -] - -[package.optional-dependencies] -crypto = [ - { name = "cryptography" }, -] - -[[package]] -name = "pymupdf" -version = "1.26.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/90/35/031556dfc0d332d8e9ed9b61ca105138606d3f8971b9eb02e20118629334/pymupdf-1.26.4.tar.gz", hash = "sha256:be13a066d42bfaed343a488168656637c4d9843ddc63b768dc827c9dfc6b9989", size = 83077563, upload-time = "2025-08-25T14:20:29.499Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/ae/3be722886cc7be2093585cd94f466db1199133ab005645a7a567b249560f/pymupdf-1.26.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cb95562a0a63ce906fd788bdad5239063b63068cf4a991684f43acb09052cb99", size = 23061974, upload-time = "2025-08-25T14:16:58.811Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b0/9a451d837e1fe18ecdbfbc34a6499f153c8a008763229cc634725383a93f/pymupdf-1.26.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:67e9e6b45832c33726651c2a031e9a20108fd9e759140b9e843f934de813a7ff", size = 22410112, upload-time = "2025-08-25T14:17:24.511Z" }, - { url = "https://files.pythonhosted.org/packages/d8/13/0916e8e02cb5453161fb9d9167c747d0a20d58633e30728645374153f815/pymupdf-1.26.4-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2604f687dd02b6a1b98c81bd8becfc0024899a2d2085adfe3f9e91607721fd22", size = 23454948, upload-time = "2025-08-25T21:20:07.71Z" }, - { url = "https://files.pythonhosted.org/packages/4e/c6/d3cfafc75d383603884edeabe4821a549345df954a88d79e6764e2c87601/pymupdf-1.26.4-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:973a6dda61ebd34040e4df3753bf004b669017663fbbfdaa294d44eceba98de0", size = 24060686, upload-time = "2025-08-25T14:17:56.536Z" }, - { url = "https://files.pythonhosted.org/packages/72/08/035e9d22c801e801bba50c6745bc90ba8696a042fe2c68793e28bf0c3b07/pymupdf-1.26.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:299a49797df5b558e695647fa791329ba3911cbbb31ed65f24a6266c118ef1a7", size = 24265046, upload-time = "2025-08-25T14:18:21.238Z" }, - { url = "https://files.pythonhosted.org/packages/28/8c/c201e4846ec0fb6ae5d52aa3a5d66f9355f0c69fb94230265714df0de65e/pymupdf-1.26.4-cp39-abi3-win32.whl", hash = "sha256:51b38379aad8c71bd7a8dd24d93fbe7580c2a5d9d7e1f9cd29ebbba315aa1bd1", size = 17127332, upload-time = "2025-08-25T14:18:39.132Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c4/87d27b108c2f6d773aa5183c5ae367b2a99296ea4bc16eb79f453c679e30/pymupdf-1.26.4-cp39-abi3-win_amd64.whl", hash = "sha256:0b6345a93a9afd28de2567e433055e873205c52e6b920b129ca50e836a3aeec6", size = 18743491, upload-time = "2025-08-25T14:19:01.104Z" }, -] - -[[package]] -name = "pypdf2" -version = "3.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9f/bb/18dc3062d37db6c491392007dfd1a7f524bb95886eb956569ac38a23a784/PyPDF2-3.0.1.tar.gz", hash = "sha256:a74408f69ba6271f71b9352ef4ed03dc53a31aa404d29b5d31f53bfecfee1440", size = 227419, upload-time = "2022-12-31T10:36:13.13Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/5e/c86a5643653825d3c913719e788e41386bee415c2b87b4f955432f2de6b2/pypdf2-3.0.1-py3-none-any.whl", hash = "sha256:d16e4205cfee272fbdc0568b68d82be796540b1537508cef59388f839c191928", size = 232572, upload-time = "2022-12-31T10:36:10.327Z" }, -] - -[[package]] -name = "pytest" -version = "9.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, -] - -[[package]] -name = "python-dotenv" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" }, -] - -[[package]] -name = "python-multipart" -version = "0.0.28" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/54/a85eb421fbdd5007bc5af39d0f4ed9fa609e0fedbfdc2adcf0b34526870e/python_multipart-0.0.28.tar.gz", hash = "sha256:8550da197eac0f7ab748961fc9509b999fa2662ea25cef857f05249f6893c0f8", size = 45314, upload-time = "2026-05-10T11:05:16.596Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/a2/43bbc5860b5034e2af4ef99a0e04d726ff329c43e192ef3abaa8d7ecfce5/python_multipart-0.0.28-py3-none-any.whl", hash = "sha256:10faac07eb966c3f48dc415f9dee46c04cb10d58d30a35677db8027c825ed9b6", size = 29438, upload-time = "2026-05-10T11:05:15.052Z" }, -] - -[[package]] -name = "pywin32" -version = "311" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, - { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, - { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, - { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, - { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, - { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, - { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, -] - -[[package]] -name = "referencing" -version = "0.37.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "rpds-py" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, -] - -[[package]] -name = "regex" -version = "2026.5.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270", size = 416074, upload-time = "2026-05-09T23:15:19.37Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06", size = 490451, upload-time = "2026-05-09T23:12:34.72Z" }, - { url = "https://files.pythonhosted.org/packages/1e/95/fc7ba4303b5a0f92446a12ee6778ef2c6c799233f5060042a31bf390cfe9/regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6", size = 292112, upload-time = "2026-05-09T23:12:36.285Z" }, - { url = "https://files.pythonhosted.org/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225", size = 289599, upload-time = "2026-05-09T23:12:38.089Z" }, - { url = "https://files.pythonhosted.org/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0", size = 796732, upload-time = "2026-05-09T23:12:40.062Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107", size = 865440, upload-time = "2026-05-09T23:12:42.059Z" }, - { url = "https://files.pythonhosted.org/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309", size = 912329, upload-time = "2026-05-09T23:12:44.373Z" }, - { url = "https://files.pythonhosted.org/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8", size = 801239, upload-time = "2026-05-09T23:12:46.268Z" }, - { url = "https://files.pythonhosted.org/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66", size = 777054, upload-time = "2026-05-09T23:12:48.051Z" }, - { url = "https://files.pythonhosted.org/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026", size = 785098, upload-time = "2026-05-09T23:12:49.851Z" }, - { url = "https://files.pythonhosted.org/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962", size = 860095, upload-time = "2026-05-09T23:12:51.666Z" }, - { url = "https://files.pythonhosted.org/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621", size = 765762, upload-time = "2026-05-09T23:12:53.413Z" }, - { url = "https://files.pythonhosted.org/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d", size = 852100, upload-time = "2026-05-09T23:12:55.256Z" }, - { url = "https://files.pythonhosted.org/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce", size = 789479, upload-time = "2026-05-09T23:12:57.573Z" }, - { url = "https://files.pythonhosted.org/packages/c3/1c/bdcc98f9a4af4fdd166c74941174619ccff4726d3ce32faa8e9a2ecd38dd/regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e", size = 266699, upload-time = "2026-05-09T23:12:59.14Z" }, - { url = "https://files.pythonhosted.org/packages/78/87/240d36864f9e48ace85f72e79ced97ceb7f27ce87739a947dcb834b4e6bc/regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e", size = 277783, upload-time = "2026-05-09T23:13:00.789Z" }, - { url = "https://files.pythonhosted.org/packages/4f/b5/7b30f312b0669dff5beebe5b0989dc2d1a312b1a44fab852199c387a5b96/regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070", size = 270513, upload-time = "2026-05-09T23:13:02.426Z" }, - { url = "https://files.pythonhosted.org/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb", size = 490303, upload-time = "2026-05-09T23:13:04.382Z" }, - { url = "https://files.pythonhosted.org/packages/44/da/bf30abaaa737b58f4a4b8c4a03659e02fd92092c822e0197ed9e0daab917/regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f", size = 292019, upload-time = "2026-05-09T23:13:06.022Z" }, - { url = "https://files.pythonhosted.org/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c", size = 289468, upload-time = "2026-05-09T23:13:08.214Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed", size = 796749, upload-time = "2026-05-09T23:13:10.573Z" }, - { url = "https://files.pythonhosted.org/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020", size = 865445, upload-time = "2026-05-09T23:13:12.523Z" }, - { url = "https://files.pythonhosted.org/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2", size = 912322, upload-time = "2026-05-09T23:13:14.281Z" }, - { url = "https://files.pythonhosted.org/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2", size = 801269, upload-time = "2026-05-09T23:13:16.569Z" }, - { url = "https://files.pythonhosted.org/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04", size = 777085, upload-time = "2026-05-09T23:13:18.675Z" }, - { url = "https://files.pythonhosted.org/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c", size = 785153, upload-time = "2026-05-09T23:13:20.525Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f", size = 860164, upload-time = "2026-05-09T23:13:22.263Z" }, - { url = "https://files.pythonhosted.org/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8", size = 765731, upload-time = "2026-05-09T23:13:24.277Z" }, - { url = "https://files.pythonhosted.org/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6", size = 852062, upload-time = "2026-05-09T23:13:26.125Z" }, - { url = "https://files.pythonhosted.org/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21", size = 789577, upload-time = "2026-05-09T23:13:27.814Z" }, - { url = "https://files.pythonhosted.org/packages/05/a4/018e71f7d2ad48c1ebe6d3ae0026f9b7cb4802fd15c7cc02fdf724355102/regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127", size = 266691, upload-time = "2026-05-09T23:13:29.549Z" }, - { url = "https://files.pythonhosted.org/packages/e6/1d/861a93719fb9ee7dbfc3761b3797b7a3e112a5d42c6129459d2d741be9b5/regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca", size = 277747, upload-time = "2026-05-09T23:13:31.859Z" }, - { url = "https://files.pythonhosted.org/packages/d9/c6/0a2436ae4da1ba76e51cb98943c6838a9a721faa40ebe2dce07694ae34e3/regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6", size = 270500, upload-time = "2026-05-09T23:13:33.525Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3", size = 494172, upload-time = "2026-05-09T23:13:35.935Z" }, - { url = "https://files.pythonhosted.org/packages/c4/43/fd1177a2032037c681baecdb3422ee4e1424aec4e4f470ef47793d325274/regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6", size = 293952, upload-time = "2026-05-09T23:13:38.307Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff", size = 292314, upload-time = "2026-05-09T23:13:40.353Z" }, - { url = "https://files.pythonhosted.org/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88", size = 811681, upload-time = "2026-05-09T23:13:42.543Z" }, - { url = "https://files.pythonhosted.org/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178", size = 871135, upload-time = "2026-05-09T23:13:44.326Z" }, - { url = "https://files.pythonhosted.org/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100", size = 917265, upload-time = "2026-05-09T23:13:47.295Z" }, - { url = "https://files.pythonhosted.org/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e", size = 816311, upload-time = "2026-05-09T23:13:49.885Z" }, - { url = "https://files.pythonhosted.org/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2", size = 785498, upload-time = "2026-05-09T23:13:52.25Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b", size = 801348, upload-time = "2026-05-09T23:13:54.127Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e", size = 866493, upload-time = "2026-05-09T23:13:56.067Z" }, - { url = "https://files.pythonhosted.org/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041", size = 772811, upload-time = "2026-05-09T23:13:57.867Z" }, - { url = "https://files.pythonhosted.org/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0", size = 856584, upload-time = "2026-05-09T23:13:59.679Z" }, - { url = "https://files.pythonhosted.org/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081", size = 803453, upload-time = "2026-05-09T23:14:01.978Z" }, - { url = "https://files.pythonhosted.org/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5", size = 269951, upload-time = "2026-05-09T23:14:03.661Z" }, - { url = "https://files.pythonhosted.org/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4", size = 281240, upload-time = "2026-05-09T23:14:05.56Z" }, - { url = "https://files.pythonhosted.org/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de", size = 272383, upload-time = "2026-05-09T23:14:07.843Z" }, - { url = "https://files.pythonhosted.org/packages/13/3e/9c3cd292d8808b3645a2ce517e200179b6d0e903f176300bd8b542e14de5/regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a", size = 490376, upload-time = "2026-05-09T23:14:09.64Z" }, - { url = "https://files.pythonhosted.org/packages/60/70/d43ee8a2ca0a8b68d167f21658b85520ac0574617c7f320367c5047f7556/regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4", size = 291964, upload-time = "2026-05-09T23:14:11.424Z" }, - { url = "https://files.pythonhosted.org/packages/21/91/9d50b433828d8e74196904e168a43abf1e6e88b2a15d47ed742456720c37/regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c", size = 289682, upload-time = "2026-05-09T23:14:13.123Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/b835e3cafbb9d977736912436259ff551d60919f7d7b3d37d46659c63564/regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9", size = 796996, upload-time = "2026-05-09T23:14:14.923Z" }, - { url = "https://files.pythonhosted.org/packages/2c/a6/9f992d00019166b9de01c546dd4549bc679f2a68df11b877740b0760b7c2/regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af", size = 866089, upload-time = "2026-05-09T23:14:17.757Z" }, - { url = "https://files.pythonhosted.org/packages/e0/08/4d32af657e049b19cb62b02e46e38fe1518797bfb2203ee93a510b21b0dc/regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0", size = 911530, upload-time = "2026-05-09T23:14:20.353Z" }, - { url = "https://files.pythonhosted.org/packages/d9/27/2af43dd1dc201d1fecefda64a45f4ad0995855b92724f795a777b402ee69/regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4", size = 800643, upload-time = "2026-05-09T23:14:22.265Z" }, - { url = "https://files.pythonhosted.org/packages/a4/dd/23a249047013b5321d4a60c4d2437462086f601b061776a525e5fba2a59f/regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf", size = 777223, upload-time = "2026-05-09T23:14:24.179Z" }, - { url = "https://files.pythonhosted.org/packages/94/6a/e85ed9538cd19586d0465076a4578a12e093ce776d15f3f8ce92733a8dd6/regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346", size = 785760, upload-time = "2026-05-09T23:14:26.065Z" }, - { url = "https://files.pythonhosted.org/packages/2a/c4/f25473209438638e947c55f9156fd8f236f74169229028cc99116380868e/regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676", size = 860891, upload-time = "2026-05-09T23:14:28.17Z" }, - { url = "https://files.pythonhosted.org/packages/f9/f7/f4f86e3c74419c37370e91f150ae0c2ef7d34b2e0e4cdd5da046a02e4022/regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14", size = 765891, upload-time = "2026-05-09T23:14:30.06Z" }, - { url = "https://files.pythonhosted.org/packages/26/70/704d8e13765939146b1cd0ef4e2feb71d7929727d2290f026eed10095955/regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd", size = 851380, upload-time = "2026-05-09T23:14:32.123Z" }, - { url = "https://files.pythonhosted.org/packages/26/29/1a13582a8460038edc38e49f64ceb0dd7c60f5caba77571f4bf6601965d9/regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e", size = 789350, upload-time = "2026-05-09T23:14:34.799Z" }, - { url = "https://files.pythonhosted.org/packages/73/56/3dcafe34fc72e271d62ad9a291801e88a1457bb251c132f15fcc2e5aad1a/regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad", size = 272130, upload-time = "2026-05-09T23:14:36.729Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9c/02eebf0be95efe416c664db7fb8b6b05b7a0b06a7544f2884f2558b0526f/regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763", size = 280999, upload-time = "2026-05-09T23:14:39.126Z" }, - { url = "https://files.pythonhosted.org/packages/70/5a/1dd1abee76cb7a846a0bcf42fdc87e5720c3c33c24f3e37814310a513d9f/regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372", size = 273500, upload-time = "2026-05-09T23:14:41.059Z" }, - { url = "https://files.pythonhosted.org/packages/86/c1/c5f619b0057a7965cb78ec559c1d7a45ce8c99a35bea95483d64959a93d9/regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499", size = 494269, upload-time = "2026-05-09T23:14:42.869Z" }, - { url = "https://files.pythonhosted.org/packages/05/2c/5d01f1aee33de4bbe60c8452945bfc8477ca7c5ae4450f6bfe711036cb36/regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1", size = 293954, upload-time = "2026-05-09T23:14:44.822Z" }, - { url = "https://files.pythonhosted.org/packages/7a/fe/e8988b2ae2108c6ef71bd4aa8d87fbe257976dd0810e826cd75f701c68b6/regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d", size = 292405, upload-time = "2026-05-09T23:14:47.211Z" }, - { url = "https://files.pythonhosted.org/packages/79/34/d2b0937faa7859263f7f0a3c6b103a1296306be6952dc173d0154e9a2f49/regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c", size = 811855, upload-time = "2026-05-09T23:14:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/80/fe/daf53a47457a8486db66c66c01ceb9c2303eecee3f87197f1e77eb1a736d/regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5", size = 871189, upload-time = "2026-05-09T23:14:51.555Z" }, - { url = "https://files.pythonhosted.org/packages/1c/75/058fc4470cbfbf57d800aff1a0022b929a3f9fa553ee10a0cdf2070eb31f/regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20", size = 917485, upload-time = "2026-05-09T23:14:53.633Z" }, - { url = "https://files.pythonhosted.org/packages/88/e7/179cfda3a28bc843b5c6cfe7f79f23489c791ed95f151083803660878432/regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0", size = 816369, upload-time = "2026-05-09T23:14:56.198Z" }, - { url = "https://files.pythonhosted.org/packages/41/90/6f0cc422071688266d344fca8462d787cba0a2c144acb25721f9a61ec265/regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d", size = 785869, upload-time = "2026-05-09T23:14:58.602Z" }, - { url = "https://files.pythonhosted.org/packages/02/67/a31f1760f09c27b251ef39e9beb541f462cf977381d067faa764c2c0e393/regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b", size = 801427, upload-time = "2026-05-09T23:15:00.642Z" }, - { url = "https://files.pythonhosted.org/packages/e3/c4/1a80654597b6bc1e1ea0494824c31200e8a956abe290afae9b19a166a148/regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a", size = 866482, upload-time = "2026-05-09T23:15:03.384Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/960724e06482c08466ff5611e242e86f80062949cdf6b4b9cc317b9dd93d/regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415", size = 773022, upload-time = "2026-05-09T23:15:05.625Z" }, - { url = "https://files.pythonhosted.org/packages/50/a8/a9979c3e7918280e93159ebcab5ef1a65116dd4f3bd6091be0eae4a126e8/regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2", size = 856642, upload-time = "2026-05-09T23:15:07.966Z" }, - { url = "https://files.pythonhosted.org/packages/fe/d4/a9b732f2f0072c0ab12227483abb24fffcb9f73f8a2b203df0a6d0434735/regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41", size = 803552, upload-time = "2026-05-09T23:15:10.215Z" }, - { url = "https://files.pythonhosted.org/packages/d5/fe/1b3113817447a1d4155e4ac76d2e072f42c0bcba2f43fa8a0e756ea2cd91/regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58", size = 275746, upload-time = "2026-05-09T23:15:12.609Z" }, - { url = "https://files.pythonhosted.org/packages/92/73/93d42045302636c91f2e5ef588b65b84b01428f28ec77de256b1dfdfbe5c/regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77", size = 285685, upload-time = "2026-05-09T23:15:15.086Z" }, - { url = "https://files.pythonhosted.org/packages/da/80/35b4c33c804a165a7f55289afda3ea9e3eb6d15800341a2d66455c0f1f30/regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa", size = 275713, upload-time = "2026-05-09T23:15:16.98Z" }, -] - -[[package]] -name = "requests" -version = "2.34.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/24/36/7180e7f077c38108945dbbdf60fe04db681c3feb6e96419f8c6dc8723741/requests-2.34.1.tar.gz", hash = "sha256:0fc5669f2b69704449fe1552360bd2a73a54512dfd03e65529157f1513322beb", size = 142783, upload-time = "2026-05-13T19:20:24.662Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/5a/4a949d170476de3c04ac036b5466422fbcbf348a917d8042eedf2cac7d1b/requests-2.34.1-py3-none-any.whl", hash = "sha256:bf38a3ff993960d3dd819c08862c40b3c703306eb7c744fcd9f4ddbb95b548f0", size = 73085, upload-time = "2026-05-13T19:20:22.827Z" }, -] - -[[package]] -name = "rich" -version = "15.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, -] - -[[package]] -name = "rpds-py" -version = "0.30.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, - { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, - { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, - { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, - { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, - { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, - { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, - { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, - { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, - { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, - { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, - { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, - { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, - { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, - { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, - { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, - { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, - { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, - { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, - { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, - { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, - { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, - { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, - { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, - { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, - { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, - { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, - { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, - { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, - { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, - { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, - { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, - { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, - { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, - { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, - { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, - { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, - { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, - { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, - { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, - { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, - { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, - { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, - { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, - { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, - { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, - { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, - { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, - { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, - { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, - { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, - { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, - { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, -] - -[[package]] -name = "shellingham" -version = "1.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, -] - -[[package]] -name = "sqlite-vec" -version = "0.1.9" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/85/9fad0045d8e7c8df3e0fa5a56c630e8e15ad6e5ca2e6106fceb666aa6638/sqlite_vec-0.1.9-py3-none-macosx_10_6_x86_64.whl", hash = "sha256:1b62a7f0a060d9475575d4e599bbf94a13d85af896bc1ce86ee80d1b5b48e5fb", size = 131171, upload-time = "2026-03-31T08:02:31.717Z" }, - { url = "https://files.pythonhosted.org/packages/a4/3d/3677e0cd2f92e5ebc43cd29fbf565b75582bff1ccfa0b8327c7508e1084f/sqlite_vec-0.1.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d52e30513bae4cc9778ddbf6145610434081be4c3afe57cd877893bad9f6b6c", size = 165434, upload-time = "2026-03-31T08:02:32.712Z" }, - { url = "https://files.pythonhosted.org/packages/00/d4/f2b936d3bdc38eadcbd2a87875815db36430fab0363182ba5d12cd8e0b51/sqlite_vec-0.1.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e921e592f24a5f9a18f590b6ddd530eb637e2d474e3b1972f9bbeb773aa3cb9", size = 160076, upload-time = "2026-03-31T08:02:33.796Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ad/6afd073b0f817b3e03f9e37ad626ae341805891f23c74b5292818f49ac63/sqlite_vec-0.1.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux1_x86_64.whl", hash = "sha256:1515727990b49e79bcaf75fdee2ffc7d461f8b66905013231251f1c8938e7786", size = 163388, upload-time = "2026-03-31T08:02:34.888Z" }, - { url = "https://files.pythonhosted.org/packages/42/89/81b2907cda14e566b9bf215e2ad82fc9b349edf07d2010756ffdb902f328/sqlite_vec-0.1.9-py3-none-win_amd64.whl", hash = "sha256:4a28dc12fa4b53d7b1dced22da2488fade444e96b5d16fd2d698cd670675cf32", size = 292804, upload-time = "2026-03-31T08:02:36.035Z" }, -] - -[[package]] -name = "sse-starlette" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "starlette" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f7/2b/58abc2d1fd397e7dde08e947e05c884d8ef2f78d5e2588c17a12d42d6994/sse_starlette-3.4.4.tar.gz", hash = "sha256:07e0fa0460138baf25cdd5fb28683472c3995dc1642225191b3832d62526bcb0", size = 31819, upload-time = "2026-05-12T17:37:17.019Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/67/805710444ea8cc75fbf70b920ed431a560c4bf9c57f7d5a3117213189399/sse_starlette-3.4.4-py3-none-any.whl", hash = "sha256:3f4dd50d8aed2771a091f3a83000323fc3844541c16b4fe585ae2420cc6df973", size = 16514, upload-time = "2026-05-12T17:37:15.601Z" }, -] - -[[package]] -name = "starlette" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, -] - -[[package]] -name = "tiktoken" -version = "0.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "regex" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, - { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, - { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, - { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, - { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, - { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, - { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, - { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, - { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, - { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, - { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, - { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, - { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, - { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, - { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, - { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, - { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, - { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, - { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, - { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, - { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, - { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, - { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, - { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, - { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, - { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, - { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, - { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, - { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, -] - -[[package]] -name = "tokenizers" -version = "0.22.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "huggingface-hub" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, - { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, - { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, - { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, - { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, - { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, - { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, - { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, - { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, - { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, - { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, - { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, - { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, - { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, - { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, -] - -[[package]] -name = "tqdm" -version = "4.67.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, -] - -[[package]] -name = "typer" -version = "0.23.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-doc" }, - { name = "click" }, - { name = "rich" }, - { name = "shellingham" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fd/07/b822e1b307d40e263e8253d2384cf98c51aa2368cc7ba9a07e523a1d964b/typer-0.23.1.tar.gz", hash = "sha256:2070374e4d31c83e7b61362fd859aa683576432fd5b026b060ad6b4cd3b86134", size = 120047, upload-time = "2026-02-13T10:04:30.984Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/91/9b286ab899c008c2cb05e8be99814807e7fbbd33f0c0c960470826e5ac82/typer-0.23.1-py3-none-any.whl", hash = "sha256:3291ad0d3c701cbf522012faccfbb29352ff16ad262db2139e6b01f15781f14e", size = 56813, upload-time = "2026-02-13T10:04:32.008Z" }, -] - -[[package]] -name = "types-requests" -version = "2.33.0.20260513" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6d/f7/3228dd3794941bcb92ca6ca2045a6671a828ec0b47becbef23310bc45559/types_requests-2.33.0.20260513.tar.gz", hash = "sha256:bd845450e954e751373d5d33526742592f298808a3ee3bda7e858e46b839b57f", size = 24714, upload-time = "2026-05-13T05:39:23.481Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/f5/233a78be8367a9888de718f002fb27b1ea4be39471cd88aedeafceed872e/types_requests-2.33.0.20260513-py3-none-any.whl", hash = "sha256:d5a965f9d18b6e06b72039a69565de9027e58f36a7f709857da747fbe7521122", size = 21390, upload-time = "2026-05-13T05:39:22.262Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] - -[[package]] -name = "typing-inspection" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, -] - -[[package]] -name = "urllib3" -version = "2.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, -] - -[[package]] -name = "uvicorn" -version = "0.46.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1f/93/041fca8274050e40e6791f267d82e0e2e27dd165627bd640d3e0e378d877/uvicorn-0.46.0.tar.gz", hash = "sha256:fb9da0926999cc6cb22dc7cd71a94a632f078e6ae47ff683c5c420750fb7413d", size = 88758, upload-time = "2026-04-23T07:16:00.151Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl", hash = "sha256:bbebbcbed972d162afca128605223022bedd345b7bc7855ce66deb31487a9048", size = 70926, upload-time = "2026-04-23T07:15:58.355Z" }, -] - -[[package]] -name = "websockets" -version = "16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, - { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, - { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, - { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, - { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, - { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, - { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, - { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, - { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, - { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, - { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, - { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, - { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, - { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, - { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, - { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, - { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, - { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, - { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, - { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, - { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, - { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, - { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, -] - -[[package]] -name = "yarl" -version = "1.23.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "multidict" }, - { name = "propcache" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, - { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, - { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, - { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, - { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, - { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, - { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, - { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, - { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, - { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, - { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, - { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, - { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, - { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, - { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, - { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, - { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, - { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, - { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, - { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, - { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, - { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, - { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, - { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, - { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, - { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, - { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, - { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, - { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, - { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, - { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, - { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, - { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, - { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, - { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, - { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, - { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, - { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, - { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, - { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, - { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, - { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, - { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, - { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, - { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, - { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, - { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, - { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, - { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, - { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, - { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, - { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, - { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, - { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, - { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, - { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, - { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, - { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, - { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, - { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, - { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, - { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, - { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, - { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, - { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, - { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, - { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, - { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, - { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, - { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, - { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, - { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, -] - -[[package]] -name = "zipp" -version = "3.23.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, -] From c13cb203cdeeceefea65cc000804c4104b7b5ef6 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Wed, 27 May 2026 02:30:06 +0800 Subject: [PATCH 44/50] ci(codeql): add python analysis job --- .github/workflows/codeql.yml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0674ae35d..3a9f10fdd 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -14,7 +14,7 @@ permissions: actions: read jobs: - analyze: + analyze-actions: name: Analyze (actions) runs-on: ubuntu-latest timeout-minutes: 30 @@ -32,3 +32,22 @@ jobs: uses: github/codeql-action/analyze@v4 with: category: /language:actions + + analyze-python: + name: Analyze (python) + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: python + build-mode: none + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: /language:python From 0243732e22a415203d4139170c7609e8194cddba Mon Sep 17 00:00:00 2001 From: BukeLy Date: Wed, 27 May 2026 03:35:12 +0800 Subject: [PATCH 45/50] fix(filesystem): clarify agent evidence recovery --- .gitignore | 1 + pageindex/filesystem/agent.py | 16 +++++++++++----- pageindex/filesystem/commands.py | 6 +++++- tests/test_pageindex_structural_read.py | 2 +- tests/test_pifs_agent_stream.py | 8 ++++++++ 5 files changed, 26 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 3702bdaa9..31664a930 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__ .venv/ logs/ examples/pifs_workspace/ +examples/Benchmark/enterpriseragbenchmark_workspace/ diff --git a/pageindex/filesystem/agent.py b/pageindex/filesystem/agent.py index facaceb6f..b1f162504 100644 --- a/pageindex/filesystem/agent.py +++ b/pageindex/filesystem/agent.py @@ -60,9 +60,11 @@ grep does not support regex alternation such as "a|b"; run multiple grep commands or use search-summary for semantic candidate discovery instead. semantic search commands such as search-summary return candidate documents and -do not guarantee literal text matches. Use search-summary when the user asks for -summary search, semantic search, or vector search and the command is listed as -available. Quote multi-word semantic queries, for example: +do not guarantee literal text matches or final answer evidence. After choosing +a likely search-summary candidate, verify the relevant claim with cat before +answering. Use search-summary when the user asks for summary search, semantic +search, or vector search and the command is listed as available. Quote +multi-word semantic queries, for example: search-summary "Federal Reserve" /documents. Do not write search-summary Federal Reserve /documents. Errors are returned as text prefixed with ERROR. Do not call @@ -70,7 +72,9 @@ with cat or grep before answering. Prefer shell-like target-first cat syntax with stable targets: cat --structure, cat --page 31-59, and cat --node 0009. You may also use file_ref or document_id when a path is -ambiguous. After structure identifies a relevant section node, prefer +ambiguous. Do not reconstruct paths from document titles; use exact targets +returned by PIFS commands and quote paths containing spaces. After structure +identifies a relevant section node, prefer cat --node ; use cat --page when the user asks for page-level evidence, no suitable node exists, or exact page text is needed. cat --structure is paginated; request more with --offset if needed. Page @@ -94,11 +98,13 @@ - Use --where only with metadata fields shown by stat --schema. - grep -R performs lexical evidence search. - grep does not support regex alternation such as "a|b"; run separate grep commands or use search-summary for semantic candidate discovery. -- Semantic search commands are candidate-discovery tools and do not guarantee literal text matches. +- Semantic search commands are candidate-discovery tools and do not guarantee literal text matches or final answer evidence. After selecting a likely search-summary candidate, verify the relevant facts with cat before answering. +- Do not use find | grep as an exhaustive search or as proof that no document exists; find output can be scoped or limited. Use metadata filters, search-summary, grep on a narrowed target, or cat on likely candidates instead. - A single failed grep is not enough evidence to say there is no relevant document. If grep returns no matches for a workspace-topic question, verify with search-summary or another available semantic/vector candidate command, or inspect likely document structure, before answering no-evidence. - If search-summary is available and the user asks for summary search, semantic search, vector search, or "用 summary 搜", use search-summary "" ; quote multi-word queries, for example search-summary "Federal Reserve" /documents; do not translate that request into find --where. - Tool errors are returned as ERROR text; recover by trying an available command. - Use cat or grep to gather evidence before making source-backed claims. +- Do not reconstruct a file path from a title. Use exact paths returned by PIFS commands, or use file_ref/document_id when available; quote paths that contain spaces. - For broad topic, method, or "what solution" questions that are likely about the workspace, search for candidate documents before asking the user to choose a document. - Use stat only for metadata/schema/status questions or to resolve ambiguous target identity. Do not run stat merely to understand what a document says. - Prefer target-first cat syntax with stable targets: cat --structure, cat --page 31-59, cat --node . diff --git a/pageindex/filesystem/commands.py b/pageindex/filesystem/commands.py index 73470a625..2ec5920ed 100644 --- a/pageindex/filesystem/commands.py +++ b/pageindex/filesystem/commands.py @@ -534,7 +534,11 @@ def _cmd_cat(self, args: list[str]) -> Any: "cat accepts one file target. Use target-first syntax: " "cat --structure, " "cat --node 0002 0004, or " - "cat --page 31-33" + "cat --page 31-33. " + f"Unexpected extra argument: {arg!r}. If the target path or title contains " + "spaces, quote the whole target, for example: cat \"/documents/report name.pdf\" " + "--structure. If a title-derived path is ambiguous, use the file_ref or " + "document_id instead." ) i += 1 if structural_mode == "structure": diff --git a/tests/test_pageindex_structural_read.py b/tests/test_pageindex_structural_read.py index a78f1d15a..3994aa413 100644 --- a/tests/test_pageindex_structural_read.py +++ b/tests/test_pageindex_structural_read.py @@ -517,7 +517,7 @@ def test_cat_structure_page_node_and_text_outputs_are_hard_limited(): "0006 0007 0008 0009 0010 0011" ) - with pytest.raises(PIFSCommandError, match="cat accepts one file target"): + with pytest.raises(PIFSCommandError, match="quote the whole target"): executor.execute("cat dsid_limited_pdf 0001") text = json.loads(executor.execute("cat dsid_long_text --all")) diff --git a/tests/test_pifs_agent_stream.py b/tests/test_pifs_agent_stream.py index 5dae40bd7..69f62edde 100644 --- a/tests/test_pifs_agent_stream.py +++ b/tests/test_pifs_agent_stream.py @@ -204,6 +204,8 @@ def test_prompt_tells_agent_when_to_choose_node_or_page(self): self.assertIn("prefer\ncat --node ", BASH_TOOL_DESCRIPTION) self.assertIn("stop if the evidence is sufficient", AGENT_TOOL_POLICY) self.assertIn("continue with another chunk before answering", BASH_TOOL_DESCRIPTION) + self.assertIn("Do not reconstruct paths from document titles", BASH_TOOL_DESCRIPTION) + self.assertIn("file_ref/document_id", AGENT_TOOL_POLICY) def test_prompt_requires_stat_for_metadata_questions(self): self.assertIn("stat --schema and stat ", AGENT_TOOL_POLICY) @@ -218,6 +220,12 @@ def test_prompt_routes_summary_search_to_search_summary(self): self.assertIn('use search-summary "" ', AGENT_TOOL_POLICY) self.assertIn('search-summary "Federal Reserve" /documents', BASH_TOOL_DESCRIPTION) self.assertIn("do not translate that request into find --where", AGENT_TOOL_POLICY) + self.assertIn("verify the relevant facts with cat", AGENT_TOOL_POLICY) + self.assertIn("verify the relevant claim with cat", BASH_TOOL_DESCRIPTION) + + def test_prompt_rejects_find_grep_as_exhaustive_search(self): + self.assertIn("Do not use find | grep as an exhaustive search", AGENT_TOOL_POLICY) + self.assertIn("find output can be scoped or limited", AGENT_TOOL_POLICY) def test_system_prompt_sets_workspace_identity_and_scope(self): self.assertIn("PageIndex FileSystem Demo Agent", AGENT_SYSTEM_PROMPT) From 977f5966666cf863d25ed81c88fab337377e50bb Mon Sep 17 00:00:00 2001 From: BukeLy Date: Wed, 27 May 2026 03:48:44 +0800 Subject: [PATCH 46/50] fix(filesystem): return stable semantic search targets --- pageindex/filesystem/commands.py | 19 +++- pageindex/filesystem/store.py | 27 ++++- tests/test_pageindex_filesystem_scope.py | 125 +++++++++++++++++++++-- tests/test_pifs_path_resolution.py | 27 +++++ 4 files changed, 186 insertions(+), 12 deletions(-) diff --git a/pageindex/filesystem/commands.py b/pageindex/filesystem/commands.py index 2ec5920ed..18a85cc2f 100644 --- a/pageindex/filesystem/commands.py +++ b/pageindex/filesystem/commands.py @@ -1337,6 +1337,23 @@ def _file_target_path(self, item: dict[str, Any]) -> str: return f"{folder}/{title}" if folder else f"/{title}" return str(item.get("source_path") or item.get("external_id") or file_ref or "-") + def _stable_file_target_path(self, item: dict[str, Any]) -> str: + file_ref = str(item.get("file_ref") or "").strip() + source_path = str(item.get("source_path") or "").strip() + if source_path: + target = "/" + source_path.strip("/") + try: + if not file_ref or self.filesystem.store.resolve_file_ref(target) == file_ref: + return target + except KeyError: + pass + external_id = str(item.get("external_id") or "").strip() + if external_id: + return external_id + if file_ref: + return file_ref + return str(item.get("external_id") or item.get("file_ref") or "-") + def _semantic_retrieval_query(self, query: str) -> str: query = str(query or "").strip() context = str(self.query_context or "").strip() @@ -1464,7 +1481,7 @@ def _semantic_channel_hits_from_results( if text: line_text = f"{line}: {self._compact_text(text, max_chars=220)}" hit = { - "path": self._file_target_path( + "path": self._stable_file_target_path( { "file_ref": result.file_ref, "title": result.title, diff --git a/pageindex/filesystem/store.py b/pageindex/filesystem/store.py index b88b54e2e..7517d70ed 100644 --- a/pageindex/filesystem/store.py +++ b/pageindex/filesystem/store.py @@ -1050,12 +1050,29 @@ def _resolve_file_ref(self, conn: sqlite3.Connection, target: str) -> str: if row: return row["file_ref"] stripped = target.strip("/") - row = conn.execute( - "SELECT file_ref FROM files WHERE source_path = ? AND deleted_at IS NULL", + rows = conn.execute( + """ + SELECT + f.file_ref, + f.external_id, + f.title, + f.source_path, + COALESCE(MIN(fo.path), '/') AS folder_path + FROM files f + LEFT JOIN file_folders ff ON ff.file_ref = f.file_ref + LEFT JOIN folders fo ON fo.folder_id = ff.folder_id + WHERE f.source_path = ? AND f.deleted_at IS NULL + GROUP BY f.file_ref, f.external_id, f.title, f.source_path + ORDER BY f.file_ref + LIMIT 2 + """, (stripped,), - ).fetchone() - if row: - return row["file_ref"] + ).fetchall() + if len(rows) > 1: + matches = "; ".join(self._virtual_match_summary(row) for row in rows) + raise KeyError(f"Ambiguous file target: {target}. Matches: {matches}") + if rows: + return rows[0]["file_ref"] virtual_file_ref = self._resolve_virtual_file_ref(conn, target) if virtual_file_ref: return virtual_file_ref diff --git a/tests/test_pageindex_filesystem_scope.py b/tests/test_pageindex_filesystem_scope.py index 7c9e31bd1..087473aab 100644 --- a/tests/test_pageindex_filesystem_scope.py +++ b/tests/test_pageindex_filesystem_scope.py @@ -83,7 +83,7 @@ def generate(self, document, *, fields): workspace=tmp_path / "workspace", metadata_generator=SummaryGenerator(), ) - filesystem.register_file( + file_ref = filesystem.register_file( storage_uri="file:///tmp/report.pdf", source_path="examples/documents/report.pdf", folder_path="/documents", @@ -110,20 +110,133 @@ def generate(self, document, *, fields): assert backend.calls[0][2] == {} assert result["data"]["data"][0] == { - "path": "/documents/report.pdf", + "path": "/examples/documents/report.pdf", "summary": "Federal Reserve annual report summary", "line_text": "1: Federal Reserve supervision and regulation annual report.", } + assert filesystem.store.resolve_file_ref(result["data"]["data"][0]["path"]) == file_ref executor.json_output = False rendered = executor.execute('search-summary "Federal Reserve annual report" /documents') - assert "path: /documents/report.pdf" in rendered + assert "path: /examples/documents/report.pdf" in rendered assert "summary: Federal Reserve annual report summary" in rendered assert "line_text: 1: Federal Reserve supervision and regulation annual report." in rendered assert "id=dsid_report" not in rendered assert "file_ref=" not in rendered +def test_semantic_search_path_is_unique_source_target_when_titles_collide(tmp_path): + from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem + from pageindex.filesystem.metadata_generation import MetadataGenerationResult + + class SummaryGenerator: + def generate(self, document, *, fields): + return MetadataGenerationResult( + values={"summary": f"summary for {document.external_id}"} + ) + + filesystem = PageIndexFileSystem( + workspace=tmp_path / "workspace", + metadata_generator=SummaryGenerator(), + ) + first_ref = filesystem.register_file( + storage_uri="file:///tmp/first.json", + source_path="slack/dsid_first.json", + folder_path="/documents", + external_id="dsid_first", + title="announcements", + content="first announcement mentions H200 reservations.", + metadata_policy={ + "fields": { + "summary": True, + "doc_type": False, + "domain": False, + "topic": False, + } + }, + ) + filesystem.register_file( + storage_uri="file:///tmp/second.json", + source_path="slack/dsid_second.json", + folder_path="/documents", + external_id="dsid_second", + title="announcements", + content="second announcement mentions unrelated maintenance.", + metadata_policy={ + "fields": { + "summary": True, + "doc_type": False, + "domain": False, + "topic": False, + } + }, + ) + filesystem.semantic_retrieval_backend = SummaryBackend("dsid_first") + executor = PIFSCommandExecutor(filesystem, json_output=True) + + result = json.loads(executor.execute('search-summary "H200 reservations" /documents')) + + assert result["data"]["data"][0]["path"] == "/slack/dsid_first.json" + assert filesystem.store.resolve_file_ref(result["data"]["data"][0]["path"]) == first_ref + with pytest.raises(KeyError, match="Ambiguous file target"): + filesystem.store.resolve_file_ref("/documents/announcements") + + +def test_semantic_search_path_falls_back_when_source_target_is_ambiguous(tmp_path): + from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem + from pageindex.filesystem.metadata_generation import MetadataGenerationResult + + class SummaryGenerator: + def generate(self, document, *, fields): + return MetadataGenerationResult( + values={"summary": f"summary for {document.external_id}"} + ) + + filesystem = PageIndexFileSystem( + workspace=tmp_path / "workspace", + metadata_generator=SummaryGenerator(), + ) + first_ref = filesystem.register_file( + storage_uri="file:///tmp/first.json", + source_path="shared/source.json", + folder_path="/documents", + external_id="dsid_first", + title="First", + content="first content", + metadata_policy={ + "fields": { + "summary": True, + "doc_type": False, + "domain": False, + "topic": False, + } + }, + ) + filesystem.register_file( + storage_uri="file:///tmp/second.json", + source_path="shared/source.json", + folder_path="/documents", + external_id="dsid_second", + title="Second", + content="second content", + metadata_policy={ + "fields": { + "summary": True, + "doc_type": False, + "domain": False, + "topic": False, + } + }, + ) + filesystem.semantic_retrieval_backend = SummaryBackend("dsid_first") + executor = PIFSCommandExecutor(filesystem, json_output=True) + + result = json.loads(executor.execute('search-summary "first" /documents')) + + assert result["data"]["data"][0]["path"] == "dsid_first" + assert filesystem.store.resolve_file_ref(result["data"]["data"][0]["path"]) == first_ref + + def test_entity_relation_search_return_minimal_fields_with_summary(tmp_path): from pageindex.filesystem import PIFSCommandExecutor, PageIndexFileSystem from pageindex.filesystem.metadata_generation import MetadataGenerationResult @@ -164,7 +277,7 @@ def generate(self, document, *, fields): entity = json.loads(executor.execute('search-entity "Federal Reserve" /documents')) assert entity["data"]["data"][0] == { - "path": "/documents/market-note.pdf", + "path": "/examples/documents/market-note.pdf", "summary": "Risk and compliance summary", "line_text": "1: Federal Reserve policy affects Disney valuation.", "entity": "Federal Reserve; Disney", @@ -172,7 +285,7 @@ def generate(self, document, *, fields): relation = json.loads(executor.execute('search-relation "Disney valuation" /documents')) assert relation["data"]["data"][0] == { - "path": "/documents/market-note.pdf", + "path": "/examples/documents/market-note.pdf", "summary": "Risk and compliance summary", "line_text": "1: Federal Reserve policy affects Disney valuation.", "relation": "Federal Reserve affects Disney valuation", @@ -180,7 +293,7 @@ def generate(self, document, *, fields): executor.json_output = False rendered = executor.execute('search-entity "Federal Reserve" /documents') - assert "path: /documents/market-note.pdf" in rendered + assert "path: /examples/documents/market-note.pdf" in rendered assert "summary: Risk and compliance summary" in rendered assert "entity: Federal Reserve; Disney" in rendered assert "file_ref=" not in rendered diff --git a/tests/test_pifs_path_resolution.py b/tests/test_pifs_path_resolution.py index 08cf28f72..184fc53da 100644 --- a/tests/test_pifs_path_resolution.py +++ b/tests/test_pifs_path_resolution.py @@ -42,3 +42,30 @@ def test_ambiguous_virtual_file_path_raises_clear_error(tmp_path): filesystem.store.resolve_file_ref("/a/b/file.txt") assert first_ref != second_ref + + +def test_duplicate_source_path_target_raises_clear_error(tmp_path): + from pageindex.filesystem import PageIndexFileSystem + + filesystem = PageIndexFileSystem(workspace=tmp_path / "workspace") + first_ref = filesystem.register_file( + storage_uri="file:///tmp/first.txt", + source_path="shared/source.txt", + folder_path="/first", + external_id="doc_first", + title="First", + content="first content", + ) + second_ref = filesystem.register_file( + storage_uri="file:///tmp/second.txt", + source_path="shared/source.txt", + folder_path="/second", + external_id="doc_second", + title="Second", + content="second content", + ) + + with pytest.raises(KeyError, match="Ambiguous file target"): + filesystem.store.resolve_file_ref("/shared/source.txt") + + assert first_ref != second_ref From 4ff184fed28da4358a4961e45f3d12987157130e Mon Sep 17 00:00:00 2001 From: BukeLy Date: Wed, 27 May 2026 14:28:03 +0800 Subject: [PATCH 47/50] feat(filesystem): add pifs workspace default --- pageindex/filesystem/cli.py | 74 +++++++++++++++++++++++++++++++++---- pifs | 6 +++ tests/test_pifs_cli.py | 51 +++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 7 deletions(-) create mode 100755 pifs diff --git a/pageindex/filesystem/cli.py b/pageindex/filesystem/cli.py index 8af12e649..e808d32ea 100644 --- a/pageindex/filesystem/cli.py +++ b/pageindex/filesystem/cli.py @@ -2,6 +2,7 @@ import argparse import contextlib +import json import os import re import shlex @@ -23,6 +24,45 @@ DEFAULT_AGENT_MODEL = "gpt-5.4-mini" EXIT_COMMANDS = {"exit", "quit", ":q"} ANSI_ESCAPE_RE = re.compile(r"\x1b(?:\[[0-?]*[ -/]*[@-~]|.)") +PIFS_CONFIG_FILE_ENV = "PIFS_CONFIG_FILE" +PIFS_WORKSPACE_ENV = "PIFS_WORKSPACE" + + +def _config_path() -> Path: + override = os.environ.get(PIFS_CONFIG_FILE_ENV) + if override: + return Path(override).expanduser() + config_home = os.environ.get("XDG_CONFIG_HOME") + root = Path(config_home).expanduser() if config_home else Path.home() / ".config" + return root / "pageindex" / "pifs.json" + + +def _read_config() -> dict[str, str]: + path = _config_path() + if not path.exists(): + return {} + with path.open("r", encoding="utf-8") as handle: + payload = json.load(handle) + if not isinstance(payload, dict): + raise ValueError(f"invalid PIFS config file: {path}") + return {str(key): str(value) for key, value in payload.items() if value is not None} + + +def _write_config(config: dict[str, str]) -> Path: + path = _config_path() + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", encoding="utf-8") as handle: + json.dump(config, handle, indent=2, sort_keys=True) + handle.write("\n") + return path + + +def _configured_workspace() -> str | None: + return _read_config().get("workspace") + + +def _resolve_workspace(value: str | None) -> str | None: + return value or os.environ.get(PIFS_WORKSPACE_ENV) or _configured_workspace() def _load_env_file(path: str | None = None, *, workspace: str | None = None) -> Path | None: @@ -114,10 +154,9 @@ def _parse_agent_command( parser.add_argument("question", nargs=argparse.REMAINDER) args = parser.parse_args(argv) _load_env_file(args.env_file, workspace=args.workspace) + args.workspace = _resolve_workspace(args.workspace) if not args.workspace: - args.workspace = os.environ.get("PIFS_WORKSPACE") - if not args.workspace: - parser.error("--workspace is required unless PIFS_WORKSPACE is set") + parser.error("--workspace is required unless PIFS_WORKSPACE is set or `pifs set workspace ` has been run") return args @@ -241,18 +280,37 @@ def _run_passthrough( return 0 +def _run_set(argv: list[str]) -> int: + parser = argparse.ArgumentParser( + prog="pifs set", + description="Set PageIndex FileSystem CLI defaults", + ) + parser.add_argument("name", choices=["workspace"]) + parser.add_argument("value") + args = parser.parse_args(argv) + + config = _read_config() + if args.name == "workspace": + workspace = Path(args.value).expanduser().resolve(strict=False) + config["workspace"] = str(workspace) + path = _write_config(config) + print(f"workspace: {workspace}") + print(f"config: {path}") + return 0 + raise ValueError(f"unknown config key: {args.name}") + + def main(argv: list[str] | None = None) -> int: argv = list(sys.argv[1:] if argv is None else argv) _load_env_file() parser = argparse.ArgumentParser(description="PageIndex FileSystem CLI") - parser.add_argument("--workspace", default=os.environ.get("PIFS_WORKSPACE")) + parser.add_argument("--workspace", default=None) parser.add_argument("--env-file", default=None) parser.add_argument("--json", action="store_true", dest="json_output") parser.add_argument("command", nargs=argparse.REMAINDER) args = parser.parse_args(argv) _load_env_file(args.env_file, workspace=args.workspace) - if not args.workspace: - args.workspace = os.environ.get("PIFS_WORKSPACE") + args.workspace = _resolve_workspace(args.workspace) command_tokens = [token for token in args.command if token != "--"] json_output = args.json_output @@ -263,6 +321,8 @@ def main(argv: list[str] | None = None) -> int: try: command_name = command_tokens[0] command_args = command_tokens[1:] + if command_name == "set": + return _run_set(command_args) if command_name == "ask": return _run_ask(command_args, workspace_default=args.workspace) if command_name == "chat": @@ -272,7 +332,7 @@ def main(argv: list[str] | None = None) -> int: command_tokens = [token for token in command_tokens if token != "--json"] json_output = True if not args.workspace: - parser.error("--workspace is required unless PIFS_WORKSPACE is set") + parser.error("--workspace is required unless PIFS_WORKSPACE is set or `pifs set workspace ` has been run") return _run_passthrough( command_tokens, workspace=args.workspace, diff --git a/pifs b/pifs new file mode 100755 index 000000000..46b6e0e3a --- /dev/null +++ b/pifs @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from pageindex.filesystem.cli import main + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_pifs_cli.py b/tests/test_pifs_cli.py index 74832b859..491cbb93c 100644 --- a/tests/test_pifs_cli.py +++ b/tests/test_pifs_cli.py @@ -55,6 +55,57 @@ def execute(self, command): assert executor_instances[0].commands == ["ls /documents"] +def test_cli_set_workspace_persists_default(monkeypatch, capsys, tmp_path): + from pageindex.filesystem import cli + + config_path = tmp_path / "pifs.json" + workspace = tmp_path / "workspace" + monkeypatch.setenv("PIFS_CONFIG_FILE", str(config_path)) + + status = cli.main(["set", "workspace", str(workspace)]) + + assert status == 0 + output = capsys.readouterr().out + assert f"workspace: {workspace}" in output + assert f"config: {config_path}" in output + assert config_path.read_text(encoding="utf-8") == ( + '{\n "workspace": "' + str(workspace) + '"\n}\n' + ) + + +def test_cli_passthrough_uses_configured_workspace(monkeypatch, capsys, tmp_path): + from pageindex.filesystem import cli + + config_path = tmp_path / "pifs.json" + workspace = tmp_path / "workspace" + executor_instances = [] + monkeypatch.setenv("PIFS_CONFIG_FILE", str(config_path)) + monkeypatch.delenv("PIFS_WORKSPACE", raising=False) + + class FakeExecutor: + def __init__(self, filesystem, *, json_output=False): + self.filesystem = filesystem + self.json_output = json_output + self.commands = [] + executor_instances.append(self) + + def execute(self, command): + self.commands.append(command) + return f"executed:{command}" + + monkeypatch.setattr(cli, "PageIndexFileSystem", FakeFileSystem) + monkeypatch.setattr(cli, "PIFSCommandExecutor", FakeExecutor) + + assert cli.main(["set", "workspace", str(workspace)]) == 0 + capsys.readouterr() + + status = cli.main(["ls", "/documents"]) + + assert status == 0 + assert capsys.readouterr().out == "executed:ls /documents\n" + assert executor_instances[0].filesystem.workspace == workspace + + def test_cli_ask_invokes_agent_with_question(monkeypatch, capsys, tmp_path): from pageindex.filesystem import cli From a70fc672bedd1aa5888e20c0ab35e63062fadeba Mon Sep 17 00:00:00 2001 From: BukeLy Date: Wed, 27 May 2026 14:29:13 +0800 Subject: [PATCH 48/50] chore(filesystem): ignore local pifs artifacts --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 31664a930..685045e17 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ __pycache__ .DS_Store .env* .venv/ +.claude/ +.codex/ logs/ examples/pifs_workspace/ -examples/Benchmark/enterpriseragbenchmark_workspace/ +examples/Benchmark/ From 21c83b6398ae8f0a3b130c280b5b3174492933e1 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Wed, 27 May 2026 14:34:42 +0800 Subject: [PATCH 49/50] fix(filesystem): run pifs from local venv --- pifs | 12 ++++++++---- requirements.txt | 3 ++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/pifs b/pifs index 46b6e0e3a..fb2dbc08e 100755 --- a/pifs +++ b/pifs @@ -1,6 +1,10 @@ -#!/usr/bin/env python3 -from pageindex.filesystem.cli import main +#!/bin/sh +set -eu +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) -if __name__ == "__main__": - raise SystemExit(main()) +if [ -x "$SCRIPT_DIR/.venv/bin/python" ]; then + exec "$SCRIPT_DIR/.venv/bin/python" -m pageindex.filesystem.cli "$@" +fi + +exec python3 -m pageindex.filesystem.cli "$@" diff --git a/requirements.txt b/requirements.txt index e6ad80531..f88e7cb05 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ litellm==1.83.7 -# openai-agents # optional: required for examples/agentic_vectorless_rag_demo.py +# openai-agents==0.17.2 # optional: required for pifs chat/ask and examples/agentic_vectorless_rag_demo.py pymupdf==1.26.4 PyPDF2==3.0.1 python-dotenv==1.2.2 pyyaml==6.0.2 +sqlite-vec>=0.1.9 From 8071889508ab4f9736c8dadf393be5792f48793f Mon Sep 17 00:00:00 2001 From: BukeLy Date: Wed, 27 May 2026 14:37:57 +0800 Subject: [PATCH 50/50] chore(filesystem): remove duplicate pifs cli wrapper --- pifs-cli | 6 ------ 1 file changed, 6 deletions(-) delete mode 100755 pifs-cli diff --git a/pifs-cli b/pifs-cli deleted file mode 100755 index 46b6e0e3a..000000000 --- a/pifs-cli +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python3 -from pageindex.filesystem.cli import main - - -if __name__ == "__main__": - raise SystemExit(main())