Files
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

160 lines
5.9 KiB
Python

"""
Pre-built wage and compensation series IDs — OES, ECI, ECEC, CPS earnings.
"""
from ..series import oes_national, eci, ecec
# ---------------------------------------------------------------------------
# OES — Occupational Employment & Wage Statistics
# ---------------------------------------------------------------------------
def all_occupations_employment() -> str:
"""National employment across all occupations, all industries."""
return oes_national("000000", "000000", "employment")
def occupation_annual_median_wage(soc_code: str) -> str:
"""
Annual median wage for a specific occupation (national, all industries).
Args:
soc_code: 6-digit SOC code (e.g. "151252" for software developers,
"291141" for registered nurses, "119021" for construction mgrs)
"""
return oes_national(soc_code, "000000", "annual_median")
def occupation_employment(soc_code: str) -> str:
"""Employment level for a specific occupation (national)."""
return oes_national(soc_code, "000000", "employment")
# Common occupation codes
SOC_CODES = {
"software_developers": "151252",
"registered_nurses": "291141",
"teachers_elementary": "252021",
"accountants": "132011",
"construction_managers": "119021",
"truck_drivers": "533032",
"janitors": "372011",
"retail_salespersons": "412031",
"first_line_supervisors_mfg":"511011",
"lawyers": "231011",
"physicians": "291229",
"police_officers": "333051",
"social_workers": "211029",
"financial_analysts": "132051",
"data_scientists": "152051",
}
# ---------------------------------------------------------------------------
# ECI — Employment Cost Index
# ---------------------------------------------------------------------------
def eci_total_compensation(seasonal: bool = False) -> str:
"""ECI — civilian workers, all industries, total compensation (12-mo % change)."""
return eci(owner="10", component="10", seasonal=seasonal)
def eci_wages(seasonal: bool = False) -> str:
"""ECI — civilian workers, wages and salaries only (12-mo % change)."""
return eci(owner="10", component="20", seasonal=seasonal)
def eci_benefits(seasonal: bool = False) -> str:
"""ECI — civilian workers, benefit costs only (12-mo % change)."""
return eci(owner="10", component="30", seasonal=seasonal)
def eci_private(seasonal: bool = False) -> str:
"""ECI — private sector, total compensation (12-mo % change)."""
return eci(owner="20", component="10", seasonal=seasonal)
def eci_state_local(seasonal: bool = False) -> str:
"""ECI — state and local government, total compensation (12-mo % change)."""
return eci(owner="30", component="10", seasonal=seasonal)
def eci_dashboard() -> dict:
return {
"Total Compensation (Civilian)": eci_total_compensation(),
"Wages & Salaries (Civilian)": eci_wages(),
"Benefits (Civilian)": eci_benefits(),
"Total Comp (Private)": eci_private(),
"Total Comp (State/Local Gov)": eci_state_local(),
}
# ---------------------------------------------------------------------------
# ECEC — Employer Costs for Employee Compensation
# ---------------------------------------------------------------------------
def ecec_total_compensation(owner: str = "civilian") -> str:
"""ECEC — total compensation cost per hour worked."""
return ecec("total_compensation", owner)
def ecec_wages_and_salaries(owner: str = "civilian") -> str:
"""ECEC — wages and salaries cost per hour worked."""
return ecec("wages_and_salaries", owner)
def ecec_total_benefits(owner: str = "civilian") -> str:
"""ECEC — total benefits cost per hour worked."""
return ecec("total_benefits", owner)
def ecec_paid_leave(owner: str = "civilian") -> str:
"""ECEC — paid leave cost per hour worked (vacation, holiday, sick, personal)."""
return ecec("paid_leave", owner)
def ecec_supplemental_pay(owner: str = "civilian") -> str:
"""ECEC — supplemental pay cost per hour worked (overtime, bonuses, shift diff)."""
return ecec("supplemental_pay", owner)
def ecec_health_insurance(owner: str = "civilian") -> str:
"""ECEC — health insurance cost per hour worked."""
return ecec("health_insurance", owner)
def ecec_retirement(owner: str = "civilian") -> str:
"""ECEC — retirement & savings cost per hour worked (defined benefit + contribution)."""
return ecec("retirement_and_savings", owner)
def ecec_legally_required(owner: str = "civilian") -> str:
"""ECEC — legally required benefits per hour (Social Security, Medicare, UI, workers' comp)."""
return ecec("legally_required", owner)
def ecec_dashboard(owner: str = "civilian") -> dict:
"""The compensation cost breakdown — what an hour of labor costs an employer."""
return {
"Total Compensation": ecec_total_compensation(owner),
"Wages & Salaries": ecec_wages_and_salaries(owner),
"Total Benefits": ecec_total_benefits(owner),
"Paid Leave": ecec_paid_leave(owner),
"Supplemental Pay": ecec_supplemental_pay(owner),
"Health Insurance": ecec_health_insurance(owner),
"Retirement & Savings": ecec_retirement(owner),
"Legally Required": ecec_legally_required(owner),
}
# ---------------------------------------------------------------------------
# CPS Earnings (LE)
# ---------------------------------------------------------------------------
def median_weekly_earnings(seasonal: bool = True) -> str:
"""Median usual weekly earnings — full-time wage and salary workers."""
return "LEU0252881600" if seasonal else "LEU0252881500"
def median_weekly_earnings_men() -> str:
return "LEU0252882800"
def median_weekly_earnings_women() -> str:
return "LEU0252882900"