mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
- 删除全部 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)
197 lines
5.7 KiB
Python
197 lines
5.7 KiB
Python
"""Shared helpers for admin stats routes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from datetime import date, datetime, time, timedelta, timezone
|
|
from typing import Any, Literal
|
|
|
|
from fastapi import HTTPException
|
|
from sqlalchemy import and_, or_
|
|
|
|
from src.api.base.pipeline import get_pipeline
|
|
from src.config.settings import config
|
|
from src.models.database import Usage
|
|
from src.services.system.time_range import TimeRangeParams
|
|
|
|
pipeline = get_pipeline()
|
|
|
|
|
|
def _apply_admin_default_range(
|
|
params: TimeRangeParams | None,
|
|
) -> TimeRangeParams | None:
|
|
"""Apply a default range to avoid unbounded scans."""
|
|
if params is not None:
|
|
return params
|
|
|
|
days = int(getattr(config, "admin_usage_default_days", 0) or 0)
|
|
if days <= 0:
|
|
return None
|
|
|
|
today = datetime.now(timezone.utc).date()
|
|
start_date = today - timedelta(days=days - 1)
|
|
return TimeRangeParams(
|
|
start_date=start_date,
|
|
end_date=today,
|
|
timezone="UTC",
|
|
tz_offset_minutes=0,
|
|
).validate_and_resolve()
|
|
|
|
|
|
def _build_time_range_params(
|
|
start_date: date | None,
|
|
end_date: date | None,
|
|
preset: str | None,
|
|
timezone_name: str | None,
|
|
tz_offset_minutes: int | None,
|
|
) -> TimeRangeParams | None:
|
|
if not preset and start_date is None and end_date is None:
|
|
return None
|
|
try:
|
|
return TimeRangeParams(
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
preset=preset,
|
|
timezone=timezone_name,
|
|
tz_offset_minutes=tz_offset_minutes or 0,
|
|
).validate_and_resolve()
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
|
|
def _hash_filters(filters: dict[str, Any]) -> str:
|
|
raw = json.dumps(filters, sort_keys=True, ensure_ascii=False, default=str)
|
|
return hashlib.sha1(raw.encode("utf-8")).hexdigest()[:16]
|
|
|
|
|
|
def _build_time_range_from_days(
|
|
days: int, timezone_name: str | None, tz_offset_minutes: int | None
|
|
) -> TimeRangeParams:
|
|
base = TimeRangeParams(
|
|
preset="today",
|
|
timezone=timezone_name,
|
|
tz_offset_minutes=tz_offset_minutes or 0,
|
|
).validate_and_resolve()
|
|
user_today = base.start_date
|
|
start_date = user_today - timedelta(days=days - 1)
|
|
return TimeRangeParams(
|
|
start_date=start_date,
|
|
end_date=user_today,
|
|
timezone=timezone_name,
|
|
tz_offset_minutes=tz_offset_minutes or 0,
|
|
).validate_and_resolve()
|
|
|
|
|
|
def _linear_regression(values: list[float]) -> tuple[float, float]:
|
|
n = len(values)
|
|
if n <= 1:
|
|
return 0.0, values[0] if values else 0.0
|
|
xs = list(range(n))
|
|
sum_x = sum(xs)
|
|
sum_y = sum(values)
|
|
sum_x2 = sum(x * x for x in xs)
|
|
sum_xy = sum(x * y for x, y in zip(xs, values))
|
|
denom = n * sum_x2 - sum_x * sum_x
|
|
if denom == 0:
|
|
return 0.0, values[-1]
|
|
slope = (n * sum_xy - sum_x * sum_y) / denom
|
|
intercept = (sum_y - slope * sum_x) / n
|
|
return slope, intercept
|
|
|
|
|
|
def _build_cache_key(
|
|
leaderboard_type: str,
|
|
metric: str,
|
|
time_range: TimeRangeParams | None,
|
|
filters: dict[str, Any],
|
|
) -> str:
|
|
start_value = time_range.start_date.isoformat() if time_range else "all"
|
|
end_value = time_range.end_date.isoformat() if time_range else "all"
|
|
tz_value = time_range.timezone if time_range else "utc"
|
|
offset_value = time_range.tz_offset_minutes if time_range else 0
|
|
return (
|
|
f"leaderboard:{leaderboard_type}:{metric}:{start_value}:{end_value}:"
|
|
f"{tz_value}:{offset_value}:{_hash_filters(filters)}"
|
|
)
|
|
|
|
|
|
def _is_today_range(time_range: TimeRangeParams | None) -> bool:
|
|
if not time_range:
|
|
return False
|
|
try:
|
|
user_today = time_range._get_user_today()
|
|
except Exception:
|
|
return False
|
|
return time_range.end_date == user_today
|
|
|
|
|
|
def _split_daily_and_usage_segments(
|
|
time_range: TimeRangeParams | None,
|
|
use_daily: bool,
|
|
) -> tuple[tuple[datetime, datetime] | None, list[tuple[datetime, datetime]] | None]:
|
|
if not time_range:
|
|
return None, None
|
|
|
|
start_utc, end_utc = time_range.to_utc_datetime_range()
|
|
if not use_daily:
|
|
return None, [(start_utc, end_utc)]
|
|
|
|
complete_dates, head_boundary, tail_boundary = time_range.get_complete_utc_dates()
|
|
daily_range = None
|
|
if complete_dates:
|
|
daily_start = datetime.combine(complete_dates[0], time.min, tzinfo=timezone.utc)
|
|
daily_end = datetime.combine(
|
|
complete_dates[-1] + timedelta(days=1), time.min, tzinfo=timezone.utc
|
|
)
|
|
daily_range = (daily_start, daily_end)
|
|
|
|
usage_segments: list[tuple[datetime, datetime]] = []
|
|
if head_boundary:
|
|
usage_segments.append(head_boundary)
|
|
if tail_boundary:
|
|
usage_segments.append(tail_boundary)
|
|
|
|
if not daily_range and not usage_segments:
|
|
usage_segments = [(start_utc, end_utc)]
|
|
|
|
return daily_range, usage_segments
|
|
|
|
|
|
def _apply_usage_time_segments(
|
|
query: Any, segments: list[tuple[datetime, datetime]] | None
|
|
) -> Any | None:
|
|
if segments is None:
|
|
return query
|
|
if not segments:
|
|
return None
|
|
|
|
conditions = []
|
|
for start_utc, end_utc in segments:
|
|
if start_utc >= end_utc:
|
|
continue
|
|
conditions.append(and_(Usage.created_at >= start_utc, Usage.created_at < end_utc))
|
|
|
|
if not conditions:
|
|
return None
|
|
|
|
return query.filter(or_(*conditions))
|
|
|
|
|
|
def _union_queries(queries: list[Any]) -> Any | None:
|
|
base = None
|
|
for query in queries:
|
|
if query is None:
|
|
continue
|
|
if base is None:
|
|
base = query
|
|
else:
|
|
base = base.union_all(query)
|
|
return base
|
|
|
|
|
|
def _metric_order(
|
|
metric: Literal["requests", "tokens", "cost"], order: Literal["asc", "desc"], expr: Any
|
|
) -> Any:
|
|
return expr.asc() if order == "asc" else expr.desc()
|