""" 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("151252", data_type="annual_median") → software devs median wage """ dtype = _OES_DATATYPE.get(data_type, data_type) # 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}" # --------------------------------------------------------------------------- # 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() → "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) + 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'*9}{elem}{rate_level}" # --------------------------------------------------------------------------- # ECI — Employment Cost Index # --------------------------------------------------------------------------- def eci( 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, 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}{owner}{component}{'0'*9}{estimate}" # --------------------------------------------------------------------------- # Productivity (Major Sector) # --------------------------------------------------------------------------- _PR_SECTOR = { "business": "8400", "nonfarm_business": "8500", "manufacturing": "3000", "durable_mfg": "3100", "nondurable_mfg": "3200", } _PR_MEASURE = { "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 = "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" (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}{mea}"