Files
stack/tests/lead/test_explore.py
kert ed2e21ce8d
All checks were successful
CI / lint (push) Successful in 43s
CI / notebooks-smoke (push) Successful in 1m57s
CI / lint (pull_request) Successful in 33s
CI / test (push) Successful in 3m37s
CI / notebooks-smoke (pull_request) Successful in 1m48s
Infra CI / notebooks (push) Successful in 53s
Infra CI / zotero (push) Successful in 18s
CI / test (pull_request) Successful in 3m21s
Infra CI / docs (push) Successful in 26s
Infra CI / api (push) Successful in 1m19s
Infra CI / bcda (push) Successful in 1m38s
Infra CI / llm (push) Successful in 53s
Infra CI / mc (push) Successful in 15s
Infra CI / zotero (pull_request) Successful in 18s
Infra CI / llm (pull_request) Successful in 21s
Infra CI / mc (pull_request) Successful in 16s
Infra CI / docs (pull_request) Successful in 16s
Infra CI / api (pull_request) Successful in 16s
Infra CI / notebooks (pull_request) Successful in 1m4s
Infra CI / bcda (pull_request) Successful in 17s
fix(lead): open the Zotero library on each labels run
The cached connection was created on one script thread and reused on the next, so the labels page died in project_keys. The file is mounted read-only, so the open is immutable and does not create a -shm beside it. The explorer pages are covered for the 99% gate.
2026-11-02 00:48:20 -05:00

531 lines
17 KiB
Python

