"""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)