Files
analytics/app/routers/sklik.py
T
JiriUhlir 6934f22253 first
2026-06-18 11:58:23 +02:00

132 lines
4.0 KiB
Python

"""Sklik (Seznam) - JSON-RPC proxy.
The proxy logs in with X-Sklik-Token per request (client.loginByToken) and then
performs the requested call, injecting the session for you. See
``app.clients.sklik_client`` and https://api.sklik.cz/drak/ for methods.
Credentials: X-Sklik-Token (required).
"""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Body, Depends, Header, Path
from ..clients.sklik_client import SklikClient
from ..credentials import get_sklik_token
from ..errors import UpstreamError
router = APIRouter(prefix="/sklik", tags=["sklik"])
# Entities that expose createReport/readReport for the report helper.
_REPORT_ENTITIES = {
"campaigns",
"groups",
"ads",
"keywords",
"queries",
"sitelinks",
"productSets",
"banners",
}
_REPORT_EXAMPLE = [
{"dateFrom": "2026-06-01", "dateTo": "2026-06-18", "statGranularity": "daily"},
{"statGranularity": "daily"},
]
def _optional_user_id(
x_sklik_user_id: str | None = Header(
default=None,
alias="X-Sklik-User-Id",
description="Optional managed account id (userId) to act on behalf of, "
"for agency/MCC access.",
),
) -> int | None:
raw = (x_sklik_user_id or "").strip()
if not raw:
return None
try:
return int(raw)
except ValueError as exc:
raise UpstreamError(
"X-Sklik-User-Id must be an integer.", status=400
) from exc
@router.post("/login", summary="Verify the Sklik token (client.loginByToken)")
async def login(
token: str = Depends(get_sklik_token),
user_id: int | None = Depends(_optional_user_id),
) -> dict:
"""Check the token works. The session itself is internal and not returned."""
async with SklikClient(token, user_id=user_id) as client:
payload = await client.login()
return {
"valid": True,
"status": payload.get("status"),
"statusMessage": payload.get("statusMessage"),
}
@router.get("/limits", summary="API limits and quota (api.limits)")
async def limits(
token: str = Depends(get_sklik_token),
user_id: int | None = Depends(_optional_user_id),
) -> Any:
async with SklikClient(token, user_id=user_id) as client:
return await client.call("api.limits")
@router.post(
"/report/{entity}",
summary="Create and read a Sklik stats report (createReport + readReport)",
)
async def report(
entity: str = Path(
...,
description="Entity to report on: "
+ ", ".join(sorted(_REPORT_ENTITIES)),
),
body: list[Any] = Body(
...,
examples=[_REPORT_EXAMPLE],
description="Arguments for {entity}.createReport (restriction filter and "
"optional display options). The session is injected automatically.",
),
token: str = Depends(get_sklik_token),
user_id: int | None = Depends(_optional_user_id),
) -> Any:
if entity not in _REPORT_ENTITIES:
raise UpstreamError(
f"Unsupported report entity '{entity}'. Allowed: "
+ ", ".join(sorted(_REPORT_ENTITIES)),
status=400,
)
async with SklikClient(token, user_id=user_id) as client:
return await client.fetch_report(entity, body)
@router.post(
"/rpc/{method}",
summary="Generic authenticated Sklik call (any method)",
)
async def rpc(
method: str = Path(
...,
description="Sklik method name, e.g. campaigns.list, groups.list, "
"ads.list, api.limits. (client.loginByToken is managed by the proxy.)",
),
args: list[Any] = Body(
default=[],
description="Positional arguments AFTER the session struct (which the "
"proxy injects as the first argument). Example for campaigns.list: "
'[{"statuses": ["active"]}, {"displayColumns": ["id","name"]}]',
),
token: str = Depends(get_sklik_token),
user_id: int | None = Depends(_optional_user_id),
) -> Any:
async with SklikClient(token, user_id=user_id) as client:
return await client.call(method, args)