feat(prisma): grey-literature harvest parsers
This commit is contained in:
111
src/prisma/harvest.py
Normal file
111
src/prisma/harvest.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""Grey-literature HTML parsers. The caller supplies the page. No HTTP.
|
||||
|
||||
Each source has its own function. They share one stdlib parser because
|
||||
the fixture contract is the same: one report link and one date. A source
|
||||
whose real page differs gets its own fixture later.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from html.parser import HTMLParser
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Harvested:
|
||||
title: str
|
||||
url: str
|
||||
date: str
|
||||
source: str
|
||||
|
||||
|
||||
class _Page(HTMLParser):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(convert_charrefs=True)
|
||||
self.links: list[tuple[str, str]] = []
|
||||
self.times: list[str] = []
|
||||
self._href: str | None = None
|
||||
self._link_text: list[str] = []
|
||||
self._in_time = False
|
||||
self._time_text: list[str] = []
|
||||
|
||||
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
||||
values = {name: value or "" for name, value in attrs}
|
||||
if tag == "a" and values.get("href"):
|
||||
self._href = values["href"]
|
||||
self._link_text = []
|
||||
elif tag == "time":
|
||||
self._in_time = True
|
||||
self._time_text = []
|
||||
|
||||
def handle_endtag(self, tag: str) -> None:
|
||||
if tag == "a" and self._href is not None:
|
||||
self.links.append((self._href, "".join(self._link_text).strip()))
|
||||
self._href = None
|
||||
elif tag == "time" and self._in_time:
|
||||
self.times.append("".join(self._time_text).strip())
|
||||
self._in_time = False
|
||||
|
||||
def handle_data(self, data: str) -> None:
|
||||
if self._href is not None:
|
||||
self._link_text.append(data)
|
||||
if self._in_time:
|
||||
self._time_text.append(data)
|
||||
|
||||
|
||||
def _parse(html: str, source: str) -> tuple[Harvested, ...]:
|
||||
if not html:
|
||||
return ()
|
||||
page = _Page()
|
||||
page.feed(html)
|
||||
page.close()
|
||||
date = page.times[0] if page.times else ""
|
||||
return tuple(
|
||||
Harvested(title=title, url=url, date=date, source=source)
|
||||
for url, title in page.links
|
||||
)
|
||||
|
||||
|
||||
def parse_cms(html: str) -> tuple[Harvested, ...]:
|
||||
return _parse(html, "cms")
|
||||
|
||||
|
||||
def parse_medpac(html: str) -> tuple[Harvested, ...]:
|
||||
return _parse(html, "medpac")
|
||||
|
||||
|
||||
def parse_macpac(html: str) -> tuple[Harvested, ...]:
|
||||
return _parse(html, "macpac")
|
||||
|
||||
|
||||
def parse_aspe(html: str) -> tuple[Harvested, ...]:
|
||||
return _parse(html, "aspe")
|
||||
|
||||
|
||||
def parse_gao(html: str) -> tuple[Harvested, ...]:
|
||||
return _parse(html, "gao")
|
||||
|
||||
|
||||
def parse_cbo(html: str) -> tuple[Harvested, ...]:
|
||||
return _parse(html, "cbo")
|
||||
|
||||
|
||||
def parse_crs(html: str) -> tuple[Harvested, ...]:
|
||||
return _parse(html, "crs")
|
||||
|
||||
|
||||
def parse_kff(html: str) -> tuple[Harvested, ...]:
|
||||
return _parse(html, "kff")
|
||||
|
||||
|
||||
PARSERS: dict[str, Callable[[str], tuple[Harvested, ...]]] = {
|
||||
"cms": parse_cms,
|
||||
"medpac": parse_medpac,
|
||||
"macpac": parse_macpac,
|
||||
"aspe": parse_aspe,
|
||||
"gao": parse_gao,
|
||||
"cbo": parse_cbo,
|
||||
"crs": parse_crs,
|
||||
"kff": parse_kff,
|
||||
}
|
||||
27
tests/prisma/test_harvest.py
Normal file
27
tests/prisma/test_harvest.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""Grey-literature HTML parsers. No HTTP."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from prisma.harvest import PARSERS, Harvested
|
||||
|
||||
_SOURCES = ("cms", "medpac", "macpac", "aspe", "gao", "cbo", "crs", "kff")
|
||||
_HTML = '<a href="https://example.test/r">Report title</a><time>2026-01-02</time>'
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source", _SOURCES)
|
||||
def test_parser_reads_the_report_link_and_date(source: str):
|
||||
assert PARSERS[source](_HTML) == (
|
||||
Harvested(
|
||||
title="Report title",
|
||||
url="https://example.test/r",
|
||||
date="2026-01-02",
|
||||
source=source,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source", _SOURCES)
|
||||
def test_empty_html_returns_nothing(source: str):
|
||||
assert PARSERS[source]("") == ()
|
||||
Reference in New Issue
Block a user