diff --git a/.gitignore b/.gitignore index c69f283..6546803 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.vscode *.txt *.__pycache__ +.venv/ # compilation and distribution __pycache__ diff --git a/README.md b/README.md index 6e88f67..60526b4 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ uniparser parse report.pdf ```python import os + parser = UniParserClient( host="https://uniparser.dp.tech/", api_key=os.getenv("UNIPARSER_API_KEY"), @@ -152,13 +153,10 @@ import os from uniparser_tools.api.clients import UniParserClient # 设置 API 密钥 -api_key = os.getenv('UNIPARSER_API_KEY') +api_key = os.getenv("UNIPARSER_API_KEY") # 初始化客户端 -parser = UniParserClient( - host="https://uniparser.dp.tech/", - api_key=api_key -) +parser = UniParserClient(host="https://uniparser.dp.tech/", api_key=api_key) ``` ### 2. 解析 PDF 文件(科学文献推荐默认) @@ -170,12 +168,12 @@ from uniparser_tools.common.constant import ParseMode, ParseModeTextual result = parser.trigger_file( pdf_path="./example.pdf", textual=ParseModeTextual.OCRHighQuality, # high quality - equation=ParseMode.OCRHighQuality, # high quality - table=ParseMode.OCRHighQuality, # high quality - chart=ParseMode.DumpBase64, # original image base64 - figure=ParseMode.DumpBase64, # original image base64 - expression=ParseMode.DumpBase64, # original image base64 - molecule=ParseMode.OCRFast, # fast + equation=ParseMode.OCRHighQuality, # high quality + table=ParseMode.OCRHighQuality, # high quality + chart=ParseMode.DumpBase64, # original image base64 + figure=ParseMode.DumpBase64, # original image base64 + expression=ParseMode.DumpBase64, # original image base64 + molecule=ParseMode.OCRFast, # fast ) if result["status"] == "success": @@ -327,9 +325,9 @@ from uniparser_tools.common.constant import FormatFlag result = parser.get_formatted( token, content=True, - textual=FormatFlag.Markdown, # 文本使用 Markdown - table=FormatFlag.Html, # 表格使用 HTML - equation=FormatFlag.Latex, # 公式使用 LaTeX + textual=FormatFlag.Markdown, # 文本使用 Markdown + table=FormatFlag.Html, # 表格使用 HTML + equation=FormatFlag.Latex, # 公式使用 LaTeX ) if result["status"] == "success": diff --git a/skills/UniParser-Tools/references/api-reference.md b/skills/UniParser-Tools/references/api-reference.md index 7036256..1495abc 100644 --- a/skills/UniParser-Tools/references/api-reference.md +++ b/skills/UniParser-Tools/references/api-reference.md @@ -15,9 +15,9 @@ ```python result = parser.trigger_file( file_path="./document.pdf", - sync=False, # Required for async mode - callback_url="https://...", # Your callback endpoint - callback_secret="your-secret", # For signature verification + sync=False, # Required for async mode + callback_url="https://...", # Your callback endpoint + callback_secret="your-secret", # For signature verification # ... other parse mode parameters ) ``` diff --git a/skills/UniParser-Tools/references/data-classes.md b/skills/UniParser-Tools/references/data-classes.md index d8c6e4c..56dfbd3 100644 --- a/skills/UniParser-Tools/references/data-classes.md +++ b/skills/UniParser-Tools/references/data-classes.md @@ -54,6 +54,6 @@ for page in pages_tree: print(f"LaTeX: {item.latex}") # For tables, access DataFrame - if hasattr(item, 'df'): + if hasattr(item, "df"): print(f"DataFrame:\n{item.df}") ``` diff --git a/skills/UniParser-Tools/references/notes.md b/skills/UniParser-Tools/references/notes.md index 4513e39..fa5d1e5 100644 --- a/skills/UniParser-Tools/references/notes.md +++ b/skills/UniParser-Tools/references/notes.md @@ -14,6 +14,7 @@ import hmac import hashlib + def verify_callback(content, checksum, secret): expected = hmac.new(secret.encode(), content.encode(), hashlib.sha256).hexdigest() return hmac.compare_digest(expected, checksum) diff --git a/skills/UniParser-Tools/references/patterns.md b/skills/UniParser-Tools/references/patterns.md index ed60131..a0cb10f 100644 --- a/skills/UniParser-Tools/references/patterns.md +++ b/skills/UniParser-Tools/references/patterns.md @@ -77,26 +77,23 @@ from flask import Flask, request app = Flask(__name__) CALLBACK_SECRET = "your-secret-key" -@app.route('/callback', methods=['POST']) + +@app.route("/callback", methods=["POST"]) def handle_callback(): data = request.json - content = data['content'] - received_checksum = data['checksum'] - + content = data["content"] + received_checksum = data["checksum"] + # Verify signature - expected = hmac.new( - CALLBACK_SECRET.encode(), - json.dumps(content).encode(), - hashlib.sha256 - ).hexdigest() - + expected = hmac.new(CALLBACK_SECRET.encode(), json.dumps(content).encode(), hashlib.sha256).hexdigest() + if not hmac.compare_digest(received_checksum, expected): - return {'error': 'Invalid signature'}, 401 - + return {"error": "Invalid signature"}, 401 + # Process the result - token = data['token'] + token = data["token"] print(f"Task {token} completed!") - return {'status': 'ok'} + return {"status": "ok"} ``` ## Pattern 4: Mixed Format Output @@ -105,10 +102,10 @@ def handle_callback(): result = parser.get_formatted( token, content=True, - textual=FormatFlag.Markdown, # Text as Markdown - table=FormatFlag.Html, # Tables as HTML - equation=FormatFlag.Latex, # Equations as LaTeX - figure=FormatFlag.Markdown, # Figures as Markdown img + textual=FormatFlag.Markdown, # Text as Markdown + table=FormatFlag.Html, # Tables as HTML + equation=FormatFlag.Latex, # Equations as LaTeX + figure=FormatFlag.Markdown, # Figures as Markdown img ) ``` diff --git a/skills/UniParser-Tools/references/utilities.md b/skills/UniParser-Tools/references/utilities.md index 2b90562..530d0e8 100644 --- a/skills/UniParser-Tools/references/utilities.md +++ b/skills/UniParser-Tools/references/utilities.md @@ -25,22 +25,22 @@ clean_text = clean_scientific_text(text, strict=False) from uniparser_tools.utils.bbox import BBox, Point # BBox properties -bbox.area # Area -bbox.width # Width -bbox.height # Height -bbox.tl # Top-left point -bbox.br # Bottom-right point -bbox.ctr # Center point -bbox.xyxy # (x1, y1, x2, y2) tuple -bbox.xywh # (x, y, w, h) tuple +bbox.area # Area +bbox.width # Width +bbox.height # Height +bbox.tl # Top-left point +bbox.br # Bottom-right point +bbox.ctr # Center point +bbox.xyxy # (x1, y1, x2, y2) tuple +bbox.xywh # (x, y, w, h) tuple # BBox operations -bbox.iou(other) # Intersection over Union -bbox.iof(other) # Intersection over Foreground +bbox.iou(other) # Intersection over Union +bbox.iof(other) # Intersection over Foreground bbox.intersection(other) # Intersection box -bbox.union(other) # Union box -bbox.expand(pix, wh) # Expand by pixels -bbox.shrink(pix, wh) # Shrink by pixels +bbox.union(other) # Union box +bbox.expand(pix, wh) # Expand by pixels +bbox.shrink(pix, wh) # Shrink by pixels ``` ## Text Processing diff --git a/uniparser_agent/README.md b/uniparser_agent/README.md new file mode 100644 index 0000000..a7479d5 --- /dev/null +++ b/uniparser_agent/README.md @@ -0,0 +1,11 @@ +# UniParser Agent + +UniParser Agent 是基于 UniParser 的文档处理包,提供统一的命令行入口: + +- `parse`:将 PDF、图片或公开 PDF URL 解析为结构化文档。 +- `vqa`:从习题、试卷、题册或答案册中提取题目、答案、解析和相关图片,并生成结构化 VQA 数据。 + +本包包含文档解析、LLM 调用和 pdf2vqa 流程所需的公共模块。 + +安装方式、配置说明、命令参数、处理流程和输出格式,请参阅 +[pdf2vqa 完整文档](pdf2vqa/README.md)。 diff --git a/uniparser_agent/__init__.py b/uniparser_agent/__init__.py new file mode 100644 index 0000000..fc87af0 --- /dev/null +++ b/uniparser_agent/__init__.py @@ -0,0 +1,3 @@ +"""UniParser document parsing and exam VQA extraction.""" + +__version__ = "0.1.0" diff --git a/uniparser_agent/cli.py b/uniparser_agent/cli.py new file mode 100644 index 0000000..553223d --- /dev/null +++ b/uniparser_agent/cli.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +import json +from typing import Optional + +import typer + +from uniparser_agent.llm import LLMConfig, resolve_llm_config +from uniparser_agent.parse.service import parse_document +from uniparser_agent.pdf2vqa.pipeline import run_vqa_pipeline + + +app = typer.Typer( + name="uniparser-agent", + help="UniParser document parsing and exam VQA extraction.", + no_args_is_help=True, +) + + +@app.command("parse") +def parse_cmd( + input_path: str = typer.Argument(..., help="Local PDF/image path or public PDF URL."), + output_dir: Optional[str] = typer.Option(None, "-o", "--output-dir", help="Output directory."), + overwrite: bool = typer.Option(False, "--overwrite", help="Replace output directory if it exists."), + json_output: bool = typer.Option(False, "--json", help="Print machine-readable JSON."), +) -> None: + """Parse a document with UniParser scientific-paper defaults.""" + result = parse_document(input_path, output_dir=output_dir, overwrite=overwrite) + if json_output: + typer.echo(json.dumps(result, ensure_ascii=False, indent=2)) + return + typer.echo(f"Token: {result.get('token', '')}") + typer.echo(f"Pages tree: {result['pages_tree_path']}") + typer.echo(f"Markdown: {result['markdown_path']}") + typer.echo(f"Output directory: {result['output_dir']}") + + +@app.command("vqa") +def vqa_cmd( + input_path: Optional[str] = typer.Argument( + None, + help="Local PDF/image path or public PDF URL. Omit when using --pages-tree.", + ), + output_dir: Optional[str] = typer.Option(None, "-o", "--output-dir", help="VQA output directory."), + answer_pdf: Optional[str] = typer.Option( + None, + "--answer-pdf", + help="Answer booklet PDF. Merged after the question booklet (local PDFs only).", + ), + pages_tree: Optional[str] = typer.Option( + None, + "--pages-tree", + help="Skip UniParser parse and use an existing pages_tree.json.", + ), + overwrite: bool = typer.Option(False, "--overwrite", help="Replace output directory if it exists."), + api_key: Optional[str] = typer.Option( + None, + "--api-key", + help="LLM API key (overrides OPENAI_API_KEY).", + envvar=[], + ), + base_url: Optional[str] = typer.Option( + None, + "--base-url", + help="LLM base URL (overrides OPENAI_BASE_URL).", + ), + model: Optional[str] = typer.Option( + None, + "--model", + help="LLM model name (overrides OPENAI_MODEL).", + ), + enable_thinking: bool = typer.Option( + False, + "--enable-thinking/--no-enable-thinking", + help="Pass chat_template_kwargs.enable_thinking for Qwen-compatible servers.", + ), + json_output: bool = typer.Option(False, "--json", help="Print machine-readable JSON."), +) -> None: + """Parse with UniParser (unless --pages-tree), then extract VQA pairs via LLM.""" + if answer_pdf and pages_tree: + raise typer.BadParameter("Use either --answer-pdf or --pages-tree, not both.") + if answer_pdf and not input_path: + raise typer.BadParameter("--answer-pdf requires the question booklet as INPUT.") + if not input_path and not pages_tree: + raise typer.BadParameter("Provide INPUT (pdf/url/image) or --pages-tree.") + if input_path and pages_tree: + raise typer.BadParameter("Use either INPUT or --pages-tree, not both.") + + llm_config = _build_llm_config( + api_key=api_key, + base_url=base_url, + model=model, + enable_thinking=enable_thinking, + ) + result = run_vqa_pipeline( + input_path=input_path, + answer_pdf=answer_pdf, + pages_tree_path=pages_tree, + output_dir=output_dir, + overwrite=overwrite, + llm_config=llm_config, + ) + if json_output: + typer.echo(json.dumps(result, ensure_ascii=False, indent=2)) + return + + paths = result["paths"] + if paths.get("merged_pdf"): + typer.echo(f"Merged PDF: {paths['merged_pdf']}") + typer.echo(f"Pages tree: {paths['pages_tree']}") + typer.echo(f"Content list items: {result['n_content_items']}") + typer.echo(f"VQA images: {result.get('n_vqa_images', 0)} -> {paths.get('vqa_images', '')}") + typer.echo(f"Merged VQA pairs: {result['n_merged_vqa']}") + typer.echo(f"JSONL: {paths['merged_vqa_pairs_jsonl']}") + typer.echo(f"Markdown: {paths['merged_vqa_pairs_md']}") + if paths.get("vqa_sharegpt"): + typer.echo(f"ShareGPT: {paths['vqa_sharegpt']}") + typer.echo(f"Output directory: {paths['output_dir']}") + + +def _build_llm_config( + *, + api_key: Optional[str], + base_url: Optional[str], + model: Optional[str], + enable_thinking: bool, +) -> LLMConfig: + try: + return resolve_llm_config( + api_key=api_key, + base_url=base_url, + model=model, + enable_thinking=enable_thinking, + ) + except ValueError as exc: + raise typer.BadParameter(str(exc)) from exc + + +def main() -> None: + app() + + +if __name__ == "__main__": + main() diff --git a/uniparser_agent/llm/__init__.py b/uniparser_agent/llm/__init__.py new file mode 100644 index 0000000..603ef80 --- /dev/null +++ b/uniparser_agent/llm/__init__.py @@ -0,0 +1,11 @@ +"""Shared OpenAI-compatible LLM helpers for uniparser_agent.""" + +from uniparser_agent.llm.client import OpenAICompatLLM +from uniparser_agent.llm.config import LLMConfig, resolve_llm_config + + +__all__ = [ + "LLMConfig", + "OpenAICompatLLM", + "resolve_llm_config", +] diff --git a/uniparser_agent/llm/client.py b/uniparser_agent/llm/client.py new file mode 100644 index 0000000..83aa2a9 --- /dev/null +++ b/uniparser_agent/llm/client.py @@ -0,0 +1,86 @@ +"""OpenAI-compatible chat client.""" + +from __future__ import annotations + +from typing import Any + +from openai import OpenAI + +from uniparser_agent.llm.config import LLMConfig, resolve_llm_config + + +class OpenAICompatLLM: + """Thin wrapper around ``openai.OpenAI`` chat completions.""" + + def __init__( + self, + config: LLMConfig | None = None, + *, + api_key: str | None = None, + base_url: str | None = None, + model: str | None = None, + timeout: float | None = None, + max_tokens: int | None = None, + enable_thinking: bool | None = None, + extra_body: dict[str, Any] | None = None, + ) -> None: + self.config = resolve_llm_config( + config=config, + api_key=api_key, + base_url=base_url, + model=model, + timeout=timeout, + max_tokens=max_tokens, + enable_thinking=enable_thinking, + extra_body=extra_body, + ) + self._client = OpenAI( + api_key=self.config.api_key, + base_url=self.config.base_url, + timeout=self.config.timeout, + ) + + @property + def api_key(self) -> str: + return self.config.api_key + + @property + def base_url(self) -> str: + return self.config.base_url + + @property + def model(self) -> str: + return self.config.model + + @property + def timeout(self) -> float: + return self.config.timeout + + @property + def max_tokens(self) -> int: + return self.config.max_tokens + + @property + def enable_thinking(self) -> bool: + return self.config.enable_thinking + + def chat(self, *, system_prompt: str, user_content: str) -> str: + kwargs: dict[str, Any] = { + "model": self.config.model, + "messages": [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_content}, + ], + "max_tokens": self.config.max_tokens, + } + extra = self.config.resolved_extra_body() + if extra is not None: + kwargs["extra_body"] = extra + response = self._client.chat.completions.create(**kwargs) + content = response.choices[0].message.content + if content is None: + raise RuntimeError("LLM returned empty content") + return content + + def meta(self) -> dict[str, Any]: + return self.config.meta() diff --git a/uniparser_agent/llm/config.py b/uniparser_agent/llm/config.py new file mode 100644 index 0000000..a25c6c5 --- /dev/null +++ b/uniparser_agent/llm/config.py @@ -0,0 +1,109 @@ +"""Shared LLM configuration (OpenAI-compatible, no hardcoded defaults).""" + +from __future__ import annotations + +import os +from dataclasses import dataclass, replace +from typing import Any + + +@dataclass(frozen=True) +class LLMConfig: + """OpenAI-compatible LLM settings. + + Required fields have no library defaults: set via constructor, CLI, or + ``OPENAI_API_KEY`` / ``OPENAI_BASE_URL`` / ``OPENAI_MODEL``. + """ + + api_key: str + base_url: str + model: str + timeout: float = 3600.0 + max_tokens: int = 81920 + enable_thinking: bool = False + extra_body: dict[str, Any] | None = None + + def resolved_extra_body(self) -> dict[str, Any] | None: + """Return request ``extra_body``, applying Qwen thinking kwargs when needed.""" + if self.extra_body is not None: + return self.extra_body + if self.enable_thinking or "qwen" in self.model.lower(): + return { + "chat_template_kwargs": {"enable_thinking": self.enable_thinking}, + } + return None + + def meta(self) -> dict[str, Any]: + return { + "base_url": self.base_url, + "model": self.model, + "timeout": self.timeout, + "max_tokens": self.max_tokens, + "enable_thinking": self.enable_thinking, + "extra_body": self.resolved_extra_body(), + } + + +def _env(name: str) -> str: + return (os.environ.get(name) or "").strip() + + +def resolve_llm_config( + *, + api_key: str | None = None, + base_url: str | None = None, + model: str | None = None, + timeout: float | None = None, + max_tokens: int | None = None, + enable_thinking: bool | None = None, + extra_body: dict[str, Any] | None = None, + config: LLMConfig | None = None, +) -> LLMConfig: + """Resolve LLM settings: explicit args / ``config`` override ``OPENAI_*`` env. + + Raises: + ValueError: if api_key, base_url, or model is missing after resolution. + """ + if config is not None: + overrides: dict[str, Any] = {} + if api_key is not None: + overrides["api_key"] = api_key + if base_url is not None: + overrides["base_url"] = base_url + if model is not None: + overrides["model"] = model + if timeout is not None: + overrides["timeout"] = timeout + if max_tokens is not None: + overrides["max_tokens"] = max_tokens + if enable_thinking is not None: + overrides["enable_thinking"] = enable_thinking + if extra_body is not None: + overrides["extra_body"] = extra_body + resolved = replace(config, **overrides) if overrides else config + else: + resolved = LLMConfig( + api_key=(api_key if api_key is not None else _env("OPENAI_API_KEY")), + base_url=(base_url if base_url is not None else _env("OPENAI_BASE_URL")).rstrip("/"), + model=(model if model is not None else _env("OPENAI_MODEL")), + timeout=3600.0 if timeout is None else timeout, + max_tokens=81920 if max_tokens is None else max_tokens, + enable_thinking=False if enable_thinking is None else enable_thinking, + extra_body=extra_body, + ) + + missing: list[str] = [] + if not resolved.api_key: + missing.append("OPENAI_API_KEY (or --api-key / LLMConfig.api_key)") + if not resolved.base_url: + missing.append("OPENAI_BASE_URL (or --base-url / LLMConfig.base_url)") + if not resolved.model: + missing.append("OPENAI_MODEL (or --model / LLMConfig.model)") + if missing: + raise ValueError( + "Missing required LLM config: " + + "; ".join(missing) + + ". Set environment variables or pass them explicitly." + ) + + return replace(resolved, base_url=resolved.base_url.rstrip("/")) diff --git a/uniparser_agent/parse/__init__.py b/uniparser_agent/parse/__init__.py new file mode 100644 index 0000000..b473e24 --- /dev/null +++ b/uniparser_agent/parse/__init__.py @@ -0,0 +1,6 @@ +"""Shared UniParser parse helpers.""" + +from uniparser_agent.parse.service import load_pages_tree, make_client, parse_document + + +__all__ = ["load_pages_tree", "make_client", "parse_document"] diff --git a/uniparser_agent/parse/api_client.py b/uniparser_agent/parse/api_client.py new file mode 100644 index 0000000..0abc6f0 --- /dev/null +++ b/uniparser_agent/parse/api_client.py @@ -0,0 +1,190 @@ +"""Lightweight UniParser HTTP client (no uniparser-tools / OpenCV).""" + +from __future__ import annotations + +import base64 +import traceback +import uuid +from pathlib import Path +from typing import Any +from urllib.parse import urlparse + +import requests + +from uniparser_agent.parse.options import SCIENTIFIC_PAPER_TRIGGER + + +PENDING_STATUSES = frozenset({"undefined", "waiting", "processing"}) +IMAGE_SUFFIXES = frozenset({".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tif", ".tiff"}) + + +class UniParserApiClient: + def __init__(self, host: str, api_key: str) -> None: + if not api_key: + raise ValueError("api_key can not be empty") + if not host.startswith("http"): + raise ValueError("host must start with http or https") + self.api_key = api_key + self.host = host.rstrip("/") + self._user = uuid.uuid5(uuid.NAMESPACE_DNS, api_key) + + def to_token(self, task_id: str) -> str: + return uuid.uuid5(self._user, task_id).hex + + def _headers(self) -> dict[str, str]: + return {"X-API-Key": self.api_key} + + def trigger_file(self, file_path: str, *, trigger_kwargs: dict[str, Any] | None = None) -> dict[str, Any]: + token = self.to_token(file_path) + data = dict(SCIENTIFIC_PAPER_TRIGGER) + if trigger_kwargs: + data.update(trigger_kwargs) + data["token"] = token + try: + with open(file_path, "rb") as fh: + response = requests.post( + f"{self.host}/trigger-file-async", + files={"file": fh}, + data=data, + headers=self._headers(), + timeout=600, + ) + return response.json() + except Exception: + return { + "status": "error", + "token": token, + "message": "trigger file failed", + "description": traceback.format_exc(), + } + + def trigger_url(self, pdf_url: str, *, trigger_kwargs: dict[str, Any] | None = None) -> dict[str, Any]: + token = self.to_token(pdf_url) + data = {"url": pdf_url, **SCIENTIFIC_PAPER_TRIGGER} + if trigger_kwargs: + data.update(trigger_kwargs) + data["token"] = token + try: + response = requests.post( + f"{self.host}/trigger-url-async", + json=data, + headers=self._headers(), + timeout=600, + ) + return response.json() + except Exception: + return { + "status": "error", + "token": token, + "message": "trigger url failed", + "description": traceback.format_exc(), + } + + def trigger_snip(self, snip_path: str, *, trigger_kwargs: dict[str, Any] | None = None) -> dict[str, Any]: + token = self.to_token(snip_path) + data = dict(SCIENTIFIC_PAPER_TRIGGER) + if trigger_kwargs: + data.update(trigger_kwargs) + data["token"] = token + try: + raw = Path(snip_path).read_bytes() + img_b64 = base64.b64encode(raw).decode("ascii") + response = requests.post( + f"{self.host}/trigger-snip-async", + data={"img": img_b64, **data}, + headers=self._headers(), + timeout=600, + ) + return response.json() + except Exception: + return { + "status": "error", + "token": token, + "message": "trigger snip failed", + "description": traceback.format_exc(), + } + + def get_result(self, token: str, *, pages_tree: bool = False) -> dict[str, Any]: + payload = { + "token": token, + "content": False, + "objects": False, + "pages_dict": False, + "pages_tree": pages_tree, + "molecule_source": False, + } + try: + response = requests.post( + f"{self.host}/get-result", + json=payload, + headers=self._headers(), + timeout=600, + ) + return response.json() + except Exception: + return { + "status": "error", + "token": token, + "message": "get result failed", + "description": traceback.format_exc(), + } + + def get_formatted(self, token: str) -> dict[str, Any]: + payload = { + "token": token, + "content": True, + "objects": False, + "pages_dict": False, + "pages_tree": False, + "molecule_source": False, + "textual": "markdown", + "table": "markdown", + "molecule": "markdown", + "chart": "markdown", + "figure": "markdown", + "expression": "markdown", + "equation": "latex", + "marginalia": False, + } + try: + response = requests.post( + f"{self.host}/get-formatted", + json=payload, + headers=self._headers(), + timeout=600, + ) + return response.json() + except Exception: + return { + "status": "error", + "token": token, + "message": "get formatted failed", + "description": traceback.format_exc(), + } + + +def resolve_input(raw: str) -> tuple[str, str, Path | None]: + """Return (kind, source_stem, path) where kind is file|image|url.""" + text = raw.strip() + if not text: + raise ValueError("INPUT must not be empty.") + if text.startswith("http://") or text.startswith("https://"): + segment = urlparse(text).path.rstrip("/").rsplit("/", 1)[-1] + stem = segment or "url_document" + for ext in (".pdf", ".png", ".jpg", ".jpeg", ".webp"): + if stem.lower().endswith(ext): + stem = stem[: -len(ext)] + break + return "url", stem or "url_document", None + + path = Path(text).expanduser().resolve() + if not path.is_file(): + raise ValueError(f"File not found: {path}") + stem = path.stem or "document" + if path.suffix.lower() in IMAGE_SUFFIXES: + return "image", stem, path + return "file", stem, path + + +def default_output_dir(source_stem: str) -> Path: + return (Path.home() / "Uni-Parser-Skill" / source_stem).expanduser().resolve() diff --git a/uniparser_agent/parse/config.py b/uniparser_agent/parse/config.py new file mode 100644 index 0000000..1268a21 --- /dev/null +++ b/uniparser_agent/parse/config.py @@ -0,0 +1,19 @@ +"""UniParser API configuration.""" + +from __future__ import annotations + +import os + + +DEFAULT_BASE_URL = "https://uniparser.dp.tech" + + +def get_api_key() -> str: + key = (os.environ.get("UNIPARSER_API_KEY") or "").strip() + if not key: + raise ValueError("UNIPARSER_API_KEY is not set.") + return key + + +def get_base_url() -> str: + return (os.environ.get("UNIPARSER_BASE_URL") or DEFAULT_BASE_URL).strip().rstrip("/") diff --git a/uniparser_agent/parse/options.py b/uniparser_agent/parse/options.py new file mode 100644 index 0000000..dbabdd9 --- /dev/null +++ b/uniparser_agent/parse/options.py @@ -0,0 +1,17 @@ +"""Scientific-paper parse preset (matches uniparser_tools.cli.core.parse_options).""" + +from __future__ import annotations + + +SCIENTIFIC_PAPER_TRIGGER: dict[str, object] = { + "lang": "unknown", + "sync": True, + "textual": 2, + "equation": 2, + "table": 2, + "chart": -1, + "figure": -1, + "expression": -1, + "molecule": 1, + "ordering_method": "xy_cut_exp", +} diff --git a/uniparser_agent/parse/service.py b/uniparser_agent/parse/service.py new file mode 100644 index 0000000..2071d07 --- /dev/null +++ b/uniparser_agent/parse/service.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from uniparser_agent.parse.api_client import UniParserApiClient, resolve_input +from uniparser_agent.parse.config import get_api_key, get_base_url +from uniparser_agent.parse.storage import ( + complete_parse_job, + resolve_output_dir, + save_stage_error, + write_trigger_meta, +) + + +def make_client() -> UniParserApiClient: + return UniParserApiClient(get_base_url(), get_api_key()) + + +def parse_document( + input_path: str, + *, + output_dir: str | None = None, + overwrite: bool = False, +) -> dict[str, Any]: + """Parse with scientific-paper defaults via HTTP API (no OpenCV).""" + kind, source_stem, path = resolve_input(input_path) + out = resolve_output_dir(source_stem, output_dir, overwrite=overwrite) + client = make_client() + + if kind == "file": + trigger = client.trigger_file(str(path)) + input_type = "file" + elif kind == "image": + trigger = client.trigger_snip(str(path)) + input_type = "image" + else: + trigger = client.trigger_url(input_path) + input_type = "url" + + if trigger.get("status") != "success": + save_stage_error(out, "trigger_error.json", trigger) + raise RuntimeError(trigger.get("message") or trigger.get("description") or "trigger failed") + + token = trigger.get("token") + if not token: + raise RuntimeError("trigger response missing token") + + meta_path = write_trigger_meta( + out, + token=token, + input_type=input_type, + input_value=input_path, + ) + + summary = complete_parse_job(client, token, out_dir=out, source_stem=source_stem) + return { + "output_dir": summary["output_dir"], + "pages_tree_path": summary["pages_tree_path"], + "markdown_path": summary["markdown_path"], + "token": summary.get("token", ""), + "input_type": input_type, + "source_stem": source_stem, + "source": input_path, + "trigger_meta_path": str(meta_path), + } + + +def load_pages_tree(pages_tree_path: str | Path) -> dict[str, Any]: + path = Path(pages_tree_path).expanduser().resolve() + data = json.loads(path.read_text(encoding="utf-8")) + if "pages_tree" not in data: + raise ValueError(f"Invalid pages_tree file (missing pages_tree key): {path}") + return data diff --git a/uniparser_agent/parse/storage.py b/uniparser_agent/parse/storage.py new file mode 100644 index 0000000..33694b3 --- /dev/null +++ b/uniparser_agent/parse/storage.py @@ -0,0 +1,147 @@ +"""Parse artifact persistence and polling.""" + +from __future__ import annotations + +import json +import shutil +import time +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +from uniparser_agent.parse.api_client import PENDING_STATUSES, UniParserApiClient + + +POLL_INTERVAL_SEC = 3 +POLL_TIMEOUT_SEC = 1800 + + +def resolve_output_dir( + source_stem: str, + output_dir: str | None, + *, + overwrite: bool, +) -> Path: + out = Path(output_dir).expanduser().resolve() if output_dir else default_output_dir(source_stem) + if out.exists() and not overwrite: + raise FileExistsError(f"Output directory exists: {out}. Use --overwrite to replace.") + if out.exists() and overwrite: + shutil.rmtree(out) + return out + + +def default_output_dir(source_stem: str) -> Path: + return (Path.home() / "Uni-Parser-Skill" / source_stem).expanduser().resolve() + + +def write_trigger_meta( + out_dir: Path, + *, + token: str, + input_type: str, + input_value: str, +) -> Path: + out_dir.mkdir(parents=True, exist_ok=True) + meta_path = out_dir / "trigger_meta.json" + payload = { + "token": token, + "input_type": input_type, + "input": input_value, + "submitted_at": datetime.now(timezone.utc).isoformat(), + "parse_preset": "scientific-paper", + } + meta_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8") + return meta_path + + +def save_parse_results( + *, + out_dir: Path, + source_stem: str, + pages_tree: dict[str, Any], + formatted: dict[str, Any], +) -> dict[str, Any]: + out_dir.mkdir(parents=True, exist_ok=True) + stem = source_stem or "document" + + pages_tree_path = out_dir / "pages_tree.json" + pages_tree_path.write_text( + json.dumps(pages_tree, indent=2, ensure_ascii=False, default=str), + encoding="utf-8", + ) + + md_path = out_dir / f"{stem}.md" + content = formatted.get("content", "") + md_path.write_text(content, encoding="utf-8") + + meta = {k: v for k, v in formatted.items() if k != "content"} + (out_dir / "formatted_meta.json").write_text( + json.dumps(meta, indent=2, ensure_ascii=False, default=str), + encoding="utf-8", + ) + + return { + "output_dir": str(out_dir), + "pages_tree_path": str(pages_tree_path), + "markdown_path": str(md_path), + "content_chars": len(content), + } + + +def poll_until_success(client: UniParserApiClient, token: str) -> dict[str, Any]: + deadline = time.time() + POLL_TIMEOUT_SEC + last: dict[str, Any] = {} + while time.time() < deadline: + last = client.get_result(token, pages_tree=False) + status = last.get("status") + if status == "success": + return last + if status == "error": + return last + if status in PENDING_STATUSES or status is None: + time.sleep(POLL_INTERVAL_SEC) + continue + return last + return { + "status": "error", + "description": f"Timed out after {POLL_TIMEOUT_SEC}s waiting for parsing to finish.", + "token": token, + "last_status": last.get("status"), + } + + +def complete_parse_job( + client: UniParserApiClient, + token: str, + *, + out_dir: Path, + source_stem: str, +) -> dict[str, Any]: + poll_result = poll_until_success(client, token) + if poll_result.get("status") != "success": + save_stage_error(out_dir, "poll_error.json", poll_result) + raise RuntimeError(poll_result.get("description") or poll_result.get("message") or "poll failed") + + pages_tree = client.get_result(token, pages_tree=True) + if pages_tree.get("status") != "success": + save_stage_error(out_dir, "pages_tree_error.json", pages_tree) + raise RuntimeError(pages_tree.get("description") or "get_result pages_tree failed") + + formatted = client.get_formatted(token) + if formatted.get("status") != "success": + save_stage_error(out_dir, "formatted_error.json", formatted) + raise RuntimeError(formatted.get("description") or "get_formatted failed") + + summary = save_parse_results( + out_dir=out_dir, + source_stem=source_stem, + pages_tree=pages_tree, + formatted=formatted, + ) + summary["token"] = token + return summary + + +def save_stage_error(out_dir: Path, filename: str, payload: dict[str, Any]) -> None: + out_dir.mkdir(parents=True, exist_ok=True) + (out_dir / filename).write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8") diff --git a/uniparser_agent/pdf2vqa/README.md b/uniparser_agent/pdf2vqa/README.md new file mode 100644 index 0000000..2661abd --- /dev/null +++ b/uniparser_agent/pdf2vqa/README.md @@ -0,0 +1,334 @@ +# 习题 VQA 抽取(`uniparser-agent vqa`) + +从习题、试卷、题册或答案册中提取结构化问答数据,包括题号、题干、短答案和详细解析,并生成适合题库整理、人工检查或模型训练的 JSONL、Markdown 和 ShareGPT 数据。 + +支持本地 PDF、图片和公开 PDF URL;题册与答案册分开时,也可以同时输入两个本地 PDF 自动配对。 + +## 核心能力 + +- 从习题或试卷中提取题号、题干、答案和解析 +- 支持题目与答案位于同一文档 +- 支持“题册 + 答案册”两个独立 PDF,并自动配对相同题号 +- 保留题干和解析中的公式 +- 导出题目相关图片,生成图文结合的 VQA 样本 +- 输出 JSONL 和 Markdown,方便数据处理与人工检查 +- 输出 ShareGPT 格式,可用于多模态或纯文本模型训练 +- 支持复用已有 `pages_tree.json`,方便更换模型后重新抽取 + +## 安装 + +运行要求: + +- Python 3.11+ +- 已安装 `uniparser-agent` +- 输入原始文档时,需要 UniParser API Key +- OpenAI 兼容的 LLM 服务 + +安装项目依赖: + +```bash +cd uniparser_agent +uv sync +``` + +或在已有虚拟环境中安装: + +```bash +cd uniparser_agent +uv pip install -e ".[dev]" +``` + + + +## 配置 + +设置 UniParser 和 LLM 环境变量: + +```bash +export UNIPARSER_API_KEY="your-uniparser-key" +export OPENAI_API_KEY="your-llm-key" +export OPENAI_BASE_URL="https://example.com/v1" +export OPENAI_MODEL="your-model" +``` + + +| 变量 | 是否必填 | 用途 | +| -------------------- | ------------------ | ------------------ | +| `UNIPARSER_API_KEY` | 输入 PDF、图片或 URL 时必填 | 解析原始文档 | +| `OPENAI_API_KEY` | 必填 | 调用问答抽取模型 | +| `OPENAI_BASE_URL` | 必填 | OpenAI 兼容服务地址 | +| `OPENAI_MODEL` | 必填 | 问答抽取模型名称 | +| `UNIPARSER_BASE_URL` | 可选 | 自定义 UniParser 服务地址 | + + +使用已有 `pages_tree.json` 时不需要 `UNIPARSER_API_KEY`。 + +也可以通过 `--api-key`、`--base-url` 和 `--model` 在命令行中覆盖 LLM 配置。 + +不要把真实 API Key 写入源码或提交到仓库。 + +## 快速开始 + + + +### 从一个 PDF 中抽取 + +适用于题目、答案和解析位于同一文档: + +```bash +uv run uniparser-agent vqa /path/to/exam.pdf \ + -o ./vqa_out +``` + +完成后主要查看: + +```text +vqa_out/merged_vqa_pairs.jsonl +vqa_out/merged_vqa_pairs.md +``` + + + +## 使用指南 + + + +### 题册与答案册分别存放 + +题册和答案册都必须是本地 PDF: + +```bash +uv run uniparser-agent vqa /path/to/questions.pdf \ + --answer-pdf /path/to/answers.pdf \ + -o ./vqa_out +``` + +程序会按“题册在前、答案册在后”的顺序处理,并根据章节和题号配对题干、答案与解析。 + +`--answer-pdf` 不能与图片、URL 或 `--pages-tree` 同时使用。 + +### 输入图片 + +```bash +uv run uniparser-agent vqa /path/to/page.png \ + -o ./vqa_out +``` + +适合单页试题、截图或扫描图片。 + +### 输入公开 PDF URL + +```bash +uv run uniparser-agent vqa "https://example.com/exam.pdf" \ + -o ./vqa_out +``` + +URL 必须能够公开访问,并直接返回 PDF。 + +### 使用已有解析结果 + +如果文档已完成 UniParser 解析: + +```bash +uv run uniparser-agent vqa \ + --pages-tree /path/to/pages_tree.json \ + -o ./vqa_out +``` + +该方式适合更换模型或重新抽取,不会再次消耗 UniParser 解析额度。 + +## 常用参数 + +```text +uniparser-agent vqa [OPTIONS] [INPUT_PATH] +``` + + +| 参数 | 用途 | +| --------------------- | ---------------------- | +| `INPUT_PATH` | 本地 PDF、图片或公开 PDF URL | +| `-o` / `--output-dir` | 输出目录;默认 `./vqa_out` | +| `--answer-pdf` | 输入独立答案册 PDF | +| `--pages-tree` | 复用已有 `pages_tree.json` | +| `--overwrite` | 删除并重建已存在的输出目录 | +| `--json` | 在终端输出机器可读的运行摘要 | + + +输入规则: + +- `INPUT_PATH` 与 `--pages-tree` 二选一 +- `--answer-pdf` 必须与题册 PDF 一起使用 +- `--answer-pdf` 与 `--pages-tree` 不能同时使用 + +查看全部参数: + +```bash +uv run uniparser-agent vqa --help +``` + + + +## 输出结果 + +默认输出目录如下: + +```text +vqa_out/ +├── merged_vqa_pairs.jsonl +├── merged_vqa_pairs.md +├── vqa_sharegpt.json +├── vqa_images/ +├── run_meta.json +├── parse/ +│ └── pages_tree.json +├── merge/ +│ └── merged.pdf +├── extracted_vqa.jsonl +├── llm_content_list.json +└── llm_raw_response.txt +``` + +`merge/merged.pdf` 只在使用“题册 + 答案册”模式时生成。`vqa_images/` 中是否有图片取决于原始解析结果。 + +### 主要结果 + + +| 文件 | 用途 | +| ------------------------ | ---------------------- | +| `merged_vqa_pairs.jsonl` | **结构化主结果**,每行一条完整问答对 | +| `merged_vqa_pairs.md` | 便于人工阅读和检查的 Markdown 版本 | +| `vqa_sharegpt.json` | ShareGPT 格式的模型训练数据 | +| `vqa_images/` | 与题目或解析相关的图片 | +| `run_meta.json` | 本次运行的模型、题量、图片数、耗时和文件路径 | + + + + +### 辅助结果 + + +| 文件 | 用途 | +| ----------------------- | ---------------------- | +| `parse/pages_tree.json` | UniParser 解析结果,可用于重新抽取 | +| `merge/merged.pdf` | 合并后的题册与答案册,仅双 PDF 模式生成 | +| `extracted_vqa.jsonl` | 合并前的题目、答案和解析片段 | +| `llm_content_list.json` | 送入问答抽取阶段的文档内容 | +| `llm_raw_response.txt` | 模型原始返回,主要用于问题排查 | + + + + +### JSONL 数据格式 + +`merged_vqa_pairs.jsonl` 每行是一道题: + +```json +{ + "question_chapter_title": "第一章", + "answer_chapter_title": "第一章答案", + "label": 1, + "question": "题干内容", + "answer": "A", + "solution": "详细解析" +} +``` + +字段说明: + + +| 字段 | 含义 | +| ------------------------ | ----------------- | +| `question_chapter_title` | 题目所在章节或栏目 | +| `answer_chapter_title` | 答案所在章节或栏目 | +| `label` | 题号 | +| `question` | 题干,可包含公式和图片引用 | +| `answer` | 短答案,如选项字母、数值或填空结果 | +| `solution` | 解题过程或详细解析 | + + +题目没有章节标题时,对应字段可能为空。 + +### ShareGPT 数据格式 + +`vqa_sharegpt.json` 是 JSON 数组,每条数据包含: + +```json +{ + "messages": [ + { + "role": "user", + "content": "题干内容" + }, + { + "role": "assistant", + "content": "答案\n\n详细解析" + } + ], + "images": [ + "/absolute/path/to/vqa_images/question_1.png" + ] +} +``` + +- 有图片时,user 内容包含对应数量的 `` 标记 +- `images` 保存相关图片路径 +- 无图片时,`images` 为空数组,可作为纯文本问答使用 +- 没有题干或没有答案/解析的数据不会进入 ShareGPT 主结果 + + + +## 结果检查 + +1. 打开 `merged_vqa_pairs.md`,快速检查题目、答案和解析是否配对。 +2. 使用 `merged_vqa_pairs.jsonl` 进行数据处理或导入题库。 +3. 训练模型前检查 `vqa_sharegpt.json` 中的图片路径是否在目标环境可访问。 +4. 如果题目数量明显偏少,查看 `extracted_vqa.jsonl` 判断是抽取不足还是配对失败。 +5. 如果公式、图片或章节识别不正确,结合 `parse/pages_tree.json` 检查原始解析质量。 + + + +## 常见问题 + + + +### 为什么最终题目数量比原文少? + +主结果只保留能够识别题号并完成题目与答案配对的数据。题号不清晰、章节不一致、答案缺失或模型未正确识别都可能导致数量减少。 + +### 为什么题目图片没有导出? + +只有原始解析结果中实际包含图片数据的内容才能写入 `vqa_images/`。PDF 中看得到图片不代表解析结果一定包含可导出的图片。 + +### 题册和答案册可以使用 URL 吗? + +不可以。双文档模式要求题册和答案册都是本地 PDF。单文档模式支持公开 PDF URL。 + +### 为什么输出目录已存在时报错? + +程序默认避免覆盖已有结果。确认可以删除旧目录后,添加 `--overwrite`。 + +```bash +uv run uniparser-agent vqa /path/to/exam.pdf \ + -o ./vqa_out \ + --overwrite +``` + + + +### 如何更换模型后重新抽取? + +保留首次运行生成的 `parse/pages_tree.json`,然后使用 `--pages-tree` 重新运行。 + +### ShareGPT 中为什么使用绝对图片路径? + +结果会记录当前运行环境中的图片绝对路径。将数据迁移到其他机器或训练环境后,需要同步图片并按需要更新路径。 + +## 当前限制 + +- 双 PDF 模式只支持本地 PDF +- 题号需要能够识别为正整数 +- 题目和答案主要依靠章节与题号配对 +- 跨页题目、复杂多栏版式或严重粘连内容可能配对不准确 +- 图片导出取决于 UniParser 是否提供可用图片数据 +- 公式和题目结构的准确性依赖原始文档解析质量 +- 抽取结果适合批量整理,但正式训练或入库前仍建议抽样检查 diff --git a/uniparser_agent/pdf2vqa/__init__.py b/uniparser_agent/pdf2vqa/__init__.py new file mode 100644 index 0000000..68a9957 --- /dev/null +++ b/uniparser_agent/pdf2vqa/__init__.py @@ -0,0 +1,6 @@ +"""pdf2vqa: UniParser pages_tree → LLM VQA extraction.""" + +from uniparser_agent.pdf2vqa.pipeline import run_vqa_pipeline + + +__all__ = ["run_vqa_pipeline"] diff --git a/uniparser_agent/pdf2vqa/image_export.py b/uniparser_agent/pdf2vqa/image_export.py new file mode 100644 index 0000000..0f3ab2a --- /dev/null +++ b/uniparser_agent/pdf2vqa/image_export.py @@ -0,0 +1,189 @@ +"""Export UniParser block ``source`` fields into a local ``vqa_images`` directory.""" + +from __future__ import annotations + +import base64 +import hashlib +import re +import shutil +from pathlib import Path +from typing import Any + + +_DATA_URL_RE = re.compile( + r"^data:image/(?P[a-zA-Z0-9.+-]+);base64,(?P.+)$", + re.DOTALL, +) + +# Types that may carry visual payloads under SCIENTIFIC_PAPER_TRIGGER. +IMAGE_SOURCE_TYPES = frozenset( + { + "figure", + "image", + "chart", + "table", + "figuregroup", + "imagegroup", + "molecule", + } +) + +_SKIP_EXPORT_TYPES = frozenset( + { + "figurecaption", + "imagecaption", + "paragraph", + "title", + "documenttitle", + "equation", + "expression", + "hline", + "pageheader", + "pagefooter", + "pagenumber", + } +) + + +def _block_key(block: dict[str, Any]) -> tuple[Any, Any]: + return (block.get("page"), block.get("block")) + + +def _ordered_dict_blocks(blocks: list[Any]) -> list[dict[str, Any]]: + return sorted( + [b for b in blocks if isinstance(b, dict)], + key=lambda b: b.get("order") if b.get("order") is not None else 10**9, + ) + + +def iter_all_blocks(pages_tree: list[Any]) -> list[dict[str, Any]]: + """Flatten pages including nested ``items`` at any depth (DFS, reading order).""" + flat: list[dict[str, Any]] = [] + + def _walk(blocks: list[Any]) -> None: + for block in _ordered_dict_blocks(blocks): + flat.append(block) + items = block.get("items") + if isinstance(items, list) and items: + _walk(items) + + for page in pages_tree: + if not isinstance(page, list): + continue + _walk(page) + return flat + + +def _guess_ext(fmt: str | None, raw: bytes) -> str: + if fmt: + fmt = fmt.lower().replace("jpeg", "jpg") + if fmt in {"jpg", "jpeg", "png", "gif", "webp", "bmp"}: + return "jpg" if fmt == "jpeg" else fmt + if raw.startswith(b"\x89PNG"): + return "png" + if raw.startswith(b"GIF8"): + return "gif" + if raw.startswith(b"RIFF") and b"WEBP" in raw[:16]: + return "webp" + return "jpg" + + +def _looks_like_filesystem_path(source: str) -> bool: + """Heuristic: avoid treating long base64 blobs as paths (OSError: name too long).""" + if len(source) >= 4096: + return False + if source.startswith(("http://", "https://", "data:")): + return False + return ("/" in source) or ("\\" in source) or bool(Path(source).suffix) + + +def decode_source_to_bytes(source: str) -> tuple[bytes, str] | None: + """Return (bytes, extension) for a block source string, or None if unsupported.""" + source = source.strip() + if not source: + return None + + match = _DATA_URL_RE.match(source) + if match: + fmt = match.group("fmt") + raw = base64.b64decode(match.group("data"), validate=False) + return raw, _guess_ext(fmt, raw) + + if _looks_like_filesystem_path(source): + path = Path(source) + try: + if path.is_file(): + raw = path.read_bytes() + return raw, path.suffix.lstrip(".").lower() or "jpg" + except OSError: + pass + + try: + raw = base64.b64decode(source, validate=False) + except Exception: + return None + if len(raw) < 32: + return None + return raw, _guess_ext(None, raw) + + +def export_images_from_pages_tree( + pages_tree_data: dict[str, Any] | list[Any], + images_dir: str | Path, +) -> dict[tuple[Any, Any], Path]: + """Decode / copy block ``source`` images into ``images_dir``. + + Returns a map from ``(page, block)`` to the written absolute path. + Duplicate content hashes share one file on disk. + """ + if isinstance(pages_tree_data, dict): + pages = pages_tree_data.get("pages_tree") + if pages is None: + raise ValueError("Invalid pages_tree data: missing 'pages_tree' key") + else: + pages = pages_tree_data + if not isinstance(pages, list): + raise ValueError(f"Expected pages_tree list, got {type(pages)}") + + out_dir = Path(images_dir).expanduser().resolve() + out_dir.mkdir(parents=True, exist_ok=True) + + hash_to_path: dict[str, Path] = {} + key_to_path: dict[tuple[Any, Any], Path] = {} + + for block in iter_all_blocks(pages): + btype = (block.get("type") or "").strip().lower() + if btype in _SKIP_EXPORT_TYPES: + continue + + source = block.get("source") + if not isinstance(source, str) or not source.strip(): + continue + + decoded = decode_source_to_bytes(source) + if decoded is None: + continue + raw, ext = decoded + digest = hashlib.sha256(raw).hexdigest() + if digest in hash_to_path: + key_to_path[_block_key(block)] = hash_to_path[digest] + continue + + filename = f"{digest}.{ext or 'jpg'}" + dest = out_dir / filename + if not dest.exists(): + copied = False + if _looks_like_filesystem_path(source): + src_path = Path(source.strip()) + try: + if src_path.is_file(): + shutil.copy2(src_path, dest) + copied = True + except OSError: + copied = False + if not copied: + dest.write_bytes(raw) + hash_to_path[digest] = dest + key_to_path[_block_key(block)] = dest + + return key_to_path diff --git a/uniparser_agent/pdf2vqa/layout_adapter.py b/uniparser_agent/pdf2vqa/layout_adapter.py new file mode 100644 index 0000000..3b5fdd5 --- /dev/null +++ b/uniparser_agent/pdf2vqa/layout_adapter.py @@ -0,0 +1,166 @@ +"""Convert UniParser pages_tree into a flat LLM content list with ids.""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from uniparser_agent.pdf2vqa.image_export import ( + IMAGE_SOURCE_TYPES, + export_images_from_pages_tree, + iter_all_blocks, +) + + +SKIP_TYPES = frozenset({"hline", "pageheader", "pagefooter", "pagenumber"}) +TEXT_TYPES = frozenset({"paragraph", "title", "documenttitle"}) +CAPTION_TYPES = frozenset({"figurecaption", "imagecaption"}) +IMAGE_TYPES = frozenset({"figure", "image", "chart", "table", "molecule", "figuregroup", "imagegroup"}) + + +def _block_text(block: dict[str, Any]) -> str: + btype = (block.get("type") or "").strip().lower() + if btype == "equation": + latex = (block.get("latex_repr") or "").strip() + if latex: + if latex.startswith("$$") or latex.startswith("$"): + return latex + return f"$$\n{latex}\n$$" + return (block.get("text") or "").strip() + return (block.get("text") or "").strip() + + +def _iter_blocks(pages_tree: list[Any]) -> list[dict[str, Any]]: + """Flatten pages into a reading-order list including nested ``items``.""" + return iter_all_blocks(pages_tree) + + +def _caption_from_group(block: dict[str, Any]) -> list[str]: + captions: list[str] = [] + items = block.get("items") + if not isinstance(items, list): + return captions + for child in items: + if not isinstance(child, dict): + continue + ctype = (child.get("type") or "").strip().lower() + if ctype in CAPTION_TYPES: + text = (child.get("text") or "").strip() + if text: + captions.append(text) + return captions + + +def _resolve_image_source_block(block: dict[str, Any]) -> dict[str, Any] | None: + """Return the block that actually holds image ``source`` (self or first child).""" + source = block.get("source") + if isinstance(source, str) and source.strip(): + return block + items = block.get("items") + if isinstance(items, list): + for child in items: + if not isinstance(child, dict): + continue + child_source = child.get("source") + if isinstance(child_source, str) and child_source.strip(): + return child + return None + + +def pages_tree_to_content_list( + pages_tree_data: dict[str, Any] | list[Any], + *, + image_path_map: dict[tuple[Any, Any], Path] | None = None, + image_prefix: str = "vqa_images", +) -> list[dict[str, Any]]: + """Adapt UniParser pages_tree envelope (or raw list) to LLM content list.""" + if isinstance(pages_tree_data, dict): + pages = pages_tree_data.get("pages_tree") + if pages is None: + raise ValueError("Invalid pages_tree data: missing 'pages_tree' key") + else: + pages = pages_tree_data + + if not isinstance(pages, list): + raise ValueError(f"Expected pages_tree list, got {type(pages)}") + + image_path_map = image_path_map or {} + content: list[dict[str, Any]] = [] + next_id = 0 + emitted_image_files: set[str] = set() + + for block in _iter_blocks(pages): + btype = (block.get("type") or "").strip().lower() + if btype in SKIP_TYPES or btype in CAPTION_TYPES: + continue + + text = _block_text(block) + + if btype == "equation": + source = block.get("source") or "" + has_source = isinstance(source, str) and bool(source.strip()) + if not text and not has_source: + continue + content.append({"id": next_id, "type": "equation", "text": text}) + next_id += 1 + continue + + if btype in IMAGE_TYPES or btype in IMAGE_SOURCE_TYPES: + source_block = _resolve_image_source_block(block) + if source_block is not None: + key = (source_block.get("page"), source_block.get("block")) + img_file = image_path_map.get(key) + if img_file is not None and img_file.is_file(): + name = img_file.name + if name not in emitted_image_files: + captions = _caption_from_group(block) + if not captions: + desc = (block.get("desc") or source_block.get("desc") or "").strip() + if desc: + captions = [desc] + content.append( + { + "id": next_id, + "type": "image", + "img_path": f"{image_prefix}/{name}", + "image_caption": captions, + } + ) + emitted_image_files.add(name) + next_id += 1 + continue + if btype in {"figuregroup", "imagegroup"}: + continue + + if btype in TEXT_TYPES or text: + if not text: + continue + content.append({"id": next_id, "type": "text", "text": text}) + next_id += 1 + continue + + return content + + +def adapt_pages_tree_file( + pages_tree_path: str | Path, + output_path: str | Path, + *, + images_dir: str | Path | None = None, +) -> list[dict[str, Any]]: + path = Path(pages_tree_path).expanduser().resolve() + data = json.loads(path.read_text(encoding="utf-8")) + + if images_dir is None: + images_dir = Path(output_path).expanduser().resolve().parent / "vqa_images" + else: + images_dir = Path(images_dir).expanduser().resolve() + + image_path_map = export_images_from_pages_tree(data, images_dir) + content = pages_tree_to_content_list(data, image_path_map=image_path_map) + + out = Path(output_path).expanduser().resolve() + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(content, ensure_ascii=False, indent=2), encoding="utf-8") + return content diff --git a/uniparser_agent/pdf2vqa/llm_client.py b/uniparser_agent/pdf2vqa/llm_client.py new file mode 100644 index 0000000..12be7b7 --- /dev/null +++ b/uniparser_agent/pdf2vqa/llm_client.py @@ -0,0 +1,65 @@ +"""VQA LLM client (OpenAI-compatible via shared llm module).""" + +from __future__ import annotations + +from typing import Any + +from uniparser_agent.llm import LLMConfig, OpenAICompatLLM, resolve_llm_config + + +class VQALLMClient: + def __init__( + self, + *, + api_key: str | None = None, + base_url: str | None = None, + model: str | None = None, + timeout: float | None = None, + max_tokens: int | None = None, + enable_thinking: bool = False, + extra_body: dict[str, Any] | None = None, + config: LLMConfig | None = None, + ) -> None: + self._llm = OpenAICompatLLM( + config=config, + api_key=api_key, + base_url=base_url, + model=model, + timeout=timeout, + max_tokens=max_tokens, + enable_thinking=enable_thinking, + extra_body=extra_body, + ) + + @property + def api_key(self) -> str: + return self._llm.api_key + + @property + def base_url(self) -> str: + return self._llm.base_url + + @property + def model(self) -> str: + return self._llm.model + + @property + def timeout(self) -> float: + return self._llm.timeout + + @property + def max_tokens(self) -> int: + return self._llm.max_tokens + + @property + def enable_thinking(self) -> bool: + return self._llm.enable_thinking + + def chat(self, *, system_prompt: str, user_content: str) -> str: + return self._llm.chat(system_prompt=system_prompt, user_content=user_content) + + def meta(self) -> dict[str, Any]: + return self._llm.meta() + + +__all__ = ["VQALLMClient", "resolve_llm_config", "LLMConfig"] diff --git a/uniparser_agent/pdf2vqa/output_parser.py b/uniparser_agent/pdf2vqa/output_parser.py new file mode 100644 index 0000000..862fd6b --- /dev/null +++ b/uniparser_agent/pdf2vqa/output_parser.py @@ -0,0 +1,78 @@ +"""Parse LLM id-based VQA responses back into text VQA items.""" + +from __future__ import annotations + +import json +import re +from pathlib import Path +from typing import Any + + +def _id_to_text(input_ids: str, content_list: list[dict[str, Any]], image_prefix: str = "vqa_images") -> str: + texts: list[str] = [] + for raw_id in input_ids.replace(" ", "").split(","): + if not raw_id: + continue + try: + idx = int(raw_id) + except ValueError: + continue + if idx < 0 or idx >= len(content_list): + continue + item = content_list[idx] + if "text" in item and item["text"]: + texts.append(str(item["text"])) + elif "table_body" in item and item["table_body"]: + texts.append(str(item["table_body"])) + elif "img_path" in item: + img_name = Path(str(item.get("img_path", ""))).name + caption = item.get("image_caption") or ["image"] + if isinstance(caption, list): + alt = " ".join(str(c) for c in caption) + else: + alt = str(caption) + texts.append(f"![{alt}]({image_prefix}/{img_name})") + return "\n".join(texts) + + +def parse_llm_response( + response: str, + content_list: list[dict[str, Any]], + *, + image_prefix: str = "vqa_images", +) -> list[dict[str, Any]]: + if "" in response and "" in response and "" not in response: + return [] + + qa_list: list[dict[str, Any]] = [] + for chapter_block in re.findall(r"(.*?)", response, flags=re.DOTALL): + title_match = re.search(r"(.*?)", chapter_block, flags=re.DOTALL) + chapter_title = _id_to_text(title_match.group(1).strip(), content_list, image_prefix) if title_match else "" + for pair in re.findall(r"(.*?)", chapter_block, flags=re.DOTALL): + q_match = re.search(r"(.*?)", pair, flags=re.DOTALL) + a_match = re.search(r"(.*?)", pair, flags=re.DOTALL) + s_match = re.search(r"(.*?)", pair, flags=re.DOTALL) + label_match = re.search(r"", pair, flags=re.DOTALL) + if not label_match: + continue + if not ((q_match and label_match) or (a_match and label_match) or (s_match and label_match)): + continue + qa_list.append( + { + "question": (_id_to_text(q_match.group(1).strip(), content_list, image_prefix) if q_match else ""), + "answer": a_match.group(1).strip() if a_match else "", + "solution": (_id_to_text(s_match.group(1).strip(), content_list, image_prefix) if s_match else ""), + "label": label_match.group(1).strip(), + "chapter_title": chapter_title, + } + ) + return qa_list + + +def write_vqa_jsonl(qa_list: list[dict[str, Any]], output_path: str | Path) -> Path: + out = Path(output_path).expanduser().resolve() + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", encoding="utf-8") as fh: + for qa in qa_list: + fh.write(json.dumps(qa, ensure_ascii=False) + "\n") + return out diff --git a/uniparser_agent/pdf2vqa/pdf_merger.py b/uniparser_agent/pdf2vqa/pdf_merger.py new file mode 100644 index 0000000..bd221b4 --- /dev/null +++ b/uniparser_agent/pdf2vqa/pdf_merger.py @@ -0,0 +1,38 @@ +"""Merge local PDFs in order (question booklet then answer booklet).""" + +from __future__ import annotations + +from pathlib import Path + + +def merge_pdfs(paths: list[str | Path], output_path: str | Path) -> Path: + """Append PDFs in order into ``output_path`` and return the resolved path.""" + from pypdf import PdfWriter + + if not paths: + raise ValueError("At least one PDF path is required.") + + resolved: list[Path] = [] + for raw in paths: + path = Path(raw).expanduser().resolve() + if not path.is_file(): + raise FileNotFoundError(f"PDF not found: {path}") + if path.suffix.lower() != ".pdf": + raise ValueError(f"Not a PDF file: {path}") + resolved.append(path) + + out = Path(output_path).expanduser().resolve() + out.parent.mkdir(parents=True, exist_ok=True) + + writer = PdfWriter() + try: + for path in resolved: + writer.append(str(path)) + if len(writer.pages) == 0: + raise ValueError("Merged PDF has no pages.") + with out.open("wb") as fh: + writer.write(fh) + finally: + writer.close() + + return out diff --git a/uniparser_agent/pdf2vqa/pipeline.py b/uniparser_agent/pdf2vqa/pipeline.py new file mode 100644 index 0000000..98b1fe4 --- /dev/null +++ b/uniparser_agent/pdf2vqa/pipeline.py @@ -0,0 +1,183 @@ +"""End-to-end VQA pipeline: UniParser parse → adapt → LLM extract → merge.""" + +from __future__ import annotations + +import json +import shutil +import time +from pathlib import Path +from typing import Any + +from uniparser_agent.llm import LLMConfig +from uniparser_agent.parse.service import load_pages_tree, parse_document +from uniparser_agent.pdf2vqa.layout_adapter import adapt_pages_tree_file +from uniparser_agent.pdf2vqa.llm_client import VQALLMClient +from uniparser_agent.pdf2vqa.output_parser import parse_llm_response, write_vqa_jsonl +from uniparser_agent.pdf2vqa.pdf_merger import merge_pdfs +from uniparser_agent.pdf2vqa.prompts import build_vqa_extract_prompt +from uniparser_agent.pdf2vqa.vqa_formatter import write_sharegpt +from uniparser_agent.pdf2vqa.vqa_merger import jsonl_to_md, merge_vqa_pairs, write_merged_jsonl + + +def _resolve_output_dir(output_dir: str | Path | None, overwrite: bool) -> Path: + if output_dir: + out = Path(output_dir).expanduser().resolve() + else: + out = (Path.cwd() / "vqa_out").resolve() + if out.exists(): + if not overwrite: + raise FileExistsError(f"Output directory already exists: {out}. Pass overwrite=True or --overwrite.") + shutil.rmtree(out) + out.mkdir(parents=True, exist_ok=True) + return out + + +def _require_local_pdf(path: str | Path, *, label: str) -> Path: + resolved = Path(path).expanduser().resolve() + if not resolved.is_file(): + raise FileNotFoundError(f"{label} not found: {resolved}") + if resolved.suffix.lower() != ".pdf": + raise ValueError(f"{label} must be a local PDF file: {resolved}") + return resolved + + +def run_vqa_pipeline( + input_path: str | None = None, + *, + answer_pdf: str | None = None, + pages_tree_path: str | None = None, + output_dir: str | None = None, + overwrite: bool = False, + strict_title_match: bool = False, + llm_config: LLMConfig | None = None, + llm_client: VQALLMClient | None = None, +) -> dict[str, Any]: + """Run pdf2vqa extraction. + + Primary path: ``input_path`` (pdf/url/image) → UniParser parse → extract. + Dual PDF: ``input_path`` (question) + ``answer_pdf`` → merge → parse → extract. + Bypass: ``pages_tree_path`` skips UniParser parse. + """ + if answer_pdf and pages_tree_path: + raise ValueError("Use either answer_pdf or pages_tree_path, not both.") + if answer_pdf and not input_path: + raise ValueError("answer_pdf requires input_path (question booklet PDF).") + if not input_path and not pages_tree_path: + raise ValueError("Provide input_path (pdf/url/image) or pages_tree_path.") + + started = time.time() + out = _resolve_output_dir(output_dir, overwrite=overwrite) + parse_dir = out / "parse" + parse_meta: dict[str, Any] = {} + merged_pdf_path: Path | None = None + + if pages_tree_path: + src_tree = Path(pages_tree_path).expanduser().resolve() + if not src_tree.is_file(): + raise FileNotFoundError(f"pages_tree not found: {src_tree}") + parse_dir.mkdir(parents=True, exist_ok=True) + dest_tree = parse_dir / "pages_tree.json" + shutil.copy2(src_tree, dest_tree) + tree_path = dest_tree + parse_meta = {"mode": "pages_tree", "pages_tree_path": str(tree_path)} + else: + assert input_path is not None + parse_source = input_path + question_pdf: Path | None = None + answer_path: Path | None = None + if answer_pdf: + question_pdf = _require_local_pdf(input_path, label="question PDF") + answer_path = _require_local_pdf(answer_pdf, label="answer PDF") + merge_dir = out / "merge" + merge_dir.mkdir(parents=True, exist_ok=True) + merged_pdf_path = merge_pdfs( + [question_pdf, answer_path], + merge_dir / "merged.pdf", + ) + parse_source = str(merged_pdf_path) + + parse_result = parse_document(parse_source, output_dir=str(parse_dir), overwrite=True) + tree_path = Path(parse_result["pages_tree_path"]) + if answer_pdf: + assert question_pdf is not None and answer_path is not None and merged_pdf_path is not None + parse_meta = { + "mode": "dual_pdf", + "question_pdf": str(question_pdf), + "answer_pdf": str(answer_path), + "merged_pdf": str(merged_pdf_path), + "token": parse_result.get("token", ""), + "pages_tree_path": parse_result["pages_tree_path"], + "markdown_path": parse_result.get("markdown_path", ""), + } + else: + parse_meta = { + "mode": "parse", + "source": input_path, + "token": parse_result.get("token", ""), + "pages_tree_path": parse_result["pages_tree_path"], + "markdown_path": parse_result.get("markdown_path", ""), + } + + load_pages_tree(tree_path) + + images_dir = out / "vqa_images" + content_list_path = out / "llm_content_list.json" + content_list = adapt_pages_tree_file( + tree_path, + content_list_path, + images_dir=images_dir, + ) + n_images = len(list(images_dir.glob("*"))) if images_dir.is_dir() else 0 + + llm = llm_client or VQALLMClient(config=llm_config) + system_prompt = build_vqa_extract_prompt() + user_content = json.dumps(content_list, ensure_ascii=False) + llm_started = time.time() + raw_response = llm.chat(system_prompt=system_prompt, user_content=user_content) + llm_elapsed = time.time() - llm_started + + raw_path = out / "llm_raw_response.txt" + raw_path.write_text(raw_response, encoding="utf-8") + + extracted = parse_llm_response(raw_response, content_list, image_prefix="vqa_images") + extracted_path = out / "extracted_vqa.jsonl" + write_vqa_jsonl(extracted, extracted_path) + + merged = merge_vqa_pairs(extracted, strict_title_match=strict_title_match) + merged_jsonl = out / "merged_vqa_pairs.jsonl" + merged_md = out / "merged_vqa_pairs.md" + write_merged_jsonl(merged, merged_jsonl) + jsonl_to_md(merged_jsonl, merged_md) + + sharegpt_path = out / "vqa_sharegpt.json" + write_sharegpt(merged, images_dir, sharegpt_path, base_dir=out) + + paths: dict[str, str] = { + "output_dir": str(out), + "pages_tree": str(tree_path), + "llm_content_list": str(content_list_path), + "llm_raw_response": str(raw_path), + "extracted_vqa": str(extracted_path), + "merged_vqa_pairs_jsonl": str(merged_jsonl), + "merged_vqa_pairs_md": str(merged_md), + "vqa_images": str(images_dir), + "vqa_sharegpt": str(sharegpt_path), + } + if merged_pdf_path is not None: + paths["merged_pdf"] = str(merged_pdf_path) + + meta = { + "parse": parse_meta, + "llm": llm.meta(), + "n_content_items": len(content_list), + "n_vqa_images": n_images, + "n_extracted": len(extracted), + "n_merged_vqa": len(merged), + "llm_elapsed_sec": round(llm_elapsed, 2), + "total_elapsed_sec": round(time.time() - started, 2), + "paths": paths, + } + meta_path = out / "run_meta.json" + meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8") + meta["paths"]["run_meta"] = str(meta_path) + return meta diff --git a/uniparser_agent/pdf2vqa/prompts.py b/uniparser_agent/pdf2vqa/prompts.py new file mode 100644 index 0000000..57a3fea --- /dev/null +++ b/uniparser_agent/pdf2vqa/prompts.py @@ -0,0 +1,70 @@ +"""VQA extraction system prompt (ported from DataFlow QAExtractPrompt).""" + +from __future__ import annotations + + +def build_vqa_extract_prompt() -> str: + return """ + You are an expert in answer college-level questions. You are given a json file. Your task is to segment the content, insert images tags, and extract labels: +1. Every json item has an "id" field. Your main task is to output this field. +2. You need to segment the content into multiple ``…`` blocks, each containing a question and its corresponding answer with solution. +3. If the problem or answer/solution is not complete, omit them. An answer/solution should be considered complete as long as either the answer or solution exists. +4. You need to put the images id into proper positions. You could look at the caption or context to decide where to put the image tags. +5. You will also need to extract the chapter title and each problem's label/number from the text. +6. You only need to output "id" field for **chapter titles, questions and solutions**. DO NOT OUTPUT ORIGINAL TEXT. Use ',' to separate different ids. +7. However, use original labels/numbers for labels, and use original numbers for answers. DO NOT output "id" field for labels and answers. You will need to extract them from the text. + +Strict extraction rules: +** About questions and answers/solutions ** +- Preserve each problem’s original label/number, such as "例1", "Example 3", "习题1", "11". Do not include the period after the number. Use Arabic numerals only. For example, if the label is "例一", convert it to "例1". If the label is "IV", convert it to "4". +- If the full label is "三、16", keep only "16". If the full label is "5.4", keep only "4". +- If there are multiple sub-questions (such as "(1)", "(a)") under one main question, always put them together in the same ``…`` block. +- If a question and its answer/solution are contiguous, wrap them together as a single ``…`` block, e.g.: + `………` +- If a question and its answer/solution are NOT contiguous (e.g. only question; only answer and/or solution; all questions at the front and all answers/solutions at the back), wrap each question or answer/solution in a ``…`` block with the missing part left empty. For example, if only questions appear: + `…` +- In total, there are 7 possibilities: only question, only answer, only solution, question with answer, question with solution, answer with solution, full question and answer and solution. +- If multiple vqa pairs appear, wrap each vqa pair in its own ``…`` block. +- If you do not see the full solution, only extract the short answer and leave the solution empty. YOU MUST KEEP SHORT ANSWERS !!! +** About chapter/section titles ** +- Always enclose vqa pairs in a ``…`` block, where MAIN_TITLE_ID is the id of the chapter title or section title. +- Normally, chapter/section titles appear before the questions/answers in an independent json item. +- There could be multiple ``…`` blocks if multiple chapters/sections exist. +- **Any title followed by a question/answer whose label/number is not 1, or title with a score such as "一、选择题(每题1分,共10分)", should NOT be extracted.** +- Do not use nested titles. +- Leave the title blank if there is no chapter title. +** About figures/diagrams ** +- Whenever the question or answer/solution refers to a figure or diagram, record its "id" in question/answer/solution just like other text content. +- You MUST include all images referenced in the question/answer/solution. + + +If no qualifying content is found, output: + + +Output format (all tags run together, no extra whitespace or newlines except between entries): +MAIN_TITLE_ID +QUESTION_IDS +ANSWER(EXTRACTED FROM SOLUTION)SOLUTION_IDS +QUESTION_IDS +ANSWER(EXTRACTED FROM SOLUTION) + +MAIN_TITLE_ID +QUESTION_IDS +ANSWER(EXTRACTED FROM SOLUTION)SOLUTION_IDS + + + +Example: +7 +2,3 +Yes5,6,7 +8,9,10 +3.14 + +12 + +2^616 + + +Please now process the provided json and output your result. +""".strip() diff --git a/uniparser_agent/pdf2vqa/vqa_formatter.py b/uniparser_agent/pdf2vqa/vqa_formatter.py new file mode 100644 index 0000000..59ac30a --- /dev/null +++ b/uniparser_agent/pdf2vqa/vqa_formatter.py @@ -0,0 +1,106 @@ +"""Convert merged VQA pairs with Markdown image refs into ShareGPT VQA format.""" + +from __future__ import annotations + +import json +import re +from pathlib import Path +from typing import Any + + +_IMAGE_PATTERN = re.compile(r"!\[.*?\]\((.*?)\)") + + +def extract_image_paths(text: str) -> list[str]: + return _IMAGE_PATTERN.findall(text or "") + + +def strip_image_tags(text: str) -> str: + cleaned = _IMAGE_PATTERN.sub("", text or "") + cleaned = re.sub(r"\n{3,}", "\n\n", cleaned).strip() + return cleaned + + +def _build_user_content(question: str, images: list[str], placeholder: str = "") -> str: + prefix = "".join(placeholder for _ in images) + question_clean = strip_image_tags(question) + return f"{prefix}{question_clean}" if prefix else question_clean + + +def _build_assistant_content(answer: str, solution: str) -> str: + ans_text = (answer or "").strip() + sol_text = strip_image_tags(solution) + if ans_text and sol_text: + return f"{ans_text}\n\n{sol_text}" + if ans_text: + return ans_text + return sol_text + + +def _index_images(images_dir: Path) -> dict[str, Path]: + index: dict[str, Path] = {} + if not images_dir.is_dir(): + return index + for path in images_dir.iterdir(): + if path.is_file(): + index[path.name] = path.resolve() + return index + + +def convert_vqa_pair_to_sharegpt( + qa: dict[str, Any], + *, + image_index: dict[str, Path], + base_dir: Path, + placeholder: str = "", +) -> dict[str, Any] | None: + question = str(qa.get("question") or "").strip() + answer = str(qa.get("answer") or "").strip() + solution = str(qa.get("solution") or "").strip() + if not question: + return None + + abs_images: list[str] = [] + for rel in extract_image_paths(question) + extract_image_paths(solution): + name = Path(rel).name + if name in image_index: + abs_images.append(str(image_index[name])) + else: + candidate = (base_dir / rel).resolve() + abs_images.append(str(candidate)) + + assistant = _build_assistant_content(answer, solution) + if not assistant: + return None + + return { + "messages": [ + {"role": "user", "content": _build_user_content(question, abs_images, placeholder)}, + {"role": "assistant", "content": assistant}, + ], + "images": abs_images, + } + + +def write_sharegpt( + merged_pairs: list[dict[str, Any]], + images_dir: str | Path, + output_json: str | Path, + *, + base_dir: str | Path | None = None, +) -> Path: + """Write ShareGPT JSON list; returns output path.""" + images_path = Path(images_dir).expanduser().resolve() + out = Path(output_json).expanduser().resolve() + out.parent.mkdir(parents=True, exist_ok=True) + root = Path(base_dir).expanduser().resolve() if base_dir else out.parent + index = _index_images(images_path) + + records: list[dict[str, Any]] = [] + for qa in merged_pairs: + item = convert_vqa_pair_to_sharegpt(qa, image_index=index, base_dir=root) + if item is not None: + records.append(item) + + out.write_text(json.dumps(records, ensure_ascii=False, indent=2), encoding="utf-8") + return out diff --git a/uniparser_agent/pdf2vqa/vqa_merger.py b/uniparser_agent/pdf2vqa/vqa_merger.py new file mode 100644 index 0000000..055eae8 --- /dev/null +++ b/uniparser_agent/pdf2vqa/vqa_merger.py @@ -0,0 +1,153 @@ +"""Merge extracted question/answer fragments into final VQA pairs.""" + +from __future__ import annotations + +import json +import re +from pathlib import Path +from typing import Any + + +def refine_title(title: str, strict_title_match: bool = False) -> str: + title = re.sub(r"\s+", "", title) + if strict_title_match: + return title + try: + return re.search(r"\d+\.\d+|\d+", title).group() # type: ignore[union-attr] + except Exception: + try: + return re.search(r"[一二三四五六七八九零十百]+", title).group() # type: ignore[union-attr] + except Exception: + return title + + +def merge_vqa_pairs( + extracted: list[dict[str, Any]], + *, + strict_title_match: bool = False, +) -> list[dict[str, Any]]: + question_list: list[dict[str, Any]] = [] + answer_list: list[dict[str, Any]] = [] + for data in extracted: + if data.get("question"): + question_list.append(dict(data)) + else: + answer_list.append(dict(data)) + + merged: list[dict[str, Any]] = [] + chapter_title = "" + label = float("inf") + questions: dict[tuple[str, int], dict[str, Any]] = {} + answers: dict[tuple[str, int], dict[str, Any]] = {} + already_complete = 0 + + for data in question_list: + label_match = re.search(r"\d+", str(data.get("label", ""))) + if label_match: + data["label"] = label_match.group() + if not data.get("chapter_title"): + data["chapter_title"] = chapter_title + try: + data["label"] = int(data["label"]) + except Exception: + continue + + if data["chapter_title"] and data["chapter_title"] != chapter_title: + if data["label"] < label: + chapter_title = data["chapter_title"] + else: + data["chapter_title"] = chapter_title + label = data["label"] + data["chapter_title"] = refine_title(data["chapter_title"], strict_title_match) + + if data["label"] <= 0: + continue + if data.get("answer") or data.get("solution"): + already_complete += 1 + merged.append( + { + "question_chapter_title": data["chapter_title"], + "answer_chapter_title": data["chapter_title"], + "label": data["label"], + "question": data["question"], + "answer": data.get("answer", ""), + "solution": data.get("solution", ""), + } + ) + else: + questions[(data["chapter_title"], data["label"])] = data + + chapter_title = "" + label = float("inf") + for data in answer_list: + label_match = re.search(r"\d+", str(data.get("label", ""))) + if label_match: + data["label"] = label_match.group() + if not data.get("chapter_title"): + data["chapter_title"] = chapter_title + try: + data["label"] = int(data["label"]) + except Exception: + continue + + if data["chapter_title"] and data["chapter_title"] != chapter_title: + if data["label"] < label: + chapter_title = data["chapter_title"] + else: + data["chapter_title"] = chapter_title + label = data["label"] + data["chapter_title"] = refine_title(data["chapter_title"], strict_title_match) + + if data["label"] <= 0: + continue + key = (data["chapter_title"], data["label"]) + existing = answers.get(key) + if not existing: + answers[key] = data + else: + if not existing.get("solution") and data.get("solution"): + existing["solution"] = data["solution"] + if not existing.get("answer") and data.get("answer"): + existing["answer"] = data["answer"] + + for key, qdata in questions.items(): + if key not in answers: + continue + adata = answers[key] + merged.append( + { + "question_chapter_title": qdata["chapter_title"], + "answer_chapter_title": adata["chapter_title"], + "label": key[1], + "question": qdata["question"], + "answer": adata.get("answer", ""), + "solution": adata.get("solution", ""), + } + ) + + return merged + + +def write_merged_jsonl(merged: list[dict[str, Any]], output_path: str | Path) -> Path: + out = Path(output_path).expanduser().resolve() + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", encoding="utf-8") as fh: + for item in merged: + fh.write(json.dumps(item, ensure_ascii=False) + "\n") + return out + + +def jsonl_to_md(jsonl_path: str | Path, md_path: str | Path) -> Path: + src = Path(jsonl_path).expanduser().resolve() + out = Path(md_path).expanduser().resolve() + out.parent.mkdir(parents=True, exist_ok=True) + with src.open("r", encoding="utf-8") as infile, out.open("w", encoding="utf-8") as outfile: + for line in infile: + data = json.loads(line) + outfile.write(f"### Question {data['label']}\n\n") + outfile.write(f"{data['question']}\n\n") + outfile.write(f"**Answer:** {data['answer']}\n\n") + if data.get("solution"): + outfile.write(f"**Solution:**\n\n{data['solution']}\n\n") + outfile.write("---\n\n") + return out diff --git a/uniparser_agent/pyproject.toml b/uniparser_agent/pyproject.toml new file mode 100644 index 0000000..f6a2622 --- /dev/null +++ b/uniparser_agent/pyproject.toml @@ -0,0 +1,39 @@ +[project] +name = "uniparser-agent" +version = "0.1.0" +description = "UniParser document parsing and exam VQA extraction" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "openai>=1.0.0", + "pypdf>=4.0.0", + "requests>=2.32.0", + "typer>=0.12.0", +] + +[project.scripts] +uniparser-agent = "uniparser_agent.cli:app" + +[project.optional-dependencies] +dev = [ + "pytest>=8.0", +] + +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.package-dir] +"uniparser_agent" = "." + +[tool.setuptools] +packages = [ + "uniparser_agent", + "uniparser_agent.parse", + "uniparser_agent.pdf2vqa", + "uniparser_agent.llm", +] + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = [".."] diff --git a/uniparser_agent/tests/test_llm_config.py b/uniparser_agent/tests/test_llm_config.py new file mode 100644 index 0000000..009536e --- /dev/null +++ b/uniparser_agent/tests/test_llm_config.py @@ -0,0 +1,117 @@ +"""Unit tests for shared LLM config resolution.""" + +from __future__ import annotations + +import pytest + +from uniparser_agent.llm.config import LLMConfig, resolve_llm_config + + +def test_resolve_from_openai_env(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("OPENAI_API_KEY", "sk-env") + monkeypatch.setenv("OPENAI_BASE_URL", "http://example.com/v1/") + monkeypatch.setenv("OPENAI_MODEL", "gpt-test") + cfg = resolve_llm_config() + assert cfg.api_key == "sk-env" + assert cfg.base_url == "http://example.com/v1" + assert cfg.model == "gpt-test" + assert cfg.enable_thinking is False + + +def test_explicit_overrides_env(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("OPENAI_API_KEY", "sk-env") + monkeypatch.setenv("OPENAI_BASE_URL", "http://env/v1") + monkeypatch.setenv("OPENAI_MODEL", "env-model") + cfg = resolve_llm_config( + api_key="sk-cli", + base_url="http://cli/v1", + model="cli-model", + enable_thinking=True, + ) + assert cfg.api_key == "sk-cli" + assert cfg.base_url == "http://cli/v1" + assert cfg.model == "cli-model" + assert cfg.enable_thinking is True + + +def test_config_object_with_partial_override(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + monkeypatch.delenv("OPENAI_BASE_URL", raising=False) + monkeypatch.delenv("OPENAI_MODEL", raising=False) + base = LLMConfig( + api_key="sk-base", + base_url="http://base/v1", + model="base-model", + ) + cfg = resolve_llm_config(config=base, model="override-model") + assert cfg.api_key == "sk-base" + assert cfg.model == "override-model" + + +def test_missing_required_raises(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + monkeypatch.delenv("OPENAI_BASE_URL", raising=False) + monkeypatch.delenv("OPENAI_MODEL", raising=False) + with pytest.raises(ValueError, match="OPENAI_API_KEY"): + resolve_llm_config() + monkeypatch.setenv("OPENAI_API_KEY", "sk") + with pytest.raises(ValueError, match="OPENAI_BASE_URL"): + resolve_llm_config() + monkeypatch.setenv("OPENAI_BASE_URL", "http://x/v1") + with pytest.raises(ValueError, match="OPENAI_MODEL"): + resolve_llm_config() + + +def test_legacy_env_names_ignored(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + monkeypatch.delenv("OPENAI_BASE_URL", raising=False) + monkeypatch.delenv("OPENAI_MODEL", raising=False) + monkeypatch.setenv("API_KEY", "legacy-key") + monkeypatch.setenv("ARK_API_KEY", "ark-key") + monkeypatch.setenv("BASE_URL", "http://legacy/v1") + monkeypatch.setenv("MODEL_NAME", "legacy-model") + with pytest.raises(ValueError, match="OPENAI_API_KEY"): + resolve_llm_config() + + +def test_qwen_extra_body_injected() -> None: + cfg = LLMConfig( + api_key="sk", + base_url="https://api.openai.com/v1", + model="qwen-test-model", + enable_thinking=False, + ) + assert cfg.resolved_extra_body() == { + "chat_template_kwargs": {"enable_thinking": False}, + } + + +def test_non_qwen_no_extra_body() -> None: + cfg = LLMConfig( + api_key="sk", + base_url="https://api.openai.com/v1", + model="gpt-4o-mini", + ) + assert cfg.resolved_extra_body() is None + + +def test_enable_thinking_forces_extra_body() -> None: + cfg = LLMConfig( + api_key="sk", + base_url="https://api.openai.com/v1", + model="gpt-4o-mini", + enable_thinking=True, + ) + assert cfg.resolved_extra_body() == { + "chat_template_kwargs": {"enable_thinking": True}, + } + + +def test_explicit_extra_body_wins() -> None: + cfg = LLMConfig( + api_key="sk", + base_url="https://api.openai.com/v1", + model="Qwen3", + extra_body={"foo": 1}, + ) + assert cfg.resolved_extra_body() == {"foo": 1} diff --git a/uniparser_agent/tests/test_pdf_merger.py b/uniparser_agent/tests/test_pdf_merger.py new file mode 100644 index 0000000..18da1d7 --- /dev/null +++ b/uniparser_agent/tests/test_pdf_merger.py @@ -0,0 +1,42 @@ +"""Tests for PDF merge helper.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +from pypdf import PdfReader, PdfWriter + +from uniparser_agent.pdf2vqa.pdf_merger import merge_pdfs + + +def _write_blank_pdf(path: Path, n_pages: int) -> Path: + writer = PdfWriter() + for _ in range(n_pages): + writer.add_blank_page(width=200, height=200) + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("wb") as fh: + writer.write(fh) + writer.close() + return path + + +def test_merge_pdfs_sums_pages(tmp_path: Path): + a = _write_blank_pdf(tmp_path / "a.pdf", 2) + b = _write_blank_pdf(tmp_path / "b.pdf", 3) + out = merge_pdfs([a, b], tmp_path / "merged.pdf") + assert out.is_file() + assert len(PdfReader(str(out)).pages) == 5 + + +def test_merge_pdfs_rejects_missing(tmp_path: Path): + a = _write_blank_pdf(tmp_path / "a.pdf", 1) + with pytest.raises(FileNotFoundError): + merge_pdfs([a, tmp_path / "missing.pdf"], tmp_path / "out.pdf") + + +def test_merge_pdfs_rejects_non_pdf(tmp_path: Path): + txt = tmp_path / "notes.txt" + txt.write_text("x", encoding="utf-8") + with pytest.raises(ValueError, match="Not a PDF"): + merge_pdfs([txt], tmp_path / "out.pdf") diff --git a/uniparser_agent/tests/test_vqa_adapter.py b/uniparser_agent/tests/test_vqa_adapter.py new file mode 100644 index 0000000..971aebb --- /dev/null +++ b/uniparser_agent/tests/test_vqa_adapter.py @@ -0,0 +1,30 @@ +"""Unit tests for UniParser → LLM content-list adapter.""" + +from __future__ import annotations + +from uniparser_agent.pdf2vqa.layout_adapter import pages_tree_to_content_list + + +def test_adapter_skips_noise_and_numbers_ids(): + data = { + "pages_tree": [ + [ + {"type": "hline", "order": 0, "text": ""}, + {"type": "pageheader", "order": 1, "text": "header"}, + {"type": "paragraph", "order": 2, "text": "Q1 text"}, + { + "type": "equation", + "order": 3, + "latex_repr": "x^2=1", + "text": "", + }, + {"type": "pagenumber", "order": 4, "text": "1"}, + ] + ] + } + content = pages_tree_to_content_list(data) + assert [c["id"] for c in content] == [0, 1] + assert content[0]["type"] == "text" + assert content[0]["text"] == "Q1 text" + assert content[1]["type"] == "equation" + assert "x^2=1" in content[1]["text"] diff --git a/uniparser_agent/tests/test_vqa_images.py b/uniparser_agent/tests/test_vqa_images.py new file mode 100644 index 0000000..8984b35 --- /dev/null +++ b/uniparser_agent/tests/test_vqa_images.py @@ -0,0 +1,198 @@ +"""Tests for VQA image export, adapter image items, and ShareGPT formatting.""" + +from __future__ import annotations + +import json +from pathlib import Path + +from uniparser_agent.pdf2vqa.image_export import export_images_from_pages_tree +from uniparser_agent.pdf2vqa.layout_adapter import adapt_pages_tree_file, pages_tree_to_content_list +from uniparser_agent.pdf2vqa.output_parser import parse_llm_response +from uniparser_agent.pdf2vqa.vqa_formatter import convert_vqa_pair_to_sharegpt, write_sharegpt + + +# Minimal valid 1x1 PNG (preferred — magic bytes detect format) +_PNG_1X1_B64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==" + + +def _tiny_image_b64() -> str: + return _PNG_1X1_B64 + + +def test_export_and_adapt_figuregroup(tmp_path: Path): + b64 = _tiny_image_b64() + tree = { + "pages_tree": [ + [ + { + "type": "figuregroup", + "page": 0, + "block": 10, + "order": 0, + "source": "", + "items": [ + { + "type": "figure", + "page": 0, + "block": 11, + "order": 0, + "source": b64, + "desc": "", + }, + { + "type": "figurecaption", + "page": 0, + "block": 12, + "order": 1, + "text": "Fig. 1 Energy diagram", + }, + ], + }, + {"type": "paragraph", "page": 0, "block": 13, "order": 1, "text": "Q1 text"}, + ] + ] + } + images_dir = tmp_path / "vqa_images" + path_map = export_images_from_pages_tree(tree, images_dir) + assert path_map + assert list(images_dir.iterdir()) + + content = pages_tree_to_content_list(tree, image_path_map=path_map) + image_items = [c for c in content if c.get("type") == "image"] + assert len(image_items) == 1 + assert image_items[0]["img_path"].startswith("vqa_images/") + assert "Fig. 1" in " ".join(image_items[0].get("image_caption") or []) + assert any(c.get("text") == "Q1 text" for c in content) + + +def test_export_deeply_nested_figure(tmp_path: Path): + """UniParser often nests figure under group → image → figuregroup.""" + b64 = _tiny_image_b64() + tree = { + "pages_tree": [ + [ + { + "type": "group", + "page": 0, + "block": 1, + "order": 0, + "source": "", + "items": [ + { + "type": "image", + "page": 0, + "block": 2, + "order": 0, + "source": "", + "items": [ + { + "type": "figuregroup", + "page": 0, + "block": 3, + "order": 0, + "source": "", + "items": [ + { + "type": "figure", + "page": 0, + "block": 4, + "order": 0, + "source": b64, + } + ], + } + ], + } + ], + } + ] + ] + } + images_dir = tmp_path / "vqa_images" + path_map = export_images_from_pages_tree(tree, images_dir) + assert len(path_map) == 1 + assert list(images_dir.iterdir()) + content = pages_tree_to_content_list(tree, image_path_map=path_map) + assert sum(1 for c in content if c.get("type") == "image") == 1 + + +def test_adapt_pages_tree_file_writes_images(tmp_path: Path): + b64 = _tiny_image_b64() + tree = { + "pages_tree": [ + [ + { + "type": "molecule", + "page": 1, + "block": 2, + "order": 0, + "source": b64, + "text": "", + } + ] + ] + } + tree_path = tmp_path / "pages_tree.json" + tree_path.write_text(json.dumps(tree), encoding="utf-8") + content_path = tmp_path / "llm_content_list.json" + content = adapt_pages_tree_file(tree_path, content_path, images_dir=tmp_path / "vqa_images") + assert (tmp_path / "vqa_images").is_dir() + assert any(c.get("type") == "image" for c in content) + + +def test_parser_and_sharegpt_image_placeholders(tmp_path: Path): + images_dir = tmp_path / "vqa_images" + images_dir.mkdir() + img = images_dir / "abc123.jpg" + img.write_bytes(b"\xff\xd8\xff" + b"\x00" * 64) + + content = [ + {"id": 0, "type": "text", "text": "What is shown?"}, + { + "id": 1, + "type": "image", + "img_path": "vqa_images/abc123.jpg", + "image_caption": ["diagram"], + }, + {"id": 2, "type": "text", "text": "It is A."}, + ] + response = ( + "" + "0,1" + "A2" + "" + ) + extracted = parse_llm_response(response, content) + assert len(extracted) == 1 + assert "![diagram](vqa_images/abc123.jpg)" in extracted[0]["question"] + + merged = [ + { + "label": 1, + "question": extracted[0]["question"], + "answer": "A", + "solution": extracted[0]["solution"], + "question_chapter_title": "", + "answer_chapter_title": "", + } + ] + out = write_sharegpt(merged, images_dir, tmp_path / "vqa_sharegpt.json", base_dir=tmp_path) + records = json.loads(out.read_text(encoding="utf-8")) + assert len(records) == 1 + user = records[0]["messages"][0]["content"] + assert user.startswith("") + assert len(records[0]["images"]) == 1 + assert user.count("") == len(records[0]["images"]) + assert "What is shown?" in user + assert "![" not in user + + +def test_sharegpt_no_images_ok(): + item = convert_vqa_pair_to_sharegpt( + {"question": "2+2?", "answer": "4", "solution": ""}, + image_index={}, + base_dir=Path("."), + ) + assert item is not None + assert item["images"] == [] + assert "" not in item["messages"][0]["content"] diff --git a/uniparser_agent/tests/test_vqa_parser.py b/uniparser_agent/tests/test_vqa_parser.py new file mode 100644 index 0000000..d62af04 --- /dev/null +++ b/uniparser_agent/tests/test_vqa_parser.py @@ -0,0 +1,71 @@ +"""Tests for LLM response parsing and VQA merge.""" + +from __future__ import annotations + +from uniparser_agent.pdf2vqa.output_parser import parse_llm_response +from uniparser_agent.pdf2vqa.vqa_merger import merge_vqa_pairs + + +def test_parse_and_merge_contiguous_qa(): + content = [ + {"id": 0, "type": "text", "text": "Chapter Title"}, + {"id": 1, "type": "text", "text": "1. What is 1+1?"}, + {"id": 2, "type": "text", "text": "A. 1 B. 2"}, + {"id": 3, "type": "text", "text": "【答案】B"}, + {"id": 4, "type": "text", "text": "【解析】1+1=2"}, + ] + response = ( + "0" + "1,2" + "B3,4" + "" + ) + extracted = parse_llm_response(response, content) + assert len(extracted) == 1 + assert "What is 1+1?" in extracted[0]["question"] + assert extracted[0]["answer"] == "B" + merged = merge_vqa_pairs(extracted) + assert len(merged) == 1 + assert merged[0]["label"] == 1 + assert "1+1=2" in merged[0]["solution"] + + +def test_merge_question_only_and_answer_only_rows(): + extracted = [ + { + "label": "1", + "chapter_title": "1.1", + "question": "What is 2+2?", + "answer": "", + "solution": "", + }, + { + "label": "2", + "chapter_title": "1.1", + "question": "What is 3+3?", + "answer": "", + "solution": "", + }, + { + "label": "1", + "chapter_title": "1.1", + "question": "", + "answer": "4", + "solution": "2+2=4", + }, + { + "label": "2", + "chapter_title": "1.1", + "question": "", + "answer": "6", + "solution": "3+3=6", + }, + ] + merged = merge_vqa_pairs(extracted) + assert len(merged) == 2 + by_label = {item["label"]: item for item in merged} + assert by_label[1]["question"] == "What is 2+2?" + assert by_label[1]["answer"] == "4" + assert "2+2=4" in by_label[1]["solution"] + assert by_label[2]["answer"] == "6" + assert "3+3=6" in by_label[2]["solution"] diff --git a/uniparser_agent/uv.lock b/uniparser_agent/uv.lock new file mode 100644 index 0000000..7ed9218 --- /dev/null +++ b/uniparser_agent/uv.lock @@ -0,0 +1,622 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.12' and sys_platform == 'darwin'", + "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, + { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "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 = "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.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jiter" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/3f/fae6cc967d120ec89e31c5418a51176d8278b3087fbb384a9176754f353c/jiter-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3", size = 309289, upload-time = "2026-06-29T13:02:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/97c6c3562c077f6247d6e6ce5c82562500b6316c0d928e97e106b7a1321a/jiter-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e", size = 315181, upload-time = "2026-06-29T13:02:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/7b/89/d8d073f8aa2667e46c6c0873f86fe4a512bba4293cc730f626a076211a62/jiter-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037", size = 340939, upload-time = "2026-06-29T13:02:55.412Z" }, + { url = "https://files.pythonhosted.org/packages/87/c9/db4fda3ed73fb864139305e935e5b8b38a5a24692a5a9dd356c22f1b9c8d/jiter-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e", size = 364932, upload-time = "2026-06-29T13:02:57.28Z" }, + { url = "https://files.pythonhosted.org/packages/a2/74/52b5e86241057f52ddd7c9a580f90effb51f9d06239f6fc612279b91a838/jiter-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b", size = 461132, upload-time = "2026-06-29T13:02:58.994Z" }, + { url = "https://files.pythonhosted.org/packages/a9/87/544a700f7447c1f31c5d7833821a4daa5683165c2d5a094fbf5b5800c3dc/jiter-0.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c", size = 374857, upload-time = "2026-06-29T13:03:00.455Z" }, + { url = "https://files.pythonhosted.org/packages/40/cd/0fcc3f7d39183674d5bfa9ec640faaeb506c60be7c8f94625dfba366e37c/jiter-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee", size = 347053, upload-time = "2026-06-29T13:03:02.045Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/c7e64e7932ad597fa395b61440b249ada6366716e25c6e08dd2afbd021e6/jiter-0.16.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03", size = 356153, upload-time = "2026-06-29T13:03:03.706Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/1c719044f14da814e1a060191ab19b96f3e99207bc5b4bfc6d6be34b3f80/jiter-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea", size = 393956, upload-time = "2026-06-29T13:03:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/3b/dc/7b2f303a2847207e265503853a2d964a55354cffd62a5f2936c155486798/jiter-0.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91", size = 521081, upload-time = "2026-06-29T13:03:06.886Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5f/501cf6e1e09caeb420195179ffc6f62aca603f1220ec53fd80d0d70b3e56/jiter-0.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3", size = 552085, upload-time = "2026-06-29T13:03:08.339Z" }, + { url = "https://files.pythonhosted.org/packages/79/54/aa5be86520113b79455c3877f3d1f07a348098df4083ba3688e9537e52dd/jiter-0.16.0-cp311-cp311-win32.whl", hash = "sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7", size = 206755, upload-time = "2026-06-29T13:03:09.653Z" }, + { url = "https://files.pythonhosted.org/packages/64/ec/2feb893eb330bd69b413866f4d5daada33c3962f1c6f270c91ca2d87fdf9/jiter-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1", size = 199155, upload-time = "2026-06-29T13:03:10.979Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9c/ca040d94415048a3666fc237774df8151c96f8d2b661cbe3b184acc95876/jiter-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056", size = 194403, upload-time = "2026-06-29T13:03:12.341Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943, upload-time = "2026-06-29T13:03:14.035Z" }, + { url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779, upload-time = "2026-06-29T13:03:15.418Z" }, + { url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826, upload-time = "2026-06-29T13:03:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573, upload-time = "2026-06-29T13:03:18.781Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979, upload-time = "2026-06-29T13:03:20.293Z" }, + { url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302, upload-time = "2026-06-29T13:03:21.739Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805, upload-time = "2026-06-29T13:03:23.384Z" }, + { url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107, upload-time = "2026-06-29T13:03:24.815Z" }, + { url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441, upload-time = "2026-06-29T13:03:26.266Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354, upload-time = "2026-06-29T13:03:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880, upload-time = "2026-06-29T13:03:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473, upload-time = "2026-06-29T13:03:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905, upload-time = "2026-06-29T13:03:32.472Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618, upload-time = "2026-06-29T13:03:34.672Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" }, + { url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" }, + { url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" }, + { url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" }, + { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" }, + { url = "https://files.pythonhosted.org/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239, upload-time = "2026-06-29T13:03:57.205Z" }, + { url = "https://files.pythonhosted.org/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928, upload-time = "2026-06-29T13:03:58.643Z" }, + { url = "https://files.pythonhosted.org/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998, upload-time = "2026-06-29T13:04:00.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112, upload-time = "2026-06-29T13:04:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807, upload-time = "2026-06-29T13:04:03.214Z" }, + { url = "https://files.pythonhosted.org/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181, upload-time = "2026-06-29T13:04:04.629Z" }, + { url = "https://files.pythonhosted.org/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927, upload-time = "2026-06-29T13:04:06.067Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754, upload-time = "2026-06-29T13:04:07.477Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553, upload-time = "2026-06-29T13:04:08.92Z" }, + { url = "https://files.pythonhosted.org/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900, upload-time = "2026-06-29T13:04:10.407Z" }, + { url = "https://files.pythonhosted.org/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754, upload-time = "2026-06-29T13:04:12.046Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381, upload-time = "2026-06-29T13:04:13.413Z" }, + { url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578, upload-time = "2026-06-29T13:04:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154, upload-time = "2026-06-29T13:04:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458, upload-time = "2026-06-29T13:04:17.707Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739, upload-time = "2026-06-29T13:04:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911, upload-time = "2026-06-29T13:04:21.257Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747, upload-time = "2026-06-29T13:04:22.677Z" }, + { url = "https://files.pythonhosted.org/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225, upload-time = "2026-06-29T13:04:24.441Z" }, + { url = "https://files.pythonhosted.org/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169, upload-time = "2026-06-29T13:04:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332, upload-time = "2026-06-29T13:04:27.302Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377, upload-time = "2026-06-29T13:04:28.731Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746, upload-time = "2026-06-29T13:04:30.319Z" }, + { url = "https://files.pythonhosted.org/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292, upload-time = "2026-06-29T13:04:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259, upload-time = "2026-06-29T13:04:33.721Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" }, + { url = "https://files.pythonhosted.org/packages/06/d3/8e278946d43eeca2585b4dd0834a887cd71136329b837f3a16ed86a8b4b0/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad", size = 304518, upload-time = "2026-06-29T13:05:00.172Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/28d4ef495028bf0506a413d4db3f4eb3e7288a382e0f065f306a17bbeb5e/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f", size = 310207, upload-time = "2026-06-29T13:05:02.123Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ca/c366b1012da1d640de975d9683acd44e4d150d9068845d0ca2610435253f/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5", size = 342771, upload-time = "2026-06-29T13:05:03.55Z" }, + { url = "https://files.pythonhosted.org/packages/16/52/50cc4056fc1ae02e7154704e7ecc89df0afb8300222cfe8a52d3f67e4730/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85", size = 346468, upload-time = "2026-06-29T13:05:05.452Z" }, + { url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106, upload-time = "2026-06-29T13:05:07.118Z" }, + { url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658, upload-time = "2026-06-29T13:05:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719, upload-time = "2026-06-29T13:05:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "openai" +version = "2.46.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/af/ac/f725c4efbda8657d02be684607e5a2e5ce362e4790fdbcbdfb7c15018647/openai-2.46.0.tar.gz", hash = "sha256:0421e0735ac41451cad894af4cddf0435bfbf8cbc538ac0e15b3c062f2ddc06a", size = 1114628, upload-time = "2026-07-17T02:48:06.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/7b/206238ebcb50b235942b1c66dba4974776f2057402a8d91c399be587d66a/openai-2.46.0-py3-none-any.whl", hash = "sha256:672381db55efb3a1e2610f29304c130cccdd0b319bace4d492b2443cb64c1e7c", size = 1637556, upload-time = "2026-07-17T02:48:03.695Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +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/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pypdf" +version = "6.14.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/72/7dfd5ff1c9c37de97a731701f51af091325f123d9d4270361c9c69e4431f/pypdf-6.14.2.tar.gz", hash = "sha256:7873f502fe4385e79539b21d872392dc0c4e3714327c15881cbc7fbfd1f95b25", size = 6491182, upload-time = "2026-06-23T14:18:30.859Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/e6/136aa8993a2ae7214e0b0ef2edaa0d2e08d1d4e4982635b08a835ff31ec8/pypdf-6.14.2-py3-none-any.whl", hash = "sha256:3f07891af76dc002657e04993ab9b4de81de29f9013b9761d0b7968bff12e946", size = 349514, upload-time = "2026-06-23T14:18:28.867Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "tqdm" +version = "4.69.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/69/40407dfc835517f058b603dbf37a6df094d8582b015a51eddc988febbcb7/tqdm-4.69.0.tar.gz", hash = "sha256:700c5e85dcd5f009dd6222588a29180a193a748247a5d855b4d67db93d79a53b", size = 792569, upload-time = "2026-07-17T18:09:06.2Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/21/99a0cdaf54eb35e77623c41b5a2c9472ee4404bba687052791fe2aba6773/tqdm-4.69.0-py3-none-any.whl", hash = "sha256:9979978912be667a6ef21fd5d8abf54e324e63d82f7f43c360792ebc2bc4e622", size = 676680, upload-time = "2026-07-17T18:09:04.172Z" }, +] + +[[package]] +name = "typer" +version = "0.26.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/f7/68adc395201b20b872d68e975386832e8005ffeacedd43a1d837a32815be/typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e", size = 202097, upload-time = "2026-06-26T09:22:45.705Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/87/b9fd69c92c6102a066e1b86a35243f53e70bd4c709f2a26d9f4fee4f4dc0/typer-0.26.8-py3-none-any.whl", hash = "sha256:3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c", size = 122564, upload-time = "2026-06-26T09:22:44.72Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[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 = "uniparser-agent" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "openai" }, + { name = "pypdf" }, + { name = "requests" }, + { name = "typer" }, +] + +[package.optional-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "openai", specifier = ">=1.0.0" }, + { name = "pypdf", specifier = ">=4.0.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, + { name = "requests", specifier = ">=2.32.0" }, + { name = "typer", specifier = ">=0.12.0" }, +] +provides-extras = ["dev"] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +]