Files
bls-data/tests/test_query_helpers.py
Dave Boyd 9e2c55c568 Add QCEW CSV module, ECEC breakdown, response caching + typed errors
Item 1 — QCEW county/industry detail:
- new bls_client/qcew.py: thin client over the QCEW Open Data CSV service
  (area/industry/size slices; no API key, no quota), with filter_rows helper
- covers the county/industry detail the timeseries API can't reach

Item 2 — ECEC benefit breakdown:
- new series.ecec() builder from the authoritative cm.estimate decode table
- real helpers: wages, total benefits, paid leave, supplemental pay,
  health insurance, retirement & savings, legally required + ecec_dashboard()
- FIX: ecec_total_benefits was CMU1036... (education/health industries only);
  correct all-civilian total benefits is CMU1030000000000D

Item 3 — caching + typed errors:
- bls_client/cache.py FileCache (on-disk, TTL); BLSClient(cache=True),
  queries_used counter (cache hits don't spend quota)
- bls_client/errors.py: BLSError / BLSQuotaError / BLSRequestError; quota
  exhaustion now raises a clear BLSQuotaError instead of a generic RuntimeError

Tests: 119 offline (+24) incl. ECEC goldens, QCEW fixture parse, cache/quota
behavior; live smoke extended to ECEC + QCEW. Helper sweep: 96/96 live.
README: caching/QCEW usage, updated coverage + limitations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:38:12 -04:00

88 lines
3.5 KiB
Python

"""Unit tests for the pre-built query helpers (offline).
Asserts the high-value helpers emit the exact known-good IDs, and that every
zero-argument helper across all modules emits a structurally valid series ID
(correct survey prefix + length). No network.
"""
import inspect
import pytest
from bls_client.queries import employment as E, prices as P, wages as W, productivity as PR
GOLDEN = [
(E.nonfarm_payrolls, "CES0000000001"),
(E.qcew_national_private, "ENUUS00010510"),
(E.qcew_dc_private, "ENU1100010510"),
(P.ppi_final_demand, "WPUFD4"),
(P.ppi_finished_goods, "WPUFD4"), # back-compat alias
(W.eci_wages, "CIU1020000000000A"),
(W.eci_total_compensation, "CIU1010000000000A"),
(W.ecec_total_compensation, "CMU1010000000000D"),
(W.ecec_total_benefits, "CMU1030000000000D"),
(W.ecec_health_insurance, "CMU1150000000000D"),
(W.ecec_retirement, "CMU1180000000000D"),
(PR.business_output_per_hour, "PRS84006092"),
]
@pytest.mark.parametrize("fn,expected", GOLDEN, ids=[f.__name__ for f, _ in GOLDEN])
def test_helper_exact_id(fn, expected):
assert fn() == expected
def test_software_developers_uses_2018_soc():
# 15-1252 (2018 SOC); the old 15-1132 was retired and returns no data.
assert W.SOC_CODES["software_developers"] == "151252"
assert "151252" in W.occupation_annual_median_wage("151252")
# --- Structural sweep: every zero-arg helper -> well-formed ID -----------------
# Surveys with a single fixed-width series ID — these are the ones whose
# encodings broke before, so we enforce exact length. Other valid surveys
# (CPI, PPI, average prices, import/export, CPS) have variable-width IDs and
# are checked only for a well-formed prefix.
FIXED_LEN = {
"LA": 20, "CE": 13, "SM": 20, "OE": 25, "JT": 21,
"CI": 17, "CM": 17, "PR": 11, "EN": 13,
}
KNOWN_PREFIXES = set(FIXED_LEN) | {"CU", "CW", "SU", "WP", "LN", "LE", "AP", "EI"}
def _zero_arg_helpers():
out = []
for mod in (E, P, W, PR):
for name, fn in inspect.getmembers(mod, inspect.isfunction):
if fn.__module__ != mod.__name__:
continue
sig = inspect.signature(fn)
if any(p.default is inspect._empty for p in sig.parameters.values()):
continue
out.append((f"{mod.__name__.split('.')[-1]}.{name}", fn))
return out
def _ids_from(fn):
out = fn()
if isinstance(out, str):
return [out]
if isinstance(out, dict):
return [v for v in out.values() if isinstance(v, str)]
return []
@pytest.mark.parametrize("label,fn", _zero_arg_helpers(), ids=lambda v: v if isinstance(v, str) else "")
def test_helper_ids_well_formed(label, fn):
ids = _ids_from(fn)
assert ids, f"{label} produced no series IDs"
for sid in ids:
prefix = sid[:2]
assert sid.isalnum(), f"{label}: non-alphanumeric series ID {sid!r}"
assert prefix.isupper() and prefix.isalpha(), f"{label}: bad prefix in {sid!r}"
assert prefix in KNOWN_PREFIXES, f"{label}: unknown survey prefix in {sid!r}"
if prefix in FIXED_LEN:
assert len(sid) == FIXED_LEN[prefix], (
f"{label}: {sid!r} is {len(sid)} chars, expected {FIXED_LEN[prefix]} for {prefix}"
)