chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
# OpenDataLoader PDF MCP Server
|
||||
|
||||
MCP (Model Context Protocol) server for [OpenDataLoader PDF](https://github.com/opendataloader-project/opendataloader-pdf).
|
||||
|
||||
Enables AI agents to convert PDFs to Markdown, JSON, HTML, and more via MCP.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Java 11+
|
||||
- Python 3.10+
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install opendataloader-pdf-mcp
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Claude Desktop
|
||||
|
||||
Add to your Claude Desktop config (`claude_desktop_config.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"opendataloader-pdf": {
|
||||
"command": "uvx",
|
||||
"args": ["opendataloader-pdf-mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Claude Code
|
||||
|
||||
```bash
|
||||
claude mcp add opendataloader-pdf -- uvx opendataloader-pdf-mcp
|
||||
```
|
||||
|
||||
### OpenAI Codex
|
||||
|
||||
```bash
|
||||
codex --mcp-config mcp.json
|
||||
```
|
||||
|
||||
`mcp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"opendataloader-pdf": {
|
||||
"command": "uvx",
|
||||
"args": ["opendataloader-pdf-mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cursor
|
||||
|
||||
Add to `.cursor/mcp.json` in your project:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"opendataloader-pdf": {
|
||||
"command": "uvx",
|
||||
"args": ["opendataloader-pdf-mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Windsurf
|
||||
|
||||
Add to `~/.codeium/windsurf/mcp_config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"opendataloader-pdf": {
|
||||
"command": "uvx",
|
||||
"args": ["opendataloader-pdf-mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Other MCP Clients
|
||||
|
||||
Any MCP-compatible client can use this server. The command is:
|
||||
|
||||
```bash
|
||||
uvx opendataloader-pdf-mcp
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
### convert_pdf
|
||||
|
||||
Convert a PDF file to the specified format.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `input_path` (required): Path to the input PDF file
|
||||
- `format`: Output format — `json`, `text`, `html`, `markdown` (default), `markdown-with-html`, `markdown-with-images`
|
||||
- `pages`: Pages to extract (e.g., `"1,3,5-7"`)
|
||||
- `password`: Password for encrypted PDFs
|
||||
- All other [OpenDataLoader PDF options](https://opendataloader.org/docs/options) are supported
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
@@ -0,0 +1,21 @@
|
||||
[project]
|
||||
name = "opendataloader-pdf-mcp"
|
||||
version = "0.0.0"
|
||||
description = "MCP server for OpenDataLoader PDF — convert PDFs to Markdown, JSON, HTML, and more"
|
||||
readme = "README.md"
|
||||
license = "Apache-2.0"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"mcp[cli]>=1.27.0,<2",
|
||||
"opendataloader-pdf",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
opendataloader-pdf-mcp = "opendataloader_pdf_mcp.server:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/opendataloader_pdf_mcp"]
|
||||
@@ -0,0 +1 @@
|
||||
"""MCP server for OpenDataLoader PDF."""
|
||||
@@ -0,0 +1,180 @@
|
||||
"""MCP server for OpenDataLoader PDF."""
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
import opendataloader_pdf
|
||||
|
||||
mcp = FastMCP("opendataloader-pdf")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def convert_pdf(
|
||||
input_path: str,
|
||||
format: str = "markdown",
|
||||
password: str | None = None,
|
||||
pages: str | None = None,
|
||||
keep_line_breaks: bool = False,
|
||||
sanitize: bool = False,
|
||||
content_safety_off: str | None = None,
|
||||
replace_invalid_chars: str | None = None,
|
||||
use_struct_tree: bool = False,
|
||||
table_method: str | None = None,
|
||||
reading_order: str | None = None,
|
||||
markdown_page_separator: str | None = None,
|
||||
text_page_separator: str | None = None,
|
||||
html_page_separator: str | None = None,
|
||||
image_output: str | None = None,
|
||||
image_format: str | None = None,
|
||||
include_header_footer: bool = False,
|
||||
detect_strikethrough: bool = False,
|
||||
hybrid: str | None = None,
|
||||
hybrid_mode: str | None = None,
|
||||
hybrid_url: str | None = None,
|
||||
hybrid_timeout: str | None = None,
|
||||
hybrid_fallback: bool = False,
|
||||
image_dir: str | None = None,
|
||||
) -> str:
|
||||
"""Convert a PDF file to the specified format.
|
||||
|
||||
Args:
|
||||
input_path: Path to the input PDF file.
|
||||
format: Output format. Values: json, text, html, markdown,
|
||||
markdown-with-html, markdown-with-images. Default: markdown.
|
||||
password: Password for encrypted PDF files.
|
||||
pages: Pages to extract (e.g., "1,3,5-7"). Default: all pages.
|
||||
keep_line_breaks: Preserve original line breaks in extracted text.
|
||||
sanitize: Replace emails, phone numbers, IPs, credit cards, URLs with placeholders.
|
||||
content_safety_off: Disable content safety filters.
|
||||
Values: all, hidden-text, off-page, tiny, hidden-ocg.
|
||||
replace_invalid_chars: Replacement character for invalid/unrecognized characters.
|
||||
use_struct_tree: Use PDF structure tree for reading order and semantic structure.
|
||||
table_method: Table detection method. Values: default, cluster.
|
||||
reading_order: Reading order algorithm. Values: off, xycut.
|
||||
markdown_page_separator: Separator between pages in Markdown output.
|
||||
Use %page-number% for page numbers.
|
||||
text_page_separator: Separator between pages in text output.
|
||||
html_page_separator: Separator between pages in HTML output.
|
||||
image_output: Image output mode. Values: off, embedded, external.
|
||||
image_format: Image format. Values: png, jpeg.
|
||||
include_header_footer: Include page headers and footers in output.
|
||||
detect_strikethrough: Detect strikethrough text (experimental).
|
||||
hybrid: Hybrid backend. Values: off, docling-fast.
|
||||
hybrid_mode: Hybrid triage mode. Values: auto, full.
|
||||
hybrid_url: Hybrid backend server URL.
|
||||
hybrid_timeout: Hybrid backend timeout in milliseconds.
|
||||
hybrid_fallback: Enable Java fallback on hybrid backend error.
|
||||
image_dir: Directory path to save extracted images.
|
||||
|
||||
Returns:
|
||||
The converted content as text.
|
||||
"""
|
||||
input_file = Path(input_path).expanduser().resolve()
|
||||
if not input_file.is_file():
|
||||
raise FileNotFoundError(f"Input file not found: {input_path}")
|
||||
|
||||
# Determine output file extension from format
|
||||
ext_map = {
|
||||
"json": ".json",
|
||||
"text": ".txt",
|
||||
"html": ".html",
|
||||
"markdown": ".md",
|
||||
"markdown-with-html": ".md",
|
||||
"markdown-with-images": ".md",
|
||||
}
|
||||
if format not in ext_map:
|
||||
raise ValueError(
|
||||
f"Unsupported format: {format!r}. "
|
||||
f"Supported formats: {', '.join(ext_map)}"
|
||||
)
|
||||
ext = ext_map[format]
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
kwargs: dict[str, Any] = {
|
||||
"input_path": str(input_file),
|
||||
"output_dir": tmp_dir,
|
||||
"format": format,
|
||||
"quiet": True,
|
||||
}
|
||||
|
||||
# Only pass non-default values
|
||||
if password is not None:
|
||||
kwargs["password"] = password
|
||||
if pages is not None:
|
||||
kwargs["pages"] = pages
|
||||
if keep_line_breaks:
|
||||
kwargs["keep_line_breaks"] = True
|
||||
if sanitize:
|
||||
kwargs["sanitize"] = True
|
||||
if content_safety_off is not None:
|
||||
kwargs["content_safety_off"] = content_safety_off
|
||||
if replace_invalid_chars is not None:
|
||||
kwargs["replace_invalid_chars"] = replace_invalid_chars
|
||||
if use_struct_tree:
|
||||
kwargs["use_struct_tree"] = True
|
||||
if table_method is not None:
|
||||
kwargs["table_method"] = table_method
|
||||
if reading_order is not None:
|
||||
kwargs["reading_order"] = reading_order
|
||||
if markdown_page_separator is not None:
|
||||
kwargs["markdown_page_separator"] = markdown_page_separator
|
||||
if text_page_separator is not None:
|
||||
kwargs["text_page_separator"] = text_page_separator
|
||||
if html_page_separator is not None:
|
||||
kwargs["html_page_separator"] = html_page_separator
|
||||
if format == "markdown-with-images" and image_output is None:
|
||||
# Force embedded images so they survive the temp directory cleanup
|
||||
kwargs["image_output"] = "embedded"
|
||||
elif image_output is not None:
|
||||
kwargs["image_output"] = image_output
|
||||
if image_format is not None:
|
||||
kwargs["image_format"] = image_format
|
||||
if include_header_footer:
|
||||
kwargs["include_header_footer"] = True
|
||||
if detect_strikethrough:
|
||||
kwargs["detect_strikethrough"] = True
|
||||
if hybrid is not None:
|
||||
kwargs["hybrid"] = hybrid
|
||||
if hybrid_mode is not None:
|
||||
kwargs["hybrid_mode"] = hybrid_mode
|
||||
if hybrid_url is not None:
|
||||
kwargs["hybrid_url"] = hybrid_url
|
||||
if hybrid_timeout is not None:
|
||||
kwargs["hybrid_timeout"] = hybrid_timeout
|
||||
if hybrid_fallback:
|
||||
kwargs["hybrid_fallback"] = True
|
||||
if image_dir is not None:
|
||||
kwargs["image_dir"] = image_dir
|
||||
|
||||
opendataloader_pdf.convert(**kwargs)
|
||||
|
||||
# Find and read the output file
|
||||
stem = input_file.stem
|
||||
output_file = Path(tmp_dir) / f"{stem}{ext}"
|
||||
|
||||
if not output_file.is_file():
|
||||
files = [f for f in Path(tmp_dir).iterdir() if f.is_file()]
|
||||
if not files:
|
||||
raise RuntimeError(
|
||||
"Conversion completed but no output file was generated."
|
||||
)
|
||||
matching_ext = sorted(f for f in files if f.suffix == ext)
|
||||
if not matching_ext:
|
||||
raise RuntimeError(
|
||||
f"Conversion completed but no '{ext}' output file was generated."
|
||||
)
|
||||
output_file = matching_ext[0]
|
||||
|
||||
return output_file.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def main():
|
||||
"""Run the MCP server."""
|
||||
mcp.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,17 @@
|
||||
"""Shared test fixtures for opendataloader-pdf-mcp tests."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def input_pdf():
|
||||
"""Return path to the sample lorem PDF."""
|
||||
return Path(__file__).resolve().parents[3] / "samples" / "pdf" / "lorem.pdf"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def input_pdf_academic():
|
||||
"""Return path to the sample academic PDF."""
|
||||
return Path(__file__).resolve().parents[3] / "samples" / "pdf" / "1901.03003.pdf"
|
||||
@@ -0,0 +1,95 @@
|
||||
"""Tests for the convert_pdf MCP tool."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
|
||||
from opendataloader_pdf_mcp.server import convert_pdf, mcp
|
||||
|
||||
|
||||
class TestConvertPdfValidation:
|
||||
"""Tests for input validation."""
|
||||
|
||||
def test_nonexistent_file_raises_error(self):
|
||||
"""Should raise FileNotFoundError for missing files."""
|
||||
with pytest.raises(FileNotFoundError):
|
||||
convert_pdf(input_path="/nonexistent/file.pdf")
|
||||
|
||||
def test_directory_raises_error(self, tmp_path):
|
||||
"""Should raise FileNotFoundError for directories."""
|
||||
with pytest.raises(FileNotFoundError):
|
||||
convert_pdf(input_path=str(tmp_path))
|
||||
|
||||
def test_unsupported_format_raises_error(self, input_pdf):
|
||||
"""Should raise ValueError for unsupported formats."""
|
||||
with pytest.raises(ValueError, match="Unsupported format"):
|
||||
convert_pdf(input_path=str(input_pdf), format="docx")
|
||||
|
||||
|
||||
class TestConvertPdfFormats:
|
||||
"""Tests for output format support."""
|
||||
|
||||
def test_markdown_output(self, input_pdf):
|
||||
"""Should convert PDF to Markdown."""
|
||||
result = convert_pdf(input_path=str(input_pdf), format="markdown")
|
||||
assert len(result) > 0
|
||||
assert "Lorem" in result
|
||||
|
||||
def test_json_output(self, input_pdf):
|
||||
"""Should convert PDF to JSON."""
|
||||
import json
|
||||
|
||||
result = convert_pdf(input_path=str(input_pdf), format="json")
|
||||
parsed = json.loads(result)
|
||||
assert isinstance(parsed, (dict, list))
|
||||
|
||||
def test_html_output(self, input_pdf):
|
||||
"""Should convert PDF to HTML."""
|
||||
result = convert_pdf(input_path=str(input_pdf), format="html")
|
||||
assert "<" in result
|
||||
assert ">" in result
|
||||
|
||||
def test_text_output(self, input_pdf):
|
||||
"""Should convert PDF to plain text."""
|
||||
result = convert_pdf(input_path=str(input_pdf), format="text")
|
||||
assert len(result) > 0
|
||||
assert "Lorem" in result
|
||||
|
||||
def test_default_format_is_markdown(self, input_pdf):
|
||||
"""Default format should be markdown."""
|
||||
result = convert_pdf(input_path=str(input_pdf))
|
||||
assert "Lorem" in result
|
||||
|
||||
|
||||
class TestConvertPdfOptions:
|
||||
"""Tests for optional parameters passed through to convert()."""
|
||||
|
||||
def test_pages_option(self, input_pdf_academic):
|
||||
"""Should extract only specified pages."""
|
||||
result_all = convert_pdf(input_path=str(input_pdf_academic), format="text")
|
||||
result_page1 = convert_pdf(input_path=str(input_pdf_academic), format="text", pages="1")
|
||||
assert len(result_page1) < len(result_all)
|
||||
|
||||
def test_quiet_is_always_true(self, input_pdf, tmp_path):
|
||||
"""convert() should always be called with quiet=True."""
|
||||
fake_output = tmp_path / "lorem.md"
|
||||
fake_output.write_text("mocked")
|
||||
|
||||
with patch("opendataloader_pdf_mcp.server.opendataloader_pdf.convert") as mock_convert, \
|
||||
patch("opendataloader_pdf_mcp.server.tempfile.TemporaryDirectory") as mock_tmpdir:
|
||||
mock_tmpdir.return_value.__enter__ = lambda self: str(tmp_path)
|
||||
mock_tmpdir.return_value.__exit__ = lambda *args: None
|
||||
result = convert_pdf(input_path=str(input_pdf))
|
||||
mock_convert.assert_called_once()
|
||||
kwargs = mock_convert.call_args[1]
|
||||
assert kwargs["quiet"] is True
|
||||
|
||||
|
||||
class TestMcpToolRegistration:
|
||||
"""Tests for MCP server tool registration."""
|
||||
|
||||
def test_convert_pdf_tool_is_registered(self):
|
||||
"""convert_pdf should be registered as an MCP tool."""
|
||||
# FastMCP does not expose a public tool-listing API as of v1.x;
|
||||
# _tool_manager is the only way to introspect registered tools.
|
||||
tool_names = [tool.name for tool in mcp._tool_manager.list_tools()]
|
||||
assert "convert_pdf" in tool_names
|
||||
Generated
+840
@@ -0,0 +1,840 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-doc"
|
||||
version = "0.0.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||
{ name = "idna" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "26.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.4.22"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "48.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/12/45/870e7f4bef50e5f53b9f51d4428aee5290eedf58ba443f16b1ebb7ab8e66/cryptography-48.0.1.tar.gz", hash = "sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a", size = 832989, upload-time = "2026-06-09T22:32:31.8Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/bc/ee4137cbbe105652c0ee4252792b78fc8e7afa4b8e61d9d5dc05a7f45731/cryptography-48.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3e4a1a3232eef2e6c732827d5722db29a0cc8b27af2a4d865b094cf954be9ca1", size = 8008324, upload-time = "2026-06-09T22:31:00.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/85/6379d42181bfc713094f081360fc5784d6c816b599d45e7f082502d173ce/cryptography-48.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32143b24adb918f078134e1e230f1eb8cc04886b92c28b5f0041aaf3e5699225", size = 4696243, upload-time = "2026-06-09T22:32:33.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/87/c85d147b53323c7eb4d850920c8901377323c2a0ff8d79c262d4fee89aa2/cryptography-48.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0d27a5696721ef7a672b8c810f6aded391058e0b9486e63e6d93baf765da691", size = 4713235, upload-time = "2026-06-09T22:31:40.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/58/67cbf8cf1ee7c54b439ca07bbecf8362c07afc11a3724fea70f745784add/cryptography-48.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb86ce1af36fe65041b6db9a8bb064ee621a7e5fded0f80d475ec243477cd242", size = 4702323, upload-time = "2026-06-09T22:31:42.191Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c6/24266ac10c47f6cd2a865f4446062b466da1d1f10b27189eac00e61bf0c9/cryptography-48.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b024e784ad6c077ee0147b35ea9cbfc1e34e1fd4c1dcca214c2794d73a12df08", size = 5300085, upload-time = "2026-06-09T22:31:58.703Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/bb/cc4b78784f97efc8c5874c2a9743708d172be6663024b34a0467885ae0c8/cryptography-48.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3752f2dbc8f07a30aad2932c986cea495b03bb554887828225da104f732852b6", size = 4746137, upload-time = "2026-06-09T22:31:31.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/52/0c44de3f5267f8fbe8e835138017522a333436166e406f0db9b9e6e3033f/cryptography-48.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:bd81490cd5801d755cf97bb68ac191f14b708470b1c7cf4580f669b9c9264cd8", size = 4333867, upload-time = "2026-06-09T22:32:28.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/2e/772d7adbfa931537bc401640b7cac9976bff689bda187833e5d63b428e49/cryptography-48.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:66fd0771e7b9c6dcd44cf1120690d2338d16d72795cf40cae2786a39eba65429", size = 4701805, upload-time = "2026-06-09T22:31:38.284Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/a3/b06844f303873493c963caf581c04df31c7035e0c1b0f02c4814d319ec80/cryptography-48.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:3fd2ca57062b241c856670b073487d2e86c4637937ca5601e48f97bf8e11fc8f", size = 5258461, upload-time = "2026-06-09T22:31:04.187Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/13/8b765e2e12b07c74941caadb9d1c8fdc006c4dfbf2b8f2d610519758954d/cryptography-48.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:0ee6ea481db1ab889cba043ec1eda17bb9c1ea79db6722f779c3667f9f70322f", size = 4745488, upload-time = "2026-06-09T22:32:30.07Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/aa/48972bce55049b32a94f4907eda4d75fa385aad8a39506cc2fc72196ecf0/cryptography-48.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f2ceef93cb096aa3c4cc4b5c94ca6131f9196d28c64d6111533402a9b2054d41", size = 4830256, upload-time = "2026-06-09T22:31:43.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/a2/e5079a032fb85cf6005046ca92bbd78b0c82dad2b5751ab8c311659da06f/cryptography-48.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6", size = 4979117, upload-time = "2026-06-09T22:31:05.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/a0/8f50cae9c74e718ed769d63ed5c74bd0ea830c9550a74629cebd1b9c7bc7/cryptography-48.0.1-cp311-abi3-win32.whl", hash = "sha256:b9a32b876490d66c8bcc9963ef220199569748434ab01a9d6aaeabf88e7f5158", size = 3304154, upload-time = "2026-06-09T22:32:16.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/69/0572c77dbace6fef72f33755bd52ea399c71367250d366237f8691826b9e/cryptography-48.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:39489bfca54c7a1f6b297efcd8bc608ab92d16c4ca631b0cad4da46724588b24", size = 3817138, upload-time = "2026-06-09T22:32:00.388Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/06/3e768b4c3bc78201583fa35a0e18f640dd782ff41afba88f8545481a8874/cryptography-48.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:f817adc181390bd54f2f700107a7419040fb7c1bdf2fc26f36551a06a68c3345", size = 7989830, upload-time = "2026-06-09T22:31:07.8Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/13/6476736484b94041110c8340a3eb63962fea4975baea8cb4a512adb44d4d/cryptography-48.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d5d30989c6917b478b5817902e85fddaea2261efa8648383d965381ccb9e1ac4", size = 4689201, upload-time = "2026-06-09T22:31:09.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/62/65a87f34d2a431546e2509b85d55e8c90df86d668f6731da64d538512ac2/cryptography-48.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:df637c05205ea7c1d7fbcbe54bbfea648a52951155f997af13d895d0ecc96991", size = 4702822, upload-time = "2026-06-09T22:32:24.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/59/810b5204b0a9b10f4b6bc06bd551a8b609803cd931806bc3b71884b225e5/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:869c3b8a53bfe27147832df48b32adadf558249d50e76cb3769d40e986b13265", size = 4694875, upload-time = "2026-06-09T22:32:08.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/dc/d8ca05ffea724eec6d232ea6f18e74c269eb6bdfdcc9bfba689790d1325f/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:e361afba8918070d376df76f408a4f67fec0ee9cff81a99e48fe9a233ef59e17", size = 5290385, upload-time = "2026-06-09T22:31:15.212Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/8c/3be6cb4da181f5bb6c19cf560c2359d60644a6b5fc5b57854e528f47b296/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d069066deead00ac7f090be101be875a06855908f7ec004c27b8fefb4acfb411", size = 4737082, upload-time = "2026-06-09T22:32:22.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/f6/d5f60a5a1434dbfd949e227fd0065d194c7e6b6ac526b17f5c06152b8231/cryptography-48.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:09f73a725d582cef64b91281a322cd798d14a33b2b6f2b7ad9531dc336d84c02", size = 4325328, upload-time = "2026-06-09T22:32:10.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/b7/ba75dd947a14b6ad907b01ae8f6b5b348cdd1b48142f0063dee9e20c1d9d/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:15254441469dd6bf027039453288e2072124f8b6603563f5d759e1c9b69273fa", size = 4694530, upload-time = "2026-06-09T22:31:53.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/29/50d6b9e8aff12d8b67afaeb3569335e32dc83a5723e3bbded24fdac9f809/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:8ace4507d1e6533c125f4fac754f8bb8b6a74c08e92179dabd7e16571a3efbf3", size = 5245046, upload-time = "2026-06-09T22:31:25.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/04/618f4115cfc0add0838c82507aa18a346089428da8653ad38b3ff36f5cb3/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b4e391975f038e66432328639620a4aff2d307513b004f1ca06d6225bced815c", size = 4736660, upload-time = "2026-06-09T22:32:12.676Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/9c/06e062462a0de28a3b3911322eded4c16deb9f441b1b7575d3dc59488ab5/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42fcd8e26fe555d9b3577a135f5091fefa0aa4e99129c23fb56787a1bd4ada72", size = 4822229, upload-time = "2026-06-09T22:31:17.062Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/be/0561971eaaee4b8a0e7d5113c536921063ab91aaf23278ac374eaf881e11/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1400da5e32a43253392277eac7490a60e497d810a63dd5608d71bbd7af507c9", size = 4966364, upload-time = "2026-06-09T22:31:32.842Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/27/728c77876f12b000820b69ae490f3c4083775e79e07827e9e60be07ad209/cryptography-48.0.1-cp314-cp314t-win32.whl", hash = "sha256:0df56b056bc17c1b7d6821dfa65216e62bd232d8ab05eb3db44e71d235651471", size = 3278498, upload-time = "2026-06-09T22:31:29.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/e3/79a612c6d7b1e6ee0edd43633d53035bec2cfb78c82b76f7864f39e36f34/cryptography-48.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:9de21387aa95e2a895823d0745b430bed4f33503ba9ab5e0b5311f33e37d66d2", size = 3798790, upload-time = "2026-06-09T22:31:56.697Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/6c/00fa2a95997164c8b2072ce327c23d4ab20809ccc323ea5fab91e53a4bba/cryptography-48.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67", size = 7987408, upload-time = "2026-06-09T22:32:20.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/d9/45f309a7e4e5f3f8f121d6d3be9e94024a7726ec598d6e08ae04edb2f04d/cryptography-48.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8", size = 4690196, upload-time = "2026-06-09T22:31:54.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/9f/a1bc8bcc798811b8527eb374bbccf30a3f3e806829d967118222bf1125eb/cryptography-48.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a", size = 4696782, upload-time = "2026-06-09T22:31:45.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/c2/81a4fb4e4373c500bb526bc337ac5719dd31dd15b970b84a238168c6aa08/cryptography-48.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4ab0a343c807bbcd90c971cd1ecf072937cd01847a9e002bef88fb47ac6be577", size = 4696618, upload-time = "2026-06-09T22:31:11.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/0b/aa68b221dde92d09cb29a024ede17550ee21e77a404e59fc093c82bb51e1/cryptography-48.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9621de99d2da096006b629979efd8ae7eb2d8b822488d0c89ee4000c306c59b1", size = 5289970, upload-time = "2026-06-09T22:31:20.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/13/fba657f958d2af66ea959a4ba01212632089249d34af1ae48054136344d7/cryptography-48.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:88c852a0ae366e262e5a1744b685e6a433dc8788dd2a277e418bf4904203609d", size = 4731873, upload-time = "2026-06-09T22:31:22.253Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/4c/9a964756d24a26b3e34dfcb16f961b89838786e6700b635b0d1e3adff4b6/cryptography-48.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:43c5835e2cb98c8733d86f57d6fc879b613f5c3478607281c3e36daffc6dd8a6", size = 4330804, upload-time = "2026-06-09T22:31:36.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/0f/a10f3a6eb12950a10e3a874070283aa2dd5875b2bfd15fad8a3e17b3f13e/cryptography-48.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46", size = 4696217, upload-time = "2026-06-09T22:31:13.351Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/6f/5cd12f951165ea73ef85266775d97e4c763b2474ccfd816dd69d3a18d6f8/cryptography-48.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:b7a2d1a937a738a881737cec135a38bb61470589b17515b9f73f571d0ae10401", size = 5245252, upload-time = "2026-06-09T22:32:02.193Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/ab/8aaa12e4516ec4464033ab79b6f3b592bd5a92102467c4ace8a0d970203f/cryptography-48.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b74ca3b8e5ecdd833bf6a002ca41b4793bb27fb8f1c06ffaf2643c9e9140e31b", size = 4731388, upload-time = "2026-06-09T22:32:04.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/24/50027ea4dca85ec1f40688f3c24fb32ccacd520583c9592c3cc95628e6fb/cryptography-48.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c37f2461406063b417837f5f3daab668652acd82423efcd7f0a9f04be972de1", size = 4824186, upload-time = "2026-06-09T22:32:18.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/41/04cb5eb17085ade6f50cc611fb657df6a0f5885350de8764ece89c050197/cryptography-48.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:86fe77abb1bd87afb251d4d02ada7ecf53a32cee9b67d976abb2e45a13297475", size = 4964539, upload-time = "2026-06-09T22:31:18.793Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/bf/ed70785c496e89d7e73b7cda2d21f2447fd6d4e821714b8d04ff217fed92/cryptography-48.0.1-cp39-abi3-win32.whl", hash = "sha256:6b2c0c3e6ccf3ade7750f836ef3ee36eea250cc467d45c256895573ac08cc6f1", size = 3282307, upload-time = "2026-06-09T22:30:53.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/ff/371ea7d252656ee1eb6d83eeeef3d1d0c6baf1d6497687d081ea03814670/cryptography-48.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:9a49ca6c81417f6a5edb50375a60cccdd70fa0a91a5211829dbea74eba94d2ac", size = 3793408, upload-time = "2026-06-09T22:32:15.191Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/d3/eb4e394e587341fdad09a09101fa76478ead3a78b0ad63e55c22f0d75c02/cryptography-48.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:08a597acce1ff37f347400087776599e2348a3a8bc53b44120e463cd274efe4a", size = 3951747, upload-time = "2026-06-09T22:31:23.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/4a/3f43451b4f858bfceaaaffc649e6e787e8d4fb332a1d443af39ab02cc8f1/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:735824ec41b7f74a7c45fb1591349333e4c696cb6c044e5f46356e560143e4cd", size = 4641226, upload-time = "2026-06-09T22:31:02.532Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/4e/855584c2c23b09e4ce2d3b9c30e983e679cd60b068c513c6bbdb91e11782/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:92a46e1d638daa264ba2971c0b0489c9409787943efae4d60ffda3d091ef832c", size = 4668958, upload-time = "2026-06-09T22:32:06.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/3b/d35750e41d803d1e516fd6d6011f065424924da7af1748cef4cc9cb3ede1/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:7e234ac052af99f2700826a5c29ea99d9c1b1f80341cde62d11c8154dc8e0bd9", size = 4640793, upload-time = "2026-06-09T22:32:26.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/aa/cdb7181fe865285e87e96825aaab239400f1de0c3bfba9bd9769b79f1a92/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:33842cf0888951cef5bc7ac724ab844a42044c1727b967b7f8997289a0464f92", size = 4668505, upload-time = "2026-06-09T22:31:27.534Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/8c/ce3823c06c2804f194f9e64f0d67fa3f4094a39f2bb1a990cd03603af8fc/cryptography-48.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6184ca7b174f28d7c703f1290d4b297217c45355f77a98f67e9b7f14549ac54a", size = 3742204, upload-time = "2026-06-09T22:31:34.773Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
||||
]
|
||||
|
||||
[[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 = "httpx-sse"
|
||||
version = "0.4.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.15"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema"
|
||||
version = "4.26.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "attrs" },
|
||||
{ name = "jsonschema-specifications" },
|
||||
{ name = "referencing" },
|
||||
{ name = "rpds-py" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema-specifications"
|
||||
version = "2025.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "referencing" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcp"
|
||||
version = "1.27.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "httpx" },
|
||||
{ name = "httpx-sse" },
|
||||
{ name = "jsonschema" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "pyjwt", extra = ["crypto"] },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
||||
{ name = "sse-starlette" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8b/eb/c0cfc62075dc6e1ec1c64d352ae09ac051d9334311ed226f1f425312848a/mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83", size = 607509, upload-time = "2026-04-02T14:48:08.88Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
cli = [
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "typer" },
|
||||
]
|
||||
|
||||
[[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 = "opendataloader-pdf"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/89/7904cbff3befb0219b30486d80254409cb4f14b82364f7d7c763f3e4687f/opendataloader_pdf-2.3.0.tar.gz", hash = "sha256:4abb0737c70b6b6a42b04748945b598f3776d2af7bb26ed275ed1e1f26629ee2", size = 22474091, upload-time = "2026-04-21T12:16:04.512Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/a4/e8b350bbea4f56d990dbf6b599c7cfb43c5ed3914b0d9efab6ac62c24474/opendataloader_pdf-2.3.0-py3-none-any.whl", hash = "sha256:1a6245d3f774dab2414ae759b1fc04edfad2f6e17b3c9f9b386b74a58604e20d", size = 22487135, upload-time = "2026-04-21T12:16:01.724Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opendataloader-pdf-mcp"
|
||||
version = "0.0.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "mcp", extra = ["cli"] },
|
||||
{ name = "opendataloader-pdf" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "mcp", extras = ["cli"], specifier = ">=1.27.0,<2" },
|
||||
{ name = "opendataloader-pdf" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.13.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.46.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/22/98/b50eb9a411e87483b5c65dba4fa430a06bac4234d3403a40e5a9905ebcd0/pydantic_core-2.46.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1da3786b8018e60349680720158cc19161cc3b4bdd815beb0a321cd5ce1ad5b1", size = 2108971, upload-time = "2026-04-20T14:43:51.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/4b/f364b9d161718ff2217160a4b5d41ce38de60aed91c3689ebffa1c939d23/pydantic_core-2.46.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc0988cb29d21bf4a9d5cf2ef970b5c0e38d8d8e107a493278c05dc6c1dda69f", size = 1949588, upload-time = "2026-04-20T14:44:10.386Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/8b/30bd03ee83b2f5e29f5ba8e647ab3c456bf56f2ec72fdbcc0215484a0854/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f9067c3bfadd04c55484b89c0d267981b2f3512850f6f66e1e74204a4e4ce3", size = 1975986, upload-time = "2026-04-20T14:43:57.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/54/13ccf954d84ec275d5d023d5786e4aa48840bc9f161f2838dc98e1153518/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a642ac886ecf6402d9882d10c405dcf4b902abeb2972cd5fb4a48c83cd59279a", size = 2055830, upload-time = "2026-04-20T14:44:15.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/0e/65f38125e660fdbd72aa858e7dfae893645cfa0e7b13d333e174a367cd23/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79f561438481f28681584b89e2effb22855e2179880314bcddbf5968e935e807", size = 2222340, upload-time = "2026-04-20T14:41:51.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/88/f3ab7739efe0e7e80777dbb84c59eb98518e3f57ea433206194c2e425272/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57a973eae4665352a47cf1a99b4ee864620f2fe663a217d7a8da68a1f3a5bfda", size = 2280727, upload-time = "2026-04-20T14:41:30.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/6d/c228219080817bec4982f9531cadb18da6aaa770fdeb114f49c237ac2c9f/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83d002b97072a53ea150d63e0a3adfae5670cef5aa8a6e490240e482d3b22e57", size = 2092158, upload-time = "2026-04-20T14:44:07.305Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/b1/525a16711e7c6d61635fac3b0bd54600b5c5d9f60c6fc5aaab26b64a2297/pydantic_core-2.46.3-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b40ddd51e7c44b28cfaef746c9d3c506d658885e0a46f9eeef2ee815cbf8e045", size = 2116626, upload-time = "2026-04-20T14:42:34.118Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/7c/17d30673351439a6951bf54f564cf2443ab00ae264ec9df00e2efd710eb5/pydantic_core-2.46.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac5ec7fb9b87f04ee839af2d53bcadea57ded7d229719f56c0ed895bff987943", size = 2160691, upload-time = "2026-04-20T14:41:14.023Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/66/af8adbcbc0886ead7f1a116606a534d75a307e71e6e08226000d51b880d2/pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a3b11c812f61b3129c4905781a2601dfdfdea5fe1e6c1cfb696b55d14e9c054f", size = 2182543, upload-time = "2026-04-20T14:40:48.886Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/37/6de71e0f54c54a4190010f57deb749e1ddf75c568ada3b1320b70067f121/pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1108da631e602e5b3c38d6d04fe5bb3bfa54349e6918e3ca6cf570b2e2b2f9d4", size = 2324513, upload-time = "2026-04-20T14:42:36.121Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/b1/9fc74ce94f603d5ef59ff258ca9c2c8fb902fb548d340a96f77f4d1c3b7f/pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:de885175515bcfa98ae618c1df7a072f13d179f81376c8007112af20567fd08a", size = 2361853, upload-time = "2026-04-20T14:43:24.886Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/d0/4c652fc592db35f100279ee751d5a145aca1b9a7984b9684ba7c1b5b0535/pydantic_core-2.46.3-cp310-cp310-win32.whl", hash = "sha256:d11058e3201527d41bc6b545c79187c9e4bf85e15a236a6007f0e991518882b7", size = 1980465, upload-time = "2026-04-20T14:44:46.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/b8/a920453c38afbe1f355e1ea0b0d94a0a3e0b0879d32d793108755fa171d5/pydantic_core-2.46.3-cp310-cp310-win_amd64.whl", hash = "sha256:3612edf65c8ea67ac13616c4d23af12faef1ae435a8a93e5934c2a0cbbdd1fd6", size = 2073884, upload-time = "2026-04-20T14:43:01.201Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/a2/1ba90a83e85a3f94c796b184f3efde9c72f2830dcda493eea8d59ba78e6d/pydantic_core-2.46.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5", size = 2106740, upload-time = "2026-04-20T14:41:20.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/f6/99ae893c89a0b9d3daec9f95487aa676709aa83f67643b3f0abaf4ab628a/pydantic_core-2.46.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c", size = 1948293, upload-time = "2026-04-20T14:43:42.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/b8/2e8e636dc9e3f16c2e16bf0849e24be82c5ee82c603c65fc0326666328fc/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e", size = 1973222, upload-time = "2026-04-20T14:41:57.841Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/36/0e730beec4d83c5306f417afbd82ff237d9a21e83c5edf675f31ed84c1fe/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287", size = 2053852, upload-time = "2026-04-20T14:40:43.077Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/f0/3071131f47e39136a17814576e0fada9168569f7f8c0e6ac4d1ede6a4958/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe", size = 2221134, upload-time = "2026-04-20T14:43:03.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/a9/a2dc023eec5aa4b02a467874bad32e2446957d2adcab14e107eab502e978/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050", size = 2279785, upload-time = "2026-04-20T14:41:19.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/44/93f489d16fb63fbd41c670441536541f6e8cfa1e5a69f40bc9c5d30d8c90/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2", size = 2089404, upload-time = "2026-04-20T14:43:10.108Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/78/8692e3aa72b2d004f7a5d937f1dfdc8552ba26caf0bec75f342c40f00dec/pydantic_core-2.46.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa", size = 2114898, upload-time = "2026-04-20T14:44:51.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/62/e83133f2e7832532060175cebf1f13748f4c7e7e7165cdd1f611f174494b/pydantic_core-2.46.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c", size = 2157856, upload-time = "2026-04-20T14:43:46.64Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/ec/6a500e3ad7718ee50583fae79c8651f5d37e3abce1fa9ae177ae65842c53/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf", size = 2180168, upload-time = "2026-04-20T14:42:00.302Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/53/8267811054b1aa7fc1dc7ded93812372ef79a839f5e23558136a6afbfde1/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b", size = 2322885, upload-time = "2026-04-20T14:41:05.253Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/c1/1c0acdb3aa0856ddc4ecc55214578f896f2de16f400cf51627eb3c26c1c4/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e", size = 2360328, upload-time = "2026-04-20T14:41:43.991Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/d0/ef39cd0f4a926814f360e71c1adeab48ad214d9727e4deb48eedfb5bce1a/pydantic_core-2.46.3-cp311-cp311-win32.whl", hash = "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb", size = 1979464, upload-time = "2026-04-20T14:43:12.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/9c/f41951b0d858e343f1cf09398b2a7b3014013799744f2c4a8ad6a3eec4f2/pydantic_core-2.46.3-cp311-cp311-win_amd64.whl", hash = "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346", size = 2070837, upload-time = "2026-04-20T14:41:47.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/1e/264a17cd582f6ed50950d4d03dd5fefd84e570e238afe1cb3e25cf238769/pydantic_core-2.46.3-cp311-cp311-win_arm64.whl", hash = "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6", size = 2053647, upload-time = "2026-04-20T14:42:27.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/cb/5b47425556ecc1f3fe18ed2a0083188aa46e1dd812b06e406475b3a5d536/pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", size = 2101946, upload-time = "2026-04-20T14:40:52.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", size = 1951612, upload-time = "2026-04-20T14:42:42.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/6e/b7348fd30d6556d132cddd5bd79f37f96f2601fe0608afac4f5fb01ec0b3/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", size = 1977027, upload-time = "2026-04-20T14:42:02.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/11/31d60ee2b45540d3fb0b29302a393dbc01cd771c473f5b5147bcd353e593/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", size = 2063008, upload-time = "2026-04-20T14:44:17.952Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/db/3a9d1957181b59258f44a2300ab0f0be9d1e12d662a4f57bb31250455c52/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", size = 2233082, upload-time = "2026-04-20T14:40:57.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/e1/3277c38792aeb5cfb18c2f0c5785a221d9ff4e149abbe1184d53d5f72273/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", size = 2304615, upload-time = "2026-04-20T14:42:12.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", size = 2094380, upload-time = "2026-04-20T14:43:05.522Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/20/abac35dedcbfd66c6f0b03e4e3564511771d6c9b7ede10a362d03e110d9b/pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", size = 2135429, upload-time = "2026-04-20T14:41:55.549Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/a5/41bfd1df69afad71b5cf0535055bccc73022715ad362edbc124bc1e021d7/pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", size = 2174582, upload-time = "2026-04-20T14:41:45.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/65/38d86ea056b29b2b10734eb23329b7a7672ca604df4f2b6e9c02d4ee22fe/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", size = 2187533, upload-time = "2026-04-20T14:40:55.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/55/a1129141678a2026badc539ad1dee0a71d06f54c2f06a4bd68c030ac781b/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", size = 2332985, upload-time = "2026-04-20T14:44:13.05Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/60/cb26f4077719f709e54819f4e8e1d43f4091f94e285eb6bd21e1190a7b7c/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", size = 2373670, upload-time = "2026-04-20T14:41:53.421Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/7e/c3f21882bdf1d8d086876f81b5e296206c69c6082551d776895de7801fa0/pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", size = 1966722, upload-time = "2026-04-20T14:44:30.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", size = 2072970, upload-time = "2026-04-20T14:42:54.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/f8/a989b21cc75e9a32d24192ef700eea606521221a89faa40c919ce884f2b1/pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", size = 2035963, upload-time = "2026-04-20T14:44:20.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/7f/03dbad45cd3aa9083fbc93c210ae8b005af67e4136a14186950a747c6874/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46", size = 2105683, upload-time = "2026-04-20T14:42:19.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/22/4dc186ac8ea6b257e9855031f51b62a9637beac4d68ac06bee02f046f836/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874", size = 1940052, upload-time = "2026-04-20T14:43:59.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/ca/d376391a5aff1f2e8188960d7873543608130a870961c2b6b5236627c116/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76", size = 1988172, upload-time = "2026-04-20T14:41:17.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/6b/523b9f85c23788755d6ab949329de692a2e3a584bc6beb67fef5e035aa9d/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531", size = 2128596, upload-time = "2026-04-20T14:40:41.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/42/f426db557e8ab2791bc7562052299944a118655496fbff99914e564c0a94/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", size = 2091877, upload-time = "2026-04-20T14:43:27.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/4f/86a832a9d14df58e663bfdf4627dc00d3317c2bd583c4fb23390b0f04b8e/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", size = 1932428, upload-time = "2026-04-20T14:40:45.781Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/1a/fe857968954d93fb78e0d4b6df5c988c74c4aaa67181c60be7cfe327c0ca/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", size = 1997550, upload-time = "2026-04-20T14:44:02.425Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/da/99d40830684f81dec901cac521b5b91c095394cc1084b9433393cde1c2df/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25", size = 2107973, upload-time = "2026-04-20T14:42:06.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/a5/87024121818d75bbb2a98ddbaf638e40e7a18b5e0f5492c9ca4b1b316107/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3", size = 1947191, upload-time = "2026-04-20T14:43:14.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/62/0c1acfe10945b83a6a59d19fbaa92f48825381509e5701b855c08f13db76/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536", size = 2123791, upload-time = "2026-04-20T14:43:22.766Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/3e/3b2393b4c8f44285561dc30b00cf307a56a2eff7c483a824db3b8221ca51/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1", size = 2153197, upload-time = "2026-04-20T14:44:27.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/75/5af02fb35505051eee727c061f2881c555ab4f8ddb2d42da715a42c9731b/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c", size = 2181073, upload-time = "2026-04-20T14:43:20.729Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/92/7e0e1bd9ca3c68305db037560ca2876f89b2647deb2f8b6319005de37505/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85", size = 2315886, upload-time = "2026-04-20T14:44:04.826Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/d8/101655f27eaf3e44558ead736b2795d12500598beed4683f279396fa186e/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8", size = 2360528, upload-time = "2026-04-20T14:40:47.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/0f/1c34a74c8d07136f0d729ffe5e1fdab04fbdaa7684f61a92f92511a84a15/pydantic_core-2.46.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff", size = 2184144, upload-time = "2026-04-20T14:42:57Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.14.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyjwt"
|
||||
version = "2.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
crypto = [
|
||||
{ name = "cryptography" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.31"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/64/7e/9b35ad8f3d9ca680f7c87a88f19612fdd8da9796c4d3b46e560ac79dcc4a/python_multipart-0.0.31.tar.gz", hash = "sha256:fc631183bb13e56db3158a4909908dfb2e23565286744e798241e63750e5d680", size = 46689, upload-time = "2026-06-04T08:27:49.014Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/1e/7f7f299527a5a8ad90acd5f2f78dfa6c8495c6301a3205106ea68a84de96/python_multipart-0.0.31-py3-none-any.whl", hash = "sha256:8408153d68a9773291fc1da39a8b85a50044bddbabd2dd72e9229776b7b15e28", size = 29996, upload-time = "2026-06-04T08:27:47.804Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32"
|
||||
version = "311"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "referencing"
|
||||
version = "0.37.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "attrs" },
|
||||
{ name = "rpds-py" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "15.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rpds-py"
|
||||
version = "0.30.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" },
|
||||
]
|
||||
|
||||
[[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 = "sse-starlette"
|
||||
version = "3.3.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "starlette" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/26/8c/f9290339ef6d79badbc010f067cd769d6601ec11a57d78569c683fb4dd87/sse_starlette-3.3.4.tar.gz", hash = "sha256:aaf92fc067af8a5427192895ac028e947b484ac01edbc3caf00e7e7137c7bef1", size = 32427, upload-time = "2026-03-29T09:00:23.307Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/7f/3de5402f39890ac5660b86bcf5c03f9d855dad5c4ed764866d7b592b46fd/sse_starlette-3.3.4-py3-none-any.whl", hash = "sha256:84bb06e58939a8b38d8341f1bc9792f06c2b53f48c608dd207582b664fc8f3c1", size = 14330, upload-time = "2026-03-29T09:00:21.846Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.24.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
{ name = "click" },
|
||||
{ name = "rich" },
|
||||
{ name = "shellingham" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/83/b8/9ebb531b6c2d377af08ac6746a5df3425b21853a5d2260876919b58a2a4a/typer-0.24.2.tar.gz", hash = "sha256:ec070dcfca1408e85ee203c6365001e818c3b7fffe686fd07ff2d68095ca0480", size = 119849, upload-time = "2026-04-22T17:45:34.413Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/39/d1/9484b497e0a0410b901c12b8251c3e746e1e863f7d28419ffe06f7892fda/typer-0.24.2-py3-none-any.whl", hash = "sha256:b618bc3d721f9a8d30f3e05565be26416d06e9bcc29d49bc491dc26aba674fa8", size = 55977, upload-time = "2026-04-22T17:45:33.055Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.46.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/93/041fca8274050e40e6791f267d82e0e2e27dd165627bd640d3e0e378d877/uvicorn-0.46.0.tar.gz", hash = "sha256:fb9da0926999cc6cb22dc7cd71a94a632f078e6ae47ff683c5c420750fb7413d", size = 88758, upload-time = "2026-04-23T07:16:00.151Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl", hash = "sha256:bbebbcbed972d162afca128605223022bedd345b7bc7855ce66deb31487a9048", size = 70926, upload-time = "2026-04-23T07:15:58.355Z" },
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
# Build artifacts
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
|
||||
# Ignore JAR file
|
||||
*.jar
|
||||
|
||||
# Docs
|
||||
README.md
|
||||
LICENSE
|
||||
NOTICE
|
||||
THIRD_PARTY/
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Custom build hook for hatch to copy JAR and license files."""
|
||||
|
||||
import glob
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
||||
|
||||
|
||||
class CustomBuildHook(BuildHookInterface):
|
||||
def initialize(self, version, build_data):
|
||||
root_dir = Path(self.root)
|
||||
pkg_dir = root_dir / "src/opendataloader_pdf"
|
||||
dest_jar_dir = pkg_dir / "jar"
|
||||
dest_jar_path = dest_jar_dir / "opendataloader-pdf-cli.jar"
|
||||
license_path = pkg_dir / "LICENSE"
|
||||
notice_path = pkg_dir / "NOTICE"
|
||||
third_party_dest = pkg_dir / "THIRD_PARTY"
|
||||
|
||||
readme_path = root_dir / "README.md"
|
||||
|
||||
# sdist-install code path: when users `pip install <sdist>.tar.gz`,
|
||||
# the extracted sdist already contains JAR/LICENSE/NOTICE/THIRD_PARTY
|
||||
# (force-included via [tool.hatch.build] artifacts in pyproject.toml),
|
||||
# and there is no java/ tree to rebuild from. Do not remove — sdist
|
||||
# installs would break with a spurious "mvn package" error.
|
||||
if (
|
||||
dest_jar_path.exists()
|
||||
and license_path.exists()
|
||||
and notice_path.exists()
|
||||
and third_party_dest.exists()
|
||||
and readme_path.exists()
|
||||
):
|
||||
print("All required files already exist (building from sdist), skipping copy")
|
||||
return
|
||||
|
||||
# --- Copy JAR ---
|
||||
print(f"Root DIR: {root_dir}")
|
||||
source_jar_glob = str(
|
||||
root_dir / "../../java/opendataloader-pdf-cli/target/opendataloader-pdf-cli-*.jar"
|
||||
)
|
||||
resolved_glob_path = Path(source_jar_glob).resolve()
|
||||
print(f"Searching for JAR file in: {resolved_glob_path}")
|
||||
|
||||
source_jar_paths = glob.glob(source_jar_glob)
|
||||
if not source_jar_paths:
|
||||
raise RuntimeError(
|
||||
f"Could not find the JAR file. Please run 'mvn package' in the 'java/' directory first. Searched in: {resolved_glob_path}"
|
||||
)
|
||||
if len(source_jar_paths) > 1:
|
||||
raise RuntimeError(f"Found multiple JAR files, expected one: {source_jar_paths}")
|
||||
source_jar_path = source_jar_paths[0]
|
||||
print(f"Found source JAR: {source_jar_path}")
|
||||
|
||||
dest_jar_dir.mkdir(parents=True, exist_ok=True)
|
||||
print(f"Copying JAR to {dest_jar_path}")
|
||||
shutil.copy(source_jar_path, dest_jar_path)
|
||||
|
||||
# --- Copy LICENSE, NOTICE ---
|
||||
# README is copied by build-python.sh before this hook runs, because
|
||||
# hatchling validates [project.readme] during metadata parsing, which
|
||||
# happens before build hooks. Do not copy README here.
|
||||
shutil.copy(root_dir / "../../LICENSE", license_path)
|
||||
shutil.copy(root_dir / "../../NOTICE", notice_path)
|
||||
third_party_src = root_dir / "../../THIRD_PARTY"
|
||||
print(f"Copying THIRD_PARTY directory to {third_party_dest}")
|
||||
if third_party_dest.exists():
|
||||
shutil.rmtree(third_party_dest)
|
||||
shutil.copytree(third_party_src, third_party_dest)
|
||||
@@ -0,0 +1,79 @@
|
||||
[project]
|
||||
name = "opendataloader-pdf"
|
||||
version = "0.0.0"
|
||||
description = "A Python wrapper for the opendataloader-pdf Java CLI."
|
||||
readme = "README.md"
|
||||
license = "Apache-2.0"
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
{ name = "opendataloader-project", email = "open.dataloader@hancom.com" }
|
||||
]
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"Operating System :: OS Independent",
|
||||
]
|
||||
dependencies = []
|
||||
|
||||
[project.optional-dependencies]
|
||||
hybrid = [
|
||||
"docling[easyocr]>=2.91.0",
|
||||
"fastapi>=0.136.1",
|
||||
"uvicorn>=0.46.0",
|
||||
"python-multipart>=0.0.28",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
opendataloader-pdf = "opendataloader_pdf.wrapper:main"
|
||||
opendataloader-pdf-hybrid = "opendataloader_pdf.hybrid_server:main"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/opendataloader-project/opendataloader-pdf"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
# Shared by wheel and sdist: force-include gitignored build outputs
|
||||
# (JAR, LICENSE, NOTICE, THIRD_PARTY) that hatch_build.py copies at build time.
|
||||
[tool.hatch.build]
|
||||
artifacts = [
|
||||
"src/opendataloader_pdf/jar/*.jar",
|
||||
"src/opendataloader_pdf/LICENSE",
|
||||
"src/opendataloader_pdf/NOTICE",
|
||||
"src/opendataloader_pdf/THIRD_PARTY/**",
|
||||
]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/opendataloader_pdf"]
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
include = [
|
||||
"src/opendataloader_pdf/**",
|
||||
"README.md",
|
||||
"hatch_build.py",
|
||||
]
|
||||
|
||||
[tool.hatch.build.hooks.custom]
|
||||
path = "hatch_build.py"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=9.0.3",
|
||||
"pytest-asyncio>=1.3.0",
|
||||
"httpx>=0.28.1",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
package = true
|
||||
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py310"
|
||||
exclude = ["dist", "build"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I"]
|
||||
ignore = []
|
||||
@@ -0,0 +1,3 @@
|
||||
from .wrapper import run, convert, run_jar
|
||||
|
||||
__all__ = ["run", "convert", "run_jar"]
|
||||
@@ -0,0 +1,5 @@
|
||||
from .wrapper import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,308 @@
|
||||
# AUTO-GENERATED FROM options.json - DO NOT EDIT DIRECTLY
|
||||
# Run `npm run generate-options` to regenerate
|
||||
|
||||
"""
|
||||
CLI option definitions for opendataloader-pdf.
|
||||
"""
|
||||
from typing import Any, Dict, List
|
||||
|
||||
|
||||
# Option metadata list
|
||||
CLI_OPTIONS: List[Dict[str, Any]] = [
|
||||
{
|
||||
"name": "output-dir",
|
||||
"python_name": "output_dir",
|
||||
"short_name": "o",
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"default": None,
|
||||
"description": "Directory where output files are written. Default: input file directory",
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"python_name": "password",
|
||||
"short_name": "p",
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"default": None,
|
||||
"description": "Password for encrypted PDF files",
|
||||
},
|
||||
{
|
||||
"name": "format",
|
||||
"python_name": "format",
|
||||
"short_name": "f",
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"default": None,
|
||||
"description": "Output formats (comma-separated). Values: json, text, html, pdf, markdown, tagged-pdf. Default: json. For HTML inside Markdown use --markdown-with-html. For image extraction control use --image-output.",
|
||||
},
|
||||
{
|
||||
"name": "quiet",
|
||||
"python_name": "quiet",
|
||||
"short_name": "q",
|
||||
"type": "boolean",
|
||||
"required": False,
|
||||
"default": False,
|
||||
"description": "Suppress console logging output",
|
||||
},
|
||||
{
|
||||
"name": "content-safety-off",
|
||||
"python_name": "content_safety_off",
|
||||
"short_name": None,
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"default": None,
|
||||
"description": "Disable content safety filters. Values: all, hidden-text, off-page, tiny, hidden-ocg",
|
||||
},
|
||||
{
|
||||
"name": "sanitize",
|
||||
"python_name": "sanitize",
|
||||
"short_name": None,
|
||||
"type": "boolean",
|
||||
"required": False,
|
||||
"default": False,
|
||||
"description": "Enable sensitive data sanitization. Replaces emails, phone numbers, IPs, credit cards, and URLs with placeholders",
|
||||
},
|
||||
{
|
||||
"name": "keep-line-breaks",
|
||||
"python_name": "keep_line_breaks",
|
||||
"short_name": None,
|
||||
"type": "boolean",
|
||||
"required": False,
|
||||
"default": False,
|
||||
"description": "Preserve original line breaks in extracted text",
|
||||
},
|
||||
{
|
||||
"name": "replace-invalid-chars",
|
||||
"python_name": "replace_invalid_chars",
|
||||
"short_name": None,
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"default": " ",
|
||||
"description": "Replacement character for invalid/unrecognized characters. Default: space",
|
||||
},
|
||||
{
|
||||
"name": "use-struct-tree",
|
||||
"python_name": "use_struct_tree",
|
||||
"short_name": None,
|
||||
"type": "boolean",
|
||||
"required": False,
|
||||
"default": False,
|
||||
"description": "Use PDF structure tree (tagged PDF) for reading order and semantic structure. Output quality depends on tag quality",
|
||||
},
|
||||
{
|
||||
"name": "table-method",
|
||||
"python_name": "table_method",
|
||||
"short_name": None,
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"default": "default",
|
||||
"description": "Table detection method. Values: default (border-based), cluster (border + cluster). Default: default",
|
||||
},
|
||||
{
|
||||
"name": "reading-order",
|
||||
"python_name": "reading_order",
|
||||
"short_name": None,
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"default": "xycut",
|
||||
"description": "Reading order algorithm. Values: off, xycut. Default: xycut",
|
||||
},
|
||||
{
|
||||
"name": "markdown-page-separator",
|
||||
"python_name": "markdown_page_separator",
|
||||
"short_name": None,
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"default": None,
|
||||
"description": "Separator between pages in Markdown output. Use %%page-number%% for page numbers. Default: none",
|
||||
},
|
||||
{
|
||||
"name": "markdown-with-html",
|
||||
"python_name": "markdown_with_html",
|
||||
"short_name": None,
|
||||
"type": "boolean",
|
||||
"required": False,
|
||||
"default": False,
|
||||
"description": "Allow HTML tags inside Markdown output for complex structures such as multi-row-span tables. Implies --format markdown.",
|
||||
},
|
||||
{
|
||||
"name": "text-page-separator",
|
||||
"python_name": "text_page_separator",
|
||||
"short_name": None,
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"default": None,
|
||||
"description": "Separator between pages in text output. Use %%page-number%% for page numbers. Default: none",
|
||||
},
|
||||
{
|
||||
"name": "html-page-separator",
|
||||
"python_name": "html_page_separator",
|
||||
"short_name": None,
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"default": None,
|
||||
"description": "Separator between pages in HTML output. Use %%page-number%% for page numbers. Default: none",
|
||||
},
|
||||
{
|
||||
"name": "image-output",
|
||||
"python_name": "image_output",
|
||||
"short_name": None,
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"default": "external",
|
||||
"description": "Image output mode. Values: off (no images), embedded (Base64 data URIs), external (file references). Default: external",
|
||||
},
|
||||
{
|
||||
"name": "image-format",
|
||||
"python_name": "image_format",
|
||||
"short_name": None,
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"default": "png",
|
||||
"description": "Output format for extracted images. Values: png, jpeg. Default: png",
|
||||
},
|
||||
{
|
||||
"name": "image-dir",
|
||||
"python_name": "image_dir",
|
||||
"short_name": None,
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"default": None,
|
||||
"description": "Directory for extracted images (applies only with --image-output external)",
|
||||
},
|
||||
{
|
||||
"name": "pages",
|
||||
"python_name": "pages",
|
||||
"short_name": None,
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"default": None,
|
||||
"description": "Pages to extract (e.g., \"1,3,5-7\"). Default: all pages",
|
||||
},
|
||||
{
|
||||
"name": "include-header-footer",
|
||||
"python_name": "include_header_footer",
|
||||
"short_name": None,
|
||||
"type": "boolean",
|
||||
"required": False,
|
||||
"default": False,
|
||||
"description": "Include page headers and footers in output",
|
||||
},
|
||||
{
|
||||
"name": "detect-strikethrough",
|
||||
"python_name": "detect_strikethrough",
|
||||
"short_name": None,
|
||||
"type": "boolean",
|
||||
"required": False,
|
||||
"default": False,
|
||||
"description": "Detect strikethrough text and wrap with ~~ in Markdown output or <del></del> tag in HTML output (experimental)",
|
||||
},
|
||||
{
|
||||
"name": "hybrid",
|
||||
"python_name": "hybrid",
|
||||
"short_name": None,
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"default": "off",
|
||||
"description": "Hybrid backend (requires a running server). Quick start: pip install \"opendataloader-pdf[hybrid]\" && opendataloader-pdf-hybrid --port 5002. For remote servers use --hybrid-url. Values: off (default), docling-fast, hancom-ai",
|
||||
},
|
||||
{
|
||||
"name": "hybrid-mode",
|
||||
"python_name": "hybrid_mode",
|
||||
"short_name": None,
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"default": "auto",
|
||||
"description": "Hybrid triage mode. Values: auto (default, dynamic triage), full (skip triage, all pages to backend)",
|
||||
},
|
||||
{
|
||||
"name": "hybrid-url",
|
||||
"python_name": "hybrid_url",
|
||||
"short_name": None,
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"default": None,
|
||||
"description": "Hybrid backend server URL (overrides default)",
|
||||
},
|
||||
{
|
||||
"name": "hybrid-timeout",
|
||||
"python_name": "hybrid_timeout",
|
||||
"short_name": None,
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"default": "0",
|
||||
"description": "Hybrid backend request timeout in milliseconds (0 = no timeout). Default: 0",
|
||||
},
|
||||
{
|
||||
"name": "hybrid-fallback",
|
||||
"python_name": "hybrid_fallback",
|
||||
"short_name": None,
|
||||
"type": "boolean",
|
||||
"required": False,
|
||||
"default": False,
|
||||
"description": "Opt in to Java fallback on hybrid backend error (default: disabled)",
|
||||
},
|
||||
{
|
||||
"name": "hybrid-hancom-ai-regionlist-strategy",
|
||||
"python_name": "hybrid_hancom_ai_regionlist_strategy",
|
||||
"short_name": None,
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"default": "table-first",
|
||||
"description": "DLA label 7 (regionlist) handling. Requires --hybrid=hancom-ai. Values: table-first (default; check TSR overlap), list-only (skip TSR, always treat as list)",
|
||||
},
|
||||
{
|
||||
"name": "hybrid-hancom-ai-ocr-strategy",
|
||||
"python_name": "hybrid_hancom_ai_ocr_strategy",
|
||||
"short_name": None,
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"default": "auto",
|
||||
"description": "OCR strategy. Requires --hybrid=hancom-ai. Values: off (stream-only), auto (default; stream first, OCR fallback), force (OCR-only)",
|
||||
},
|
||||
{
|
||||
"name": "hybrid-hancom-ai-image-cache",
|
||||
"python_name": "hybrid_hancom_ai_image_cache",
|
||||
"short_name": None,
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"default": "memory",
|
||||
"description": "Page image cache backing. Requires --hybrid=hancom-ai. Values: memory (default), disk",
|
||||
},
|
||||
{
|
||||
"name": "to-stdout",
|
||||
"python_name": "to_stdout",
|
||||
"short_name": None,
|
||||
"type": "boolean",
|
||||
"required": False,
|
||||
"default": False,
|
||||
"description": "Write output to stdout instead of file (single format only)",
|
||||
},
|
||||
{
|
||||
"name": "threads",
|
||||
"python_name": "threads",
|
||||
"short_name": None,
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"default": "1",
|
||||
"description": "Number of worker threads for per-page processing. Default: 1 (sequential, stable). Values >1 (experimental) run pages in parallel for faster throughput; output may vary slightly on some PDFs. Capped at the number of available CPU cores. Applies to the native Java pipeline only; ignored in --hybrid mode",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def add_options_to_parser(parser) -> None:
|
||||
"""Add all CLI options to an argparse.ArgumentParser."""
|
||||
for opt in CLI_OPTIONS:
|
||||
flags = []
|
||||
if opt["short_name"]:
|
||||
flags.append(f'-{opt["short_name"]}')
|
||||
flags.append(f'--{opt["name"]}')
|
||||
|
||||
kwargs = {"help": opt["description"]}
|
||||
if opt["type"] == "boolean":
|
||||
kwargs["action"] = "store_true"
|
||||
else:
|
||||
kwargs["default"] = None
|
||||
|
||||
parser.add_argument(*flags, **kwargs)
|
||||
@@ -0,0 +1,162 @@
|
||||
# AUTO-GENERATED FROM options.json - DO NOT EDIT DIRECTLY
|
||||
# Run `npm run generate-options` to regenerate
|
||||
|
||||
"""
|
||||
Auto-generated convert function for opendataloader-pdf.
|
||||
"""
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from .runner import run_jar
|
||||
|
||||
|
||||
def convert(
|
||||
input_path: Union[str, List[str]],
|
||||
output_dir: Optional[str] = None,
|
||||
password: Optional[str] = None,
|
||||
format: Optional[Union[str, List[str]]] = None,
|
||||
quiet: bool = False,
|
||||
content_safety_off: Optional[Union[str, List[str]]] = None,
|
||||
sanitize: bool = False,
|
||||
keep_line_breaks: bool = False,
|
||||
replace_invalid_chars: Optional[str] = None,
|
||||
use_struct_tree: bool = False,
|
||||
table_method: Optional[str] = None,
|
||||
reading_order: Optional[str] = None,
|
||||
markdown_page_separator: Optional[str] = None,
|
||||
markdown_with_html: bool = False,
|
||||
text_page_separator: Optional[str] = None,
|
||||
html_page_separator: Optional[str] = None,
|
||||
image_output: Optional[str] = None,
|
||||
image_format: Optional[str] = None,
|
||||
image_dir: Optional[str] = None,
|
||||
pages: Optional[str] = None,
|
||||
include_header_footer: bool = False,
|
||||
detect_strikethrough: bool = False,
|
||||
hybrid: Optional[str] = None,
|
||||
hybrid_mode: Optional[str] = None,
|
||||
hybrid_url: Optional[str] = None,
|
||||
hybrid_timeout: Optional[str] = None,
|
||||
hybrid_fallback: bool = False,
|
||||
hybrid_hancom_ai_regionlist_strategy: Optional[str] = None,
|
||||
hybrid_hancom_ai_ocr_strategy: Optional[str] = None,
|
||||
hybrid_hancom_ai_image_cache: Optional[str] = None,
|
||||
to_stdout: bool = False,
|
||||
threads: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Convert PDF(s) into the requested output format(s).
|
||||
|
||||
Args:
|
||||
input_path: One or more input PDF file paths or directories
|
||||
output_dir: Directory where output files are written. Default: input file directory
|
||||
password: Password for encrypted PDF files
|
||||
format: Output formats (comma-separated). Values: json, text, html, pdf, markdown, tagged-pdf. Default: json. For HTML inside Markdown use --markdown-with-html. For image extraction control use --image-output.
|
||||
quiet: Suppress console logging output
|
||||
content_safety_off: Disable content safety filters. Values: all, hidden-text, off-page, tiny, hidden-ocg
|
||||
sanitize: Enable sensitive data sanitization. Replaces emails, phone numbers, IPs, credit cards, and URLs with placeholders
|
||||
keep_line_breaks: Preserve original line breaks in extracted text
|
||||
replace_invalid_chars: Replacement character for invalid/unrecognized characters. Default: space
|
||||
use_struct_tree: Use PDF structure tree (tagged PDF) for reading order and semantic structure. Output quality depends on tag quality
|
||||
table_method: Table detection method. Values: default (border-based), cluster (border + cluster). Default: default
|
||||
reading_order: Reading order algorithm. Values: off, xycut. Default: xycut
|
||||
markdown_page_separator: Separator between pages in Markdown output. Use %page-number% for page numbers. Default: none
|
||||
markdown_with_html: Allow HTML tags inside Markdown output for complex structures such as multi-row-span tables. Implies --format markdown.
|
||||
text_page_separator: Separator between pages in text output. Use %page-number% for page numbers. Default: none
|
||||
html_page_separator: Separator between pages in HTML output. Use %page-number% for page numbers. Default: none
|
||||
image_output: Image output mode. Values: off (no images), embedded (Base64 data URIs), external (file references). Default: external
|
||||
image_format: Output format for extracted images. Values: png, jpeg. Default: png
|
||||
image_dir: Directory for extracted images (applies only with --image-output external)
|
||||
pages: Pages to extract (e.g., "1,3,5-7"). Default: all pages
|
||||
include_header_footer: Include page headers and footers in output
|
||||
detect_strikethrough: Detect strikethrough text and wrap with ~~ in Markdown output or <del></del> tag in HTML output (experimental)
|
||||
hybrid: Hybrid backend (requires a running server). Quick start: pip install "opendataloader-pdf[hybrid]" && opendataloader-pdf-hybrid --port 5002. For remote servers use --hybrid-url. Values: off (default), docling-fast, hancom-ai
|
||||
hybrid_mode: Hybrid triage mode. Values: auto (default, dynamic triage), full (skip triage, all pages to backend)
|
||||
hybrid_url: Hybrid backend server URL (overrides default)
|
||||
hybrid_timeout: Hybrid backend request timeout in milliseconds (0 = no timeout). Default: 0
|
||||
hybrid_fallback: Opt in to Java fallback on hybrid backend error (default: disabled)
|
||||
hybrid_hancom_ai_regionlist_strategy: DLA label 7 (regionlist) handling. Requires --hybrid=hancom-ai. Values: table-first (default; check TSR overlap), list-only (skip TSR, always treat as list)
|
||||
hybrid_hancom_ai_ocr_strategy: OCR strategy. Requires --hybrid=hancom-ai. Values: off (stream-only), auto (default; stream first, OCR fallback), force (OCR-only)
|
||||
hybrid_hancom_ai_image_cache: Page image cache backing. Requires --hybrid=hancom-ai. Values: memory (default), disk
|
||||
to_stdout: Write output to stdout instead of file (single format only)
|
||||
threads: Number of worker threads for per-page processing. Default: 1 (sequential, stable). Values >1 (experimental) run pages in parallel for faster throughput; output may vary slightly on some PDFs. Capped at the number of available CPU cores. Applies to the native Java pipeline only; ignored in --hybrid mode
|
||||
"""
|
||||
args: List[str] = []
|
||||
|
||||
# Build input paths
|
||||
if isinstance(input_path, list):
|
||||
args.extend(input_path)
|
||||
else:
|
||||
args.append(input_path)
|
||||
|
||||
if output_dir:
|
||||
args.extend(["--output-dir", output_dir])
|
||||
if password:
|
||||
args.extend(["--password", password])
|
||||
if format:
|
||||
if isinstance(format, list):
|
||||
if format:
|
||||
args.extend(["--format", ",".join(format)])
|
||||
else:
|
||||
args.extend(["--format", format])
|
||||
if quiet:
|
||||
args.append("--quiet")
|
||||
if content_safety_off:
|
||||
if isinstance(content_safety_off, list):
|
||||
if content_safety_off:
|
||||
args.extend(["--content-safety-off", ",".join(content_safety_off)])
|
||||
else:
|
||||
args.extend(["--content-safety-off", content_safety_off])
|
||||
if sanitize:
|
||||
args.append("--sanitize")
|
||||
if keep_line_breaks:
|
||||
args.append("--keep-line-breaks")
|
||||
if replace_invalid_chars:
|
||||
args.extend(["--replace-invalid-chars", replace_invalid_chars])
|
||||
if use_struct_tree:
|
||||
args.append("--use-struct-tree")
|
||||
if table_method:
|
||||
args.extend(["--table-method", table_method])
|
||||
if reading_order:
|
||||
args.extend(["--reading-order", reading_order])
|
||||
if markdown_page_separator:
|
||||
args.extend(["--markdown-page-separator", markdown_page_separator])
|
||||
if markdown_with_html:
|
||||
args.append("--markdown-with-html")
|
||||
if text_page_separator:
|
||||
args.extend(["--text-page-separator", text_page_separator])
|
||||
if html_page_separator:
|
||||
args.extend(["--html-page-separator", html_page_separator])
|
||||
if image_output:
|
||||
args.extend(["--image-output", image_output])
|
||||
if image_format:
|
||||
args.extend(["--image-format", image_format])
|
||||
if image_dir:
|
||||
args.extend(["--image-dir", image_dir])
|
||||
if pages:
|
||||
args.extend(["--pages", pages])
|
||||
if include_header_footer:
|
||||
args.append("--include-header-footer")
|
||||
if detect_strikethrough:
|
||||
args.append("--detect-strikethrough")
|
||||
if hybrid:
|
||||
args.extend(["--hybrid", hybrid])
|
||||
if hybrid_mode:
|
||||
args.extend(["--hybrid-mode", hybrid_mode])
|
||||
if hybrid_url:
|
||||
args.extend(["--hybrid-url", hybrid_url])
|
||||
if hybrid_timeout:
|
||||
args.extend(["--hybrid-timeout", hybrid_timeout])
|
||||
if hybrid_fallback:
|
||||
args.append("--hybrid-fallback")
|
||||
if hybrid_hancom_ai_regionlist_strategy:
|
||||
args.extend(["--hybrid-hancom-ai-regionlist-strategy", hybrid_hancom_ai_regionlist_strategy])
|
||||
if hybrid_hancom_ai_ocr_strategy:
|
||||
args.extend(["--hybrid-hancom-ai-ocr-strategy", hybrid_hancom_ai_ocr_strategy])
|
||||
if hybrid_hancom_ai_image_cache:
|
||||
args.extend(["--hybrid-hancom-ai-image-cache", hybrid_hancom_ai_image_cache])
|
||||
if to_stdout:
|
||||
args.append("--to-stdout")
|
||||
if threads:
|
||||
args.extend(["--threads", threads])
|
||||
|
||||
run_jar(args, quiet)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
Low-level JAR runner for opendataloader-pdf.
|
||||
"""
|
||||
import subprocess
|
||||
import sys
|
||||
import importlib.resources as resources
|
||||
from typing import List
|
||||
|
||||
# The consistent name of the JAR file bundled with the package
|
||||
_JAR_NAME = "opendataloader-pdf-cli.jar"
|
||||
|
||||
|
||||
def run_jar(args: List[str], quiet: bool = False) -> str:
|
||||
"""Run the opendataloader-pdf JAR with the given arguments."""
|
||||
try:
|
||||
# Access the embedded JAR inside the package
|
||||
jar_ref = resources.files("opendataloader_pdf").joinpath("jar", _JAR_NAME)
|
||||
with resources.as_file(jar_ref) as jar_path:
|
||||
# Force headless AWT so macOS doesn't surface a Dock icon (and
|
||||
# steal focus) every time the JVM touches ImageIO/PDFBox
|
||||
# rendering. Safe on all OSes — the CLI never opens a UI window,
|
||||
# only manipulates BufferedImages.
|
||||
command = [
|
||||
"java",
|
||||
"-Djava.awt.headless=true",
|
||||
"-Dapple.awt.UIElement=true",
|
||||
"-jar",
|
||||
str(jar_path),
|
||||
*args,
|
||||
]
|
||||
|
||||
if quiet:
|
||||
# Quiet mode → suppress the JAR's log stream (stderr) but
|
||||
# relay its stdout to the caller: --to-stdout content and the
|
||||
# folder summary line arrive on stdout, and swallowing them
|
||||
# breaks pipe consumers (`... --quiet --to-stdout | jq`).
|
||||
result = subprocess.run(
|
||||
command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
check=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
if result.stdout:
|
||||
if hasattr(sys.stdout, "buffer"):
|
||||
sys.stdout.buffer.write(
|
||||
result.stdout.encode("utf-8", errors="replace")
|
||||
)
|
||||
sys.stdout.buffer.flush()
|
||||
else:
|
||||
sys.stdout.write(result.stdout)
|
||||
return result.stdout
|
||||
|
||||
# Streaming mode → live output
|
||||
with subprocess.Popen(
|
||||
command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
) as process:
|
||||
output_lines: List[str] = []
|
||||
for line in process.stdout:
|
||||
if hasattr(sys.stdout, "buffer"):
|
||||
sys.stdout.buffer.write(line.encode("utf-8", errors="replace"))
|
||||
sys.stdout.buffer.flush()
|
||||
else:
|
||||
sys.stdout.write(line)
|
||||
output_lines.append(line)
|
||||
|
||||
return_code = process.wait()
|
||||
captured_output = "".join(output_lines)
|
||||
|
||||
if return_code:
|
||||
raise subprocess.CalledProcessError(
|
||||
return_code, command, output=captured_output
|
||||
)
|
||||
return captured_output
|
||||
|
||||
except FileNotFoundError:
|
||||
print(
|
||||
"Error: 'java' command not found. Please ensure Java is installed and in your system's PATH.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise
|
||||
|
||||
except subprocess.CalledProcessError as error:
|
||||
print("Error running opendataloader-pdf CLI.", file=sys.stderr)
|
||||
print(f"Return code: {error.returncode}", file=sys.stderr)
|
||||
# Streaming mode already wrote the JAR's output live to stdout, so
|
||||
# re-printing the captured copy would duplicate it. Only surface the
|
||||
# captured streams in quiet mode, where the caller has not seen them.
|
||||
# Note: CalledProcessError.output and .stdout are aliases for the same
|
||||
# attribute — printing both produces the same content twice.
|
||||
if quiet:
|
||||
if error.stdout:
|
||||
print(f"Stdout: {error.stdout}", file=sys.stderr)
|
||||
if error.stderr:
|
||||
print(f"Stderr: {error.stderr}", file=sys.stderr)
|
||||
raise
|
||||
@@ -0,0 +1,117 @@
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
import warnings
|
||||
from typing import List, Optional
|
||||
|
||||
from .cli_options_generated import add_options_to_parser
|
||||
from .convert_generated import convert
|
||||
from .runner import run_jar
|
||||
|
||||
# Re-export for backward compatibility
|
||||
__all__ = ["convert", "run", "run_jar", "main"]
|
||||
|
||||
|
||||
# Deprecated : Use `convert()` instead. This function will be removed in a future version.
|
||||
def run(
|
||||
input_path: str,
|
||||
output_folder: Optional[str] = None,
|
||||
password: Optional[str] = None,
|
||||
replace_invalid_chars: Optional[str] = None,
|
||||
generate_markdown: bool = False,
|
||||
generate_html: bool = False,
|
||||
generate_annotated_pdf: bool = False,
|
||||
keep_line_breaks: bool = False,
|
||||
content_safety_off: Optional[str] = None,
|
||||
html_in_markdown: bool = False,
|
||||
add_image_to_markdown: bool = False,
|
||||
no_json: bool = False,
|
||||
debug: bool = False,
|
||||
use_struct_tree: bool = False,
|
||||
):
|
||||
"""
|
||||
Runs the opendataloader-pdf with the given arguments.
|
||||
|
||||
.. deprecated::
|
||||
Use :func:`convert` instead. This function will be removed in a future version.
|
||||
|
||||
Args:
|
||||
input_path: Path to the input PDF file or folder.
|
||||
output_folder: Path to the output folder. Defaults to the input folder.
|
||||
password: Password for the PDF file.
|
||||
replace_invalid_chars: Character to replace invalid or unrecognized characters (e.g., , \\u0000) with.
|
||||
generate_markdown: If True, generates a Markdown output file.
|
||||
generate_html: If True, generates an HTML output file.
|
||||
generate_annotated_pdf: If True, generates an annotated PDF output file.
|
||||
keep_line_breaks: If True, keeps line breaks in the output.
|
||||
html_in_markdown: If True, uses HTML in the Markdown output.
|
||||
add_image_to_markdown: If True, adds images to the Markdown output.
|
||||
no_json: If True, disable the JSON output.
|
||||
debug: If True, prints all messages from the CLI to the console during execution.
|
||||
use_struct_tree: If True, enable processing structure tree (disabled by default)
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the 'java' command is not found or input_path is invalid.
|
||||
subprocess.CalledProcessError: If the CLI tool returns a non-zero exit code.
|
||||
"""
|
||||
warnings.warn(
|
||||
"run() is deprecated and will be removed in a future version. Use convert() instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
# Build format list based on legacy boolean options
|
||||
formats: List[str] = []
|
||||
if not no_json:
|
||||
formats.append("json")
|
||||
if generate_markdown:
|
||||
if add_image_to_markdown:
|
||||
formats.append("markdown-with-images")
|
||||
elif html_in_markdown:
|
||||
formats.append("markdown-with-html")
|
||||
else:
|
||||
formats.append("markdown")
|
||||
if generate_html:
|
||||
formats.append("html")
|
||||
if generate_annotated_pdf:
|
||||
formats.append("pdf")
|
||||
|
||||
convert(
|
||||
input_path=input_path,
|
||||
output_dir=output_folder,
|
||||
password=password,
|
||||
replace_invalid_chars=replace_invalid_chars,
|
||||
keep_line_breaks=keep_line_breaks,
|
||||
content_safety_off=content_safety_off,
|
||||
use_struct_tree=use_struct_tree,
|
||||
format=formats if formats else None,
|
||||
quiet=not debug,
|
||||
)
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
"""CLI entry point for running the wrapper from the command line."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Run the opendataloader-pdf CLI using the bundled JAR."
|
||||
)
|
||||
parser.add_argument(
|
||||
"input_path", nargs="+", help="Path to the input PDF file or directory."
|
||||
)
|
||||
|
||||
# Register CLI options from auto-generated module
|
||||
add_options_to_parser(parser)
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
try:
|
||||
convert(**vars(args))
|
||||
return 0
|
||||
except FileNotFoundError as err:
|
||||
print(err, file=sys.stderr)
|
||||
return 1
|
||||
except subprocess.CalledProcessError as err:
|
||||
return err.returncode or 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,23 @@
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def input_pdf():
|
||||
return Path(__file__).resolve().parents[3] / "samples" / "pdf" / "1901.03003.pdf"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def output_dir():
|
||||
path = (
|
||||
Path(__file__).resolve().parents[3]
|
||||
/ "python"
|
||||
/ "opendataloader-pdf"
|
||||
/ "tests"
|
||||
/ "temp"
|
||||
)
|
||||
path.mkdir(exist_ok=True)
|
||||
yield path
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
@@ -0,0 +1,142 @@
|
||||
"""Unit tests for auto-generated cli_options module"""
|
||||
|
||||
import pytest
|
||||
from opendataloader_pdf.cli_options_generated import CLI_OPTIONS, add_options_to_parser
|
||||
|
||||
|
||||
class TestCLIOptions:
|
||||
"""Tests for CLI_OPTIONS metadata list"""
|
||||
|
||||
def test_cli_options_is_list(self):
|
||||
"""CLI_OPTIONS should be a list"""
|
||||
assert isinstance(CLI_OPTIONS, list)
|
||||
|
||||
def test_cli_options_not_empty(self):
|
||||
"""CLI_OPTIONS should not be empty"""
|
||||
assert len(CLI_OPTIONS) > 0
|
||||
|
||||
def test_each_option_has_required_fields(self):
|
||||
"""Each option should have all required fields"""
|
||||
required_fields = [
|
||||
"name",
|
||||
"python_name",
|
||||
"short_name",
|
||||
"type",
|
||||
"required",
|
||||
"default",
|
||||
"description",
|
||||
]
|
||||
for opt in CLI_OPTIONS:
|
||||
for field in required_fields:
|
||||
assert field in opt, f"Option {opt.get('name', 'unknown')} missing field: {field}"
|
||||
|
||||
def test_option_types_are_valid(self):
|
||||
"""Option types should be 'string' or 'boolean'"""
|
||||
valid_types = {"string", "boolean"}
|
||||
for opt in CLI_OPTIONS:
|
||||
assert opt["type"] in valid_types, f"Invalid type for {opt['name']}: {opt['type']}"
|
||||
|
||||
def test_python_name_is_snake_case(self):
|
||||
"""Python names should be snake_case (no hyphens)"""
|
||||
for opt in CLI_OPTIONS:
|
||||
assert "-" not in opt["python_name"], f"Python name should not contain hyphen: {opt['python_name']}"
|
||||
|
||||
def test_known_options_exist(self):
|
||||
"""Known options should exist in the list"""
|
||||
option_names = {opt["name"] for opt in CLI_OPTIONS}
|
||||
expected_options = {
|
||||
"output-dir",
|
||||
"password",
|
||||
"format",
|
||||
"quiet",
|
||||
"content-safety-off",
|
||||
"keep-line-breaks",
|
||||
"image-output",
|
||||
"image-format",
|
||||
}
|
||||
for expected in expected_options:
|
||||
assert expected in option_names, f"Expected option not found: {expected}"
|
||||
|
||||
def test_sanitize_option_exists(self):
|
||||
option_names = [opt["name"] for opt in CLI_OPTIONS]
|
||||
assert "sanitize" in option_names
|
||||
sanitize_opt = next(opt for opt in CLI_OPTIONS if opt["name"] == "sanitize")
|
||||
assert sanitize_opt["type"] == "boolean"
|
||||
assert sanitize_opt["default"] == False
|
||||
|
||||
|
||||
|
||||
class TestAddOptionsToParser:
|
||||
"""Tests for add_options_to_parser function"""
|
||||
|
||||
def test_adds_all_options(self):
|
||||
"""Should add all options to argparse parser"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
add_options_to_parser(parser)
|
||||
|
||||
# Parse empty args to get defaults
|
||||
args = parser.parse_args([])
|
||||
|
||||
# Check that all options are added
|
||||
for opt in CLI_OPTIONS:
|
||||
python_name = opt["python_name"]
|
||||
assert hasattr(args, python_name.replace("-", "_")), f"Option {python_name} not added to parser"
|
||||
|
||||
def test_boolean_options_default_to_false(self):
|
||||
"""Boolean options should default to False"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
add_options_to_parser(parser)
|
||||
args = parser.parse_args([])
|
||||
|
||||
for opt in CLI_OPTIONS:
|
||||
if opt["type"] == "boolean":
|
||||
python_name = opt["python_name"].replace("-", "_")
|
||||
assert getattr(args, python_name) is False, f"Boolean option {python_name} should default to False"
|
||||
|
||||
def test_string_options_default_to_none(self):
|
||||
"""String options should default to None"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
add_options_to_parser(parser)
|
||||
args = parser.parse_args([])
|
||||
|
||||
for opt in CLI_OPTIONS:
|
||||
if opt["type"] == "string":
|
||||
python_name = opt["python_name"].replace("-", "_")
|
||||
assert getattr(args, python_name) is None, f"String option {python_name} should default to None"
|
||||
|
||||
def test_short_options_work(self):
|
||||
"""Short option flags should work"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
add_options_to_parser(parser)
|
||||
|
||||
# Test with -o (short for --output-dir)
|
||||
args = parser.parse_args(["-o", "/output"])
|
||||
assert args.output_dir == "/output"
|
||||
|
||||
# Test with -f (short for --format)
|
||||
args = parser.parse_args(["-f", "json"])
|
||||
assert args.format == "json"
|
||||
|
||||
# Test with -q (short for --quiet)
|
||||
args = parser.parse_args(["-q"])
|
||||
assert args.quiet is True
|
||||
|
||||
def test_long_options_work(self):
|
||||
"""Long option flags should work"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
add_options_to_parser(parser)
|
||||
|
||||
args = parser.parse_args(["--output-dir", "/output", "--format", "json,markdown", "--quiet"])
|
||||
assert args.output_dir == "/output"
|
||||
assert args.format == "json,markdown"
|
||||
assert args.quiet is True
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Integration tests that actually run the JAR (slow)"""
|
||||
|
||||
import opendataloader_pdf
|
||||
|
||||
|
||||
def test_convert_generates_output(input_pdf, output_dir):
|
||||
"""Verify that convert() actually generates output files"""
|
||||
opendataloader_pdf.convert(
|
||||
input_path=str(input_pdf),
|
||||
output_dir=str(output_dir),
|
||||
format="json",
|
||||
quiet=True,
|
||||
)
|
||||
output = output_dir / "1901.03003.json"
|
||||
assert output.exists(), f"Output file not found at {output}"
|
||||
assert output.stat().st_size > 0, "Output file is empty"
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Tests for hybrid_server."""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
def test_gpu_detected_logging(caplog):
|
||||
"""GPU detection should log GPU name and CUDA version when available."""
|
||||
mock_torch = MagicMock()
|
||||
mock_torch.cuda.is_available.return_value = True
|
||||
mock_torch.cuda.get_device_name.return_value = "NVIDIA A100"
|
||||
mock_torch.version.cuda = "12.1"
|
||||
|
||||
with patch.dict("sys.modules", {"torch": mock_torch}):
|
||||
# Re-import to pick up the mock
|
||||
import importlib
|
||||
from opendataloader_pdf import hybrid_server
|
||||
|
||||
importlib.reload(hybrid_server)
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
# Simulate the GPU detection block from main()
|
||||
try:
|
||||
import torch
|
||||
if torch.cuda.is_available():
|
||||
gpu_name = torch.cuda.get_device_name(0)
|
||||
cuda_version = torch.version.cuda
|
||||
logging.getLogger(__name__).info(
|
||||
f"GPU detected: {gpu_name} (CUDA {cuda_version})"
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
assert "GPU detected: NVIDIA A100 (CUDA 12.1)" in caplog.text
|
||||
|
||||
|
||||
def test_no_gpu_logging(caplog):
|
||||
"""Should log CPU fallback when no GPU is available."""
|
||||
mock_torch = MagicMock()
|
||||
mock_torch.cuda.is_available.return_value = False
|
||||
|
||||
with patch.dict("sys.modules", {"torch": mock_torch}):
|
||||
with caplog.at_level(logging.INFO):
|
||||
try:
|
||||
import torch
|
||||
if torch.cuda.is_available():
|
||||
pass
|
||||
else:
|
||||
logging.getLogger(__name__).info("No GPU detected, using CPU.")
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
assert "No GPU detected, using CPU." in caplog.text
|
||||
|
||||
|
||||
def test_no_pytorch_logging(caplog):
|
||||
"""Should log CPU fallback when PyTorch is not installed."""
|
||||
with patch.dict("sys.modules", {"torch": None}):
|
||||
with caplog.at_level(logging.INFO):
|
||||
try:
|
||||
import torch # noqa: F811
|
||||
if torch.cuda.is_available():
|
||||
pass
|
||||
else:
|
||||
logging.getLogger(__name__).info("No GPU detected, using CPU.")
|
||||
except (ImportError, TypeError):
|
||||
logging.getLogger(__name__).info(
|
||||
"No GPU detected, using CPU. (PyTorch not installed)"
|
||||
)
|
||||
|
||||
assert "No GPU detected, using CPU. (PyTorch not installed)" in caplog.text
|
||||
|
||||
|
||||
def test_get_loop_setting_returns_asyncio_on_windows():
|
||||
"""On Windows, should return 'asyncio' to avoid uvloop errors (#323)."""
|
||||
from opendataloader_pdf.hybrid_server import _get_loop_setting
|
||||
|
||||
with patch("sys.platform", "win32"):
|
||||
assert _get_loop_setting() == "asyncio"
|
||||
|
||||
|
||||
def test_get_loop_setting_returns_auto_on_non_windows():
|
||||
"""On non-Windows platforms, should return 'auto' (uvloop if available)."""
|
||||
from opendataloader_pdf.hybrid_server import _get_loop_setting
|
||||
|
||||
with patch("sys.platform", "darwin"):
|
||||
assert _get_loop_setting() == "auto"
|
||||
|
||||
with patch("sys.platform", "linux"):
|
||||
assert _get_loop_setting() == "auto"
|
||||
@@ -0,0 +1,139 @@
|
||||
"""Tests for hybrid_server non-blocking conversion.
|
||||
|
||||
Verifies that converter.convert() runs in a thread pool rather than blocking
|
||||
the event loop. This is the root cause of issue #301: the Java client's
|
||||
3-second health check times out when the server is busy with a synchronous
|
||||
conversion call inside an async endpoint.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import threading
|
||||
import time
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_docling():
|
||||
"""Mock docling modules so tests don't need the actual dependency."""
|
||||
mock_converter = MagicMock()
|
||||
mock_result = MagicMock()
|
||||
mock_result.status.value = "success"
|
||||
mock_result.errors = []
|
||||
mock_result.input.page_count = 1
|
||||
mock_result.document.export_to_dict.return_value = {
|
||||
"pages": {"1": {}},
|
||||
"body": {},
|
||||
}
|
||||
|
||||
# Track which thread the conversion runs on
|
||||
convert_thread_name = {}
|
||||
|
||||
def tracking_convert(path, page_range=None):
|
||||
convert_thread_name["thread"] = threading.current_thread().name
|
||||
time.sleep(2)
|
||||
return mock_result
|
||||
|
||||
mock_converter.convert = tracking_convert
|
||||
mock_converter._convert_thread = convert_thread_name
|
||||
|
||||
mock_conversion_status = MagicMock()
|
||||
mock_conversion_status.PARTIAL_SUCCESS = "partial_success"
|
||||
|
||||
with patch.dict("sys.modules", {
|
||||
"docling": MagicMock(),
|
||||
"docling.datamodel.base_models": MagicMock(
|
||||
InputFormat=MagicMock(PDF="pdf"),
|
||||
ConversionStatus=mock_conversion_status,
|
||||
),
|
||||
"docling.datamodel.pipeline_options": MagicMock(),
|
||||
"docling.document_converter": MagicMock(),
|
||||
"uvicorn": MagicMock(),
|
||||
}):
|
||||
yield mock_converter
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app_with_converter(mock_docling):
|
||||
"""Create a FastAPI app with the mock converter."""
|
||||
import importlib
|
||||
from opendataloader_pdf import hybrid_server
|
||||
|
||||
importlib.reload(hybrid_server)
|
||||
|
||||
app = hybrid_server.create_app()
|
||||
hybrid_server.converter = mock_docling
|
||||
return app
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_convert_runs_in_thread_pool(app_with_converter, mock_docling):
|
||||
"""converter.convert() must run in a worker thread, not on the event loop.
|
||||
|
||||
When converter.convert() runs directly on the async event loop thread,
|
||||
it blocks all concurrent request handling — including /health checks.
|
||||
The fix wraps converter.convert() with asyncio.to_thread() so it runs
|
||||
in a separate thread.
|
||||
|
||||
Reproduces issue #301.
|
||||
"""
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
transport = ASGITransport(app=app_with_converter)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
event_loop_thread = threading.current_thread().name
|
||||
|
||||
response = await client.post(
|
||||
"/v1/convert/file",
|
||||
files={"files": ("test.pdf", b"%PDF-1.4 minimal", "application/pdf")},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
convert_thread = mock_docling._convert_thread.get("thread")
|
||||
assert convert_thread is not None, "converter.convert() was not called"
|
||||
assert convert_thread != event_loop_thread, (
|
||||
f"converter.convert() ran on the event loop thread '{event_loop_thread}'. "
|
||||
f"It must run in a worker thread via asyncio.to_thread() to avoid "
|
||||
f"blocking /health and other endpoints during long conversions."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_responds_during_conversion(app_with_converter):
|
||||
"""Health endpoint must respond quickly even during active conversion.
|
||||
|
||||
This is the user-facing symptom of issue #301: the Java CLI gets
|
||||
SocketTimeoutException when the hybrid server is busy processing
|
||||
another document. Mock sleep is 2s; health must respond under 0.2s.
|
||||
"""
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
transport = ASGITransport(app=app_with_converter)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
# Start conversion (takes 2s in mock)
|
||||
convert_task = asyncio.create_task(
|
||||
client.post(
|
||||
"/v1/convert/file",
|
||||
files={"files": ("test.pdf", b"%PDF-1.4 minimal", "application/pdf")},
|
||||
)
|
||||
)
|
||||
|
||||
# Wait for conversion to start in the worker thread
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
# Health check should respond quickly — well under the 2s conversion
|
||||
start = time.monotonic()
|
||||
health_response = await client.get("/health")
|
||||
health_time = time.monotonic() - start
|
||||
|
||||
assert health_response.status_code == 200
|
||||
assert health_response.json() == {"status": "ok"}
|
||||
assert health_time < 0.2, (
|
||||
f"Health endpoint took {health_time:.2f}s during conversion. "
|
||||
f"Expected < 0.2s. The event loop is likely blocked."
|
||||
)
|
||||
|
||||
convert_response = await convert_task
|
||||
assert convert_response.status_code == 200
|
||||
@@ -0,0 +1,447 @@
|
||||
"""Tests for the OCR option surface added to the hybrid server.
|
||||
|
||||
Covers:
|
||||
* `create_converter` delegation to docling's `get_ocr_factory`
|
||||
* Defaults preserve prior behavior (do_ocr=True, engine=easyocr)
|
||||
* `disable_ocr=True` -> do_ocr=False (#387)
|
||||
* Engine selection produces the matching OcrOptions subclass (#436, #439)
|
||||
* `psm` is applied only for Tesseract engines (silently ignored otherwise)
|
||||
* Unknown / denylisted engines exit with a clear message
|
||||
* argparse `--no-ocr` and `--force-ocr` are mutually exclusive
|
||||
* argparse `--ocr-engine` choices exclude `kserve_v2_ocr`
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import io
|
||||
from contextlib import redirect_stderr
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from opendataloader_pdf import hybrid_server
|
||||
|
||||
|
||||
# ---------- create_converter: factory delegation ----------
|
||||
|
||||
def _capture_pipeline_options(**kwargs):
|
||||
"""Call create_converter while mocking the docling document_converter so we
|
||||
can introspect the PdfPipelineOptions instance that would be passed in.
|
||||
"""
|
||||
captured = {}
|
||||
|
||||
def fake_pdf_format_option(*, pipeline_options):
|
||||
captured["pipeline_options"] = pipeline_options
|
||||
return object()
|
||||
|
||||
with patch(
|
||||
"docling.document_converter.DocumentConverter"
|
||||
) as mock_dc, patch(
|
||||
"docling.document_converter.PdfFormatOption", side_effect=fake_pdf_format_option
|
||||
):
|
||||
mock_dc.return_value = object()
|
||||
hybrid_server.create_converter(**kwargs)
|
||||
|
||||
return captured["pipeline_options"]
|
||||
|
||||
|
||||
def test_defaults_preserve_prior_behavior():
|
||||
"""No flags -> do_ocr=True, EasyOcrOptions (matches pre-change behavior)."""
|
||||
from docling.datamodel.pipeline_options import EasyOcrOptions
|
||||
|
||||
opts = _capture_pipeline_options()
|
||||
assert opts.do_ocr is True
|
||||
assert isinstance(opts.ocr_options, EasyOcrOptions)
|
||||
|
||||
|
||||
def test_disable_ocr_sets_do_ocr_false():
|
||||
"""`disable_ocr=True` -> do_ocr=False (#387)."""
|
||||
opts = _capture_pipeline_options(disable_ocr=True)
|
||||
assert opts.do_ocr is False
|
||||
|
||||
|
||||
def test_ocr_engine_tesseract_yields_tesseract_options():
|
||||
"""`ocr_engine='tesseract'` -> TesseractCliOcrOptions (#439 path)."""
|
||||
from docling.datamodel.pipeline_options import TesseractCliOcrOptions
|
||||
|
||||
opts = _capture_pipeline_options(ocr_engine="tesseract")
|
||||
assert isinstance(opts.ocr_options, TesseractCliOcrOptions)
|
||||
|
||||
|
||||
def test_ocr_engine_rapidocr_yields_rapidocr_options():
|
||||
"""`ocr_engine='rapidocr'` -> RapidOcrOptions (#436 path)."""
|
||||
from docling.datamodel.pipeline_options import RapidOcrOptions
|
||||
|
||||
opts = _capture_pipeline_options(ocr_engine="rapidocr")
|
||||
assert isinstance(opts.ocr_options, RapidOcrOptions)
|
||||
|
||||
|
||||
def test_force_full_page_ocr_propagates_to_engine_options():
|
||||
"""`force_full_page_ocr=True` flows to the engine's options instance."""
|
||||
opts = _capture_pipeline_options(
|
||||
ocr_engine="rapidocr", force_full_page_ocr=True
|
||||
)
|
||||
assert opts.ocr_options.force_full_page_ocr is True
|
||||
|
||||
|
||||
def test_ocr_lang_overrides_engine_default():
|
||||
"""A non-empty ocr_lang list replaces the engine's default lang."""
|
||||
opts = _capture_pipeline_options(
|
||||
ocr_engine="tesseract", ocr_lang=["mal", "eng"]
|
||||
)
|
||||
assert opts.ocr_options.lang == ["mal", "eng"]
|
||||
|
||||
|
||||
def test_psm_is_applied_to_tesseract():
|
||||
"""`psm` is applied when the engine is Tesseract."""
|
||||
opts = _capture_pipeline_options(ocr_engine="tesseract", psm=6)
|
||||
assert opts.ocr_options.psm == 6
|
||||
|
||||
|
||||
def test_psm_is_ignored_for_non_tesseract_engines():
|
||||
"""`psm` is silently ignored for engines that do not expose it."""
|
||||
# EasyOcrOptions has no `psm` field; passing psm should not raise.
|
||||
opts = _capture_pipeline_options(ocr_engine="easyocr", psm=6)
|
||||
assert not hasattr(opts.ocr_options, "psm")
|
||||
|
||||
|
||||
def test_ocr_engine_auto_yields_auto_options():
|
||||
"""`ocr_engine='auto'` -> OcrAutoOptions; engine choice is deferred to docling."""
|
||||
from docling.datamodel.pipeline_options import OcrAutoOptions
|
||||
|
||||
opts = _capture_pipeline_options(ocr_engine="auto")
|
||||
assert isinstance(opts.ocr_options, OcrAutoOptions)
|
||||
|
||||
|
||||
def test_unknown_engine_raises_value_error_with_clear_message():
|
||||
"""An unknown engine kind raises ValueError listing valid engines.
|
||||
|
||||
ValueError (not SystemExit) keeps the function library-friendly: programmatic
|
||||
callers can catch and retry with a different engine. main() relies on
|
||||
argparse `choices` to gate invalid CLI input separately.
|
||||
"""
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
_capture_pipeline_options(ocr_engine="bogus-engine")
|
||||
msg = str(excinfo.value)
|
||||
assert "bogus-engine" in msg
|
||||
assert "Available engines" in msg
|
||||
|
||||
|
||||
def test_create_converter_rejects_denylisted_engine():
|
||||
"""Programmatic callers cannot bypass `_OCR_ENGINE_DENYLIST` via create_converter().
|
||||
|
||||
The CLI layer rejects denylisted engines via argparse `choices`. This test
|
||||
locks in the same enforcement at the function-call layer, so importing the
|
||||
module and passing `ocr_engine="kserve_v2_ocr"` directly fails with a clear
|
||||
ValueError instead of building a converter that fails opaquely on the first
|
||||
request.
|
||||
"""
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
_capture_pipeline_options(ocr_engine="kserve_v2_ocr")
|
||||
msg = str(excinfo.value)
|
||||
assert "kserve_v2_ocr" in msg
|
||||
assert "hybrid local mode" in msg
|
||||
assert "Available engines" in msg
|
||||
# Must also mention the denylist explicitly so a future maintainer searching
|
||||
# for the constant lands here.
|
||||
assert "_OCR_ENGINE_DENYLIST" in msg
|
||||
|
||||
|
||||
# ---------- argparse: --no-ocr / --force-ocr / --ocr-engine / --psm ----------
|
||||
|
||||
def _build_parser_subset():
|
||||
"""Reconstruct the OCR-relevant argparse subset from main().
|
||||
|
||||
Mirrors the layout in hybrid_server.main() so changes to the CLI surface
|
||||
are caught by these tests. Uses the same `_OCR_ENGINE_DENYLIST` module
|
||||
constant as production code so tests stay in sync with main() automatically.
|
||||
"""
|
||||
from docling.models.factories import get_ocr_factory
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
ocr_mode = parser.add_mutually_exclusive_group()
|
||||
ocr_mode.add_argument("--force-ocr", action="store_true")
|
||||
ocr_mode.add_argument("--no-ocr", action="store_true")
|
||||
|
||||
choices = sorted(
|
||||
set(get_ocr_factory(allow_external_plugins=False).registered_kind)
|
||||
- hybrid_server._OCR_ENGINE_DENYLIST
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ocr-engine", default="easyocr", choices=choices
|
||||
)
|
||||
parser.add_argument("--psm", type=int, default=None)
|
||||
parser.add_argument("--ocr-lang", default=None)
|
||||
return parser
|
||||
|
||||
|
||||
def test_argparse_defaults():
|
||||
"""No flags -> no_ocr=False, force_ocr=False, engine=easyocr, psm=None."""
|
||||
args = _build_parser_subset().parse_args([])
|
||||
assert args.no_ocr is False
|
||||
assert args.force_ocr is False
|
||||
assert args.ocr_engine == "easyocr"
|
||||
assert args.psm is None
|
||||
|
||||
|
||||
def test_argparse_no_ocr_and_force_ocr_are_mutually_exclusive():
|
||||
"""Passing both --no-ocr and --force-ocr exits with an argparse error."""
|
||||
err = io.StringIO()
|
||||
with pytest.raises(SystemExit), redirect_stderr(err):
|
||||
_build_parser_subset().parse_args(["--no-ocr", "--force-ocr"])
|
||||
assert "not allowed with argument" in err.getvalue()
|
||||
|
||||
|
||||
def test_argparse_kserve_engine_is_rejected():
|
||||
"""`--ocr-engine kserve_v2_ocr` is rejected (denylist filter)."""
|
||||
err = io.StringIO()
|
||||
with pytest.raises(SystemExit), redirect_stderr(err):
|
||||
_build_parser_subset().parse_args(["--ocr-engine", "kserve_v2_ocr"])
|
||||
assert "invalid choice" in err.getvalue()
|
||||
|
||||
|
||||
def test_argparse_tesseract_path_for_issue_439():
|
||||
"""The CLI invocation that resolves #439 parses cleanly."""
|
||||
args = _build_parser_subset().parse_args(
|
||||
["--ocr-engine", "tesseract", "--ocr-lang", "mal", "--force-ocr"]
|
||||
)
|
||||
assert args.ocr_engine == "tesseract"
|
||||
assert args.ocr_lang == "mal"
|
||||
assert args.force_ocr is True
|
||||
assert args.no_ocr is False
|
||||
|
||||
|
||||
def test_argparse_psm_accepted_as_integer():
|
||||
"""`--psm 6` is parsed as an integer.
|
||||
|
||||
Range and semantics belong to Tesseract / docling; this server passes the
|
||||
integer through unchanged. Out-of-range values surface from docling /
|
||||
Tesseract at conversion time, not here.
|
||||
"""
|
||||
args = _build_parser_subset().parse_args(
|
||||
["--ocr-engine", "tesseract", "--psm", "6"]
|
||||
)
|
||||
assert args.psm == 6
|
||||
|
||||
|
||||
# ---------- _check_ocr_engine_available ----------
|
||||
|
||||
def test_engine_check_easyocr_always_ok():
|
||||
"""easyocr ships with the `[hybrid]` extra; check is a no-op."""
|
||||
ok, msg = hybrid_server._check_ocr_engine_available("easyocr")
|
||||
assert ok is True
|
||||
assert msg == ""
|
||||
|
||||
|
||||
def test_engine_check_auto_always_ok():
|
||||
"""`auto` defers engine choice to docling; check is a no-op."""
|
||||
ok, msg = hybrid_server._check_ocr_engine_available("auto")
|
||||
assert ok is True
|
||||
assert msg == ""
|
||||
|
||||
|
||||
def test_engine_check_unknown_kind_returns_false():
|
||||
"""Unknown engine kinds fail closed instead of silently passing.
|
||||
|
||||
Before this contract was tightened, the helper returned `(True, "")` for any
|
||||
engine name it did not recognize, so a docling release that registered a
|
||||
new engine kind would slip past the probe and fail at first conversion.
|
||||
Locks in the fail-closed behavior.
|
||||
"""
|
||||
ok, msg = hybrid_server._check_ocr_engine_available("hypothetical_new_engine")
|
||||
assert ok is False
|
||||
assert "hypothetical_new_engine" in msg
|
||||
# Maintainer-targeted hint: where to add the probe branch.
|
||||
assert "_check_ocr_engine_available" in msg
|
||||
|
||||
|
||||
def test_engine_check_tesseract_missing_binary():
|
||||
"""`tesseract` engine without the binary on PATH fails with an actionable message."""
|
||||
with patch("shutil.which", return_value=None):
|
||||
ok, msg = hybrid_server._check_ocr_engine_available("tesseract")
|
||||
assert ok is False
|
||||
assert "tesseract" in msg.lower()
|
||||
assert "PATH" in msg
|
||||
|
||||
|
||||
def test_engine_check_tesseract_present():
|
||||
"""`tesseract` engine passes when the binary resolves on PATH."""
|
||||
with patch("shutil.which", return_value="/usr/bin/tesseract"):
|
||||
ok, msg = hybrid_server._check_ocr_engine_available("tesseract")
|
||||
assert ok is True
|
||||
assert msg == ""
|
||||
|
||||
|
||||
def test_engine_check_tesserocr_missing_package():
|
||||
"""`tesserocr` engine without the Python package fails with install hint."""
|
||||
with patch("importlib.util.find_spec", return_value=None):
|
||||
ok, msg = hybrid_server._check_ocr_engine_available("tesserocr")
|
||||
assert ok is False
|
||||
assert "tesserocr" in msg
|
||||
assert "pip install" in msg
|
||||
|
||||
|
||||
def test_engine_check_rapidocr_missing_package():
|
||||
"""`rapidocr` engine without the Python package fails with install hint."""
|
||||
with patch("importlib.util.find_spec", return_value=None):
|
||||
ok, msg = hybrid_server._check_ocr_engine_available("rapidocr")
|
||||
assert ok is False
|
||||
assert "rapidocr" in msg
|
||||
assert "onnxruntime" in msg
|
||||
|
||||
|
||||
def test_engine_check_rapidocr_missing_onnxruntime():
|
||||
"""`rapidocr` installed without `onnxruntime` fails with a backend-specific hint."""
|
||||
# Return a truthy spec for rapidocr, None for onnxruntime, None for anything else.
|
||||
def fake_find_spec(name):
|
||||
return object() if name == "rapidocr" else None
|
||||
|
||||
with patch("importlib.util.find_spec", side_effect=fake_find_spec):
|
||||
ok, msg = hybrid_server._check_ocr_engine_available("rapidocr")
|
||||
assert ok is False
|
||||
assert "onnxruntime" in msg
|
||||
# The message should not duplicate the "rapidocr is not installed" wording.
|
||||
assert "rapidocr` Python package" not in msg
|
||||
|
||||
|
||||
def test_engine_check_ocrmac_off_macos():
|
||||
"""`ocrmac` selected on a non-Darwin platform fails with a clear platform message."""
|
||||
with patch("sys.platform", "linux"):
|
||||
ok, msg = hybrid_server._check_ocr_engine_available("ocrmac")
|
||||
assert ok is False
|
||||
assert "macOS" in msg
|
||||
|
||||
|
||||
def test_engine_check_ocrmac_on_macos_missing_package():
|
||||
"""`ocrmac` on macOS without the `ocrmac` Python package fails with install hint."""
|
||||
with patch("sys.platform", "darwin"), patch(
|
||||
"importlib.util.find_spec", return_value=None
|
||||
):
|
||||
ok, msg = hybrid_server._check_ocr_engine_available("ocrmac")
|
||||
assert ok is False
|
||||
assert "ocrmac" in msg
|
||||
assert "pip install" in msg
|
||||
# Should NOT be the platform-mismatch message.
|
||||
assert "macOS only" not in msg and "not macOS" not in msg
|
||||
|
||||
|
||||
# ---------- --no-ocr ignored-flag warning ----------
|
||||
|
||||
def _run_main_to_warning(argv, monkeypatch, caplog):
|
||||
"""Drive `main()` far enough to capture the --no-ocr warning, then short-circuit.
|
||||
|
||||
`main()` ends in `uvicorn.run(...)`; we patch `create_app` and `uvicorn.run`
|
||||
so the function returns cleanly after argparse + the warning logic without
|
||||
starting a server.
|
||||
"""
|
||||
monkeypatch.setattr("sys.argv", ["opendataloader-pdf-hybrid", *argv])
|
||||
|
||||
# Skip dep import checks — pytest already runs inside the [hybrid] env.
|
||||
monkeypatch.setattr(hybrid_server, "_check_dependencies", lambda: None)
|
||||
|
||||
# Avoid building a real DocumentConverter / FastAPI app.
|
||||
monkeypatch.setattr(hybrid_server, "create_app", lambda **kwargs: object())
|
||||
|
||||
# Stub uvicorn so main() returns instead of starting a server.
|
||||
fake_uvicorn = type("FakeUvicorn", (), {"run": staticmethod(lambda *a, **k: None)})
|
||||
monkeypatch.setitem(__import__("sys").modules, "uvicorn", fake_uvicorn)
|
||||
|
||||
caplog.set_level("WARNING", logger=hybrid_server.logger.name)
|
||||
hybrid_server.main()
|
||||
|
||||
|
||||
def test_no_ocr_warns_when_engine_explicitly_set(monkeypatch, caplog):
|
||||
"""`--no-ocr --ocr-engine tesseract` warns that --ocr-engine has no effect."""
|
||||
_run_main_to_warning(
|
||||
["--no-ocr", "--ocr-engine", "tesseract"], monkeypatch, caplog
|
||||
)
|
||||
warnings = [r.message for r in caplog.records if r.levelname == "WARNING"]
|
||||
assert any("--ocr-engine tesseract" in w for w in warnings), warnings
|
||||
|
||||
|
||||
def test_no_ocr_warns_when_engine_explicitly_set_to_easyocr(monkeypatch, caplog):
|
||||
"""Explicit `--ocr-engine easyocr` under --no-ocr is still inert and must warn.
|
||||
|
||||
argparse cannot distinguish "user typed easyocr" from "default was used", so
|
||||
main() inspects argv directly. This regression test locks the path in.
|
||||
"""
|
||||
_run_main_to_warning(
|
||||
["--no-ocr", "--ocr-engine", "easyocr"], monkeypatch, caplog
|
||||
)
|
||||
warnings = [r.message for r in caplog.records if r.levelname == "WARNING"]
|
||||
assert any("--ocr-engine easyocr" in w for w in warnings), warnings
|
||||
|
||||
|
||||
def test_no_ocr_no_warning_when_engine_left_default(monkeypatch, caplog):
|
||||
"""`--no-ocr` without --ocr-engine does not falsely report easyocr as inert."""
|
||||
_run_main_to_warning(["--no-ocr"], monkeypatch, caplog)
|
||||
warnings = [r.message for r in caplog.records if r.levelname == "WARNING"]
|
||||
# No "--ocr-engine" mention because the user did not type it.
|
||||
assert not any("--ocr-engine" in w for w in warnings), warnings
|
||||
|
||||
|
||||
def test_no_ocr_warns_when_ocr_lang_set(monkeypatch, caplog):
|
||||
"""`--no-ocr --ocr-lang ko` warns that --ocr-lang has no effect."""
|
||||
_run_main_to_warning(["--no-ocr", "--ocr-lang", "ko"], monkeypatch, caplog)
|
||||
warnings = [r.message for r in caplog.records if r.levelname == "WARNING"]
|
||||
assert any("--ocr-lang" in w for w in warnings), warnings
|
||||
|
||||
|
||||
def test_no_ocr_warns_when_psm_set(monkeypatch, caplog):
|
||||
"""`--no-ocr --psm 6` warns that --psm has no effect."""
|
||||
_run_main_to_warning(["--no-ocr", "--psm", "6"], monkeypatch, caplog)
|
||||
warnings = [r.message for r in caplog.records if r.levelname == "WARNING"]
|
||||
assert any("--psm 6" in w for w in warnings), warnings
|
||||
|
||||
|
||||
def test_no_ocr_alone_emits_no_warning(monkeypatch, caplog):
|
||||
"""`--no-ocr` on its own does not warn — there are no inert flags to call out."""
|
||||
_run_main_to_warning(["--no-ocr"], monkeypatch, caplog)
|
||||
warnings = [
|
||||
r.message
|
||||
for r in caplog.records
|
||||
if r.levelname == "WARNING" and "no effect" in r.message
|
||||
]
|
||||
assert warnings == []
|
||||
|
||||
|
||||
# ---------- main() exits when engine prerequisites are missing ----------
|
||||
|
||||
def test_main_exits_when_tesseract_binary_missing(monkeypatch, caplog):
|
||||
"""Selecting `--ocr-engine tesseract` without the binary on PATH exits at startup."""
|
||||
monkeypatch.setattr(
|
||||
"sys.argv", ["opendataloader-pdf-hybrid", "--ocr-engine", "tesseract"]
|
||||
)
|
||||
monkeypatch.setattr(hybrid_server, "_check_dependencies", lambda: None)
|
||||
monkeypatch.setattr(hybrid_server, "create_app", lambda **kwargs: object())
|
||||
monkeypatch.setattr("shutil.which", lambda _name: None)
|
||||
fake_uvicorn = type("FakeUvicorn", (), {"run": staticmethod(lambda *a, **k: None)})
|
||||
monkeypatch.setitem(__import__("sys").modules, "uvicorn", fake_uvicorn)
|
||||
|
||||
caplog.set_level("ERROR", logger=hybrid_server.logger.name)
|
||||
with pytest.raises(SystemExit) as excinfo:
|
||||
hybrid_server.main()
|
||||
assert excinfo.value.code == 2
|
||||
errors = [r.message for r in caplog.records if r.levelname == "ERROR"]
|
||||
assert any("tesseract" in e.lower() for e in errors), errors
|
||||
|
||||
|
||||
def test_main_skips_engine_check_when_no_ocr(monkeypatch, caplog):
|
||||
"""`--no-ocr --ocr-engine tesseract` skips the binary check entirely."""
|
||||
called = {"which": False}
|
||||
|
||||
def spy_which(_name):
|
||||
called["which"] = True
|
||||
return None # Would fail the check if it were called.
|
||||
|
||||
monkeypatch.setattr(
|
||||
"sys.argv",
|
||||
["opendataloader-pdf-hybrid", "--no-ocr", "--ocr-engine", "tesseract"],
|
||||
)
|
||||
monkeypatch.setattr(hybrid_server, "_check_dependencies", lambda: None)
|
||||
monkeypatch.setattr(hybrid_server, "create_app", lambda **kwargs: object())
|
||||
monkeypatch.setattr("shutil.which", spy_which)
|
||||
fake_uvicorn = type("FakeUvicorn", (), {"run": staticmethod(lambda *a, **k: None)})
|
||||
monkeypatch.setitem(__import__("sys").modules, "uvicorn", fake_uvicorn)
|
||||
|
||||
hybrid_server.main() # must not raise SystemExit
|
||||
assert called["which"] is False
|
||||
@@ -0,0 +1,299 @@
|
||||
"""Tests for PARTIAL_SUCCESS handling in hybrid server responses.
|
||||
|
||||
Validates that when Docling encounters errors during PDF preprocessing
|
||||
(e.g., Invalid code point), the hybrid server correctly reports:
|
||||
- partial_success status instead of success
|
||||
- list of failed page numbers
|
||||
- error messages from Docling
|
||||
"""
|
||||
|
||||
from opendataloader_pdf.hybrid_server import (
|
||||
_extract_failed_pages_from_errors,
|
||||
build_conversion_response,
|
||||
)
|
||||
|
||||
|
||||
class TestBuildConversionResponse:
|
||||
"""Tests for the build_conversion_response function."""
|
||||
|
||||
def test_success_status(self):
|
||||
"""Fully successful conversion should return status=success."""
|
||||
response = build_conversion_response(
|
||||
status_value="success",
|
||||
json_content={"pages": {"1": {}, "2": {}, "3": {}}},
|
||||
processing_time=1.5,
|
||||
errors=[],
|
||||
requested_pages=None,
|
||||
)
|
||||
assert response["status"] == "success"
|
||||
assert response["failed_pages"] == []
|
||||
assert response["processing_time"] == 1.5
|
||||
|
||||
def test_partial_success_status(self):
|
||||
"""PARTIAL_SUCCESS should return status=partial_success with failed pages."""
|
||||
response = build_conversion_response(
|
||||
status_value="partial_success",
|
||||
json_content={"pages": {"1": {}, "2": {}, "4": {}, "5": {}}},
|
||||
processing_time=2.0,
|
||||
errors=["Unknown page: pipeline terminated early"],
|
||||
requested_pages=(1, 5),
|
||||
)
|
||||
assert response["status"] == "partial_success"
|
||||
assert response["failed_pages"] == [3]
|
||||
assert response["errors"] == ["Unknown page: pipeline terminated early"]
|
||||
|
||||
def test_partial_success_multiple_failed_pages(self):
|
||||
"""Multiple failed pages should all be reported."""
|
||||
response = build_conversion_response(
|
||||
status_value="partial_success",
|
||||
json_content={"pages": {"1": {}, "3": {}, "5": {}}},
|
||||
processing_time=3.0,
|
||||
errors=[
|
||||
"Unknown page: pipeline terminated early",
|
||||
"Unknown page: pipeline terminated early",
|
||||
],
|
||||
requested_pages=(1, 5),
|
||||
)
|
||||
assert response["status"] == "partial_success"
|
||||
assert sorted(response["failed_pages"]) == [2, 4]
|
||||
|
||||
def test_partial_success_no_page_range_with_total_pages(self):
|
||||
"""When total_pages is provided, boundary page failures are detected."""
|
||||
# 5-page document, page 1 (first) and page 5 (last) failed
|
||||
response = build_conversion_response(
|
||||
status_value="partial_success",
|
||||
json_content={"pages": {"2": {}, "3": {}, "4": {}}},
|
||||
processing_time=2.0,
|
||||
errors=["error1", "error2"],
|
||||
requested_pages=None,
|
||||
total_pages=5,
|
||||
)
|
||||
assert response["status"] == "partial_success"
|
||||
assert response["failed_pages"] == [1, 5]
|
||||
|
||||
def test_partial_success_no_page_range_fallback(self):
|
||||
"""When no page range or total_pages, interior gaps are still detected."""
|
||||
response = build_conversion_response(
|
||||
status_value="partial_success",
|
||||
json_content={"pages": {"1": {}, "2": {}, "4": {}, "5": {}}},
|
||||
processing_time=2.0,
|
||||
errors=["Unknown page: pipeline terminated early"],
|
||||
requested_pages=None,
|
||||
)
|
||||
assert response["status"] == "partial_success"
|
||||
assert response["failed_pages"] == [3]
|
||||
|
||||
def test_success_no_errors_field(self):
|
||||
"""Successful conversion should have empty errors list."""
|
||||
response = build_conversion_response(
|
||||
status_value="success",
|
||||
json_content={"pages": {"1": {}, "2": {}}},
|
||||
processing_time=1.0,
|
||||
errors=[],
|
||||
requested_pages=None,
|
||||
)
|
||||
assert response["errors"] == []
|
||||
|
||||
def test_document_field_present(self):
|
||||
"""Response should contain document.json_content."""
|
||||
json_content = {"pages": {"1": {}}, "body": {"text": "hello"}}
|
||||
response = build_conversion_response(
|
||||
status_value="success",
|
||||
json_content=json_content,
|
||||
processing_time=1.0,
|
||||
errors=[],
|
||||
requested_pages=None,
|
||||
)
|
||||
assert response["document"]["json_content"] == json_content
|
||||
|
||||
def test_partial_success_first_page_failed_with_page_range(self):
|
||||
"""First page failure should be detected when page range is specified."""
|
||||
response = build_conversion_response(
|
||||
status_value="partial_success",
|
||||
json_content={"pages": {"2": {}, "3": {}}},
|
||||
processing_time=1.0,
|
||||
errors=["error"],
|
||||
requested_pages=(1, 3),
|
||||
)
|
||||
assert response["failed_pages"] == [1]
|
||||
|
||||
def test_partial_success_last_page_failed_with_page_range(self):
|
||||
"""Last page failure should be detected when page range is specified."""
|
||||
response = build_conversion_response(
|
||||
status_value="partial_success",
|
||||
json_content={"pages": {"1": {}, "2": {}}},
|
||||
processing_time=1.0,
|
||||
errors=["error"],
|
||||
requested_pages=(1, 3),
|
||||
)
|
||||
assert response["failed_pages"] == [3]
|
||||
|
||||
def test_partial_success_all_pages_failed(self):
|
||||
"""All pages failing should report every page in failed_pages."""
|
||||
response = build_conversion_response(
|
||||
status_value="partial_success",
|
||||
json_content={"pages": {}},
|
||||
processing_time=2.0,
|
||||
errors=["error1", "error2", "error3"],
|
||||
requested_pages=(1, 3),
|
||||
)
|
||||
assert response["status"] == "partial_success"
|
||||
assert response["failed_pages"] == [1, 2, 3]
|
||||
|
||||
def test_partial_success_all_pages_failed_with_total_pages(self):
|
||||
"""All pages failing with total_pages should report every page."""
|
||||
response = build_conversion_response(
|
||||
status_value="partial_success",
|
||||
json_content={"pages": {}},
|
||||
processing_time=2.0,
|
||||
errors=["error1", "error2"],
|
||||
requested_pages=None,
|
||||
total_pages=3,
|
||||
)
|
||||
assert response["status"] == "partial_success"
|
||||
assert response["failed_pages"] == [1, 2, 3]
|
||||
|
||||
def test_failure_status_no_failed_pages_detection(self):
|
||||
"""Failure status should not trigger failed page detection."""
|
||||
response = build_conversion_response(
|
||||
status_value="failure",
|
||||
json_content={"pages": {"1": {}}},
|
||||
processing_time=1.0,
|
||||
errors=["PDF conversion failed"],
|
||||
requested_pages=(1, 3),
|
||||
)
|
||||
assert response["status"] == "failure"
|
||||
assert response["failed_pages"] == []
|
||||
|
||||
def test_partial_success_missing_pages_key(self):
|
||||
"""json_content without 'pages' key should mark all requested pages as failed."""
|
||||
response = build_conversion_response(
|
||||
status_value="partial_success",
|
||||
json_content={"body": {"text": "hello"}},
|
||||
processing_time=1.0,
|
||||
errors=["error"],
|
||||
requested_pages=(1, 3),
|
||||
)
|
||||
assert response["status"] == "partial_success"
|
||||
assert response["failed_pages"] == [1, 2, 3]
|
||||
|
||||
|
||||
class TestExtractFailedPagesFromErrors:
|
||||
"""Tests for error message-based failed page detection."""
|
||||
|
||||
def test_std_bad_alloc_errors(self):
|
||||
"""Page numbers should be extracted from 'Page N: std::bad_alloc' messages."""
|
||||
errors = [
|
||||
"Page 26: std::bad_alloc",
|
||||
"Page 27: std::bad_alloc",
|
||||
"Page 28: std::bad_alloc",
|
||||
]
|
||||
assert _extract_failed_pages_from_errors(errors) == [26, 27, 28]
|
||||
|
||||
def test_mixed_error_formats(self):
|
||||
"""Only 'Page N:' prefixed messages should be matched."""
|
||||
errors = [
|
||||
"Page 5: Invalid code point",
|
||||
"Unknown page: pipeline terminated early",
|
||||
"Page 10: std::bad_alloc",
|
||||
]
|
||||
assert _extract_failed_pages_from_errors(errors) == [5, 10]
|
||||
|
||||
def test_no_page_errors(self):
|
||||
"""Non-page errors should return empty list."""
|
||||
errors = [
|
||||
"Unknown page: pipeline terminated early",
|
||||
"General error occurred",
|
||||
]
|
||||
assert _extract_failed_pages_from_errors(errors) == []
|
||||
|
||||
def test_bare_page_no_colon(self):
|
||||
"""Bare 'Page N' (no colon) should be matched when error_msg is empty."""
|
||||
assert _extract_failed_pages_from_errors(["Page 26"]) == [26]
|
||||
|
||||
|
||||
class TestBuildConversionResponseErrorParsing:
|
||||
"""Tests for failed page detection via error message parsing.
|
||||
|
||||
When docling includes failed pages as empty entries in the pages dict,
|
||||
gap detection fails. Error message parsing handles this case.
|
||||
"""
|
||||
|
||||
def test_failed_pages_with_empty_entries_in_pages_dict(self):
|
||||
"""Failed pages present as empty entries should still be detected via errors."""
|
||||
response = build_conversion_response(
|
||||
status_value="partial_success",
|
||||
json_content={"pages": {"1": {}, "2": {}, "3": {}, "4": {}, "5": {}}},
|
||||
processing_time=2.0,
|
||||
errors=["Page 4: std::bad_alloc", "Page 5: std::bad_alloc"],
|
||||
requested_pages=(1, 5),
|
||||
)
|
||||
assert response["failed_pages"] == [4, 5]
|
||||
|
||||
def test_boundary_pages_detected_via_errors(self):
|
||||
"""Boundary page failures should be detected even without page range."""
|
||||
response = build_conversion_response(
|
||||
status_value="partial_success",
|
||||
json_content={"pages": {"1": {}, "2": {}, "3": {}, "4": {}, "5": {}}},
|
||||
processing_time=2.0,
|
||||
errors=["Page 4: std::bad_alloc", "Page 5: std::bad_alloc"],
|
||||
requested_pages=None,
|
||||
total_pages=None,
|
||||
)
|
||||
assert response["failed_pages"] == [4, 5]
|
||||
|
||||
def test_both_strategies_combined(self):
|
||||
"""Union of error-parsed and gap-detected pages should be reported.
|
||||
|
||||
Page 2 is missing from dict (gap), pages 4-5 have error messages
|
||||
but are present as empty entries. All three must appear.
|
||||
"""
|
||||
response = build_conversion_response(
|
||||
status_value="partial_success",
|
||||
json_content={"pages": {"1": {}, "3": {}, "4": {}, "5": {}}},
|
||||
processing_time=2.0,
|
||||
errors=["Page 4: std::bad_alloc", "Page 5: std::bad_alloc"],
|
||||
requested_pages=(1, 5),
|
||||
)
|
||||
assert response["failed_pages"] == [2, 4, 5]
|
||||
|
||||
def test_overlap_between_gap_and_error_is_deduplicated(self):
|
||||
"""Same page from gap and error parsing should appear once."""
|
||||
response = build_conversion_response(
|
||||
status_value="partial_success",
|
||||
json_content={"pages": {"1": {}, "3": {}}},
|
||||
processing_time=1.0,
|
||||
errors=["Page 2: std::bad_alloc"],
|
||||
requested_pages=(1, 3),
|
||||
)
|
||||
assert response["failed_pages"] == [2]
|
||||
|
||||
def test_duplicate_page_in_errors(self):
|
||||
"""Duplicate page numbers in error messages should be deduplicated."""
|
||||
errors = [
|
||||
"Page 3: std::bad_alloc",
|
||||
"Page 3: Invalid code point",
|
||||
]
|
||||
assert _extract_failed_pages_from_errors(errors) == [3]
|
||||
|
||||
def test_no_page_pattern_errors_falls_back_to_gap(self):
|
||||
"""When errors lack 'Page N:' pattern, gap detection should still work."""
|
||||
response = build_conversion_response(
|
||||
status_value="partial_success",
|
||||
json_content={"pages": {"1": {}, "3": {}}},
|
||||
processing_time=1.0,
|
||||
errors=["Unknown page: pipeline terminated early"],
|
||||
requested_pages=(1, 3),
|
||||
)
|
||||
assert response["failed_pages"] == [2]
|
||||
|
||||
def test_empty_errors_with_partial_success(self):
|
||||
"""Partial success with no error messages should still detect gaps."""
|
||||
response = build_conversion_response(
|
||||
status_value="partial_success",
|
||||
json_content={"pages": {"1": {}, "3": {}}},
|
||||
processing_time=1.0,
|
||||
errors=[],
|
||||
requested_pages=(1, 3),
|
||||
)
|
||||
assert response["failed_pages"] == [2]
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Tests for `--picture-description-prompt` propagation in the hybrid server.
|
||||
|
||||
Covers PDFDLOSP-20: the CLI accepted `--picture-description-prompt` without
|
||||
error but the prompt was never written into `PictureDescriptionVlmOptions`,
|
||||
so output was byte-identical regardless of the user's prompt. These tests
|
||||
lock in both halves of the contract:
|
||||
|
||||
* A custom prompt reaches `PictureDescriptionVlmOptions.prompt`.
|
||||
* Omitting the prompt preserves docling's built-in default.
|
||||
* Blank / whitespace-only prompts are treated as "not provided" so they
|
||||
never silently inject an empty prompt into the VLM.
|
||||
* The whole feature is gated by `enrich_picture_description=True`.
|
||||
"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from opendataloader_pdf import hybrid_server
|
||||
|
||||
|
||||
def _capture_pipeline_options(**kwargs):
|
||||
"""Build a converter while mocking out docling's heavy bits so we can
|
||||
introspect the PdfPipelineOptions instance.
|
||||
"""
|
||||
captured = {}
|
||||
|
||||
def fake_pdf_format_option(*, pipeline_options):
|
||||
captured["pipeline_options"] = pipeline_options
|
||||
return object()
|
||||
|
||||
with patch(
|
||||
"docling.document_converter.DocumentConverter"
|
||||
) as mock_dc, patch(
|
||||
"docling.document_converter.PdfFormatOption", side_effect=fake_pdf_format_option
|
||||
):
|
||||
mock_dc.return_value = object()
|
||||
hybrid_server.create_converter(**kwargs)
|
||||
|
||||
return captured["pipeline_options"]
|
||||
|
||||
|
||||
def test_custom_prompt_is_forwarded_to_vlm_options():
|
||||
"""The user-supplied prompt must end up on PictureDescriptionVlmOptions.
|
||||
|
||||
Regression for PDFDLOSP-20.
|
||||
"""
|
||||
opts = _capture_pipeline_options(
|
||||
enrich_picture_description=True,
|
||||
picture_description_prompt="HELLO WORLD",
|
||||
)
|
||||
assert opts.picture_description_options is not None
|
||||
assert opts.picture_description_options.prompt == "HELLO WORLD"
|
||||
|
||||
|
||||
def test_default_prompt_is_preserved_when_user_omits_flag():
|
||||
"""When no prompt is given, docling's built-in default must survive.
|
||||
|
||||
Locks the current docling default. If a docling upgrade changes the
|
||||
phrase, this canary fires — at which point we decide whether to track
|
||||
the new default or pin our own.
|
||||
"""
|
||||
opts = _capture_pipeline_options(enrich_picture_description=True)
|
||||
assert opts.picture_description_options is not None
|
||||
assert (
|
||||
opts.picture_description_options.prompt
|
||||
== "Describe this image in a few sentences."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("blank", ["", " ", "\t\n"])
|
||||
def test_blank_prompt_falls_back_to_default(blank):
|
||||
"""Empty / whitespace-only prompts must not inject an empty prompt.
|
||||
|
||||
Otherwise we recreate the same class of silent-flag misbehavior that
|
||||
PDFDLOSP-20 reported.
|
||||
"""
|
||||
opts = _capture_pipeline_options(
|
||||
enrich_picture_description=True,
|
||||
picture_description_prompt=blank,
|
||||
)
|
||||
assert opts.picture_description_options is not None
|
||||
assert (
|
||||
opts.picture_description_options.prompt
|
||||
== "Describe this image in a few sentences."
|
||||
)
|
||||
|
||||
|
||||
def test_prompt_ignored_when_enrichment_disabled():
|
||||
"""Without --enrich-picture-description, the prompt has no destination.
|
||||
|
||||
Docling populates a default `picture_description_options` object on
|
||||
`PdfPipelineOptions` even when do_picture_description=False, so we
|
||||
assert on the gate (`do_picture_description`) rather than identity of
|
||||
the options object.
|
||||
"""
|
||||
opts = _capture_pipeline_options(
|
||||
enrich_picture_description=False,
|
||||
picture_description_prompt="HELLO WORLD",
|
||||
)
|
||||
assert opts.do_picture_description is False
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Tests for Unicode sanitization in hybrid server responses.
|
||||
|
||||
Validates that lone surrogates and null characters from Docling OCR output
|
||||
are sanitized before JSON serialization to prevent UnicodeEncodeError in
|
||||
Starlette's JSONResponse.render().
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from opendataloader_pdf.hybrid_server import sanitize_unicode
|
||||
|
||||
|
||||
class TestSanitizeUnicode:
|
||||
"""Tests for the sanitize_unicode function."""
|
||||
|
||||
def test_lone_surrogate_replaced(self):
|
||||
"""Lone surrogates should be replaced with U+FFFD."""
|
||||
data = {"text": "Hello \ud800 World"}
|
||||
result = sanitize_unicode(data)
|
||||
assert "\ud800" not in result["text"]
|
||||
assert "\ufffd" in result["text"]
|
||||
|
||||
def test_all_surrogate_range_replaced(self):
|
||||
"""All surrogate code points (U+D800 to U+DFFF) should be replaced."""
|
||||
data = {"text": "\ud800\udbff\udc00\udfff"}
|
||||
result = sanitize_unicode(data)
|
||||
assert result["text"] == "\ufffd" * 4
|
||||
|
||||
def test_null_character_replaced(self):
|
||||
"""Null characters should be replaced with U+FFFD."""
|
||||
data = {"text": "Hello\x00World"}
|
||||
result = sanitize_unicode(data)
|
||||
assert "\x00" not in result["text"]
|
||||
assert result["text"] == "Hello\ufffdWorld"
|
||||
|
||||
def test_nested_dict_sanitized(self):
|
||||
"""Nested dictionaries should be sanitized recursively."""
|
||||
data = {"level1": {"level2": {"text": "bad\ud800char"}}}
|
||||
result = sanitize_unicode(data)
|
||||
assert "\ud800" not in result["level1"]["level2"]["text"]
|
||||
assert "\ufffd" in result["level1"]["level2"]["text"]
|
||||
|
||||
def test_list_sanitized(self):
|
||||
"""Lists within the data should be sanitized."""
|
||||
data = {"items": ["good", "bad\ud800text", "also\x00bad"]}
|
||||
result = sanitize_unicode(data)
|
||||
assert result["items"][0] == "good"
|
||||
assert "\ud800" not in result["items"][1]
|
||||
assert "\x00" not in result["items"][2]
|
||||
|
||||
def test_clean_data_unchanged(self):
|
||||
"""Clean data without problematic characters should pass through unchanged."""
|
||||
data = {"text": "Hello World", "number": 42, "flag": True, "nothing": None}
|
||||
result = sanitize_unicode(data)
|
||||
assert result == data
|
||||
|
||||
def test_non_string_values_preserved(self):
|
||||
"""Non-string values (int, float, bool, None) should be preserved as-is."""
|
||||
data = {"int": 42, "float": 3.14, "bool": True, "none": None}
|
||||
result = sanitize_unicode(data)
|
||||
assert result == data
|
||||
|
||||
def test_sanitized_output_json_serializable(self):
|
||||
"""Sanitized output must survive json.dumps + encode('utf-8') without error."""
|
||||
data = {
|
||||
"status": "success",
|
||||
"document": {
|
||||
"json_content": {
|
||||
"body": {"text": "OCR text with \ud800 lone surrogate and \x00 null"}
|
||||
}
|
||||
},
|
||||
}
|
||||
result = sanitize_unicode(data)
|
||||
# This is the exact operation that Starlette's JSONResponse.render() performs
|
||||
json_bytes = json.dumps(result, ensure_ascii=False).encode("utf-8")
|
||||
assert isinstance(json_bytes, bytes)
|
||||
|
||||
def test_mixed_valid_and_invalid_unicode(self):
|
||||
"""Valid Unicode (including CJK, emoji) should be preserved alongside sanitization."""
|
||||
data = {"text": "Valid \u4e16\u754c \ud800 text"}
|
||||
result = sanitize_unicode(data)
|
||||
assert "\u4e16\u754c" in result["text"] # CJK preserved
|
||||
assert "\ud800" not in result["text"] # surrogate removed
|
||||
assert "\ufffd" in result["text"] # replacement added
|
||||
@@ -0,0 +1,153 @@
|
||||
"""Unit tests for runner.py error-handling behaviour.
|
||||
|
||||
Regression: when the JAR fails, the streaming branch already wrote the
|
||||
JAR's stdout to the console live, so the except handler must not re-emit
|
||||
the captured copy. The quiet branch, conversely, has not surfaced anything
|
||||
yet and is allowed to print the captured streams — but only once
|
||||
(``CalledProcessError.output`` and ``.stdout`` are the same attribute).
|
||||
"""
|
||||
|
||||
import io
|
||||
import subprocess
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from opendataloader_pdf import runner
|
||||
|
||||
|
||||
class _FakeAsFile:
|
||||
def __init__(self, path):
|
||||
self._path = path
|
||||
|
||||
def __enter__(self):
|
||||
return self._path
|
||||
|
||||
def __exit__(self, *_args):
|
||||
return False
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def patched_jar(monkeypatch, tmp_path):
|
||||
"""Bypass the real resources lookup so run_jar reaches subprocess."""
|
||||
fake_jar = tmp_path / "opendataloader-pdf-cli.jar"
|
||||
fake_jar.write_bytes(b"")
|
||||
fake_traversable = MagicMock()
|
||||
fake_traversable.joinpath = lambda *_a, **_kw: fake_jar
|
||||
monkeypatch.setattr(runner.resources, "files", lambda _pkg: fake_traversable)
|
||||
monkeypatch.setattr(runner.resources, "as_file", lambda p: _FakeAsFile(p))
|
||||
|
||||
|
||||
def test_streaming_failure_does_not_duplicate_output(monkeypatch, capsys, patched_jar):
|
||||
"""Streaming mode prints JAR output live; the except handler must not
|
||||
re-emit the captured copy on stderr."""
|
||||
jar_output = "Invalid page range format: '-10'\nusage: [options] ...\n"
|
||||
|
||||
fake_process = MagicMock()
|
||||
fake_process.stdout = iter([jar_output])
|
||||
fake_process.wait.return_value = 2
|
||||
fake_process.__enter__ = lambda self: self
|
||||
fake_process.__exit__ = lambda self, *_a: False
|
||||
|
||||
monkeypatch.setattr(runner.subprocess, "Popen", lambda *_a, **_kw: fake_process)
|
||||
|
||||
with pytest.raises(subprocess.CalledProcessError):
|
||||
runner.run_jar(["--bogus"], quiet=False)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
# JAR text appears exactly once: the live streaming write.
|
||||
assert "Invalid page range format" in captured.out
|
||||
assert captured.out.count("usage: [options]") == 1
|
||||
# The except handler did NOT re-emit the captured copy on stderr.
|
||||
assert "Invalid page range format" not in captured.err
|
||||
assert "usage: [options]" not in captured.err
|
||||
# Meta info is still surfaced.
|
||||
assert "Error running opendataloader-pdf CLI." in captured.err
|
||||
assert "Return code: 2" in captured.err
|
||||
|
||||
|
||||
def test_quiet_success_relays_stdout_but_not_stderr(monkeypatch, capsys, patched_jar):
|
||||
"""Quiet mode must relay the JAR's stdout (--to-stdout payload, folder
|
||||
summary line) to the caller while still suppressing the JAR's log
|
||||
stream (stderr). Regression: --quiet + --to-stdout produced no output,
|
||||
breaking pipe consumers."""
|
||||
result = subprocess.CompletedProcess(
|
||||
args=["java", "-jar", "fake.jar"],
|
||||
returncode=0,
|
||||
stdout="extracted text payload\n",
|
||||
stderr="[INFO] java log noise\n",
|
||||
)
|
||||
monkeypatch.setattr(runner.subprocess, "run", MagicMock(return_value=result))
|
||||
|
||||
returned = runner.run_jar(["doc.pdf", "--quiet", "--to-stdout"], quiet=True)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
# Payload reaches the caller's stdout exactly once.
|
||||
assert captured.out.count("extracted text payload") == 1
|
||||
# The JAR's log stream stays suppressed (that is what quiet means).
|
||||
assert "java log noise" not in captured.out
|
||||
assert "java log noise" not in captured.err
|
||||
# Return value is unchanged for library callers.
|
||||
assert returned == "extracted text payload\n"
|
||||
|
||||
|
||||
def test_quiet_relays_through_stdout_buffer_byte_path(monkeypatch, patched_jar):
|
||||
"""The production CLI writes the relayed payload through
|
||||
``sys.stdout.buffer`` (bytes), not the ``sys.stdout.write`` (str)
|
||||
fallback. ``capsys`` replaces ``sys.stdout`` with an object whose
|
||||
``.buffer`` semantics differ, so the sibling test only exercises the
|
||||
str branch. This drives the real byte-relay path and asserts the
|
||||
payload is encoded to UTF-8 and written exactly once, intact — covering
|
||||
the ``encode("utf-8", "replace")`` step that the str branch skips.
|
||||
"""
|
||||
payload = "héllo 한글 payload\n" # non-ASCII: exercises the utf-8 encode
|
||||
result = subprocess.CompletedProcess(
|
||||
args=["java", "-jar", "fake.jar"],
|
||||
returncode=0,
|
||||
stdout=payload,
|
||||
stderr="[INFO] java log noise\n",
|
||||
)
|
||||
monkeypatch.setattr(runner.subprocess, "run", MagicMock(return_value=result))
|
||||
|
||||
# Fake stdout with a real binary buffer, mirroring a genuine TextIOWrapper
|
||||
# (hasattr(sys.stdout, "buffer") is True), so run_jar takes the byte path.
|
||||
fake_stdout = MagicMock()
|
||||
fake_stdout.buffer = io.BytesIO()
|
||||
monkeypatch.setattr(runner.sys, "stdout", fake_stdout)
|
||||
|
||||
returned = runner.run_jar(["doc.pdf", "--quiet", "--to-stdout"], quiet=True)
|
||||
|
||||
written = fake_stdout.buffer.getvalue()
|
||||
# Byte path was taken: payload written as UTF-8 exactly once, intact.
|
||||
assert written == payload.encode("utf-8")
|
||||
assert written.decode("utf-8") == payload
|
||||
# The str fallback branch was NOT used.
|
||||
fake_stdout.write.assert_not_called()
|
||||
fake_stdout.buffer.flush() # buffer is flushed by run_jar; no error
|
||||
# Return value is unchanged for library callers.
|
||||
assert returned == payload
|
||||
|
||||
|
||||
def test_quiet_failure_prints_captured_streams_once(monkeypatch, capsys, patched_jar):
|
||||
"""Quiet mode captures output, so the except handler surfaces it — but
|
||||
must avoid the old bug where Output and Stdout (aliases) both printed."""
|
||||
error = subprocess.CalledProcessError(
|
||||
returncode=2,
|
||||
cmd=["java", "-jar", "fake.jar"],
|
||||
output="captured stdout text",
|
||||
stderr="captured stderr text",
|
||||
)
|
||||
monkeypatch.setattr(runner.subprocess, "run", MagicMock(side_effect=error))
|
||||
|
||||
with pytest.raises(subprocess.CalledProcessError):
|
||||
runner.run_jar(["--bogus"], quiet=True)
|
||||
|
||||
err = capsys.readouterr().err
|
||||
assert err.count("captured stdout text") == 1
|
||||
assert err.count("captured stderr text") == 1
|
||||
# The pre-fix code printed both "Output:" and "Stdout:" with the same text.
|
||||
assert "Output:" not in err
|
||||
assert "Stdout: captured stdout text" in err
|
||||
assert "Stderr: captured stderr text" in err
|
||||
assert "Error running opendataloader-pdf CLI." in err
|
||||
assert "Return code: 2" in err
|
||||
Generated
+3641
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user