feat(stats): T32-T33 implement dashboard and usage endpoints

Add statistics router with two endpoints:
- GET /api/stats/dashboard: Aggregated dashboard statistics
  - Query param: days (1-365, default 30)
  - Auth required
  - Returns DashboardResponse

- GET /api/usage: Detailed usage statistics with filtering
  - Required params: start_date, end_date
  - Optional filters: api_key_id, model
  - Pagination: skip, limit (max 1000)
  - Auth required
  - Returns List[UsageStatsResponse]

Also add get_usage_stats() service function for querying
individual usage records with filtering and pagination.
This commit is contained in:
Luca Sacchi Ricciardi
2026-04-07 15:22:31 +02:00
parent b075ae47fe
commit 16f740f023
5 changed files with 453 additions and 1 deletions

View File

@@ -8,6 +8,7 @@ from fastapi.middleware.cors import CORSMiddleware
from openrouter_monitor.config import get_settings
from openrouter_monitor.routers import api_keys
from openrouter_monitor.routers import auth
from openrouter_monitor.routers import stats
settings = get_settings()
@@ -31,6 +32,7 @@ app.add_middleware(
# Include routers
app.include_router(auth.router, prefix="/api/auth", tags=["authentication"])
app.include_router(api_keys.router, prefix="/api/keys", tags=["api-keys"])
app.include_router(stats.router)
@app.get("/")

View File

@@ -1,5 +1,6 @@
"""Routers package for OpenRouter Monitor."""
from openrouter_monitor.routers import api_keys
from openrouter_monitor.routers import auth
from openrouter_monitor.routers import stats
__all__ = ["auth", "api_keys"]
__all__ = ["auth", "api_keys", "stats"]

View File

@@ -0,0 +1,118 @@
"""Statistics router for OpenRouter API Key Monitor.
T32-T33: Stats endpoints for dashboard and usage data.
"""
from datetime import date
from typing import List, Optional
from fastapi import APIRouter, Depends, Query, status
from sqlalchemy.orm import Session
from openrouter_monitor.database import get_db
from openrouter_monitor.dependencies import get_current_user
from openrouter_monitor.models import User
from openrouter_monitor.schemas.stats import (
DashboardResponse,
UsageStatsResponse,
)
from openrouter_monitor.services.stats import (
get_dashboard_data,
get_usage_stats,
)
router = APIRouter(prefix="/api", tags=["statistics"])
@router.get(
"/stats/dashboard",
response_model=DashboardResponse,
status_code=status.HTTP_200_OK,
summary="Get dashboard statistics",
description="Get aggregated statistics for the dashboard view.",
)
async def get_dashboard(
days: int = Query(
default=30,
ge=1,
le=365,
description="Number of days to look back (1-365)",
),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> DashboardResponse:
"""Get dashboard statistics for the current user.
Args:
days: Number of days to look back (default 30, max 365)
db: Database session
current_user: Authenticated user
Returns:
DashboardResponse with summary, by_model, by_date, and top_models
"""
return get_dashboard_data(db, current_user.id, days)
@router.get(
"/usage",
response_model=List[UsageStatsResponse],
status_code=status.HTTP_200_OK,
summary="Get detailed usage statistics",
description="Get detailed usage statistics with filtering and pagination.",
)
async def get_usage(
start_date: date = Query(
...,
description="Start date for the query (YYYY-MM-DD)",
),
end_date: date = Query(
...,
description="End date for the query (YYYY-MM-DD)",
),
api_key_id: Optional[int] = Query(
default=None,
description="Filter by specific API key ID",
),
model: Optional[str] = Query(
default=None,
description="Filter by model name",
),
skip: int = Query(
default=0,
ge=0,
description="Number of records to skip for pagination",
),
limit: int = Query(
default=100,
ge=1,
le=1000,
description="Maximum number of records to return (1-1000)",
),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> List[UsageStatsResponse]:
"""Get detailed usage statistics with filtering.
Args:
start_date: Start date for the query period (required)
end_date: End date for the query period (required)
api_key_id: Optional filter by API key ID
model: Optional filter by model name
skip: Number of records to skip (pagination)
limit: Maximum number of records to return
db: Database session
current_user: Authenticated user
Returns:
List of UsageStatsResponse matching the filters
"""
return get_usage_stats(
db=db,
user_id=current_user.id,
start_date=start_date,
end_date=end_date,
api_key_id=api_key_id,
model=model,
skip=skip,
limit=limit,
)

View File

@@ -16,6 +16,7 @@ from openrouter_monitor.schemas.stats import (
StatsByDate,
StatsByModel,
StatsSummary,
UsageStatsResponse,
)
@@ -253,3 +254,61 @@ def get_dashboard_data(
by_date=by_date,
top_models=top_models,
)
def get_usage_stats(
db: Session,
user_id: int,
start_date: date,
end_date: date,
api_key_id: Optional[int] = None,
model: Optional[str] = None,
skip: int = 0,
limit: int = 100,
) -> List[UsageStatsResponse]:
"""Get detailed usage statistics with filtering.
Args:
db: Database session
user_id: User ID to filter by
start_date: Start date for the query period
end_date: End date for the query period
api_key_id: Optional filter by API key ID
model: Optional filter by model name
skip: Number of records to skip (pagination)
limit: Maximum number of records to return
Returns:
List of UsageStatsResponse matching the filters
"""
from openrouter_monitor.models import UsageStats
# Build base query with join to ApiKey for user filtering
query = (
db.query(UsageStats)
.join(ApiKey, UsageStats.api_key_id == ApiKey.id)
.filter(ApiKey.user_id == user_id)
.filter(UsageStats.date >= start_date)
.filter(UsageStats.date <= end_date)
)
# Apply optional filters
if api_key_id is not None:
query = query.filter(UsageStats.api_key_id == api_key_id)
if model is not None:
query = query.filter(UsageStats.model == model)
# Apply ordering and pagination
results = (
query.order_by(UsageStats.date.desc(), UsageStats.model)
.offset(skip)
.limit(limit)
.all()
)
# Convert to response schema
return [
UsageStatsResponse.model_validate(record)
for record in results
]