| """ |
| Fetch FRED observation data for one or more series by ID. |
| |
| This is the "data retrieval" counterpart to ``search.py``. After semantic |
| search identifies relevant series IDs, use this module to pull the actual |
| time series values from the FRED API. |
| |
| CLI |
| --- |
| fred-fetch UNRATE DGS10 --start 2020-01-01 |
| fred-fetch CPIAUCSL --last 24 |
| fred-fetch PAYEMS --start 2023-01-01 --end 2024-12-31 |
| |
| Library |
| ------- |
| from fred_search import fetch_series |
| |
| data = fetch_series(["UNRATE", "DGS10"], start="2020-01-01") |
| for series_id, observations in data.items(): |
| print(series_id, len(observations), "observations") |
| """ |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import json |
| import logging |
| import os |
| import sys |
| from datetime import date, timedelta |
| from typing import Any |
|
|
| from fred_search._client import FREDClient |
|
|
| logger = logging.getLogger(__name__) |
|
|
| _DEFAULT_LOOKBACK_YEARS = 5 |
|
|
|
|
| def fetch_series( |
| series_ids: list[str], |
| api_key: str | None = None, |
| start: str | None = None, |
| end: str | None = None, |
| last: int | None = None, |
| ) -> dict[str, list[dict[str, str | float | None]]]: |
| """ |
| Fetch observation data for one or more FRED series. |
| |
| Parameters |
| ---------- |
| series_ids: |
| One or more FRED series identifiers (e.g. ``["UNRATE", "DGS10"]``). |
| api_key: |
| FRED API key. Falls back to ``FRED_API_KEY`` env var. |
| start: |
| Observation start date (YYYY-MM-DD). Defaults to 5 years ago. |
| end: |
| Observation end date (YYYY-MM-DD). Defaults to today. |
| last: |
| If set, return only the last N observations per series. |
| Overrides ``start`` — fetches all data then truncates. |
| |
| Returns |
| ------- |
| Dict mapping each series_id to a list of observations:: |
| |
| {"UNRATE": [{"date": "2024-01-01", "value": 3.7}, ...]} |
| |
| Values are floats where parseable, or None for missing data |
| (FRED returns ``"."`` for missing observations). |
| """ |
| key = api_key or os.environ.get("FRED_API_KEY") |
| if not key: |
| raise ValueError( |
| "FRED API key required. Set FRED_API_KEY env var or pass api_key=." |
| ) |
|
|
| if not start and not last: |
| start = (date.today() - timedelta(days=365 * _DEFAULT_LOOKBACK_YEARS)).isoformat() |
|
|
| result: dict[str, list[dict[str, str | float | None]]] = {} |
|
|
| with FREDClient(api_key=key) as client: |
| for sid in series_ids: |
| sid_upper = sid.upper() |
| logger.info("Fetching observations for %s", sid_upper) |
|
|
| raw = client.get_series_observations( |
| series_id=sid_upper, |
| observation_start=start, |
| observation_end=end, |
| ) |
|
|
| observations = _parse_observations(raw) |
|
|
| if last is not None: |
| observations = observations[-last:] |
|
|
| result[sid_upper] = observations |
|
|
| return result |
|
|
|
|
| def _parse_observations( |
| raw: list[dict[str, str]], |
| ) -> list[dict[str, str | float | None]]: |
| """Convert FRED string values to floats, treating '.' as None.""" |
| parsed = [] |
| for obs in raw: |
| val_str = obs["value"] |
| if val_str == ".": |
| value: float | None = None |
| else: |
| try: |
| value = float(val_str) |
| except (ValueError, TypeError): |
| value = None |
| parsed.append({"date": obs["date"], "value": value}) |
| return parsed |
|
|
|
|
| |
| |
| |
|
|
| def _format_output( |
| data: dict[str, list[dict[str, str | float | None]]], |
| as_json: bool, |
| ) -> str: |
| if as_json: |
| return json.dumps(data, indent=2) |
|
|
| lines = [] |
| for sid, observations in data.items(): |
| lines.append(f"\n{sid} ({len(observations)} observations)") |
| lines.append("-" * 40) |
| for obs in observations: |
| val = obs["value"] |
| val_str = f"{val:>12.4f}" if val is not None else " ." |
| lines.append(f" {obs['date']} {val_str}") |
| return "\n".join(lines) |
|
|
|
|
| def main() -> None: |
| parser = argparse.ArgumentParser( |
| description="Fetch FRED observation data for one or more series.", |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| epilog=( |
| "Examples:\n" |
| " fred-fetch UNRATE DGS10 --start 2020-01-01\n" |
| " fred-fetch CPIAUCSL --last 24\n" |
| " fred-fetch PAYEMS --start 2023-01-01 --end 2024-12-31 --json" |
| ), |
| ) |
| parser.add_argument( |
| "series_ids", nargs="+", metavar="SERIES_ID", |
| help="One or more FRED series identifiers", |
| ) |
| parser.add_argument( |
| "--start", default=None, metavar="YYYY-MM-DD", |
| help=f"Observation start date (default: {_DEFAULT_LOOKBACK_YEARS} years ago)", |
| ) |
| parser.add_argument( |
| "--end", default=None, metavar="YYYY-MM-DD", |
| help="Observation end date (default: most recent)", |
| ) |
| parser.add_argument( |
| "--last", type=int, default=None, metavar="N", |
| help="Return only the last N observations per series", |
| ) |
| parser.add_argument( |
| "--api-key", default=None, metavar="KEY", |
| help="FRED API key (default: FRED_API_KEY env var)", |
| ) |
| parser.add_argument( |
| "--json", action="store_true", |
| help="Output as JSON (default: human-readable table)", |
| ) |
| args = parser.parse_args() |
|
|
| logging.basicConfig( |
| level=logging.WARNING, |
| format="%(levelname)s %(message)s", |
| ) |
|
|
| try: |
| data = fetch_series( |
| series_ids=args.series_ids, |
| api_key=args.api_key, |
| start=args.start, |
| end=args.end, |
| last=args.last, |
| ) |
| except ValueError as exc: |
| print(f"Error: {exc}", file=sys.stderr) |
| sys.exit(1) |
|
|
| if not data or all(len(v) == 0 for v in data.values()): |
| print("No observations found.") |
| return |
|
|
| print(_format_output(data, as_json=args.json)) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|