Skip to content

Commit 65ae384

Browse files
committed
Address review: date-parse safety, HHS user-agent and CSV detection, Maine nested links
1 parent 28ec885 commit 65ae384

5 files changed

Lines changed: 36 additions & 8 deletions

File tree

breach_scraper/sources/hhs_ocr.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ def _normalize_date(value: str) -> str:
4747
cleaned = _clean_text(value)
4848
if not cleaned:
4949
return ""
50-
return datetime.strptime(cleaned, "%m/%d/%Y").date().isoformat()
50+
try:
51+
return datetime.strptime(cleaned, "%m/%d/%Y").date().isoformat()
52+
except ValueError:
53+
return cleaned
5154

5255

5356
def _normalize_count(value: str) -> str:
@@ -217,7 +220,10 @@ def fetch_html(
217220
export_request = Request(
218221
url,
219222
data=data,
220-
headers={**_build_headers(), "Content-Type": "application/x-www-form-urlencoded"},
223+
headers={
224+
**_build_headers(user_agent),
225+
"Content-Type": "application/x-www-form-urlencoded",
226+
},
221227
)
222228
with opener.open(export_request, timeout=timeout) as response: # nosec B310
223229
charset = response.headers.get_content_charset() or "utf-8"
@@ -228,7 +234,8 @@ def fetch_html(
228234

229235

230236
def _parse_csv_export(text: str) -> list[dict[str, str]]:
231-
if not text.lstrip().startswith('"'):
237+
cleaned = text.lstrip()
238+
if cleaned.startswith("<") or "Covered Entity Type" not in cleaned:
232239
return []
233240

234241
rows = list(csv.reader(io.StringIO(text)))

breach_scraper/sources/maine_ag.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,8 @@ def handle_endtag(self, tag: str) -> None:
411411
text = item.text
412412
if self.item_stack:
413413
self.item_stack[-1].add_child_text(text)
414+
if item.href and not self.item_stack[-1].href:
415+
self.item_stack[-1].href = item.href
414416
elif text:
415417
self.items.append((self.current_section, text, item.href))
416418

breach_scraper/sources/or_doj.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ def _normalize_single_date(value: str) -> str:
4848
cleaned = _clean_text(value)
4949
if not cleaned or cleaned in SENTINEL_DATES:
5050
return ""
51-
return datetime.strptime(cleaned, "%m/%d/%Y").date().isoformat()
51+
try:
52+
return datetime.strptime(cleaned, "%m/%d/%Y").date().isoformat()
53+
except ValueError:
54+
return cleaned
5255

5356

5457
def _normalize_dateish(value: str) -> str:
@@ -61,8 +64,8 @@ def _normalize_dateish(value: str) -> str:
6164
if not part:
6265
continue
6366

64-
if " - " in part:
65-
range_parts = [_normalize_single_date(item) for item in part.split(" - ")]
67+
if "-" in part:
68+
range_parts = [_normalize_single_date(item) for item in re.split(r"\s*-\s*", part)]
6669
range_parts = [item for item in range_parts if item]
6770
if range_parts:
6871
normalized_parts.append(" to ".join(range_parts))

tests/test_hhs_ocr.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import unittest
22

3-
from breach_scraper.sources.hhs_ocr import parse_breach_table
3+
from breach_scraper.sources.hhs_ocr import _parse_csv_export, parse_breach_table
44

55
HHS_HTML = """
66
<html>
@@ -59,5 +59,13 @@ def test_parse_breach_table_handles_csv_export(self) -> None:
5959
self.assertEqual(records[0]["individuals_affected"], "14,442")
6060

6161

62+
class TestHhsCsvDetection(unittest.TestCase):
63+
def test_html_is_not_treated_as_csv(self) -> None:
64+
self.assertEqual(_parse_csv_export(" <html><body>nope</body></html>"), [])
65+
66+
def test_csv_without_known_header_is_rejected(self) -> None:
67+
self.assertEqual(_parse_csv_export('"a","b"\n"1","2"\n'), [])
68+
69+
6270
if __name__ == "__main__":
6371
unittest.main()

tests/test_or_doj.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import unittest
22

3-
from breach_scraper.sources.or_doj import parse_breach_table
3+
from breach_scraper.sources.or_doj import _normalize_dateish, parse_breach_table
44

55
OREGON_HTML = """
66
<html>
@@ -100,5 +100,13 @@ def test_parse_breach_table_handles_live_table_markup(self) -> None:
100100
self.assertEqual(records[0]["number_affected"], "139,899")
101101

102102

103+
class TestOrDojDateNormalization(unittest.TestCase):
104+
def test_tight_range_is_split(self) -> None:
105+
self.assertEqual(_normalize_dateish("8/13/2025-8/14/2025"), "2025-08-13 to 2025-08-14")
106+
107+
def test_invalid_date_returns_original(self) -> None:
108+
self.assertEqual(_normalize_dateish("13/45/2025"), "13/45/2025")
109+
110+
103111
if __name__ == "__main__":
104112
unittest.main()

0 commit comments

Comments
 (0)