"""Hourly market ingestion: fetch every (symbol, group) defined in TOML and insert one Quote row per fetch.""" from __future__ import annotations import asyncio import httpx from app.config import get_settings, load_groups from app.db import utcnow from app.jobs._helpers import job_lifecycle, log from app.models import Quote from app.services.market import fetch async def run() -> None: async with job_lifecycle("market_job") as (session, run): if run.status == "skipped": return s = get_settings() groups = load_groups(s.BASELINE_TOML, s.PORTFOLIO_TOML) anchor = s.CASSANDRA_ANCHOR_DATE or None async with httpx.AsyncClient(follow_redirects=True) as client: tasks = [ fetch(client, sym, lab, note, anchor) for group, items in groups.items() for sym, lab, note in items ] # Run in parallel but bounded — Yahoo can throttle if we hammer. sem = asyncio.Semaphore(16) async def bounded(t): async with sem: return await t quotes = await asyncio.gather(*(bounded(t) for t in tasks)) # Re-index quotes back to their group for persistence. items_flat = [ (group, sym) for group, items in groups.items() for sym, _, _ in items ] now = utcnow() for (group, _sym), q in zip(items_flat, quotes): session.add(Quote( symbol=q.symbol, source=q.source, label=q.label, group_name=group, price=q.price, currency=q.currency, as_of=q.as_of, changes=q.changes or None, error=q.error, fetched_at=now, )) await session.commit() run.items_written = len(quotes) log.info("market_job.done", count=len(quotes)) if __name__ == "__main__": asyncio.run(run())