"""The public-data explorer reads a mirror and does not write one."""
from __future__ import annotations
from pathlib import Path
import duckdb
import pytest
from lead.explore import catalog, guard, writeup
from lead.explore.catalog import HEAVY_ROWS, NeedsFilter, QueryTimeout, TableCard
from lead.load.frame import ensure_log, record
def _mirror(tmp_path: Path) -> duckdb.DuckDBPyConnection:
con = duckdb.connect(str(tmp_path / "public.duckdb"))
ensure_log(con)
record(
con,
source_id="cms_demo",
release="2024",
sha256="a" * 64,
path="/data/cms_demo.csv",
table="lead_public.cms_demo",
status="loaded",
rows=2,
reason="",
actor="stack",
)
record(
con,
source_id="qcew",
release="2020",
sha256="b" * 64,
path="/data/2020_qcew.zip",
table="bls.qcew",
status="loaded",
rows=HEAVY_ROWS,
reason="",
actor="stack",
)
record(
con,
source_id="bls_ces",
release="ces.csv",
sha256="c" * 64,
path="/data/ces.csv",
table="bls.ces",
status="loaded",
rows=2,
reason="",
actor="stack",
)
record(
con,
source_id="fred",
release="",
sha256="",
path="/data/fred.csv",
table="",
status="absent",
rows=0,
reason="unreachable",
actor="stack",
)
record(
con,
source_id="cms_note",
release="note",
sha256="d" * 64,
path="/data/note.txt",
table="lead_public.cms_note",
status="loaded",
rows=99,
reason="",
actor="stack",
)
record(
con,
source_id="cms_note",
release="note",
sha256="d" * 64,
path="/data/note.txt",
table="lead_public.cms_note",
status="skipped",
rows=0,
reason="kind:note",
actor="stack",
)
con.execute("CREATE SCHEMA IF NOT EXISTS bls")
con.execute(
"""
CREATE TABLE lead_public.cms_demo (
geo_fips VARCHAR, spine_year INTEGER, agi VARCHAR, geo_zip VARCHAR
)
"""
)
con.execute(
"""
INSERT INTO lead_public.cms_demo VALUES
('01001', 2010, '5', NULL),
('01003', 2011, '9', '30301')
"""
)
con.execute(
"""
CREATE TABLE bls.qcew (
geo_fips VARCHAR, spine_year INTEGER, series_id VARCHAR, month3_emplvl VARCHAR
)
"""
)
con.execute(
"""
INSERT INTO bls.qcew VALUES
('01001', 2020, 'X', '10'),
('01001', 2021, 'X', '11'),
('01003', 2020, 'Y', '9')
"""
)
con.execute(
"""
CREATE TABLE bls.ces (
series_id VARCHAR, year INTEGER, month INTEGER, value DOUBLE
)
"""
)
con.execute(
"""
INSERT INTO bls.ces VALUES
('CES1', 2020, 1, 10),
('CES1', 2022, 1, 14)
"""
)
return con
def _cards(con):
return catalog.tables(con)
def _card(con, name):
return catalog.card_for(_cards(con), name)
def test_latest_file_wins_and_its_rows_are_not_double_counted(tmp_path):
con = _mirror(tmp_path)
files = catalog.latest_files(con)
note = next(row for row in files if row.source_id == "cms_note")
assert note.status == "skipped"
assert note.row_count == 0
loaded = [row for row in files if row.status == "loaded"]
assert {row.source_id for row in loaded} == {"cms_demo", "qcew", "bls_ces"}
assert sum(row.row_count for row in loaded) == 2 + HEAVY_ROWS + 2
def test_a_heavy_table_is_not_aggregated_without_a_year(tmp_path):
con = _mirror(tmp_path)
qcew = _card(con, "bls.qcew")
assert qcew.heavy
with pytest.raises(NeedsFilter):
catalog.count_rows(con, qcew, geo=("geo_fips", "01001"))
with pytest.raises(NeedsFilter):
catalog.sample(con, qcew, geo=("geo_fips", "01001"))
with pytest.raises(NeedsFilter):
catalog.profile(con, qcew)
got = catalog.sample(con, qcew, geo=("geo_fips", "01001"), year=2020)
assert [row["month3_emplvl"] for row in got] == ["10"]
assert catalog.count_rows(con, qcew, geo=("geo_fips", "01001"), year=2020) == 1
def test_a_quoted_value_is_a_parameter(tmp_path):
con = _mirror(tmp_path)
demo = _card(con, "lead_public.cms_demo")
got = catalog.sample(con, demo, geo=("geo_fips", "01001' OR '1'='1"))
assert got == []
plain = catalog.sample(con, demo, geo=("geo_fips", "01001"))
assert [row["agi"] for row in plain] == ["5"]
def test_an_unfiltered_sample_of_a_heavy_table_is_capped(tmp_path):
con = _mirror(tmp_path)
got = catalog.sample(con, _card(con, "bls.qcew"), limit=2)
assert len(got) == 2
def test_series_span_is_last_minus_first_in_year_order(tmp_path):
con = _mirror(tmp_path)
ces = _card(con, "bls.ces")
points = catalog.series_points(
con, ces, series_column="series_id", series_id="CES1", value_column="value"
)
got = catalog.span(points, year_key="year", value_key="value")
assert (got.first_year, got.last_year, got.change) == (2020, 2022, 4)
def test_place_summary_skips_a_heavy_scan_until_a_year_is_set(tmp_path):
con = _mirror(tmp_path)
early = catalog.place_summary(
con, _cards(con), geo_column="geo_fips", geo_value="01001"
)
by_table = {row["table"]: row for row in early}
assert by_table["bls.qcew"]["rows"] is None
assert by_table["bls.qcew"]["note"] == "needs a year"
assert by_table["lead_public.cms_demo"]["rows"] == 1
later = catalog.place_summary(
con, _cards(con), geo_column="geo_fips", geo_value="01001", year=2020
)
assert {row["table"]: row["rows"] for row in later}["bls.qcew"] == 1
def test_sql_is_one_capped_read():
wrapped = guard.prepare("SELECT * FROM bls.qcew")
assert wrapped.endswith("LIMIT 2000")
assert "AS _lead_q" in wrapped
assert (
guard.prepare("DESCRIBE lead_public.load_log")
== "DESCRIBE lead_public.load_log"
)
with pytest.raises(guard.SqlRejected):
guard.prepare("SELECT 1; SELECT 2")
with pytest.raises(guard.SqlRejected):
guard.prepare("DROP TABLE bls.qcew")
with pytest.raises(guard.SqlRejected):
guard.prepare("EXPLAIN ANALYZE SELECT 1")
def test_a_heavy_table_without_where_is_refused(tmp_path):
con = _mirror(tmp_path)
cards = _cards(con)
with pytest.raises(NeedsFilter):
catalog.run_select(con, "SELECT * FROM bls.qcew", cards)
headers, rows = catalog.run_select(
con, "SELECT geo_fips FROM bls.qcew WHERE spine_year = 2020", cards, cap=10
)
assert headers == ["geo_fips"]
assert len(rows) == 2
def test_a_long_scan_is_interrupted():
con = duckdb.connect()
with pytest.raises(QueryTimeout):
catalog.bounded_execute(
con, "SELECT count(*) FROM range(1000000000000)", seconds=0.3
)
def test_open_mirror_does_not_write(tmp_path):
con = _mirror(tmp_path)
path = tmp_path / "public.duckdb"
con.close()
ro = catalog.open_mirror(path)
with pytest.raises(duckdb.Error, match="(?i)read-only"):
ro.execute("CREATE TABLE lead_public.nope (a VARCHAR)")
ro.close()
def test_the_writeup_counts_the_log_and_states_the_contract(tmp_path):
con = _mirror(tmp_path)
sections = writeup.build(con)
text = "\n".join(
part for section in sections for part in (section.heading, *section.body)
)
headings = [section.heading for section in sections]
assert headings == [
"Registration",
"The mirror",
"What the load recorded",
"Largest tables",
"Families",
"How a row is keyed",
"Geography and time",
"Gaps",
"What this file is not",
"Exploring it",
]
assert "10.17605/OSF.IO/8WQ3M" in text
assert "https://osf.io/8wq3m/" in text
assert "local DuckDB mirror" in text
assert "read-only" in text
assert "does not download" in text
assert "Unity Catalog" in text
assert "evidence ledger" in text
assert "geo_zip" in text and "geo_zcta" in text
assert "000" in text
assert "time_key" in text
assert "month 13" in text
assert "beneficiary" in text
assert "FRED is absent" in text
assert "econ.fred" in text
recorded = next(
section for section in sections if section.heading == "What the load recorded"
)
assert "3 files loaded" in recorded.body[0]
assert f"{2 + HEAVY_ROWS + 2:,} rows recorded" in recorded.body[0]
assert ("loaded", 3) in recorded.rows
assert ("absent", 1) in recorded.rows
assert ("skipped", 1) in recorded.rows
families = next(section for section in sections if section.heading == "Families")
by_name = {row[0]: row for row in families.rows}
assert by_name["Bureau of Labor Statistics"][1] == 2
assert "QCEW" in by_name["Bureau of Labor Statistics"][4]
assert by_name["CMS"][1] == 1
largest = next(
section for section in sections if section.heading == "Largest tables"
)
assert largest.rows[0][0] == "bls.qcew"
assert largest.rows[0][3] == "yes"
gaps = next(section for section in sections if section.heading == "Gaps")
assert ("fred", "fred.csv", "unreachable") in gaps.rows
spine = next(
section for section in sections if section.heading == "How a row is keyed"
)
fips = next(row for row in spine.rows if row[0] == "geo_fips")
assert "000" in fips[1]
def test_writeup_page_renders_the_recorded_count(tmp_path, monkeypatch):
con = _mirror(tmp_path)
path = str(tmp_path / "public.duckdb")
con.close()
monkeypatch.setenv("LEAD_PUBLIC_DB", path)
from streamlit.testing.v1 import AppTest
at = AppTest.from_string(
"from lead.explore.views import writeup\nwriteup()\n",
default_timeout=60,
)
at.run()
assert not at.exception
text = "\n".join(item.value for item in at.markdown)
assert "3 files loaded" in text
assert "FRED is absent" in text
def test_explore_command_points_streamlit_at_the_mirror(tmp_path, monkeypatch):
db = tmp_path / "public.duckdb"
db.write_bytes(b"")
seen: dict = {}
def fake_call(cmd, env=None, **_kwargs):
seen["cmd"] = cmd
seen["env"] = env
return 0
monkeypatch.setattr("subprocess.call", fake_call)
from typer.testing import CliRunner
from cli.lead import app
result = CliRunner().invoke(
app, ["explore", "--db", str(db), "--port", "8765", "--no-headless"]
)
assert result.exit_code == 0
assert "http://localhost:8765" in result.output
assert seen["env"]["LEAD_PUBLIC_DB"] == str(db)
cmd = seen["cmd"]
assert cmd[1:3] == ["-m", "lead.explore"]
assert cmd[cmd.index("--server.port") + 1] == "8765"
assert cmd[cmd.index("--server.headless") + 1] == "false"
def test_a_missing_mirror_and_a_bad_identifier_are_refused(tmp_path):
with pytest.raises(FileNotFoundError):
catalog.open_mirror(tmp_path / "missing.duckdb")
with pytest.raises(ValueError):
catalog.quote("1nope")
assert catalog.quote("geo_fips") == '"geo_fips"'
bare = duckdb.connect()
assert catalog.latest_files(bare) == ()
with pytest.raises(KeyError):
catalog.card_for((), "lead_public.missing")
def test_filters_series_and_a_profile_stay_inside_the_cap(tmp_path):
con = _mirror(tmp_path)
ces = _card(con, "bls.ces")
qcew = _card(con, "bls.qcew")
demo = _card(con, "lead_public.cms_demo")
assert ces.year_column() == "year"
assert "value" in ces.measure_columns()
plain = TableCard("lead_public", "note", (("body", "VARCHAR"),), 1, 1, ())
assert plain.year_column() == ""
measured = TableCard(
"lead_public",
"note",
(("amount", "DECIMAL(10,2)"), ("year", "INTEGER"), ("spine_month", "INTEGER")),
1,
1,
(),
)
assert measured.measure_columns() == ("amount",)
got = catalog.sample(con, ces, series=("series_id", "CES1"))
assert {row["year"] for row in got} == {2020, 2022}
with pytest.raises(ValueError, match="nope"):
catalog.sample(con, ces, series=("nope", "CES1"))
with pytest.raises(ValueError, match="geo_hsa"):
catalog.sample(con, ces, geo=("geo_hsa", "1"))
with pytest.raises(ValueError, match="limit"):
catalog.sample(con, ces, limit=0)
with pytest.raises(ValueError, match="limit"):
catalog.sample(con, ces, limit=catalog.SAMPLE_CAP + 1)
assert catalog.series_ids(con, ces) == ("CES1",)
with pytest.raises(NeedsFilter):
catalog.series_ids(con, qcew)
with pytest.raises(ValueError, match="nope"):
catalog.series_ids(con, ces, column="nope")
with pytest.raises(ValueError, match="column"):
catalog.series_points(
con, ces, series_column="nope", series_id="CES1", value_column="value"
)
with pytest.raises(NeedsFilter):
catalog.series_points(
con, ces, series_column="series_id", series_id="", value_column="value"
)
empty = catalog.span(
[{"year": None, "value": 1}, {"year": "x", "value": "no"}],
year_key="year",
value_key="value",
)
assert empty.points == 0
one = catalog.span([{"year": 2020, "value": 3}], year_key="year", value_key="value")
assert one.change is None
profiled = catalog.profile(con, demo)
assert profiled["rows"] == 2
assert profiled["year_column"] == "spine_year"
assert profiled["geo_fips"] == 2
con.execute("CREATE TABLE lead_public.note (body VARCHAR)")
with pytest.raises(NeedsFilter):
catalog.profile(con, _card(con, "lead_public.note"))
con.execute("INSERT INTO lead_public.note VALUES ('a')")
record(
con,
source_id="cms_note_body",
release="1",
sha256="e" * 64,
path="/data/note-body.csv",
table="lead_public.note",
status="loaded",
rows=1,
reason="",
actor="stack",
)
note = _card(con, "lead_public.note")
assert catalog.profile(con, note)["rows"] == 1
with pytest.raises(ValueError, match="year"):
catalog.profile(con, note, year=2020)
with pytest.raises(ValueError, match="year"):
catalog.sample(con, note, year=2020)
con.execute("CREATE TABLE lead_public.county (geo_fips VARCHAR)")
con.execute("INSERT INTO lead_public.county VALUES ('01001')")
looked = catalog.place_summary(
con,
(_card(con, "lead_public.county"), demo),
geo_column="geo_fips",
geo_value="01001",
year=2010,
)
by_table = {row["table"]: row for row in looked}
assert by_table["lead_public.county"]["note"] == "no year column"
assert by_table["lead_public.county"]["rows"] == 1
assert by_table["lead_public.cms_demo"]["rows"] == 1
def test_a_timed_out_place_is_recorded_and_the_guard_rejects_the_rest(
tmp_path, monkeypatch
):
con = _mirror(tmp_path)
def _timeout(*_args, **_kwargs):
raise QueryTimeout(1)
monkeypatch.setattr(catalog, "count_rows", _timeout)
rows = catalog.place_summary(
con, _cards(con), geo_column="geo_fips", geo_value="01001", year=2020
)
assert {row["note"] for row in rows} == {"timed out"}
with pytest.raises(ValueError):
catalog.place_summary(con, (), geo_column="zip", geo_value="1")
with pytest.raises(guard.SqlRejected, match="empty"):
guard.prepare(" ")
with pytest.raises(guard.SqlRejected, match="start with"):
guard.prepare("hello")
with pytest.raises(guard.SqlRejected, match="cap"):
guard.prepare("SELECT 1", cap=0)
def test_the_writeup_names_every_family_and_an_empty_file():
assert writeup.family_of("irs_soi") == "irs"
assert writeup.family_of("census_acs") == "census"
assert writeup.family_of("cdc_places") == "cdc"
assert writeup.family_of("ahrq_ccsr") == "ahrq"
assert writeup.family_of("nlm_rxnorm") == "nlm"
assert writeup.family_of("hrsa_hpsa") == "hrsa"
assert writeup.family_of("ecqi_measures") == "ecqi"
assert writeup.family_of("ma_penetration") == "ma"
assert writeup.family_of("ma") == "ma"
assert writeup.family_of("fred") == "fred"
assert writeup.family_of("noaa") == "other"
recorded = writeup._recorded({"loaded": 1, "partial": 2}, 5, 1)
assert ("partial", 2) in recorded.rows
bare = duckdb.connect()
mirror = next(
section for section in writeup.build(bare) if section.heading == "The mirror"
)
assert "no load log and no observation tables" in mirror.body[1]
bare.execute("CREATE TABLE notes (body VARCHAR)")
mirror = next(
section for section in writeup.build(bare) if section.heading == "The mirror"
)
assert "no load log" in mirror.body[1]
present = writeup._gaps(
(),
(TableCard("econ", "fred", (), 1, 1, ()),),
)
assert present.body[0] == "`econ.fred` is present in this file."
other = writeup._gaps(
(
catalog.FileRow(
"fred", "fred.csv", "/data/fred.csv", "", "skipped", 0, "later"
),
),
(),
)
assert "skipped" in other.body[0]
assert "reason later" in other.body[0]
none = writeup._gaps((), ())
assert none.body[0] == (
"The load log has no FRED row, and `econ.fred` is not in this file."
)
assert "No file's latest status is skipped." in none.body