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>
This commit is contained in:
@ -336,6 +336,55 @@ def eci(
|
||||
return f"CI{adj}{owner}{component}{'0'*9}{estimate}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ECEC — Employer Costs for Employee Compensation
|
||||
# Component ("estimate") codes from the BLS cm.estimate decode table.
|
||||
# ---------------------------------------------------------------------------
|
||||
_ECEC_COMPONENT = {
|
||||
"total_compensation": "01",
|
||||
"wages_and_salaries": "02",
|
||||
"total_benefits": "03",
|
||||
"paid_leave": "04",
|
||||
"supplemental_pay": "09",
|
||||
"insurance": "13",
|
||||
"health_insurance": "15",
|
||||
"retirement_and_savings": "18",
|
||||
"legally_required": "21",
|
||||
"workers_compensation": "27",
|
||||
}
|
||||
|
||||
_ECEC_OWNER = {
|
||||
"civilian": "1",
|
||||
"private": "2",
|
||||
"state_local_gov": "3",
|
||||
}
|
||||
|
||||
def ecec(
|
||||
component: str = "total_compensation",
|
||||
owner: str = "civilian",
|
||||
datatype: str = "D", # D = cost per hour worked (dollars)
|
||||
) -> str:
|
||||
"""
|
||||
ECEC series — employer cost per hour worked, all industries / all workers.
|
||||
|
||||
Format: CM + U + owner(1) + component(2) + 10 zeros + datatype(1) = 17 chars.
|
||||
|
||||
Args:
|
||||
component: key in _ECEC_COMPONENT (e.g. "health_insurance") or a raw 2-digit code
|
||||
owner: "civilian" | "private" | "state_local_gov" (or raw "1"/"2"/"3")
|
||||
datatype: "D" = cost per hour in dollars (default); "P" = percent of total comp
|
||||
|
||||
Examples:
|
||||
ecec() → "CMU1010000000000D" (civilian total compensation)
|
||||
ecec("health_insurance") → "CMU1150000000000D"
|
||||
ecec("retirement_and_savings") → "CMU1180000000000D"
|
||||
ecec("total_benefits", "private") → "CMU2030000000000D"
|
||||
"""
|
||||
own = _ECEC_OWNER.get(owner, owner)
|
||||
code = _ECEC_COMPONENT.get(component, component)
|
||||
return f"CMU{own}{code}{'0'*10}{datatype}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Productivity (Major Sector)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user