Add src/selectel_ml_tui/models.py
This commit is contained in:
@@ -0,0 +1,197 @@
|
|||||||
|
"""Pydantic-модели данных Selectel ML.
|
||||||
|
|
||||||
|
Модели ответов Billing Statistics API и Gateway API ИИ-роутера, а также
|
||||||
|
агрегированные структуры для TUI (потребление, метрики, история).
|
||||||
|
|
||||||
|
Все модели устойчивы к отсутствующим полям (значения по умолчанию) и
|
||||||
|
валидируют неотрицательные числа.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||||
|
|
||||||
|
|
||||||
|
def parse_iso_dt(value: str) -> datetime:
|
||||||
|
"""Распарсить ISO-дату, допускающую 'Z' и нативные (без tz) значения."""
|
||||||
|
text = value.replace("Z", "+00:00")
|
||||||
|
try:
|
||||||
|
return datetime.fromisoformat(text)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError(f"Некорректная дата: {value!r}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_dt(value: Any) -> Any:
|
||||||
|
if isinstance(value, str):
|
||||||
|
return parse_iso_dt(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def format_rub(amount: Decimal | int | float | str) -> str:
|
||||||
|
"""Сумма в рублях: 854.83 -> '854,83 ₽', 1234.5 -> '1 234,50 ₽'."""
|
||||||
|
value = Decimal(str(amount))
|
||||||
|
rub, kop = f"{value:.2f}".split(".")
|
||||||
|
rub = f"{int(rub):,}".replace(",", " ")
|
||||||
|
return f"{rub},{kop} ₽"
|
||||||
|
|
||||||
|
|
||||||
|
def format_tokens(value: int) -> str:
|
||||||
|
"""Число токенов с разделителями: 1234567 -> '1 234 567'."""
|
||||||
|
return f"{int(value):,}".replace(",", " ")
|
||||||
|
|
||||||
|
|
||||||
|
def format_percent(value: float) -> str:
|
||||||
|
"""Процент: 99.912 -> '99,9%'."""
|
||||||
|
return f"{float(value):.1f}".replace(".", ",") + "%"
|
||||||
|
|
||||||
|
|
||||||
|
class TokenUsage(BaseModel):
|
||||||
|
"""Метрики токенов (все неотрицательные)."""
|
||||||
|
|
||||||
|
input_tokens: int = Field(default=0, ge=0)
|
||||||
|
output_tokens: int = Field(default=0, ge=0)
|
||||||
|
cache_read_tokens: int = Field(default=0, ge=0)
|
||||||
|
cache_write_tokens: int = Field(default=0, ge=0)
|
||||||
|
|
||||||
|
|
||||||
|
class Consumption(TokenUsage):
|
||||||
|
"""Потребление за период: стоимость + токены."""
|
||||||
|
|
||||||
|
period: datetime
|
||||||
|
amount_rub: Decimal = Field(default=Decimal("0"), ge=0)
|
||||||
|
requests: int = Field(default=0, ge=0)
|
||||||
|
tokens_total: int = Field(default=0, ge=0)
|
||||||
|
|
||||||
|
_validate_period = field_validator("period", mode="before")(_validate_dt)
|
||||||
|
|
||||||
|
|
||||||
|
class Metrics(TokenUsage):
|
||||||
|
"""Общие метрики роутера за 24 часа."""
|
||||||
|
|
||||||
|
availability_pct: float = Field(default=100.0, ge=0, le=100)
|
||||||
|
avg_ttft_ms: float = Field(default=0.0, ge=0)
|
||||||
|
avg_latency_ms: float = Field(default=0.0, ge=0)
|
||||||
|
requests: int = Field(default=0, ge=0)
|
||||||
|
|
||||||
|
|
||||||
|
class ModelMetrics(Metrics):
|
||||||
|
"""Метрики по конкретной модели."""
|
||||||
|
|
||||||
|
model_id: str = ""
|
||||||
|
model_name: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class ApiKeyUsage(BaseModel):
|
||||||
|
"""Лимиты и расход по одному API-ключу роутера."""
|
||||||
|
|
||||||
|
name: str = ""
|
||||||
|
prefix: str = ""
|
||||||
|
is_active: bool = True
|
||||||
|
spend_limit_rub: Decimal = Field(default=Decimal("0"), ge=0)
|
||||||
|
spend_rub: Decimal = Field(default=Decimal("0"), ge=0)
|
||||||
|
remaining_rub: Decimal = Field(default=Decimal("0"), ge=0)
|
||||||
|
|
||||||
|
|
||||||
|
class BudgetInfo(BaseModel):
|
||||||
|
"""Бюджет роутера: лимит, расход, остаток."""
|
||||||
|
|
||||||
|
spend_limit_rub: Decimal = Field(default=Decimal("0"), ge=0)
|
||||||
|
spend_rub: Decimal = Field(default=Decimal("0"), ge=0)
|
||||||
|
remaining_rub: Decimal = Field(default=Decimal("0"), ge=0)
|
||||||
|
window: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class HistoryPoint(BaseModel):
|
||||||
|
"""Точка истории для графика: время, стоимость, токены."""
|
||||||
|
|
||||||
|
timestamp: datetime
|
||||||
|
amount_rub: Decimal = Field(default=Decimal("0"), ge=0)
|
||||||
|
tokens: TokenUsage = Field(default_factory=TokenUsage)
|
||||||
|
|
||||||
|
_validate_timestamp = field_validator("timestamp", mode="before")(_validate_dt)
|
||||||
|
|
||||||
|
|
||||||
|
class MetricInfo(BaseModel):
|
||||||
|
"""Информация о метрике из billing statistics API."""
|
||||||
|
|
||||||
|
id: str = ""
|
||||||
|
name: str = ""
|
||||||
|
unit: str = ""
|
||||||
|
quantity: Decimal | None = None
|
||||||
|
locale: str | None = None
|
||||||
|
region: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectRef(BaseModel):
|
||||||
|
"""Ссылка на проект (ML)."""
|
||||||
|
|
||||||
|
id: str = ""
|
||||||
|
name: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectRef(BaseModel):
|
||||||
|
"""Ссылка на объект (ключ/роутер)."""
|
||||||
|
|
||||||
|
id: str = ""
|
||||||
|
name: str = ""
|
||||||
|
parent_id: str | None = None
|
||||||
|
parent_name: str | None = None
|
||||||
|
type: str | None = None
|
||||||
|
type_name: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class ConsumptionItem(BaseModel):
|
||||||
|
"""Элемент потребления из /v1/cloud_billing/statistic/consumption.
|
||||||
|
|
||||||
|
``project`` и ``object`` могут быть строкой (id), объектом или null.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
|
account_id: str = ""
|
||||||
|
period: str = ""
|
||||||
|
provider_key: str = ""
|
||||||
|
metric: MetricInfo | None = None
|
||||||
|
value: Decimal = Decimal("0")
|
||||||
|
project: ProjectRef | str | None = Field(default=None, alias="project")
|
||||||
|
object: ObjectRef | str | None = Field(default=None, alias="object")
|
||||||
|
balance: Decimal | None = None
|
||||||
|
provision_start: str | None = None
|
||||||
|
provision_end: str | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def project_id(self) -> str | None:
|
||||||
|
"""ID проекта (из строки или объекта)."""
|
||||||
|
if isinstance(self.project, ProjectRef):
|
||||||
|
return self.project.id
|
||||||
|
return self.project
|
||||||
|
|
||||||
|
@property
|
||||||
|
def object_id(self) -> str | None:
|
||||||
|
"""ID объекта (из строки или объекта)."""
|
||||||
|
if isinstance(self.object, ObjectRef):
|
||||||
|
return self.object.id
|
||||||
|
return self.object
|
||||||
|
|
||||||
|
|
||||||
|
class RouterOverview(BaseModel):
|
||||||
|
"""Потребление ИИ-роутера (GET /aig/gateway/v1/gateways/{id}/overview).
|
||||||
|
|
||||||
|
``raw`` хранит необработанные данные до полного описания схемы.
|
||||||
|
"""
|
||||||
|
|
||||||
|
raw: dict[str, Any] = Field(
|
||||||
|
default_factory=dict, description="Необработанные данные ответа"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RouterMetrics(BaseModel):
|
||||||
|
"""Метрики ИИ-роутера за 24 часа (GET /aig/gateway/v1/gateways/{id}/metrics)."""
|
||||||
|
|
||||||
|
raw: dict[str, Any] = Field(
|
||||||
|
default_factory=dict, description="Необработанные данные ответа"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user