"""Generate GIAS code->name dictionaries from the live bulk CSV. Writes: - backend/gias_codes.py (canonical Python module) - pipeline/scripts/gias_codes.py (byte-identical copy) - pipeline/transform/seeds/gias_code_names.csv (dbt seed for drift test) Run from the repo root whenever the dbt drift test warns that DfE added/renamed a value: python pipeline/scripts/generate_gias_codes.py """ from __future__ import annotations import io import sys from datetime import date, timedelta from pathlib import Path import pandas as pd import requests GIAS_URL = ( "https://ea-edubase-api-prod.azurewebsites.net" "/edubase/downloads/public/edubasealldata{date}.csv" ) # (CSV code column, CSV name column, python dict name, seed field key) FIELDS = [ ("TypeOfEstablishment (code)", "TypeOfEstablishment (name)", "SCHOOL_TYPE", "school_type"), ("EstablishmentStatus (code)", "EstablishmentStatus (name)", "ESTABLISHMENT_STATUS", "establishment_status"), ("PhaseOfEducation (code)", "PhaseOfEducation (name)", "PHASE_OF_EDUCATION", "phase_of_education"), ("OfficialSixthForm (code)", "OfficialSixthForm (name)", "OFFICIAL_SIXTH_FORM", "official_sixth_form"), ("ReligiousCharacter (code)", "ReligiousCharacter (name)", "RELIGIOUS_CHARACTER", "religious_character"), ("AdmissionsPolicy (code)", "AdmissionsPolicy (name)", "ADMISSIONS_POLICY", "admissions_policy"), ] MODULE_HEADER = '''"""GIAS code -> name dictionaries. GENERATED by pipeline/scripts/generate_gias_codes.py from the GIAS bulk CSV — do not edit by hand; rerun the script when the dbt drift test warns. The canonical file is backend/gias_codes.py; pipeline/scripts/gias_codes.py must be byte-identical (enforced by backend/tests/test_gias_codes.py). """ from __future__ import annotations import logging import math logger = logging.getLogger(__name__) ''' MODULE_FOOTER = ''' def translate(code, mapping: dict[int, str]) -> str | None: """Translate a GIAS code to its display name. None/NaN -> None (column absent or suppressed). Unknown codes degrade to "Unknown ()" with a warning so a new DfE value never blanks the UI. """ if code is None or (isinstance(code, float) and math.isnan(code)): return None code = int(code) if code not in mapping: logger.warning("Unknown GIAS code %s (not in dictionary)", code) return f"Unknown ({code})" return mapping[code] ''' def download_csv() -> pd.DataFrame: for day in (date.today(), date.today() - timedelta(days=1)): url = GIAS_URL.format(date=day.strftime("%Y%m%d")) print(f"Downloading {url}") resp = requests.get(url, timeout=300) if resp.status_code == 404: continue resp.raise_for_status() return pd.read_csv( io.StringIO(resp.content.decode("latin-1")), dtype=str, keep_default_na=False, ) sys.exit("GIAS CSV not available for today or yesterday") def main() -> None: repo = Path(__file__).resolve().parents[2] df = download_csv() module_parts = [MODULE_HEADER] seed_rows: list[tuple[str, int, str]] = [] for code_col, name_col, dict_name, field_key in FIELDS: pairs = ( df[[code_col, name_col]] .loc[lambda d: d[code_col] != ""] .drop_duplicates() ) by_code: dict[int, set] = {} for c, n in pairs.itertuples(index=False): by_code.setdefault(int(c), set()).add(n) mapping = [] for code, names in sorted(by_code.items()): named = sorted(n for n in names if n != "") if len(named) > 1: sys.exit(f"{code_col}: code {code} maps to multiple names {named} — investigate before generating") # Codes that only ever appear with a blank (name) are GIAS # "not recorded" sentinels (e.g. ReligiousCharacter 99, # AdmissionsPolicy 9). Map them to "" so the API serves the same # empty string the old name pipeline did — the "Unknown ()" # path is reserved for genuinely new codes. mapping.append((code, named[0] if named else "")) lines = [f"{dict_name}: dict[int, str] = {{"] for code, name in mapping: escaped = name.replace('"', '\\"') lines.append(f' {code}: "{escaped}",') lines.append("}\n") module_parts.append("\n".join(lines)) seed_rows += [(field_key, code, name) for code, name in mapping] module = "\n".join(module_parts) + MODULE_FOOTER (repo / "backend" / "gias_codes.py").write_text(module) (repo / "pipeline" / "scripts" / "gias_codes.py").write_text(module) seed_path = repo / "pipeline" / "transform" / "seeds" / "gias_code_names.csv" with open(seed_path, "w", newline="") as fh: import csv w = csv.writer(fh) w.writerow(["field", "code", "name"]) w.writerows(seed_rows) print(f"Wrote backend/gias_codes.py, pipeline/scripts/gias_codes.py, {seed_path.name}") print("\nKey codes for the dbt work (Task 3):") for field in ("establishment_status", "phase_of_education", "official_sixth_form"): print(f" {field}:") for f, code, name in seed_rows: if f == field: print(f" {code} = {name}") if __name__ == "__main__": main()