Some checks failed
CI / lint (push) Successful in 39s
CI / notebooks-smoke (push) Successful in 1m39s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
CI / test (push) Failing after 2m29s
Deploy / api (push) Has been skipped
Deploy / llm (push) Has been skipped
Deploy / mc (push) Has been skipped
Infra CI / zotero (push) Successful in 15s
Infra CI / docs (push) Successful in 20s
Infra CI / notebooks (push) Successful in 52s
Infra CI / api (push) Successful in 1m2s
Infra CI / llm (push) Successful in 47s
Deploy / report (push) Successful in 14s
Infra CI / mc (push) Failing after 37s
Rules keep their federalregister.gov links; this is for the files the library holds itself: a regulations.gov comment's downloaded attachments (.state/comments/<docket>/<id>/attachment_N.*), an item's bib attachments (data/storage/<att>/<name>) and Zotero-only storage PDFs (zotero.sqlite read immutable, one indexed lookup per request). llm.pdfs lists an item's attachments from those three places (comment first, de-duplicated by name) and resolves a file by *name* only, refusing anything outside the storage roots. GET /pdf/<key> lists them (name, size, source, renderable, media type); GET /pdf/<key>/file?name= serves a PDF inline with range requests and other formats as downloads. /ui/vendor/<name> serves the vendored pdf.js 4.10.38 (same-origin worker; no CDN dependency). /ui/pdf/<key>?file=&page=&q=: continuous scroll, pages rendered lazily (IntersectionObserver, ~1.5 screens ahead) at device pixel ratio, re-fit on resize/orientation (ResizeObserver, debounced), fit-to-width and zoom, page input with hash tracking, a text layer for selection, whole-document find with highlights (seeded from the cited snippet), keyboard shortcuts, a file selector when an item has several, and a download fallback when a file is not a PDF or cannot be rendered. as_source carries attachment + page from the chunk metadata; the chat sources and search hits show a 'PDF p.N' link into the viewer for comment/corpus sources. compose: the llm service mounts ./.state/comments read-only.
129 lines
5.1 KiB
Python
129 lines
5.1 KiB
Python
"""llm.pdfs — attachments on file for the viewer: comment downloads, bib
|
|
attachments, Zotero storage PDFs; name-only resolution inside the roots."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sqlite3
|
|
from pathlib import Path
|
|
|
|
from bib.item import Source
|
|
from bib.store import Store
|
|
from llm.pdfs import PdfFile, pdf_files, resolve
|
|
|
|
|
|
def _zotero(
|
|
tmp_path: Path, parent_key: str, att_key: str, name: str
|
|
) -> tuple[Path, Path]:
|
|
db = tmp_path / "zotero.sqlite"
|
|
con = sqlite3.connect(db)
|
|
con.executescript(
|
|
"CREATE TABLE items (itemID INTEGER PRIMARY KEY, key TEXT);"
|
|
"CREATE TABLE itemAttachments (itemID INTEGER, parentItemID INTEGER, path TEXT);"
|
|
"CREATE TABLE deletedItems (itemID INTEGER);"
|
|
)
|
|
con.execute("INSERT INTO items VALUES (1, ?)", (parent_key,))
|
|
con.execute("INSERT INTO items VALUES (2, ?)", (att_key,))
|
|
con.execute("INSERT INTO items VALUES (3, 'GONE1234')")
|
|
con.execute("INSERT INTO itemAttachments VALUES (2, 1, ?)", (f"storage:{name}",))
|
|
con.execute("INSERT INTO itemAttachments VALUES (3, 1, 'storage:deleted.pdf')")
|
|
con.execute("INSERT INTO deletedItems VALUES (3)")
|
|
con.commit()
|
|
con.close()
|
|
storage = tmp_path / "zstorage"
|
|
(storage / att_key).mkdir(parents=True)
|
|
(storage / att_key / name).write_bytes(b"%PDF-1.4 z")
|
|
(storage / "GONE1234").mkdir()
|
|
(storage / "GONE1234" / "deleted.pdf").write_bytes(b"%PDF-1.4 d")
|
|
return db, storage
|
|
|
|
|
|
def test_all_three_sources_in_order_and_deduped(tmp_path):
|
|
store = Store(":memory:", storage_dir=tmp_path / "bibstorage")
|
|
key = store.upsert(Source(title="Paper", url="https://doi.org/10.1/x"))
|
|
pdf = tmp_path / "paper.pdf"
|
|
pdf.write_bytes(b"%PDF-1.4 b")
|
|
store.attach_file(key, pdf)
|
|
(tmp_path / "notes.docx").write_bytes(b"PK docx")
|
|
store.attach_file(key, tmp_path / "notes.docx")
|
|
zdb, zstore = _zotero(
|
|
tmp_path, key, "ATT00001", "paper.pdf"
|
|
) # same name as the bib copy → deduped
|
|
files = pdf_files(
|
|
store,
|
|
key,
|
|
zotero_sqlite=zdb,
|
|
zotero_storage=zstore,
|
|
comments_root=tmp_path / "comments",
|
|
)
|
|
assert [(f.name, f.source, f.renderable) for f in files] == [
|
|
("paper.pdf", "bib", True),
|
|
("notes.docx", "bib", False),
|
|
]
|
|
assert files[1].media_type.startswith("application/vnd.openxmlformats")
|
|
assert all(isinstance(f, PdfFile) and f.size > 0 for f in files)
|
|
store.close()
|
|
|
|
|
|
def test_zotero_only_item_and_deleted_attachments_skipped(tmp_path):
|
|
store = Store(":memory:", storage_dir=tmp_path / "bibstorage")
|
|
key = store.upsert(Source(title="Zot only", url="https://example.org/z"))
|
|
zdb, zstore = _zotero(tmp_path, key, "ATT00002", "Cassidy - 2025 - RFI.pdf")
|
|
files = pdf_files(store, key, zotero_sqlite=zdb, zotero_storage=zstore)
|
|
assert [(f.name, f.source) for f in files] == [
|
|
("Cassidy - 2025 - RFI.pdf", "zotero")
|
|
]
|
|
assert pdf_files(store, "NOPE1234", zotero_sqlite=zdb, zotero_storage=zstore) == []
|
|
assert (
|
|
pdf_files(
|
|
store, key, zotero_sqlite=tmp_path / "missing.sqlite", zotero_storage=zstore
|
|
)
|
|
== []
|
|
)
|
|
store.close()
|
|
|
|
|
|
def test_comment_attachments_from_the_state_tree(tmp_path):
|
|
store = Store(":memory:", storage_dir=tmp_path / "bibstorage")
|
|
key = store.upsert(
|
|
Source(title="c", url="https://www.regulations.gov/comment/CMS-2026-2377-40314")
|
|
)
|
|
d = tmp_path / "comments" / "CMS-2026-2377" / "CMS-2026-2377-40314"
|
|
d.mkdir(parents=True)
|
|
(d / "attachment_1.pdf").write_bytes(b"%PDF-1.4 a1")
|
|
(d / "attachment_2.docx").write_bytes(b"PK")
|
|
(d / "combined.md").write_text("# not an attachment")
|
|
(d / "attachment_1.png").write_bytes(b"png")
|
|
files = pdf_files(store, key, comments_root=tmp_path / "comments")
|
|
assert [(f.name, f.source, f.renderable) for f in files] == [
|
|
("attachment_1.pdf", "comment", True),
|
|
("attachment_2.docx", "comment", False),
|
|
]
|
|
# a non-comment url, or a comment with no downloaded dir, yields nothing
|
|
other = store.upsert(Source(title="p", url="https://doi.org/10.1/y"))
|
|
assert pdf_files(store, other, comments_root=tmp_path / "comments") == []
|
|
gone = store.upsert(
|
|
Source(
|
|
title="c2", url="https://www.regulations.gov/comment/CMS-2026-2377-99999"
|
|
)
|
|
)
|
|
assert pdf_files(store, gone, comments_root=tmp_path / "comments") == []
|
|
store.close()
|
|
|
|
|
|
def test_resolve_by_name_inside_roots_only(tmp_path):
|
|
inside = tmp_path / "root" / "K" / "a.pdf"
|
|
inside.parent.mkdir(parents=True)
|
|
inside.write_bytes(b"%PDF")
|
|
outside = tmp_path / "elsewhere" / "b.pdf"
|
|
outside.parent.mkdir()
|
|
outside.write_bytes(b"%PDF")
|
|
files = [PdfFile("a.pdf", inside, 4, "bib"), PdfFile("b.pdf", outside, 4, "bib")]
|
|
roots = (tmp_path / "root",)
|
|
assert resolve(files, "", roots=roots).name == "a.pdf"
|
|
assert resolve(files, "a.pdf", roots=roots).name == "a.pdf"
|
|
assert (
|
|
resolve(files, "b.pdf", roots=roots) is None
|
|
) # listed, but outside every root
|
|
assert resolve(files, "../a.pdf", roots=roots) is None
|
|
assert resolve([], "", roots=roots) is None
|