"""Pydantic v2 request/response models for the 拼团 (group-buy) funnel query API. Field names match the API contract in docs/02 §5 (v3) exactly: request: period, snapshot_dt (optional) response: period, snapshot_dt, results[].step_index, results[].name, results[].event_key, results[].uv, results[].conversion_rate, results[].dropoff_rate, data_status """ from __future__ import annotations from datetime import date from enum import Enum from pydantic import BaseModel, Field class Period(str, Enum): """Supported periods (docs/02 §5 v3). Routing: day -> table ads_trd_group_funnel_daily (single day, full history). last_7d -> table ads_trd_group_funnel_rolling, columns uv_*_7d. last_30d -> table ads_trd_group_funnel_rolling, columns uv_*_30d. Any other value is rejected with HTTP 422. """ day = "day" last_7d = "last_7d" last_30d = "last_30d" class FunnelQueryRequest(BaseModel): """Funnel query request body. ``snapshot_dt`` (ISO ``YYYY-MM-DD``) is optional and only meaningful for ``period=day``: omitted -> latest daily row; given -> that historical day (must be <= yesterday). For ``last_7d`` / ``last_30d`` it is ignored. """ period: Period snapshot_dt: date | None = Field( None, description=( "Optional ISO date (YYYY-MM-DD). Only meaningful for period=day; " "ignored for last_7d/last_30d. Must be <= yesterday." ), ) class DataStatus(str, Enum): """Data completeness status for a query result (docs/02 §5 v3). ready - the target row exists and the period's columns are non-null. missing - no target row, or the period columns are NULL. Never zero-filled. """ ready = "ready" missing = "missing" class FunnelStepResult(BaseModel): """Computed UV and conversion metrics for one fixed step.""" step_index: int = Field(..., ge=1, description="1-based step index") name: str = Field(..., description="Chinese display name of the step") event_key: str = Field(..., description="Stable step key (start/show/...)") uv: int = Field(..., ge=0) conversion_rate: float | None = Field( None, description="uv[i] / uv[i-1]; null for step 1 or when uv[i-1] == 0" ) dropoff_rate: float | None = Field( None, description="1 - conversion_rate; null when conversion_rate is null" ) class FunnelQueryResponse(BaseModel): """Funnel query response body.""" period: Period snapshot_dt: str | None = Field( None, description=( "dt (yyyyMMdd) of the row actually used; for day = that day, for " "7d/30d = the rolling row's as-of dt. null when missing." ), ) results: list[FunnelStepResult] data_status: DataStatus # --------------------------------------------------------------------------- # # Trend (按日折线) — daily time series over ads_trd_group_funnel_daily # # --------------------------------------------------------------------------- # class FunnelTrendRequest(BaseModel): """Trend query request body. Both bounds optional (ISO ``YYYY-MM-DD``). Omitted -> the 30 most recent available days ending at the latest daily ``dt``. ``end_dt`` must be <= yesterday (T+1); ``start_dt`` must be <= ``end_dt``. """ start_dt: date | None = Field( None, description="Inclusive start (YYYY-MM-DD). Default: end - 29 days." ) end_dt: date | None = Field( None, description="Inclusive end (YYYY-MM-DD). Default: latest available day. <= yesterday.", ) class FunnelTrendPoint(BaseModel): """One day in the trend series: its ``dt`` + the full per-step funnel. Reuses :class:`FunnelStepResult` so uv + conversion_rate match the funnel query口径 exactly (computed by the same server code). """ dt: str = Field(..., description="Day as yyyyMMdd") results: list[FunnelStepResult] class FunnelTrendResponse(BaseModel): """Trend query response body. ``points`` is ascending by ``dt``, with leading all-zero (pre-launch) days trimmed. ``start_dt`` / ``end_dt`` are the bounds of the returned series (yyyyMMdd) — i.e. the first kept day and the last available day — null when ``missing``. Calendar gaps inside the range are simply absent points (the client breaks the line; no zero-fill). """ start_dt: str | None = Field(None, description="First kept day (yyyyMMdd); null when missing.") end_dt: str | None = Field(None, description="Last day (yyyyMMdd); null when missing.") data_status: DataStatus points: list[FunnelTrendPoint]