Add src/selectel_ml_tui/api/billing.py
This commit is contained in:
@@ -0,0 +1,132 @@
|
|||||||
|
"""Клиент Billing Statistics API Selectel (провайдер aig).
|
||||||
|
|
||||||
|
Аутентификация: статический токен в заголовке X-Token.
|
||||||
|
Данные: стоимость потребления ИИ-роутера по дням/часам.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from selectel_ml_tui.api.client import BaseApiClient, SelectelApiError
|
||||||
|
from selectel_ml_tui.models import ConsumptionItem
|
||||||
|
|
||||||
|
BillingApiError = SelectelApiError
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_dt(value: str) -> str:
|
||||||
|
"""Привести дату к формату, принимаемому Billing API.
|
||||||
|
|
||||||
|
Billing API ожидает полный ISO-таймстамп (например
|
||||||
|
``2026-08-01T00:00:00.000``); короткая дата дополняется.
|
||||||
|
"""
|
||||||
|
if "T" in value:
|
||||||
|
return value
|
||||||
|
return f"{value}T00:00:00.000"
|
||||||
|
|
||||||
|
|
||||||
|
class BillingClient(BaseApiClient):
|
||||||
|
"""Клиент /v1/cloud_billing/statistic/* для провайдера aig."""
|
||||||
|
|
||||||
|
PROVIDER = "aig"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
api_base_url: str,
|
||||||
|
static_token: str,
|
||||||
|
timeout: float = 30.0,
|
||||||
|
max_retries: int = 2,
|
||||||
|
transport: httpx.AsyncBaseTransport | None = None,
|
||||||
|
sleep: Any = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(
|
||||||
|
timeout=timeout,
|
||||||
|
max_retries=max_retries,
|
||||||
|
transport=transport,
|
||||||
|
sleep=sleep,
|
||||||
|
)
|
||||||
|
self._base_url = api_base_url.rstrip("/")
|
||||||
|
self._token = static_token
|
||||||
|
|
||||||
|
def _headers(self) -> dict[str, str]:
|
||||||
|
return {"X-Token": self._token, "Content-Type": "application/json"}
|
||||||
|
|
||||||
|
async def _get(
|
||||||
|
self, path: str, params: dict[str, str] | None = None
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
url = f"{self._base_url}{path}"
|
||||||
|
response = await self.request(
|
||||||
|
"GET", url, params=params, headers=self._headers()
|
||||||
|
)
|
||||||
|
data = response.json()
|
||||||
|
if not isinstance(data, dict) or data.get("status") != "success":
|
||||||
|
raise BillingApiError(f"Billing API: {data}")
|
||||||
|
return data
|
||||||
|
|
||||||
|
async def get_consumption(
|
||||||
|
self,
|
||||||
|
start: str,
|
||||||
|
end: str,
|
||||||
|
locale: str = "ru",
|
||||||
|
group_type: str = "metric",
|
||||||
|
period_group_type: str = "day",
|
||||||
|
) -> list[ConsumptionItem]:
|
||||||
|
"""Получить потребление за период.
|
||||||
|
|
||||||
|
group_type: project | object | metric | project_metric | region_metric
|
||||||
|
period_group_type: hour | day | week | month | year | all
|
||||||
|
"""
|
||||||
|
data = await self._get(
|
||||||
|
"/v1/cloud_billing/statistic/consumption",
|
||||||
|
params={
|
||||||
|
"provider_keys": self.PROVIDER,
|
||||||
|
"start": normalize_dt(start),
|
||||||
|
"end": normalize_dt(end),
|
||||||
|
"locale": locale,
|
||||||
|
"group_type": group_type,
|
||||||
|
"period_group_type": period_group_type,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return [ConsumptionItem.model_validate(item) for item in data.get("data", [])]
|
||||||
|
|
||||||
|
async def get_consumption_history(
|
||||||
|
self,
|
||||||
|
start: str,
|
||||||
|
end: str,
|
||||||
|
group: str = "day",
|
||||||
|
) -> list[ConsumptionItem]:
|
||||||
|
"""История потребления по дням/часам за период.
|
||||||
|
|
||||||
|
group: hour | day | week | month | year | all
|
||||||
|
"""
|
||||||
|
return await self.get_consumption(
|
||||||
|
start, end, period_group_type=group, group_type="metric"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_summary(
|
||||||
|
self,
|
||||||
|
start: str,
|
||||||
|
end: str,
|
||||||
|
group_type: str = "metric",
|
||||||
|
period_group_type: str = "month",
|
||||||
|
) -> Decimal:
|
||||||
|
"""Суммарная стоимость за период в копейках (value)."""
|
||||||
|
data = await self._get(
|
||||||
|
"/v1/cloud_billing/statistic/summary_stats",
|
||||||
|
params={
|
||||||
|
"provider_keys": self.PROVIDER,
|
||||||
|
"start": normalize_dt(start),
|
||||||
|
"end": normalize_dt(end),
|
||||||
|
"group_type": group_type,
|
||||||
|
"period_group_type": period_group_type,
|
||||||
|
"items_count_period_group_type": "all",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
rows = data.get("data", [])
|
||||||
|
total: Decimal = Decimal("0")
|
||||||
|
for row in cast(list[dict[str, Any]], rows):
|
||||||
|
total += Decimal(str(row.get("value", 0)))
|
||||||
|
return total
|
||||||
Reference in New Issue
Block a user