From 20bd3e9a2f1f62530e4d5a1e2506343d56c3444b Mon Sep 17 00:00:00 2001 From: Dave Boyd Date: Fri, 19 Jun 2026 08:39:47 -0400 Subject: [PATCH] Add BLS client library, example scripts, and usage docs Co-Authored-By: Claude Sonnet 4.6 --- USAGE.md | 368 +++++++++++++++++++++++++++++ bls_client/__init__.py | 2 + bls_client/client.py | 171 ++++++++++++++ bls_client/queries/__init__.py | 1 + bls_client/queries/employment.py | 133 +++++++++++ bls_client/queries/prices.py | 124 ++++++++++ bls_client/queries/productivity.py | 36 +++ bls_client/queries/wages.py | 120 ++++++++++ bls_client/series.py | 368 +++++++++++++++++++++++++++++ examples/basic_pull.py | 43 ++++ examples/custom_series.py | 77 ++++++ 11 files changed, 1443 insertions(+) create mode 100644 USAGE.md create mode 100644 bls_client/__init__.py create mode 100644 bls_client/client.py create mode 100644 bls_client/queries/__init__.py create mode 100644 bls_client/queries/employment.py create mode 100644 bls_client/queries/prices.py create mode 100644 bls_client/queries/productivity.py create mode 100644 bls_client/queries/wages.py create mode 100644 bls_client/series.py create mode 100644 examples/basic_pull.py create mode 100644 examples/custom_series.py diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 0000000..90975c5 --- /dev/null +++ b/USAGE.md @@ -0,0 +1,368 @@ +# BLS Data Library — Usage Guide + +A Python library for querying the BLS Public Data API v2, with pre-built series for employment, prices, wages, and productivity. + +--- + +## Data Flow + +```mermaid +flowchart TD + subgraph YOUR_CODE ["Your Code"] + direction TB + Q["Query functions\nbls_client/queries/\nemployment · prices · wages · productivity"] + S["Series ID builders\nbls_client/series.py\nlaus_state() · ces_national() · cpi() · jolts()"] + C["BLSClient\nbls_client/client.py\nfetch() · fetch_named() · to_rows()"] + Q -->|"returns series ID string"| C + S -->|"returns series ID string"| C + end + + subgraph BLS_API ["BLS Public Data API — api.bls.gov/publicAPI/v2"] + direction TB + EP1["/timeseries/data/\nPOST · up to 50 series · 20 years"] + EP2["/surveys\nGET · all 68 survey codes"] + EP3["/timeseries/popular?survey=XX\nGET · top series per survey"] + end + + subgraph OUTPUT ["Output"] + R["dict keyed by series ID\nor labeled dict via fetch_named()"] + ROW["Flat row list\nclient.to_rows() → CSV / DataFrame"] + HTML["HTML Reports\ngenerate_report.py\nbls_dataset_explorer.py"] + end + + C -->|"POST JSON\nwith API key + series IDs"| EP1 + C -->|GET| EP2 + C -->|GET| EP3 + EP1 -->|"JSON: value · period · footnotes\n± catalog · calculations"| R + R -->|"client.to_rows()"| ROW + R -->|"passed to report generator"| HTML +``` + +--- + +## API Limits + +| | Unregistered (v1) | Registered (v2) | +|---|---|---| +| Daily queries | 25 | **500** | +| Series per call | 25 | **50** | +| Years per call | 10 | **20** | +| Catalog metadata | No | Yes | +| MoM/YoY calculations | No | Yes | + +Register free at **https://data.bls.gov/registrationEngine/** + +--- + +## Setup + +```bash +# 1. Copy and fill in your API key +cp config.example.py config.py +# Edit config.py and paste your key + +# 2. Install dependencies (standard library only + requests) +pip install requests +``` + +--- + +## Quick Start + +```python +from config import BLS_API_KEY +from bls_client import BLSClient +from bls_client.queries import employment, prices + +client = BLSClient(BLS_API_KEY) + +# Fetch a single series +result = client.fetch_latest(employment.nonfarm_payrolls(), years=2) + +# Fetch multiple named series at once +results = client.fetch_named(prices.cpi_dashboard(), 2023, 2025) +for label, s in results.items(): + obs = client.latest_obs(s) + print(f"{label}: {obs['value']} ({obs['periodName']} {obs['year']})") +``` + +--- + +## BLSClient API + +### `fetch(series_ids, start_year, end_year, **kwargs)` + +Fetch one or more series. Handles batching automatically (50 per API call). + +```python +result = client.fetch( + ["CES0000000001", "LAUST110000000000003"], + start_year=2020, + end_year=2025, + catalog=True, # include series title and metadata + calculations=True, # include MoM/YoY net and % changes + annual_average=False, # include M13 annual average row +) +# Returns: {"CES0000000001": {"data": [...], "catalog": {...}}, ...} +``` + +**Observation format:** +```python +{ + "year": "2025", + "period": "M04", # M01-M12=monthly, Q01-Q04=quarterly, A01=annual + "periodName": "April", + "value": "6.4", # "-" when not available + "footnotes": [{"code": "R", "text": "Revised..."}], + "calculations": { # only when calculations=True + "net_changes": {"1": "-0.2", "3": "0.1", "12": "1.3"}, + "pct_changes": {"1": "-3.0", "3": "1.5", "12": "25.5"}, + } +} +``` + +**Footnote codes:** +| Code | Meaning | +|------|---------| +| R | Revised | +| P | Preliminary | +| X | Not available (government shutdown gap, etc.) | +| N | Not available | + +### `fetch_latest(series_ids, years=2, **kwargs)` + +Convenience wrapper — fetches the most recent N years. + +```python +result = client.fetch_latest(employment.nonfarm_payrolls(), years=3) +``` + +### `fetch_named(named_dict, start_year, end_year, **kwargs)` + +Fetch a `{label: series_id}` dict and return results keyed by label. + +```python +results = client.fetch_named( + {"DC Unemployment": "LAUST110000000000003", + "MD Unemployment": "LAUST240000000000003"}, + 2023, 2025 +) +``` + +### `client.to_rows(results)` + +Flatten fetch results into a list of dicts for CSV/DataFrame use. + +```python +rows = client.to_rows(result) +# [{"series_id": "CES0000000001", "series_title": "...", +# "year": "2025", "period": "M04", "period_name": "April", "value": "158987"}, ...] + +import csv +with open("output.csv", "w", newline="") as f: + w = csv.DictWriter(f, fieldnames=rows[0].keys()) + w.writeheader(); w.writerows(rows) +``` + +--- + +## Query Library + +### Employment (`bls_client/queries/employment.py`) + +```mermaid +graph LR + E["employment module"] + E --> NP["nonfarm_payrolls()"] + E --> PP["private_payrolls()"] + E --> GE["government_employment()"] + E --> NUR["national_unemployment_rate()"] + E --> LFP["national_labor_force_participation()"] + E --> DC["dc_unemployment_rate()"] + E --> MD["md_unemployment_rate()"] + E --> VA["va_unemployment_rate()"] + E --> SU["state_unemployment(state_fips)\nreturns dict of 4 measures"] + E --> DRU["dc_region_unemployment()\n6 geos as labeled dict"] + E --> JD["jolts_dashboard()\n5 JOLTS measures"] + E --> QCEW["qcew_national_private()\nqcew_dc_private()\nqcew_state(fips)"] +``` + +| Function | Series | Description | +|---|---|---| +| `nonfarm_payrolls()` | CES0000000001 | Total nonfarm payroll employment | +| `private_payrolls()` | CES0500000001 | Total private employment | +| `government_employment()` | CES9000000001 | Federal + state + local | +| `national_unemployment_rate()` | LNS14000000 | CPS U-3 rate (SA) | +| `dc_unemployment_rate()` | LAUST110000000000003 | DC, not SA | +| `state_unemployment(fips)` | 4 LAUS series | Rate/employed/unemployed/LF | +| `dc_region_unemployment()` | 6 series | DC/MD/VA + 3 MSAs | +| `job_openings_level()` | JTS000000000000JOL | JOLTS openings | +| `jolts_dashboard()` | 5 series | Full JOLTS flow | +| `qcew_state(fips)` | EN… | QCEW quarterly by state | + +### Prices (`bls_client/queries/prices.py`) + +| Function | Series | Description | +|---|---|---| +| `cpi_all_items()` | CUUR0000SA0 | CPI-U headline | +| `cpi_core()` | CUSR0000SA0L1E | All items less food & energy (SA) | +| `cpi_food()` | CUUR0000SAF | Food at home + away | +| `cpi_energy()` | CUUR0000SA0E | Energy component | +| `cpi_shelter()` | CUUR0000SAH1 | Shelter/housing | +| `cpi_gasoline()` | CUSR0000SS47014 | Gasoline (SA) | +| `cpi_dashboard()` | 7 series | Full CPI labeled dict | +| `cpi_w_all_items()` | CWUR0000SA0 | CPI-W (COLA basis) | +| `ppi_all_commodities()` | WPU00000000 | PPI all commodities | +| `ppi_dashboard()` | 4 series | PPI components | +| `avg_price_electricity()` | APU000072610 | ¢/KWh retail | +| `avg_price_gasoline_regular()` | APU00007471A | $/gallon regular | +| `import_price_index()` | EIUIR | Import price index | +| `export_price_index()` | EIUIQ | Export price index | + +### Wages (`bls_client/queries/wages.py`) + +| Function | Series | Description | +|---|---|---| +| `eci_total_compensation()` | CIU1010000000000A | ECI civilian all workers | +| `eci_wages()` | CIU1010000000000W | ECI wages only | +| `eci_benefits()` | CIU1010000000000B | ECI benefits only | +| `eci_dashboard()` | 5 series | Civilian + private + state/local | +| `ecec_total_compensation()` | CMU1010000000000D | Cost per hour worked | +| `ecec_health_insurance()` | CMU1010000000000H | Health ins. cost/hr | +| `median_weekly_earnings()` | LEU0252881600 | Median weekly earnings (SA) | +| `occupation_annual_median_wage(soc)` | OE… | OEWS wage for SOC code | + +**SOC codes** available in `wages.SOC_CODES` dict. + +### Productivity (`bls_client/queries/productivity.py`) + +| Function | Series | Description | +|---|---|---| +| `business_output_per_hour()` | PRS85006092 | Business sector productivity | +| `business_unit_labor_cost()` | PRS85006111 | Unit labor costs | +| `nonfarm_output_per_hour()` | PRS86006092 | Nonfarm business | +| `manufacturing_output_per_hour()` | PRS88006092 | Manufacturing | +| `productivity_dashboard()` | 4 series | Labeled dict | + +--- + +## Series ID Builders + +Use `bls_client/series.py` when you need series not covered by the query library. + +```mermaid +flowchart LR + S["series.py"] + S --> LA["laus_state(state_fips, measure, seasonal)\nlaus_county(state, county, measure)\nlaus_msa(state, cbsa_code, measure)"] + S --> CE["ces_national(industry, data_type, seasonal)\nces_state(state_fips, industry, data_type)"] + S --> CP["cpi(item, area, seasonal, series)"] + S --> PP["ppi_commodity(commodity_code)"] + S --> OE["oes_national(occupation, industry, data_type)"] + S --> JT["jolts(element, rate_level, industry, ownership)"] + S --> EC["eci(worker_type, occupation, industry, component)"] + S --> PR["productivity(sector, measure, seasonal)"] +``` + +**Examples:** +```python +from bls_client import series + +# LAUS: any geography +series.laus_state(36, "rate") # New York unemployment rate +series.laus_county(24, 33, "employed") # Howard County MD employment +series.laus_msa(51, 40060, "rate") # Richmond VA MSA rate + +# CES: specific industry +series.ces_national("healthcare", "avg_hourly_earn") # healthcare wages +series.ces_state(11, "government") # DC govt employment + +# CPI: any component +series.cpi("shelter", seasonal=True) # SA shelter index +series.cpi("all_items", series="W") # CPI-W all items + +# JOLTS: any flow +series.jolts("quits", "R") # quits rate +series.jolts("hires", "L", seasonal=False) # hires level, NSA + +# OES: any occupation +series.oes_national("291141", data_type="annual_median") # RN median wage +``` + +--- + +## Examples + +| File | What it shows | +|---|---| +| `examples/basic_pull.py` | Fetch single series, named dict, flatten to rows | +| `examples/custom_series.py` | Build series IDs manually; JOLTS, OES, CPI, LAUS by state | +| `dc_md_va_unemployment.py` | LAUS pull for 6 DC-region geos → CSV | +| `generate_report.py` | Full HTML report with Chart.js charts | +| `bls_dataset_explorer.py` | All 68 surveys with live data samples | + +--- + +## LAUS State FIPS Reference + +| FIPS | State | | FIPS | State | +|------|-------|-|------|-------| +| 01 | Alabama | | 30 | Montana | +| 02 | Alaska | | 31 | Nebraska | +| 04 | Arizona | | 32 | Nevada | +| 05 | Arkansas | | 33 | New Hampshire | +| 06 | California | | 34 | New Jersey | +| 08 | Colorado | | 35 | New Mexico | +| 09 | Connecticut | | 36 | New York | +| 10 | Delaware | | 37 | North Carolina | +| **11** | **District of Columbia** | | 38 | North Dakota | +| 12 | Florida | | 39 | Ohio | +| 13 | Georgia | | 40 | Oklahoma | +| 15 | Hawaii | | 41 | Oregon | +| 16 | Idaho | | 42 | Pennsylvania | +| 17 | Illinois | | 44 | Rhode Island | +| 18 | Indiana | | 45 | South Carolina | +| 19 | Iowa | | 46 | South Dakota | +| 20 | Kansas | | 47 | Tennessee | +| 21 | Kentucky | | 48 | Texas | +| 22 | Louisiana | | 49 | Utah | +| 23 | Maine | | 50 | Vermont | +| **24** | **Maryland** | | **51** | **Virginia** | +| 25 | Massachusetts | | 53 | Washington | +| 26 | Michigan | | 54 | West Virginia | +| 27 | Minnesota | | 55 | Wisconsin | +| 28 | Mississippi | | 56 | Wyoming | +| 29 | Missouri | | | | + +--- + +## Files in This Repository + +``` +bls-data/ +├── USAGE.md ← this file +├── README.md ← API reference (endpoints, limits, formats) +├── config.example.py ← copy to config.py and add your key +│ +├── bls_client/ ← Python library +│ ├── __init__.py +│ ├── client.py ← BLSClient class +│ ├── series.py ← series ID builder functions +│ └── queries/ +│ ├── employment.py ← LAUS, CES, JOLTS, QCEW series +│ ├── prices.py ← CPI, PPI, Import/Export series +│ ├── wages.py ← OES, ECI, ECEC, CPS earnings +│ └── productivity.py ← Major sector productivity +│ +├── examples/ +│ ├── basic_pull.py ← Getting started +│ └── custom_series.py ← Building series IDs manually +│ +├── generate_report.py ← DC/MD/VA HTML report generator +├── bls_dataset_explorer.py ← All-surveys HTML reference +├── dc_md_va_unemployment.py ← CLI pull → CSV +│ +├── series_id_formats.md ← Series ID decode tables (all surveys) +├── qcew_field_schema.md ← QCEW CSV field layouts +├── surveys.json ← All 68 surveys from API +└── api_response_example.json ← Sample API response +``` diff --git a/bls_client/__init__.py b/bls_client/__init__.py new file mode 100644 index 0000000..b425f46 --- /dev/null +++ b/bls_client/__init__.py @@ -0,0 +1,2 @@ +from .client import BLSClient +from . import queries, series diff --git a/bls_client/client.py b/bls_client/client.py new file mode 100644 index 0000000..458c223 --- /dev/null +++ b/bls_client/client.py @@ -0,0 +1,171 @@ +""" +BLSClient — thin wrapper around the BLS Public Data API v2. + +Usage: + from bls_client import BLSClient + client = BLSClient("YOUR_API_KEY") + data = client.fetch(["CES0000000001"], 2020, 2025) +""" + +import requests +from datetime import datetime + + +class BLSClient: + BASE_URL = "https://api.bls.gov/publicAPI/v2" + MAX_SERIES_PER_CALL = 50 + MAX_YEARS_PER_CALL = 20 + + def __init__(self, api_key: str): + self.api_key = api_key + + # ------------------------------------------------------------------ + # Core fetch + # ------------------------------------------------------------------ + def fetch( + self, + series_ids: list[str] | str, + start_year: int, + end_year: int, + catalog: bool = True, + calculations: bool = False, + annual_average: bool = False, + ) -> dict[str, list[dict]]: + """ + Fetch time-series data for one or more series IDs. + + Automatically batches requests when series_ids > 50. + Returns a dict keyed by series ID, value is the list of + observations (newest first). + + Args: + series_ids: Single series ID or list of series IDs. + start_year: First year to retrieve. + end_year: Last year to retrieve (max 20 years from start). + catalog: Include series title/metadata in response. + calculations: Include MoM / YoY net and pct changes. + annual_average: Include M13 annual average rows. + + Returns: + {series_id: [{"year":..., "period":..., "value":..., ...}, ...]} + """ + if isinstance(series_ids, str): + series_ids = [series_ids] + + results = {} + for i in range(0, len(series_ids), self.MAX_SERIES_PER_CALL): + batch = series_ids[i : i + self.MAX_SERIES_PER_CALL] + payload = { + "seriesid": batch, + "startyear": str(start_year), + "endyear": str(end_year), + "registrationkey": self.api_key, + "catalog": catalog, + "calculations": calculations, + "annualaverage": annual_average, + } + r = requests.post( + f"{self.BASE_URL}/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']}") + for s in body["Results"]["series"]: + results[s["seriesID"]] = { + "data": s["data"], + "catalog": s.get("catalog", {}), + } + + return results + + def fetch_latest( + self, + series_ids: list[str] | str, + years: int = 2, + **kwargs, + ) -> dict[str, list[dict]]: + """Convenience: fetch the most recent `years` years.""" + end = datetime.now().year + start = end - (years - 1) + return self.fetch(series_ids, start, end, **kwargs) + + def fetch_named( + self, + named: dict[str, str], + start_year: int, + end_year: int, + **kwargs, + ) -> dict[str, list[dict]]: + """ + Fetch a labeled dict of series IDs. + + Args: + named: {"Human label": "SERIES_ID", ...} + Returns: + {"Human label": {"data": [...], "catalog": {...}}} + """ + id_to_label = {v: k for k, v in named.items()} + raw = self.fetch(list(named.values()), start_year, end_year, **kwargs) + return {id_to_label[sid]: v for sid, v in raw.items()} + + # ------------------------------------------------------------------ + # Discovery endpoints + # ------------------------------------------------------------------ + def surveys(self) -> list[dict]: + """Return all BLS surveys with abbreviation and name.""" + r = requests.get(f"{self.BASE_URL}/surveys", timeout=15) + r.raise_for_status() + return r.json()["Results"]["survey"] + + def popular(self, survey: str) -> list[str]: + """Return popular series IDs for a given survey abbreviation.""" + r = requests.get( + f"{self.BASE_URL}/timeseries/popular", + params={"survey": survey}, + timeout=15, + ) + r.raise_for_status() + body = r.json() + return [ + s["seriesID"] + for s in body.get("Results", {}).get("series", []) + if s + ] + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + @staticmethod + def latest_obs(series_result: dict) -> dict | None: + """Return the most recent non-null observation from a fetch result.""" + for obs in series_result.get("data", []): + if obs["value"] != "-": + return obs + return None + + @staticmethod + def to_rows(results: dict[str, dict]) -> list[dict]: + """ + Flatten fetch results into a list of dicts suitable for CSV or + pandas DataFrame ingestion. + + Each row: {series_id, series_title, year, period, period_name, value} + """ + rows = [] + for sid, s in results.items(): + title = s.get("catalog", {}).get("series_title", "") + for obs in s.get("data", []): + if obs["value"] == "-": + continue + rows.append({ + "series_id": sid, + "series_title": title, + "year": obs["year"], + "period": obs["period"], + "period_name": obs.get("periodName", ""), + "value": obs["value"], + }) + return rows diff --git a/bls_client/queries/__init__.py b/bls_client/queries/__init__.py new file mode 100644 index 0000000..15e5622 --- /dev/null +++ b/bls_client/queries/__init__.py @@ -0,0 +1 @@ +from . import employment, prices, wages, productivity diff --git a/bls_client/queries/employment.py b/bls_client/queries/employment.py new file mode 100644 index 0000000..47d9e5f --- /dev/null +++ b/bls_client/queries/employment.py @@ -0,0 +1,133 @@ +""" +Pre-built employment series IDs — ready to pass to BLSClient.fetch(). + +All functions return a single series ID string or a dict of {label: series_id}. +""" + +from ..series import laus_state, laus_msa, ces_national, ces_state, jolts + +# --------------------------------------------------------------------------- +# National employment +# --------------------------------------------------------------------------- +def nonfarm_payrolls(seasonal: bool = True) -> str: + """Total nonfarm payroll employment (CES). The headline monthly jobs number.""" + return ces_national("total_nonfarm", "employees", seasonal) + + +def private_payrolls(seasonal: bool = True) -> str: + """Total private sector payroll employment.""" + return ces_national("total_private", "employees", seasonal) + + +def government_employment(seasonal: bool = True) -> str: + """Total government employment (federal + state + local).""" + return ces_national("government", "employees", seasonal) + + +def manufacturing_employment(seasonal: bool = True) -> str: + return ces_national("manufacturing", "employees", seasonal) + + +def national_unemployment_rate(seasonal: bool = True) -> str: + """National unemployment rate from the CPS (U-3 rate).""" + return "LNS14000000" if seasonal else "LNU04000000" + + +def national_labor_force_participation(seasonal: bool = True) -> str: + """National labor force participation rate.""" + return "LNS11300000" if seasonal else "LNU01300000" + + +# --------------------------------------------------------------------------- +# LAUS — state unemployment +# --------------------------------------------------------------------------- +def dc_unemployment_rate(seasonal: bool = False) -> str: + return laus_state(11, "rate", seasonal) + + +def md_unemployment_rate(seasonal: bool = False) -> str: + return laus_state(24, "rate", seasonal) + + +def va_unemployment_rate(seasonal: bool = False) -> str: + return laus_state(51, "rate", seasonal) + + +def state_unemployment(state_fips: int, seasonal: bool = False) -> dict: + """All four LAUS measures for a state.""" + return { + "rate": laus_state(state_fips, "rate", seasonal), + "unemployed": laus_state(state_fips, "unemployed", seasonal), + "employed": laus_state(state_fips, "employed", seasonal), + "laborforce": laus_state(state_fips, "laborforce", seasonal), + } + + +def dc_region_unemployment() -> dict: + """Unemployment rates for DC, MD, VA states + DC Metro, Baltimore, Richmond MSAs.""" + return { + "DC State": laus_state(11, "rate"), + "Maryland": laus_state(24, "rate"), + "Virginia": laus_state(51, "rate"), + "DC Metro MSA": laus_msa(11, 47900, "rate"), + "Baltimore MSA":laus_msa(24, 12580, "rate"), + "Richmond MSA": laus_msa(51, 40060, "rate"), + } + + +# --------------------------------------------------------------------------- +# JOLTS +# --------------------------------------------------------------------------- +def job_openings_level(seasonal: bool = True) -> str: + return jolts("job_openings", "L", seasonal=seasonal) + + +def job_openings_rate(seasonal: bool = True) -> str: + return jolts("job_openings", "R", seasonal=seasonal) + + +def quits_level(seasonal: bool = True) -> str: + return jolts("quits", "L", seasonal=seasonal) + + +def hires_level(seasonal: bool = True) -> str: + return jolts("hires", "L", seasonal=seasonal) + + +def layoffs_level(seasonal: bool = True) -> str: + return jolts("layoffs", "L", seasonal=seasonal) + + +def jolts_dashboard() -> dict: + """All five JOLTS measures (levels, SA) as a labeled dict.""" + return { + "Job Openings": job_openings_level(), + "Hires": hires_level(), + "Quits": quits_level(), + "Layoffs": layoffs_level(), + "Total Separations": jolts("total_separations", "L"), + } + + +# --------------------------------------------------------------------------- +# QCEW +# --------------------------------------------------------------------------- +def qcew_national_private() -> str: + """QCEW — national, private sector, all industries, quarterly.""" + return "ENU0000010510000" + + +def qcew_dc_private() -> str: + """QCEW — DC, private sector, all industries.""" + return "ENU1100010510000" + + +def qcew_state(state_fips: int, ownership: str = "5") -> str: + """ + QCEW state-level series. + + Args: + state_fips: 2-digit FIPS + ownership: "0"=all, "5"=private, "1"=federal, "2"=state, "3"=local + """ + return f"ENU{state_fips:02d}0001{ownership}10000" diff --git a/bls_client/queries/prices.py b/bls_client/queries/prices.py new file mode 100644 index 0000000..22f9831 --- /dev/null +++ b/bls_client/queries/prices.py @@ -0,0 +1,124 @@ +""" +Pre-built price series IDs — CPI, PPI, Import/Export, Average Prices. +""" + +from ..series import cpi, ppi_commodity + +# --------------------------------------------------------------------------- +# CPI-U +# --------------------------------------------------------------------------- +def cpi_all_items(seasonal: bool = False) -> str: + """CPI-U all items, US city average.""" + return cpi("all_items", seasonal=seasonal) + + +def cpi_core(seasonal: bool = True) -> str: + """CPI-U all items less food and energy (core inflation).""" + return cpi("core", seasonal=seasonal) + + +def cpi_food(seasonal: bool = False) -> str: + return cpi("food", seasonal=seasonal) + + +def cpi_energy(seasonal: bool = False) -> str: + return cpi("energy", seasonal=seasonal) + + +def cpi_shelter(seasonal: bool = False) -> str: + return cpi("shelter", seasonal=seasonal) + + +def cpi_gasoline(seasonal: bool = True) -> str: + return cpi("gasoline", seasonal=seasonal) + + +def cpi_medical(seasonal: bool = False) -> str: + return cpi("medical", seasonal=seasonal) + + +def cpi_dashboard(seasonal: bool = False) -> dict: + """Key CPI components as a labeled dict.""" + return { + "All Items": cpi_all_items(seasonal), + "Core (ex food/NRG)": cpi_core(True), + "Food": cpi_food(seasonal), + "Energy": cpi_energy(seasonal), + "Shelter": cpi_shelter(seasonal), + "Medical Care": cpi_medical(seasonal), + "Gasoline": cpi_gasoline(True), + } + + +# --------------------------------------------------------------------------- +# CPI-W +# --------------------------------------------------------------------------- +def cpi_w_all_items(seasonal: bool = False) -> str: + """CPI-W all items (used for Social Security COLA).""" + return cpi("all_items", seasonal=seasonal, series="W") + + +# --------------------------------------------------------------------------- +# PPI +# --------------------------------------------------------------------------- +def ppi_all_commodities() -> str: + """PPI — all commodities index.""" + return ppi_commodity("00000000") + + +def ppi_finished_goods() -> str: + """PPI — finished goods.""" + return ppi_commodity("3") + + +def ppi_energy() -> str: + return ppi_commodity("05") + + +def ppi_food() -> str: + return ppi_commodity("02") + + +def ppi_dashboard() -> dict: + return { + "All Commodities": ppi_all_commodities(), + "Finished Goods": ppi_finished_goods(), + "Food": ppi_food(), + "Energy": ppi_energy(), + } + + +# --------------------------------------------------------------------------- +# Average Prices (AP) +# --------------------------------------------------------------------------- +def avg_price_electricity() -> str: + """Average retail price of electricity per KWh.""" + return "APU000072610" + + +def avg_price_gasoline_regular() -> str: + """Average retail price of regular unleaded gasoline per gallon.""" + return "APU00007471A" + + +def avg_price_eggs() -> str: + """Average retail price of eggs per dozen.""" + return "APU0000708111" + + +def avg_price_ground_beef() -> str: + """Average retail price of ground beef per pound.""" + return "APU0000703112" + + +# --------------------------------------------------------------------------- +# Import/Export Price Indexes (EI) +# --------------------------------------------------------------------------- +def import_price_index() -> str: + """Import price index — all imports.""" + return "EIUIR" + + +def export_price_index() -> str: + """Export price index — all exports.""" + return "EIUIQ" diff --git a/bls_client/queries/productivity.py b/bls_client/queries/productivity.py new file mode 100644 index 0000000..6d36235 --- /dev/null +++ b/bls_client/queries/productivity.py @@ -0,0 +1,36 @@ +""" +Pre-built productivity series IDs — Major Sector Productivity & Costs. +""" + +from ..series import productivity + +def business_output_per_hour(seasonal: bool = True) -> str: + """Business sector output per hour worked (quarterly).""" + return productivity("business", "output_per_hour", seasonal) + + +def business_unit_labor_cost(seasonal: bool = True) -> str: + """Business sector unit labor costs (quarterly).""" + return productivity("business", "unit_labor_cost", seasonal) + + +def business_real_comp_per_hour(seasonal: bool = True) -> str: + """Business sector real compensation per hour.""" + return productivity("business", "real_comp_per_hr", seasonal) + + +def nonfarm_output_per_hour(seasonal: bool = True) -> str: + return productivity("nonfarm_business", "output_per_hour", seasonal) + + +def manufacturing_output_per_hour(seasonal: bool = True) -> str: + return productivity("manufacturing", "output_per_hour", seasonal) + + +def productivity_dashboard() -> dict: + return { + "Business Output/Hr": business_output_per_hour(), + "Business Unit Labor Cost": business_unit_labor_cost(), + "Nonfarm Output/Hr": nonfarm_output_per_hour(), + "Manufacturing Output/Hr": manufacturing_output_per_hour(), + } diff --git a/bls_client/queries/wages.py b/bls_client/queries/wages.py new file mode 100644 index 0000000..4f14ca6 --- /dev/null +++ b/bls_client/queries/wages.py @@ -0,0 +1,120 @@ +""" +Pre-built wage and compensation series IDs — OES, ECI, ECEC, CPS earnings. +""" + +from ..series import oes_national, eci + +# --------------------------------------------------------------------------- +# 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. "151132" 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": "151132", + "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 = True) -> str: + """ECI — civilian workers, all industries, total compensation.""" + return eci("10", "10", component="A", seasonal=seasonal) + + +def eci_wages(seasonal: bool = True) -> str: + """ECI — civilian workers, wages and salaries only.""" + return eci("10", "10", component="W", seasonal=seasonal) + + +def eci_benefits(seasonal: bool = True) -> str: + """ECI — civilian workers, benefit costs only.""" + return eci("10", "10", component="B", seasonal=seasonal) + + +def eci_private(seasonal: bool = True) -> str: + """ECI — private sector, total compensation.""" + return eci("20", "10", component="A", seasonal=seasonal) + + +def eci_state_local(seasonal: bool = True) -> str: + """ECI — state and local government, total compensation.""" + return eci("30", "10", component="A", 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() -> str: + """ECEC — civilian workers, total compensation cost per hour.""" + return "CMU1010000000000D" + + +def ecec_health_insurance() -> str: + """ECEC — health insurance cost per hour worked.""" + return "CMU1010000000000H" + + +def ecec_retirement() -> str: + """ECEC — retirement & savings cost per hour worked.""" + return "CMU1010000000000R" + + +# --------------------------------------------------------------------------- +# 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" diff --git a/bls_client/series.py b/bls_client/series.py new file mode 100644 index 0000000..edbf83a --- /dev/null +++ b/bls_client/series.py @@ -0,0 +1,368 @@ +""" +Series ID builders for major BLS surveys. + +These functions construct the coded series IDs used by the BLS API. +Pass the returned string directly to BLSClient.fetch(). + +Example: + from bls_client.series import laus_state, ces_national + client.fetch([laus_state(11, "rate"), ces_national("00000000", "01")], 2020, 2025) +""" + +# --------------------------------------------------------------------------- +# LAUS — Local Area Unemployment Statistics +# Series format: LA[adj][area_type+area_code 15 chars][measure 2 chars] +# --------------------------------------------------------------------------- +_LAUS_MEASURE = { + "rate": "03", + "unemployed": "04", + "employed": "05", + "laborforce": "06", + "emp_pop": "07", + "lfpr": "08", + "pop": "09", +} + +def laus_state(state_fips: int, measure: str = "rate", seasonal: bool = False) -> str: + """ + LAUS series for a state. + + Args: + state_fips: 2-digit state FIPS (e.g. 11=DC, 24=MD, 51=VA) + measure: "rate" | "unemployed" | "employed" | "laborforce" + seasonal: True for seasonally adjusted + + Examples: + laus_state(11, "rate") → "LAUST110000000000003" + laus_state(24, "employed") → "LAUST240000000000005" + """ + adj = "S" if seasonal else "U" + m = _LAUS_MEASURE.get(measure, measure) + # Area code = ST(2) + state_fips(2) + 11 zeros = 15 chars → total series = 20 + return f"LA{adj}ST{state_fips:02d}{'0'*11}{m}" + + +def laus_county(state_fips: int, county_fips: int, measure: str = "rate", seasonal: bool = False) -> str: + """ + LAUS series for a county. + + Args: + state_fips: 2-digit state FIPS + county_fips: 3-digit county FIPS + measure: "rate" | "unemployed" | "employed" | "laborforce" + seasonal: True for seasonally adjusted + + Examples: + laus_county(11, 1, "rate") → "LAUCN110010000000003" + """ + adj = "S" if seasonal else "U" + m = _LAUS_MEASURE.get(measure, measure) + # Area code = CN(2) + state_fips(2) + county_fips(3) + 8 zeros = 15 chars → total = 20 + return f"LA{adj}CN{state_fips:02d}{county_fips:03d}{'0'*8}{m}" + + +def laus_msa(state_fips: int, cbsa_code: int, measure: str = "rate") -> str: + """ + LAUS series for a Metropolitan Statistical Area. + + The state_fips should be the primary state in the MSA. + + Args: + state_fips: 2-digit FIPS of the primary state + cbsa_code: 5-digit CBSA code + measure: "rate" | "unemployed" | "employed" | "laborforce" + + Examples: + laus_msa(11, 47900, "rate") → DC-Arlington MSA unemployment rate + laus_msa(24, 12580, "rate") → Baltimore MSA unemployment rate + laus_msa(51, 40060, "rate") → Richmond MSA unemployment rate + """ + m = _LAUS_MEASURE.get(measure, measure) + return f"LAUMT{state_fips:02d}{cbsa_code:05d}000000{m}" + + +# --------------------------------------------------------------------------- +# CES — Current Employment Statistics (National) +# Series format: CE[adj][industry 8 chars][data_type 2 chars] +# --------------------------------------------------------------------------- +_CES_SUPERSECTOR = { + "total_nonfarm": "00000000", + "total_private": "05000000", + "mining_logging": "10000000", + "construction": "20000000", + "manufacturing": "30000000", + "durable": "31000000", + "nondurable": "32000000", + "trade_trans_util":"40000000", + "wholesale": "41000000", + "retail": "42000000", + "information": "50000000", + "financial": "55000000", + "professional": "60000000", + "education_health":"65000000", + "leisure": "70000000", + "other_services": "80000000", + "government": "90000000", +} + +_CES_DATATYPE = { + "employees": "01", + "avg_weekly_hours": "02", + "avg_hourly_earn": "03", + "prod_employees": "06", + "women_employees": "10", + "avg_weekly_earn": "11", +} + +def ces_national( + industry: str = "total_nonfarm", + data_type: str = "employees", + seasonal: bool = True, +) -> str: + """ + CES national employment series. + + Args: + industry: Supersector name (see _CES_SUPERSECTOR) or 8-digit code string + data_type: Measure name (see _CES_DATATYPE) or 2-digit code string + seasonal: True for seasonally adjusted + + Examples: + ces_national() → "CES0000000001" + ces_national("manufacturing", "employees") → "CES3000000001" + ces_national("retail", "avg_hourly_earn") → "CES4200000003" + """ + adj = "S" if seasonal else "U" + ind = _CES_SUPERSECTOR.get(industry, industry) + dtype = _CES_DATATYPE.get(data_type, data_type) + return f"CE{adj}{ind}{dtype}" + + +def ces_state( + state_fips: int, + industry: str = "total_nonfarm", + data_type: str = "employees", + seasonal: bool = True, + area_code: str = "00000", +) -> str: + """ + State or metro employment series (SM prefix). + + Args: + state_fips: 2-digit state FIPS + industry: Supersector name or 8-digit code + data_type: Measure name or 2-digit code + seasonal: True for SA + area_code: 5-digit metro area code; "00000" = statewide + + Examples: + ces_state(11) → "SMS110000000000001" (DC total nonfarm) + ces_state(24, "retail") → "SMS240000042000001" (MD retail) + """ + adj = "S" if seasonal else "U" + ind = _CES_SUPERSECTOR.get(industry, industry) + dtype = _CES_DATATYPE.get(data_type, data_type) + return f"SM{adj}{state_fips:02d}{area_code}{ind}{dtype}" + + +# --------------------------------------------------------------------------- +# CPI — Consumer Price Index +# Series format: CU[adj][periodicity][area 4 chars][item 6 chars] +# --------------------------------------------------------------------------- +_CPI_ITEM = { + "all_items": "SA0", + "core": "SA0L1E", # all items less food & energy + "food": "SAF", + "food_at_home": "SAF1", + "energy": "SA0E", + "shelter": "SAH1", + "apparel": "SAA", + "transportation":"SAT", + "medical": "SAM", + "education": "SAE", + "recreation": "SAR", + "gasoline": "SS47014", + "electricity": "SEHF01", + "new_vehicles": "SETA01", +} + +def cpi( + item: str = "all_items", + area: str = "0000", + seasonal: bool = False, + series: str = "U", +) -> str: + """ + CPI series ID. + + Args: + item: Item code name (see _CPI_ITEM) or raw item code + area: 4-char area code ("0000" = US city average) + seasonal: True for seasonally adjusted + series: "U" = CPI-U, "W" = CPI-W, "S" = Chained CPI-U + + Examples: + cpi() → "CUUR0000SA0" + cpi("core", seasonal=True) → "CUSR0000SA0L1E" + cpi("gasoline", seasonal=True) → "CUSR0000SS47014" + cpi("all_items", series="W") → "CWUR0000SA0" + """ + prefix_map = {"U": "CU", "W": "CW", "S": "SU"} + prefix = prefix_map.get(series, "CU") + adj = "S" if seasonal else "U" + itm = _CPI_ITEM.get(item, item) + return f"{prefix}{adj}R{area}{itm}" + + +# --------------------------------------------------------------------------- +# PPI — Producer Price Index (commodity-based) +# --------------------------------------------------------------------------- +def ppi_commodity(commodity_code: str = "00000000") -> str: + """ + PPI commodity series. + + Args: + commodity_code: 8-char commodity code ("00000000" = all commodities) + + Examples: + ppi_commodity() → "WPU00000000" + ppi_commodity("1012") → "WPU1012" (iron & steel scrap) + """ + return f"WPU{commodity_code}" + + +# --------------------------------------------------------------------------- +# OES — Occupational Employment & Wage Statistics +# --------------------------------------------------------------------------- +_OES_DATATYPE = { + "employment": "01", + "hourly_mean": "03", + "annual_mean": "04", + "hourly_10pct": "06", + "hourly_25pct": "07", + "hourly_median":"08", + "hourly_75pct": "09", + "hourly_90pct": "10", + "annual_median":"13", +} + +def oes_national( + occupation_code: str = "000000", + industry_code: str = "000000", + data_type: str = "employment", +) -> str: + """ + OES national series. + + Args: + occupation_code: 6-digit SOC code or "000000" for all occupations + industry_code: 6-digit NAICS or "000000" for cross-industry + data_type: Measure name (see _OES_DATATYPE) or 2-digit code + + Examples: + oes_national() → all occupations, employment + oes_national("151132", data_type="annual_median") → software devs median wage + """ + dtype = _OES_DATATYPE.get(data_type, data_type) + # OE+U+N(area_type)+0000400(national area 7 chars)+industry(6)+occupation(6)+dtype(2) = 25 + return f"OEUN0000400{industry_code:0<6}{occupation_code:0<6}{dtype}" + + +# --------------------------------------------------------------------------- +# JOLTS — Job Openings & Labor Turnover +# --------------------------------------------------------------------------- +_JOLTS_ELEMENT = { + "job_openings": "JO", + "hires": "HI", + "quits": "QU", + "layoffs": "LD", + "total_separations": "TS", +} + +def jolts( + element: str = "job_openings", + rate_level: str = "L", + industry: str = "000000", + ownership: str = "00", + seasonal: bool = True, +) -> str: + """ + JOLTS series. + + Args: + element: "job_openings" | "hires" | "quits" | "layoffs" | "total_separations" + rate_level: "L" = level (thousands) | "R" = rate + industry: 6-digit industry code; "000000" = total nonfarm + ownership: "00" = total | "10" = private | "20" = government + seasonal: True for SA + + Examples: + jolts() → "JTS000000000000JOL" (openings level) + jolts("quits", rate_level="R") → "JTS000000000000QUR" (quits rate) + jolts("hires", seasonal=False) → "JTU000000000000HIL" (hires level, NSA) + """ + adj = "S" if seasonal else "U" + elem = _JOLTS_ELEMENT.get(element, element) + # Format: JT+adj+industry(6)+zeros(6)+elem(2)+rate_level(1) = 18 chars + ind = industry[:6].ljust(6, "0") + return f"JT{adj}{ind}{'0'*6}{elem}{rate_level}" + + +# --------------------------------------------------------------------------- +# ECI — Employment Cost Index +# --------------------------------------------------------------------------- +def eci( + worker_type: str = "10", # 10=civilian, 20=private, 30=state/local + occupation: str = "00", # 00=all, 10=mgmt/prof, 20=service, etc. + industry: str = "000000000", + component: str = "A", # A=total comp, W=wages, B=benefits + seasonal: bool = True, +) -> str: + """ + ECI series for employment cost changes. + + Examples: + eci() → "CIU1010000000000A" (civilian, all workers, total compensation) + eci(component="W") → wages only + """ + adj = "S" if seasonal else "U" + return f"CI{adj}{worker_type}{occupation}{industry}{component}" + + +# --------------------------------------------------------------------------- +# Productivity (Major Sector) +# --------------------------------------------------------------------------- +_PR_SECTOR = { + "business": "85", + "nonfarm_business":"86", + "manufacturing": "88", + "durable_mfg": "89", + "nondurable_mfg": "90", +} + +_PR_MEASURE = { + "output_per_hour": "092", + "output": "041", + "hours": "051", + "compensation": "061", + "real_comp_per_hr": "071", + "unit_labor_cost": "111", + "unit_nonlabor_pay":"112", +} + +def productivity( + sector: str = "business", + measure: str = "output_per_hour", + seasonal: bool = True, +) -> str: + """ + Major Sector Productivity series. + + Examples: + productivity() → "PRS85006092" (business output per hour, SA) + productivity("manufacturing", "unit_labor_cost") + """ + adj = "S" if seasonal else "U" + sec = _PR_SECTOR.get(sector, sector) + mea = _PR_MEASURE.get(measure, measure) + return f"PR{adj}{sec}06{mea}" diff --git a/examples/basic_pull.py b/examples/basic_pull.py new file mode 100644 index 0000000..7c66dea --- /dev/null +++ b/examples/basic_pull.py @@ -0,0 +1,43 @@ +""" +Basic example — fetch a few series and print the latest values. +Run from the bls/ directory: python3 examples/basic_pull.py +""" + +import sys, os +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) + +from config import BLS_API_KEY +from bls_client import BLSClient +from bls_client.queries import employment, prices, wages + +client = BLSClient(BLS_API_KEY) + +# ── Single series ──────────────────────────────────────────────────────────── +print("=== National Nonfarm Payrolls ===") +result = client.fetch_latest(employment.nonfarm_payrolls(), years=1) +for sid, s in result.items(): + obs = client.latest_obs(s) + print(f" {obs['periodName']} {obs['year']}: {float(obs['value']):,.0f} thousand jobs") + +# ── Named dict pull ────────────────────────────────────────────────────────── +print("\n=== CPI Dashboard (latest month) ===") +results = client.fetch_named(prices.cpi_dashboard(), 2025, 2026) +for label, s in results.items(): + obs = client.latest_obs(s) + if obs: + print(f" {label:<30} {obs['value']:>8} ({obs['periodName']} {obs['year']})") + +# ── DC region unemployment ─────────────────────────────────────────────────── +print("\n=== DC Region Unemployment Rates ===") +results = client.fetch_named(employment.dc_region_unemployment(), 2025, 2026) +for label, s in results.items(): + obs = client.latest_obs(s) + if obs: + print(f" {label:<20} {obs['value']}% ({obs['periodName']} {obs['year']})") + +# ── Flatten to rows ────────────────────────────────────────────────────────── +print("\n=== Raw rows (first 3) ===") +result = client.fetch_latest(employment.nonfarm_payrolls(), years=1, catalog=True) +rows = client.to_rows(result) +for row in rows[:3]: + print(f" {row}") diff --git a/examples/custom_series.py b/examples/custom_series.py new file mode 100644 index 0000000..c3e4c81 --- /dev/null +++ b/examples/custom_series.py @@ -0,0 +1,77 @@ +""" +Custom series example — build series IDs from scratch using the series module. +Run from the bls/ directory: python3 examples/custom_series.py +""" + +import sys, os +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) + +from config import BLS_API_KEY +from bls_client import BLSClient +from bls_client import series + +client = BLSClient(BLS_API_KEY) + +# ── Build LAUS series for any state ───────────────────────────────────────── +print("=== LAUS: unemployment rates for New York, Texas, California ===") +state_series = { + "New York": series.laus_state(36, "rate"), + "Texas": series.laus_state(48, "rate"), + "California": series.laus_state(6, "rate"), +} +results = client.fetch_named(state_series, 2025, 2026) +for label, s in results.items(): + obs = client.latest_obs(s) + if obs: + print(f" {label:<15} {obs['value']}%") + +# ── Build CES for specific industries ─────────────────────────────────────── +print("\n=== CES: employment by sector ===") +industry_series = { + "Healthcare": series.ces_national("education_health", "employees"), + "Retail": series.ces_national("retail", "employees"), + "Manufacturing": series.ces_national("manufacturing", "employees"), + "Leisure/Hosp": series.ces_national("leisure", "employees"), +} +results = client.fetch_named(industry_series, 2025, 2026) +for label, s in results.items(): + obs = client.latest_obs(s) + if obs: + print(f" {label:<20} {float(obs['value']):>10,.0f}K ({obs['periodName']} {obs['year']})") + +# ── Build CPI for specific items ───────────────────────────────────────────── +print("\n=== CPI: selected item prices (index, 1982-84=100) ===") +cpi_series = { + "All Items": series.cpi("all_items"), + "Shelter": series.cpi("shelter"), + "Gasoline": series.cpi("gasoline", seasonal=True), + "New Vehicles":series.cpi("new_vehicles"), +} +results = client.fetch_named(cpi_series, 2025, 2026) +for label, s in results.items(): + obs = client.latest_obs(s) + if obs: + print(f" {label:<20} {obs['value']:>8} ({obs['periodName']} {obs['year']})") + +# ── JOLTS: full dashboard ──────────────────────────────────────────────────── +print("\n=== JOLTS: labor market flows ===") +from bls_client.queries import employment +results = client.fetch_named(employment.jolts_dashboard(), 2025, 2026) +for label, s in results.items(): + obs = client.latest_obs(s) + if obs: + print(f" {label:<25} {float(obs['value']):>8,.0f}K ({obs['periodName']} {obs['year']})") + +# ── OES: wages for specific occupations ───────────────────────────────────── +print("\n=== OES: annual median wages by occupation ===") +from bls_client.queries import wages +from bls_client.queries.wages import SOC_CODES +occ_series = { + label.replace("_", " ").title(): wages.occupation_annual_median_wage(code) + for label, code in list(SOC_CODES.items())[:5] +} +results = client.fetch_named(occ_series, 2023, 2024) +for label, s in results.items(): + obs = client.latest_obs(s) + if obs: + print(f" {label:<35} ${float(obs['value']):>10,.0f} ({obs['year']})")