Add BLS client library, example scripts, and usage docs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
368
USAGE.md
Normal file
368
USAGE.md
Normal file
@ -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
|
||||||
|
```
|
||||||
2
bls_client/__init__.py
Normal file
2
bls_client/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from .client import BLSClient
|
||||||
|
from . import queries, series
|
||||||
171
bls_client/client.py
Normal file
171
bls_client/client.py
Normal file
@ -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
|
||||||
1
bls_client/queries/__init__.py
Normal file
1
bls_client/queries/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import employment, prices, wages, productivity
|
||||||
133
bls_client/queries/employment.py
Normal file
133
bls_client/queries/employment.py
Normal file
@ -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"
|
||||||
124
bls_client/queries/prices.py
Normal file
124
bls_client/queries/prices.py
Normal file
@ -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"
|
||||||
36
bls_client/queries/productivity.py
Normal file
36
bls_client/queries/productivity.py
Normal file
@ -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(),
|
||||||
|
}
|
||||||
120
bls_client/queries/wages.py
Normal file
120
bls_client/queries/wages.py
Normal file
@ -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"
|
||||||
368
bls_client/series.py
Normal file
368
bls_client/series.py
Normal file
@ -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}"
|
||||||
43
examples/basic_pull.py
Normal file
43
examples/basic_pull.py
Normal file
@ -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}")
|
||||||
77
examples/custom_series.py
Normal file
77
examples/custom_series.py
Normal file
@ -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']})")
|
||||||
Reference in New Issue
Block a user