"""Статистика использования API (#17): число запросов, их время и время, которое был открыт браузер.""" from dataclasses import dataclass, field from datetime import datetime, timedelta from sqlalchemy import select from db.engine import Database from db.models import Client, Request, Window, utcnow @dataclass class ClientUsage: client_id: int | None name: str requests: int = 0 ok: int = 0 # по code: challenge_failed, action_failed, request_failed; failed — задачу не выполнили errors: dict[str, int] = field(default_factory=dict) durations_ms: list[int] = field(default_factory=list) windows: int = 0 browser_seconds: float = 0.0 @property def avg_ms(self) -> int | None: return int(sum(self.durations_ms) / len(self.durations_ms)) if self.durations_ms else None @property def p95_ms(self) -> int | None: if not self.durations_ms: return None ordered = sorted(self.durations_ms) return ordered[min(len(ordered) - 1, int(len(ordered) * 0.95))] def _aware(dt: datetime) -> datetime: # sqlite отдаёт время без зоны return dt if dt.tzinfo else dt.replace(tzinfo=utcnow().tzinfo) async def usage(db: Database, since: datetime) -> list[ClientUsage]: """По клиентам за период: запросы, начатые после since, и время окон в пределах периода.""" now = utcnow() async with db.session() as session: names = {c.id: c.name for c in await session.scalars(select(Client))} requests = list(await session.scalars(select(Request).where(Request.started_at >= since))) windows = list(await session.scalars( select(Window).where((Window.closed_at.is_(None)) | (Window.closed_at >= since)) )) result: dict[int | None, ClientUsage] = {} def entry(client_id): return result.setdefault(client_id, ClientUsage(client_id, names.get(client_id, "—"))) for r in requests: u = entry(r.client_id) u.requests += 1 if r.status == "ok": u.ok += 1 elif r.finished_at is not None: key = r.code or r.status or "unknown" u.errors[key] = u.errors.get(key, 0) + 1 if r.duration_ms is not None: u.durations_ms.append(r.duration_ms) for w in windows: u = entry(w.client_id) u.windows += 1 start = max(_aware(w.started_at), since) end = _aware(w.closed_at) if w.closed_at else now u.browser_seconds += max(0.0, (end - start).total_seconds()) return sorted(result.values(), key=lambda u: -u.requests) def since_days(days: float) -> datetime: return utcnow() - timedelta(days=days)