-
📝 发票数据(可编辑)
+
📝 付款记录(可编辑)
diff --git a/tests/README.md b/tests/README.md
new file mode 100644
index 0000000..98e27a7
--- /dev/null
+++ b/tests/README.md
@@ -0,0 +1,23 @@
+---
+last_reviewed: 2026-06-11
+---
+
+# tests — 单元测试目录
+
+存放项目单元测试,使用 pytest 运行。
+
+## 测试清单
+
+| 文件 | 覆盖范围 |
+|------|---------|
+| `test_config.py` | 配置加载模块 |
+| `test_extractor.py` | 发票提取编排:空目录、提取失败、正常流程、分类结果、申请单分离、支付匹配 |
+| `test_invoice.py` | 发票分类、CSV 读写 |
+| `test_llm_extractor.py` | LLM 信息提取 |
+| `test_matcher.py` | 发票与支付记录匹配 |
+
+## 运行方式
+
+```bash
+uv run pytest tests/
+```
\ No newline at end of file
diff --git a/tests/test_extractor.py b/tests/test_extractor.py
new file mode 100644
index 0000000..ea5022c
--- /dev/null
+++ b/tests/test_extractor.py
@@ -0,0 +1,340 @@
+"""发票提取编排模块单元测试
+
+覆盖范围:
+- extract_invoices:空目录、提取失败、正常流程、分类结果、申请单分离
+"""
+
+from __future__ import annotations
+
+from pathlib import Path
+from typing import Any
+
+from src.doc.extractor import extract_invoices
+
+# 字段键名(与源码中的字符串字面量保持一致)
+K_INVOICE_TYPE = "invoice_type"
+K_INVOICE_NUMBER = "invoice_number"
+K_TOTAL_AMOUNT = "total_amount"
+K_ITEM_NAME = "item_name"
+K_PERSON_NAME = "person_name"
+K_CARD_DATE = "card_date"
+K_CARD_NO = "card_no"
+K_CARD_AMOUNT = "card_amount"
+K_MATCHED_INVOICES = "_matched_invoices"
+K_RELATIVE_INVOICE_COUNT = "relative_invoice_count"
+K_INVOICE_DETAIL = "invoice_detail"
+K_REMARK = "remark"
+
+# 发票类型
+INVOICE_TYPE_TRAIN = "train"
+INVOICE_TYPE_HOTEL = "hotel"
+INVOICE_TYPE_GENERAL = "general"
+INVOICE_TYPE_PAYMENT = "payment"
+DOCUMENT_TYPE_APPLICATION = "application"
+
+# ------------------------------------------------------------------
+# Fixture helpers
+# ------------------------------------------------------------------
+
+
+def _make_invoice(number: str, amount: float, inv_type: str = INVOICE_TYPE_GENERAL) -> dict[str, Any]:
+ inv: dict[str, Any] = {
+ K_INVOICE_NUMBER: number,
+ K_INVOICE_TYPE: inv_type,
+ K_TOTAL_AMOUNT: str(amount),
+ }
+ if inv_type == INVOICE_TYPE_TRAIN:
+ inv[K_PERSON_NAME] = f"person{number}"
+ elif inv_type == INVOICE_TYPE_GENERAL:
+ inv[K_ITEM_NAME] = f"item{number}"
+ return inv
+
+
+def _make_application() -> dict[str, Any]:
+ return {
+ K_INVOICE_TYPE: DOCUMENT_TYPE_APPLICATION,
+ "applicant": "张三",
+ }
+
+
+def _make_card(amount: float) -> dict[str, Any]:
+ return {
+ K_INVOICE_TYPE: INVOICE_TYPE_PAYMENT,
+ K_CARD_DATE: "2026-01-01",
+ K_CARD_NO: "6228480000000000",
+ K_CARD_AMOUNT: f"{amount:.2f}",
+ }
+
+
+# ------------------------------------------------------------------
+# extract_invoices
+# ------------------------------------------------------------------
+
+
+class TestExtractInvoices:
+ """extract_invoices 编排函数"""
+
+ def test_empty_directory(self, tmp_path: Path, monkeypatch):
+ monkeypatch.setattr("src.doc.extractor._find_all_files", lambda d: [])
+
+ records, apps, groups = extract_invoices(str(tmp_path))
+ assert records == []
+ assert apps == []
+ assert groups == {"travel": [], "general": [], "application": []}
+
+ def test_extraction_fails(self, tmp_path: Path, monkeypatch):
+ pdf = tmp_path / "broken.pdf"
+ pdf.touch()
+
+ monkeypatch.setattr("src.doc.extractor._find_all_files", lambda d: [pdf])
+ monkeypatch.setattr("src.doc.extractor._extract_document", lambda p, c: None)
+
+ records, apps, groups = extract_invoices(str(tmp_path))
+ assert records == []
+ assert apps == []
+ assert groups == {"travel": [], "general": [], "application": []}
+
+ def test_normal_flow_general_invoices(self, tmp_path: Path, monkeypatch):
+ pdf1 = tmp_path / "inv1.pdf"
+ pdf2 = tmp_path / "inv2.pdf"
+ pdf1.touch()
+ pdf2.touch()
+
+ inv1 = _make_invoice("INV001", 300.0, INVOICE_TYPE_GENERAL)
+ inv2 = _make_invoice("INV002", 200.0, INVOICE_TYPE_GENERAL)
+
+ monkeypatch.setattr("src.doc.extractor._find_all_files", lambda d: [pdf1, pdf2])
+
+ call_index = [0]
+
+ def fake_extract(path, cache_dir):
+ idx = call_index[0]
+ call_index[0] += 1
+ return inv1 if idx == 0 else inv2
+
+ monkeypatch.setattr("src.doc.extractor._extract_document", fake_extract)
+
+ def fake_match(invoices, cards):
+ return [
+ {
+ K_CARD_DATE: "2026-01-01",
+ K_CARD_NO: "6228480000000000",
+ K_CARD_AMOUNT: "500.00",
+ K_RELATIVE_INVOICE_COUNT: "2",
+ K_INVOICE_DETAIL: "",
+ K_REMARK: "",
+ K_MATCHED_INVOICES: [inv1, inv2],
+ }
+ ]
+
+ monkeypatch.setattr("src.doc.extractor.match_invoices_to_cards", fake_match)
+
+ records, apps, groups = extract_invoices(str(tmp_path))
+
+ assert len(records) == 1
+ assert records[0][K_RELATIVE_INVOICE_COUNT] == "2"
+ assert len(groups["travel"]) == 0
+ assert len(groups["general"]) == 2
+
+ def test_normal_flow_mixed_invoices(self, tmp_path: Path, monkeypatch):
+ pdf1 = tmp_path / "train.pdf"
+ pdf2 = tmp_path / "hotel.pdf"
+ pdf3 = tmp_path / "general.pdf"
+ pdf1.touch()
+ pdf2.touch()
+ pdf3.touch()
+
+ inv_train = _make_invoice("TRAIN001", 500.0, INVOICE_TYPE_TRAIN)
+ inv_hotel = _make_invoice("HOTEL001", 800.0, INVOICE_TYPE_HOTEL)
+ inv_general = _make_invoice("GEN001", 150.0, INVOICE_TYPE_GENERAL)
+
+ monkeypatch.setattr(
+ "src.doc.extractor._find_all_files",
+ lambda d: [pdf1, pdf2, pdf3],
+ )
+
+ invoices_list = [inv_train, inv_hotel, inv_general]
+ call_index = [0]
+
+ def fake_extract(path, cache_dir):
+ idx = call_index[0]
+ call_index[0] += 1
+ return invoices_list[idx]
+
+ monkeypatch.setattr("src.doc.extractor._extract_document", fake_extract)
+
+ def fake_match(invoices, cards):
+ return [
+ {
+ K_CARD_DATE: "2026-01-01",
+ K_CARD_NO: "6228480000000000",
+ K_CARD_AMOUNT: "500.00",
+ K_RELATIVE_INVOICE_COUNT: "1",
+ K_INVOICE_DETAIL: "",
+ K_REMARK: "",
+ K_MATCHED_INVOICES: [inv_train],
+ },
+ {
+ K_CARD_DATE: "2026-01-02",
+ K_CARD_NO: "6228480000000000",
+ K_CARD_AMOUNT: "800.00",
+ K_RELATIVE_INVOICE_COUNT: "1",
+ K_INVOICE_DETAIL: "",
+ K_REMARK: "",
+ K_MATCHED_INVOICES: [inv_hotel],
+ },
+ {
+ K_CARD_DATE: "",
+ K_CARD_NO: "",
+ K_CARD_AMOUNT: "",
+ K_RELATIVE_INVOICE_COUNT: "1",
+ K_INVOICE_DETAIL: "",
+ K_REMARK: "unmatched",
+ K_MATCHED_INVOICES: [inv_general],
+ },
+ ]
+
+ monkeypatch.setattr("src.doc.extractor.match_invoices_to_cards", fake_match)
+
+ records, apps, groups = extract_invoices(str(tmp_path))
+
+ assert len(records) == 3
+ assert len(groups["travel"]) == 2
+ assert len(groups["general"]) == 1
+
+ travel_numbers = {inv[K_INVOICE_NUMBER] for inv in groups["travel"]}
+ assert "TRAIN001" in travel_numbers
+ assert "HOTEL001" in travel_numbers
+ assert groups["general"][0][K_INVOICE_NUMBER] == "GEN001"
+
+ def test_application_documents_separated(self, tmp_path: Path, monkeypatch):
+ pdf1 = tmp_path / "invoice.pdf"
+ pdf2 = tmp_path / "application.pdf"
+ pdf1.touch()
+ pdf2.touch()
+
+ inv = _make_invoice("INV001", 300.0, INVOICE_TYPE_GENERAL)
+ app = _make_application()
+
+ monkeypatch.setattr(
+ "src.doc.extractor._find_all_files",
+ lambda d: [pdf1, pdf2],
+ )
+
+ results = [inv, app]
+ call_index = [0]
+
+ def fake_extract(path, cache_dir):
+ idx = call_index[0]
+ call_index[0] += 1
+ return results[idx]
+
+ monkeypatch.setattr("src.doc.extractor._extract_document", fake_extract)
+
+ def fake_match(invoices, cards):
+ return [
+ {
+ K_CARD_DATE: "2026-01-01",
+ K_CARD_NO: "6228480000000000",
+ K_CARD_AMOUNT: "300.00",
+ K_RELATIVE_INVOICE_COUNT: "1",
+ K_INVOICE_DETAIL: "",
+ K_REMARK: "",
+ K_MATCHED_INVOICES: [inv],
+ }
+ ]
+
+ monkeypatch.setattr("src.doc.extractor.match_invoices_to_cards", fake_match)
+
+ records, apps, groups = extract_invoices(str(tmp_path))
+
+ assert len(apps) == 1
+ assert apps[0]["applicant"] == "张三"
+ assert len(groups["application"]) == 1
+
+ def test_payment_records_included(self, tmp_path: Path, monkeypatch):
+ pdf1 = tmp_path / "invoice.pdf"
+ pdf2 = tmp_path / "card.png"
+ pdf1.touch()
+ pdf2.touch()
+
+ inv = _make_invoice("INV001", 300.0, INVOICE_TYPE_GENERAL)
+ card = _make_card(300.0)
+
+ monkeypatch.setattr(
+ "src.doc.extractor._find_all_files",
+ lambda d: [pdf1, pdf2],
+ )
+
+ results = [inv, card]
+ call_index = [0]
+
+ def fake_extract(path, cache_dir):
+ idx = call_index[0]
+ call_index[0] += 1
+ return results[idx]
+
+ monkeypatch.setattr("src.doc.extractor._extract_document", fake_extract)
+
+ def fake_match(invoices, cards):
+ return [
+ {
+ K_CARD_DATE: "2026-01-01",
+ K_CARD_NO: "6228480000000000",
+ K_CARD_AMOUNT: "300.00",
+ K_RELATIVE_INVOICE_COUNT: "1",
+ K_INVOICE_DETAIL: "",
+ K_REMARK: "",
+ K_MATCHED_INVOICES: [inv],
+ }
+ ]
+
+ monkeypatch.setattr("src.doc.extractor.match_invoices_to_cards", fake_match)
+
+ records, apps, groups = extract_invoices(str(tmp_path))
+
+ assert len(records) == 1
+ assert len(groups["general"]) == 1
+
+ def test_partial_extraction_failure(self, tmp_path: Path, monkeypatch):
+ pdf1 = tmp_path / "good.pdf"
+ pdf2 = tmp_path / "bad.pdf"
+ pdf1.touch()
+ pdf2.touch()
+
+ inv = _make_invoice("INV001", 300.0, INVOICE_TYPE_GENERAL)
+
+ monkeypatch.setattr(
+ "src.doc.extractor._find_all_files",
+ lambda d: [pdf1, pdf2],
+ )
+
+ results = [inv, None]
+ call_index = [0]
+
+ def fake_extract(path, cache_dir):
+ idx = call_index[0]
+ call_index[0] += 1
+ return results[idx]
+
+ monkeypatch.setattr("src.doc.extractor._extract_document", fake_extract)
+
+ def fake_match(invoices, cards):
+ return [
+ {
+ K_CARD_DATE: "2026-01-01",
+ K_CARD_NO: "6228480000000000",
+ K_CARD_AMOUNT: "300.00",
+ K_RELATIVE_INVOICE_COUNT: "1",
+ K_INVOICE_DETAIL: "",
+ K_REMARK: "",
+ K_MATCHED_INVOICES: [inv],
+ }
+ ]
+
+ monkeypatch.setattr("src.doc.extractor.match_invoices_to_cards", fake_match)
+
+ records, apps, groups = extract_invoices(str(tmp_path))
+
+ assert len(records) == 1
+ assert len(groups["general"]) == 1
diff --git a/tests/test_invoice.py b/tests/test_invoice.py
index f5c16b1..4744b44 100644
--- a/tests/test_invoice.py
+++ b/tests/test_invoice.py
@@ -5,14 +5,36 @@
from src.doc.invoice import (
INVOICE_LEVEL_COLUMNS,
- INVOICE_TYPE_GENERAL,
- INVOICE_TYPE_HOTEL,
- INVOICE_TYPE_TRAIN,
- INVOICE_TYPE_TRAVEL,
PAYMENT_RECORD_COLUMNS,
- classify_invoice_batch,
- is_travel_invoice,
)
+from src.doc.invoice import (
+ _classify_invoice_batch as classify_invoice_batch,
+)
+
+# 字段键名与发票类型(与源码中的字符串字面量保持一致)
+K_INVOICE_TYPE = "invoice_type"
+K_INVOICE_NUMBER = "invoice_number"
+K_TOTAL_AMOUNT = "total_amount"
+K_ITEM_NAME = "item_name"
+K_PERSON_NAME = "person_name"
+K_CARD_DATE = "card_date"
+K_CARD_NO = "card_no"
+K_CARD_AMOUNT = "card_amount"
+K_MATCHED_INVOICES = "_matched_invoices"
+K_RELATIVE_INVOICE_COUNT = "relative_invoice_count"
+K_INVOICE_DETAIL = "invoice_detail"
+K_REMARK = "remark"
+K_SOURCE_FILE = "source_file"
+
+INVOICE_TYPE_TRAIN = "train"
+INVOICE_TYPE_HOTEL = "hotel"
+INVOICE_TYPE_GENERAL = "general"
+DOCUMENT_TYPE_APPLICATION = "application"
+INVOICE_TYPE_TRAVEL = [INVOICE_TYPE_TRAIN, INVOICE_TYPE_HOTEL]
+
+
+def is_travel_invoice(invoice_type: str) -> bool:
+ return invoice_type in INVOICE_TYPE_TRAVEL
class TestInvoiceConstants:
@@ -47,7 +69,7 @@ class TestIsTravelInvoice:
assert is_travel_invoice(INVOICE_TYPE_GENERAL) is False
def test_unknown_not_travel(self) -> None:
- assert is_travel_invoice("未知类型") is False
+ assert is_travel_invoice("unknown") is False
class TestClassifyInvoiceBatch:
@@ -55,12 +77,12 @@ class TestClassifyInvoiceBatch:
def test_empty_list(self) -> None:
result = classify_invoice_batch([])
- assert result == {"travel": [], "general": []}
+ assert result == {"travel": [], "general": [], "application": []}
def test_all_travel(self) -> None:
invoices = [
- {"发票类型": INVOICE_TYPE_TRAIN, "发票号码": "001"},
- {"发票类型": INVOICE_TYPE_HOTEL, "发票号码": "002"},
+ {K_INVOICE_TYPE: INVOICE_TYPE_TRAIN, K_INVOICE_NUMBER: "001"},
+ {K_INVOICE_TYPE: INVOICE_TYPE_HOTEL, K_INVOICE_NUMBER: "002"},
]
result = classify_invoice_batch(invoices)
assert len(result["travel"]) == 2
@@ -68,7 +90,7 @@ class TestClassifyInvoiceBatch:
def test_all_general(self) -> None:
invoices = [
- {"发票类型": INVOICE_TYPE_GENERAL, "发票号码": "001"},
+ {K_INVOICE_TYPE: INVOICE_TYPE_GENERAL, K_INVOICE_NUMBER: "001"},
]
result = classify_invoice_batch(invoices)
assert len(result["travel"]) == 0
@@ -76,14 +98,14 @@ class TestClassifyInvoiceBatch:
def test_mixed(self) -> None:
invoices = [
- {"发票类型": INVOICE_TYPE_TRAIN, "发票号码": "001"},
- {"发票类型": INVOICE_TYPE_GENERAL, "发票号码": "002"},
+ {K_INVOICE_TYPE: INVOICE_TYPE_TRAIN, K_INVOICE_NUMBER: "001"},
+ {K_INVOICE_TYPE: INVOICE_TYPE_GENERAL, K_INVOICE_NUMBER: "002"},
]
result = classify_invoice_batch(invoices)
assert len(result["travel"]) == 1
assert len(result["general"]) == 1
def test_missing_type_defaults_to_general(self) -> None:
- invoices = [{"发票号码": "001"}]
+ invoices = [{K_INVOICE_NUMBER: "001"}]
result = classify_invoice_batch(invoices)
assert len(result["general"]) == 1
diff --git a/tests/test_llm_extractor.py b/tests/test_llm_extractor.py
new file mode 100644
index 0000000..c7103b2
--- /dev/null
+++ b/tests/test_llm_extractor.py
@@ -0,0 +1,202 @@
+"""LLM 信息提取模块单元测试
+
+覆盖范围:
+- _parse_json_response:纯 JSON、Markdown 包裹、带前缀、解析失败
+- _image_to_base64:图片转 base64
+- extract_document:成功提取、LLM 失败
+"""
+
+from __future__ import annotations
+
+import base64
+import json
+from pathlib import Path
+
+import pytest
+
+from src.doc.llm_extractor import (
+ _image_to_base64,
+ _parse_json_response,
+ extract_document,
+)
+
+# 字段键名(与源码中的字符串字面量保持一致)
+K_INVOICE_NUMBER = "invoice_number"
+K_TOTAL_AMOUNT = "total_amount"
+K_CARD_DATE = "card_date"
+K_CARD_NO = "card_no"
+K_CARD_AMOUNT = "card_amount"
+
+# ------------------------------------------------------------------
+# _parse_json_response
+# ------------------------------------------------------------------
+
+
+class TestParseJsonResponse:
+ """JSON 响应解析"""
+
+ def test_pure_json(self):
+ raw = json.dumps({K_INVOICE_NUMBER: "123456", K_TOTAL_AMOUNT: "100.00"})
+ result = _parse_json_response(raw)
+ assert result[K_INVOICE_NUMBER] == "123456"
+ assert result[K_TOTAL_AMOUNT] == "100.00"
+
+ def test_markdown_json_block(self):
+ raw = f'```json\n{{"{K_INVOICE_NUMBER}": "789"}}\n```'
+ result = _parse_json_response(raw)
+ assert result[K_INVOICE_NUMBER] == "789"
+
+ def test_markdown_block_without_lang(self):
+ raw = '```\n{"key": "value"}\n```'
+ result = _parse_json_response(raw)
+ assert result["key"] == "value"
+
+ def test_json_prefix(self):
+ raw = f'json\n{{"{K_INVOICE_NUMBER}": "001"}}'
+ result = _parse_json_response(raw)
+ assert result[K_INVOICE_NUMBER] == "001"
+
+ def test_json_prefix_with_whitespace(self):
+ raw = ' json \n{"a": 1}'
+ result = _parse_json_response(raw)
+ assert result["a"] == 1
+
+ def test_nested_json(self):
+ raw = json.dumps({"outer": {"inner": [1, 2, 3]}})
+ result = _parse_json_response(raw)
+ assert result["outer"]["inner"] == [1, 2, 3]
+
+ def test_whitespace_around_json(self):
+ raw = ' \n {"x": 42} \n '
+ result = _parse_json_response(raw)
+ assert result["x"] == 42
+
+ def test_invalid_json_raises(self):
+ with pytest.raises(ValueError):
+ _parse_json_response("not json at all")
+
+ def test_empty_string_raises(self):
+ with pytest.raises(ValueError):
+ _parse_json_response("")
+
+
+# ------------------------------------------------------------------
+# _image_to_base64
+# ------------------------------------------------------------------
+
+
+class TestImageToBase64:
+ """图片转 base64"""
+
+ def test_png_to_base64(self, tmp_path: Path):
+ content = b"\x89PNG\r\n\x1a\nfake_png_data"
+ img_path = tmp_path / "test.png"
+ img_path.write_bytes(content)
+
+ result = _image_to_base64(img_path)
+ assert isinstance(result, str)
+ assert base64.b64decode(result) == content
+
+ def test_jpg_to_base64(self, tmp_path: Path):
+ content = b"\xff\xd8\xff\xe0fake_jpg_data"
+ img_path = tmp_path / "test.jpg"
+ img_path.write_bytes(content)
+
+ result = _image_to_base64(img_path)
+ assert base64.b64decode(result) == content
+
+ def test_file_not_found_raises(self, tmp_path: Path):
+ img_path = tmp_path / "nonexistent.png"
+ with pytest.raises(FileNotFoundError):
+ _image_to_base64(img_path)
+
+ def test_returns_utf8_string(self, tmp_path: Path):
+ content = b"test_image_content"
+ img_path = tmp_path / "test.png"
+ img_path.write_bytes(content)
+
+ result = _image_to_base64(img_path)
+ assert type(result) is str
+
+
+# ------------------------------------------------------------------
+# extract_document (mock LLM)
+# ------------------------------------------------------------------
+
+
+class TestExtractDocument:
+ """图片提取:通过 mock _llm_query_multimodal 避免真实 LLM 调用"""
+
+ def _mock_multimodal(self, monkeypatch, response_text: str):
+ def fake_query(system_prompt, text, image_b64, max_tokens=4096):
+ return response_text
+
+ monkeypatch.setattr("src.doc.llm_extractor._llm_query_multimodal", fake_query)
+
+ def test_success(self, tmp_path: Path, monkeypatch):
+ img_path = tmp_path / "card.png"
+ img_path.write_bytes(b"fake_image")
+
+ mock_result = {
+ K_CARD_DATE: "2026-01-10",
+ K_CARD_NO: "6228480000000000",
+ K_CARD_AMOUNT: "500.00",
+ }
+ self._mock_multimodal(monkeypatch, json.dumps(mock_result))
+
+ result = extract_document(img_path)
+ assert result[K_CARD_DATE] == "2026-01-10"
+ assert result[K_CARD_NO] == "6228480000000000"
+ assert result[K_CARD_AMOUNT] == "500.00"
+
+ def test_llm_failure_propagates(self, tmp_path: Path, monkeypatch):
+ img_path = tmp_path / "card.png"
+ img_path.write_bytes(b"fake_image")
+
+ def fake_query(system_prompt, text, image_b64, max_tokens=4096):
+ raise RuntimeError("模型不可用")
+
+ monkeypatch.setattr("src.doc.llm_extractor._llm_query_multimodal", fake_query)
+ with pytest.raises(RuntimeError, match="模型不可用"):
+ extract_document(img_path)
+
+ def test_file_not_found_propagates(self, tmp_path: Path, monkeypatch):
+ img_path = tmp_path / "missing.png"
+ self._mock_multimodal(monkeypatch, "{}")
+ with pytest.raises(FileNotFoundError):
+ extract_document(img_path)
+
+ def test_markdown_wrapped_json(self, tmp_path: Path, monkeypatch):
+ img_path = tmp_path / "card.png"
+ img_path.write_bytes(b"fake_image")
+
+ mock_result = {
+ K_CARD_DATE: "2026-02-01",
+ K_CARD_NO: "6228481111111111",
+ K_CARD_AMOUNT: "300.00",
+ }
+ wrapped = f"```json\n{json.dumps(mock_result)}\n```"
+ self._mock_multimodal(monkeypatch, wrapped)
+
+ result = extract_document(img_path)
+ assert result[K_CARD_DATE] == "2026-02-01"
+
+ def test_image_encoded_as_base64(self, tmp_path: Path, monkeypatch):
+ img_path = tmp_path / "card.png"
+ expected_content = b"test_image_data"
+ img_path.write_bytes(expected_content)
+
+ received_b64 = None
+
+ def capture_b64(system_prompt, text, image_b64s, max_tokens=4096):
+ nonlocal received_b64
+ received_b64 = image_b64s
+ return json.dumps({K_CARD_DATE: "2026-01-01", K_CARD_NO: "0000", K_CARD_AMOUNT: "100"})
+
+ monkeypatch.setattr("src.doc.llm_extractor._llm_query_multimodal", capture_b64)
+ extract_document(img_path)
+
+ assert received_b64 is not None
+ assert isinstance(received_b64, list)
+ assert len(received_b64) >= 1
+ assert base64.b64decode(received_b64[0]) == expected_content
diff --git a/tests/test_matcher.py b/tests/test_matcher.py
new file mode 100644
index 0000000..46167d5
--- /dev/null
+++ b/tests/test_matcher.py
@@ -0,0 +1,564 @@
+"""发票与支付记录匹配模块单元测试
+
+覆盖范围:
+- 辅助函数:_safe_float, _relative_tolerance, _build_invoice_summary
+- 一对一匹配:精确匹配、容差内匹配、容差外不匹配
+- 一对多匹配:精确匹配阶段、贪心匹配阶段、回滚逻辑
+- 记录构建:_build_payment_records, _invoices_to_records
+- 端到端:match_invoices_to_cards(直接传入分类好的支付记录)
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+from src.doc.matcher import (
+ _build_invoice_summary,
+ _build_payment_records,
+ _invoices_to_records,
+ _match,
+ _match_one_to_many,
+ _match_one_to_one,
+ _relative_tolerance,
+ _safe_float,
+ match_invoices_to_cards,
+)
+
+# 字段键名(与源码中的字符串字面量保持一致)
+K_INVOICE_TYPE = "invoice_type"
+K_INVOICE_NUMBER = "invoice_number"
+K_TOTAL_AMOUNT = "total_amount"
+K_ITEM_NAME = "item_name"
+K_PERSON_NAME = "person_name"
+K_CARD_DATE = "card_date"
+K_CARD_NO = "card_no"
+K_CARD_AMOUNT = "card_amount"
+K_MATCHED_INVOICES = "_matched_invoices"
+K_RELATIVE_INVOICE_COUNT = "relative_invoice_count"
+K_INVOICE_DETAIL = "invoice_detail"
+K_REMARK = "remark"
+K_SOURCE_FILE = "source_file"
+
+INVOICE_TYPE_TRAIN = "train"
+INVOICE_TYPE_HOTEL = "hotel"
+INVOICE_TYPE_GENERAL = "general"
+
+# ------------------------------------------------------------------
+# Fixture helpers
+# ------------------------------------------------------------------
+
+
+def _make_invoice(number: str, amount: float, inv_type: str = INVOICE_TYPE_GENERAL) -> dict[str, Any]:
+ inv: dict[str, Any] = {
+ K_INVOICE_NUMBER: number,
+ K_INVOICE_TYPE: inv_type,
+ K_TOTAL_AMOUNT: str(amount),
+ }
+ if inv_type == INVOICE_TYPE_TRAIN:
+ inv[K_PERSON_NAME] = f"person{number}"
+ elif inv_type == INVOICE_TYPE_GENERAL:
+ inv[K_ITEM_NAME] = f"item{number}"
+ return inv
+
+
+def _make_card(date: str, amount: float, card_no: str = "6228480000000000") -> dict[str, Any]:
+ return {
+ K_CARD_DATE: date,
+ K_CARD_NO: card_no,
+ K_CARD_AMOUNT: str(amount),
+ K_SOURCE_FILE: "card.png",
+ }
+
+
+# ------------------------------------------------------------------
+# 辅助函数
+# ------------------------------------------------------------------
+
+
+class TestSafeFloat:
+ """安全浮点转换"""
+
+ def test_normal_string(self):
+ assert _safe_float("123.45") == 123.45
+
+ def test_with_comma(self):
+ assert _safe_float("1,234.56") == 1234.56
+
+ def test_none_returns_default(self):
+ assert _safe_float(None) == 0.0
+
+ def test_empty_string_returns_default(self):
+ assert _safe_float("") == 0.0
+
+ def test_whitespace_returns_default(self):
+ assert _safe_float(" ") == 0.0
+
+ def test_invalid_string_returns_default(self):
+ assert _safe_float("abc") == 0.0
+
+ def test_custom_default(self):
+ assert _safe_float(None, default=-1.0) == -1.0
+
+ def test_int_input(self):
+ assert _safe_float(42) == 42.0
+
+
+class TestRelativeTolerance:
+ """相对容差计算"""
+
+ def test_default_rate(self):
+ assert _relative_tolerance(1000) == 30.0
+
+ def test_custom_rate(self):
+ assert _relative_tolerance(1000, 0.03) == 30.0
+
+ def test_negative_base(self):
+ assert _relative_tolerance(-200, 0.03) == 6.0
+
+ def test_zero_base(self):
+ assert _relative_tolerance(0, 0.03) == 0.0
+
+
+class TestBuildInvoiceSummary:
+ """发票汇总字符串"""
+
+ def test_single_invoice(self):
+ invoices = [_make_invoice("INV001", 100.0)]
+ result = _build_invoice_summary(invoices)
+ assert "INV001" in result
+ assert "100.0" in result
+
+ def test_multiple_invoices(self):
+ invoices = [
+ _make_invoice("INV001", 100.0),
+ _make_invoice("INV002", 200.0),
+ ]
+ result = _build_invoice_summary(invoices)
+ assert " | " in result
+ assert "INV001" in result
+ assert "INV002" in result
+
+ def test_train_uses_person_name(self):
+ invoices = [_make_invoice("TRAIN001", 500.0, INVOICE_TYPE_TRAIN)]
+ result = _build_invoice_summary(invoices)
+ assert "personTRAIN001" in result
+ assert INVOICE_TYPE_TRAIN in result
+
+ def test_hotel_uses_fixed_label(self):
+ invoices = [_make_invoice("HOTEL001", 800.0, INVOICE_TYPE_HOTEL)]
+ result = _build_invoice_summary(invoices)
+ assert "hotel[hotel]" in result
+
+ def test_general_uses_project_name(self):
+ invoices = [_make_invoice("INV001", 100.0, INVOICE_TYPE_GENERAL)]
+ result = _build_invoice_summary(invoices)
+ assert "itemINV001" in result
+
+ def test_missing_fields_falls_back_to_number(self):
+ inv = {K_INVOICE_NUMBER: "INV001", K_TOTAL_AMOUNT: "50.0"}
+ result = _build_invoice_summary([inv])
+ assert "INV001" in result
+
+ def test_empty_invoices_returns_empty(self):
+ result = _build_invoice_summary([])
+ assert result == ""
+
+
+# ------------------------------------------------------------------
+# 一对一匹配
+# ------------------------------------------------------------------
+
+
+class TestMatchOneToOne:
+ """一对一匹配"""
+
+ def test_exact_match(self):
+ invoices = [_make_invoice("A", 500), _make_invoice("B", 300)]
+ cards = [_make_card("2026-01-01", 500), _make_card("2026-01-02", 300)]
+ for inv in invoices:
+ inv["_amount"] = _safe_float(inv[K_TOTAL_AMOUNT])
+ for card in cards:
+ card["_amount"] = _safe_float(card[K_CARD_AMOUNT])
+
+ assigned: set[int] = set()
+ result: dict[int, list[int]] = {}
+ _match_one_to_one(invoices, cards, 0.03, assigned, result)
+
+ assert 0 in result
+ assert 1 in result
+ assert result[0] == [0]
+ assert result[1] == [1]
+
+ def test_within_tolerance(self):
+ invoices = [_make_invoice("A", 500)]
+ cards = [_make_card("2026-01-01", 490)]
+ for inv in invoices:
+ inv["_amount"] = _safe_float(inv[K_TOTAL_AMOUNT])
+ for card in cards:
+ card["_amount"] = _safe_float(card[K_CARD_AMOUNT])
+
+ assigned: set[int] = set()
+ result: dict[int, list[int]] = {}
+ _match_one_to_one(invoices, cards, 0.03, assigned, result)
+
+ assert 0 in result
+
+ def test_outside_tolerance_no_match(self):
+ invoices = [_make_invoice("A", 500)]
+ cards = [_make_card("2026-01-01", 400)]
+ for inv in invoices:
+ inv["_amount"] = _safe_float(inv[K_TOTAL_AMOUNT])
+ for card in cards:
+ card["_amount"] = _safe_float(card[K_CARD_AMOUNT])
+
+ assigned: set[int] = set()
+ result: dict[int, list[int]] = {}
+ _match_one_to_one(invoices, cards, 0.03, assigned, result)
+
+ assert 0 not in result
+
+ def test_sorted_by_amount_desc(self):
+ invoices = [_make_invoice("A", 100), _make_invoice("B", 500)]
+ cards = [_make_card("2026-01-01", 100), _make_card("2026-01-02", 500)]
+ for inv in invoices:
+ inv["_amount"] = _safe_float(inv[K_TOTAL_AMOUNT])
+ for card in cards:
+ card["_amount"] = _safe_float(card[K_CARD_AMOUNT])
+
+ invoices.sort(key=lambda i: i["_amount"], reverse=True)
+ cards.sort(key=lambda c: c["_amount"], reverse=True)
+
+ assigned: set[int] = set()
+ result: dict[int, list[int]] = {}
+ _match_one_to_one(invoices, cards, 0.03, assigned, result)
+
+ assert len(result) == 2
+
+
+# ------------------------------------------------------------------
+# 一对多匹配
+# ------------------------------------------------------------------
+
+
+class TestMatchOneToMany:
+ """一对多匹配"""
+
+ def _prepare(self, invoices, cards):
+ for inv in invoices:
+ inv["_amount"] = _safe_float(inv[K_TOTAL_AMOUNT])
+ for card in cards:
+ card["_amount"] = _safe_float(card[K_CARD_AMOUNT])
+ invoices.sort(key=lambda i: i["_amount"], reverse=True)
+ cards.sort(key=lambda c: c["_amount"], reverse=True)
+
+ def test_exact_match_phase(self):
+ invoices = [
+ _make_invoice("A", 500),
+ _make_invoice("B", 300),
+ _make_invoice("C", 200),
+ ]
+ cards = [_make_card("2026-01-01", 500)]
+ self._prepare(invoices, cards)
+
+ assigned: set[int] = set()
+ result: dict[int, list[int]] = {}
+ _match_one_to_many(invoices, cards, 0.03, assigned, result)
+
+ assert 0 in result
+ matched_inv = invoices[result[0][0]]
+ assert matched_inv["_amount"] == 500.0
+
+ def test_greedy_match_multiple_invoices(self):
+ invoices = [
+ _make_invoice("A", 300),
+ _make_invoice("B", 200),
+ _make_invoice("C", 100),
+ ]
+ cards = [_make_card("2026-01-01", 500)]
+ self._prepare(invoices, cards)
+
+ assigned: set[int] = set()
+ result: dict[int, list[int]] = {}
+ _match_one_to_many(invoices, cards, 0.03, assigned, result)
+
+ assert 0 in result
+ assert len(result[0]) == 2
+
+ def test_greedy_with_remaining_invoice(self):
+ invoices = [
+ _make_invoice("A", 300),
+ _make_invoice("B", 200),
+ _make_invoice("C", 100),
+ ]
+ cards = [_make_card("2026-01-01", 500)]
+ self._prepare(invoices, cards)
+
+ assigned: set[int] = set()
+ result: dict[int, list[int]] = {}
+ _match_one_to_many(invoices, cards, 0.03, assigned, result)
+
+ assert 0 in result
+ assert len(assigned) == 2
+
+ def test_rollback_when_over_shooting(self):
+ invoices = [
+ _make_invoice("A", 600),
+ _make_invoice("B", 500),
+ ]
+ cards = [_make_card("2026-01-01", 1000)]
+ self._prepare(invoices, cards)
+
+ assigned: set[int] = set()
+ result: dict[int, list[int]] = {}
+ _match_one_to_many(invoices, cards, 0.03, assigned, result)
+
+ assert 0 in result
+ assert len(result[0]) == 1
+
+ def test_zero_amount_card_skipped(self):
+ invoices = [_make_invoice("A", 100)]
+ cards = [_make_card("2026-01-01", 0)]
+ self._prepare(invoices, cards)
+
+ assigned: set[int] = set()
+ result: dict[int, list[int]] = {}
+ _match_one_to_many(invoices, cards, 0.03, assigned, result)
+
+ assert 0 not in result
+
+ def test_zero_amount_invoice_skipped(self):
+ invoices = [
+ _make_invoice("A", 100),
+ _make_invoice("B", 0),
+ ]
+ cards = [_make_card("2026-01-01", 100)]
+ self._prepare(invoices, cards)
+
+ assigned: set[int] = set()
+ result: dict[int, list[int]] = {}
+ _match_one_to_many(invoices, cards, 0.03, assigned, result)
+
+ assert 0 in result
+ matched_inv = invoices[result[0][0]]
+ assert matched_inv["_amount"] == 100.0
+
+ def test_multiple_cards_greedy(self):
+ invoices = [
+ _make_invoice("A", 300),
+ _make_invoice("B", 200),
+ _make_invoice("C", 150),
+ _make_invoice("D", 100),
+ ]
+ cards = [
+ _make_card("2026-01-01", 500),
+ _make_card("2026-01-02", 150),
+ ]
+ self._prepare(invoices, cards)
+
+ assigned: set[int] = set()
+ result: dict[int, list[int]] = {}
+ _match_one_to_many(invoices, cards, 0.03, assigned, result)
+
+ assert 0 in result
+ assert 1 in result
+
+
+# ------------------------------------------------------------------
+# _match 路由
+# ------------------------------------------------------------------
+
+
+class TestMatchRouter:
+ """_match 根据数量选择策略"""
+
+ def _prepare(self, invoices, cards):
+ for inv in invoices:
+ inv["_amount"] = _safe_float(inv[K_TOTAL_AMOUNT])
+ for card in cards:
+ card["_amount"] = _safe_float(card[K_CARD_AMOUNT])
+ invoices.sort(key=lambda i: i["_amount"], reverse=True)
+ cards.sort(key=lambda c: c["_amount"], reverse=True)
+
+ def test_equal_count_routes_to_one_to_one(self):
+ invoices = [_make_invoice("A", 500)]
+ cards = [_make_card("2026-01-01", 500)]
+ self._prepare(invoices, cards)
+
+ result = _match(cards, invoices, 0.03)
+ assert 0 in result
+
+ def test_more_invoices_routes_to_one_to_many(self):
+ invoices = [_make_invoice("A", 300), _make_invoice("B", 200)]
+ cards = [_make_card("2026-01-01", 500)]
+ self._prepare(invoices, cards)
+
+ result = _match(cards, invoices, 0.03)
+ assert 0 in result
+
+
+# ------------------------------------------------------------------
+# 记录构建
+# ------------------------------------------------------------------
+
+
+class TestBuildPaymentRecords:
+ """构建支付记录列表"""
+
+ def _prepare(self, invoices, cards):
+ for inv in invoices:
+ inv["_amount"] = _safe_float(inv[K_TOTAL_AMOUNT])
+ for card in cards:
+ card["_amount"] = _safe_float(card[K_CARD_AMOUNT])
+
+ def test_matched_records_have_card_info(self):
+ invoices = [_make_invoice("A", 500)]
+ cards = [_make_card("2026-01-01", 500)]
+ self._prepare(invoices, cards)
+
+ card_to_invoices = {0: [0]}
+ records = _build_payment_records(cards, invoices, card_to_invoices)
+
+ assert len(records) == 1
+ assert records[0][K_CARD_DATE] == "2026-01-01"
+ assert records[0][K_RELATIVE_INVOICE_COUNT] == "1"
+ assert "unmatched" not in records[0][K_REMARK]
+
+ def test_unmatched_invoices_become_separate_records(self):
+ invoices = [_make_invoice("A", 500), _make_invoice("B", 300)]
+ cards = [_make_card("2026-01-01", 500)]
+ self._prepare(invoices, cards)
+
+ card_to_invoices = {0: [0]}
+ records = _build_payment_records(cards, invoices, card_to_invoices)
+
+ assert len(records) == 2
+ unmatched = [r for r in records if r[K_REMARK] == "unmatched"]
+ assert len(unmatched) == 1
+
+ def test_empty_mapping_returns_no_records(self):
+ invoices = []
+ cards = []
+ records = _build_payment_records(cards, invoices, {})
+ assert records == []
+
+
+class TestInvoicesToRecords:
+ """无刷卡记录时将发票转为独立记录"""
+
+ def test_single_invoice(self):
+ invoices = [_make_invoice("A", 100)]
+ records = _invoices_to_records(invoices)
+ assert len(records) == 1
+ assert records[0][K_RELATIVE_INVOICE_COUNT] == "1"
+
+ def test_multiple_invoices(self):
+ invoices = [_make_invoice("A", 100), _make_invoice("B", 200)]
+ records = _invoices_to_records(invoices)
+ assert len(records) == 2
+
+ def test_empty_invoices(self):
+ records = _invoices_to_records([])
+ assert records == []
+
+
+# ------------------------------------------------------------------
+# 端到端集成
+# ------------------------------------------------------------------
+
+
+class TestMatchInvoicesToCards:
+ """match_invoices_to_cards 端到端测试"""
+
+ def test_no_cards_returns_invoice_records(self):
+ invoices = [_make_invoice("A", 100), _make_invoice("B", 200)]
+ result = match_invoices_to_cards(invoices, cards=None)
+
+ assert len(result) == 2
+ for rec in result:
+ assert rec[K_CARD_DATE] == ""
+ assert rec[K_CARD_NO] == ""
+
+ def test_one_to_one_end_to_end(self):
+ cards = [
+ {
+ K_CARD_DATE: "2026-01-01",
+ K_CARD_NO: "6228480000000000",
+ K_CARD_AMOUNT: "500.00",
+ }
+ ]
+ invoices = [_make_invoice("A", 500)]
+ result = match_invoices_to_cards(invoices, cards=cards)
+
+ assert len(result) == 1
+ assert result[0][K_CARD_DATE] == "2026-01-01"
+ assert result[0][K_RELATIVE_INVOICE_COUNT] == "1"
+ assert "_amount" not in result[0]
+
+ def test_one_to_many_end_to_end(self):
+ cards = [
+ {
+ K_CARD_DATE: "2026-01-01",
+ K_CARD_NO: "6228480000000000",
+ K_CARD_AMOUNT: "500.00",
+ }
+ ]
+ invoices = [
+ _make_invoice("A", 300),
+ _make_invoice("B", 200),
+ ]
+ result = match_invoices_to_cards(invoices, cards=cards)
+
+ assert len(result) == 1
+ assert result[0][K_RELATIVE_INVOICE_COUNT] == "2"
+
+ def test_unmatched_invoices_included(self):
+ cards = [
+ {
+ K_CARD_DATE: "2026-01-01",
+ K_CARD_NO: "6228480000000000",
+ K_CARD_AMOUNT: "500.00",
+ }
+ ]
+ invoices = [
+ _make_invoice("A", 500),
+ _make_invoice("B", 100),
+ ]
+ result = match_invoices_to_cards(invoices, cards=cards)
+
+ assert len(result) == 2
+ unmatched = [r for r in result if r[K_REMARK] == "unmatched"]
+ assert len(unmatched) == 1
+
+ def test_internal_fields_cleaned(self):
+ cards = [
+ {
+ K_CARD_DATE: "2026-01-01",
+ K_CARD_NO: "6228480000000000",
+ K_CARD_AMOUNT: "500.00",
+ }
+ ]
+ invoices = [_make_invoice("A", 500)]
+ result = match_invoices_to_cards(invoices, cards=cards)
+
+ for inv in result[0][K_MATCHED_INVOICES]:
+ assert "_amount" not in inv
+
+ def test_empty_cards_list(self):
+ invoices = [_make_invoice("A", 100)]
+ result = match_invoices_to_cards(invoices, cards=[])
+
+ assert len(result) == 1
+ assert result[0][K_CARD_DATE] == ""
+
+ def test_multiple_cards(self):
+ cards = [
+ {K_CARD_DATE: "2026-01-01", K_CARD_NO: "6228480000000001", K_CARD_AMOUNT: "500.00"},
+ {K_CARD_DATE: "2026-01-02", K_CARD_NO: "6228480000000002", K_CARD_AMOUNT: "300.00"},
+ ]
+ invoices = [
+ _make_invoice("A", 500),
+ _make_invoice("B", 300),
+ ]
+ result = match_invoices_to_cards(invoices, cards=cards)
+
+ assert len(result) == 2
diff --git a/uv.lock b/uv.lock
index 6a182b4..f40a3dd 100644
--- a/uv.lock
+++ b/uv.lock
@@ -229,8 +229,8 @@ dependencies = [
{ name = "flask" },
{ name = "llama-index" },
{ name = "llama-index-llms-openai-like" },
- { name = "pdfplumber" },
{ name = "playwright" },
+ { name = "pymupdf" },
{ name = "pywin32" },
]
@@ -249,8 +249,8 @@ requires-dist = [
{ name = "flask", specifier = ">=3.0" },
{ name = "llama-index", specifier = ">=0.12.0" },
{ name = "llama-index-llms-openai-like", specifier = "==0.7.2" },
- { name = "pdfplumber", specifier = ">=0.10" },
{ name = "playwright", specifier = ">=1.40" },
+ { name = "pymupdf", specifier = ">=1.24" },
{ name = "pywin32", specifier = ">=306" },
]
@@ -299,63 +299,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" },
]
-[[package]]
-name = "cffi"
-version = "2.0.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pycparser", marker = "implementation_name != 'PyPy'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
- { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
- { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
- { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
- { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
- { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
- { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
- { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
- { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
- { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
- { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
- { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
- { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
- { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
- { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
- { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
- { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
- { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
- { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
- { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
- { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
- { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
- { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
- { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
- { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
- { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
- { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
- { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
- { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
- { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
- { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
- { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
- { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
- { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
- { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
- { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
- { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
- { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
- { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
- { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
- { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
- { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
- { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
- { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
- { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
- { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
-]
-
[[package]]
name = "cfgv"
version = "3.5.0"
@@ -543,59 +486,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" },
]
-[[package]]
-name = "cryptography"
-version = "48.0.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" },
- { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" },
- { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" },
- { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" },
- { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" },
- { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" },
- { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" },
- { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" },
- { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" },
- { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" },
- { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" },
- { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" },
- { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" },
- { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" },
- { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" },
- { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" },
- { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" },
- { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" },
- { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" },
- { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" },
- { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" },
- { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" },
- { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" },
- { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" },
- { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" },
- { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" },
- { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" },
- { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" },
- { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" },
- { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" },
- { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" },
- { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" },
- { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" },
- { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" },
- { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" },
- { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" },
- { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" },
- { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" },
- { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" },
- { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" },
- { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" },
- { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" },
-]
-
[[package]]
name = "dataclasses-json"
version = "0.6.7"
@@ -1621,33 +1511,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" },
]
-[[package]]
-name = "pdfminer-six"
-version = "20251230"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "charset-normalizer" },
- { name = "cryptography" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/46/9a/d79d8fa6d47a0338846bb558b39b9963b8eb2dfedec61867c138c1b17eeb/pdfminer_six-20251230.tar.gz", hash = "sha256:e8f68a14c57e00c2d7276d26519ea64be1b48f91db1cdc776faa80528ca06c1e", size = 8511285, upload-time = "2025-12-30T15:49:13.104Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/65/d7/b288ea32deb752a09aab73c75e1e7572ab2a2b56c3124a5d1eb24c62ceb3/pdfminer_six-20251230-py3-none-any.whl", hash = "sha256:9ff2e3466a7dfc6de6fd779478850b6b7c2d9e9405aa2a5869376a822771f485", size = 6591909, upload-time = "2025-12-30T15:49:10.76Z" },
-]
-
-[[package]]
-name = "pdfplumber"
-version = "0.11.9"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pdfminer-six" },
- { name = "pillow" },
- { name = "pypdfium2" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/38/37/9ca3519e92a8434eb93be570b131476cc0a4e840bb39c62ddb7813a39d53/pdfplumber-0.11.9.tar.gz", hash = "sha256:481224b678b2bbdbf376e2c39bf914144eef7c3d301b4a28eebf0f7f6109d6dc", size = 102768, upload-time = "2026-01-05T08:10:29.072Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/8b/c8/cdbc975f5b634e249cfa6597e37c50f3078412474f21c015e508bfbfe3c3/pdfplumber-0.11.9-py3-none-any.whl", hash = "sha256:33ec5580959ba524e9100138746e090879504c42955df1b8a997604dd326c443", size = 60045, upload-time = "2026-01-05T08:10:27.512Z" },
-]
-
[[package]]
name = "pillow"
version = "12.2.0"
@@ -1864,15 +1727,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" },
]
-[[package]]
-name = "pycparser"
-version = "3.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
-]
-
[[package]]
name = "pydantic"
version = "2.13.4"
@@ -1985,32 +1839,19 @@ wheels = [
]
[[package]]
-name = "pypdfium2"
-version = "5.9.0"
+name = "pymupdf"
+version = "1.27.2.3"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b0/98/6b44bf82ddb3c7a3e0249203772aad8981b4491d6227f182685f310faeff/pypdfium2-5.9.0.tar.gz", hash = "sha256:db1274bd27844db6fda17ef1dbcd0026c47d357437058d838e98060c0da9e92e", size = 272455, upload-time = "2026-06-01T15:43:38.08Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/22/32/708bedc9dde7b328d45abbc076091769d44f2f24ad151ad92d56a6ec142b/pymupdf-1.27.2.3.tar.gz", hash = "sha256:7a92faa25129e8bbec5e50eeb9214f187665428c31b05c4ef6e36c58c0b1c6d2", size = 85759618, upload-time = "2026-04-24T14:13:14.42Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/8b/d9/59630cb40e5f37e7712e6ea65e9cac633f4195e8b737bb3a46054aa63340/pypdfium2-5.9.0-py3-none-android_23_arm64_v8a.whl", hash = "sha256:91914837c4a4285b3e0724a84eca8079363db7475acbcab405933d1807785664", size = 3407817, upload-time = "2026-06-01T15:42:58.426Z" },
- { url = "https://files.pythonhosted.org/packages/0f/3d/e205708835a3730d5242652b6577ac06ad4721e6fcef77cc7c9d3541c686/pypdfium2-5.9.0-py3-none-android_23_armeabi_v7a.whl", hash = "sha256:90610d352f050b065b703f3a46602a852fce7dd8787300c8c7a472485b644d8f", size = 2862706, upload-time = "2026-06-01T15:43:00.581Z" },
- { url = "https://files.pythonhosted.org/packages/01/47/e843fb895a891438b3f8c6d834fdc9c19183cd60980fc9325429d5c01505/pypdfium2-5.9.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6c4fbe3a7190b329c526358fb2855d797f7b74b5ecfc61d19657ef20bcebc108", size = 3489945, upload-time = "2026-06-01T15:43:02.542Z" },
- { url = "https://files.pythonhosted.org/packages/35/bd/f5e6afd556f97fcaa2bec4cb04669664c166028fc2a059bd65447c852b43/pypdfium2-5.9.0-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:e93f0cf440169a3e445e6fbd06c803877e7418f3e13254287875cb67f208bb5a", size = 3674186, upload-time = "2026-06-01T15:43:04.496Z" },
- { url = "https://files.pythonhosted.org/packages/6d/4d/5286812216a292d51dfba8e7bff276da198f126508f8c2afa3630bf701dc/pypdfium2-5.9.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d902e03dff5efd51d93cd23d3e55bde53802fa6207bcd0e455239518859a069", size = 3669571, upload-time = "2026-06-01T15:43:06.571Z" },
- { url = "https://files.pythonhosted.org/packages/ac/c8/822db2c89baa13e6cee321d587fcd42df463a1fc2f7520b3f6814768bc71/pypdfium2-5.9.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cf38d7ad3575947b82384869f2ab69ba345eb21d83118d25db3e83f967b0421", size = 3400412, upload-time = "2026-06-01T15:43:08.35Z" },
- { url = "https://files.pythonhosted.org/packages/1a/dd/7d09d8cdc28383df13f739a97ac4f1215a704a97a29506dee2bf89d8a350/pypdfium2-5.9.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:77f7479a28b43aa658735e3ce79cfd1fccd5d42db035c21bb4c26e8bd7e280e5", size = 3803326, upload-time = "2026-06-01T15:43:10.054Z" },
- { url = "https://files.pythonhosted.org/packages/99/58/3f4e04ffe1ae62b437de07a96da672091cef62b619d0dc78207c1af442e6/pypdfium2-5.9.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:07e6ba170d577eabf60dbba701d051c64318dd029d38ca5907d83ae1a66fe779", size = 4216890, upload-time = "2026-06-01T15:43:11.701Z" },
- { url = "https://files.pythonhosted.org/packages/1d/f6/2dde4656750c4a6da99e1f070ca09d2b5a9d68186b42e711a1a3e5b1cb32/pypdfium2-5.9.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ce3a3dd23ec0adaa079d8be54565ba2aa2f6060e76a4989cd42dabc163d74ee", size = 3728830, upload-time = "2026-06-01T15:43:13.329Z" },
- { url = "https://files.pythonhosted.org/packages/d0/ca/f2ff8b9200c7dfc5aee85126edc856eb93c7056085da2454a75ef1e4dbc4/pypdfium2-5.9.0-py3-none-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae177938f5cf95a275db25a4f8553e2ebd954ecda2f9bc84848ba4b027ce438f", size = 4063322, upload-time = "2026-06-01T15:43:15.158Z" },
- { url = "https://files.pythonhosted.org/packages/64/88/0b587de03c873c28adc59f6ac959de4032d3f3bc946094523b14a192d9c3/pypdfium2-5.9.0-py3-none-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffe49edde2ac86f28ca7e58f565255a442f38a7508fff31b79a55f508f25a31e", size = 4039738, upload-time = "2026-06-01T15:43:16.975Z" },
- { url = "https://files.pythonhosted.org/packages/83/4c/fa627f00a954e66465e929077cf43bd012595091fff82758d989486e7bdc/pypdfium2-5.9.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b7b760bc2957ecf73c274af6ed8b168a2dcb328ac0a0f7ed6123cd92f6e7c9c9", size = 4997259, upload-time = "2026-06-01T15:43:18.915Z" },
- { url = "https://files.pythonhosted.org/packages/32/f0/1736d80c5d12d931f74ca6b4213b006ee016ec33c6325fad870234cc240c/pypdfium2-5.9.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7cdc8e5d2f8d82add1e4f70a4fbe5f3b33c17f301ebde38c669fd7f78a7d032c", size = 4537061, upload-time = "2026-06-01T15:43:20.879Z" },
- { url = "https://files.pythonhosted.org/packages/01/00/aa8890dfd385b2e7365034231987029cff15cc7eb4f06e8380da5608738a/pypdfium2-5.9.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:38a058dbd4929acaf0ab9171179eb86c24d8c6655a6836006796105a9f200890", size = 5232786, upload-time = "2026-06-01T15:43:23.73Z" },
- { url = "https://files.pythonhosted.org/packages/65/12/8f45ea698781a0bed96ac4fbde440060790863273943461f0f160a993d52/pypdfium2-5.9.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:1894511a0e862e7ec5679f3a6dc43ac72c4ef92c7ca438357203913e8634a643", size = 5170121, upload-time = "2026-06-01T15:43:25.858Z" },
- { url = "https://files.pythonhosted.org/packages/25/bd/9bb6ba375796e1de1d6c1af8d8303dd1781190346871c81a94d4e09eddfd/pypdfium2-5.9.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:040f5513b808db705d4878f57e2bf0b9dc6e6a0ad8d765c36cf62febf3933b28", size = 4663540, upload-time = "2026-06-01T15:43:27.677Z" },
- { url = "https://files.pythonhosted.org/packages/d2/4a/fd103bac197f22038bf70be1f7507ced7519f1214ea0dae137f37803ab8a/pypdfium2-5.9.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:f4991ae39bcea757552579bba4aebfaedb71c96dd35c2292f957b8ac9132f1ff", size = 5090619, upload-time = "2026-06-01T15:43:29.522Z" },
- { url = "https://files.pythonhosted.org/packages/22/89/9531fa1e6e004fe522cdca0cd945cd6a9d7338e7125e6b0734d632d31fa6/pypdfium2-5.9.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:25ff1a5abd08ff9e87f62e5dac114ea95647c257fbbdbe029be8db71a6d7650b", size = 5050806, upload-time = "2026-06-01T15:43:31.322Z" },
- { url = "https://files.pythonhosted.org/packages/fc/d0/e53c68555ff128b2470e4a468762b320d9c6ae2c914decea3487d923982f/pypdfium2-5.9.0-py3-none-win32.whl", hash = "sha256:b0057dc8c2033584dc3e61afb5f23a135dab52b081695b435e27f9b7b074c605", size = 3670966, upload-time = "2026-06-01T15:43:32.991Z" },
- { url = "https://files.pythonhosted.org/packages/da/0c/22e5fc035ad1594b44f265bc0a59ae34d377bc2ea74a92793e7a674bf96d/pypdfium2-5.9.0-py3-none-win_amd64.whl", hash = "sha256:06508c33b9772cf3878e48364c6e14c70cefc18a3abd6983ac9f338da9305275", size = 3800959, upload-time = "2026-06-01T15:43:34.536Z" },
- { url = "https://files.pythonhosted.org/packages/11/e3/cf1711add7add22a17f7c7633cd795edc92f17ab7bdf1930493ae0f56680/pypdfium2-5.9.0-py3-none-win_arm64.whl", hash = "sha256:565ddfc98795fd2f6054b544ee9791d7b9032f9cf77a57891b6e501fafd0ef3f", size = 3585718, upload-time = "2026-06-01T15:43:36.521Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/09/ddbdfa7ee91fbabd6f63d7d744884cbdfe3e7ff9b8604749fb38bddf5c5d/pymupdf-1.27.2.3-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc1bc3cae6e9e150b0dbb0a9221bdfd411d65f0db2fe359eaa22467d7cc2a05f", size = 24002636, upload-time = "2026-04-24T14:09:17.459Z" },
+ { url = "https://files.pythonhosted.org/packages/01/89/3f8edd6c4f50ca370e2a2f2a3011face36f3760728ffe76dffec91c0fca0/pymupdf-1.27.2.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:660d93cb6da5bbddf11d3982ae27745dd3a9902d9f24cdb69adab83962294b5a", size = 23278238, upload-time = "2026-04-24T14:09:32.882Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/26/b7e5a70eb83bd189f8b5df87ec442746b992f2f632662839b288170d357d/pymupdf-1.27.2.3-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1dd460a3ae4597a755f00a3bd9771f5ebf1531dc111f6a36bf05dd00a6b84425", size = 24333923, upload-time = "2026-04-24T14:09:47.341Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/a0/aa1ee2240f29481a04a827c313333b4ecd8a14d6ac3e15d3f41a30574781/pymupdf-1.27.2.3-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:857842b4888827bd6155a1131341b2822a7ebe9a8c15a975fd7d490d7a64a30c", size = 24963198, upload-time = "2026-04-24T14:10:07.408Z" },
+ { url = "https://files.pythonhosted.org/packages/69/49/4f742451f980840829fc00ba158bebb25d389c846d8f4f8c65936ee55de8/pymupdf-1.27.2.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:580983849c64a08d08344ca3d1580e87c01f046a8392421797bc850efd72a5b6", size = 25184609, upload-time = "2026-04-24T14:10:22.911Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/3f/3853d6608f394faf6eec2bd4e8ea9f6a00beea329b071abdb29f4164cc3d/pymupdf-1.27.2.3-cp310-abi3-win32.whl", hash = "sha256:a5c1088a87189891a4946ab314a14b7934ac4c5b6077f7e74ebee956f8906d0e", size = 18019286, upload-time = "2026-04-24T14:10:34.239Z" },
+ { url = "https://files.pythonhosted.org/packages/44/47/5fb10fe73f96b31253a41647c362ea9e0380920bddf16028414a051247fc/pymupdf-1.27.2.3-cp310-abi3-win_amd64.whl", hash = "sha256:d20f68ef15195e073071dbc4ae7455257c7889af7584e39df490c0a92728526e", size = 19249102, upload-time = "2026-04-24T14:10:46.72Z" },
+ { url = "https://files.pythonhosted.org/packages/53/a4/b9e91aac82293f9c954654c85581ee8212b5b05efadc534b581141241e6f/pymupdf-1.27.2.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:77691604c5d1d0233827139bbcdea61fd57879c84712b8e49b1f45520f7ab9c2", size = 25000393, upload-time = "2026-04-24T14:11:01.669Z" },
]
[[package]]