Add pyproject.toml packaging and pytest suite
- pyproject.toml: setuptools build, deps (requests), dev extra (pytest), pytest config with opt-in `live` marker - tests/: 95 offline tests locking every series-ID encoding against known-good IDs (regression guard for the JOLTS/OES/ECI/productivity/QCEW fixes), plus a live API smoke test behind `-m live` - README: install-as-package + test instructions; drop the now-resolved "no packaging"/"no tests" limitations - gitignore build/test artifacts Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
88
tests/test_series_builders.py
Normal file
88
tests/test_series_builders.py
Normal file
@ -0,0 +1,88 @@
|
||||
"""Unit tests for the low-level series-ID builders in bls_client.series.
|
||||
|
||||
These lock the exact encodings against known-good BLS series IDs. They run
|
||||
offline (no network) and are the regression guard for the encoding bugs that
|
||||
were fixed in June 2026 (JOLTS width, OES area code, ECI component/estimate,
|
||||
productivity sector/measure widths, QCEW form).
|
||||
"""
|
||||
import pytest
|
||||
|
||||
from bls_client import series as S
|
||||
|
||||
# (callable, kwargs) -> exact expected series ID. Every value here has been
|
||||
# confirmed to return data from the live BLS API.
|
||||
GOLDEN = [
|
||||
# LAUS — 20 chars
|
||||
(S.laus_state, dict(state_fips=11, measure="rate"), "LAUST110000000000003"),
|
||||
(S.laus_state, dict(state_fips=11, measure="rate", seasonal=True), "LASST110000000000003"),
|
||||
(S.laus_county, dict(state_fips=11, county_fips=1, measure="rate"), "LAUCN110010000000003"),
|
||||
(S.laus_msa, dict(state_fips=11, cbsa_code=47900, measure="rate"),"LAUMT114790000000003"),
|
||||
# CES national / state
|
||||
(S.ces_national, dict(), "CES0000000001"),
|
||||
(S.ces_national, dict(industry="manufacturing"), "CES3000000001"),
|
||||
(S.ces_state, dict(state_fips=11), "SMS11000000000000001"),
|
||||
# CPI / PPI
|
||||
(S.cpi, dict(), "CUUR0000SA0"),
|
||||
(S.cpi, dict(item="core", seasonal=True), "CUSR0000SA0L1E"),
|
||||
(S.ppi_commodity,dict(), "WPU00000000"),
|
||||
# OES — 25 chars, national area 0000000 (was the 0000400 bug)
|
||||
(S.oes_national, dict(), "OEUN000000000000000000001"),
|
||||
(S.oes_national, dict(occupation_code="151252", data_type="annual_median"),
|
||||
"OEUN000000000000015125213"),
|
||||
# JOLTS — 21 chars / 15 zeros (was 18 chars / 12 zeros)
|
||||
(S.jolts, dict(), "JTS000000000000000JOL"),
|
||||
(S.jolts, dict(element="quits", rate_level="R"), "JTS000000000000000QUR"),
|
||||
(S.jolts, dict(element="hires", seasonal=False), "JTU000000000000000HIL"),
|
||||
# ECI — 17 chars, unadjusted, component 10/20/30, estimate A
|
||||
(S.eci, dict(), "CIU1010000000000A"),
|
||||
(S.eci, dict(component="20"), "CIU1020000000000A"),
|
||||
(S.eci, dict(component="30"), "CIU1030000000000A"),
|
||||
(S.eci, dict(owner="20"), "CIU2010000000000A"),
|
||||
# Productivity — 11 chars, 4-digit sector + 4-digit measure
|
||||
(S.productivity, dict(), "PRS85006092"),
|
||||
(S.productivity, dict(sector="manufacturing", measure="unit_labor_cost"), "PRS30006112"),
|
||||
(S.productivity, dict(sector="business"), "PRS84006092"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fn,kwargs,expected", GOLDEN, ids=[f"{f.__name__}-{e}" for f, _, e in GOLDEN])
|
||||
def test_builder_exact_id(fn, kwargs, expected):
|
||||
assert fn(**kwargs) == expected
|
||||
|
||||
|
||||
# --- Format invariants (catch width regressions even for un-goldened inputs) ---
|
||||
|
||||
def test_jolts_is_21_chars():
|
||||
for elem in ("job_openings", "hires", "quits", "layoffs", "total_separations"):
|
||||
sid = S.jolts(elem)
|
||||
assert len(sid) == 21, f"{sid} should be 21 chars"
|
||||
assert sid.startswith("JTS")
|
||||
|
||||
|
||||
def test_oes_is_25_chars_national():
|
||||
sid = S.oes_national()
|
||||
assert len(sid) == 25
|
||||
assert sid.startswith("OEUN0000000"), "national area code must be 0000000"
|
||||
|
||||
|
||||
def test_eci_is_17_chars_and_unadjusted_by_default():
|
||||
sid = S.eci()
|
||||
assert len(sid) == 17
|
||||
assert sid.startswith("CIU"), "12-month % change ECI is published unadjusted (CIU)"
|
||||
|
||||
|
||||
def test_productivity_is_11_chars():
|
||||
sid = S.productivity()
|
||||
assert len(sid) == 11
|
||||
assert sid.startswith("PRS")
|
||||
|
||||
|
||||
def test_laus_state_is_20_chars():
|
||||
assert len(S.laus_state(48, "rate")) == 20
|
||||
|
||||
|
||||
def test_seasonal_flag_flips_adjustment_char():
|
||||
assert S.cpi(seasonal=False).startswith("CUU")
|
||||
assert S.cpi(seasonal=True).startswith("CUS")
|
||||
assert S.jolts(seasonal=False).startswith("JTU")
|
||||
assert S.jolts(seasonal=True).startswith("JTS")
|
||||
Reference in New Issue
Block a user