"""Quota usage attribution: a user's own consumption + an admin breakdown across users. The aggregate daily budget lives in ApiQuotaUsage; here we read the per-spend audit log (QuotaEvent) to attribute units to the user who triggered them (NULL actor = system). """ from datetime import datetime, timedelta, timezone from fastapi import APIRouter, Depends, Query from sqlalchemy import func, select from sqlalchemy.orm import Session from app.userscope import owned_by from app import quota from app.auth import admin_user, current_user from app.db import get_db from app.models import QuotaEvent, User from app.routes import _account as account from app.routes._keyset import utc router = APIRouter(prefix="/api/quota", tags=["quota"]) def _sum(db: Session, *conds) -> int: return int(db.scalar(select(func.coalesce(func.sum(QuotaEvent.units), 0)).where(*conds)) or 0) @router.get("/my-usage") def my_usage(user: User = Depends(current_user), db: Session = Depends(get_db)) -> dict: now = datetime.now(timezone.utc) mine = owned_by(QuotaEvent.user_id, user) by_action = { action: int(units) for action, units in db.execute( select(QuotaEvent.action, func.sum(QuotaEvent.units)) .where(mine, QuotaEvent.created_at >= now - timedelta(days=30)) .group_by(QuotaEvent.action) ).all() } return { "today": _sum(db, mine, QuotaEvent.created_at >= quota.pacific_day_start_utc()), "last_7d": _sum(db, mine, QuotaEvent.created_at >= now - timedelta(days=7)), "last_30d": _sum(db, mine, QuotaEvent.created_at >= now - timedelta(days=30)), "all_time": _sum(db, mine), "by_action": by_action, } @router.get("/admin") def admin_usage( since: datetime | None = None, until: datetime | None = None, user: str | None = Query(default=None, pattern=account.PATTERN), _: User = Depends(admin_user), db: Session = Depends(get_db), ) -> dict: """Spend per account (and per action) and per day, between `since` (default: 30 days back) and `until` (default: now), optionally for one account (`none` = the background).""" now = datetime.now(timezone.utc) lo = utc(since) or now - timedelta(days=30) window = [QuotaEvent.created_at >= lo] if until is not None: window.append(QuotaEvent.created_at < utc(until)) if user is not None: window.append(account.condition(QuotaEvent.user_id, user)) totals = db.execute( select(QuotaEvent.user_id, func.sum(QuotaEvent.units)) .where(*window) .group_by(QuotaEvent.user_id) ).all() per_action: dict[int | None, dict[str, int]] = {} for uid, action, units in db.execute( select(QuotaEvent.user_id, QuotaEvent.action, func.sum(QuotaEvent.units)) .where(*window) .group_by(QuotaEvent.user_id, QuotaEvent.action) ).all(): per_action.setdefault(uid, {})[action] = int(units) uids = [uid for uid, _ in totals if uid is not None] emails = ( {u.id: u.email for u in db.execute(select(User).where(User.id.in_(uids))).scalars()} if uids else {} ) rows = [ { "user_id": uid, "email": emails.get(uid, "(deleted user)") if uid is not None else "System", "total": int(total), "by_action": per_action.get(uid, {}), } for uid, total in totals ] rows.sort(key=lambda r: r["total"], reverse=True) # The daily series in Pacific days (matches the budget reset), of the one account when filtered. day_expr = func.date(func.timezone("America/Los_Angeles", QuotaEvent.created_at)) daily = [ {"day": str(day), "total": int(units)} for day, units in db.execute( select(day_expr, func.sum(QuotaEvent.units)) .where(*window) .group_by(day_expr) .order_by(day_expr) ).all() ] return {"rows": rows, "daily": daily}