rozsireni o keywords a uprava swaggeru
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
"""Request models for the Sklik write endpoints.
|
||||
|
||||
These exist for **discoverability**: without them Swagger shows the create/update
|
||||
bodies as a bare "array of objects" and nobody can tell what a campaign struct
|
||||
needs. Since these endpoints spend money, the fields, units and allowed values
|
||||
have to be visible in the UI.
|
||||
|
||||
Two rules make the models safe as a passthrough:
|
||||
|
||||
* ``extra="allow"`` - every model accepts fields it does not declare, so the
|
||||
less common parts of the Sklik API (regions, schedule, premise, retargeting
|
||||
settings, anything added upstream later) keep working without a code change.
|
||||
The declared fields document the common path; they do not fence it in.
|
||||
* ``exclude_unset`` when dumping - only the fields the caller actually sent are
|
||||
forwarded. This is what makes ``update`` a genuine partial update instead of
|
||||
silently resetting every omitted field to a default.
|
||||
|
||||
Field names match the Sklik API exactly (camelCase), so what you see in Swagger
|
||||
is what goes on the wire. Monetary amounts are in **halers** (100 = 1 Kc), the
|
||||
unit the Sklik API itself uses.
|
||||
|
||||
Reference: https://api.sklik.cz/drak/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class SklikStruct(BaseModel):
|
||||
"""Base: unknown fields are forwarded upstream untouched."""
|
||||
|
||||
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
||||
|
||||
def to_sklik(self) -> dict[str, Any]:
|
||||
"""Only the fields the caller actually set (declared or extra).
|
||||
|
||||
``mode="json"`` so enums become plain strings and nested structs plain
|
||||
dicts - exactly what goes on the wire.
|
||||
"""
|
||||
return self.model_dump(mode="json", exclude_unset=True, by_alias=True)
|
||||
|
||||
|
||||
# --- enums --------------------------------------------------------------------
|
||||
class Status(str, Enum):
|
||||
active = "active"
|
||||
suspend = "suspend"
|
||||
|
||||
|
||||
class CampaignType(str, Enum):
|
||||
fulltext = "fulltext"
|
||||
context = "context"
|
||||
product = "product"
|
||||
video = "video"
|
||||
simple = "simple"
|
||||
zbozi = "zbozi"
|
||||
|
||||
|
||||
class AdSelection(str, Enum):
|
||||
weighted = "weighted"
|
||||
random = "random"
|
||||
cpa = "cpa"
|
||||
cos = "cos"
|
||||
|
||||
|
||||
class PaymentMethod(str, Enum):
|
||||
cpc = "cpc"
|
||||
cpm = "cpm"
|
||||
|
||||
|
||||
class VideoFormat(str, Enum):
|
||||
both = "both"
|
||||
instream = "instream"
|
||||
outstream = "outstream"
|
||||
|
||||
|
||||
class AdType(str, Enum):
|
||||
eta = "eta"
|
||||
combined = "combined"
|
||||
branding = "branding"
|
||||
dynamicBanner = "dynamicBanner"
|
||||
|
||||
|
||||
class PremiseMode(str, Enum):
|
||||
disabled = "disabled"
|
||||
one = "one"
|
||||
nearest = "nearest"
|
||||
inherit = "inherit"
|
||||
|
||||
|
||||
# --- shared sub-structs -------------------------------------------------------
|
||||
class DevicesPriceRatio(SklikStruct):
|
||||
"""Per-device CPC/CPT modifier, in whole percent relative to the base bid."""
|
||||
|
||||
desktop: int | None = None
|
||||
mobile: int | None = None
|
||||
tablet: int | None = None
|
||||
other: int | None = None
|
||||
|
||||
|
||||
class NegativeKeyword(SklikStruct):
|
||||
name: str
|
||||
matchType: str | None = Field(
|
||||
default=None, description="Match type, e.g. 'broad', 'phrase', 'exact'."
|
||||
)
|
||||
|
||||
|
||||
# --- campaigns ----------------------------------------------------------------
|
||||
class CampaignCreate(SklikStruct):
|
||||
"""A campaign to create.
|
||||
|
||||
``status`` is deliberately absent: creation always forces ``suspend``.
|
||||
"""
|
||||
|
||||
name: str = Field(description="Campaign name.")
|
||||
type: CampaignType = Field(
|
||||
description="Campaign type. Cannot be changed later."
|
||||
)
|
||||
dayBudget: int = Field(
|
||||
description="Daily budget in HALERS (100 = 1 Kc). 20000 = 200 Kc/day.",
|
||||
ge=0,
|
||||
)
|
||||
totalBudget: int | None = Field(
|
||||
default=None, description="Total budget in halers, or null for none."
|
||||
)
|
||||
totalClicks: int | None = Field(
|
||||
default=None, description="Total click limit, or null for none."
|
||||
)
|
||||
adSelection: AdSelection | None = Field(
|
||||
default=None, description="How ads are rotated within a group."
|
||||
)
|
||||
startDate: str | None = Field(
|
||||
default=None, description="Start date, e.g. '2026-08-01'."
|
||||
)
|
||||
endDate: str | None = Field(default=None, description="End date.")
|
||||
paymentMethod: PaymentMethod | None = Field(
|
||||
default=None, description="cpc, or cpm for context campaigns."
|
||||
)
|
||||
videoFormat: VideoFormat | None = Field(
|
||||
default=None, description="Video campaigns only."
|
||||
)
|
||||
excludedSearchServices: list[Any] | None = Field(
|
||||
default=None, description="Search service ids to exclude."
|
||||
)
|
||||
excludedUrls: list[str] | None = Field(
|
||||
default=None, description="Excluded URLs, e.g. 'http://domain.com'."
|
||||
)
|
||||
negativeKeywords: list[NegativeKeyword] | None = None
|
||||
regions: list[Any] | None = Field(
|
||||
default=None, description="Geotargeting region ids."
|
||||
)
|
||||
schedule: list[Any] | None = Field(
|
||||
default=None, description="7-day spending schedule (Mon-Sun)."
|
||||
)
|
||||
premise: dict[str, Any] | None = Field(
|
||||
default=None, description="Firmy.cz connection settings."
|
||||
)
|
||||
devicesPriceRatio: DevicesPriceRatio | None = None
|
||||
|
||||
|
||||
class CampaignUpdate(SklikStruct):
|
||||
"""Fields to change on one campaign. Only what you send is changed.
|
||||
|
||||
``type`` is missing on purpose - Sklik cannot change a campaign's type.
|
||||
"""
|
||||
|
||||
id: int = Field(description="Id of the campaign to update. Required.")
|
||||
name: str | None = None
|
||||
status: Status | None = Field(
|
||||
default=None,
|
||||
description="'suspend' pauses the campaign, 'active' resumes it and "
|
||||
"spending starts. Omit to leave the current state untouched.",
|
||||
)
|
||||
dayBudget: int | None = Field(
|
||||
default=None, description="Daily budget in HALERS (100 = 1 Kc).", ge=0
|
||||
)
|
||||
totalBudget: int | None = Field(default=None, description="In halers, or null.")
|
||||
totalClicks: int | None = None
|
||||
sharedBudgetId: int | None = None
|
||||
resetExhaustedTotalBudget: bool | None = None
|
||||
resetExhaustedTotalClicks: bool | None = None
|
||||
adSelection: AdSelection | None = None
|
||||
startDate: str | None = None
|
||||
endDate: str | None = None
|
||||
paymentMethod: PaymentMethod | None = None
|
||||
videoFormat: VideoFormat | None = None
|
||||
zboziBiddingType: str | None = None
|
||||
excludedSearchServices: list[Any] | None = None
|
||||
excludedUrls: list[str] | None = None
|
||||
negativeKeywords: list[NegativeKeyword] | None = None
|
||||
regions: list[Any] | None = None
|
||||
schedule: list[Any] | None = None
|
||||
premise: dict[str, Any] | None = None
|
||||
devicesPriceRatio: DevicesPriceRatio | None = None
|
||||
|
||||
|
||||
# --- groups -------------------------------------------------------------------
|
||||
class GroupCreate(SklikStruct):
|
||||
"""A group (ad set) to create. ``status`` is forced to ``suspend``."""
|
||||
|
||||
campaignId: int = Field(description="Campaign this group belongs to.")
|
||||
name: str = Field(description="Group name.")
|
||||
cpc: int = Field(
|
||||
description="Default max cost per click in HALERS (100 = 1 Kc). "
|
||||
"300 = 3 Kc.",
|
||||
ge=0,
|
||||
)
|
||||
cpt: int | None = Field(
|
||||
default=None, description="Cost per thousand impressions, in halers."
|
||||
)
|
||||
maxUserDailyImpression: int | None = Field(
|
||||
default=None, description="Max impressions per user per day."
|
||||
)
|
||||
devicesPriceRatio: DevicesPriceRatio | None = None
|
||||
|
||||
|
||||
class GroupUpdate(SklikStruct):
|
||||
id: int = Field(description="Id of the group to update. Required.")
|
||||
name: str | None = None
|
||||
status: Status | None = Field(
|
||||
default=None, description="'suspend' pauses, 'active' resumes."
|
||||
)
|
||||
cpc: int | None = Field(default=None, description="Max CPC in halers.", ge=0)
|
||||
cpt: int | None = Field(default=None, description="CPT in halers.", ge=0)
|
||||
maxUserDailyImpression: int | None = None
|
||||
devicesPriceRatio: DevicesPriceRatio | None = None
|
||||
|
||||
|
||||
# --- ads ----------------------------------------------------------------------
|
||||
class _AdFields(SklikStruct):
|
||||
"""Fields shared by ad create and update."""
|
||||
|
||||
adType: AdType | None = Field(
|
||||
default=None, description="Defaults to 'eta' (expanded text ad)."
|
||||
)
|
||||
name: str | None = Field(
|
||||
default=None, description="Required for 'branding' ads."
|
||||
)
|
||||
headline1: str | None = Field(default=None, description="Required for 'eta'.")
|
||||
headline2: str | None = Field(default=None, description="Required for 'eta'.")
|
||||
headline3: str | None = Field(default=None, description="'eta' only.")
|
||||
path1: str | None = Field(default=None, description="'eta' only.")
|
||||
path2: str | None = Field(default=None, description="'eta' only; needs path1.")
|
||||
description: str | None = Field(
|
||||
default=None, description="Required for 'eta' and 'combined'."
|
||||
)
|
||||
description2: str | None = Field(default=None, description="'eta' only.")
|
||||
finalUrl: str | None = Field(
|
||||
default=None, description="Landing page. Required for eta/combined/branding."
|
||||
)
|
||||
mobileFinalUrl: str | None = None
|
||||
trackingTemplate: str | None = None
|
||||
impressionTrackingTemplate: str | None = None
|
||||
impressionTrackingTemplate2: str | None = None
|
||||
longLine: str | None = Field(default=None, description="Required for 'combined'.")
|
||||
shortLine: str | None = Field(default=None, description="Required for 'combined'.")
|
||||
companyName: str | None = Field(
|
||||
default=None, description="Required for 'combined'."
|
||||
)
|
||||
colorAccent: str | None = Field(
|
||||
default=None, description="Hex without '#'. 'combined' only."
|
||||
)
|
||||
colorMain: str | None = Field(
|
||||
default=None, description="Hex without '#'. 'combined' only."
|
||||
)
|
||||
imageId: int | None = None
|
||||
imageLogoId: int | None = None
|
||||
imageSquareId: int | None = None
|
||||
imageLandscapeLogoId: int | None = None
|
||||
premiseMode: PremiseMode | None = None
|
||||
premiseId: int | None = Field(
|
||||
default=None, description="Only with premiseMode='one'."
|
||||
)
|
||||
dynamicTemplateId: int | None = Field(
|
||||
default=None, description="'dynamicBanner' only."
|
||||
)
|
||||
schedule: list[Any] | None = Field(
|
||||
default=None, description="7-day schedule (Mon-Sun), or null."
|
||||
)
|
||||
|
||||
|
||||
class AdCreate(_AdFields):
|
||||
"""An ad to create. ``status`` is forced to ``suspend``."""
|
||||
|
||||
groupId: int = Field(description="Group the ad is placed in.")
|
||||
requestId: int | None = Field(
|
||||
default=None, description="Echoed back in diagnostics to match items up."
|
||||
)
|
||||
|
||||
|
||||
class MatchType(str, Enum):
|
||||
broad = "broad"
|
||||
phrase = "phrase"
|
||||
exact = "exact"
|
||||
|
||||
|
||||
# --- keywords -----------------------------------------------------------------
|
||||
class KeywordCreate(SklikStruct):
|
||||
"""A keyword to create.
|
||||
|
||||
Unlike campaigns/groups/ads, ``status`` IS settable here - see
|
||||
``app.sklik_guards`` for why.
|
||||
"""
|
||||
|
||||
name: str = Field(description="The keyword text.")
|
||||
groupId: int = Field(description="Group the keyword is created in.")
|
||||
matchType: MatchType | None = Field(
|
||||
default=None,
|
||||
description="Match type; Sklik defaults to 'broad'. Cannot be changed "
|
||||
"later - to change it, remove the keyword and create a new one.",
|
||||
)
|
||||
cpc: int | None = Field(
|
||||
default=None,
|
||||
description="Max cost per click in HALERS (100 = 1 Kc), or null to use "
|
||||
"the group's default.",
|
||||
ge=0,
|
||||
)
|
||||
url: str | None = Field(
|
||||
default=None, description="Target URL, or null for the ad's URL."
|
||||
)
|
||||
status: Status | None = Field(
|
||||
default=None,
|
||||
description="Sklik defaults to 'active'. A keyword in a paused campaign "
|
||||
"cannot spend anything, so this is left to you.",
|
||||
)
|
||||
|
||||
|
||||
class KeywordUpdate(SklikStruct):
|
||||
"""Fields to change on one keyword.
|
||||
|
||||
``name`` and ``matchType`` are **not** updatable in Sklik - to change either,
|
||||
remove the keyword and create a new one.
|
||||
"""
|
||||
|
||||
id: int = Field(description="Id of the keyword to update. Required.")
|
||||
cpc: int | None = Field(
|
||||
default=None,
|
||||
description="Max CPC in halers, or null to fall back to the group's.",
|
||||
ge=0,
|
||||
)
|
||||
url: str | None = Field(default=None, description="Target URL, or null to unset.")
|
||||
status: Status | None = Field(
|
||||
default=None, description="'suspend' pauses the keyword, 'active' resumes."
|
||||
)
|
||||
|
||||
|
||||
class AdUpdate(_AdFields):
|
||||
"""Fields to change on one ad.
|
||||
|
||||
Sklik cannot edit an existing ad's creative: changing headlines, description
|
||||
or URLs makes it **delete the old ad and create a new one with a new id**.
|
||||
Changing only ``status`` keeps the id.
|
||||
"""
|
||||
|
||||
id: int = Field(description="Id of the ad to update. Required.")
|
||||
status: Status | None = Field(
|
||||
default=None, description="'suspend' pauses, 'active' resumes."
|
||||
)
|
||||
Reference in New Issue
Block a user