diff --git a/.gitignore b/.gitignore index 0a74cbffb..a5ff6a294 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,8 @@ venv/ env/ python/.venv/ .python-version +llm_config.yaml +python/knowledge_graph_data/ # IDE & tooling .vscode/ diff --git a/README.md b/README.md index ca2856ddf..146705986 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ Lance Graph is a Cypher-capable graph query engine built in Rust with Python bin This repository contains: - `rust/lance-graph` – the Cypher-capable query engine implemented in Rust -- `python/` – PyO3 bindings and a thin `lance_graph` Python package +- `python/` – PyO3 bindings and Python packages: + - `lance_graph` – thin wrapper around the Rust query engine + - `knowledge_graph` – Lance-backed knowledge graph CLI, API, and utilities ## Prerequisites @@ -62,6 +64,65 @@ result = query.execute({"Person": people}) print(result.to_pydict()) # {'name': ['Bob', 'David'], 'age': [34, 42]} ``` +## Knowledge Graph CLI & API + +The `knowledge_graph` package layers a simple Lance-backed knowledge graph +service on top of the `lance_graph` engine. It provides: + +- A CLI (`knowledge_graph.main`) for initializing storage, running Cypher + queries, and bootstrapping data via heuristic text extraction. +- A reusable FastAPI component, plus a standalone web service + (`knowledge_graph.webservice`) that exposes query and dataset endpoints. +- Storage helpers that persist node and relationship tables as Lance datasets. + +### CLI usage + +```bash +uv run knowledge_graph --init # initialize storage and schema stub +uv run knowledge_graph --list-datasets # list Lance datasets on disk +uv run knowledge_graph --extract-preview notes.txt +uv run knowledge_graph --extract-preview "Alice joined the graph team" +uv run knowledge_graph --extract-and-add notes.txt +uv run knowledge_graph "MATCH (n) RETURN n LIMIT 5" +uv run knowledge_graph --log-level DEBUG --extract-preview "Inline text" +uv run knowledge_graph --ask "Who is working on the Presto project?" + + +# Configure LLM extraction (default) +uv sync --extra llm # install optional LLM dependencies +uv sync --extra lance-storage # install Lance dataset support +export OPENAI_API_KEY=sk-... +uv run knowledge_graph --llm-model gpt-4o-mini --extract-preview notes.txt + +# Supply additional OpenAI client options via YAML (base_url, headers, etc.) +uv run knowledge_graph --llm-config llm_config.yaml --extract-and-add notes.txt + +# Fall back to the heuristic extractor when LLM access is unavailable +uv run knowledge_graph --extractor heuristic --extract-preview notes.txt + +``` + +The default extractor uses OpenAI. Configure credentials via environment +variables supported by the SDK (for example `OPENAI_API_BASE` or +`OPENAI_API_KEY`), or place them in a YAML file passed through `--llm-config`. +Override the model and temperature with `--llm-model` and `--llm-temperature`. +``` + +By default the CLI writes datasets under `./knowledge_graph_data`. Provide +`--root` and `--schema` to point at alternate storage locations and schema YAML. + +### FastAPI service + +Run the web service after installing the `knowledge_graph` package (and +dependencies such as FastAPI): + +```bash +uv run --package knowledge_graph knowledge_graph-webservice +``` + +The service exposes endpoints under `/graph`, including `/graph/health`, +`/graph/query`, `/graph/datasets`, and `/graph/schema`. + ### Development workflow For linting and type checks: diff --git a/python/README.md b/python/README.md index 0230dcd48..28fe26bf5 100644 --- a/python/README.md +++ b/python/README.md @@ -43,6 +43,14 @@ maturin develop - `python/src/` – PyO3 bridge that exposes graph APIs to Python - `python/python/lance_graph/` – pure-Python wrapper and `__init__` +- `python/python/knowledge_graph/` – CLI, FastAPI, and extractor utilities built on Lance - `python/python/tests/` – graph-centric functional tests Refer to the repository root `README.md` for information about the Rust crate. + +> Run CLI commands through `uv run knowledge_graph ...`. The default uses an +> LLM-backed extractor; install the LLM extra with `uv sync --extra llm` (or +> `uv pip install -e '.[llm]'`) and configure `OPENAI_API_KEY`. Install +> `uv sync --extra lance-storage` to enable Lance dataset persistence. Supply +> extra options (e.g., `base_url`, HTTP headers) via `--llm-config`. Use +> `--extractor heuristic` to avoid LLM calls during testing or offline work. diff --git a/python/llm_config.example.yaml b/python/llm_config.example.yaml new file mode 100644 index 000000000..753cfbf2e --- /dev/null +++ b/python/llm_config.example.yaml @@ -0,0 +1,10 @@ +# Example LLM client configuration (public-safe template) +# Copy this file to llm_config.yaml and fill in your values. +api_key: YOUR_API_KEY_HERE +# Optional: override base URL for self-hosted or compatible APIs (e.g., OpenAI-compatible gateways) +base_url: https://api.openai.com/v1 +# Optional: additional HTTP headers to send with each request +default_headers: + # openai-organization: YOUR_ORG_ID + # Authorization is derived from api_key by the client; do not duplicate here + # Custom-Header: value diff --git a/python/pyproject.toml b/python/pyproject.toml index 07946f914..d04800abf 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,7 +1,13 @@ [project] name = "lance-graph" dynamic = ["version"] -dependencies = ["pyarrow>=14"] +dependencies = [ + "pyarrow>=14", + "pyyaml>=6.0", + "fastapi>=0.104.0", + "uvicorn>=0.24.0", + "pydantic>=2.0.0", +] description = "Python bindings for the lance-graph Cypher engine" authors = [{ name = "Lance Devs", email = "dev@lancedb.com" }] license = { file = "LICENSE" } @@ -37,6 +43,8 @@ build-backend = "maturin" [project.optional-dependencies] tests = ["pytest", "pyarrow>=14", "pandas"] dev = ["ruff", "pyright"] +llm = ["openai>=1.52.0"] +lance-storage = ["lance>=0.17.0"] [project.scripts] knowledge_graph = "knowledge_graph.main:main" diff --git a/python/python/knowledge_graph/__init__.py b/python/python/knowledge_graph/__init__.py index 374e0d7b2..c1669836c 100644 --- a/python/python/knowledge_graph/__init__.py +++ b/python/python/knowledge_graph/__init__.py @@ -13,6 +13,19 @@ except ImportError: # pragma: no cover - builder is available in normal installs. GraphConfigBuilder = object # type: ignore[assignment] +from .component import KnowledgeGraphComponent +from .config import KnowledgeGraphConfig, build_graph_config_from_mapping +from .extraction import ( + DEFAULT_STRATEGY, + BaseExtractor, + get_extractor, + preview_extraction, +) +from .extractors import HeuristicExtractor, LLMExtractor +from .service import LanceKnowledgeGraph, create_default_service +from .store import LanceGraphStore +from .webservice import create_app + TableMapping = Mapping[str, pa.Table] @@ -100,4 +113,20 @@ def build(self) -> KnowledgeGraph: return KnowledgeGraph(config, self._datasets) -__all__ = ["KnowledgeGraph", "KnowledgeGraphBuilder"] +__all__ = [ + "KnowledgeGraph", + "KnowledgeGraphBuilder", + "KnowledgeGraphConfig", + "build_graph_config_from_mapping", + "LanceGraphStore", + "LanceKnowledgeGraph", + "create_default_service", + "KnowledgeGraphComponent", + "create_app", + "DEFAULT_STRATEGY", + "BaseExtractor", + "get_extractor", + "preview_extraction", + "HeuristicExtractor", + "LLMExtractor", +] diff --git a/python/python/knowledge_graph/component.py b/python/python/knowledge_graph/component.py new file mode 100644 index 000000000..ecc1460c0 --- /dev/null +++ b/python/python/knowledge_graph/component.py @@ -0,0 +1,111 @@ +"""Reusable FastAPI component for the Lance knowledge graph service.""" + +from __future__ import annotations + +from typing import Any, Dict, List, Optional + +import pyarrow as pa +import yaml +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel + +from .config import KnowledgeGraphConfig +from .service import LanceKnowledgeGraph +from .store import LanceGraphStore + + +class QueryRequest(BaseModel): + query: str + + +class QueryResponse(BaseModel): + rows: List[Dict[str, Any]] + row_count: int + + +class DatasetUpsertRequest(BaseModel): + records: List[Dict[str, Any]] + merge: bool = True + + +class KnowledgeGraphComponent: + """Bundle FastAPI routes that expose the Lance knowledge graph.""" + + def __init__(self, config: Optional[KnowledgeGraphConfig] = None): + self._config = config or KnowledgeGraphConfig.default() + self._service: Optional[LanceKnowledgeGraph] = None + self.router = APIRouter(tags=["knowledge-graph"]) + self._setup_routes() + + def _get_service(self) -> LanceKnowledgeGraph: + if self._service is None: + try: + self._service = _create_service(self._config) + except FileNotFoundError as exc: + raise HTTPException(status_code=500, detail=str(exc)) from exc + return self._service + + def _setup_routes(self) -> None: + @self.router.get("/health") + async def health() -> Dict[str, str]: + return {"status": "healthy", "service": "lance-knowledge-graph"} + + @self.router.get("/datasets") + async def list_datasets() -> Dict[str, List[str]]: + service = self._get_service() + names = list(service.dataset_names()) + return {"datasets": names} + + @self.router.get("/datasets/{name}") + async def get_dataset(name: str, limit: int = 100) -> Dict[str, Any]: + service = self._get_service() + if not service.has_dataset(name): + raise HTTPException( + status_code=404, detail=f"Dataset '{name}' not found" + ) + + table = service.load_table(name) + rows = table.to_pylist() + if limit is not None: + rows = rows[:limit] + return {"name": name, "row_count": len(rows), "rows": rows} + + @self.router.post("/datasets/{name}") + async def upsert_dataset( + name: str, request: DatasetUpsertRequest + ) -> Dict[str, Any]: + if not request.records: + raise HTTPException(status_code=400, detail="records cannot be empty") + + table = pa.Table.from_pylist(request.records) + service = self._get_service() + service.upsert_table(name, table, merge=request.merge) + return {"status": "ok", "dataset": name, "row_count": table.num_rows} + + @self.router.post("/query", response_model=QueryResponse) + async def execute_query(request: QueryRequest) -> QueryResponse: + service = self._get_service() + result = service.query(request.query) + rows = result.to_pylist() + return QueryResponse(rows=rows, row_count=len(rows)) + + @self.router.get("/schema") + async def get_schema() -> Dict[str, Any]: + schema_path = self._config.resolved_schema_path() + if not schema_path.exists(): + raise HTTPException(status_code=404, detail="Schema file not found") + with schema_path.open("r", encoding="utf-8") as handle: + payload = yaml.safe_load(handle) or {} + return {"path": str(schema_path), "schema": payload} + + def close(self) -> None: + """Release retained resources.""" + self._service = None + + +def _create_service(config: KnowledgeGraphConfig) -> LanceKnowledgeGraph: + graph_config = config.load_graph_config() + storage = LanceGraphStore(config) + service = LanceKnowledgeGraph(graph_config, storage=storage) + service.ensure_initialized() + return service diff --git a/python/python/knowledge_graph/config.py b/python/python/knowledge_graph/config.py new file mode 100644 index 000000000..4647bfa70 --- /dev/null +++ b/python/python/knowledge_graph/config.py @@ -0,0 +1,151 @@ +"""Configuration helpers for Lance-backed knowledge graphs.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Mapping, Optional + +import yaml +from lance_graph import GraphConfig + + +@dataclass(slots=True) +class KnowledgeGraphConfig: + """Root configuration for the Lance-backed knowledge graph.""" + + storage_path: Path + schema_path: Optional[Path] = None + default_dataset: Optional[str] = None + entity_types: tuple[str, ...] = field(default_factory=tuple) + relationship_types: tuple[str, ...] = field(default_factory=tuple) + + @classmethod + def from_root( + cls, + root: Path, + *, + default_dataset: Optional[str] = None, + ) -> KnowledgeGraphConfig: + """Create a configuration anchored at ``root``.""" + schema_path = root / "graph.yaml" + return cls( + storage_path=root, + schema_path=schema_path, + default_dataset=default_dataset, + ) + + @classmethod + def default(cls) -> KnowledgeGraphConfig: + """Use a storage folder relative to the current working directory.""" + return cls.from_root(Path.cwd() / "knowledge_graph_data") + + def resolved_schema_path(self) -> Path: + """Return the expected path to the graph schema definition.""" + return self.schema_path or (self.storage_path / "graph.yaml") + + def ensure_directories(self) -> None: + """Create required directories for persistent storage.""" + self.storage_path.mkdir(parents=True, exist_ok=True) + self.resolved_schema_path().parent.mkdir(parents=True, exist_ok=True) + + def with_schema(self, schema_path: Path) -> KnowledgeGraphConfig: + """Return a copy of the config with an explicit schema path.""" + return KnowledgeGraphConfig( + storage_path=self.storage_path, + schema_path=schema_path, + default_dataset=self.default_dataset, + entity_types=self.entity_types, + relationship_types=self.relationship_types, + ) + + def load_graph_config(self) -> GraphConfig: + """Load the Lance ``GraphConfig`` from the schema document.""" + payload = self._load_schema_payload() + entity_types = payload.get("entity_types") or [] + relationship_types = payload.get("relationship_types") or [] + self.entity_types = _normalize_type_list(entity_types) + self.relationship_types = _normalize_type_list(relationship_types) + if not payload.get("nodes") and not payload.get("relationships"): + return build_default_graph_config() + return build_graph_config_from_mapping(payload) + + def type_hints(self) -> dict[str, tuple[str, ...]]: + """Return configured entity and relationship type hints.""" + # Ensure the latest schema values are loaded. + try: + self.load_graph_config() + except FileNotFoundError: + pass + return { + "entity_types": self.entity_types, + "relationship_types": self.relationship_types, + } + + def _load_schema_payload(self) -> Mapping[str, Any]: + schema_path = self.resolved_schema_path() + if not schema_path.exists(): + raise FileNotFoundError( + f"Graph schema configuration not found at {schema_path}" + ) + with schema_path.open("r", encoding="utf-8") as handle: + payload = yaml.safe_load(handle) or {} + if not isinstance(payload, Mapping): + raise ValueError("Graph schema configuration must be a mapping") + return payload # type: ignore[return-value] + + +def build_graph_config_from_mapping(data: Mapping[str, Any]) -> GraphConfig: + """Create a ``GraphConfig`` instance from a schema mapping.""" + builder = GraphConfig.builder() + + for label, node_spec in data.get("nodes", {}).items(): + if isinstance(node_spec, str): + builder = builder.with_node_label(label, node_spec) + else: + id_field = node_spec.get("id_field") + if not id_field: + raise ValueError(f"Node '{label}' is missing 'id_field'") + builder = builder.with_node_label(label, id_field) + + for rel_type, rel_spec in data.get("relationships", {}).items(): + if not isinstance(rel_spec, Mapping): + raise ValueError( + ( + f"Relationship '{rel_type}' must be a mapping with 'source' and " + "'target'" + ) + ) + source_field = rel_spec.get("source") + target_field = rel_spec.get("target") + if not (source_field and target_field): + raise ValueError( + f"Relationship '{rel_type}' must define 'source' and 'target' fields" + ) + builder = builder.with_relationship(rel_type, source_field, target_field) + + return builder.build() + + +def build_default_graph_config() -> GraphConfig: + builder = GraphConfig.builder() + builder = builder.with_node_label("Entity", "entity_id") + builder = builder.with_relationship( + "RELATIONSHIP", + "source_entity_id", + "target_entity_id", + ) + return builder.build() + + +def _normalize_type_list(values: Any) -> tuple[str, ...]: + if not isinstance(values, list): + return tuple() + cleaned: list[str] = [] + for item in values: + if not isinstance(item, str): + continue + trimmed = item.strip() + if trimmed: + cleaned.append(trimmed) + return tuple(cleaned) diff --git a/python/python/knowledge_graph/extraction.py b/python/python/knowledge_graph/extraction.py new file mode 100644 index 000000000..3a7b24650 --- /dev/null +++ b/python/python/knowledge_graph/extraction.py @@ -0,0 +1,84 @@ +"""High-level extraction utilities combining heuristic and LLM strategies.""" + +from __future__ import annotations + +from typing import Any, Optional + +from .extractors import HeuristicExtractor, LLMExtractor +from .extractors.base import ExtractedEntity, ExtractedRelationship, ExtractionResult +from .extractors.llm import load_json_payload + +DEFAULT_STRATEGY = "llm" + + +def get_extractor( + strategy: str = DEFAULT_STRATEGY, + *, + llm_callable=None, + llm_model: str = "gpt-4o-mini", + llm_temperature: float = 0.2, + llm_options: Optional[dict] = None, +) -> "BaseExtractor": + strategy = strategy.lower() + if strategy == "heuristic": + return HeuristicExtractor() + if strategy == "llm": + return LLMExtractor( + send_callable=llm_callable, + model=llm_model, + temperature=llm_temperature, + client_options=llm_options or {}, + ) + raise ValueError(f"Unknown extractor strategy '{strategy}'") + + +def preview_extraction( + text: str, + *, + extractor: Optional["BaseExtractor"] = None, + strategy: str = DEFAULT_STRATEGY, +) -> ExtractionResult: + extractor = extractor or get_extractor(strategy) + entities = extractor.extract_entities(text) + relationships = extractor.extract_relationships(text, entities) + return ExtractionResult(entities=entities, relationships=relationships) + + +class BaseExtractor: + """Protocol-like base class for IDE support.""" + + def extract_entities(self, text: str) -> list[ExtractedEntity]: # pragma: no cover + raise NotImplementedError + + def extract_relationships( + self, text: str, entities: list[ExtractedEntity] + ) -> list[ExtractedRelationship]: # pragma: no cover + raise NotImplementedError + + +class LLMClient: + def __init__(self, extractor: LLMExtractor): + self._extractor = extractor + + def complete(self, prompt: str) -> str: + return self._extractor.generate(prompt) + + +def get_llm_client( + *, + llm_callable=None, + llm_model: str = "gpt-4o-mini", + llm_temperature: float = 0.2, + llm_options: Optional[dict] = None, +) -> LLMClient: + extractor = LLMExtractor( + send_callable=llm_callable, + model=llm_model, + temperature=llm_temperature, + client_options=llm_options or {}, + ) + return LLMClient(extractor) + + +def parse_llm_json(raw: str) -> Any: + return load_json_payload(raw) diff --git a/python/python/knowledge_graph/extractors/__init__.py b/python/python/knowledge_graph/extractors/__init__.py new file mode 100644 index 000000000..191510a97 --- /dev/null +++ b/python/python/knowledge_graph/extractors/__init__.py @@ -0,0 +1,13 @@ +"""Extraction strategies for converting text into graph entities.""" + +from .base import ExtractedEntity, ExtractedRelationship, ExtractionResult +from .heuristic import HeuristicExtractor +from .llm import LLMExtractor + +__all__ = [ + "ExtractedEntity", + "ExtractedRelationship", + "ExtractionResult", + "HeuristicExtractor", + "LLMExtractor", +] diff --git a/python/python/knowledge_graph/extractors/base.py b/python/python/knowledge_graph/extractors/base.py new file mode 100644 index 000000000..4e1551272 --- /dev/null +++ b/python/python/knowledge_graph/extractors/base.py @@ -0,0 +1,28 @@ +"""Shared dataclasses and helpers for extraction implementations.""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class ExtractedEntity: + name: str + entity_type: str + context: str + confidence: float + + +@dataclass(frozen=True) +class ExtractedRelationship: + source: str + target: str + relationship_type: str + description: str + confidence: float + + +@dataclass(frozen=True) +class ExtractionResult: + entities: list[ExtractedEntity] + relationships: list[ExtractedRelationship] diff --git a/python/python/knowledge_graph/extractors/heuristic.py b/python/python/knowledge_graph/extractors/heuristic.py new file mode 100644 index 000000000..00ac36a3a --- /dev/null +++ b/python/python/knowledge_graph/extractors/heuristic.py @@ -0,0 +1,51 @@ +"""Rule-based extraction that identifies simple entity candidates.""" + +from __future__ import annotations + +import re + +from .base import ExtractedEntity, ExtractedRelationship + + +class HeuristicExtractor: + """Simplified extractor that tags title-cased words as entity candidates.""" + + _word_pattern = re.compile(r"\b[A-Z][a-zA-Z0-9_]+\b") + + def extract_entities(self, text: str) -> list[ExtractedEntity]: + seen: set[str] = set() + entities: list[ExtractedEntity] = [] + + for match in self._word_pattern.finditer(text): + word = match.group() + if len(word) <= 2: + continue + if word.upper() == word: + continue # Skip acronyms such as SQL. + if word in seen: + continue + seen.add(word) + entities.append( + ExtractedEntity( + name=word, + entity_type="TERM", + context=_surrounding_context(text, match.start(), match.end()), + confidence=0.4, + ) + ) + return entities + + def extract_relationships( + self, + text: str, + entities: list[ExtractedEntity], + ) -> list[ExtractedRelationship]: + del text, entities + return [] + + +def _surrounding_context(text: str, start: int, end: int, radius: int = 50) -> str: + left = max(0, start - radius) + right = min(len(text), end + radius) + snippet = text[left:right].strip() + return snippet.replace("\n", " ") diff --git a/python/python/knowledge_graph/extractors/llm.py b/python/python/knowledge_graph/extractors/llm.py new file mode 100644 index 000000000..29914f0ac --- /dev/null +++ b/python/python/knowledge_graph/extractors/llm.py @@ -0,0 +1,230 @@ +"""LLM-backed extraction using a configurable text-generation callable.""" + +from __future__ import annotations + +import json +import logging +from typing import Any, Callable, Mapping, Sequence + +from .base import ExtractedEntity, ExtractedRelationship + +LOGGER = logging.getLogger(__name__) + + +class LLMExtractor: + """Use a large language model to extract entities and relationships.""" + + def __init__( + self, + *, + send_callable: Callable[[str], str] | None = None, + model: str = "gpt-4o-mini", + temperature: float = 0.2, + client_options: Mapping[str, Any] | None = None, + ) -> None: + self._model = model + self._temperature = temperature + self._send = send_callable or self._default_send + self._client_options = dict(client_options or {}) + + def generate(self, prompt: str) -> str: + """Return the raw text output from the LLM for a given prompt.""" + return self._send(prompt) + + def extract_entities(self, text: str) -> list[ExtractedEntity]: + prompt = _entity_prompt(text) + response = self._send(prompt) + payload = load_json_payload(response) + records = _select_records(payload, preferred_key="entities") + entities: list[ExtractedEntity] = [] + for item in records: + name = str(item.get("name", "")).strip() + if not name: + continue + entity_type = str(item.get("type", "TERM")) + context = str(item.get("description", "")) + confidence = float(item.get("confidence", 0.5) or 0.5) + entities.append( + ExtractedEntity( + name=name, + entity_type=entity_type, + context=context, + confidence=confidence, + ) + ) + return entities + + def extract_relationships( + self, + text: str, + entities: Sequence[ExtractedEntity], + ) -> list[ExtractedRelationship]: + if not entities: + return [] + prompt = _relationship_prompt(text, entities) + response = self._send(prompt) + payload = load_json_payload(response) + records = _select_records(payload, preferred_key="relationships") + relationships: list[ExtractedRelationship] = [] + for item in records: + source = str(item.get("source_entity_name", "")).strip() + target = str(item.get("target_entity_name", "")).strip() + if not source or not target: + continue + rel_type = str(item.get("type", "RELATED_TO")) + description = str(item.get("description", "")) + confidence = float(item.get("confidence", 0.5) or 0.5) + relationships.append( + ExtractedRelationship( + source=source, + target=target, + relationship_type=rel_type, + description=description, + confidence=confidence, + ) + ) + return relationships + + # Default send --------------------------------------------------------- + def _default_send(self, prompt: str) -> str: + try: + from openai import OpenAI # type: ignore[import-not-found] + except ImportError as exc: + raise RuntimeError( + "The `openai` package is required for LLM extraction. " + "Install it or provide `send_callable`." + ) from exc + + sanitized = _sanitize_options(self._client_options) + LOGGER.debug( + "Initializing OpenAI client with options", + extra={"lance_graph": {"openai_options": sanitized}}, + ) + + client = OpenAI(**self._client_options) + LOGGER.debug( + "Sending extraction prompt", + extra={ + "lance_graph": { + "openai_model": self._model, + "openai_temperature": self._temperature, + "openai_base_url": getattr(client, "base_url", None), + "openai_headers": sanitized.get("default_headers"), + } + }, + ) + response = client.responses.create( + model=self._model, + input=prompt, + temperature=self._temperature, + ) + if hasattr(response, "output_text"): + return response.output_text + if getattr(response, "output", None): + try: + chunks = [] + for item in response.output: # type: ignore[attr-defined] + for content in getattr(item, "content", []): + text = getattr(content, "text", None) + if text and hasattr(text, "value"): + chunks.append(text.value) + if chunks: + return "\n".join(chunks) + except Exception: # pragma: no cover - defensive fallback + pass + if getattr(response, "choices", None): + choice = response.choices[0] + message = getattr(choice, "message", None) + if message and hasattr(message, "content"): + return message.content # type: ignore[no-any-return] + raise RuntimeError("Unexpected response format from OpenAI responses API.") + + +# Prompt helpers -------------------------------------------------------------- +ENTITY_PROMPT_TEMPLATE = ( + "You are an information extraction assistant. Read the text and list entities " + "that are explicitly mentioned (do not infer unstated companies or people). " + "Return ONLY a valid JSON array. Each item must include keys `name`, `type`, " + "`description`, and `confidence` (0.0-1.0). Use types such as PERSON, " + "ORGANIZATION, TEAM, PROJECT, LOCATION, TECHNOLOGY when appropriate.\n\n" + "Text:\n" + "{body}\n\n" + "JSON:\n" +) + + +def _entity_prompt(text: str) -> str: + return ENTITY_PROMPT_TEMPLATE.format(body=text.strip()) + + +RELATIONSHIP_PROMPT_TEMPLATE = ( + "Given the text and the explicitly extracted entities, list relationships that " + "are directly stated (do not infer unstated connections). Return ONLY a valid " + "JSON array where each item contains `source_entity_name`, `target_entity_name`, " + "`type`, `description`, and `confidence` (0.0-1.0).\n\n" + "Text:\n" + "{body}\n\n" + "Entities:\n" + "{entities}\n\n" + "JSON:\n" +) + + +def _relationship_prompt( + text: str, + entities: Sequence[ExtractedEntity], +) -> str: + entity_lines = "\n".join( + f"- {entity.name} ({entity.entity_type})" for entity in entities + ) + return RELATIONSHIP_PROMPT_TEMPLATE.format( + body=text.strip(), + entities=entity_lines, + ) + + +def load_json_payload(raw: str) -> Any: + LOGGER.debug("LLM raw response: %s", raw) + cleaned = raw.strip() + if cleaned.startswith("```"): + parts = cleaned.split("```", 2) + if len(parts) >= 2: + cleaned = parts[1] + if cleaned.startswith("json"): + cleaned = cleaned[4:] + try: + return json.loads(cleaned) + except json.JSONDecodeError: + LOGGER.debug("Failed to decode LLM response as JSON") + return [] + + +def _select_records(payload: Any, preferred_key: str) -> list[dict[str, Any]]: + if isinstance(payload, list): + return [item for item in payload if isinstance(item, dict)] + if isinstance(payload, dict): + if preferred_key in payload and isinstance(payload[preferred_key], list): + return _select_records(payload[preferred_key], preferred_key) + for value in payload.values(): + if isinstance(value, list): + records = [item for item in value if isinstance(item, dict)] + if records: + return records + return [] + + +def _sanitize_options(options: Mapping[str, Any]) -> dict[str, Any]: + sanitized: dict[str, Any] = {} + for key, value in options.items(): + if key.lower() in {"api_key", "organization"}: + sanitized[key] = "***" + elif key == "default_headers" and isinstance(value, Mapping): + sanitized[key] = { + header_key: "***" + if "authorization" in header_key.lower() + else header_value + for header_key, header_value in value.items() + } + else: + sanitized[key] = value + return sanitized diff --git a/python/python/knowledge_graph/main.py b/python/python/knowledge_graph/main.py index 2dd48f77c..1fca1b9f9 100644 --- a/python/python/knowledge_graph/main.py +++ b/python/python/knowledge_graph/main.py @@ -1,40 +1,638 @@ -"""Command line interface for the knowledge_graph helpers.""" +"""Command line interface for the Lance-backed knowledge graph.""" from __future__ import annotations import argparse +import hashlib +import json +import logging +import sys +from dataclasses import asdict, is_dataclass from pathlib import Path -from typing import Optional, Sequence +from typing import TYPE_CHECKING, Any, Mapping, Optional, Sequence +import yaml -def init_graph() -> None: - """Initialize storage for the knowledge graph.""" - pass +from . import extraction as kg_extraction +from .config import KnowledgeGraphConfig +from .service import LanceKnowledgeGraph +from .store import LanceGraphStore +if TYPE_CHECKING: + import pyarrow as pa -def run_interactive() -> None: - """Enter an interactive shell for issuing commands.""" - pass + from .extractors import ExtractionResult -def execute_query(text: str) -> None: - """Execute a single knowledge graph query.""" - del text # placeholder until implementation +def init_graph(config: KnowledgeGraphConfig) -> None: + """Initialize the on-disk storage and scaffold the schema file.""" + config.ensure_directories() + LanceGraphStore(config).ensure_layout() + schema_path = config.resolved_schema_path() + if schema_path.exists(): + print(f"Schema already present at {schema_path}") + return + schema_stub = """\ +# Lance knowledge graph schema +# +# Define node labels and relationship mappings. Example: +# nodes: +# Person: +# id_field: person_id +# relationships: +# WORKS_FOR: +# source: person_id +# target: company_id +# entity_types: +# - PERSON +# - ORGANIZATION +# relationship_types: +# - WORKS_FOR +# - PART_OF +nodes: {} +relationships: {} +entity_types: [] +relationship_types: [] +""" + schema_path.write_text(schema_stub, encoding="utf-8") + print(f"Created schema template at {schema_path}") -def preview_extraction(path: Path) -> None: - """Preview extracted knowledge from a text source.""" - del path # placeholder until implementation +def list_datasets(config: KnowledgeGraphConfig) -> None: + """List the Lance datasets available under the configured root.""" + store = LanceGraphStore(config) + store.ensure_layout() + datasets = store.list_datasets() + if not datasets: + print("No Lance datasets found. Load data or run extraction first.") + return + print("Available Lance datasets:") + for name, path in sorted(datasets.items()): + print(f" - {name}: {path}") -def extract_and_add(path: Path) -> None: + +def run_interactive(service: LanceKnowledgeGraph) -> None: + """Enter an interactive shell for issuing Cypher queries.""" + print("Lance Knowledge Graph interactive shell") + print("Type ':help' for commands, or 'quit' to exit.") + + while True: + try: + text = input("kg> ").strip() + except EOFError: + print() + break + + if not text: + continue + lowered = text.lower() + if lowered in {"quit", "exit", "q"}: + break + if text.startswith(":"): + _handle_command(text, service) + continue + + _execute_query(service, text) + + +def _handle_command(command: str, service: LanceKnowledgeGraph) -> None: + """Handle meta-commands in the interactive shell.""" + cmd = command.strip() + if cmd in {":help", ":h"}: + print("Commands:") + print(" :help Show this message") + print(" :datasets List persisted Lance datasets") + print(" :config Show the configured node/relationship mappings") + print(" quit/exit/q Leave the shell") + return + if cmd in {":datasets", ":ls"}: + list_datasets(service.store.config) + return + if cmd in {":config", ":schema"}: + _print_config_summary(service) + return + print(f"Unknown command: {command}") + + +def _print_config_summary(service: LanceKnowledgeGraph) -> None: + """Print a brief summary of the graph configuration.""" + config = service.config + # GraphConfig does not currently expose direct iterators; rely on repr. + print("Graph configuration:") + print(f" {config!r}") + + +def _execute_query(service: LanceKnowledgeGraph, statement: str) -> None: + """Execute a single Cypher statement and print results.""" + try: + result = service.run(statement) + except Exception as exc: # pragma: no cover - CLI feedback path + print(f"Query failed: {exc}", file=sys.stderr) + return + + _print_table(result) + + +def _print_table(table: "pa.Table") -> None: + """Render a PyArrow table in a simple textual format.""" + if table.num_rows == 0: + print("(no rows)") + return + + column_names = table.column_names + columns = [table.column(i).to_pylist() for i in range(len(column_names))] + widths = [] + for name, values in zip(column_names, columns): + str_values = ["" if value is None else str(value) for value in values] + if str_values: + width = max(len(name), *(len(value) for value in str_values)) + else: + width = len(name) + widths.append(width) + + header = " | ".join(name.ljust(width) for name, width in zip(column_names, widths)) + separator = "-+-".join("-" * width for width in widths) + print(header) + print(separator) + for row_values in zip(*columns): + str_row = ["" if value is None else str(value) for value in row_values] + line = " | ".join(value.ljust(width) for value, width in zip(str_row, widths)) + print(line) + + +def preview_extraction(source: str, extractor: kg_extraction.BaseExtractor) -> None: + """Preview extracted knowledge from a text source or inline text.""" + text = _resolve_text_input(source) + result = kg_extraction.preview_extraction(text, extractor=extractor) + print(json.dumps(_result_to_dict(result), indent=2)) + + +def extract_and_add( + source: str, + service: LanceKnowledgeGraph, + extractor: kg_extraction.BaseExtractor, +) -> None: """Extract knowledge and append it to the backing graph.""" - del path # placeholder until implementation + import pyarrow as pa + + text = _resolve_text_input(source) + result = kg_extraction.preview_extraction(text, extractor=extractor) + entity_rows, name_to_id = _prepare_entity_rows(result.entities) + relationships = result.relationships + + if not entity_rows and not relationships: + print("No candidate entities or relationships detected.") + return + + if entity_rows: + entity_table = pa.Table.from_pylist(entity_rows) + service.upsert_table("Entity", entity_table, merge=True) + message = f"Upserted {entity_table.num_rows} entity rows into dataset 'Entity'." + print(message) + + relationship_rows = _prepare_relationship_rows(relationships, name_to_id) + if relationship_rows: + rel_table = pa.Table.from_pylist(relationship_rows) + service.upsert_table("RELATIONSHIP", rel_table, merge=True) + message = ( + "Upserted " + f"{rel_table.num_rows} relationship rows into dataset " + "'RELATIONSHIP'." + ) + print(message) + + +def ask_question( + question: str, + service: LanceKnowledgeGraph, + args: argparse.Namespace, +) -> None: + """Answer a natural-language question using the graph via LLM-assisted Cypher.""" + llm_client = _create_llm_client(args) + schema_summary = _summarize_schema(service) + type_hints = service.store.config.type_hints() + type_hint_lines = _build_type_hint_lines(type_hints) + query_prompt = _build_query_prompt( + question, + schema_summary, + type_hint_lines, + type_hints, + ) + + raw_plan = llm_client.complete(query_prompt) + plan_payload = kg_extraction.parse_llm_json(raw_plan) + query_plan = _extract_query_plan(plan_payload) + + if not query_plan: + print("Unable to generate Cypher queries for the question.") + return + + execution_results = _execute_queries(service, query_plan) + answer_prompt = _build_answer_prompt(question, schema_summary, execution_results) + raw_answer = llm_client.complete(answer_prompt) + + print(raw_answer.strip()) + + +def _resolve_text_input(raw: str) -> str: + """Load text from a file if it exists, otherwise treat the string as content.""" + candidate = Path(raw) + if candidate.exists(): + if candidate.is_dir(): + raise IsADirectoryError(f"Expected text file, got directory: {candidate}") + return candidate.read_text(encoding="utf-8") + return raw + + +def _ensure_dict(item: object) -> dict: + if is_dataclass(item): + return asdict(item) # type: ignore[arg-type] + if isinstance(item, dict): + return item + raise TypeError(f"Unsupported extraction item type: {type(item)!r}") + + +def _result_to_dict(result: "ExtractionResult") -> dict[str, list[dict]]: + return { + "entities": [asdict(entity) for entity in result.entities], + "relationships": [asdict(rel) for rel in result.relationships], + } -def ask_question(question: str) -> None: - """Answer a natural-language question using the graph.""" - del question # placeholder until implementation +def _prepare_entity_rows( + entities: list[Any], +) -> tuple[list[dict[str, Any]], dict[str, str]]: + rows: list[dict[str, Any]] = [] + name_to_id: dict[str, str] = {} + for entity in entities: + payload = _ensure_dict(entity) + name = str(payload.get("name", "")).strip() + entity_type = str( + payload.get("entity_type") or payload.get("type") or "" + ).strip() + if not name: + continue + base = f"{name}|{entity_type}".encode("utf-8") + entity_id = hashlib.md5(base).hexdigest() + payload["entity_id"] = entity_id + payload["entity_type"] = entity_type or "UNKNOWN" + payload["name_lower"] = name.lower() + rows.append(payload) + name_to_id.setdefault(name.lower(), entity_id) + return rows, name_to_id + + +def _prepare_relationship_rows( + relationships: list[Any], + name_to_id: dict[str, str], +) -> list[dict[str, Any]]: + rows: list[dict[str, Any]] = [] + for relation in relationships: + payload = _ensure_dict(relation) + source_name = str( + payload.get("source_entity_name") or payload.get("source") or "" + ).strip() + target_name = str( + payload.get("target_entity_name") or payload.get("target") or "" + ).strip() + source_id = name_to_id.get(source_name.lower()) + target_id = name_to_id.get(target_name.lower()) + if not (source_id and target_id): + continue + payload["source_entity_id"] = source_id + payload["target_entity_id"] = target_id + payload["relationship_type"] = ( + payload.get("relationship_type") or payload.get("type") or "RELATED_TO" + ) + payload.setdefault("source_entity_name", source_name) + payload.setdefault("target_entity_name", target_name) + rows.append(payload) + return rows + + +def _summarize_schema( + service: LanceKnowledgeGraph, + max_columns: int = 20, + max_value_samples: int = 10, +) -> str: + type_hints = service.store.config.type_hints() + lines = [] + for name in service.dataset_names(): + try: + table = service.load_table(name) + except Exception: + continue + if hasattr(table, "schema"): + columns = list(getattr(table.schema, "names", [])) + else: + columns = [] + if len(columns) > max_columns: + columns = columns[:max_columns] + ["..."] + column_summary = f"- {name}: {', '.join(columns)}" + lines.append(column_summary) + + # Provide helpful value samples for well-known columns. + extras: list[str] = [] + upper_name = name.upper() + if upper_name == "ENTITY" and type_hints.get("entity_types"): + allowed = ", ".join(type_hints["entity_types"]) + extras.append(f"allowed entity_type values: {allowed}") + try: + if ( + upper_name == "RELATIONSHIP" + and "relationship_type" in table.column_names + ): + values = table.column("relationship_type").to_pylist() + distinct = sorted({str(value) for value in values if value is not None}) + if distinct: + if len(distinct) > max_value_samples: + display = ", ".join(distinct[:max_value_samples]) + ", ..." + else: + display = ", ".join(distinct) + extras.append(f"relationship_type values: {display}") + if upper_name == "RELATIONSHIP" and type_hints.get("relationship_types"): + allowed = ", ".join(type_hints["relationship_types"]) + extras.append(f"allowed relationship_type values: {allowed}") + except Exception: + pass + + lines.extend(f" {extra}" for extra in extras) + if not lines: + return "(no datasets available)" + return "\n".join(lines) + + +def _build_type_hint_lines(type_hints: Mapping[str, tuple[str, ...]]) -> list[str]: + hints: list[str] = [] + entity_types = type_hints.get("entity_types") or () + relationship_types = type_hints.get("relationship_types") or () + if entity_types: + hints.append(f"entity_type values: {', '.join(entity_types)}") + if relationship_types: + hints.append(f"relationship_type values: {', '.join(relationship_types)}") + return hints + + +def _select_example_relationship_type( + type_hints: Mapping[str, tuple[str, ...]], +) -> str: + relationship_types = type_hints.get("relationship_types") or () + if relationship_types: + return relationship_types[0] + return "RELATIONSHIP_TYPE" + + +def _build_query_prompt( + question: str, + schema_summary: str, + type_hint_lines: list[str], + type_hints: Mapping[str, tuple[str, ...]], +) -> str: + example_rel_type = _select_example_relationship_type(type_hints) + instructions = "\n".join( + [ + "You translate questions into Cypher for Lance graph datasets.", + ( + "Use the schema summary to craft queries that directly answer the " + "question." + ), + ( + "Always specify node labels and relationship types in MATCH patterns " + "that introduce aliases." + ), + "Supported constructs include:", + (" • MATCH (e:Entity) to scan entity rows (name, name_lower, entity_id)."), + ( + " • MATCH (src:Entity)-[rel:RELATIONSHIP]->(dst:Entity) to traverse " + "relationships (relationship_type column)." + ), + " • Use WHERE e.column = 'value' for node-level filters.", + ( + " • Filter relationships with WHERE rel.relationship_type = 'VALUE' " + "or by comparing rel.source_entity_id / rel.target_entity_id." + ), + ( + " • Select columns using the aliases you define, such as e.name or " + "rel.relationship_type." + ), + ( + " • Avoid inventing relationship datasets; match RELATIONSHIP and " + "filter rel.relationship_type instead of [:TYPE]." + ), + ( + "Example: MATCH (src:Entity)-[rel:RELATIONSHIP]->(dst:Entity) " + f"WHERE rel.relationship_type = '{example_rel_type}' RETURN rel." + ), + ( + "Example: MATCH (dst:Entity) WHERE dst.name_lower = 'acme corp' " + "RETURN dst.name, dst.entity_id." + ), + ( + f"Do not use relationship patterns like [:{example_rel_type}]; rely on " + "rel.relationship_type filters instead." + ), + ( + "Always emit at least one query when relevant data exists; only " + "return [] when it is impossible to answer." + ), + "Return ONLY a JSON array where each item has `cypher` and `description`.", + ] + ) + + if type_hint_lines: + hint_block = "\n".join(f" • {line}" for line in type_hint_lines) + instructions = "\n".join( + [ + instructions, + "Allowed labels and type values:", + hint_block, + ] + ) + + prompt_parts = [ + instructions, + f"Schema summary:\n{schema_summary}", + f"Question:\n{question}", + "JSON:", + ] + return "\n\n".join(prompt_parts) + + +def _extract_query_plan(payload: Any) -> list[dict[str, str]]: + items: list[Any] + if isinstance(payload, list): + items = payload + elif isinstance(payload, dict): + items = payload.get("queries") or payload.get("plan") or [] + else: + return [] + + plan: list[dict[str, str]] = [] + for entry in items: + if not isinstance(entry, dict): + continue + cypher = entry.get("cypher") or entry.get("query") + if not cypher: + continue + plan.append( + { + "cypher": cypher, + "description": entry.get("description", ""), + } + ) + return plan + + +def _execute_queries( + service: LanceKnowledgeGraph, plan: list[dict[str, str]], max_rows: int = 20 +) -> list[dict[str, Any]]: + results: list[dict[str, Any]] = [] + for step in plan: + cypher = step["cypher"] + description = step.get("description", "") + entry: dict[str, Any] = {"cypher": cypher, "description": description} + try: + table = service.run(cypher) + rows = table.to_pylist() if hasattr(table, "to_pylist") else [] + truncated = False + if isinstance(rows, list) and len(rows) > max_rows: + truncated = True + rows = rows[:max_rows] + entry["rows"] = rows + entry["truncated"] = truncated + preview = json.dumps(rows, ensure_ascii=False, indent=2) + if truncated: + logging.debug( + "Cypher result (truncated to %s rows): %s", + max_rows, + preview, + ) + else: + logging.debug("Cypher result rows: %s", preview) + logging.debug( + "Cypher execution", + extra={"lance_graph": {"cypher": cypher, "rows": rows}}, + ) + except Exception as exc: # pragma: no cover - runtime safety + entry["error"] = str(exc) + data_preview = {} + for name, table in service.load_tables(service.dataset_names()).items(): + if hasattr(table, "schema"): + schema_names = list(table.schema.names) + else: + schema_names = [] + try: + row_limit = min(max_rows, getattr(table, "num_rows", 0)) + sample_rows = table.slice(0, row_limit).to_pylist() + except Exception: + sample_rows = [] + data_preview[name] = { + "schema": schema_names, + "rows_preview": sample_rows, + } + dataset_summary = json.dumps(data_preview, ensure_ascii=False, indent=2) + logging.debug( + "Cypher execution error\nCypher: %s\nError: %s\nDatasets: %s", + cypher, + str(exc), + dataset_summary, + ) + results.append(entry) + return results + + +def _build_answer_prompt( + question: str, + schema_summary: str, + query_results: list[dict[str, Any]], +) -> str: + sections = [ + "You are a graph analysis assistant.", + "Provide a concise answer using the query results.", + "If the data is insufficient, state that clearly.", + "Schema summary:", + schema_summary, + "Query results:", + ] + for idx, item in enumerate(query_results, 1): + sections.append(f"Query {idx}: {item['cypher']}") + if item.get("description"): + sections.append(f"Description: {item['description']}") + if "error" in item: + sections.append(f"Error: {item['error']}") + else: + rows_json = json.dumps(item.get("rows", []), ensure_ascii=False, indent=2) + sections.append(f"Rows: {rows_json}") + if item.get("truncated"): + sections.append("(results truncated)") + sections.append(f"Question: {question}") + sections.append("Answer:") + return "\n".join(sections) + + +def _load_config(args: argparse.Namespace) -> KnowledgeGraphConfig: + """Derive the configuration object from CLI arguments.""" + if args.root: + config = KnowledgeGraphConfig.from_root(Path(args.root)) + else: + config = KnowledgeGraphConfig.default() + if args.schema: + config = config.with_schema(Path(args.schema)) + return config + + +def _load_service(config: KnowledgeGraphConfig) -> LanceKnowledgeGraph: + """Instantiate the knowledge graph service.""" + graph_config = config.load_graph_config() + storage = LanceGraphStore(config) + service = LanceKnowledgeGraph(graph_config, storage=storage) + service.ensure_initialized() + return service + + +def _resolve_extractor(args: argparse.Namespace) -> kg_extraction.BaseExtractor: + options = _load_llm_options(args.llm_config) + return kg_extraction.get_extractor( + args.extractor, + llm_model=args.llm_model, + llm_temperature=args.llm_temperature, + llm_options=options, + ) + + +def _load_llm_options(path: Optional[Path]) -> dict: + if not path: + return {} + if not path.exists(): + raise FileNotFoundError(f"LLM config file not found: {path}") + with path.open("r", encoding="utf-8") as handle: + data = yaml.safe_load(handle) or {} + if not isinstance(data, dict): + raise ValueError("LLM config must be a mapping") + # Normalize nested http headers key to match OpenAI naming (default_headers) + if "default_headers" not in data and "http_headers" in data: + headers = data.pop("http_headers") + if isinstance(headers, dict): + data["default_headers"] = headers + return data + + +def _create_llm_client(args: argparse.Namespace) -> kg_extraction.LLMClient: + options = _load_llm_options(args.llm_config) + return kg_extraction.get_llm_client( + llm_model=args.llm_model, + llm_temperature=args.llm_temperature, + llm_options=options, + ) + + +def _configure_logging(level: str) -> None: + normalized = level.upper() + numeric = getattr(logging, normalized, None) + if not isinstance(numeric, int): + raise ValueError(f"Invalid log level: {level}") + logging.basicConfig(level=numeric) def _build_parser() -> argparse.ArgumentParser: @@ -42,6 +640,52 @@ def _build_parser() -> argparse.ArgumentParser: prog="knowledge_graph", description="Operate the Lance-backed knowledge graph.", ) + parser.add_argument( + "--root", + type=Path, + help="Root directory for Lance datasets (default: ./knowledge_graph_data).", + ) + parser.add_argument( + "--schema", + type=Path, + help="Path to a YAML file describing node and relationship mappings.", + ) + parser.add_argument( + "--list-datasets", + action="store_true", + help="List stored Lance datasets and exit.", + ) + parser.add_argument( + "--extractor", + choices=["heuristic", "llm"], + default=kg_extraction.DEFAULT_STRATEGY, + help="Extraction strategy to use (default: llm).", + ) + parser.add_argument( + "--llm-model", + default="gpt-4o-mini", + help="LLM model identifier when using --extractor llm.", + ) + parser.add_argument( + "--llm-temperature", + type=float, + default=0.2, + help="Sampling temperature for --extractor llm (default: 0.2).", + ) + parser.add_argument( + "--llm-config", + type=Path, + help=( + "Optional YAML file with OpenAI client options (api_key, base_url, " + "headers, etc)." + ), + ) + parser.add_argument( + "--log-level", + default="WARNING", + help="Logging level (DEBUG, INFO, WARNING, ERROR).", + ) + group = parser.add_mutually_exclusive_group() group.add_argument( "--init", @@ -50,13 +694,13 @@ def _build_parser() -> argparse.ArgumentParser: ) group.add_argument( "--extract-preview", - metavar="PATH", - help="Preview extracted entities and relations from a text file.", + metavar="INPUT", + help="Preview extracted knowledge from a file path or raw text.", ) group.add_argument( "--extract-and-add", - metavar="PATH", - help="Extract and insert knowledge from a text file.", + metavar="INPUT", + help="Extract knowledge from a file path or raw text and insert it.", ) group.add_argument( "--ask", @@ -66,7 +710,7 @@ def _build_parser() -> argparse.ArgumentParser: parser.add_argument( "query", nargs="?", - help="Execute a single Cypher or semantic query.", + help="Execute a single Cypher query against the Lance datasets.", ) return parser @@ -74,6 +718,8 @@ def _build_parser() -> argparse.ArgumentParser: def main(argv: Optional[Sequence[str]] = None) -> int: parser = _build_parser() args = parser.parse_args(argv) + config = _load_config(args) + _configure_logging(args.log_level) exclusive_args = any( [ @@ -81,28 +727,46 @@ def main(argv: Optional[Sequence[str]] = None) -> int: args.extract_preview is not None, args.extract_and_add is not None, args.ask is not None, + args.list_datasets, ] ) if args.query and exclusive_args: - parser.error("Query argument cannot be combined with flags.") + parser.error( + "Query argument cannot be combined with --init/--ask/--extract-* flags." + ) if args.init: - init_graph() + init_graph(config) + return 0 + if args.list_datasets: + list_datasets(config) return 0 if args.extract_preview: - preview_extraction(Path(args.extract_preview)) + extractor = _resolve_extractor(args) + preview_extraction(args.extract_preview, extractor) return 0 + try: + service = _load_service(config) + except FileNotFoundError as exc: + message = ( + f"{exc}. Run `knowledge_graph --init` or provide a schema with --schema." + ) + print(message, file=sys.stderr) + return 1 + if args.extract_and_add: - extract_and_add(Path(args.extract_and_add)) + extractor = _resolve_extractor(args) + extract_and_add(args.extract_and_add, service, extractor) return 0 if args.ask: - ask_question(args.ask) + ask_question(args.ask, service, args) return 0 + if args.query: - execute_query(args.query) + _execute_query(service, args.query) return 0 - run_interactive() + run_interactive(service) return 0 diff --git a/python/python/knowledge_graph/service.py b/python/python/knowledge_graph/service.py new file mode 100644 index 000000000..d9b932ed7 --- /dev/null +++ b/python/python/knowledge_graph/service.py @@ -0,0 +1,183 @@ +"""High-level Lance knowledge graph orchestration.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Iterable, Mapping, MutableMapping, Optional + +from lance_graph import CypherQuery, GraphConfig + +from .config import KnowledgeGraphConfig, build_default_graph_config +from .store import LanceGraphStore + +if TYPE_CHECKING: + import pyarrow as pa + + from . import KnowledgeGraph + + +class LanceKnowledgeGraph: + """Coordinate Lance datasets, configs, and query execution.""" + + def __init__( + self, + config: GraphConfig, + *, + storage: LanceGraphStore, + ): + self._config = config + self._store = storage + + @property + def config(self) -> GraphConfig: + """Expose the active graph configuration.""" + return self._config + + @property + def store(self) -> LanceGraphStore: + """Access the underlying Lance dataset store.""" + return self._store + + def dataset_names(self) -> Iterable[str]: + """Return the known dataset identifiers.""" + return self._store.list_datasets().keys() + + def ensure_initialized(self) -> None: + """Create any required on-disk structure.""" + self._store.ensure_layout() + + def materialize(self) -> KnowledgeGraph: + """Materialize an in-memory ``KnowledgeGraph`` view of the datasets.""" + from . import KnowledgeGraph # Avoid circular import at module load time + + tables = self._store.load_tables() + return KnowledgeGraph(self._config, tables) + + def has_dataset(self, name: str) -> bool: + """Return ``True`` when a dataset has been persisted under ``name``.""" + path = self._store.list_datasets().get(name) + return path is not None and path.exists() + + def load_tables( + self, + names: Optional[Iterable[str]] = None, + ) -> Mapping[str, "pa.Table"]: + """Load persisted datasets as PyArrow tables.""" + return self._store.load_tables(names) + + def load_table(self, name: str) -> "pa.Table": + """Load a single dataset by name.""" + tables = self._store.load_tables([name]) + return tables[name] + + def write_tables( + self, + tables: Mapping[str, "pa.Table"], + ) -> None: + """Persist a batch of tables.""" + self._store.write_tables(tables) + + def upsert_table( + self, + name: str, + table: "pa.Table", + *, + merge: bool = True, + ) -> None: + """Insert or replace a dataset, merging with the existing table if requested.""" + import pyarrow as pa + + self.ensure_initialized() + new_rows = table.to_pylist() + if merge and name in self._store.list_datasets(): + existing = self.load_table(name) + existing_rows = existing.to_pylist() + combined_rows = _normalize_rows(name, existing_rows + new_rows) + else: + combined_rows = _normalize_rows(name, new_rows) + if combined_rows: + table = pa.Table.from_pylist(combined_rows) + else: + table = pa.Table.from_arrays([], schema=table.schema) + self._store.write_tables({name: table}) + + def upsert_tables( + self, + tables: Mapping[str, "pa.Table"], + *, + merge: bool = True, + ) -> None: + """Insert multiple datasets, merging each with existing data when requested.""" + for name, table in tables.items(): + self.upsert_table(name, table, merge=merge) + + def run( + self, + statement: str, + *, + datasets: Optional[Mapping[str, pa.Table]] = None, + ) -> pa.Table: + """Execute a Cypher statement against Lance datasets.""" + query = CypherQuery(statement).with_config(self._config) + base_tables: MutableMapping[str, "pa.Table"] = dict(self._store.load_tables()) + if datasets: + base_tables.update(datasets) + return query.execute(base_tables) + + def query( + self, + statement: str, + *, + datasets: Optional[Mapping[str, "pa.Table"]] = None, + ) -> "pa.Table": + """Alias for :meth:`run` to match the semantic service naming.""" + return self.run(statement, datasets=datasets) + + +def create_default_service( + config: Optional[KnowledgeGraphConfig] = None, + *, + graph_config: Optional[GraphConfig] = None, +) -> LanceKnowledgeGraph: + """Construct a knowledge graph service with default settings.""" + config = config or KnowledgeGraphConfig.default() + storage = LanceGraphStore(config) + if graph_config is None: + graph_config = build_default_graph_config() + return LanceKnowledgeGraph(graph_config, storage=storage) + + +def _normalize_rows(name: str, rows: list[dict]) -> list[dict]: + normalized: list[dict] = [] + upper = name.upper() + if upper == "ENTITY": + dedupe = {} + for row in rows: + entity_id = row.get("entity_id") + if not entity_id: + continue + row["name_lower"] = str(row.get("name", "")).lower() + row["entity_type"] = row.get("entity_type") or row.get("type") or "UNKNOWN" + dedupe[entity_id] = row + normalized = list(dedupe.values()) + elif upper == "RELATIONSHIP": + dedupe = {} + for row in rows: + source = row.get("source_entity_id") + target = row.get("target_entity_id") + if not source or not target: + continue + relationship_type = ( + row.get("relationship_type") or row.get("type") or "RELATED_TO" + ) + key = ( + source, + target, + relationship_type, + row.get("description"), + ) + row["relationship_type"] = relationship_type + dedupe[key] = row + normalized = list(dedupe.values()) + else: + normalized = rows + return normalized diff --git a/python/python/knowledge_graph/store.py b/python/python/knowledge_graph/store.py new file mode 100644 index 000000000..49ea0487b --- /dev/null +++ b/python/python/knowledge_graph/store.py @@ -0,0 +1,132 @@ +"""Persistent storage helpers built on Lance datasets (with Feather fallback).""" + +from __future__ import annotations + +import logging +from importlib import import_module +from typing import TYPE_CHECKING, Dict, Iterable, Mapping, Optional + +if TYPE_CHECKING: + from pathlib import Path + from types import ModuleType + + import pyarrow as pa + + from .config import KnowledgeGraphConfig + + +LOGGER = logging.getLogger(__name__) + + +class LanceGraphStore: + """Manage Lance-backed tables that feed the query engine.""" + + def __init__(self, config: "KnowledgeGraphConfig"): + self._config = config + self._root: "Path" = config.storage_path + self._lance: Optional[ModuleType] = None + self._lance_attempted = False + + @property + def config(self) -> "KnowledgeGraphConfig": + """Return the configuration backing this store.""" + return self._config + + @property + def root(self) -> "Path": + """Return the root path for persisted datasets.""" + return self._root + + def ensure_layout(self) -> None: + """Create the storage layout if it does not already exist.""" + self._root.mkdir(parents=True, exist_ok=True) + + def list_datasets(self) -> Dict[str, "Path"]: + """Enumerate known Lance datasets.""" + datasets: Dict[str, Path] = {} + if not self._root.exists(): + return datasets + valid_suffixes = {".lance", ".arrow"} + for child in self._root.iterdir(): + if child.is_dir() and child.suffix == ".lance": + datasets[child.stem] = child + elif child.is_file() and child.suffix in valid_suffixes: + datasets[child.stem] = child + return datasets + + def _dataset_path(self, name: str) -> "Path": + """Create the canonical path for a dataset.""" + safe_name = name.replace("/", "_") + suffix = ".lance" if self._get_lance() else ".arrow" + return self._root / f"{safe_name}{suffix}" + + def _get_lance(self) -> Optional[ModuleType]: + if not self._lance_attempted: + self._lance_attempted = True + try: + module = import_module("lance") + except ImportError: + module = None + else: + has_writer = hasattr(module, "write_dataset") + has_loader = hasattr(module, "dataset") + if not (has_writer and has_loader): + LOGGER.warning( + "Installed `lance` package missing dataset APIs; " + "falling back to Feather storage." + ) + module = None + self._lance = module + if module is None: + LOGGER.debug( + "Lance storage unavailable; using Feather files under %s.", + self._root, + ) + return self._lance + + def load_tables( + self, + names: Optional[Iterable[str]] = None, + ) -> Mapping[str, "pa.Table"]: + """Load Lance datasets as PyArrow tables.""" + lance = self._get_lance() + use_lance = lance is not None + + self.ensure_layout() + available = self.list_datasets() + requested = list(names) if names is not None else list(available.keys()) + + tables: Dict[str, "pa.Table"] = {} + for name in requested: + path = available.get(name, self._dataset_path(name)) + if not path.exists(): + raise FileNotFoundError(f"Dataset '{name}' not found at {path}") + if path.suffix == ".lance" and use_lance: + dataset = lance.dataset(str(path)) # type: ignore[union-attr] + table = dataset.scanner().to_table() + else: + import pyarrow.feather as feather + + table = feather.read_table(str(path)) + tables[name] = table + return tables + + def write_tables(self, tables: Mapping[str, "pa.Table"]) -> None: + """Persist PyArrow tables as Lance datasets.""" + lance = self._get_lance() + import pyarrow as pa # Local import; optional dependency + + self.ensure_layout() + for name, table in tables.items(): + if not isinstance(table, pa.Table): + raise TypeError( + f"Dataset '{name}' must be a pyarrow.Table (got {type(table)!r})" + ) + path = self._dataset_path(name) + if path.suffix == ".lance" and lance is not None: + mode = "overwrite" if path.exists() else "create" + lance.write_dataset(table, str(path), mode=mode) # type: ignore[union-attr] + else: + import pyarrow.feather as feather + + feather.write_feather(table, str(path)) diff --git a/python/python/knowledge_graph/webservice.py b/python/python/knowledge_graph/webservice.py new file mode 100644 index 000000000..1f6f59473 --- /dev/null +++ b/python/python/knowledge_graph/webservice.py @@ -0,0 +1,45 @@ +"""Standalone FastAPI application for the Lance knowledge graph.""" + +from __future__ import annotations + +from contextlib import asynccontextmanager +from typing import TYPE_CHECKING, Optional + +import uvicorn +from fastapi import FastAPI + +from .component import KnowledgeGraphComponent + +if TYPE_CHECKING: + from .config import KnowledgeGraphConfig + + +def create_app(config: Optional["KnowledgeGraphConfig"] = None) -> FastAPI: + component = KnowledgeGraphComponent(config) + + @asynccontextmanager + async def lifespan(app: FastAPI): + try: + yield + finally: + component.close() + + app = FastAPI( + title="Lance Knowledge Graph API", + version="0.1.0", + lifespan=lifespan, + ) + app.include_router(component.router, prefix="/graph") + return app + + +app = create_app() + + +def main() -> None: + """Run the web service using uvicorn.""" + uvicorn.run(app, host="0.0.0.0", port=8000) + + +if __name__ == "__main__": + main() diff --git a/python/python/tests/test_extraction.py b/python/python/tests/test_extraction.py new file mode 100644 index 000000000..3b8a79f4c --- /dev/null +++ b/python/python/tests/test_extraction.py @@ -0,0 +1,64 @@ +import json + +import pytest +from knowledge_graph.extraction import preview_extraction +from knowledge_graph.extractors import HeuristicExtractor, LLMExtractor + + +@pytest.mark.parametrize( + "text", + [ + "Alice collaborates with Bob on GraphAI", + "Carol joined DataCorp alongside David", + ], +) +def test_heuristic_extractor_identifies_title_case_entities(text): + extractor = HeuristicExtractor() + entities = extractor.extract_entities(text) + names = {entity.name for entity in entities} + assert any(name in names for name in {"Alice", "Bob", "Carol", "David"}) + + +def test_preview_extraction_returns_entities_and_relationships(): + result = preview_extraction("Alice works with Bob", strategy="heuristic") + assert result.entities + assert isinstance(result.entities[0].name, str) + assert isinstance(result.relationships, list) + + +def test_llm_extractor_can_be_injected_with_callable(): + entities_payload = json.dumps( + [ + { + "name": "Alice", + "type": "PERSON", + "description": "Engineer", + "confidence": 0.9, + } + ] + ) + relationships_payload = json.dumps( + [ + { + "source_entity_name": "Alice", + "target_entity_name": "Bob", + "type": "WORKS_WITH", + "description": "Alice works with Bob", + "confidence": 0.8, + } + ] + ) + + def fake_send(prompt: str) -> str: + if "source_entity_name" in prompt: + return relationships_payload + return entities_payload + + extractor = LLMExtractor(send_callable=fake_send) + result = preview_extraction( + "Alice works with Bob", + extractor=extractor, + ) + assert len(result.entities) == 1 + assert result.entities[0].name == "Alice" + assert result.relationships[0].relationship_type == "WORKS_WITH" diff --git a/python/python/tests/test_knowledge_graph_config.py b/python/python/tests/test_knowledge_graph_config.py new file mode 100644 index 000000000..1e34bf424 --- /dev/null +++ b/python/python/tests/test_knowledge_graph_config.py @@ -0,0 +1,29 @@ +import pyarrow as pa +import pytest +from knowledge_graph.config import build_graph_config_from_mapping +from lance_graph import CypherQuery + + +def test_build_graph_config_from_mapping_supports_simple_nodes(): + mapping = {"nodes": {"Person": "person_id"}} + config = build_graph_config_from_mapping(mapping) + + table = pa.table( + { + "person_id": [1, 2], + "name": ["Alice", "Bob"], + } + ) + + data = ( + CypherQuery("MATCH (p:Person) RETURN p.person_id AS id") + .with_config(config) + .execute({"Person": table}) + .to_pydict() + ) + assert data["person_id"] == [1, 2] + + +def test_build_graph_config_from_mapping_requires_id_field(): + with pytest.raises(ValueError): + build_graph_config_from_mapping({"nodes": {"Person": {}}}) diff --git a/python/uv.lock b/python/uv.lock index c60063817..d2a077227 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -8,6 +8,71 @@ resolution-markers = [ "python_full_version < '3.10'", ] +[[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.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, +] + +[[package]] +name = "certifi" +version = "2025.10.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and 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 = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -17,6 +82,15 @@ 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 = "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 = "exceptiongroup" version = "1.3.0" @@ -29,6 +103,66 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] +[[package]] +name = "fastapi" +version = "0.119.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/f9/5c5bcce82a7997cc0eb8c47b7800f862f6b56adc40486ed246e5010d443b/fastapi-0.119.0.tar.gz", hash = "sha256:451082403a2c1f0b99c6bd57c09110ed5463856804c8078d38e5a1f1035dbbb7", size = 336756, upload-time = "2025-10-11T17:13:40.53Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/70/584c4d7cad80f5e833715c0a29962d7c93b4d18eed522a02981a6d1b6ee5/fastapi-0.119.0-py3-none-any.whl", hash = "sha256:90a2e49ed19515320abb864df570dd766be0662c5d577688f1600170f7f73cf2", size = 107095, upload-time = "2025-10-11T17:13:39.048Z" }, +] + +[[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 = "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 = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + [[package]] name = "iniconfig" version = "2.1.0" @@ -38,11 +172,133 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] +[[package]] +name = "jiter" +version = "0.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/68/0357982493a7b20925aece061f7fb7a2678e3b232f8d73a6edb7e5304443/jiter-0.11.1.tar.gz", hash = "sha256:849dcfc76481c0ea0099391235b7ca97d7279e0fa4c86005457ac7c88e8b76dc", size = 168385, upload-time = "2025-10-17T11:31:15.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/10/d099def5716452c8d5ffa527405373a44ddaf8e3c9d4f6de1e1344cffd90/jiter-0.11.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ed58841a491bbbf3f7c55a6b68fff568439ab73b2cce27ace0e169057b5851df", size = 310078, upload-time = "2025-10-17T11:28:36.186Z" }, + { url = "https://files.pythonhosted.org/packages/fe/56/b81d010b0031ffa96dfb590628562ac5f513ce56aa2ab451d29fb3fedeb9/jiter-0.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:499beb9b2d7e51d61095a8de39ebcab1d1778f2a74085f8305a969f6cee9f3e4", size = 317138, upload-time = "2025-10-17T11:28:38.294Z" }, + { url = "https://files.pythonhosted.org/packages/89/12/31ea12af9d79671cc7bd893bf0ccaf3467624c0fc7146a0cbfe7b549bcfa/jiter-0.11.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b87b2821795e28cc990939b68ce7a038edea680a24910bd68a79d54ff3f03c02", size = 348964, upload-time = "2025-10-17T11:28:40.103Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/95cb6dc5ff962410667a29708c7a6c0691cc3c4866a0bfa79d085b56ebd6/jiter-0.11.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:83f6fa494d8bba14ab100417c80e70d32d737e805cb85be2052d771c76fcd1f8", size = 363289, upload-time = "2025-10-17T11:28:41.49Z" }, + { url = "https://files.pythonhosted.org/packages/b8/3e/37006ad5843a0bc3a3ec3a6c44710d7a154113befaf5f26d2fe190668b63/jiter-0.11.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fbc6aea1daa2ec6f5ed465f0c5e7b0607175062ceebbea5ca70dd5ddab58083", size = 487243, upload-time = "2025-10-17T11:28:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/80/5c/d38c8c801a322a0c0de47b9618c16fd766366f087ce37c4e55ae8e3c8b03/jiter-0.11.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:302288e2edc43174bb2db838e94688d724f9aad26c5fb9a74f7a5fb427452a6a", size = 376139, upload-time = "2025-10-17T11:28:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/b0/cd/442ad2389a5570b0ee673f93e14bbe8cdecd3e08a9ba7756081d84065e4c/jiter-0.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85db563fe3b367bb568af5d29dea4d4066d923b8e01f3417d25ebecd958de815", size = 359279, upload-time = "2025-10-17T11:28:46.152Z" }, + { url = "https://files.pythonhosted.org/packages/9a/35/8f5810d0e7d00bc395889085dbc1ccc36d454b56f28b2a5359dfd1bab48d/jiter-0.11.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f1c1ba2b6b22f775444ef53bc2d5778396d3520abc7b2e1da8eb0c27cb3ffb10", size = 384911, upload-time = "2025-10-17T11:28:48.03Z" }, + { url = "https://files.pythonhosted.org/packages/3c/bd/8c069ceb0bafcf6b4aa5de0c27f02faf50468df39564a02e1a12389ad6c2/jiter-0.11.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:523be464b14f8fd0cc78da6964b87b5515a056427a2579f9085ce30197a1b54a", size = 517879, upload-time = "2025-10-17T11:28:49.902Z" }, + { url = "https://files.pythonhosted.org/packages/bc/3c/9163efcf762f79f47433078b4f0a1bddc56096082c02c6cae2f47f07f56f/jiter-0.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25b99b3f04cd2a38fefb22e822e35eb203a2cd37d680dbbc0c0ba966918af336", size = 508739, upload-time = "2025-10-17T11:28:51.785Z" }, + { url = "https://files.pythonhosted.org/packages/44/07/50690f257935845d3114b95b5dd03749eeaab5e395cbb522f9e957da4551/jiter-0.11.1-cp310-cp310-win32.whl", hash = "sha256:47a79e90545a596bb9104109777894033347b11180d4751a216afef14072dbe7", size = 203948, upload-time = "2025-10-17T11:28:54.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/5964a944bf2e98ffd566153fdc2a6a368fcb11b58cc46832ca8c75808dba/jiter-0.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:cace75621ae9bd66878bf69fbd4dfc1a28ef8661e0c2d0eb72d3d6f1268eddf5", size = 207522, upload-time = "2025-10-17T11:28:56.79Z" }, + { url = "https://files.pythonhosted.org/packages/8b/34/c9e6cfe876f9a24f43ed53fe29f052ce02bd8d5f5a387dbf46ad3764bef0/jiter-0.11.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9b0088ff3c374ce8ce0168523ec8e97122ebb788f950cf7bb8e39c7dc6a876a2", size = 310160, upload-time = "2025-10-17T11:28:59.174Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9f/b06ec8181d7165858faf2ac5287c54fe52b2287760b7fe1ba9c06890255f/jiter-0.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74433962dd3c3090655e02e461267095d6c84f0741c7827de11022ef8d7ff661", size = 316573, upload-time = "2025-10-17T11:29:00.905Z" }, + { url = "https://files.pythonhosted.org/packages/66/49/3179d93090f2ed0c6b091a9c210f266d2d020d82c96f753260af536371d0/jiter-0.11.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d98030e345e6546df2cc2c08309c502466c66c4747b043f1a0d415fada862b8", size = 348998, upload-time = "2025-10-17T11:29:02.321Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/63db2c8eabda7a9cad65a2e808ca34aaa8689d98d498f5a2357d7a2e2cec/jiter-0.11.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d6db0b2e788db46bec2cf729a88b6dd36959af2abd9fa2312dfba5acdd96dcb", size = 363413, upload-time = "2025-10-17T11:29:03.787Z" }, + { url = "https://files.pythonhosted.org/packages/25/ff/3e6b3170c5053053c7baddb8d44e2bf11ff44cd71024a280a8438ae6ba32/jiter-0.11.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55678fbbda261eafe7289165dd2ddd0e922df5f9a1ae46d7c79a5a15242bd7d1", size = 487144, upload-time = "2025-10-17T11:29:05.37Z" }, + { url = "https://files.pythonhosted.org/packages/b0/50/b63fcadf699893269b997f4c2e88400bc68f085c6db698c6e5e69d63b2c1/jiter-0.11.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a6b74fae8e40497653b52ce6ca0f1b13457af769af6fb9c1113efc8b5b4d9be", size = 376215, upload-time = "2025-10-17T11:29:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/39/8c/57a8a89401134167e87e73471b9cca321cf651c1fd78c45f3a0f16932213/jiter-0.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a55a453f8b035eb4f7852a79a065d616b7971a17f5e37a9296b4b38d3b619e4", size = 359163, upload-time = "2025-10-17T11:29:09.047Z" }, + { url = "https://files.pythonhosted.org/packages/4b/96/30b0cdbffbb6f753e25339d3dbbe26890c9ef119928314578201c758aace/jiter-0.11.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2638148099022e6bdb3f42904289cd2e403609356fb06eb36ddec2d50958bc29", size = 385344, upload-time = "2025-10-17T11:29:10.69Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d5/31dae27c1cc9410ad52bb514f11bfa4f286f7d6ef9d287b98b8831e156ec/jiter-0.11.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:252490567a5d990986f83b95a5f1ca1bf205ebd27b3e9e93bb7c2592380e29b9", size = 517972, upload-time = "2025-10-17T11:29:12.174Z" }, + { url = "https://files.pythonhosted.org/packages/61/1e/5905a7a3aceab80de13ab226fd690471a5e1ee7e554dc1015e55f1a6b896/jiter-0.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d431d52b0ca2436eea6195f0f48528202100c7deda354cb7aac0a302167594d5", size = 508408, upload-time = "2025-10-17T11:29:13.597Z" }, + { url = "https://files.pythonhosted.org/packages/91/12/1c49b97aa49077e136e8591cef7162f0d3e2860ae457a2d35868fd1521ef/jiter-0.11.1-cp311-cp311-win32.whl", hash = "sha256:db6f41e40f8bae20c86cb574b48c4fd9f28ee1c71cb044e9ec12e78ab757ba3a", size = 203937, upload-time = "2025-10-17T11:29:14.894Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9d/2255f7c17134ee9892c7e013c32d5bcf4bce64eb115402c9fe5e727a67eb/jiter-0.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0cc407b8e6cdff01b06bb80f61225c8b090c3df108ebade5e0c3c10993735b19", size = 207589, upload-time = "2025-10-17T11:29:16.166Z" }, + { url = "https://files.pythonhosted.org/packages/3c/28/6307fc8f95afef84cae6caf5429fee58ef16a582c2ff4db317ceb3e352fa/jiter-0.11.1-cp311-cp311-win_arm64.whl", hash = "sha256:fe04ea475392a91896d1936367854d346724a1045a247e5d1c196410473b8869", size = 188391, upload-time = "2025-10-17T11:29:17.488Z" }, + { url = "https://files.pythonhosted.org/packages/15/8b/318e8af2c904a9d29af91f78c1e18f0592e189bbdb8a462902d31fe20682/jiter-0.11.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c92148eec91052538ce6823dfca9525f5cfc8b622d7f07e9891a280f61b8c96c", size = 305655, upload-time = "2025-10-17T11:29:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/f7/29/6c7de6b5d6e511d9e736312c0c9bfcee8f9b6bef68182a08b1d78767e627/jiter-0.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ecd4da91b5415f183a6be8f7158d127bdd9e6a3174138293c0d48d6ea2f2009d", size = 315645, upload-time = "2025-10-17T11:29:20.889Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5f/ef9e5675511ee0eb7f98dd8c90509e1f7743dbb7c350071acae87b0145f3/jiter-0.11.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7e3ac25c00b9275684d47aa42febaa90a9958e19fd1726c4ecf755fbe5e553b", size = 348003, upload-time = "2025-10-17T11:29:22.712Z" }, + { url = "https://files.pythonhosted.org/packages/56/1b/abe8c4021010b0a320d3c62682769b700fb66f92c6db02d1a1381b3db025/jiter-0.11.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:57d7305c0a841858f866cd459cd9303f73883fb5e097257f3d4a3920722c69d4", size = 365122, upload-time = "2025-10-17T11:29:24.408Z" }, + { url = "https://files.pythonhosted.org/packages/2a/2d/4a18013939a4f24432f805fbd5a19893e64650b933edb057cd405275a538/jiter-0.11.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e86fa10e117dce22c547f31dd6d2a9a222707d54853d8de4e9a2279d2c97f239", size = 488360, upload-time = "2025-10-17T11:29:25.724Z" }, + { url = "https://files.pythonhosted.org/packages/f0/77/38124f5d02ac4131f0dfbcfd1a19a0fac305fa2c005bc4f9f0736914a1a4/jiter-0.11.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae5ef1d48aec7e01ee8420155d901bb1d192998fa811a65ebb82c043ee186711", size = 376884, upload-time = "2025-10-17T11:29:27.056Z" }, + { url = "https://files.pythonhosted.org/packages/7b/43/59fdc2f6267959b71dd23ce0bd8d4aeaf55566aa435a5d00f53d53c7eb24/jiter-0.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb68e7bf65c990531ad8715e57d50195daf7c8e6f1509e617b4e692af1108939", size = 358827, upload-time = "2025-10-17T11:29:28.698Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d0/b3cc20ff5340775ea3bbaa0d665518eddecd4266ba7244c9cb480c0c82ec/jiter-0.11.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43b30c8154ded5845fa454ef954ee67bfccce629b2dea7d01f795b42bc2bda54", size = 385171, upload-time = "2025-10-17T11:29:30.078Z" }, + { url = "https://files.pythonhosted.org/packages/d2/bc/94dd1f3a61f4dc236f787a097360ec061ceeebebf4ea120b924d91391b10/jiter-0.11.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:586cafbd9dd1f3ce6a22b4a085eaa6be578e47ba9b18e198d4333e598a91db2d", size = 518359, upload-time = "2025-10-17T11:29:31.464Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8c/12ee132bd67e25c75f542c227f5762491b9a316b0dad8e929c95076f773c/jiter-0.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:677cc2517d437a83bb30019fd4cf7cad74b465914c56ecac3440d597ac135250", size = 509205, upload-time = "2025-10-17T11:29:32.895Z" }, + { url = "https://files.pythonhosted.org/packages/39/d5/9de848928ce341d463c7e7273fce90ea6d0ea4343cd761f451860fa16b59/jiter-0.11.1-cp312-cp312-win32.whl", hash = "sha256:fa992af648fcee2b850a3286a35f62bbbaeddbb6dbda19a00d8fbc846a947b6e", size = 205448, upload-time = "2025-10-17T11:29:34.217Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b0/8002d78637e05009f5e3fb5288f9d57d65715c33b5d6aa20fd57670feef5/jiter-0.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:88b5cae9fa51efeb3d4bd4e52bfd4c85ccc9cac44282e2a9640893a042ba4d87", size = 204285, upload-time = "2025-10-17T11:29:35.446Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a2/bb24d5587e4dff17ff796716542f663deee337358006a80c8af43ddc11e5/jiter-0.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:9a6cae1ab335551917f882f2c3c1efe7617b71b4c02381e4382a8fc80a02588c", size = 188712, upload-time = "2025-10-17T11:29:37.027Z" }, + { url = "https://files.pythonhosted.org/packages/7c/4b/e4dd3c76424fad02a601d570f4f2a8438daea47ba081201a721a903d3f4c/jiter-0.11.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:71b6a920a5550f057d49d0e8bcc60945a8da998019e83f01adf110e226267663", size = 305272, upload-time = "2025-10-17T11:29:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/67/83/2cd3ad5364191130f4de80eacc907f693723beaab11a46c7d155b07a092c/jiter-0.11.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b3de72e925388453a5171be83379549300db01284f04d2a6f244d1d8de36f94", size = 314038, upload-time = "2025-10-17T11:29:40.563Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3c/8e67d9ba524e97d2f04c8f406f8769a23205026b13b0938d16646d6e2d3e/jiter-0.11.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc19dd65a2bd3d9c044c5b4ebf657ca1e6003a97c0fc10f555aa4f7fb9821c00", size = 345977, upload-time = "2025-10-17T11:29:42.009Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/489ce64d992c29bccbffabb13961bbb0435e890d7f2d266d1f3df5e917d2/jiter-0.11.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d58faaa936743cd1464540562f60b7ce4fd927e695e8bc31b3da5b914baa9abd", size = 364503, upload-time = "2025-10-17T11:29:43.459Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c0/e321dd83ee231d05c8fe4b1a12caf1f0e8c7a949bf4724d58397104f10f2/jiter-0.11.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:902640c3103625317291cb73773413b4d71847cdf9383ba65528745ff89f1d14", size = 487092, upload-time = "2025-10-17T11:29:44.835Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5e/8f24ec49c8d37bd37f34ec0112e0b1a3b4b5a7b456c8efff1df5e189ad43/jiter-0.11.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30405f726e4c2ed487b176c09f8b877a957f535d60c1bf194abb8dadedb5836f", size = 376328, upload-time = "2025-10-17T11:29:46.175Z" }, + { url = "https://files.pythonhosted.org/packages/7f/70/ded107620e809327cf7050727e17ccfa79d6385a771b7fe38fb31318ef00/jiter-0.11.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3217f61728b0baadd2551844870f65219ac4a1285d5e1a4abddff3d51fdabe96", size = 356632, upload-time = "2025-10-17T11:29:47.454Z" }, + { url = "https://files.pythonhosted.org/packages/19/53/c26f7251613f6a9079275ee43c89b8a973a95ff27532c421abc2a87afb04/jiter-0.11.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1364cc90c03a8196f35f396f84029f12abe925415049204446db86598c8b72c", size = 384358, upload-time = "2025-10-17T11:29:49.377Z" }, + { url = "https://files.pythonhosted.org/packages/84/16/e0f2cc61e9c4d0b62f6c1bd9b9781d878a427656f88293e2a5335fa8ff07/jiter-0.11.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:53a54bf8e873820ab186b2dca9f6c3303f00d65ae5e7b7d6bda1b95aa472d646", size = 517279, upload-time = "2025-10-17T11:29:50.968Z" }, + { url = "https://files.pythonhosted.org/packages/60/5c/4cd095eaee68961bca3081acbe7c89e12ae24a5dae5fd5d2a13e01ed2542/jiter-0.11.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7e29aca023627b0e0c2392d4248f6414d566ff3974fa08ff2ac8dbb96dfee92a", size = 508276, upload-time = "2025-10-17T11:29:52.619Z" }, + { url = "https://files.pythonhosted.org/packages/4f/25/f459240e69b0e09a7706d96ce203ad615ca36b0fe832308d2b7123abf2d0/jiter-0.11.1-cp313-cp313-win32.whl", hash = "sha256:f153e31d8bca11363751e875c0a70b3d25160ecbaee7b51e457f14498fb39d8b", size = 205593, upload-time = "2025-10-17T11:29:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/7c/16/461bafe22bae79bab74e217a09c907481a46d520c36b7b9fe71ee8c9e983/jiter-0.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:f773f84080b667c69c4ea0403fc67bb08b07e2b7ce1ef335dea5868451e60fed", size = 203518, upload-time = "2025-10-17T11:29:55.216Z" }, + { url = "https://files.pythonhosted.org/packages/7b/72/c45de6e320edb4fa165b7b1a414193b3cae302dd82da2169d315dcc78b44/jiter-0.11.1-cp313-cp313-win_arm64.whl", hash = "sha256:635ecd45c04e4c340d2187bcb1cea204c7cc9d32c1364d251564bf42e0e39c2d", size = 188062, upload-time = "2025-10-17T11:29:56.631Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/4a57922437ca8753ef823f434c2dec5028b237d84fa320f06a3ba1aec6e8/jiter-0.11.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d892b184da4d94d94ddb4031296931c74ec8b325513a541ebfd6dfb9ae89904b", size = 313814, upload-time = "2025-10-17T11:29:58.509Z" }, + { url = "https://files.pythonhosted.org/packages/76/50/62a0683dadca25490a4bedc6a88d59de9af2a3406dd5a576009a73a1d392/jiter-0.11.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa22c223a3041dacb2fcd37c70dfd648b44662b4a48e242592f95bda5ab09d58", size = 344987, upload-time = "2025-10-17T11:30:00.208Z" }, + { url = "https://files.pythonhosted.org/packages/da/00/2355dbfcbf6cdeaddfdca18287f0f38ae49446bb6378e4a5971e9356fc8a/jiter-0.11.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330e8e6a11ad4980cd66a0f4a3e0e2e0f646c911ce047014f984841924729789", size = 356399, upload-time = "2025-10-17T11:30:02.084Z" }, + { url = "https://files.pythonhosted.org/packages/c9/07/c2bd748d578fa933d894a55bff33f983bc27f75fc4e491b354bef7b78012/jiter-0.11.1-cp313-cp313t-win_amd64.whl", hash = "sha256:09e2e386ebf298547ca3a3704b729471f7ec666c2906c5c26c1a915ea24741ec", size = 203289, upload-time = "2025-10-17T11:30:03.656Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ee/ace64a853a1acbd318eb0ca167bad1cf5ee037207504b83a868a5849747b/jiter-0.11.1-cp313-cp313t-win_arm64.whl", hash = "sha256:fe4a431c291157e11cee7c34627990ea75e8d153894365a3bc84b7a959d23ca8", size = 188284, upload-time = "2025-10-17T11:30:05.046Z" }, + { url = "https://files.pythonhosted.org/packages/8d/00/d6006d069e7b076e4c66af90656b63da9481954f290d5eca8c715f4bf125/jiter-0.11.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:0fa1f70da7a8a9713ff8e5f75ec3f90c0c870be6d526aa95e7c906f6a1c8c676", size = 304624, upload-time = "2025-10-17T11:30:06.678Z" }, + { url = "https://files.pythonhosted.org/packages/fc/45/4a0e31eb996b9ccfddbae4d3017b46f358a599ccf2e19fbffa5e531bd304/jiter-0.11.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:569ee559e5046a42feb6828c55307cf20fe43308e3ae0d8e9e4f8d8634d99944", size = 315042, upload-time = "2025-10-17T11:30:08.87Z" }, + { url = "https://files.pythonhosted.org/packages/e7/91/22f5746f5159a28c76acdc0778801f3c1181799aab196dbea2d29e064968/jiter-0.11.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f69955fa1d92e81987f092b233f0be49d4c937da107b7f7dcf56306f1d3fcce9", size = 346357, upload-time = "2025-10-17T11:30:10.222Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4f/57620857d4e1dc75c8ff4856c90cb6c135e61bff9b4ebfb5dc86814e82d7/jiter-0.11.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:090f4c9d4a825e0fcbd0a2647c9a88a0f366b75654d982d95a9590745ff0c48d", size = 365057, upload-time = "2025-10-17T11:30:11.585Z" }, + { url = "https://files.pythonhosted.org/packages/ce/34/caf7f9cc8ae0a5bb25a5440cc76c7452d264d1b36701b90fdadd28fe08ec/jiter-0.11.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbf3d8cedf9e9d825233e0dcac28ff15c47b7c5512fdfe2e25fd5bbb6e6b0cee", size = 487086, upload-time = "2025-10-17T11:30:13.052Z" }, + { url = "https://files.pythonhosted.org/packages/50/17/85b5857c329d533d433fedf98804ebec696004a1f88cabad202b2ddc55cf/jiter-0.11.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2aa9b1958f9c30d3d1a558b75f0626733c60eb9b7774a86b34d88060be1e67fe", size = 376083, upload-time = "2025-10-17T11:30:14.416Z" }, + { url = "https://files.pythonhosted.org/packages/85/d3/2d9f973f828226e6faebdef034097a2918077ea776fb4d88489949024787/jiter-0.11.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e42d1ca16590b768c5e7d723055acd2633908baacb3628dd430842e2e035aa90", size = 357825, upload-time = "2025-10-17T11:30:15.765Z" }, + { url = "https://files.pythonhosted.org/packages/f4/55/848d4dabf2c2c236a05468c315c2cb9dc736c5915e65449ccecdba22fb6f/jiter-0.11.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5db4c2486a023820b701a17aec9c5a6173c5ba4393f26662f032f2de9c848b0f", size = 383933, upload-time = "2025-10-17T11:30:17.34Z" }, + { url = "https://files.pythonhosted.org/packages/0b/6c/204c95a4fbb0e26dfa7776c8ef4a878d0c0b215868011cc904bf44f707e2/jiter-0.11.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:4573b78777ccfac954859a6eff45cbd9d281d80c8af049d0f1a3d9fc323d5c3a", size = 517118, upload-time = "2025-10-17T11:30:18.684Z" }, + { url = "https://files.pythonhosted.org/packages/88/25/09956644ea5a2b1e7a2a0f665cb69a973b28f4621fa61fc0c0f06ff40a31/jiter-0.11.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7593ac6f40831d7961cb67633c39b9fef6689a211d7919e958f45710504f52d3", size = 508194, upload-time = "2025-10-17T11:30:20.719Z" }, + { url = "https://files.pythonhosted.org/packages/09/49/4d1657355d7f5c9e783083a03a3f07d5858efa6916a7d9634d07db1c23bd/jiter-0.11.1-cp314-cp314-win32.whl", hash = "sha256:87202ec6ff9626ff5f9351507def98fcf0df60e9a146308e8ab221432228f4ea", size = 203961, upload-time = "2025-10-17T11:30:22.073Z" }, + { url = "https://files.pythonhosted.org/packages/76/bd/f063bd5cc2712e7ca3cf6beda50894418fc0cfeb3f6ff45a12d87af25996/jiter-0.11.1-cp314-cp314-win_amd64.whl", hash = "sha256:a5dd268f6531a182c89d0dd9a3f8848e86e92dfff4201b77a18e6b98aa59798c", size = 202804, upload-time = "2025-10-17T11:30:23.452Z" }, + { url = "https://files.pythonhosted.org/packages/52/ca/4d84193dfafef1020bf0bedd5e1a8d0e89cb67c54b8519040effc694964b/jiter-0.11.1-cp314-cp314-win_arm64.whl", hash = "sha256:5d761f863f912a44748a21b5c4979c04252588ded8d1d2760976d2e42cd8d991", size = 188001, upload-time = "2025-10-17T11:30:24.915Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fa/3b05e5c9d32efc770a8510eeb0b071c42ae93a5b576fd91cee9af91689a1/jiter-0.11.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2cc5a3965285ddc33e0cab933e96b640bc9ba5940cea27ebbbf6695e72d6511c", size = 312561, upload-time = "2025-10-17T11:30:26.742Z" }, + { url = "https://files.pythonhosted.org/packages/50/d3/335822eb216154ddb79a130cbdce88fdf5c3e2b43dc5dba1fd95c485aaf5/jiter-0.11.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b572b3636a784c2768b2342f36a23078c8d3aa6d8a30745398b1bab58a6f1a8", size = 344551, upload-time = "2025-10-17T11:30:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/31/6d/a0bed13676b1398f9b3ba61f32569f20a3ff270291161100956a577b2dd3/jiter-0.11.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad93e3d67a981f96596d65d2298fe8d1aa649deb5374a2fb6a434410ee11915e", size = 363051, upload-time = "2025-10-17T11:30:30.009Z" }, + { url = "https://files.pythonhosted.org/packages/a4/03/313eda04aa08545a5a04ed5876e52f49ab76a4d98e54578896ca3e16313e/jiter-0.11.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a83097ce379e202dcc3fe3fc71a16d523d1ee9192c8e4e854158f96b3efe3f2f", size = 485897, upload-time = "2025-10-17T11:30:31.429Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/a1011b9d325e40b53b1b96a17c010b8646013417f3902f97a86325b19299/jiter-0.11.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7042c51e7fbeca65631eb0c332f90c0c082eab04334e7ccc28a8588e8e2804d9", size = 375224, upload-time = "2025-10-17T11:30:33.18Z" }, + { url = "https://files.pythonhosted.org/packages/92/da/1b45026b19dd39b419e917165ff0ea629dbb95f374a3a13d2df95e40a6ac/jiter-0.11.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a68d679c0e47649a61df591660507608adc2652442de7ec8276538ac46abe08", size = 356606, upload-time = "2025-10-17T11:30:34.572Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0c/9acb0e54d6a8ba59ce923a180ebe824b4e00e80e56cefde86cc8e0a948be/jiter-0.11.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1b0da75dbf4b6ec0b3c9e604d1ee8beaf15bc046fff7180f7d89e3cdbd3bb51", size = 384003, upload-time = "2025-10-17T11:30:35.987Z" }, + { url = "https://files.pythonhosted.org/packages/3f/2b/e5a5fe09d6da2145e4eed651e2ce37f3c0cf8016e48b1d302e21fb1628b7/jiter-0.11.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:69dd514bf0fa31c62147d6002e5ca2b3e7ef5894f5ac6f0a19752385f4e89437", size = 516946, upload-time = "2025-10-17T11:30:37.425Z" }, + { url = "https://files.pythonhosted.org/packages/5f/fe/db936e16e0228d48eb81f9934e8327e9fde5185e84f02174fcd22a01be87/jiter-0.11.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:bb31ac0b339efa24c0ca606febd8b77ef11c58d09af1b5f2be4c99e907b11111", size = 507614, upload-time = "2025-10-17T11:30:38.977Z" }, + { url = "https://files.pythonhosted.org/packages/86/db/c4438e8febfb303486d13c6b72f5eb71cf851e300a0c1f0b4140018dd31f/jiter-0.11.1-cp314-cp314t-win32.whl", hash = "sha256:b2ce0d6156a1d3ad41da3eec63b17e03e296b78b0e0da660876fccfada86d2f7", size = 204043, upload-time = "2025-10-17T11:30:40.308Z" }, + { url = "https://files.pythonhosted.org/packages/36/59/81badb169212f30f47f817dfaabf965bc9b8204fed906fab58104ee541f9/jiter-0.11.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f4db07d127b54c4a2d43b4cf05ff0193e4f73e0dd90c74037e16df0b29f666e1", size = 204046, upload-time = "2025-10-17T11:30:41.692Z" }, + { url = "https://files.pythonhosted.org/packages/dd/01/43f7b4eb61db3e565574c4c5714685d042fb652f9eef7e5a3de6aafa943a/jiter-0.11.1-cp314-cp314t-win_arm64.whl", hash = "sha256:28e4fdf2d7ebfc935523e50d1efa3970043cfaa161674fe66f9642409d001dfe", size = 188069, upload-time = "2025-10-17T11:30:43.23Z" }, + { url = "https://files.pythonhosted.org/packages/6d/0c/c9f2e6bf00e873741ca9a7b44eb386d4a211b5f8fdebf3c711e1b4f5a9a2/jiter-0.11.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:baa99c8db49467527658bb479857344daf0a14dff909b7f6714579ac439d1253", size = 311957, upload-time = "2025-10-17T11:30:44.539Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/106752513486e351166edee3e7316c0f14d5278cf973d4ee265c65d8916e/jiter-0.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:860fe55fa3b01ad0edf2adde1098247ff5c303d0121f9ce028c03d4f88c69502", size = 305180, upload-time = "2025-10-17T11:30:45.942Z" }, + { url = "https://files.pythonhosted.org/packages/e6/26/0ca909a5e1af629e4395edd9e2d8e05bffe2a245694827ef78f6f5cf6c79/jiter-0.11.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:173dd349d99b6feaf5a25a6fbcaf3489a6f947708d808240587a23df711c67db", size = 351025, upload-time = "2025-10-17T11:30:47.443Z" }, + { url = "https://files.pythonhosted.org/packages/02/12/90de4d022f41ba7ee691c2d753e4dcea303ae42c94cae31eb6106f3b05bc/jiter-0.11.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:14ac1dca837514cc946a6ac2c4995d9695303ecc754af70a3163d057d1a444ab", size = 365626, upload-time = "2025-10-17T11:30:49.023Z" }, + { url = "https://files.pythonhosted.org/packages/3f/a7/e6cf83a41e20fdda9e6e3a355387f32571910e832a86e0cde852ec97f725/jiter-0.11.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69af47de5f93a231d5b85f7372d3284a5be8edb4cc758f006ec5a1406965ac5e", size = 487278, upload-time = "2025-10-17T11:30:50.848Z" }, + { url = "https://files.pythonhosted.org/packages/5d/00/25390dfd5aa664d4e0cfad76bd4f1f812e875256781339ec72bbef9c4b8e/jiter-0.11.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:685f8b3abd3bbd3e06e4dfe2429ff87fd5d7a782701151af99b1fcbd80e31b2b", size = 377638, upload-time = "2025-10-17T11:30:52.305Z" }, + { url = "https://files.pythonhosted.org/packages/8f/2c/0921a1a5358dfb8de16e9094fff26631c38a2a4aadb822f6db8f90e2713f/jiter-0.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d04afa2d4e5526e54ae8a58feea953b1844bf6e3526bc589f9de68e86d0ea01", size = 361719, upload-time = "2025-10-17T11:30:54.324Z" }, + { url = "https://files.pythonhosted.org/packages/6a/4a/b4400c5fb809ea65be5d89aee35fcc8b82bb8e3cee1a5e8aff495fa535ae/jiter-0.11.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e92b927259035b50d8e11a8fdfe0ebd014d883e4552d37881643fa289a4bcf1", size = 387173, upload-time = "2025-10-17T11:30:56.077Z" }, + { url = "https://files.pythonhosted.org/packages/f2/bc/03f21d262265734e2733d5edd7f2f7b7ccb1640e8fa9f9d53b8b13355143/jiter-0.11.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e7bd8be4fad8d4c5558b7801770cd2da6c072919c6f247cc5336edb143f25304", size = 519388, upload-time = "2025-10-17T11:30:57.472Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c8/3b49c635cef4ea557abb79984be28cb42a487f9a8a38cfc182bf480da8bc/jiter-0.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:121381a77a3c85987f3eba0d30ceaca9116f7463bedeec2fa79b2e7286b89b60", size = 509866, upload-time = "2025-10-17T11:30:58.955Z" }, + { url = "https://files.pythonhosted.org/packages/1f/bc/357a30d83dfffaf2e1bdfb0e366e5e8b7e91b4db4aa6c119c1f98ec987ba/jiter-0.11.1-cp39-cp39-win32.whl", hash = "sha256:160225407f6dfabdf9be1b44e22f06bc293a78a28ffa4347054698bd712dad06", size = 205505, upload-time = "2025-10-17T11:31:00.885Z" }, + { url = "https://files.pythonhosted.org/packages/49/cd/e37b51fdd66d76ee3047f50df91e3e8cf3a5574a27c0c484dc5f4fdfb7c4/jiter-0.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:028e0d59bcdfa1079f8df886cdaefc6f515c27a5288dec956999260c7e4a7cfd", size = 207552, upload-time = "2025-10-17T11:31:02.245Z" }, + { url = "https://files.pythonhosted.org/packages/9d/51/bd41562dd284e2a18b6dc0a99d195fd4a3560d52ab192c42e56fe0316643/jiter-0.11.1-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:e642b5270e61dd02265866398707f90e365b5db2eb65a4f30c789d826682e1f6", size = 306871, upload-time = "2025-10-17T11:31:03.616Z" }, + { url = "https://files.pythonhosted.org/packages/ba/cb/64e7f21dd357e8cd6b3c919c26fac7fc198385bbd1d85bb3b5355600d787/jiter-0.11.1-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:464ba6d000585e4e2fd1e891f31f1231f497273414f5019e27c00a4b8f7a24ad", size = 301454, upload-time = "2025-10-17T11:31:05.338Z" }, + { url = "https://files.pythonhosted.org/packages/55/b0/54bdc00da4ef39801b1419a01035bd8857983de984fd3776b0be6b94add7/jiter-0.11.1-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:055568693ab35e0bf3a171b03bb40b2dcb10352359e0ab9b5ed0da2bf1eb6f6f", size = 336801, upload-time = "2025-10-17T11:31:06.893Z" }, + { url = "https://files.pythonhosted.org/packages/de/8f/87176ed071d42e9db415ed8be787ef4ef31a4fa27f52e6a4fbf34387bd28/jiter-0.11.1-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0c69ea798d08a915ba4478113efa9e694971e410056392f4526d796f136d3fa", size = 343452, upload-time = "2025-10-17T11:31:08.259Z" }, + { url = "https://files.pythonhosted.org/packages/a6/bc/950dd7f170c6394b6fdd73f989d9e729bd98907bcc4430ef080a72d06b77/jiter-0.11.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:0d4d6993edc83cf75e8c6828a8d6ce40a09ee87e38c7bfba6924f39e1337e21d", size = 302626, upload-time = "2025-10-17T11:31:09.645Z" }, + { url = "https://files.pythonhosted.org/packages/3a/65/43d7971ca82ee100b7b9b520573eeef7eabc0a45d490168ebb9a9b5bb8b2/jiter-0.11.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f78d151c83a87a6cf5461d5ee55bc730dd9ae227377ac6f115b922989b95f838", size = 297034, upload-time = "2025-10-17T11:31:10.975Z" }, + { url = "https://files.pythonhosted.org/packages/19/4c/000e1e0c0c67e96557a279f8969487ea2732d6c7311698819f977abae837/jiter-0.11.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9022974781155cd5521d5cb10997a03ee5e31e8454c9d999dcdccd253f2353f", size = 337328, upload-time = "2025-10-17T11:31:12.399Z" }, + { url = "https://files.pythonhosted.org/packages/d9/71/71408b02c6133153336d29fa3ba53000f1e1a3f78bb2fc2d1a1865d2e743/jiter-0.11.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18c77aaa9117510d5bdc6a946baf21b1f0cfa58ef04d31c8d016f206f2118960", size = 343697, upload-time = "2025-10-17T11:31:13.773Z" }, +] + +[[package]] +name = "lance" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/2f/19bab0b1d8d5a4917581db6a7d2e132cca11323192f476cd27ed996192bc/lance-1.2.1.tar.gz", hash = "sha256:2849817a5eb7f5e610a4cf766bc3bd096d7f9e230bcc5a60c8dd3986510705e4", size = 16240, upload-time = "2020-11-04T17:18:10.806Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/1e/d47c5ff992a79d3987b4ac0a4d8a905457099f7def942dc6e1bd9c2216e2/lance-1.2.1-py3-none-any.whl", hash = "sha256:f9ac09055c7935c8d351bccd5f0deda9ecfce6b31fb1acdf650ef97140114137", size = 22548, upload-time = "2020-11-04T17:18:09.354Z" }, +] + [[package]] name = "lance-graph" source = { editable = "." } dependencies = [ + { name = "fastapi" }, { name = "pyarrow" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "uvicorn" }, ] [package.optional-dependencies] @@ -50,6 +306,12 @@ dev = [ { name = "pyright" }, { name = "ruff" }, ] +lance-storage = [ + { name = "lance" }, +] +llm = [ + { name = "openai" }, +] tests = [ { name = "pandas" }, { name = "pyarrow" }, @@ -58,14 +320,20 @@ tests = [ [package.metadata] requires-dist = [ + { name = "fastapi", specifier = ">=0.104.0" }, + { name = "lance", marker = "extra == 'lance-storage'", specifier = ">=0.17.0" }, + { name = "openai", marker = "extra == 'llm'", specifier = ">=1.52.0" }, { name = "pandas", marker = "extra == 'tests'" }, { name = "pyarrow", specifier = ">=14" }, { name = "pyarrow", marker = "extra == 'tests'", specifier = ">=14" }, + { name = "pydantic", specifier = ">=2.0.0" }, { name = "pyright", marker = "extra == 'dev'" }, { name = "pytest", marker = "extra == 'tests'" }, + { name = "pyyaml", specifier = ">=6.0" }, { name = "ruff", marker = "extra == 'dev'" }, + { name = "uvicorn", specifier = ">=0.24.0" }, ] -provides-extras = ["tests", "dev"] +provides-extras = ["tests", "dev", "llm", "lance-storage"] [[package]] name = "nodeenv" @@ -281,6 +549,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/11/0cc63f9f321ccf63886ac203336777140011fb669e739da36d8db3c53b98/numpy-2.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2e267c7da5bf7309670523896df97f93f6e469fb931161f483cd6882b3b1a5dc", size = 12971844, upload-time = "2025-09-09T15:58:57.359Z" }, ] +[[package]] +name = "openai" +version = "2.6.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/ee/c7/e42bcd89dfd47fec8a30b9e20f93e512efdbfbb3391b05bbb79a2fb295fa/openai-2.6.0.tar.gz", hash = "sha256:f119faf7fc07d7e558c1e7c32c873e241439b01bd7480418234291ee8c8f4b9d", size = 592904, upload-time = "2025-10-20T17:17:24.588Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/0a/58e9dcd34abe273eaeac3807a8483073767b5609d01bb78ea2f048e515a0/openai-2.6.0-py3-none-any.whl", hash = "sha256:f33fa12070fe347b5787a7861c8dd397786a4a17e1c3186e239338dac7e2e743", size = 1005403, upload-time = "2025-10-20T17:17:22.091Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -419,6 +706,148 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/15/6b30e77872012bbfe8265d42a01d5b3c17ef0ac0f2fae531ad91b6a6c02e/pyarrow-21.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdc4c17afda4dab2a9c0b79148a43a7f4e1094916b3e18d8975bfd6d6d52241f", size = 26227521, upload-time = "2025-07-18T00:57:29.119Z" }, ] +[[package]] +name = "pydantic" +version = "2.12.3" +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/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383, upload-time = "2025-10-17T15:04:21.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431, upload-time = "2025-10-17T15:04:19.346Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/3d/9b8ca77b0f76fcdbf8bc6b72474e264283f461284ca84ac3fde570c6c49a/pydantic_core-2.41.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2442d9a4d38f3411f22eb9dd0912b7cbf4b7d5b6c92c4173b75d3e1ccd84e36e", size = 2111197, upload-time = "2025-10-14T10:19:43.303Z" }, + { url = "https://files.pythonhosted.org/packages/59/92/b7b0fe6ed4781642232755cb7e56a86e2041e1292f16d9ae410a0ccee5ac/pydantic_core-2.41.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:30a9876226dda131a741afeab2702e2d127209bde3c65a2b8133f428bc5d006b", size = 1917909, upload-time = "2025-10-14T10:19:45.194Z" }, + { url = "https://files.pythonhosted.org/packages/52/8c/3eb872009274ffa4fb6a9585114e161aa1a0915af2896e2d441642929fe4/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d55bbac04711e2980645af68b97d445cdbcce70e5216de444a6c4b6943ebcccd", size = 1969905, upload-time = "2025-10-14T10:19:46.567Z" }, + { url = "https://files.pythonhosted.org/packages/f4/21/35adf4a753bcfaea22d925214a0c5b880792e3244731b3f3e6fec0d124f7/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1d778fb7849a42d0ee5927ab0f7453bf9f85eef8887a546ec87db5ddb178945", size = 2051938, upload-time = "2025-10-14T10:19:48.237Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d0/cdf7d126825e36d6e3f1eccf257da8954452934ede275a8f390eac775e89/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b65077a4693a98b90ec5ad8f203ad65802a1b9b6d4a7e48066925a7e1606706", size = 2250710, upload-time = "2025-10-14T10:19:49.619Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1c/af1e6fd5ea596327308f9c8d1654e1285cc3d8de0d584a3c9d7705bf8a7c/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62637c769dee16eddb7686bf421be48dfc2fae93832c25e25bc7242e698361ba", size = 2367445, upload-time = "2025-10-14T10:19:51.269Z" }, + { url = "https://files.pythonhosted.org/packages/d3/81/8cece29a6ef1b3a92f956ea6da6250d5b2d2e7e4d513dd3b4f0c7a83dfea/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfe3aa529c8f501babf6e502936b9e8d4698502b2cfab41e17a028d91b1ac7b", size = 2072875, upload-time = "2025-10-14T10:19:52.671Z" }, + { url = "https://files.pythonhosted.org/packages/e3/37/a6a579f5fc2cd4d5521284a0ab6a426cc6463a7b3897aeb95b12f1ba607b/pydantic_core-2.41.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca2322da745bf2eeb581fc9ea3bbb31147702163ccbcbf12a3bb630e4bf05e1d", size = 2191329, upload-time = "2025-10-14T10:19:54.214Z" }, + { url = "https://files.pythonhosted.org/packages/ae/03/505020dc5c54ec75ecba9f41119fd1e48f9e41e4629942494c4a8734ded1/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e8cd3577c796be7231dcf80badcf2e0835a46665eaafd8ace124d886bab4d700", size = 2151658, upload-time = "2025-10-14T10:19:55.843Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5d/2c0d09fb53aa03bbd2a214d89ebfa6304be7df9ed86ee3dc7770257f41ee/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1cae8851e174c83633f0833e90636832857297900133705ee158cf79d40f03e6", size = 2316777, upload-time = "2025-10-14T10:19:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/ea/4b/c2c9c8f5e1f9c864b57d08539d9d3db160e00491c9f5ee90e1bfd905e644/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a26d950449aae348afe1ac8be5525a00ae4235309b729ad4d3399623125b43c9", size = 2320705, upload-time = "2025-10-14T10:19:59.016Z" }, + { url = "https://files.pythonhosted.org/packages/28/c3/a74c1c37f49c0a02c89c7340fafc0ba816b29bd495d1a31ce1bdeacc6085/pydantic_core-2.41.4-cp310-cp310-win32.whl", hash = "sha256:0cf2a1f599efe57fa0051312774280ee0f650e11152325e41dfd3018ef2c1b57", size = 1975464, upload-time = "2025-10-14T10:20:00.581Z" }, + { url = "https://files.pythonhosted.org/packages/d6/23/5dd5c1324ba80303368f7569e2e2e1a721c7d9eb16acb7eb7b7f85cb1be2/pydantic_core-2.41.4-cp310-cp310-win_amd64.whl", hash = "sha256:a8c2e340d7e454dc3340d3d2e8f23558ebe78c98aa8f68851b04dcb7bc37abdc", size = 2024497, upload-time = "2025-10-14T10:20:03.018Z" }, + { url = "https://files.pythonhosted.org/packages/62/4c/f6cbfa1e8efacd00b846764e8484fe173d25b8dab881e277a619177f3384/pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:28ff11666443a1a8cf2a044d6a545ebffa8382b5f7973f22c36109205e65dc80", size = 2109062, upload-time = "2025-10-14T10:20:04.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/f8/40b72d3868896bfcd410e1bd7e516e762d326201c48e5b4a06446f6cf9e8/pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61760c3925d4633290292bad462e0f737b840508b4f722247d8729684f6539ae", size = 1916301, upload-time = "2025-10-14T10:20:06.857Z" }, + { url = "https://files.pythonhosted.org/packages/94/4d/d203dce8bee7faeca791671c88519969d98d3b4e8f225da5b96dad226fc8/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eae547b7315d055b0de2ec3965643b0ab82ad0106a7ffd29615ee9f266a02827", size = 1968728, upload-time = "2025-10-14T10:20:08.353Z" }, + { url = "https://files.pythonhosted.org/packages/65/f5/6a66187775df87c24d526985b3a5d78d861580ca466fbd9d4d0e792fcf6c/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef9ee5471edd58d1fcce1c80ffc8783a650e3e3a193fe90d52e43bb4d87bff1f", size = 2050238, upload-time = "2025-10-14T10:20:09.766Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b9/78336345de97298cf53236b2f271912ce11f32c1e59de25a374ce12f9cce/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15dd504af121caaf2c95cb90c0ebf71603c53de98305621b94da0f967e572def", size = 2249424, upload-time = "2025-10-14T10:20:11.732Z" }, + { url = "https://files.pythonhosted.org/packages/99/bb/a4584888b70ee594c3d374a71af5075a68654d6c780369df269118af7402/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a926768ea49a8af4d36abd6a8968b8790f7f76dd7cbd5a4c180db2b4ac9a3a2", size = 2366047, upload-time = "2025-10-14T10:20:13.647Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8d/17fc5de9d6418e4d2ae8c675f905cdafdc59d3bf3bf9c946b7ab796a992a/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916b9b7d134bff5440098a4deb80e4cb623e68974a87883299de9124126c2a8", size = 2071163, upload-time = "2025-10-14T10:20:15.307Z" }, + { url = "https://files.pythonhosted.org/packages/54/e7/03d2c5c0b8ed37a4617430db68ec5e7dbba66358b629cd69e11b4d564367/pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cf90535979089df02e6f17ffd076f07237efa55b7343d98760bde8743c4b265", size = 2190585, upload-time = "2025-10-14T10:20:17.3Z" }, + { url = "https://files.pythonhosted.org/packages/be/fc/15d1c9fe5ad9266a5897d9b932b7f53d7e5cfc800573917a2c5d6eea56ec/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7533c76fa647fade2d7ec75ac5cc079ab3f34879626dae5689b27790a6cf5a5c", size = 2150109, upload-time = "2025-10-14T10:20:19.143Z" }, + { url = "https://files.pythonhosted.org/packages/26/ef/e735dd008808226c83ba56972566138665b71477ad580fa5a21f0851df48/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:37e516bca9264cbf29612539801ca3cd5d1be465f940417b002905e6ed79d38a", size = 2315078, upload-time = "2025-10-14T10:20:20.742Z" }, + { url = "https://files.pythonhosted.org/packages/90/00/806efdcf35ff2ac0f938362350cd9827b8afb116cc814b6b75cf23738c7c/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c19cb355224037c83642429b8ce261ae108e1c5fbf5c028bac63c77b0f8646e", size = 2318737, upload-time = "2025-10-14T10:20:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/41/7e/6ac90673fe6cb36621a2283552897838c020db343fa86e513d3f563b196f/pydantic_core-2.41.4-cp311-cp311-win32.whl", hash = "sha256:09c2a60e55b357284b5f31f5ab275ba9f7f70b7525e18a132ec1f9160b4f1f03", size = 1974160, upload-time = "2025-10-14T10:20:23.817Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9d/7c5e24ee585c1f8b6356e1d11d40ab807ffde44d2db3b7dfd6d20b09720e/pydantic_core-2.41.4-cp311-cp311-win_amd64.whl", hash = "sha256:711156b6afb5cb1cb7c14a2cc2c4a8b4c717b69046f13c6b332d8a0a8f41ca3e", size = 2021883, upload-time = "2025-10-14T10:20:25.48Z" }, + { url = "https://files.pythonhosted.org/packages/33/90/5c172357460fc28b2871eb4a0fb3843b136b429c6fa827e4b588877bf115/pydantic_core-2.41.4-cp311-cp311-win_arm64.whl", hash = "sha256:6cb9cf7e761f4f8a8589a45e49ed3c0d92d1d696a45a6feaee8c904b26efc2db", size = 1968026, upload-time = "2025-10-14T10:20:27.039Z" }, + { url = "https://files.pythonhosted.org/packages/e9/81/d3b3e95929c4369d30b2a66a91db63c8ed0a98381ae55a45da2cd1cc1288/pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887", size = 2099043, upload-time = "2025-10-14T10:20:28.561Z" }, + { url = "https://files.pythonhosted.org/packages/58/da/46fdac49e6717e3a94fc9201403e08d9d61aa7a770fab6190b8740749047/pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2", size = 1910699, upload-time = "2025-10-14T10:20:30.217Z" }, + { url = "https://files.pythonhosted.org/packages/1e/63/4d948f1b9dd8e991a5a98b77dd66c74641f5f2e5225fee37994b2e07d391/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999", size = 1952121, upload-time = "2025-10-14T10:20:32.246Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a7/e5fc60a6f781fc634ecaa9ecc3c20171d238794cef69ae0af79ac11b89d7/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4", size = 2041590, upload-time = "2025-10-14T10:20:34.332Z" }, + { url = "https://files.pythonhosted.org/packages/70/69/dce747b1d21d59e85af433428978a1893c6f8a7068fa2bb4a927fba7a5ff/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f", size = 2219869, upload-time = "2025-10-14T10:20:35.965Z" }, + { url = "https://files.pythonhosted.org/packages/83/6a/c070e30e295403bf29c4df1cb781317b6a9bac7cd07b8d3acc94d501a63c/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b", size = 2345169, upload-time = "2025-10-14T10:20:37.627Z" }, + { url = "https://files.pythonhosted.org/packages/f0/83/06d001f8043c336baea7fd202a9ac7ad71f87e1c55d8112c50b745c40324/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47", size = 2070165, upload-time = "2025-10-14T10:20:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/14/0a/e567c2883588dd12bcbc110232d892cf385356f7c8a9910311ac997ab715/pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970", size = 2189067, upload-time = "2025-10-14T10:20:41.015Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1d/3d9fca34273ba03c9b1c5289f7618bc4bd09c3ad2289b5420481aa051a99/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed", size = 2132997, upload-time = "2025-10-14T10:20:43.106Z" }, + { url = "https://files.pythonhosted.org/packages/52/70/d702ef7a6cd41a8afc61f3554922b3ed8d19dd54c3bd4bdbfe332e610827/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8", size = 2307187, upload-time = "2025-10-14T10:20:44.849Z" }, + { url = "https://files.pythonhosted.org/packages/68/4c/c06be6e27545d08b802127914156f38d10ca287a9e8489342793de8aae3c/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431", size = 2305204, upload-time = "2025-10-14T10:20:46.781Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e5/35ae4919bcd9f18603419e23c5eaf32750224a89d41a8df1a3704b69f77e/pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd", size = 1972536, upload-time = "2025-10-14T10:20:48.39Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/49c5bb6d2a49eb2ee3647a93e3dae7080c6409a8a7558b075027644e879c/pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff", size = 2031132, upload-time = "2025-10-14T10:20:50.421Z" }, + { url = "https://files.pythonhosted.org/packages/06/23/936343dbcba6eec93f73e95eb346810fc732f71ba27967b287b66f7b7097/pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8", size = 1969483, upload-time = "2025-10-14T10:20:52.35Z" }, + { url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688, upload-time = "2025-10-14T10:20:54.448Z" }, + { url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807, upload-time = "2025-10-14T10:20:56.115Z" }, + { url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669, upload-time = "2025-10-14T10:20:57.874Z" }, + { url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629, upload-time = "2025-10-14T10:21:00.006Z" }, + { url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049, upload-time = "2025-10-14T10:21:01.801Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409, upload-time = "2025-10-14T10:21:03.556Z" }, + { url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635, upload-time = "2025-10-14T10:21:05.385Z" }, + { url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284, upload-time = "2025-10-14T10:21:07.122Z" }, + { url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566, upload-time = "2025-10-14T10:21:08.981Z" }, + { url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809, upload-time = "2025-10-14T10:21:10.805Z" }, + { url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119, upload-time = "2025-10-14T10:21:12.583Z" }, + { url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398, upload-time = "2025-10-14T10:21:14.584Z" }, + { url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735, upload-time = "2025-10-14T10:21:16.432Z" }, + { url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209, upload-time = "2025-10-14T10:21:18.213Z" }, + { url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324, upload-time = "2025-10-14T10:21:20.363Z" }, + { url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515, upload-time = "2025-10-14T10:21:22.339Z" }, + { url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819, upload-time = "2025-10-14T10:21:26.683Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866, upload-time = "2025-10-14T10:21:28.951Z" }, + { url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034, upload-time = "2025-10-14T10:21:30.869Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022, upload-time = "2025-10-14T10:21:32.809Z" }, + { url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495, upload-time = "2025-10-14T10:21:34.812Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131, upload-time = "2025-10-14T10:21:36.924Z" }, + { url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236, upload-time = "2025-10-14T10:21:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573, upload-time = "2025-10-14T10:21:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467, upload-time = "2025-10-14T10:21:44.018Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754, upload-time = "2025-10-14T10:21:46.466Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754, upload-time = "2025-10-14T10:21:48.486Z" }, + { url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115, upload-time = "2025-10-14T10:21:50.63Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400, upload-time = "2025-10-14T10:21:52.959Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070, upload-time = "2025-10-14T10:21:55.419Z" }, + { url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277, upload-time = "2025-10-14T10:21:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608, upload-time = "2025-10-14T10:21:59.557Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614, upload-time = "2025-10-14T10:22:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904, upload-time = "2025-10-14T10:22:04.062Z" }, + { url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538, upload-time = "2025-10-14T10:22:06.39Z" }, + { url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542, upload-time = "2025-10-14T10:22:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" }, + { url = "https://files.pythonhosted.org/packages/2c/36/f86d582be5fb47d4014506cd9ddd10a3979b6d0f2d237aa6ad3e7033b3ea/pydantic_core-2.41.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:646e76293345954acea6966149683047b7b2ace793011922208c8e9da12b0062", size = 2112444, upload-time = "2025-10-14T10:22:16.165Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e5/63c521dc2dd106ba6b5941c080617ea9db252f8a7d5625231e9d761bc28c/pydantic_core-2.41.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cc8e85a63085a137d286e2791037f5fdfff0aabb8b899483ca9c496dd5797338", size = 1938218, upload-time = "2025-10-14T10:22:19.443Z" }, + { url = "https://files.pythonhosted.org/packages/30/56/c84b638a3e6e9f5a612b9f5abdad73182520423de43669d639ed4f14b011/pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:692c622c8f859a17c156492783902d8370ac7e121a611bd6fe92cc71acf9ee8d", size = 1971449, upload-time = "2025-10-14T10:22:21.567Z" }, + { url = "https://files.pythonhosted.org/packages/99/c6/e974aade34fc7a0248fdfd0a373d62693502a407c596ab3470165e38183c/pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d1e2906efb1031a532600679b424ef1d95d9f9fb507f813951f23320903adbd7", size = 2054023, upload-time = "2025-10-14T10:22:24.229Z" }, + { url = "https://files.pythonhosted.org/packages/4f/91/2507dda801f50980a38d1353c313e8f51349a42b008e63a4e45bf4620562/pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e04e2f7f8916ad3ddd417a7abdd295276a0bf216993d9318a5d61cc058209166", size = 2251614, upload-time = "2025-10-14T10:22:26.498Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ad/05d886bc96938f4d31bed24e8d3fc3496d9aea7e77bcff6e4b93127c6de7/pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df649916b81822543d1c8e0e1d079235f68acdc7d270c911e8425045a8cfc57e", size = 2378807, upload-time = "2025-10-14T10:22:28.733Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0a/d26e1bb9a80b9fc12cc30d9288193fbc9e60a799e55843804ee37bd38a9c/pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66c529f862fdba70558061bb936fe00ddbaaa0c647fd26e4a4356ef1d6561891", size = 2076891, upload-time = "2025-10-14T10:22:30.853Z" }, + { url = "https://files.pythonhosted.org/packages/d9/66/af014e3a294d9933ebfecf11a5d858709014bd2315fa9616195374dd82f0/pydantic_core-2.41.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3b4c5a1fd3a311563ed866c2c9b62da06cb6398bee186484ce95c820db71cb", size = 2192179, upload-time = "2025-10-14T10:22:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3e/79783f97024037d0ea6e1b3ebcd761463a925199e04ce2625727e9f27d06/pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6e0fc40d84448f941df9b3334c4b78fe42f36e3bf631ad54c3047a0cdddc2514", size = 2153067, upload-time = "2025-10-14T10:22:35.792Z" }, + { url = "https://files.pythonhosted.org/packages/b3/97/ea83b0f87d9e742405fb687d5682e7a26334eef2c82a2de06bfbdc305fab/pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:44e7625332683b6c1c8b980461475cde9595eff94447500e80716db89b0da005", size = 2319048, upload-time = "2025-10-14T10:22:38.144Z" }, + { url = "https://files.pythonhosted.org/packages/64/4a/36d8c966a0b086362ac10a7ee75978ed15c5f2dfdfc02a1578d19d3802fb/pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:170ee6835f6c71081d031ef1c3b4dc4a12b9efa6a9540f93f95b82f3c7571ae8", size = 2321830, upload-time = "2025-10-14T10:22:40.337Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6e/d80cc4909dde5f6842861288aa1a7181e7afbfc50940c862ed2848df15bd/pydantic_core-2.41.4-cp39-cp39-win32.whl", hash = "sha256:3adf61415efa6ce977041ba9745183c0e1f637ca849773afa93833e04b163feb", size = 1976706, upload-time = "2025-10-14T10:22:42.61Z" }, + { url = "https://files.pythonhosted.org/packages/29/ee/5bda8d960d4a8b24a7eeb8a856efa9c865a7a6cab714ed387b29507dc278/pydantic_core-2.41.4-cp39-cp39-win_amd64.whl", hash = "sha256:a238dd3feee263eeaeb7dc44aea4ba1364682c4f9f9467e6af5596ba322c2332", size = 2027640, upload-time = "2025-10-14T10:22:44.907Z" }, + { url = "https://files.pythonhosted.org/packages/b0/12/5ba58daa7f453454464f92b3ca7b9d7c657d8641c48e370c3ebc9a82dd78/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a1b2cfec3879afb742a7b0bcfa53e4f22ba96571c9e54d6a3afe1052d17d843b", size = 2122139, upload-time = "2025-10-14T10:22:47.288Z" }, + { url = "https://files.pythonhosted.org/packages/21/fb/6860126a77725c3108baecd10fd3d75fec25191d6381b6eb2ac660228eac/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:d175600d975b7c244af6eb9c9041f10059f20b8bbffec9e33fdd5ee3f67cdc42", size = 1936674, upload-time = "2025-10-14T10:22:49.555Z" }, + { url = "https://files.pythonhosted.org/packages/de/be/57dcaa3ed595d81f8757e2b44a38240ac5d37628bce25fb20d02c7018776/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f184d657fa4947ae5ec9c47bd7e917730fa1cbb78195037e32dcbab50aca5ee", size = 1956398, upload-time = "2025-10-14T10:22:52.19Z" }, + { url = "https://files.pythonhosted.org/packages/2f/1d/679a344fadb9695f1a6a294d739fbd21d71fa023286daeea8c0ed49e7c2b/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed810568aeffed3edc78910af32af911c835cc39ebbfacd1f0ab5dd53028e5c", size = 2138674, upload-time = "2025-10-14T10:22:54.499Z" }, + { url = "https://files.pythonhosted.org/packages/c4/48/ae937e5a831b7c0dc646b2ef788c27cd003894882415300ed21927c21efa/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:4f5d640aeebb438517150fdeec097739614421900e4a08db4a3ef38898798537", size = 2112087, upload-time = "2025-10-14T10:22:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/5e/db/6db8073e3d32dae017da7e0d16a9ecb897d0a4d92e00634916e486097961/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a9ab037b71927babc6d9e7fc01aea9e66dc2a4a34dff06ef0724a4049629f94", size = 1920387, upload-time = "2025-10-14T10:22:59.342Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c1/dd3542d072fcc336030d66834872f0328727e3b8de289c662faa04aa270e/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4dab9484ec605c3016df9ad4fd4f9a390bc5d816a3b10c6550f8424bb80b18c", size = 1951495, upload-time = "2025-10-14T10:23:02.089Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c6/db8d13a1f8ab3f1eb08c88bd00fd62d44311e3456d1e85c0e59e0a0376e7/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8a5028425820731d8c6c098ab642d7b8b999758e24acae03ed38a66eca8335", size = 2139008, upload-time = "2025-10-14T10:23:04.539Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d4/912e976a2dd0b49f31c98a060ca90b353f3b73ee3ea2fd0030412f6ac5ec/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e5ab4fc177dd41536b3c32b2ea11380dd3d4619a385860621478ac2d25ceb00", size = 2106739, upload-time = "2025-10-14T10:23:06.934Z" }, + { url = "https://files.pythonhosted.org/packages/71/f0/66ec5a626c81eba326072d6ee2b127f8c139543f1bf609b4842978d37833/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d88d0054d3fa11ce936184896bed3c1c5441d6fa483b498fac6a5d0dd6f64a9", size = 1932549, upload-time = "2025-10-14T10:23:09.24Z" }, + { url = "https://files.pythonhosted.org/packages/c4/af/625626278ca801ea0a658c2dcf290dc9f21bb383098e99e7c6a029fccfc0/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b2a054a8725f05b4b6503357e0ac1c4e8234ad3b0c2ac130d6ffc66f0e170e2", size = 2135093, upload-time = "2025-10-14T10:23:11.626Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/2fba049f54e0f4975fef66be654c597a1d005320fa141863699180c7697d/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0d9db5a161c99375a0c68c058e227bee1d89303300802601d76a3d01f74e258", size = 2187971, upload-time = "2025-10-14T10:23:14.437Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/65ab839a2dfcd3b949202f9d920c34f9de5a537c3646662bdf2f7d999680/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:6273ea2c8ffdac7b7fda2653c49682db815aebf4a89243a6feccf5e36c18c347", size = 2147939, upload-time = "2025-10-14T10:23:16.831Z" }, + { url = "https://files.pythonhosted.org/packages/44/58/627565d3d182ce6dfda18b8e1c841eede3629d59c9d7cbc1e12a03aeb328/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:4c973add636efc61de22530b2ef83a65f39b6d6f656df97f678720e20de26caa", size = 2311400, upload-time = "2025-10-14T10:23:19.234Z" }, + { url = "https://files.pythonhosted.org/packages/24/06/8a84711162ad5a5f19a88cead37cca81b4b1f294f46260ef7334ae4f24d3/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b69d1973354758007f46cf2d44a4f3d0933f10b6dc9bf15cf1356e037f6f731a", size = 2316840, upload-time = "2025-10-14T10:23:21.738Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8b/b7bb512a4682a2f7fbfae152a755d37351743900226d29bd953aaf870eaa/pydantic_core-2.41.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3619320641fd212aaf5997b6ca505e97540b7e16418f4a241f44cdf108ffb50d", size = 2149135, upload-time = "2025-10-14T10:23:24.379Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7d/138e902ed6399b866f7cfe4435d22445e16fff888a1c00560d9dc79a780f/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:491535d45cd7ad7e4a2af4a5169b0d07bebf1adfd164b0368da8aa41e19907a5", size = 2104721, upload-time = "2025-10-14T10:23:26.906Z" }, + { url = "https://files.pythonhosted.org/packages/47/13/0525623cf94627f7b53b4c2034c81edc8491cbfc7c28d5447fa318791479/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:54d86c0cada6aba4ec4c047d0e348cbad7063b87ae0f005d9f8c9ad04d4a92a2", size = 1931608, upload-time = "2025-10-14T10:23:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f9/744bc98137d6ef0a233f808bfc9b18cf94624bf30836a18d3b05d08bf418/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca1124aced216b2500dc2609eade086d718e8249cb9696660ab447d50a758bd", size = 2132986, upload-time = "2025-10-14T10:23:32.057Z" }, + { url = "https://files.pythonhosted.org/packages/17/c8/629e88920171173f6049386cc71f893dff03209a9ef32b4d2f7e7c264bcf/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c9024169becccf0cb470ada03ee578d7348c119a0d42af3dcf9eda96e3a247c", size = 2187516, upload-time = "2025-10-14T10:23:34.871Z" }, + { url = "https://files.pythonhosted.org/packages/2e/0f/4f2734688d98488782218ca61bcc118329bf5de05bb7fe3adc7dd79b0b86/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:26895a4268ae5a2849269f4991cdc97236e4b9c010e51137becf25182daac405", size = 2146146, upload-time = "2025-10-14T10:23:37.342Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f2/ab385dbd94a052c62224b99cf99002eee99dbec40e10006c78575aead256/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:ca4df25762cf71308c446e33c9b1fdca2923a3f13de616e2a949f38bf21ff5a8", size = 2311296, upload-time = "2025-10-14T10:23:40.145Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8e/e4f12afe1beeb9823bba5375f8f258df0cc61b056b0195fb1cf9f62a1a58/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5a28fcedd762349519276c36634e71853b4541079cab4acaaac60c4421827308", size = 2315386, upload-time = "2025-10-14T10:23:42.624Z" }, + { url = "https://files.pythonhosted.org/packages/48/f7/925f65d930802e3ea2eb4d5afa4cb8730c8dc0d2cb89a59dc4ed2fcb2d74/pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f", size = 2147775, upload-time = "2025-10-14T10:23:45.406Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -480,6 +909,79 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, + { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, + { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, +] + [[package]] name = "ruff" version = "0.13.2" @@ -515,6 +1017,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[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 = "starlette" +version = "0.48.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/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" }, +] + [[package]] name = "tomli" version = "2.2.1" @@ -554,6 +1078,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -563,6 +1099,18 @@ 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 = "tzdata" version = "2025.2" @@ -571,3 +1119,18 @@ sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be76 wheels = [ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, ] + +[[package]] +name = "uvicorn" +version = "0.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, +] diff --git a/rust/lance-graph/src/parser.rs b/rust/lance-graph/src/parser.rs index 0b45bb282..e0b8a0b45 100644 --- a/rust/lance-graph/src/parser.rs +++ b/rust/lance-graph/src/parser.rs @@ -254,9 +254,66 @@ fn where_clause(input: &str) -> IResult<&str, WhereClause> { Ok((input, WhereClause { expression })) } -// Parse a boolean expression (simplified) +// Parse a boolean expression with OR precedence fn boolean_expression(input: &str) -> IResult<&str, BooleanExpression> { - // For now, just parse simple comparisons + boolean_or_expression(input) +} + +fn boolean_or_expression(input: &str) -> IResult<&str, BooleanExpression> { + let (input, first) = boolean_and_expression(input)?; + let (input, rest) = many0(preceded( + tuple((multispace0, tag_no_case("OR"), multispace1)), + boolean_and_expression, + ))(input)?; + let expr = rest.into_iter().fold(first, |acc, item| { + BooleanExpression::Or(Box::new(acc), Box::new(item)) + }); + Ok((input, expr)) +} + +fn boolean_and_expression(input: &str) -> IResult<&str, BooleanExpression> { + let (input, first) = boolean_not_expression(input)?; + let (input, rest) = many0(preceded( + tuple((multispace0, tag_no_case("AND"), multispace1)), + boolean_not_expression, + ))(input)?; + let expr = rest.into_iter().fold(first, |acc, item| { + BooleanExpression::And(Box::new(acc), Box::new(item)) + }); + Ok((input, expr)) +} + +fn boolean_not_expression(input: &str) -> IResult<&str, BooleanExpression> { + let (input, _) = multispace0(input)?; + alt(( + map( + preceded( + tuple((tag_no_case("NOT"), multispace1)), + boolean_not_expression, + ), + |expr| BooleanExpression::Not(Box::new(expr)), + ), + boolean_primary_expression, + ))(input) +} + +fn boolean_primary_expression(input: &str) -> IResult<&str, BooleanExpression> { + let (input, _) = multispace0(input)?; + alt(( + map( + delimited( + tuple((char('('), multispace0)), + boolean_expression, + tuple((multispace0, char(')'))), + ), + |expr| expr, + ), + comparison_expression, + ))(input) +} + +fn comparison_expression(input: &str) -> IResult<&str, BooleanExpression> { + let (input, _) = multispace0(input)?; let (input, left) = value_expression(input)?; let (input, _) = multispace0(input)?; let (input, operator) = comparison_operator(input)?; @@ -442,12 +499,23 @@ fn identifier(input: &str) -> IResult<&str, &str> { // Parse a string literal fn string_literal(input: &str) -> IResult<&str, String> { + alt((double_quoted_string, single_quoted_string))(input) +} + +fn double_quoted_string(input: &str) -> IResult<&str, String> { let (input, _) = char('"')(input)?; let (input, content) = take_while1(|c| c != '"')(input)?; let (input, _) = char('"')(input)?; Ok((input, content.to_string())) } +fn single_quoted_string(input: &str) -> IResult<&str, String> { + let (input, _) = char('\'')(input)?; + let (input, content) = take_while1(|c| c != '\'')(input)?; + let (input, _) = char('\'')(input)?; + Ok((input, content.to_string())) +} + // Parse an integer literal fn integer_literal(input: &str) -> IResult<&str, i64> { let (input, digits) = recognize(pair( @@ -543,6 +611,7 @@ fn length_range(input: &str) -> IResult<&str, LengthRange> { #[cfg(test)] mod tests { use super::*; + use crate::ast::{BooleanExpression, ComparisonOperator, PropertyValue, ValueExpression}; #[test] fn test_parse_simple_node_query() { @@ -609,6 +678,54 @@ mod tests { assert!(result.where_clause.is_some()); } + #[test] + fn test_parse_query_with_single_quoted_literal() { + let query = "MATCH (n:Person) WHERE n.name = 'Alice' RETURN n.name"; + let result = parse_cypher_query(query).unwrap(); + + assert!(result.where_clause.is_some()); + } + + #[test] + fn test_parse_query_with_and_conditions() { + let query = "MATCH (src:Entity)-[rel:RELATIONSHIP]->(dst:Entity) WHERE rel.relationship_type = 'WORKS_ON' AND dst.name_lower = 'presto' RETURN src.name, src.entity_id"; + let result = parse_cypher_query(query).unwrap(); + + let where_clause = result.where_clause.expect("Expected WHERE clause"); + match where_clause.expression { + BooleanExpression::And(left, right) => { + match *left { + BooleanExpression::Comparison { + left: ValueExpression::Property(ref prop), + operator, + right: ValueExpression::Literal(PropertyValue::String(ref value)), + } => { + assert_eq!(prop.variable, "rel"); + assert_eq!(prop.property, "relationship_type"); + assert_eq!(operator, ComparisonOperator::Equal); + assert_eq!(value, "WORKS_ON"); + } + _ => panic!("Expected comparison for relationship_type filter"), + } + + match *right { + BooleanExpression::Comparison { + left: ValueExpression::Property(ref prop), + operator, + right: ValueExpression::Literal(PropertyValue::String(ref value)), + } => { + assert_eq!(prop.variable, "dst"); + assert_eq!(prop.property, "name_lower"); + assert_eq!(operator, ComparisonOperator::Equal); + assert_eq!(value, "presto"); + } + _ => panic!("Expected comparison for destination name filter"), + } + } + other => panic!("Expected AND expression, got {:?}", other), + } + } + #[test] fn test_parse_query_with_limit() { let query = "MATCH (n:Person) RETURN n.name LIMIT 10";