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:
83
tests/test_client_cache.py
Normal file
83
tests/test_client_cache.py
Normal file
@ -0,0 +1,83 @@
|
||||
"""Offline tests for caching, the query counter, and typed errors.
|
||||
|
||||
A fake transport stands in for requests.post so nothing touches the network.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
import bls_client.client as client_mod
|
||||
from bls_client import BLSClient, BLSQuotaError, BLSRequestError
|
||||
from bls_client.cache import FileCache
|
||||
|
||||
|
||||
class FakeResp:
|
||||
def __init__(self, payload):
|
||||
self._payload = payload
|
||||
|
||||
def raise_for_status(self):
|
||||
pass
|
||||
|
||||
def json(self):
|
||||
return self._payload
|
||||
|
||||
|
||||
def _ok(series_id):
|
||||
return {
|
||||
"status": "REQUEST_SUCCEEDED",
|
||||
"Results": {"series": [{"seriesID": series_id, "data": [{"year": "2024", "period": "M01", "value": "1.0"}]}]},
|
||||
}
|
||||
|
||||
|
||||
def test_query_counter_and_cache(tmp_path, monkeypatch):
|
||||
calls = {"n": 0}
|
||||
|
||||
def fake_post(url, json=None, timeout=None):
|
||||
calls["n"] += 1
|
||||
return FakeResp(_ok(json["seriesid"][0]))
|
||||
|
||||
monkeypatch.setattr(client_mod.requests, "post", fake_post)
|
||||
|
||||
c = BLSClient("key", cache=FileCache(cache_dir=tmp_path, ttl=86400))
|
||||
c.fetch(["CES0000000001"], 2024, 2024)
|
||||
assert c.queries_used == 1 and calls["n"] == 1
|
||||
|
||||
# identical request -> served from cache, no new network call
|
||||
c.fetch(["CES0000000001"], 2024, 2024)
|
||||
assert c.queries_used == 1 and calls["n"] == 1
|
||||
|
||||
# different request -> one more call
|
||||
c.fetch(["CES0000000001"], 2023, 2024)
|
||||
assert c.queries_used == 2 and calls["n"] == 2
|
||||
|
||||
|
||||
def test_no_cache_by_default(monkeypatch):
|
||||
def fake_post(url, json=None, timeout=None):
|
||||
return FakeResp(_ok(json["seriesid"][0]))
|
||||
|
||||
monkeypatch.setattr(client_mod.requests, "post", fake_post)
|
||||
c = BLSClient("key")
|
||||
assert c.cache is None
|
||||
c.fetch(["CES0000000001"], 2024, 2024)
|
||||
c.fetch(["CES0000000001"], 2024, 2024)
|
||||
assert c.queries_used == 2 # no caching -> both hit the (fake) network
|
||||
|
||||
|
||||
def test_quota_error(monkeypatch):
|
||||
def fake_post(url, json=None, timeout=None):
|
||||
return FakeResp({
|
||||
"status": "REQUEST_NOT_PROCESSED",
|
||||
"message": ["Query exceeds the threshold of 500 queries per day."],
|
||||
})
|
||||
|
||||
monkeypatch.setattr(client_mod.requests, "post", fake_post)
|
||||
with pytest.raises(BLSQuotaError) as exc:
|
||||
BLSClient("key").fetch(["CES0000000001"], 2024, 2024)
|
||||
assert exc.value.status == "REQUEST_NOT_PROCESSED"
|
||||
|
||||
|
||||
def test_request_error(monkeypatch):
|
||||
def fake_post(url, json=None, timeout=None):
|
||||
return FakeResp({"status": "REQUEST_FAILED", "message": ["invalid series"]})
|
||||
|
||||
monkeypatch.setattr(client_mod.requests, "post", fake_post)
|
||||
with pytest.raises(BLSRequestError):
|
||||
BLSClient("key").fetch(["BOGUS"], 2024, 2024)
|
||||
Reference in New Issue
Block a user