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:
74
tests/test_live_smoke.py
Normal file
74
tests/test_live_smoke.py
Normal file
@ -0,0 +1,74 @@
|
||||
"""Live smoke test — hits the real BLS API.
|
||||
|
||||
Opt-in only: deselected by default (see addopts in pyproject.toml). Run with:
|
||||
pytest -m live
|
||||
Requires network and a BLS API key (BLS_API_KEY env var, or config.py).
|
||||
|
||||
Guards against silent series-ID drift: BLS occasionally retires codes (e.g. the
|
||||
2010->2018 SOC change), which unit tests alone can't catch.
|
||||
"""
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from bls_client import BLSClient
|
||||
from bls_client.queries import employment as E, prices as P, wages as W, productivity as PR
|
||||
|
||||
|
||||
def _api_key():
|
||||
key = os.environ.get("BLS_API_KEY")
|
||||
if key:
|
||||
return key
|
||||
try:
|
||||
from config import BLS_API_KEY # local, gitignored
|
||||
if BLS_API_KEY and BLS_API_KEY != "YOUR_KEY_HERE":
|
||||
return BLS_API_KEY
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
pytestmark = pytest.mark.live
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def client():
|
||||
key = _api_key()
|
||||
if not key:
|
||||
pytest.skip("no BLS_API_KEY available")
|
||||
return BLSClient(key)
|
||||
|
||||
|
||||
# One representative, previously-broken helper per repaired survey.
|
||||
REPAIRED = {
|
||||
"JOLTS": E.jolts_dashboard(), # dict of 5
|
||||
"OES wage": W.occupation_annual_median_wage("151252"),
|
||||
"OES employ": W.all_occupations_employment(),
|
||||
"ECI": W.eci_total_compensation(),
|
||||
"Productivity": PR.business_output_per_hour(),
|
||||
"QCEW": E.qcew_national_private(),
|
||||
"PPI final": P.ppi_final_demand(),
|
||||
"ECEC": W.ecec_total_benefits(),
|
||||
}
|
||||
|
||||
|
||||
def _flatten(v):
|
||||
return list(v.values()) if isinstance(v, dict) else [v]
|
||||
|
||||
|
||||
def test_repaired_surveys_return_data(client):
|
||||
"""Every repaired survey must return at least one real observation."""
|
||||
all_ids = []
|
||||
for ids in REPAIRED.values():
|
||||
all_ids.extend(_flatten(ids))
|
||||
res = client.fetch(all_ids, 2024, 2025)
|
||||
missing = [sid for sid in all_ids if not res.get(sid, {}).get("data")]
|
||||
assert not missing, f"no data returned for: {missing}"
|
||||
|
||||
|
||||
def test_headline_payrolls_plausible(client):
|
||||
res = client.fetch_latest(E.nonfarm_payrolls(), years=1)
|
||||
obs = client.latest_obs(next(iter(res.values())))
|
||||
assert obs is not None
|
||||
# US nonfarm payrolls are ~150,000 (thousands); sanity bound, not exact.
|
||||
assert 100_000 < float(obs["value"]) < 200_000
|
||||
Reference in New Issue
Block a user