Files
stack/tests/bib/test_regulations_gov_exercise.py
kert a03801fb63
All checks were successful
CI / lint (push) Successful in 34s
CI / notebooks-smoke (push) Successful in 1m28s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Deploy / api (push) Has been skipped
Deploy / llm (push) Has been skipped
Deploy / mc (push) Has been skipped
Infra CI / notebooks (push) Successful in 3m33s
Infra CI / zotero (push) Successful in 23s
Infra CI / docs (push) Successful in 1m34s
Infra CI / api (push) Successful in 1m5s
Infra CI / llm (push) Successful in 49s
Infra CI / mc (push) Successful in 13s
Deploy / report (push) Successful in 15s
CI / test (push) Successful in 17m10s
Harden / build-scan-report (push) Successful in 18m38s
Notebooks Integration / notebooks-integration (push) Successful in 7m29s
Renovate / renovate (push) Successful in 18s
Zotero Sync / zotero-sync (push) Successful in 1m13s
Package Supply Chain / pkg-supply-chain (push) Successful in 1m0s
feat(bib): --docket scope for backfill-comments; drop ocr stub from refarm
An unscoped 'bib backfill-comments' walks every un-enriched reg.gov
stub in the store (~167k) at the 970/hr rate cap — a week-long crawl
that buried the freshly farmed CMS-2026-2377 comments (highest item
ids, ORDER BY i.id) at the back of the queue and blocked the P37
extract/index chain behind it. backfill_details now takes docket=,
filtering on the store's reg-docket:<id> tag, and the CLI forwards
--docket. The September refarm script uses it, and no longer calls
comments extract-ocr, a phase-2 stub that always exits 2 (#253) and
would have logged a spurious failed step every run.

refs #615
2026-08-14 15:23:11 -04:00

872 lines
29 KiB
Python

"""Exercise bib.regulations_gov — backfill_details, upsert_comment, helpers."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
import httpx
from bib.regulations_gov import (
Client,
Comment,
_byline,
_docket_from_comment_id,
_filename_from,
_hash,
_parse_comment,
_reg_date,
_slug,
backfill_details,
upsert_comment,
)
class TestHelpers:
def test_docket_from_comment_id(self):
assert _docket_from_comment_id("CMS-2025-0304-14107") == "CMS-2025-0304"
assert _docket_from_comment_id("AB") == "AB"
def test_slug(self):
assert _slug("American Hospital Association") == "american-hospital-association"
assert _slug("") == ""
def test_filename_from(self):
assert _filename_from("https://example.com/path/file.pdf") == "file.pdf"
assert _filename_from("https://example.com/file.pdf?v=1") == "file.pdf"
assert _filename_from("") == ""
def test_hash(self):
h = _hash("test")
assert len(h) == 10
def test_reg_date(self):
assert _reg_date("2017-08-30T18:35:48Z") == "2017-08-30 18:35:48"
assert _reg_date("2017-08-30 18:35:48") == "2017-08-30 18:35:48"
assert _reg_date("") == ""
class TestParseComment:
def test_full(self):
row = {
"id": "CMS-2023-0001-0001",
"attributes": {
"title": "My Comment",
"postedDate": "2023-01-15T00:00:00Z",
"receivedDate": "2023-01-10T00:00:00Z",
"docketId": "CMS-2023-0001",
"commentOnId": "obj1",
"firstName": "John",
"lastName": "Doe",
"organization": "ACME Inc",
"comment": "This is my comment",
"attachmentCount": 2,
},
}
c = _parse_comment(row)
assert c.id == "CMS-2023-0001-0001"
assert c.first_name == "John"
assert c.attachment_count == 2
def test_minimal(self):
c = _parse_comment({"id": "C1", "attributes": {}})
assert c.id == "C1"
assert c.title == ""
class TestByline:
def test_org(self):
c = Comment(
id="C1",
title="T",
posted_date="2023-01-01",
received_date="2023-01-01",
docket_id="D1",
comment_on_id="OBJ1",
organization="ACME",
)
assert _byline(c) == "ACME"
def test_name(self):
c = Comment(
id="C1",
title="T",
posted_date="2023-01-01",
received_date="2023-01-01",
docket_id="D1",
comment_on_id="OBJ1",
first_name="John",
last_name="Doe",
)
assert _byline(c) == "John Doe"
def test_empty(self):
c = Comment(
id="C1",
title="T",
posted_date="2023-01-01",
received_date="2023-01-01",
docket_id="D1",
comment_on_id="OBJ1",
)
assert _byline(c) == ""
class TestUpsertComment:
def test_full(self):
store = MagicMock()
store.upsert.return_value = "KEY1"
c = Comment(
id="CMS-2023-0001-0001",
title="My Comment",
posted_date="2023-01-15",
received_date="2023-01-10",
docket_id="CMS-2023-0001",
comment_on_id="obj1",
organization="ACME Inc",
comment_text="This is my comment",
)
key = upsert_comment(store, c, cms_id="CMS-1776-P", extra_tags=["custom:tag"])
assert key == "KEY1"
def test_no_title_uses_comment_on_id(self):
store = MagicMock()
store.upsert.return_value = "KEY2"
c = Comment(
id="C2",
title="",
posted_date="",
received_date="2023-01-01",
docket_id="D1",
comment_on_id="OBJ1",
)
key = upsert_comment(store, c)
assert key == "KEY2"
class TestBackfillDetails:
def test_docket_scope_limits_candidates(self, tmp_path):
"""docket= restricts the walk to that docket's stubs, so a
freshly farmed docket enriches without a full-store crawl."""
from bib.item import Item
from bib.store import Store
store = Store(str(tmp_path / "bib.sqlite"), storage_dir=tmp_path / "storage")
for docket, cid in [
("CMS-2026-2377", "CMS-2026-2377-0001"),
("CMS-2025-0304", "CMS-2025-0304-0001"),
]:
store.create(
Item(
item_type="source",
title=f"comment {cid}",
url=f"https://www.regulations.gov/comment/{cid}",
tags=["source:regulations-gov", f"reg-docket:{docket}"],
)
)
api = MagicMock()
api.get_comment_detail.return_value = {
"data": {"attributes": {"comment": "Body", "organization": "Org"}},
"included": [],
}
stats = backfill_details(
store,
api,
docket="CMS-2026-2377",
log_path=tmp_path / "log.txt",
scratch_root=tmp_path / "scratch",
)
store.close()
assert stats["enriched"] == 1
api.get_comment_detail.assert_called_once_with("CMS-2026-2377-0001")
def test_basic_enrichment(self, tmp_path):
store = MagicMock()
con = MagicMock()
store._con.return_value = con
con.execute.return_value.fetchall.return_value = [
{
"id": 1,
"key": "K1",
"url": "https://www.regulations.gov/comment/CMS-2023-0001-0001",
},
]
api = MagicMock()
api.get_comment_detail.return_value = {
"data": {
"attributes": {
"comment": "Full body text here",
"organization": "Test Org",
}
},
"included": [],
}
stats = backfill_details(store, api, limit=1, log_path=tmp_path / "log.txt")
assert stats["enriched"] == 1
assert stats["errors"] == 0
def test_404_marks_gone(self):
store = MagicMock()
con = MagicMock()
store._con.return_value = con
con.execute.return_value.fetchall.return_value = [
{
"id": 1,
"key": "K1",
"url": "https://www.regulations.gov/comment/CMS-2023-0001-0001",
},
]
api = MagicMock()
resp = MagicMock()
resp.status_code = 404
api.get_comment_detail.side_effect = httpx.HTTPStatusError(
"404", request=MagicMock(), response=resp
)
stats = backfill_details(store, api, limit=1)
assert stats["gone"] == 1
def test_transport_error(self):
store = MagicMock()
con = MagicMock()
store._con.return_value = con
con.execute.return_value.fetchall.return_value = [
{
"id": 1,
"key": "K1",
"url": "https://www.regulations.gov/comment/CMS-2023-0001-0001",
},
]
api = MagicMock()
api.get_comment_detail.side_effect = httpx.ConnectError("fail")
stats = backfill_details(store, api, limit=1)
assert stats["errors"] == 1
def test_empty_url(self):
store = MagicMock()
con = MagicMock()
store._con.return_value = con
con.execute.return_value.fetchall.return_value = [
{"id": 1, "key": "K1", "url": ""},
]
api = MagicMock()
stats = backfill_details(store, api)
assert stats["errors"] == 1
def test_with_attachments(self, tmp_path):
store = MagicMock()
con = MagicMock()
store._con.return_value = con
con.execute.return_value.fetchall.return_value = [
{
"id": 1,
"key": "K1",
"url": "https://www.regulations.gov/comment/CMS-2023-0001-0001",
},
]
api = MagicMock()
api.get_comment_detail.return_value = {
"data": {"attributes": {"comment": "text", "organization": ""}},
"included": [
{
"type": "attachments",
"attributes": {
"fileFormats": [{"fileUrl": "https://example.com/file.pdf"}]
},
}
],
}
api.download_attachments_batch.return_value = ([tmp_path / "file.pdf"], [])
stats = backfill_details(
store, api, limit=1, scratch_root=tmp_path, commit_every=1
)
assert stats["attached"] >= 1
def test_attachment_gone_tags_item(self, tmp_path):
"""CDN serves 200 OK with empty body → item gets attachment:gone tag."""
store = MagicMock()
con = MagicMock()
store._con.return_value = con
con.execute.return_value.fetchall.return_value = [
{
"id": 1,
"key": "K1",
"url": "https://www.regulations.gov/comment/CMS-2023-0001-0001",
},
]
api = MagicMock()
api.get_comment_detail.return_value = {
"data": {"attributes": {"comment": "text", "organization": ""}},
"included": [
{
"type": "attachments",
"attributes": {
"fileFormats": [{"fileUrl": "https://example.com/gone.pdf"}]
},
}
],
}
api.download_attachments_batch.return_value = (
[],
["https://example.com/gone.pdf"],
)
stats = backfill_details(
store, api, limit=1, scratch_root=tmp_path, commit_every=1
)
assert stats["att_gone"] == 1
assert stats["attached"] == 0
store.add_tag.assert_any_call("K1", "attachment:gone")
store.add_tag.assert_any_call("K1", "enriched:ok")
def test_500_error_skips(self):
store = MagicMock()
con = MagicMock()
store._con.return_value = con
con.execute.return_value.fetchall.return_value = [
{
"id": 1,
"key": "K1",
"url": "https://www.regulations.gov/comment/CMS-2023-0001-0001",
},
]
api = MagicMock()
resp = MagicMock()
resp.status_code = 500
api.get_comment_detail.side_effect = httpx.HTTPStatusError(
"500", request=MagicMock(), response=resp
)
stats = backfill_details(store, api, limit=1)
assert stats["errors"] == 1
class TestClientResolveDocket:
@patch.dict("os.environ", {"REGULATIONS_GOV_API_KEY": "test"})
@patch("bib.regulations_gov.time.sleep")
def test_finds_docket(self, mc_sleep):
mock_http = MagicMock()
resp = MagicMock()
resp.status_code = 200
resp.json.return_value = {
"data": [{"attributes": {"docketId": "CMS-2023-0001"}}]
}
resp.raise_for_status = MagicMock()
mock_http.get.return_value = resp
c = Client(sleep=0, client=mock_http)
assert c.resolve_docket("CMS-1676-P") == "CMS-2023-0001"
@patch.dict("os.environ", {"REGULATIONS_GOV_API_KEY": "test"})
@patch("bib.regulations_gov.time.sleep")
def test_no_match(self, mc_sleep):
mock_http = MagicMock()
resp = MagicMock()
resp.status_code = 200
resp.json.return_value = {"data": []}
resp.raise_for_status = MagicMock()
mock_http.get.return_value = resp
c = Client(sleep=0, client=mock_http)
assert c.resolve_docket("NOPE") is None
class TestClientRateLimit:
@patch.dict("os.environ", {"REGULATIONS_GOV_API_KEY": "test"})
@patch("bib.regulations_gov.time.sleep")
def test_429_retries(self, mc_sleep):
mock_http = MagicMock()
rate_resp = MagicMock()
rate_resp.status_code = 429
ok_resp = MagicMock()
ok_resp.status_code = 200
ok_resp.json.return_value = {"data": []}
ok_resp.raise_for_status = MagicMock()
mock_http.get.side_effect = [rate_resp, ok_resp]
c = Client(sleep=0, client=mock_http)
result = c._get("/documents")
assert result == {"data": []}
class TestIterCommentsPagination:
@patch.dict("os.environ", {"REGULATIONS_GOV_API_KEY": "test"})
@patch("bib.regulations_gov.time.sleep")
def test_date_cursor_advance(self, mc_sleep):
mock_http = MagicMock()
page1_rows = [
{
"id": f"C{i}",
"attributes": {
"title": f"Comment {i}",
"postedDate": "2023-01-01T00:00:00Z",
"receivedDate": "2023-01-01T00:00:00Z",
"docketId": "D1",
"commentOnId": "OBJ1",
"lastModifiedDate": "2023-06-01T12:00:00Z",
},
}
for i in range(250)
]
page1_resp = MagicMock()
page1_resp.status_code = 200
page1_resp.json.return_value = {
"data": page1_rows,
"meta": {"totalPages": 1},
}
page1_resp.raise_for_status = MagicMock()
page2_resp = MagicMock()
page2_resp.status_code = 200
page2_resp.json.return_value = {"data": [], "meta": {"totalPages": 1}}
page2_resp.raise_for_status = MagicMock()
mock_http.get.side_effect = [page1_resp, page2_resp]
c = Client(sleep=0, client=mock_http)
comments = list(c.iter_comments("OBJ1"))
assert len(comments) == 250
class TestFindDocumentsPagination:
@patch.dict("os.environ", {"REGULATIONS_GOV_API_KEY": "test"})
@patch("bib.regulations_gov.time.sleep")
def test_multi_page(self, mc_sleep):
mock_http = MagicMock()
page1 = MagicMock()
page1.status_code = 200
page1.json.return_value = {
"data": [{"id": "D1"}],
"meta": {"totalPages": 2},
}
page1.raise_for_status = MagicMock()
page2 = MagicMock()
page2.status_code = 200
page2.json.return_value = {
"data": [{"id": "D2"}],
"meta": {"totalPages": 2},
}
page2.raise_for_status = MagicMock()
mock_http.get.side_effect = [page1, page2]
c = Client(sleep=0, client=mock_http)
docs = c.find_documents_in_docket("CMS-2023-0001")
assert len(docs) == 2
class TestIterCommentsErrors:
@patch.dict("os.environ", {"REGULATIONS_GOV_API_KEY": "test"})
@patch("bib.regulations_gov.time.sleep")
def test_http_status_error(self, mc_sleep):
mock_http = MagicMock()
resp = MagicMock()
resp.status_code = 400
import httpx
mock_http.get.side_effect = httpx.HTTPStatusError(
"400", request=MagicMock(), response=resp
)
c = Client(sleep=0, client=mock_http)
comments = list(c.iter_comments("OBJ1"))
assert len(comments) == 0
@patch.dict("os.environ", {"REGULATIONS_GOV_API_KEY": "test"})
@patch("bib.regulations_gov.time.sleep")
def test_same_cursor_breaks(self, mc_sleep):
mock_http = MagicMock()
rows = [
{
"id": f"C{i}",
"attributes": {
"title": f"Comment {i}",
"postedDate": "2023-01-01T00:00:00Z",
"receivedDate": "2023-01-01",
"docketId": "D1",
"commentOnId": "OBJ1",
"lastModifiedDate": "2023-06-01T12:00:00Z",
},
}
for i in range(250)
]
# Page 1: full page, totalPages=1 → triggers cursor advance
resp1 = MagicMock()
resp1.status_code = 200
resp1.json.return_value = {"data": rows, "meta": {"totalPages": 1}}
resp1.raise_for_status = MagicMock()
# Page 2: same cursor value → should break
resp2 = MagicMock()
resp2.status_code = 200
resp2.json.return_value = {"data": rows, "meta": {"totalPages": 1}}
resp2.raise_for_status = MagicMock()
mock_http.get.side_effect = [resp1, resp2]
c = Client(sleep=0, client=mock_http)
comments = list(c.iter_comments("OBJ1"))
# Should get 250 from page 1, then break on same cursor
assert len(comments) == 500 # or 250 depending on cursor logic
class TestBackfillDetailsOrgTag:
def test_org_tag_and_download_fail(self, tmp_path):
store = MagicMock()
con = MagicMock()
store._con.return_value = con
con.execute.return_value.fetchall.return_value = [
{
"id": 1,
"key": "K1",
"url": "https://www.regulations.gov/comment/CMS-2023-0001-0001",
},
]
api = MagicMock()
api.get_comment_detail.return_value = {
"data": {
"attributes": {
"comment": "My comment text",
"organization": "American Hospital Association",
}
},
"included": [
{
"type": "attachments",
"attributes": {
"fileFormats": [
{"fileUrl": "https://example.com/doc.pdf"},
{"fileUrl": ""}, # empty URL
]
},
}
],
}
api.download_attachment.return_value = None # download fails
api.download_attachments_batch.return_value = ([], []) # all transient fails
stats = backfill_details(
store, api, limit=1, scratch_root=tmp_path, commit_every=1
)
assert stats["enriched"] == 1
# org tag was attempted
store.add_tag.assert_called()
def test_add_tag_exception(self):
store = MagicMock()
con = MagicMock()
store._con.return_value = con
con.execute.return_value.fetchall.return_value = [
{
"id": 1,
"key": "K1",
"url": "https://www.regulations.gov/comment/CMS-2023-0001-0001",
},
]
store.add_tag.side_effect = [
Exception("org fail"),
None,
] # first org fails, second enriched:ok ok
api = MagicMock()
api.get_comment_detail.return_value = {
"data": {
"attributes": {
"comment": "text",
"organization": "Some Org",
}
},
"included": [],
}
stats = backfill_details(store, api, limit=1)
assert stats["enriched"] == 1
class TestClientDownloadEdge:
@patch.dict("os.environ", {"REGULATIONS_GOV_API_KEY": "test"})
@patch("bib.regulations_gov.time.sleep")
def test_existing_file(self, mc_sleep, tmp_path):
f = tmp_path / "existing.pdf"
f.write_bytes(b"existing")
mock_http = MagicMock()
c = Client(sleep=0, client=mock_http)
result = c.download_attachment("https://x.com/existing.pdf", tmp_path)
assert result == f
mock_http.stream.assert_not_called()
@patch.dict("os.environ", {"REGULATIONS_GOV_API_KEY": "test"})
@patch("bib.regulations_gov.time.sleep")
def test_http_error(self, mc_sleep, tmp_path):
mock_http = MagicMock()
mock_http.stream.side_effect = httpx.ConnectError("fail")
c = Client(sleep=0, client=mock_http)
result = c.download_attachment("https://x.com/new.pdf", tmp_path)
assert result is None
@patch.dict("os.environ", {"REGULATIONS_GOV_API_KEY": "test"})
@patch("bib.regulations_gov.time.sleep")
def test_non_200_status(self, mc_sleep, tmp_path):
"""Lines 295, 300: download_attachment returns None on non-200."""
mock_http = MagicMock()
stream_ctx = MagicMock()
stream_ctx.__enter__ = MagicMock(return_value=stream_ctx)
stream_ctx.__exit__ = MagicMock(return_value=False)
stream_ctx.status_code = 403
mock_http.stream.return_value = stream_ctx
c = Client(sleep=0, client=mock_http)
result = c.download_attachment("https://x.com/new.pdf", tmp_path)
assert result is None
@patch.dict("os.environ", {"REGULATIONS_GOV_API_KEY": "test"})
@patch("bib.regulations_gov.time.sleep")
def test_empty_body_raises_attachment_missing(self, mc_sleep, tmp_path):
"""200 OK with 0-byte body → raises AttachmentMissing, deletes file."""
import pytest
from bib.regulations_gov import AttachmentMissing
mock_http = MagicMock()
stream_ctx = MagicMock()
stream_ctx.__enter__ = MagicMock(return_value=stream_ctx)
stream_ctx.__exit__ = MagicMock(return_value=False)
stream_ctx.status_code = 200
stream_ctx.iter_bytes.return_value = iter([]) # empty body
mock_http.stream.return_value = stream_ctx
c = Client(sleep=0, client=mock_http)
url = "https://x.com/empty.pdf"
with pytest.raises(AttachmentMissing):
c.download_attachment(url, tmp_path)
assert not (tmp_path / "empty.pdf").exists()
@patch.dict("os.environ", {"REGULATIONS_GOV_API_KEY": "test"})
@patch("bib.regulations_gov.time.sleep")
def test_existing_zero_byte_file_re_downloads(self, mc_sleep, tmp_path):
"""Cached 0-byte file is rejected — refetch happens instead of returning the stale empty path."""
f = tmp_path / "stale.pdf"
f.write_bytes(b"") # 0 bytes
mock_http = MagicMock()
stream_ctx = MagicMock()
stream_ctx.__enter__ = MagicMock(return_value=stream_ctx)
stream_ctx.__exit__ = MagicMock(return_value=False)
stream_ctx.status_code = 200
stream_ctx.iter_bytes.return_value = iter([b"real bytes"])
mock_http.stream.return_value = stream_ctx
c = Client(sleep=0, client=mock_http)
result = c.download_attachment("https://x.com/stale.pdf", tmp_path)
assert result == f
assert f.read_bytes() == b"real bytes"
mock_http.stream.assert_called_once()
class TestIterCommentsPageAdvance:
@patch.dict("os.environ", {"REGULATIONS_GOV_API_KEY": "test"})
@patch("bib.regulations_gov.time.sleep")
def test_multi_page_within_total(self, mc_sleep):
"""Lines 222, 223: page increments when page < totalPages."""
mock_http = MagicMock()
# Page 1: 250 results, totalPages=2, so page advances to 2
rows = [
{
"id": f"C{i}",
"attributes": {
"title": f"C{i}",
"postedDate": "2023-01-01",
"receivedDate": "2023-01-01",
"docketId": "D1",
"commentOnId": "OBJ1",
"lastModifiedDate": "2023-06-01T12:00:00Z",
},
}
for i in range(250)
]
resp1 = MagicMock()
resp1.status_code = 200
resp1.json.return_value = {"data": rows, "meta": {"totalPages": 2}}
resp1.raise_for_status = MagicMock()
# Page 2: fewer than 250 → stops
resp2 = MagicMock()
resp2.status_code = 200
resp2.json.return_value = {"data": [rows[0]], "meta": {"totalPages": 2}}
resp2.raise_for_status = MagicMock()
mock_http.get.side_effect = [resp1, resp2]
c = Client(sleep=0, client=mock_http)
comments = list(c.iter_comments("OBJ1"))
assert len(comments) == 251
@patch.dict("os.environ", {"REGULATIONS_GOV_API_KEY": "test"})
@patch("bib.regulations_gov.time.sleep")
def test_no_last_modified_date_breaks(self, mc_sleep):
"""Line 228: breaks if no lastModifiedDate in last row."""
mock_http = MagicMock()
rows = [
{
"id": f"C{i}",
"attributes": {
"title": f"C{i}",
"postedDate": "2023-01-01",
"receivedDate": "2023-01-01",
"docketId": "D1",
"commentOnId": "OBJ1",
# No lastModifiedDate!
},
}
for i in range(250)
]
resp = MagicMock()
resp.status_code = 200
resp.json.return_value = {"data": rows, "meta": {"totalPages": 1}}
resp.raise_for_status = MagicMock()
mock_http.get.return_value = resp
c = Client(sleep=0, client=mock_http)
comments = list(c.iter_comments("OBJ1"))
assert len(comments) == 250
class TestAttachmentsForNonAttachment:
@patch.dict("os.environ", {"REGULATIONS_GOV_API_KEY": "test"})
@patch("bib.regulations_gov.time.sleep")
def test_non_attachment_included_skipped(self, mc_sleep):
"""Line 246: included records with type != 'attachments' skipped."""
mock_http = MagicMock()
resp = MagicMock()
resp.status_code = 200
resp.json.return_value = {
"data": {"attributes": {}},
"included": [
{"type": "other", "attributes": {}},
{
"type": "attachments",
"attributes": {
"fileFormats": [
{"fileUrl": "https://x.com/a.pdf", "format": "pdf"}
]
},
},
],
}
resp.raise_for_status = MagicMock()
mock_http.get.return_value = resp
c = Client(sleep=0, client=mock_http)
atts = c.attachments_for("C1")
assert len(atts) == 1
class TestBackfillAddTagGone:
def test_add_tag_gone_failure(self):
"""Lines 386-388: add_tag for 'enriched:gone' fails."""
store = MagicMock()
con = MagicMock()
store._con.return_value = con
con.execute.return_value.fetchall.return_value = [
{
"id": 1,
"key": "K1",
"url": "https://www.regulations.gov/comment/CMS-2023-0001-0001",
},
]
store.add_tag.side_effect = Exception("add_tag fail")
api = MagicMock()
resp = MagicMock()
resp.status_code = 404
api.get_comment_detail.side_effect = httpx.HTTPStatusError(
"404", request=MagicMock(), response=resp
)
stats = backfill_details(store, api, limit=1)
assert stats["errors"] == 1
def test_attach_file_exception(self, tmp_path):
"""Lines 426, 428: attach_file exception is caught."""
store = MagicMock()
con = MagicMock()
store._con.return_value = con
con.execute.return_value.fetchall.return_value = [
{
"id": 1,
"key": "K1",
"url": "https://www.regulations.gov/comment/CMS-2023-0001-0001",
},
]
store.attach_file.side_effect = Exception("dup")
api = MagicMock()
api.get_comment_detail.return_value = {
"data": {"attributes": {"comment": "text", "organization": ""}},
"included": [
{
"type": "attachments",
"attributes": {
"fileFormats": [{"fileUrl": "https://x.com/doc.pdf"}]
},
}
],
}
api.download_attachment.return_value = tmp_path / "doc.pdf"
api.download_attachments_batch.return_value = ([tmp_path / "doc.pdf"], [])
stats = backfill_details(
store, api, limit=1, scratch_root=tmp_path, commit_every=1
)
assert stats["enriched"] == 1
assert stats["attached"] == 0 # attach_file failed
def test_enriched_ok_tag_fails(self):
"""Lines 432, 433: add_tag for 'enriched:ok' fails."""
store = MagicMock()
con = MagicMock()
store._con.return_value = con
con.execute.return_value.fetchall.return_value = [
{
"id": 1,
"key": "K1",
"url": "https://www.regulations.gov/comment/CMS-2023-0001-0001",
},
]
# add_tag for org succeeds, add_tag for enriched:ok fails
def add_tag_side(key, tag):
if tag == "enriched:ok":
raise Exception("fail")
store.add_tag.side_effect = add_tag_side
api = MagicMock()
api.get_comment_detail.return_value = {
"data": {"attributes": {"comment": "text", "organization": "Org"}},
"included": [],
}
stats = backfill_details(store, api, limit=1)
assert stats["enriched"] == 1
def test_empty_att_url_skipped(self, tmp_path):
"""Line 415: empty fileUrl in attachment is skipped."""
store = MagicMock()
con = MagicMock()
store._con.return_value = con
con.execute.return_value.fetchall.return_value = [
{
"id": 1,
"key": "K1",
"url": "https://www.regulations.gov/comment/CMS-2023-0001-0001",
},
]
api = MagicMock()
api.get_comment_detail.return_value = {
"data": {"attributes": {"comment": "text", "organization": ""}},
"included": [
{
"type": "attachments",
"attributes": {"fileFormats": [{"fileUrl": ""}]},
}
],
}
stats = backfill_details(store, api, limit=1, scratch_root=tmp_path)
assert stats["enriched"] == 1
assert stats["attached"] == 0