rozsireni o keywords a uprava swaggeru
This commit is contained in:
+143
-26
@@ -30,6 +30,17 @@ from ..clients.sklik_client import SklikClient
|
||||
from ..credentials import get_sklik_token
|
||||
from ..errors import UpstreamError
|
||||
from ..logging_config import get_logger
|
||||
from ..sklik_models import (
|
||||
AdCreate,
|
||||
AdUpdate,
|
||||
CampaignCreate,
|
||||
CampaignUpdate,
|
||||
GroupCreate,
|
||||
GroupUpdate,
|
||||
KeywordCreate,
|
||||
KeywordUpdate,
|
||||
SklikStruct,
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -80,6 +91,11 @@ _DEFAULT_COLUMNS = {
|
||||
"description", "description2", "finalUrl", "group.id", "group.name",
|
||||
"campaign.id", "campaign.name", "createDate", "deleted",
|
||||
],
|
||||
"keywords": [
|
||||
"id", "name", "status", "matchType", "cpc", "url", "disabled",
|
||||
"group.id", "group.name", "campaign.id", "campaign.name",
|
||||
"createDate", "deleted",
|
||||
],
|
||||
}
|
||||
|
||||
_CAMPAIGN_CREATE_EXAMPLE = [
|
||||
@@ -110,6 +126,12 @@ _CAMPAIGN_UPDATE_EXAMPLE = [{"id": 123456, "dayBudget": 30000, "status": "suspen
|
||||
_GROUP_UPDATE_EXAMPLE = [{"id": 654321, "cpc": 450}]
|
||||
_AD_UPDATE_EXAMPLE = [{"id": 987654, "status": "suspend"}]
|
||||
|
||||
_KEYWORD_CREATE_EXAMPLE = [
|
||||
{"groupId": 654321, "name": "levne boty", "matchType": "phrase", "cpc": 300},
|
||||
{"groupId": 654321, "name": "damske boty", "matchType": "exact"},
|
||||
]
|
||||
_KEYWORD_UPDATE_EXAMPLE = [{"id": 555001, "cpc": 400}]
|
||||
|
||||
|
||||
def _optional_user_id(
|
||||
x_sklik_user_id: str | None = Header(
|
||||
@@ -204,14 +226,19 @@ async def _mutate(
|
||||
return payload
|
||||
|
||||
|
||||
def _structs(items: list[SklikStruct]) -> list[dict[str, Any]]:
|
||||
"""Model -> Sklik struct, keeping only the fields the caller actually sent."""
|
||||
return [item.to_sklik() for item in items]
|
||||
|
||||
|
||||
async def _create(
|
||||
entity: str,
|
||||
items: list[Any],
|
||||
items: list[SklikStruct],
|
||||
token: str,
|
||||
user_id: int | None,
|
||||
idem_key: str | None,
|
||||
) -> Any:
|
||||
prepared = sklik_guards.prepare_for_create(entity, items)
|
||||
prepared = sklik_guards.prepare_for_create(entity, _structs(items))
|
||||
return await _mutate(
|
||||
f"{entity}.create",
|
||||
[prepared],
|
||||
@@ -231,12 +258,12 @@ async def _create(
|
||||
|
||||
async def _update(
|
||||
entity: str,
|
||||
items: list[Any],
|
||||
items: list[SklikStruct],
|
||||
token: str,
|
||||
user_id: int | None,
|
||||
idem_key: str | None,
|
||||
) -> Any:
|
||||
prepared = sklik_guards.prepare_for_update(entity, items)
|
||||
prepared = sklik_guards.prepare_for_update(entity, _structs(items))
|
||||
return await _mutate(
|
||||
f"{entity}.update",
|
||||
[prepared],
|
||||
@@ -411,6 +438,42 @@ async def list_ads(
|
||||
return _strip_session(await client.fetch_list("ads", restriction, columns))
|
||||
|
||||
|
||||
@router.get("/keywords", summary="List keywords (keywords.list)")
|
||||
async def list_keywords(
|
||||
campaign_ids: str | None = Query(
|
||||
None, description="Comma-separated campaign ids to list keywords from."
|
||||
),
|
||||
group_ids: str | None = Query(
|
||||
None, description="Comma-separated group ids to list keywords from."
|
||||
),
|
||||
ids: str | None = Query(None, description="Comma-separated keyword ids."),
|
||||
is_deleted: bool | None = Query(None),
|
||||
display_columns: str | None = Query(None),
|
||||
token: str = Depends(get_sklik_token),
|
||||
user_id: int | None = Depends(_optional_user_id),
|
||||
) -> Any:
|
||||
restriction: dict[str, Any] = {}
|
||||
parsed = _id_list(ids, "ids")
|
||||
if parsed:
|
||||
restriction["ids"] = parsed
|
||||
campaigns = _id_list(campaign_ids, "campaign_ids")
|
||||
if campaigns:
|
||||
restriction["campaign"] = {"ids": campaigns}
|
||||
groups = _id_list(group_ids, "group_ids")
|
||||
if groups:
|
||||
restriction["group"] = {"ids": groups}
|
||||
if is_deleted is not None:
|
||||
restriction["isDeleted"] = is_deleted
|
||||
|
||||
columns = (
|
||||
[c.strip() for c in display_columns.split(",") if c.strip()]
|
||||
if display_columns
|
||||
else _DEFAULT_COLUMNS["keywords"]
|
||||
)
|
||||
async with SklikClient(token, user_id=user_id) as client:
|
||||
return _strip_session(await client.fetch_list("keywords", restriction, columns))
|
||||
|
||||
|
||||
# --- Read: statistics ---------------------------------------------------------
|
||||
@router.post(
|
||||
"/report/{entity}",
|
||||
@@ -444,12 +507,13 @@ async def report(
|
||||
# --- Write: creation (always paused) ------------------------------------------
|
||||
@router.post("/campaigns", summary="Create campaigns — always PAUSED")
|
||||
async def create_campaigns(
|
||||
body: list[dict[str, Any]] = Body(
|
||||
body: list[CampaignCreate] = Body(
|
||||
...,
|
||||
examples=[_CAMPAIGN_CREATE_EXAMPLE],
|
||||
description="Array of campaign structs. 'status' is ignored and forced "
|
||||
"to 'suspend'. Budgets are in halers (100 = 1 Kc). Required per item: "
|
||||
"name, type, dayBudget. Sklik batches are all-or-nothing.",
|
||||
description="Campaigns to create. Budgets are in halers (100 = 1 Kc). "
|
||||
"'status' cannot be set - creation always forces 'suspend'. Fields not "
|
||||
"listed in the schema are forwarded to Sklik as-is. Sklik batches are "
|
||||
"all-or-nothing.",
|
||||
),
|
||||
token: str = Depends(get_sklik_token),
|
||||
user_id: int | None = Depends(_optional_user_id),
|
||||
@@ -466,11 +530,11 @@ async def create_campaigns(
|
||||
|
||||
@router.post("/groups", summary="Create groups/ad sets — always PAUSED")
|
||||
async def create_groups(
|
||||
body: list[dict[str, Any]] = Body(
|
||||
body: list[GroupCreate] = Body(
|
||||
...,
|
||||
examples=[_GROUP_CREATE_EXAMPLE],
|
||||
description="Array of group structs. 'status' is forced to 'suspend'. "
|
||||
"cpc is in halers. Required per item: campaignId, name, cpc.",
|
||||
description="Groups to create. cpc is in halers (100 = 1 Kc). 'status' "
|
||||
"cannot be set - creation always forces 'suspend'.",
|
||||
),
|
||||
token: str = Depends(get_sklik_token),
|
||||
user_id: int | None = Depends(_optional_user_id),
|
||||
@@ -481,12 +545,12 @@ async def create_groups(
|
||||
|
||||
@router.post("/ads", summary="Create ads — always PAUSED")
|
||||
async def create_ads(
|
||||
body: list[dict[str, Any]] = Body(
|
||||
body: list[AdCreate] = Body(
|
||||
...,
|
||||
examples=[_AD_CREATE_EXAMPLE],
|
||||
description="Array of ad structs. 'status' is forced to 'suspend'. "
|
||||
"Required per item: groupId (plus headline1/headline2/description/"
|
||||
"finalUrl for the default 'eta' ad type).",
|
||||
description="Ads to create. 'status' cannot be set - creation always "
|
||||
"forces 'suspend'. For the default 'eta' type Sklik also requires "
|
||||
"headline1, headline2, description and finalUrl.",
|
||||
),
|
||||
token: str = Depends(get_sklik_token),
|
||||
user_id: int | None = Depends(_optional_user_id),
|
||||
@@ -495,18 +559,36 @@ async def create_ads(
|
||||
return await _create("ads", body, token, user_id, idem_key)
|
||||
|
||||
|
||||
@router.post("/keywords", summary="Create keywords")
|
||||
async def create_keywords(
|
||||
body: list[KeywordCreate] = Body(
|
||||
...,
|
||||
examples=[_KEYWORD_CREATE_EXAMPLE],
|
||||
description="Keywords to create. cpc is in halers (100 = 1 Kc); omit it "
|
||||
"to use the group's default. Unlike campaigns/groups/ads, 'status' is "
|
||||
"NOT forced here - a keyword cannot spend anything on its own, since "
|
||||
"the campaign, group and ad above it are all created paused. Sklik "
|
||||
"defaults it to 'active'. Returns positiveKeywordIds.",
|
||||
),
|
||||
token: str = Depends(get_sklik_token),
|
||||
user_id: int | None = Depends(_optional_user_id),
|
||||
idem_key: str | None = Depends(_idempotency_key),
|
||||
) -> Any:
|
||||
return await _create("keywords", body, token, user_id, idem_key)
|
||||
|
||||
|
||||
# --- Write: update ------------------------------------------------------------
|
||||
# Update is ordinary CRUD: only the supplied fields change and 'status' is NOT
|
||||
# forced, because changing it is how a campaign is paused or resumed. Set
|
||||
# SKLIK_BLOCK_ACTIVATION=true to refuse status="active" here as well.
|
||||
@router.put("/campaigns", summary="Update campaigns (partial, by id)")
|
||||
async def update_campaigns(
|
||||
body: list[dict[str, Any]] = Body(
|
||||
body: list[CampaignUpdate] = Body(
|
||||
...,
|
||||
examples=[_CAMPAIGN_UPDATE_EXAMPLE],
|
||||
description="Array of campaign structs. 'id' is required per item; every "
|
||||
"other field is optional and only the supplied ones change. 'status' "
|
||||
"accepts 'active' or 'suspend'. Budgets are in halers. 'type' cannot be "
|
||||
description="'id' is required per item; every other field is optional "
|
||||
"and ONLY the fields you send are changed. status='active' resumes the "
|
||||
"campaign and spending starts. Budgets are in halers. 'type' cannot be "
|
||||
"changed.",
|
||||
),
|
||||
token: str = Depends(get_sklik_token),
|
||||
@@ -518,11 +600,10 @@ async def update_campaigns(
|
||||
|
||||
@router.put("/groups", summary="Update groups/ad sets (partial, by id)")
|
||||
async def update_groups(
|
||||
body: list[dict[str, Any]] = Body(
|
||||
body: list[GroupUpdate] = Body(
|
||||
...,
|
||||
examples=[_GROUP_UPDATE_EXAMPLE],
|
||||
description="Array of group structs. 'id' required; name, status, cpc, "
|
||||
"cpt, maxUserDailyImpression and devicesPriceRatio are updatable.",
|
||||
description="'id' required; only the fields you send are changed.",
|
||||
),
|
||||
token: str = Depends(get_sklik_token),
|
||||
user_id: int | None = Depends(_optional_user_id),
|
||||
@@ -533,12 +614,12 @@ async def update_groups(
|
||||
|
||||
@router.put("/ads", summary="Update ads (partial, by id)")
|
||||
async def update_ads(
|
||||
body: list[dict[str, Any]] = Body(
|
||||
body: list[AdUpdate] = Body(
|
||||
...,
|
||||
examples=[_AD_UPDATE_EXAMPLE],
|
||||
description="Array of ad structs. 'id' required. NOTE: changing the "
|
||||
"creative (headlines, description, URLs) makes Sklik delete the old ad "
|
||||
"and create a new one with a NEW id - re-read the ad afterwards.",
|
||||
description="'id' required; only the fields you send are changed. NOTE: "
|
||||
"changing the creative (headlines, description, URLs) makes Sklik delete "
|
||||
"the old ad and create a new one with a NEW id - re-read it afterwards.",
|
||||
),
|
||||
token: str = Depends(get_sklik_token),
|
||||
user_id: int | None = Depends(_optional_user_id),
|
||||
@@ -547,6 +628,22 @@ async def update_ads(
|
||||
return await _update("ads", body, token, user_id, idem_key)
|
||||
|
||||
|
||||
@router.put("/keywords", summary="Update keywords (partial, by id)")
|
||||
async def update_keywords(
|
||||
body: list[KeywordUpdate] = Body(
|
||||
...,
|
||||
examples=[_KEYWORD_UPDATE_EXAMPLE],
|
||||
description="'id' required; only cpc, url and status can be changed. "
|
||||
"The keyword text and matchType are immutable in Sklik - remove the "
|
||||
"keyword and create a new one instead.",
|
||||
),
|
||||
token: str = Depends(get_sklik_token),
|
||||
user_id: int | None = Depends(_optional_user_id),
|
||||
idem_key: str | None = Depends(_idempotency_key),
|
||||
) -> Any:
|
||||
return await _update("keywords", body, token, user_id, idem_key)
|
||||
|
||||
|
||||
# --- Write: remove / restore --------------------------------------------------
|
||||
# Sklik removal is a soft delete ("marked as removed"), so every remove has a
|
||||
# matching restore.
|
||||
@@ -582,6 +679,26 @@ async def remove_ads(
|
||||
return await _remove_or_restore("ads", "remove", ids, token, user_id, idem_key)
|
||||
|
||||
|
||||
@router.delete("/keywords", summary="Remove keywords (reversible)")
|
||||
async def remove_keywords(
|
||||
ids: str = Query(..., description="Comma-separated keyword ids to remove."),
|
||||
token: str = Depends(get_sklik_token),
|
||||
user_id: int | None = Depends(_optional_user_id),
|
||||
idem_key: str | None = Depends(_idempotency_key),
|
||||
) -> Any:
|
||||
return await _remove_or_restore("keywords", "remove", ids, token, user_id, idem_key)
|
||||
|
||||
|
||||
@router.post("/keywords/restore", summary="Restore removed keywords")
|
||||
async def restore_keywords(
|
||||
ids: str = Query(..., description="Comma-separated keyword ids to restore."),
|
||||
token: str = Depends(get_sklik_token),
|
||||
user_id: int | None = Depends(_optional_user_id),
|
||||
idem_key: str | None = Depends(_idempotency_key),
|
||||
) -> Any:
|
||||
return await _remove_or_restore("keywords", "restore", ids, token, user_id, idem_key)
|
||||
|
||||
|
||||
@router.post("/campaigns/restore", summary="Restore removed campaigns")
|
||||
async def restore_campaigns(
|
||||
ids: str = Query(..., description="Comma-separated campaign ids to restore."),
|
||||
|
||||
Reference in New Issue
Block a user