Source document

pipeline_miami/export_metric_index.py

Served verbatim from the project repository. Internal working document conventions apply: documents may reference file paths, branch names, and findings-ledger anchors from the repo.

"""Export the committed metric index the Miami methodology pages read:
docs/cities/miami/sources.yaml (which files back which metric, with their
provenance) and the served coverage of every metric (years, cells,
suppression, subgroups, percentiles). Read-only.

Run:  python -m pipeline_miami.export_metric_index
Writes data/cities/miami/reference/metric_index.json (commit it).
"""
from __future__ import annotations

import datetime as dt
import json
import re

import yaml

from pipeline_miami.verify import common

OUT = common.REPO_ROOT / "data" / "cities" / "miami" / "reference" / "metric_index.json"
FINDINGS = common.REPO_ROOT / "pipeline_miami" / "findings"


def _pattern(spec: str) -> re.Pattern[str]:
    """"enrollment_oct_g{PK..12}" → enrollment_oct_g\w+; "{a,b}" → (?:a|b); "*" → \w*; plain keys match exactly."""
    out = []
    for part in re.split(r"(\{[^}]*\}|\*)", spec):
        if not part:
            continue
        if part == "*":
            out.append(r"\w*")
        elif part.startswith("{"):
            inner = part[1:-1]
            alts = [a.strip() for a in inner.split(",")] if "," in inner and ".." not in inner else None
            out.append("(?:" + "|".join(re.escape(a) for a in alts) + ")" if alts else r"\w+")
        else:
            out.append(re.escape(part))
    return re.compile("^" + "".join(out) + "$")


def sources_index() -> tuple[dict, dict]:
    reg = yaml.safe_load(open(common.SOURCES_YAML))
    sources: dict[str, dict] = {}
    backed: list[tuple[re.Pattern[str], str]] = []
    for e in reg:
        sid = e["source_id"]
        prov = None
        d = common.SOURCES_DIR / sid
        if d.exists():
            days = sorted([p for p in d.iterdir() if p.is_dir() and p.name[:4].isdigit()], reverse=True)
            for day in days:
                pj = day / "provenance.json"
                if pj.exists():
                    prov = json.load(open(pj))
                    break
        years = e.get("years")
        sources[sid] = {
            "publisher": e.get("publisher"), "url": e.get("url"), "format": e.get("format"), "role": e.get("role"),
            "year": str(e["year"]) if e.get("year") is not None else None, "years": [str(y) for y in years] if years else None,
            "fetched": str(e.get("fetched")) if e.get("fetched") else None, "metrics_backed": [str(m) for m in (e.get("metrics_backed") or [])],
            "notes": (str(e.get("notes")).strip() or None) if e.get("notes") else None,
            "sha256": prov.get("sha256") if prov else None, "bytes": prov.get("bytes") if prov else None,
            "fetched_from": prov.get("fetched_from") if prov else None, "retrieved_at": prov.get("retrieved_at") if prov else None,
            "findings_md": f"pipeline_miami/findings/{sid}.md" if (FINDINGS / f"{sid}.md").exists() else None,
        }
        for m in sources[sid]["metrics_backed"]:
            backed.append((_pattern(m), sid))
    return sources, {"patterns": backed}


def coverage() -> dict[str, dict]:
    import psycopg

    out: dict[str, dict] = {}
    with psycopg.connect(common.served_db_dsn()) as conn:
        conn.read_only = True
        with conn.cursor() as cur:
            cur.execute(
                """
                SELECT metric_key,
                       array_agg(DISTINCT year ORDER BY year),
                       count(*), count(*) FILTER (WHERE suppressed), count(DISTINCT school_number),
                       array_agg(DISTINCT subgroup::text ORDER BY subgroup::text),
                       count(*) FILTER (WHERE comparison_group_percentile IS NOT NULL),
                       min(year) FILTER (WHERE value IS NOT NULL), max(year) FILTER (WHERE value IS NOT NULL)
                  FROM miami_school_year_metrics GROUP BY metric_key
                """
            )
            for key, years, cells, sup, schools, subgroups, pct, y0, y1 in cur:
                out[key] = {"years": years, "cells": cells, "suppressed": sup, "schools": schools, "subgroups": subgroups,
                            "with_peer_percentile": pct, "first_year": y0, "last_year": y1}
    return out


def main() -> None:
    sources, backed = sources_index()
    cov = coverage()
    keys = sorted(cov)
    metric_sources = {k: sorted({sid for pat, sid in backed["patterns"] if pat.match(k)}) for k in keys}
    doc = {"generated_at": dt.datetime.now(dt.timezone.utc).isoformat(), "sources": sources, "metric_sources": metric_sources, "coverage": cov}
    OUT.parent.mkdir(parents=True, exist_ok=True)
    OUT.write_text(json.dumps(doc, indent=1, default=str))
    unbacked = [k for k in keys if not metric_sources[k]]
    print(f"[metric_index] {len(sources)} sources, {len(keys)} served metrics, {sum(1 for k in keys if metric_sources[k])} with a registered source; unbacked: {unbacked}")


if __name__ == "__main__":
    main()