Files
Aether/_deprecated_py_src/api/admin/stats/comparison.py
fawney19 1d9c77522a refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/
- 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层
- 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构
- 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations)
- 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image
- 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
2026-04-03 16:26:16 +08:00

143 lines
5.2 KiB
Python

"""Admin comparison stats routes."""
from __future__ import annotations
from datetime import date, timedelta
from typing import Any, Literal
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from sqlalchemy.orm import Session
from src.api.base.admin_adapter import AdminApiAdapter
from src.api.base.context import ApiRequestContext
from src.config.constants import CacheTTL
from src.database import get_db
from src.services.system.stats_aggregator import AggregatedStats, StatsFilter, query_stats_hybrid
from src.services.system.time_range import TimeRangeParams
from src.utils.cache_decorator import cache_result
from .common import pipeline
router = APIRouter()
class AdminComparisonAdapter(AdminApiAdapter):
def __init__(
self,
current_start: date,
current_end: date,
comparison_type: Literal["period", "year"],
timezone_name: str | None,
tz_offset_minutes: int | None,
) -> None:
self.current_start = current_start
self.current_end = current_end
self.comparison_type = comparison_type
self.timezone_name = timezone_name
self.tz_offset_minutes = tz_offset_minutes
@cache_result(
key_prefix="admin:stats:comparison",
ttl=CacheTTL.ADMIN_USAGE_AGGREGATION,
user_specific=False,
vary_by=[
"current_start",
"current_end",
"comparison_type",
"timezone_name",
"tz_offset_minutes",
],
)
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
if self.current_start > self.current_end:
raise HTTPException(status_code=400, detail="current_start must be <= current_end")
days = (self.current_end - self.current_start).days + 1
def _safe_year_shift(value: date) -> date:
try:
return value.replace(year=value.year - 1)
except ValueError:
return value.replace(year=value.year - 1, day=28)
if self.comparison_type == "period":
comparison_end = self.current_start - timedelta(days=1)
comparison_start = comparison_end - timedelta(days=days - 1)
else:
comparison_start = _safe_year_shift(self.current_start)
comparison_end = _safe_year_shift(self.current_end)
current_range = TimeRangeParams(
start_date=self.current_start,
end_date=self.current_end,
timezone=self.timezone_name,
tz_offset_minutes=self.tz_offset_minutes or 0,
).validate_and_resolve()
comparison_range = TimeRangeParams(
start_date=comparison_start,
end_date=comparison_end,
timezone=self.timezone_name,
tz_offset_minutes=self.tz_offset_minutes or 0,
).validate_and_resolve()
current_stats = query_stats_hybrid(context.db, current_range, filters=StatsFilter())
comparison_stats = query_stats_hybrid(context.db, comparison_range, filters=StatsFilter())
def _stats_payload(stats: AggregatedStats) -> dict[str, Any]:
total_tokens = (
stats.input_tokens
+ stats.output_tokens
+ stats.cache_creation_tokens
+ stats.cache_read_tokens
)
return {
"total_requests": stats.total_requests,
"total_tokens": total_tokens,
"total_cost": float(stats.total_cost),
"actual_total_cost": float(stats.actual_total_cost),
"avg_response_time_ms": float(stats.avg_response_time_ms),
"error_requests": stats.error_requests,
}
def _pct_change(current: float, previous: float) -> float | None:
if previous == 0:
return None if current != 0 else 0.0
return round((current - previous) / previous * 100, 2)
current_payload = _stats_payload(current_stats)
comparison_payload = _stats_payload(comparison_stats)
changes = {
key: _pct_change(float(current_payload[key]), float(comparison_payload[key]))
for key in current_payload.keys()
}
return {
"current": current_payload,
"comparison": comparison_payload,
"change_percent": changes,
"current_start": self.current_start.isoformat(),
"current_end": self.current_end.isoformat(),
"comparison_start": comparison_start.isoformat(),
"comparison_end": comparison_end.isoformat(),
}
@router.get("/comparison")
async def get_comparison(
request: Request,
db: Session = Depends(get_db),
current_start: date = Query(...),
current_end: date = Query(...),
comparison_type: Literal["period", "year"] = Query("period"),
timezone_name: str | None = Query(None, alias="timezone"),
tz_offset_minutes: int | None = Query(0),
) -> Any:
adapter = AdminComparisonAdapter(
current_start=current_start,
current_end=current_end,
comparison_type=comparison_type,
timezone_name=timezone_name,
tz_offset_minutes=tz_offset_minutes,
)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)