- New deps: pymupdf>=1.24 (AGPL-3.0), python-docx>=1.1
- src/rex/comments/{__init__.py,extract.py} with ExtractResult dataclass
- PDF extraction via PyMuPDF with status taxonomy:
ok | ocr_needed | failed | unsupported
- Tests cover happy path, image-only (ocr_needed), and corrupted PDF
Also fixes 19 pre-existing test failures in tests/zot/test_{duck,extract,
table}.py — all were opening data/zotero/data/zotero.sqlite directly,
which fails with "database is locked" while the Zotero container holds
the WAL lock. New tests/zot/conftest.py provides a session-scoped
host_db fixture that snapshots the live DB once via shutil.copy2;
schema rows (itemTypes/fields/creatorTypes) are stable so a hot copy
is fine for these read-only schema-parity checks.
DOCX/text handlers and combine.py land in the next batch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
54 lines
1.3 KiB
Python
54 lines
1.3 KiB
Python
"""PDF extraction via PyMuPDF."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import fitz # pymupdf
|
|
|
|
from rex.comments import extract_attachment
|
|
|
|
|
|
def _make_pdf(path: Path, text: str = "Hello world. This is page text.") -> Path:
|
|
doc = fitz.open()
|
|
page = doc.new_page()
|
|
page.insert_text((50, 72), text)
|
|
doc.save(str(path))
|
|
doc.close()
|
|
return path
|
|
|
|
|
|
def test_extract_pdf_with_text(tmp_path: Path):
|
|
pdf = _make_pdf(
|
|
tmp_path / "a.pdf",
|
|
"Position statement: oppose the proposed rule in its entirety.",
|
|
)
|
|
result = extract_attachment(pdf)
|
|
assert result.status == "ok"
|
|
assert "Position statement" in result.text
|
|
assert result.chars == len(result.text)
|
|
assert result.chars > 50
|
|
|
|
|
|
def test_extract_pdf_image_only_marked_ocr_needed(tmp_path: Path):
|
|
# Empty page — no text content, simulates a scanned image.
|
|
doc = fitz.open()
|
|
doc.new_page()
|
|
pdf = tmp_path / "scan.pdf"
|
|
doc.save(str(pdf))
|
|
doc.close()
|
|
|
|
result = extract_attachment(pdf)
|
|
assert result.status == "ocr_needed"
|
|
assert result.chars <= 50
|
|
|
|
|
|
def test_extract_pdf_corrupted_marked_failed(tmp_path: Path):
|
|
bad = tmp_path / "bad.pdf"
|
|
bad.write_bytes(b"not a real pdf")
|
|
|
|
result = extract_attachment(bad)
|
|
assert result.status == "failed"
|
|
assert result.text == ""
|
|
assert result.chars == 0
|