#!/usr/bin/env python3 from __future__ import annotations from datetime import datetime from pathlib import Path from typing import Iterable from scripts.constants import DB_PATH_DEFAULT import logging logger = logging.getLogger(__name__) import matplotlib.pyplot as plt import pandas as pd import sqlite3 def save_figure(fig: plt.Figure, output: str | None, script_path: str) -> Path: """Save `fig` to `output` or `outputs/` with a timestamped filename.""" out_path = ( Path(output) if output is not None else Path("outputs") / f"{Path(script_path).stem}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png" ) out_path.parent.mkdir(parents=True, exist_ok=True) fig.savefig(out_path) logger.info("Saved figure to %s", out_path) return out_path def fetch_series_db( series: Iterable[str], start: datetime, end: datetime, db_path: str | Path = DB_PATH_DEFAULT, ) -> pd.DataFrame: """ Load one or more FRED series from a local SQLite DB. Returns a DataFrame whose columns are the requested series names. """ db_path = Path(db_path) if not db_path.exists(): raise FileNotFoundError( f"{db_path} not found. Run scripts/refresh_data.py on a machine with internet access," " commit the DB, then retry." ) frames: list[pd.DataFrame] = [] with sqlite3.connect(db_path) as conn: for name in series: df = pd.read_sql( f"SELECT date, {name} FROM {name}", conn, parse_dates=["date"], index_col="date", ).loc[start:end] df.rename(columns={name: name}, inplace=True) frames.append(df) if not frames: raise ValueError("No series loaded") return pd.concat(frames, axis=1).sort_index() def validate_dataframe(df: pd.DataFrame, name: str) -> None: """Ensure DataFrame has a 'value' column with no nulls and a DatetimeIndex.""" if df.empty: raise ValueError(f"{name} dataframe is empty") if "value" not in df.columns: raise ValueError(f"{name} missing 'value' column") if df["value"].isna().any(): raise ValueError(f"{name} contains null values") if not isinstance(df.index, pd.DatetimeIndex): raise ValueError("Indexes must be a Pandas DatetimeIndex") def validate_overlap(df1: pd.DataFrame, df2: pd.DataFrame) -> None: """Verify that two series share a non-empty overlapping date range.""" start = max(df1.index.min(), df2.index.min()) end = min(df1.index.max(), df2.index.max()) if start >= end: raise ValueError("Series have no overlapping date range")