From c48d6113ddd3b0de4169d9f7f36071b3f67c26ad Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 13 Oct 2026 14:04:02 +0200 Subject: [PATCH] Seeded repo --- INTENT.md | 22 ++++++ README.md | 48 ++++++++++++ pyproject.toml | 16 ++++ tests/fixtures/btos_national.xlsx | Bin 0 -> 88868 bytes tests/fixtures/eurostat_isoc_eb_ai.json | 1 + tests/test_web_fetcher.py | 82 ++++++++++++++++++++ web_fetcher/__init__.py | 13 ++++ web_fetcher/__main__.py | 41 ++++++++++ web_fetcher/adapters/__init__.py | 1 + web_fetcher/adapters/eurostat.py | 61 +++++++++++++++ web_fetcher/adapters/http.py | 97 ++++++++++++++++++++++++ web_fetcher/core.py | 93 +++++++++++++++++++++++ web_fetcher/registry.py | 42 ++++++++++ web_fetcher/xlsx.py | 53 +++++++++++++ 14 files changed, 570 insertions(+) create mode 100644 INTENT.md create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 tests/fixtures/btos_national.xlsx create mode 100644 tests/fixtures/eurostat_isoc_eb_ai.json create mode 100644 tests/test_web_fetcher.py create mode 100644 web_fetcher/__init__.py create mode 100644 web_fetcher/__main__.py create mode 100644 web_fetcher/adapters/__init__.py create mode 100644 web_fetcher/adapters/eurostat.py create mode 100644 web_fetcher/adapters/http.py create mode 100644 web_fetcher/core.py create mode 100644 web_fetcher/registry.py create mode 100644 web_fetcher/xlsx.py diff --git a/INTENT.md b/INTENT.md new file mode 100644 index 0000000..903d9e8 --- /dev/null +++ b/INTENT.md @@ -0,0 +1,22 @@ +# INTENT + +`web-fetcher` exists to give research repositories a shared, trustworthy way +to pull data from the web and from public APIs. + +**Core commitments** + +1. **Provenance first.** No value without source, URL, retrieval time and + payload hash. +2. **Never invent.** Missing data is an explicit `not_found`, never a guess. + Language models are not a data source here. +3. **Reusable adapters.** Statistics APIs, CSV/JSON endpoints and + spreadsheets behind one small interface. New sources are added as adapters + with offline fixture tests. +4. **Replayable.** An optional content-addressed cache reproduces a run from + the exact bytes it used. +5. **Minimal footprint.** Standard library only, so it runs inside executors + (for example rein-aharness on railiance01) without dependency management. + +Out of scope: crawling, scraping behind logins, LLM-based extraction, +scheduling (activity-core owns *when*), and execution orchestration +(rein-aharness owns *how it runs*). diff --git a/README.md b/README.md new file mode 100644 index 0000000..7bf6aba --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# web-fetcher + +Reusable, provenance-first fetching of web and API data for research. + +Every fetch returns `Observation` records with the value **and** where it came +from: source, URL, the source's own publication or update time, retrieval +time, and the SHA-256 of the raw payload. Values are never estimated: a spec +yields a sourced value, or an explicit `not_found` or `error` record. An +optional content-addressed cache lets a research run be replayed from the +exact bytes it used. + +Standard library only. Python 3.10+. + +## Adapters + +| Adapter | Use | Key spec fields | +|---|---|---| +| `eurostat` | Eurostat dissemination API (JSON-stat 2.0) | `dataset`, `filters`, `split_by`, `last` | +| `http_json` | any JSON endpoint | `url`, `path` (dotted), `period_path` | +| `http_csv` | any CSV endpoint (e.g. FRED `fredgraph.csv`) | `url`, `filter`, `value_column`, `period_column` | +| `xlsx_wide` | spreadsheets with one column per period (e.g. Census BTOS) | `url`, `sheet`, `filter`, `period_regex` | +| `manual` | no machine-readable source; recorded as `not_found` with a note | `source` | + +## Usage + +```bash +python3 -m web_fetcher fetch specs.json [--cache .cache] [--only ID] > observations.jsonl +``` + +```python +from web_fetcher import run_spec +obs = run_spec({"id": "eu_ai", "adapter": "eurostat", "dataset": "isoc_eb_ai", + "filters": {"geo": ["EU27_2020"], "unit": "PC_ENT", "size_emp": "GE10", + "indic_is": "E_AI_TANY"}, "split_by": ["geo"]}) +``` + +## Tests + +```bash +python3 -B -m unittest discover -s tests +``` + +Tests run offline against fixtures in `tests/fixtures/`. + +## Consumers + +- `assembly-economics`: reality checkpoints (`checkpoints/sources.json`, + `checkpoints/run_checkpoint.py`). diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..94b5e55 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "web-fetcher" +version = "0.1.0" +description = "Provenance-first fetching of web and API data for research" +requires-python = ">=3.10" +dependencies = [] + +[project.scripts] +web-fetcher = "web_fetcher.__main__:main" + +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +include = ["web_fetcher*"] diff --git a/tests/fixtures/btos_national.xlsx b/tests/fixtures/btos_national.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..9c7fbdafaa72fc2eea7871dae2d1964c11682716 GIT binary patch literal 88868 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 aJF>}-42P!4#GYp>aYYL-u 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 -tcHs22mN|aL4yDvXZ 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 b{Vyq#mqGx@00V;w{Qn9}+BPcpB>evXbn*l# literal 0 HcmV?d00001 diff --git a/tests/fixtures/eurostat_isoc_eb_ai.json b/tests/fixtures/eurostat_isoc_eb_ai.json new file mode 100644 index 0000000..ac133f0 --- /dev/null +++ b/tests/fixtures/eurostat_isoc_eb_ai.json @@ -0,0 +1 @@ +{"version":"2.0","class":"dataset","label":"Artificial intelligence by size class of enterprise","source":"ESTAT","updated":"2026-06-15T11:00:00+0200","value":{"3":11.55,"4":19.75,"5":25.97,"0":8.06,"1":13.48,"2":19.95},"id":["freq","size_emp","nace_r2","indic_is","unit","geo","time"],"size":[1,1,1,1,1,2,3],"dimension":{"freq":{"label":"Time frequency","category":{"index":{"A":0},"label":{"A":"Annual"}}},"size_emp":{"label":"Size classes in number of persons employed","category":{"index":{"GE10":0},"label":{"GE10":"10 persons employed or more"}}},"nace_r2":{"label":"Statistical classification of economic activities in the European Community (NACE Rev. 2)","category":{"index":{"C10-S951_X_K":0},"label":{"C10-S951_X_K":"All activities (except agriculture, forestry and fishing, and mining and quarrying), without financial sector"}}},"indic_is":{"label":"Information society indicator","category":{"index":{"E_AI_TANY":0},"label":{"E_AI_TANY":"Enterprises using at least one of the AI technologies: AI_TTM, AI_TSR, AI_TNLG, AI_TIR, AI_TML, AI_TPA, AI_TAR, E_AI_TPVSG"}}},"unit":{"label":"Unit of measure","category":{"index":{"PC_ENT":0},"label":{"PC_ENT":"Percentage of enterprises"}}},"geo":{"label":"Geopolitical entity (reporting)","category":{"index":{"EU27_2020":0,"DE":1},"label":{"EU27_2020":"European Union - 27 countries (from 2020)","DE":"Germany"}}},"time":{"label":"Time","category":{"index":{"2023":0,"2024":1,"2025":2},"label":{"2023":"2023","2024":"2024","2025":"2025"}}}},"extension":{"lang":"EN","id":"ISOC_EB_AI","agencyId":"ESTAT","version":"1.0","datastructure":{"id":"ISOC_EB_AI","agencyId":"ESTAT","version":"45.0"},"annotation":[{"type":"CREATED","date":"2022-12-08T09:14:11+0100"},{"type":"DISSEMINATION_DOI_XML","title":"10.2908/ISOC_EB_AI2022-10-26"},{"type":"DISSEMINATION_OBJECT_TYPE","title":"DATASET"},{"type":"DISSEMINATION_TIMESTAMP_DATA","date":"2026-06-15T11:00:00+0200"},{"type":"DISSEMINATION_TIMESTAMP_GLOBAL","date":"2026-06-15T11:00:00+0200"},{"type":"DISSEMINATION_TIMESTAMP_PLANNED","date":"2026-06-15T11:00:00+0200"},{"type":"ESMS_HTML","title":"Explanatory texts (metadata)","href":"https://ec.europa.eu/eurostat/cache/metadata/en/isoc_e_esms.htm"},{"type":"ESMS_SDMX","title":"Explanatory texts (metadata)","href":"https://ec.europa.eu/eurostat/api/dissemination/files?file=metadata/isoc_e_esms.sdmx.zip"},{"type":"OBS_COUNT","title":"70756"},{"type":"OBS_PERIOD_OVERALL_LATEST","title":"2025"},{"type":"OBS_PERIOD_OVERALL_OLDEST","title":"2021"},{"type":"SOURCE_INSTITUTIONS","text":"Eurostat"},{"type":"UPDATE_DATA","date":"2026-06-15T11:00:00+0200"},{"type":"UPDATE_STRUCTURE","date":"2025-12-11T11:00:00+0100"}],"positions-with-no-data":{"freq":[],"size_emp":[],"nace_r2":[],"indic_is":[],"unit":[],"geo":[],"time":[]}}} \ No newline at end of file diff --git a/tests/test_web_fetcher.py b/tests/test_web_fetcher.py new file mode 100644 index 0000000..16a96d8 --- /dev/null +++ b/tests/test_web_fetcher.py @@ -0,0 +1,82 @@ +"""Offline tests (fixtures only). Usage: python3 -m unittest discover -s tests""" + +import json +import unittest +from pathlib import Path + +from web_fetcher.core import Fetched +from web_fetcher.registry import run_spec + +FIX = Path(__file__).parent / "fixtures" + + +def fake(body: bytes, url: str = "https://example.test/x"): + return lambda u: Fetched(url=u, body=body, retrieved_at="2026-10-12T00:00:00+00:00", + sha256="0" * 64, content_type="application/json") + + +class TestEurostat(unittest.TestCase): + def test_parses_values_per_geo_and_period(self): + body = (FIX / "eurostat_isoc_eb_ai.json").read_bytes() + spec = {"id": "eu_ai_use", "adapter": "eurostat", "dataset": "isoc_eb_ai", + "filters": {"geo": ["EU27_2020", "DE"], "unit": "PC_ENT", "size_emp": "GE10", + "indic_is": "E_AI_TANY"}, "split_by": ["geo"], "last": 3} + obs = run_spec(spec, get=fake(body)) + got = {(o.labels["geo"], o.period): o.value for o in obs} + self.assertEqual(got[("EU27_2020", "2025")], 19.95) + self.assertEqual(got[("DE", "2023")], 11.55) + self.assertTrue(all(o.status == "ok" and o.url and o.payload_sha256 for o in obs)) + self.assertEqual(obs[0].unit, "Percentage of enterprises") + + def test_url_contains_filters(self): + from web_fetcher.adapters.eurostat import build_url + url = build_url({"dataset": "isoc_eb_ai", "filters": {"geo": ["DE"], "unit": "PC_ENT"}, "last": 2}) + self.assertIn("isoc_eb_ai?", url) + self.assertIn("geo=DE", url) + self.assertIn("lastTimePeriod=2", url) + + +class TestXlsx(unittest.TestCase): + def test_btos_ai_use_latest_period(self): + body = (FIX / "btos_national.xlsx").read_bytes() + obs = run_spec({"id": "us_ai_use", "adapter": "xlsx_wide", "url": "https://x.test/National.xlsx", + "sheet": "Response Estimates", "filter": {"Question ID": "7", "Answer ID": "1"}, + "unit": "% of firms"}, get=fake(body)) + self.assertEqual(obs[0].status, "ok") + self.assertEqual(obs[0].period, "202620") + self.assertAlmostEqual(obs[0].value, 23.8) + + +class TestHttp(unittest.TestCase): + def test_json_path(self): + body = json.dumps({"data": {"rows": [{"v": 7.5, "p": "2026-Q3"}]}}).encode() + obs = run_spec({"id": "j", "adapter": "http_json", "url": "https://x.test", "path": "data.rows.0.v", + "period_path": "data.rows.0.p", "unit": "%"}, get=fake(body)) + self.assertEqual((obs[0].status, obs[0].value, obs[0].period), ("ok", 7.5, "2026-Q3")) + + def test_json_missing_path_is_not_found(self): + obs = run_spec({"id": "j", "adapter": "http_json", "url": "https://x.test", "path": "nope"}, + get=fake(b"{}")) + self.assertEqual(obs[0].status, "not_found") + + def test_csv_latest_matching_row(self): + body = b"geo,period,value\nUS,2026-08,8.1\nUS,2026-09,8.7\nDE,2026-09,9.9\n" + obs = run_spec({"id": "c", "adapter": "http_csv", "url": "https://x.test", "filter": {"geo": "US"}, + "value_column": "value", "period_column": "period"}, get=fake(body)) + self.assertEqual((obs[0].value, obs[0].period), (8.7, "2026-09")) + + def test_manual_and_unknown_adapters_never_raise(self): + self.assertEqual(run_spec({"id": "m", "adapter": "manual"})[0].status, "not_found") + self.assertEqual(run_spec({"id": "u", "adapter": "nope"})[0].status, "error") + + def test_fetch_error_becomes_error_observation(self): + from web_fetcher.core import FetchError + + def boom(url): + raise FetchError("offline") + obs = run_spec({"id": "e", "adapter": "http_json", "url": "https://x.test", "path": "a"}, get=boom) + self.assertEqual(obs[0].status, "error") + + +if __name__ == "__main__": + unittest.main() diff --git a/web_fetcher/__init__.py b/web_fetcher/__init__.py new file mode 100644 index 0000000..03a6eb5 --- /dev/null +++ b/web_fetcher/__init__.py @@ -0,0 +1,13 @@ +"""web-fetcher: reusable, provenance-first fetching of web and API data for research. + +Every fetch returns Observation records that say where a value came from +(URL), when it was retrieved, and a hash of the raw payload, so downstream +research can cite and re-verify it. Values are never estimated: a fetch either +yields a sourced value or an explicit `not_found` / `error` record. +""" + +from web_fetcher.core import Observation, fetch_bytes +from web_fetcher.registry import ADAPTERS, run_spec, run_specs + +__all__ = ["Observation", "fetch_bytes", "ADAPTERS", "run_spec", "run_specs"] +__version__ = "0.1.0" diff --git a/web_fetcher/__main__.py b/web_fetcher/__main__.py new file mode 100644 index 0000000..00ee670 --- /dev/null +++ b/web_fetcher/__main__.py @@ -0,0 +1,41 @@ +"""CLI: python -m web_fetcher fetch SPECS.json [--cache DIR] [--out FILE] + +SPECS.json is a JSON list of spec objects (or {"specs": [...]}). Output is +JSON Lines, one Observation per line. +""" + +from __future__ import annotations + +import argparse +import json +import sys + +from web_fetcher.registry import run_specs + + +def main(argv: list[str] | None = None) -> int: + ap = argparse.ArgumentParser(prog="web_fetcher") + sub = ap.add_subparsers(dest="cmd", required=True) + f = sub.add_parser("fetch", help="run fetch specs") + f.add_argument("specs") + f.add_argument("--cache", default=None, help="content-addressed cache directory") + f.add_argument("--out", default="-", help="output JSONL path (default stdout)") + f.add_argument("--only", action="append", default=[], help="run only these spec ids") + args = ap.parse_args(argv) + with open(args.specs, encoding="utf-8") as fh: + data = json.load(fh) + specs = data["specs"] if isinstance(data, dict) else data + if args.only: + specs = [s for s in specs if s.get("id") in set(args.only)] + obs = run_specs(specs, cache_dir=args.cache) + lines = "\n".join(o.to_json() for o in obs) + "\n" + if args.out == "-": + sys.stdout.write(lines) + else: + with open(args.out, "w", encoding="utf-8") as fh: + fh.write(lines) + return 0 if all(o.status != "error" for o in obs) else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/web_fetcher/adapters/__init__.py b/web_fetcher/adapters/__init__.py new file mode 100644 index 0000000..a5edce3 --- /dev/null +++ b/web_fetcher/adapters/__init__.py @@ -0,0 +1 @@ +"""Source adapters. Each exposes `fetch(spec, fetcher) -> list[Observation]`.""" diff --git a/web_fetcher/adapters/eurostat.py b/web_fetcher/adapters/eurostat.py new file mode 100644 index 0000000..3ad6bfd --- /dev/null +++ b/web_fetcher/adapters/eurostat.py @@ -0,0 +1,61 @@ +"""Eurostat dissemination API (JSON-stat 2.0). + +Spec fields: + dataset Eurostat dataset code, e.g. "isoc_eb_ai" + filters dict of dimension -> code or list of codes (all non-time dimensions + should be pinned to one code except those listed in `split_by`) + split_by dimensions that may vary; one observation per combination (e.g. ["geo"]) + last number of latest time periods to return (default 1) +""" + +from __future__ import annotations + +import itertools +import json +import urllib.parse +from typing import Any, Callable + +from web_fetcher.core import Fetched, Observation + +BASE = "https://ec.europa.eu/eurostat/api/dissemination/statistics/1.0/data/" + + +def build_url(spec: dict[str, Any]) -> str: + params: list[tuple[str, str]] = [("format", "JSON"), ("lang", "EN")] + for dim, val in (spec.get("filters") or {}).items(): + for v in (val if isinstance(val, list) else [val]): + params.append((dim, str(v))) + params.append(("lastTimePeriod", str(int(spec.get("last", 1))))) + return BASE + spec["dataset"] + "?" + urllib.parse.urlencode(params) + + +def parse(doc: dict[str, Any], spec: dict[str, Any], fetched: Fetched) -> list[Observation]: + ids = doc["id"] + dims = doc["dimension"] + cats = [sorted(dims[i]["category"]["index"], key=lambda k: dims[i]["category"]["index"][k]) for i in ids] + values = doc.get("value") or {} + unit_dim = "unit" if "unit" in ids else None + out: list[Observation] = [] + for n, combo in enumerate(itertools.product(*cats)): + v = values.get(str(n)) if isinstance(values, dict) else (values[n] if n < len(values) else None) + if v is None: + continue + coord = dict(zip(ids, combo)) + labels = {d: coord[d] for d in spec.get("split_by", [])} + out.append(Observation( + spec_id=spec["id"], status="ok", value=v, + unit=(dims[unit_dim]["category"]["label"].get(coord[unit_dim]) if unit_dim else None), + period=coord.get("time"), labels=labels, source=f"Eurostat {spec['dataset']}: {doc.get('label')}", + url=fetched.url, published=doc.get("updated"), retrieved_at=fetched.retrieved_at, + payload_sha256=fetched.sha256, + )) + if not out: + out.append(Observation(spec_id=spec["id"], status="not_found", url=fetched.url, + retrieved_at=fetched.retrieved_at, payload_sha256=fetched.sha256, + note="no values for the given filters")) + return out + + +def fetch(spec: dict[str, Any], get: Callable[[str], Fetched]) -> list[Observation]: + fetched = get(build_url(spec)) + return parse(json.loads(fetched.body), spec, fetched) diff --git a/web_fetcher/adapters/http.py b/web_fetcher/adapters/http.py new file mode 100644 index 0000000..bf296ac --- /dev/null +++ b/web_fetcher/adapters/http.py @@ -0,0 +1,97 @@ +r"""Generic HTTP adapters: JSON (dotted path) and CSV (filter + pick latest). + +json spec fields: url, path ("a.b.0.c"), unit?, period_path? +csv spec fields: url, filter (dict column -> value), value_column, + period_column?, unit?, delimiter? -> latest row by period +xlsx_wide fields: url, sheet, filter (dict header -> value), period_regex + (header pattern of period columns, default ^\d{6}$), unit? + -> value of the matching row in the latest period column +""" + +from __future__ import annotations + +import csv +import io +import json +from typing import Any, Callable + +from web_fetcher.core import Fetched, Observation + + +def _dig(obj: Any, path: str) -> Any: + for part in path.split("."): + if obj is None: + return None + if isinstance(obj, list): + try: + obj = obj[int(part)] + except (ValueError, IndexError): + return None + else: + obj = obj.get(part) if isinstance(obj, dict) else None + return obj + + +def fetch_json(spec: dict[str, Any], get: Callable[[str], Fetched]) -> list[Observation]: + f = get(spec["url"]) + doc = json.loads(f.body) + value = _dig(doc, spec["path"]) + period = _dig(doc, spec["period_path"]) if spec.get("period_path") else spec.get("period") + status = "ok" if value is not None else "not_found" + return [Observation(spec_id=spec["id"], status=status, value=value, unit=spec.get("unit"), + period=None if period is None else str(period), source=spec.get("source"), + url=f.url, retrieved_at=f.retrieved_at, payload_sha256=f.sha256, + note=None if value is not None else f"path {spec['path']} not found")] + + +def fetch_csv(spec: dict[str, Any], get: Callable[[str], Fetched]) -> list[Observation]: + f = get(spec["url"]) + text = f.body.decode(spec.get("encoding", "utf-8-sig"), errors="replace") + rows = list(csv.DictReader(io.StringIO(text), delimiter=spec.get("delimiter", ","))) + flt = spec.get("filter") or {} + rows = [r for r in rows if all(str(r.get(k, "")).strip() == str(v) for k, v in flt.items())] + pcol, vcol = spec.get("period_column"), spec["value_column"] + rows = [r for r in rows if str(r.get(vcol, "")).strip() not in ("", "NA", "N/A", "-")] + if not rows: + return [Observation(spec_id=spec["id"], status="not_found", url=f.url, retrieved_at=f.retrieved_at, + payload_sha256=f.sha256, source=spec.get("source"), note="no matching row")] + row = max(rows, key=lambda r: str(r.get(pcol, ""))) if pcol else rows[-1] + raw = str(row[vcol]).strip().replace("%", "") + try: + value: Any = float(raw) + except ValueError: + value = raw + return [Observation(spec_id=spec["id"], status="ok", value=value, unit=spec.get("unit"), + period=str(row.get(pcol)) if pcol else None, source=spec.get("source"), + labels={k: str(v) for k, v in flt.items()}, url=f.url, + retrieved_at=f.retrieved_at, payload_sha256=f.sha256)] + + +def fetch_xlsx_wide(spec: dict[str, Any], get: Callable[[str], Fetched]) -> list[Observation]: + import re + + from web_fetcher.xlsx import read_sheet + + f = get(spec["url"]) + rows = read_sheet(f.body, spec.get("sheet", 0)) + header, body = rows[0], rows[1:] + pat = re.compile(spec.get("period_regex", r"^\d{6}$")) + periods = [(i, h) for i, h in enumerate(header) if pat.match(h or "")] + flt = spec.get("filter") or {} + col = {h: i for i, h in enumerate(header)} + match = [r for r in body if all(i < len(r) and r[i].strip() == str(v) for h, v in flt.items() + for i in [col.get(h, 10**9)])] + base = dict(spec_id=spec["id"], url=f.url, retrieved_at=f.retrieved_at, payload_sha256=f.sha256, + source=spec.get("source"), labels={k: str(v) for k, v in flt.items()}) + if not match or not periods: + return [Observation(status="not_found", note="no matching row or period columns", **base)] + row = match[0] + for i, h in sorted(periods, key=lambda p: p[1], reverse=True): + raw = (row[i] if i < len(row) else "").strip().replace("%", "") + if raw not in ("", "S", "NA", "-"): + try: + value: Any = float(raw) + except ValueError: + value = raw + return [Observation(status="ok", value=value, unit=spec.get("unit"), period=h, **base)] + return [Observation(status="not_found", note="matching row has no value", **base)] diff --git a/web_fetcher/core.py b/web_fetcher/core.py new file mode 100644 index 0000000..9fc59a2 --- /dev/null +++ b/web_fetcher/core.py @@ -0,0 +1,93 @@ +"""Core types: Observation record and provenance-tracked HTTP fetching.""" + +from __future__ import annotations + +import hashlib +import json +import os +import time +import urllib.error +import urllib.request +from dataclasses import asdict, dataclass, field +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +USER_AGENT = "web-fetcher/0.1 (+https://forgejo.coulomb.social/coulomb/web-fetcher)" + + +@dataclass +class Observation: + """One fetched value with its provenance. status: ok | not_found | error.""" + + spec_id: str + status: str + value: Any = None + unit: str | None = None + period: str | None = None + labels: dict[str, str] = field(default_factory=dict) + source: str | None = None # dataset or source name + url: str | None = None + published: str | None = None # source-side update/publication timestamp + retrieved_at: str | None = None + payload_sha256: str | None = None + note: str | None = None + + def to_dict(self) -> dict[str, Any]: + return asdict(self) + + def to_json(self) -> str: + return json.dumps(self.to_dict(), ensure_ascii=False, sort_keys=True) + + +@dataclass +class Fetched: + url: str + body: bytes + retrieved_at: str + sha256: str + content_type: str | None + + +class FetchError(RuntimeError): + pass + + +def _now() -> str: + return datetime.now(timezone.utc).isoformat(timespec="seconds") + + +def fetch_bytes(url: str, *, timeout: float = 60.0, retries: int = 2, + cache_dir: str | os.PathLike | None = None, + headers: dict[str, str] | None = None) -> Fetched: + """GET `url` and return body plus provenance. Optional content-addressed cache. + + The cache stores each payload under its SHA-256 and an index entry per URL, + so a research run can be replayed offline from the exact bytes it used. + """ + hdrs = {"User-Agent": USER_AGENT, **(headers or {})} + last: Exception | None = None + for attempt in range(retries + 1): + try: + req = urllib.request.Request(url, headers=hdrs) + with urllib.request.urlopen(req, timeout=timeout) as resp: + body = resp.read() + ctype = resp.headers.get("Content-Type") + digest = hashlib.sha256(body).hexdigest() + fetched = Fetched(url=url, body=body, retrieved_at=_now(), sha256=digest, content_type=ctype) + if cache_dir: + _cache_store(Path(cache_dir), fetched) + return fetched + except (urllib.error.URLError, TimeoutError, OSError) as exc: + last = exc + if attempt < retries: + time.sleep(1.5 * (attempt + 1)) + raise FetchError(f"fetch failed for {url}: {last}") + + +def _cache_store(cache: Path, fetched: Fetched) -> None: + cache.mkdir(parents=True, exist_ok=True) + (cache / f"{fetched.sha256}.bin").write_bytes(fetched.body) + with (cache / "index.jsonl").open("a", encoding="utf-8") as fh: + fh.write(json.dumps({"url": fetched.url, "sha256": fetched.sha256, + "retrieved_at": fetched.retrieved_at}) + "\n") diff --git a/web_fetcher/registry.py b/web_fetcher/registry.py new file mode 100644 index 0000000..de64a7e --- /dev/null +++ b/web_fetcher/registry.py @@ -0,0 +1,42 @@ +"""Spec dispatch: run one or many fetch specs, never raising for a single failure.""" + +from __future__ import annotations + +from typing import Any, Callable + +from web_fetcher.adapters import eurostat, http +from web_fetcher.core import Fetched, FetchError, Observation, fetch_bytes + +ADAPTERS: dict[str, Callable[[dict[str, Any], Callable[[str], Fetched]], list[Observation]]] = { + "eurostat": eurostat.fetch, + "http_json": http.fetch_json, + "http_csv": http.fetch_csv, + "xlsx_wide": http.fetch_xlsx_wide, +} + + +def run_spec(spec: dict[str, Any], get: Callable[[str], Fetched] | None = None, + cache_dir: str | None = None) -> list[Observation]: + """Run one spec. Adapter errors become an `error` observation, not an exception.""" + sid = spec.get("id", "?") + kind = spec.get("adapter") + if kind == "manual": + return [Observation(spec_id=sid, status="not_found", source=spec.get("source"), + note="manual: no machine-readable source; collect at review")] + adapter = ADAPTERS.get(kind or "") + if adapter is None: + return [Observation(spec_id=sid, status="error", note=f"unknown adapter {kind!r}")] + getter = get or (lambda url: fetch_bytes(url, cache_dir=cache_dir)) + try: + return adapter(spec, getter) + except FetchError as exc: + return [Observation(spec_id=sid, status="error", note=str(exc)[:300])] + except (KeyError, ValueError, TypeError) as exc: + return [Observation(spec_id=sid, status="error", note=f"parse error: {exc!s}"[:300])] + + +def run_specs(specs: list[dict[str, Any]], **kw: Any) -> list[Observation]: + out: list[Observation] = [] + for spec in specs: + out.extend(run_spec(spec, **kw)) + return out diff --git a/web_fetcher/xlsx.py b/web_fetcher/xlsx.py new file mode 100644 index 0000000..b9965ab --- /dev/null +++ b/web_fetcher/xlsx.py @@ -0,0 +1,53 @@ +"""Minimal read-only XLSX reader (stdlib only): sheet -> list of rows of strings.""" + +from __future__ import annotations + +import io +import re +import zipfile +import xml.etree.ElementTree as ET + +_NS = {"m": "http://schemas.openxmlformats.org/spreadsheetml/2006/main", + "r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships"} +_T = "{%s}t" % _NS["m"] + + +def _col_index(ref: str) -> int: + letters = re.match(r"[A-Z]+", ref).group(0) + n = 0 + for ch in letters: + n = n * 26 + (ord(ch) - 64) + return n - 1 + + +def read_sheet(data: bytes, sheet: str | int = 0) -> list[list[str]]: + z = zipfile.ZipFile(io.BytesIO(data)) + shared: list[str] = [] + if "xl/sharedStrings.xml" in z.namelist(): + for si in ET.fromstring(z.read("xl/sharedStrings.xml")).findall("m:si", _NS): + shared.append("".join(t.text or "" for t in si.iter(_T))) + wb = ET.fromstring(z.read("xl/workbook.xml")) + sheets = [s for s in wb.find("m:sheets", _NS)] + if isinstance(sheet, str): + idx = next(i for i, s in enumerate(sheets) if s.get("name") == sheet) + else: + idx = sheet + rid = sheets[idx].get("{%s}id" % _NS["r"]) + rels = ET.fromstring(z.read("xl/_rels/workbook.xml.rels")) + target = next(r.get("Target") for r in rels if r.get("Id") == rid) + path = "xl/" + target.lstrip("/").removeprefix("xl/") + root = ET.fromstring(z.read(path)) + rows: list[list[str]] = [] + for r in root.find("m:sheetData", _NS).findall("m:row", _NS): + cells: dict[int, str] = {} + for c in r.findall("m:c", _NS): + v = c.find("m:v", _NS) + if v is None: + inline = c.find("m:is", _NS) + text = "".join(t.text or "" for t in inline.iter(_T)) if inline is not None else "" + else: + text = shared[int(v.text)] if c.get("t") == "s" else (v.text or "") + cells[_col_index(c.get("r"))] = text + width = max(cells) + 1 if cells else 0 + rows.append([cells.get(i, "") for i in range(width)]) + return rows