diff --git a/README.md b/README.md index 46b139b..eb2c004 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,117 @@ -# BLS Data Reference +# BLS Data Library -Bureau of Labor Statistics — API access, dataset catalog, series ID formats, and field schemas. +A small, dependency-light Python library for pulling U.S. Bureau of Labor Statistics +(BLS) time series — unemployment, payrolls, inflation, wages, job openings, and +productivity — with pre-built series-ID helpers so you never have to hand-encode a +series ID. -## Quick Start +```python +from bls_client import BLSClient +from bls_client.queries import employment, prices -1. Register for an API key: https://data.bls.gov/registrationEngine/ -2. Base API endpoint: `https://api.bls.gov/publicAPI/v2/timeseries/data/` -3. Content-Type: `application/json` (POST) +client = BLSClient(API_KEY) + +# Latest national nonfarm payrolls +client.fetch_latest(employment.nonfarm_payrolls(), years=1) + +# A labeled CPI dashboard in one call +client.fetch_named(prices.cpi_dashboard(), 2024, 2025) +``` + +All 82 zero-argument query helpers are verified against the live API. --- -## API Versions +## Install + +```bash +pip install -r requirements.txt # just `requests` +``` + +## Configure your API key + +Register for a free key (instant, no approval): https://data.bls.gov/registrationEngine/ +The free v2 key allows 500 queries/day, 50 series/query, 20 years/query. + +```bash +cp config.example.py config.py +export BLS_API_KEY="your-key" # preferred — config.py reads this env var +``` + +`config.py` is gitignored; the env var takes precedence over anything written in the file. +You can also pass the key directly: `BLSClient("your-key")`. + +## Quick start + +```bash +python3 examples/basic_pull.py # payrolls, CPI dashboard, DC-region unemployment +python3 examples/custom_series.py # building custom series IDs +``` + +--- + +## What's in the box + +``` +bls_client/ +├── client.py BLSClient — batching, named fetches, catalog metadata, row flattening +├── series.py low-level series-ID builders (LAUS, CES, CPI, PPI, OES, JOLTS, ECI, QCEW, productivity) +└── queries/ + ├── employment.py payrolls, unemployment (LAUS), JOLTS, QCEW + ├── prices.py CPI, PPI, average prices, import/export prices + ├── wages.py OES occupational wages, ECI, ECEC + └── productivity.py major-sector productivity & costs +``` + +`BLSClient` highlights: +- `fetch(ids, start, end)` — auto-batches >50 series, returns `{series_id: {data, catalog}}` +- `fetch_latest(ids, years=N)` — most recent N years +- `fetch_named({label: id})` — returns results keyed by your labels +- `latest_obs(series)` / `to_rows(results)` — convenience for the most recent value / CSV-ready rows + +See **[USAGE.md](USAGE.md)** for the full API and **[series_id_formats.md](series_id_formats.md)** +for the series-ID decode tables. + +--- + +## Coverage + +| Survey | Helpers | Notes | +|---|---|---| +| LAUS — local area unemployment | state / metro / county rates, DC-region dashboard | ✅ | +| CES — payroll employment | national by supersector; state/metro | ✅ | +| CPS — household survey | national unemployment rate, participation | ✅ | +| JOLTS — job openings & turnover | openings/hires/quits/layoffs, dashboard | ✅ | +| CPI — consumer prices | all-items, core, food, energy, gasoline, …; dashboard | ✅ | +| PPI — producer prices | all commodities, final demand, food, energy | ✅ | +| OES — occupational wages | employment + wage percentiles by SOC; 15 common occupations | ✅ | +| ECI — employment cost index | total comp / wages / benefits × civilian/private/gov | ✅ | +| ECEC — employer cost levels | total compensation, total benefits | ✅ (totals only) | +| Productivity & costs | output/hr, ULC, comp, hours × business/nonfarm/manufacturing | ✅ | +| QCEW — quarterly census | national & state private totals | ⚠️ totals only via this API | + +## Known limitations / what "complete" would add + +- **QCEW** is only partially served by the BLS *timeseries* API used here (national and + state-level totals work). County- and industry-level QCEW detail requires the separate + **QCEW Open Data API** (CSV: `https://data.bls.gov/cew/data/api/...`). Not yet wired up. +- **ECEC benefit subcomponents** (health insurance, retirement & savings, etc.) need + specific benefit-subcell codes from the ECEC component list; only the compensation and + benefits totals are currently exposed. +- **No caching / rate-limit handling.** Repeated runs spend against the 500/day quota; a + small on-disk cache and a friendly error on `REQUEST_NOT_PROCESSED` (quota hit) would help. +- **No packaging.** Importable in-tree but not `pip install`-able; add a `pyproject.toml` + to ship it as a real package. +- **No automated tests.** A pytest suite asserting builder outputs against known-good IDs + (and a live smoke test behind a marker) would lock the series-ID encodings in place. + +--- + +## Appendix: BLS API reference + +Reference material for working directly with the API (the library wraps all of this). + +### API versions | Feature | v1 (no key) | v2 (registered) | |---------------------------|-------------|-----------------| @@ -21,174 +122,45 @@ Bureau of Labor Statistics — API access, dataset catalog, series ID formats, a | Series descriptions | No | Yes | | Calculations | No | Yes | ---- +### Endpoints -## Registration - -- URL: https://data.bls.gov/registrationEngine/ -- Free, no approval needed — instant key via email -- Key goes in the JSON payload as `"registrationkey": "YOUR_KEY"` - ---- - -## Endpoints - -### GET: Survey List ``` -GET https://api.bls.gov/publicAPI/v2/surveys -GET https://api.bls.gov/publicAPI/v2/surveys/{survey_abbreviation} -``` -Returns all survey codes and names. See `surveys.json` for full list. - -### GET: Popular Series -``` -GET https://api.bls.gov/publicAPI/v2/timeseries/popular -GET https://api.bls.gov/publicAPI/v2/timeseries/popular?survey={abbreviation} -``` -Returns the 25 most-requested series IDs for a survey. - -### POST: Time Series Data -``` -POST https://api.bls.gov/publicAPI/v2/timeseries/data/ -Content-Type: application/json +GET /v2/surveys # all survey codes and names (see surveys.json) +GET /v2/surveys/{abbr} # one survey +GET /v2/timeseries/popular?survey={abbr} # 25 most-requested series IDs for a survey +POST /v2/timeseries/data/ # the time-series data endpoint ``` -**Minimal request (unregistered):** +Base URL: `https://api.bls.gov/publicAPI/v2` + +**POST body (registered, all v2 features):** ```json { "seriesid": ["LAUST110000000000003", "CES0000000001"], - "startyear": "2023", - "endyear": "2025" -} -``` - -**Full request (registered, v2 features):** -```json -{ - "seriesid": ["LAUST110000000000003", "CES0000000001"], - "startyear": "2020", - "endyear": "2025", + "startyear": "2020", "endyear": "2025", "registrationkey": "YOUR_KEY", - "catalog": true, - "calculations": true, - "annualaverage": true, - "aspects": true + "catalog": true, "calculations": true, "annualaverage": true } ``` -**Response schema:** -```json -{ - "status": "REQUEST_SUCCEEDED", - "responseTime": 114, - "message": [], - "Results": { - "series": [ - { - "seriesID": "LAUST110000000000003", - "catalog": { - "series_title": "...", - "survey_name": "...", - "measure_data_type": "..." - }, - "data": [ - { - "year": "2025", - "period": "M12", - "periodName": "December", - "value": "6.4", - "footnotes": [ - { "code": "R", "text": "Data were subject to revision on April 8, 2026." } - ], - "calculations": { - "net_changes": { "1": "0.1", "3": "-0.2", "6": "0.5", "12": "-0.3" }, - "pct_changes": { "1": "1.6", "3": "-3.0", "6": "8.3", "12": "-4.5" } - } - } - ] - } - ] - } -} -``` +**Period codes:** monthly `M01`–`M12` (`M13` = annual avg); quarterly `Q01`–`Q04` (`Q05` = annual avg); annual `A01`. +**Status codes:** `REQUEST_SUCCEEDED`, `REQUEST_FAILED`, `REQUEST_NOT_PROCESSED` (often = daily quota hit). +**Footnote codes:** `R` revised, `P` preliminary, `X`/`N` unavailable. -**Period codes:** -- Monthly: `M01`–`M12`, `M13` (annual average) -- Quarterly: `Q01`–`Q04`, `Q05` (annual average) -- Annual: `A01` +A full real response is saved in `api_response_example.json`. -**Status codes:** `REQUEST_SUCCEEDED`, `REQUEST_FAILED`, `REQUEST_NOT_PROCESSED` +### Bulk flat files -**Error footnote codes:** -- `R` — Revised -- `P` — Preliminary -- `X` — Data unavailable (e.g., government shutdown gap) -- `N` — Not available +Base: `https://download.bls.gov/pub/time.series/` — each survey folder has +`{prefix}.series` (master list), `{prefix}.data.*` (observations), and `{prefix}.{dimension}` +decode tables (area, industry, measure). Tab-delimited; handy for bulk PostgreSQL ingest. +Key prefixes: `la/` LAUS, `ce/` CES, `sm/` state-metro, `en/` QCEW, `oe/` OES, `jt/` JOLTS, +`cu/` CPI-U, `wp/` PPI. ---- +### Other files in this repo -## Python Example - -```python -import requests - -API_KEY = "YOUR_KEY" -BASE_URL = "https://api.bls.gov/publicAPI/v2/timeseries/data/" - -def get_series(series_ids, start_year, end_year): - payload = { - "seriesid": series_ids, - "startyear": str(start_year), - "endyear": str(end_year), - "registrationkey": API_KEY, - "catalog": True, - "calculations": True, - "annualaverage": True, - } - r = requests.post(BASE_URL, json=payload) - r.raise_for_status() - data = r.json() - if data["status"] != "REQUEST_SUCCEEDED": - raise ValueError(f"BLS API error: {data['message']}") - return data["Results"]["series"] - -# DC unemployment rate (LAUS) -series = get_series(["LAUST110000000000003"], 2020, 2025) -for obs in series[0]["data"]: - print(obs["year"], obs["periodName"], obs["value"]) -``` - ---- - -## Bulk Download (flat files) - -Base URL: `https://download.bls.gov/pub/time.series/` - -Each survey folder contains: -- `{prefix}.series` — master list of all series IDs with metadata -- `{prefix}.data.{N}.{name}` — actual observations, split by category -- `{prefix}.{dimension}` — lookup/decode tables (area, industry, measure, etc.) - -Key folder prefixes: -``` -la/ — LAUS (local area unemployment) -ce/ — CES national employment -sm/ — State & Metro employment (CES state) -en/ — QCEW -oe/ — OES (occupational employment) -jt/ — JOLTS -cu/ — CPI-U -wp/ — PPI commodities -``` - -Files are tab-delimited. Useful for bulk PostgreSQL ingest. - ---- - -## Files in this folder - -- `README.md` — this file +- `series_id_formats.md` — series-ID decode tables for each survey +- `qcew_field_schema.md` — QCEW quarterly/annual CSV field layouts - `surveys.json` — complete survey list from the API -- `series_id_formats.md` — series ID decode tables for each key dataset -- `qcew_field_schema.md` — QCEW quarterly and annual CSV field layouts -- `api_response_example.json` — real API response sample +- `bls_dataset_explorer.py` / `.html` — browsable survey catalog +- `dc_md_va_unemployment.py` / `generate_report.py` — example report generators diff --git a/USAGE.md b/USAGE.md index 90975c5..e3c19dd 100644 --- a/USAGE.md +++ b/USAGE.md @@ -196,7 +196,7 @@ graph LR | `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 | +| `job_openings_level()` | JTS000000000000000JOL | JOLTS openings | | `jolts_dashboard()` | 5 series | Full JOLTS flow | | `qcew_state(fips)` | EN… | QCEW quarterly by state | diff --git a/bls_client/queries/employment.py b/bls_client/queries/employment.py index 47d9e5f..0cd82c4 100644 --- a/bls_client/queries/employment.py +++ b/bls_client/queries/employment.py @@ -113,21 +113,27 @@ def jolts_dashboard() -> dict: # QCEW # --------------------------------------------------------------------------- def qcew_national_private() -> str: - """QCEW — national, private sector, all industries, quarterly.""" - return "ENU0000010510000" + """QCEW — national, private sector, all industries (monthly employment).""" + return "ENUUS00010510" def qcew_dc_private() -> str: """QCEW — DC, private sector, all industries.""" - return "ENU1100010510000" + return qcew_state(11, "5") def qcew_state(state_fips: int, ownership: str = "5") -> str: """ QCEW state-level series. + Format: EN + U + area(5: 2-digit FIPS + "000") + datatype "1" + size "0" + + ownership(1) + industry "10" (total, all industries) = 13 chars. + Args: - state_fips: 2-digit FIPS + state_fips: 2-digit FIPS (e.g. 11=DC) ownership: "0"=all, "5"=private, "1"=federal, "2"=state, "3"=local + + Note: BLS serves the full QCEW catalog (county/industry detail) through its + dedicated QCEW Open Data API, not the timeseries API used here. """ - return f"ENU{state_fips:02d}0001{ownership}10000" + return f"ENU{state_fips:02d}00010{ownership}10" diff --git a/bls_client/queries/prices.py b/bls_client/queries/prices.py index 22f9831..c7d5ae4 100644 --- a/bls_client/queries/prices.py +++ b/bls_client/queries/prices.py @@ -66,9 +66,13 @@ def ppi_all_commodities() -> str: return ppi_commodity("00000000") -def ppi_finished_goods() -> str: - """PPI — finished goods.""" - return ppi_commodity("3") +def ppi_final_demand() -> str: + """PPI — final demand (successor to the discontinued 'finished goods' index).""" + return ppi_commodity("FD4") + + +# Backwards-compatible alias; the legacy "finished goods" index was replaced by final demand. +ppi_finished_goods = ppi_final_demand def ppi_energy() -> str: @@ -82,7 +86,7 @@ def ppi_food() -> str: def ppi_dashboard() -> dict: return { "All Commodities": ppi_all_commodities(), - "Finished Goods": ppi_finished_goods(), + "Final Demand": ppi_final_demand(), "Food": ppi_food(), "Energy": ppi_energy(), } diff --git a/bls_client/queries/wages.py b/bls_client/queries/wages.py index 4f14ca6..275ac42 100644 --- a/bls_client/queries/wages.py +++ b/bls_client/queries/wages.py @@ -17,7 +17,7 @@ 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, + soc_code: 6-digit SOC code (e.g. "151252" for software developers, "291141" for registered nurses, "119021" for construction mgrs) """ return oes_national(soc_code, "000000", "annual_median") @@ -30,7 +30,7 @@ def occupation_employment(soc_code: str) -> str: # Common occupation codes SOC_CODES = { - "software_developers": "151132", + "software_developers": "151252", "registered_nurses": "291141", "teachers_elementary": "252021", "accountants": "132011", @@ -51,29 +51,29 @@ SOC_CODES = { # --------------------------------------------------------------------------- # 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_total_compensation(seasonal: bool = False) -> str: + """ECI — civilian workers, all industries, total compensation (12-mo % change).""" + return eci(owner="10", component="10", 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_wages(seasonal: bool = False) -> str: + """ECI — civilian workers, wages and salaries only (12-mo % change).""" + return eci(owner="10", component="20", 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_benefits(seasonal: bool = False) -> str: + """ECI — civilian workers, benefit costs only (12-mo % change).""" + return eci(owner="10", component="30", seasonal=seasonal) -def eci_private(seasonal: bool = True) -> str: - """ECI — private sector, total compensation.""" - return eci("20", "10", component="A", seasonal=seasonal) +def eci_private(seasonal: bool = False) -> str: + """ECI — private sector, total compensation (12-mo % change).""" + return eci(owner="20", component="10", 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_state_local(seasonal: bool = False) -> str: + """ECI — state and local government, total compensation (12-mo % change).""" + return eci(owner="30", component="10", seasonal=seasonal) def eci_dashboard() -> dict: @@ -90,18 +90,18 @@ def eci_dashboard() -> dict: # ECEC — Employer Costs for Employee Compensation # --------------------------------------------------------------------------- def ecec_total_compensation() -> str: - """ECEC — civilian workers, total compensation cost per hour.""" + """ECEC — civilian workers, total compensation cost per hour worked.""" return "CMU1010000000000D" -def ecec_health_insurance() -> str: - """ECEC — health insurance cost per hour worked.""" - return "CMU1010000000000H" +def ecec_total_benefits() -> str: + """ECEC — civilian workers, total benefits cost per hour worked.""" + return "CMU1036000000000D" - -def ecec_retirement() -> str: - """ECEC — retirement & savings cost per hour worked.""" - return "CMU1010000000000R" +# Note: ECEC benefit subcomponents (health insurance, retirement & savings, etc.) +# are encoded as specific benefit-subcell codes in the series ID (suffix stays "D"). +# Look them up against the ECEC component list before adding helpers — do not +# fabricate them with a letter suffix (the old "...H"/"...R" forms were invalid). # --------------------------------------------------------------------------- diff --git a/bls_client/series.py b/bls_client/series.py index edbf83a..21d2aaf 100644 --- a/bls_client/series.py +++ b/bls_client/series.py @@ -261,11 +261,11 @@ def oes_national( Examples: oes_national() → all occupations, employment - oes_national("151132", data_type="annual_median") → software devs median wage + oes_national("151252", 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}" + # OE + U + N(area_type) + 0000000(national area, 7 chars) + industry(6) + occupation(6) + dtype(2) = 25 + return f"OEUN0000000{industry_code:0<6}{occupation_code:0<6}{dtype}" # --------------------------------------------------------------------------- @@ -297,72 +297,80 @@ def jolts( 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) + jolts() → "JTS000000000000000JOL" (openings level) + jolts("quits", rate_level="R") → "JTS000000000000000QUR" (quits rate) + jolts("hires", seasonal=False) → "JTU000000000000000HIL" (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 + # Format: JT + adj + industry(6) + state(2) + area(5) + sizeclass(2) + elem(2) + rate_level(1) = 21 chars + # state/area/sizeclass default to national / all (the "ownership" arg is reserved; not part of the ID) ind = industry[:6].ljust(6, "0") - return f"JT{adj}{ind}{'0'*6}{elem}{rate_level}" + return f"JT{adj}{ind}{'0'*9}{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, + owner: str = "10", # 10=civilian, 20=private, 30=state/local gov + component: str = "10", # 10=total compensation, 20=wages & salaries, 30=benefits + estimate: str = "A", # A=12-month % change, Q=3-month % change, I=index + seasonal: bool = False, # the 12-month % change estimate is published unadjusted (CIU) ) -> str: """ ECI series for employment cost changes. + Format: CI + adj + owner(2) + component(2) + 9 zeros + estimate(1) = 17 chars. + + Note: the 12-month % change estimate ("A") is published unadjusted only; + seasonally adjusted ECI ("CIS") publishes the 3-month change ("Q") and index ("I"). + Examples: - eci() → "CIU1010000000000A" (civilian, all workers, total compensation) - eci(component="W") → wages only + eci() → "CIU1010000000000A" (civilian, total compensation, 12-mo % chg) + eci(component="20") → "CIU1020000000000A" (civilian, wages & salaries) + eci(component="30") → "CIU1030000000000A" (civilian, benefits) + eci(owner="20") → "CIU2010000000000A" (private, total compensation) """ adj = "S" if seasonal else "U" - return f"CI{adj}{worker_type}{occupation}{industry}{component}" + return f"CI{adj}{owner}{component}{'0'*9}{estimate}" # --------------------------------------------------------------------------- # Productivity (Major Sector) # --------------------------------------------------------------------------- _PR_SECTOR = { - "business": "85", - "nonfarm_business":"86", - "manufacturing": "88", - "durable_mfg": "89", - "nondurable_mfg": "90", + "business": "8400", + "nonfarm_business": "8500", + "manufacturing": "3000", + "durable_mfg": "3100", + "nondurable_mfg": "3200", } _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", + "output_per_hour": "6092", + "output": "6042", + "hours": "6032", + "compensation": "6062", # hourly compensation + "real_comp_per_hr": "6152", # real hourly compensation + "unit_labor_cost": "6112", } def productivity( - sector: str = "business", + sector: str = "nonfarm_business", measure: str = "output_per_hour", seasonal: bool = True, ) -> str: """ Major Sector Productivity series. + Format: PR + adj + sector(4) + measure(4) = 11 chars. Published seasonally adjusted (PRS). + Examples: - productivity() → "PRS85006092" (business output per hour, SA) - productivity("manufacturing", "unit_labor_cost") + productivity() → "PRS85006092" (nonfarm business output per hour) + productivity("manufacturing", "unit_labor_cost") → "PRS30006112" """ 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}" + return f"PR{adj}{sec}{mea}" diff --git a/bls_dataset_explorer.py b/bls_dataset_explorer.py index f5e0459..9f1ebc1 100644 --- a/bls_dataset_explorer.py +++ b/bls_dataset_explorer.py @@ -48,7 +48,7 @@ DATASETS = [ ("JT", "Employment & Labor Force", "active", "Job Openings & Labor Turnover (JOLTS)", - "JTS000000000000JOL", + "JTS000000000000000JOL", "Monthly job openings, hires, quits, layoffs, and total separations " "by industry. Key measure of labor demand."), @@ -134,7 +134,7 @@ DATASETS = [ # ── Wages & Compensation ──────────────────────────────────────────────── ("OE", "Wages & Compensation", "active", "Occupational Employment & Wage Statistics (OEWS)", - "OEUN000040000000000000001", + "OEUN000000000000000000001", "Annual employment and wage estimates for ~800 occupations " "at national, state, and metro levels."), diff --git a/config.example.py b/config.example.py index a2d014a..377021f 100644 --- a/config.example.py +++ b/config.example.py @@ -7,7 +7,14 @@ # 2. Copy this file to config.py: # cp config.example.py config.py # -# 3. Paste your key below and save. +# 3. Provide your key one of two ways: +# a) Preferred — set an environment variable (nothing to edit here): +# export BLS_API_KEY="your-key" +# b) Or replace YOUR_KEY_HERE below with your key. +# +# config.py is gitignored, so a key placed here is never committed. -BLS_API_KEY = "YOUR_KEY_HERE" +import os + +BLS_API_KEY = os.environ.get("BLS_API_KEY", "YOUR_KEY_HERE") BLS_API_BASE = "https://api.bls.gov/publicAPI/v2" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b18d513 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests>=2.28 diff --git a/series_id_formats.md b/series_id_formats.md index eb31ea4..d8551ea 100644 --- a/series_id_formats.md +++ b/series_id_formats.md @@ -191,33 +191,36 @@ ENU0000010510000 → National, quarterly, private, all industries | 14 | Annual 75th percentile wage | | 15 | Annual 90th percentile wage | -**Examples:** +**Examples** (national area type is `N`, national area code is `0000000`): ``` -OEUM0000400000000000001 → National, all industries, all occupations, employment -OEUM0000400000015113201 → National, all industries, software devs, employment +OEUN000000000000000000001 → National, all industries, all occupations, employment +OEUN000000000000000000004 → National, all industries, all occupations, annual mean wage +OEUN000000000000015125213 → National, all industries, software devs (15-1252), annual median wage ``` --- ## JOLTS — Job Openings & Labor Turnover (prefix: JT) -**Series ID:** `JT[S/U][IIIIII][E/J][RR][LL]` +**Series ID:** `JT[S/U][IIIIII][SS][AAAAA][ZZ][EE][L/R]` — 21 chars | Pos | Length | Field | Notes | |-----|--------|-------|-------| | 1-2 | 2 | Prefix | `JT` | | 3 | 1 | Seasonal adj | `S` or `U` | -| 4-9 | 6 | Industry code | NAICS supersector | -| 10 | 1 | Job status | `E`=total; `J`=job openings; `H`=hires; `L`=layoffs/discharges; `Q`=quits; `T`=total separations | -| 11-12 | 2 | Rate/level | `R`=rate; `L`=level (thousands) | -| 13-14 | 2 | Ownership | `00`=total; `10`=private; `20`=government | +| 4-9 | 6 | Industry code | NAICS supersector; `000000` = total nonfarm | +| 10-11 | 2 | State code | `00` = national | +| 12-16 | 5 | Area code | `00000` = all areas | +| 17-18 | 2 | Size class | `00` = all sizes | +| 19-20 | 2 | Data element | `JO`=job openings; `HI`=hires; `QU`=quits; `LD`=layoffs/discharges; `TS`=total separations; `OS`=other separations | +| 21 | 1 | Rate/level | `L`=level (thousands); `R`=rate | -**Examples:** +**Examples** (15 zeros sit between the seasonal code and the 2-char element): ``` -JTS000000000000JOL → Total nonfarm, job openings, level -JTS000000000000JOR → Total nonfarm, job openings, rate -JTS000000000000HIL → Total nonfarm, hires, level -JTS000000000000TSL → Total nonfarm, total separations, level +JTS000000000000000JOL → Total nonfarm, job openings, level +JTS000000000000000JOR → Total nonfarm, job openings, rate +JTS000000000000000HIL → Total nonfarm, hires, level +JTS000000000000000TSL → Total nonfarm, total separations, level ``` ---