PR Checks / Backend Smoke (pull_request) Canceled after 0s
PR Checks / Build Backend (no push) (pull_request) Canceled after 0s
PR Checks / Build Frontend (no push) (pull_request) Canceled after 0s
PR Checks / Build Pipeline (no push) (pull_request) Canceled after 0s
PR Checks / AI Code Review (Claude) (pull_request) Canceled after 0s
PR Checks / Frontend Typecheck + Tests (pull_request) Canceled after 8m16s
get_supplementary_data ran ~5 sequential DB round-trips per URN, so /api/compare scaled at ~37ms/school (measured on staging: 1 school 155ms, 3 schools 220ms, 6 schools 340ms). get_supplementary_data_batch fetches each table once with WHERE urn IN (...) and groups in Python, collapsing 5*N round-trips to a constant 5. get_supplementary_data is now a thin wrapper so the detail endpoint is unchanged; the compare endpoint makes one batched call. Each table degrades independently on failure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
112 lines
4.0 KiB
Python
112 lines
4.0 KiB
Python
"""get_supplementary_data_batch fetches one query per table for all URNs
|
|
(not ~5 per school) and returns the same per-URN block shape as the
|
|
single-URN function, picking the latest row per URN where relevant."""
|
|
|
|
import types
|
|
|
|
from backend import data_loader
|
|
from backend.data_loader import get_supplementary_data_batch
|
|
|
|
|
|
class _FakeQuery:
|
|
"""Records that a query ran and serves canned rows filtered by an in-list."""
|
|
|
|
def __init__(self, recorder, model_name, rows):
|
|
self._rec = recorder
|
|
self._model = model_name
|
|
self._rows = rows
|
|
|
|
def filter(self, *args, **kwargs):
|
|
return self
|
|
|
|
def order_by(self, *args, **kwargs):
|
|
return self
|
|
|
|
def all(self):
|
|
self._rec.append(self._model)
|
|
return self._rows
|
|
|
|
def first(self):
|
|
self._rec.append(self._model)
|
|
return self._rows[0] if self._rows else None
|
|
|
|
|
|
class _FakeSession:
|
|
def __init__(self, rows_by_model):
|
|
self.rows_by_model = rows_by_model
|
|
self.queries: list[str] = []
|
|
|
|
def query(self, model):
|
|
name = model.__name__
|
|
return _FakeQuery(self.queries, name, self.rows_by_model.get(name, []))
|
|
|
|
def rollback(self):
|
|
pass
|
|
|
|
|
|
def _ofsted_row(urn, date, oe):
|
|
base = {f: None for f in (
|
|
"framework", "inspection_type", "quality_of_education", "behaviour_attitudes",
|
|
"personal_development", "leadership_management", "early_years_provision",
|
|
"sixth_form_provision", "ungraded_outcome", "ungraded_grade",
|
|
"rc_safeguarding_met", "rc_inclusion", "rc_curriculum_teaching", "rc_achievement",
|
|
"rc_attendance_behaviour", "rc_personal_development", "rc_leadership_governance",
|
|
"rc_early_years", "rc_sixth_form", "report_url",
|
|
)}
|
|
base.update(urn=urn, inspection_date=types.SimpleNamespace(isoformat=lambda: date),
|
|
overall_effectiveness=oe, grade_source=None)
|
|
return types.SimpleNamespace(**base)
|
|
|
|
|
|
def _adm_row(urn, year):
|
|
return types.SimpleNamespace(
|
|
urn=urn, year=year, school_phase="Primary", places_offered=100,
|
|
total_applications=200, first_preference_applications=150,
|
|
first_preference_offers=140, first_preference_offer_pct=93.3,
|
|
oversubscription_ratio=1.5, oversubscribed=True,
|
|
total_offers=100, second_preference_offers=5, third_preference_offers=2,
|
|
cross_la_applications=10, cross_la_offers=3,
|
|
)
|
|
|
|
|
|
def test_one_query_per_table_and_latest_row_per_urn():
|
|
rows = {
|
|
# URN 1 has two Ofsted rows; the batch must keep the most recent (2023).
|
|
"FactOfstedInspection": [
|
|
_ofsted_row(1, "2023-01-01", 2),
|
|
_ofsted_row(1, "2019-01-01", 3),
|
|
_ofsted_row(2, "2021-06-01", 1),
|
|
],
|
|
"FactAdmissions": [_adm_row(1, 202526), _adm_row(1, 202627), _adm_row(2, 202627)],
|
|
"FactPupilCharacteristics": [],
|
|
"FactDeprivation": [],
|
|
"FactFinance": [],
|
|
}
|
|
session = _FakeSession(rows)
|
|
out = get_supplementary_data_batch(session, [1, 2])
|
|
|
|
# Exactly one query per table — five total, regardless of two URNs.
|
|
assert sorted(session.queries) == [
|
|
"FactAdmissions", "FactDeprivation", "FactFinance",
|
|
"FactOfstedInspection", "FactPupilCharacteristics",
|
|
]
|
|
|
|
# Latest Ofsted kept per URN
|
|
assert out[1]["ofsted"]["overall_effectiveness"] == 2
|
|
assert out[2]["ofsted"]["overall_effectiveness"] == 1
|
|
|
|
# Admissions history grouped per URN, latest exposed as `admissions`
|
|
assert [r["year"] for r in out[1]["admissions_history"]] == [202526, 202627]
|
|
assert out[1]["admissions"]["year"] == 202627
|
|
assert out[2]["admissions_history"] == [{**out[2]["admissions_history"][0]}]
|
|
|
|
# Empty tables degrade to the null block, not a crash
|
|
assert out[1]["census"] is None and out[1]["deprivation"] is None
|
|
|
|
|
|
def test_single_wrapper_matches_batch(monkeypatch):
|
|
session = _FakeSession({"FactOfstedInspection": [_ofsted_row(5, "2022-01-01", 2)]})
|
|
single = data_loader.get_supplementary_data(session, 5)
|
|
assert single["ofsted"]["overall_effectiveness"] == 2
|
|
assert single["admissions_history"] == []
|