fred-vdb / fred_search /fetch.py
Jacob235's picture
Deploy FRED-VDB semantic search demo
bdb25be verified
Raw
History Blame Contribute Delete
6.1 kB
"""
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
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
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()