""" DC/MD/VA Unemployment — Static HTML Report Generator Run: python3 generate_report.py Output: dc_md_va_unemployment_report.html (open in any browser) """ import requests import json import os from datetime import datetime from config import BLS_API_KEY, BLS_API_BASE SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) # --------------------------------------------------------------------------- # Series config (same as dashboard script) # --------------------------------------------------------------------------- GEOS = ["DC", "MD", "VA", "DCMSA"] LABELS = { "DC": "District of Columbia", "MD": "Maryland", "VA": "Virginia", "DCMSA": "DC Metro MSA", } COLORS = { "DC": "#c0392b", "MD": "#2471a3", "VA": "#1e8449", "DCMSA": "#7d3c98", } SERIES = { "DC_rate": "LAUST110000000000003", "DC_unemployed": "LAUST110000000000004", "DC_employed": "LAUST110000000000005", "DC_laborforce": "LAUST110000000000006", "MD_rate": "LAUST240000000000003", "MD_unemployed": "LAUST240000000000004", "MD_employed": "LAUST240000000000005", "MD_laborforce": "LAUST240000000000006", "VA_rate": "LAUST510000000000003", "VA_unemployed": "LAUST510000000000004", "VA_employed": "LAUST510000000000005", "VA_laborforce": "LAUST510000000000006", "DCMSA_rate": "LAUMT114790000000003", "DCMSA_unemployed":"LAUMT114790000000004", "DCMSA_employed": "LAUMT114790000000005", "DCMSA_laborforce":"LAUMT114790000000006", } # --------------------------------------------------------------------------- # Data fetching # --------------------------------------------------------------------------- def fetch(series_ids, start_year, end_year): payload = { "seriesid": series_ids, "startyear": str(start_year), "endyear": str(end_year), "registrationkey": BLS_API_KEY, "catalog": True, "calculations": True, "annualaverage": False, } r = requests.post(f"{BLS_API_BASE}/timeseries/data/", json=payload, timeout=30) r.raise_for_status() body = r.json() if body["status"] != "REQUEST_SUCCEEDED": raise RuntimeError(f"BLS API error: {body['message']}") return {s["seriesID"]: s["data"] for s in body["Results"]["series"]} def latest(data): for obs in data: if obs["value"] != "-": return obs return {} def chg(obs, window): try: return obs["calculations"]["net_changes"].get(window) except (KeyError, TypeError): return None def pct_chg(obs, window): try: return obs["calculations"]["pct_changes"].get(window) except (KeyError, TypeError): return None # --------------------------------------------------------------------------- # Prepare chart data — chronological, last 24 months, no gaps # --------------------------------------------------------------------------- MONTH_ORDER = {f"M{i:02d}": i for i in range(1, 13)} MONTH_NAMES = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] def sort_obs(data): """Return observations sorted chronologically, monthly only, no nulls.""" monthly = [o for o in data if o["period"].startswith("M") and o["value"] != "-"] return sorted(monthly, key=lambda o: (o["year"], MONTH_ORDER.get(o["period"], 0))) def build_rate_chart_data(results, months=24): """Return labels list and per-geo value lists for the rate trend chart.""" # Build a merged label set from all four geos all_labels = {} for geo in GEOS: sid = SERIES[f"{geo}_rate"] for obs in sort_obs(results.get(sid, [])): key = (obs["year"], obs["period"]) m = MONTH_ORDER.get(obs["period"], 0) label = f"{MONTH_NAMES[m]} {obs['year']}" all_labels[key] = label sorted_keys = sorted(all_labels.keys())[-months:] labels = [all_labels[k] for k in sorted_keys] datasets = [] for geo in GEOS: sid = SERIES[f"{geo}_rate"] obs_by_key = { (o["year"], o["period"]): float(o["value"]) for o in sort_obs(results.get(sid, [])) } values = [obs_by_key.get(k) for k in sorted_keys] datasets.append({ "label": LABELS[geo], "data": values, "borderColor": COLORS[geo], "backgroundColor": COLORS[geo] + "22", "borderWidth": 2, "pointRadius": 2, "tension": 0.3, "fill": False, }) return labels, datasets # --------------------------------------------------------------------------- # HTML helpers # --------------------------------------------------------------------------- def arrow(val): if val is None: return '—' try: n = float(val) if n > 0: return f'▲ {n:+.1f}' if n < 0: return f'▼ {n:.1f}' return f'— {n:.1f}' except (ValueError, TypeError): return '—' def fmt_rate(val): try: return f"{float(val):.1f}%" except (ValueError, TypeError): return "—" def fmt_num(val): try: return f"{float(val):,.0f}" except (ValueError, TypeError): return "—" def build_cards(results): cards_html = [] for geo in GEOS: sid = SERIES[f"{geo}_rate"] obs = latest(results.get(sid, [])) rate = fmt_rate(obs.get("value")) mom = arrow(chg(obs, "1")) yoy = arrow(chg(obs, "12")) period = f"{obs.get('periodName', '')} {obs.get('year', '')}" color = COLORS[geo] cards_html.append(f"""
| Measure | {geo_headers}
|---|
▲ Red = rate rising (more unemployment). ▼ Green = rate falling.
MoM = month-over-month net change | YoY = year-over-year net change | pp = percentage points