asa-api-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- asa_api_cli/__init__.py +15 -0
- asa_api_cli/ad_groups.py +270 -0
- asa_api_cli/auth.py +186 -0
- asa_api_cli/brand.py +809 -0
- asa_api_cli/campaigns.py +371 -0
- asa_api_cli/keywords.py +514 -0
- asa_api_cli/main.py +76 -0
- asa_api_cli/optimize.py +961 -0
- asa_api_cli/reports.py +440 -0
- asa_api_cli/utils.py +503 -0
- asa_api_cli-0.1.0.dist-info/METADATA +196 -0
- asa_api_cli-0.1.0.dist-info/RECORD +15 -0
- asa_api_cli-0.1.0.dist-info/WHEEL +4 -0
- asa_api_cli-0.1.0.dist-info/entry_points.txt +2 -0
- asa_api_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
asa_api_cli/campaigns.py
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""Campaign CLI commands."""
|
|
2
|
+
|
|
3
|
+
from datetime import date, timedelta
|
|
4
|
+
from typing import Annotated, Any
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from asa_api_client.exceptions import AppleSearchAdsError
|
|
8
|
+
from asa_api_client.models import CampaignStatus, CampaignUpdate, Money, Selector
|
|
9
|
+
|
|
10
|
+
from asa_api_cli.utils import (
|
|
11
|
+
OutputFormat,
|
|
12
|
+
confirm_action,
|
|
13
|
+
enum_value,
|
|
14
|
+
format_money,
|
|
15
|
+
get_client,
|
|
16
|
+
handle_api_error,
|
|
17
|
+
output_data,
|
|
18
|
+
print_json,
|
|
19
|
+
print_result_panel,
|
|
20
|
+
print_success,
|
|
21
|
+
print_warning,
|
|
22
|
+
spinner,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
app = typer.Typer(help="Manage campaigns")
|
|
26
|
+
|
|
27
|
+
CAMPAIGN_COLUMNS = [
|
|
28
|
+
"id",
|
|
29
|
+
"name",
|
|
30
|
+
"status",
|
|
31
|
+
"serving_status",
|
|
32
|
+
"daily_budget",
|
|
33
|
+
"countries",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
CAMPAIGN_COLUMNS_WITH_SPEND = [
|
|
37
|
+
"id",
|
|
38
|
+
"name",
|
|
39
|
+
"status",
|
|
40
|
+
"serving_status",
|
|
41
|
+
"daily_budget",
|
|
42
|
+
"spend_7d",
|
|
43
|
+
"countries",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
CAMPAIGN_COLUMN_LABELS = {
|
|
47
|
+
"id": "ID",
|
|
48
|
+
"serving_status": "Serving",
|
|
49
|
+
"daily_budget": "Daily Budget",
|
|
50
|
+
"spend_7d": "Spend (7d)",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _colorize_status(status: str) -> str:
|
|
55
|
+
"""Add color to status values."""
|
|
56
|
+
if status == "ENABLED":
|
|
57
|
+
return "[green]ENABLED[/green]"
|
|
58
|
+
elif status == "PAUSED":
|
|
59
|
+
return "[yellow]PAUSED[/yellow]"
|
|
60
|
+
return status
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _colorize_serving(serving: str) -> str:
|
|
64
|
+
"""Add color to serving status values."""
|
|
65
|
+
if serving == "RUNNING":
|
|
66
|
+
return "[green]RUNNING[/green]"
|
|
67
|
+
elif serving == "NOT_RUNNING":
|
|
68
|
+
return "[dim]NOT_RUNNING[/dim]"
|
|
69
|
+
return serving
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def campaign_to_dict(campaign: object, spend: str | None = None, colorize: bool = False) -> dict[str, Any]:
|
|
73
|
+
"""Convert campaign to display dictionary."""
|
|
74
|
+
status = enum_value(campaign.status) # type: ignore
|
|
75
|
+
serving_status = enum_value(campaign.serving_status) # type: ignore
|
|
76
|
+
|
|
77
|
+
if colorize:
|
|
78
|
+
status = _colorize_status(status)
|
|
79
|
+
serving_status = _colorize_serving(serving_status)
|
|
80
|
+
|
|
81
|
+
result = {
|
|
82
|
+
"id": campaign.id, # type: ignore
|
|
83
|
+
"name": campaign.name, # type: ignore
|
|
84
|
+
"status": status,
|
|
85
|
+
"serving_status": serving_status,
|
|
86
|
+
"daily_budget": format_money(
|
|
87
|
+
campaign.daily_budget_amount.amount if campaign.daily_budget_amount else None, # type: ignore
|
|
88
|
+
campaign.daily_budget_amount.currency if campaign.daily_budget_amount else None, # type: ignore
|
|
89
|
+
),
|
|
90
|
+
"countries": ", ".join(campaign.countries_or_regions[:3]) # type: ignore
|
|
91
|
+
+ ("..." if len(campaign.countries_or_regions) > 3 else ""), # type: ignore
|
|
92
|
+
}
|
|
93
|
+
if spend is not None:
|
|
94
|
+
result["spend_7d"] = spend
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@app.command("list")
|
|
99
|
+
def list_campaigns(
|
|
100
|
+
status: Annotated[
|
|
101
|
+
CampaignStatus | None,
|
|
102
|
+
typer.Option("--status", "-s", help="Filter by status (ENABLED or PAUSED)"),
|
|
103
|
+
] = None,
|
|
104
|
+
all_campaigns: Annotated[
|
|
105
|
+
bool,
|
|
106
|
+
typer.Option("--all", "-a", help="Show all campaigns including paused (default: enabled only)"),
|
|
107
|
+
] = False,
|
|
108
|
+
with_spend: Annotated[
|
|
109
|
+
bool,
|
|
110
|
+
typer.Option("--with-spend", "-w", help="Include 7-day spend (slower - requires report API)"),
|
|
111
|
+
] = False,
|
|
112
|
+
limit: Annotated[
|
|
113
|
+
int,
|
|
114
|
+
typer.Option("--limit", "-l", help="Maximum number of results"),
|
|
115
|
+
] = 100,
|
|
116
|
+
format: Annotated[
|
|
117
|
+
OutputFormat,
|
|
118
|
+
typer.Option("--format", "-f", help="Output format"),
|
|
119
|
+
] = OutputFormat.TABLE,
|
|
120
|
+
) -> None:
|
|
121
|
+
"""List campaigns.
|
|
122
|
+
|
|
123
|
+
By default, shows only ENABLED campaigns. Use --all to see paused campaigns too.
|
|
124
|
+
|
|
125
|
+
Examples:
|
|
126
|
+
asa campaigns list # Show enabled campaigns
|
|
127
|
+
asa campaigns list --all # Show all campaigns
|
|
128
|
+
asa campaigns list --status PAUSED # Show only paused campaigns
|
|
129
|
+
asa campaigns list --with-spend # Include 7-day spend data
|
|
130
|
+
asa campaigns list --format json
|
|
131
|
+
"""
|
|
132
|
+
client = get_client()
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
with client:
|
|
136
|
+
# Determine which status to filter by
|
|
137
|
+
filter_status = status
|
|
138
|
+
if filter_status is None and not all_campaigns:
|
|
139
|
+
filter_status = CampaignStatus.ENABLED
|
|
140
|
+
|
|
141
|
+
with spinner("Fetching campaigns..."):
|
|
142
|
+
if filter_status:
|
|
143
|
+
selector = Selector().where("status", "==", filter_status.value).limit(limit)
|
|
144
|
+
campaigns = client.campaigns.find(selector)
|
|
145
|
+
else:
|
|
146
|
+
campaigns = client.campaigns.list(limit=limit)
|
|
147
|
+
|
|
148
|
+
if not campaigns.data:
|
|
149
|
+
print_warning("No campaigns found")
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
# Get spend data if requested
|
|
153
|
+
spend_by_campaign: dict[int, str] = {}
|
|
154
|
+
if with_spend:
|
|
155
|
+
with spinner("Fetching 7-day spend data..."):
|
|
156
|
+
end = date.today()
|
|
157
|
+
start = end - timedelta(days=7)
|
|
158
|
+
report = client.reports.campaigns(start, end)
|
|
159
|
+
|
|
160
|
+
for row in report.row or []:
|
|
161
|
+
if row.metadata and row.metadata.campaign_id and row.total:
|
|
162
|
+
spend = row.total.local_spend
|
|
163
|
+
if spend:
|
|
164
|
+
spend_by_campaign[row.metadata.campaign_id] = f"{spend.amount} {spend.currency}"
|
|
165
|
+
|
|
166
|
+
# Use colors for table output
|
|
167
|
+
use_colors = format == OutputFormat.TABLE
|
|
168
|
+
|
|
169
|
+
data = [
|
|
170
|
+
campaign_to_dict(
|
|
171
|
+
c,
|
|
172
|
+
spend=spend_by_campaign.get(c.id),
|
|
173
|
+
colorize=use_colors,
|
|
174
|
+
)
|
|
175
|
+
for c in campaigns
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
# Choose columns based on whether spend is included
|
|
179
|
+
columns = CAMPAIGN_COLUMNS_WITH_SPEND if with_spend else CAMPAIGN_COLUMNS
|
|
180
|
+
|
|
181
|
+
# Build title
|
|
182
|
+
status_label = filter_status.value if filter_status else "all"
|
|
183
|
+
title = f"Campaigns ({campaigns.total_results} {status_label})"
|
|
184
|
+
|
|
185
|
+
output_data(
|
|
186
|
+
data,
|
|
187
|
+
columns,
|
|
188
|
+
format,
|
|
189
|
+
title=title,
|
|
190
|
+
column_labels=CAMPAIGN_COLUMN_LABELS,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
except AppleSearchAdsError as e:
|
|
194
|
+
handle_api_error(e)
|
|
195
|
+
raise typer.Exit(1) from None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@app.command("get")
|
|
199
|
+
def get_campaign(
|
|
200
|
+
campaign_id: Annotated[int, typer.Argument(help="Campaign ID")],
|
|
201
|
+
format: Annotated[
|
|
202
|
+
OutputFormat,
|
|
203
|
+
typer.Option("--format", "-f", help="Output format"),
|
|
204
|
+
] = OutputFormat.JSON,
|
|
205
|
+
) -> None:
|
|
206
|
+
"""Get details for a specific campaign.
|
|
207
|
+
|
|
208
|
+
Examples:
|
|
209
|
+
asa campaigns get 123456789
|
|
210
|
+
asa campaigns get 123456789 --format table
|
|
211
|
+
"""
|
|
212
|
+
client = get_client()
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
with client:
|
|
216
|
+
with spinner("Fetching campaign..."):
|
|
217
|
+
campaign = client.campaigns.get(campaign_id)
|
|
218
|
+
|
|
219
|
+
if format == OutputFormat.JSON:
|
|
220
|
+
print_json(campaign, title=f"Campaign {campaign_id}")
|
|
221
|
+
else:
|
|
222
|
+
data = [campaign_to_dict(campaign)]
|
|
223
|
+
output_data(data, CAMPAIGN_COLUMNS, format, column_labels=CAMPAIGN_COLUMN_LABELS)
|
|
224
|
+
|
|
225
|
+
except AppleSearchAdsError as e:
|
|
226
|
+
handle_api_error(e)
|
|
227
|
+
raise typer.Exit(1) from None
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@app.command("pause")
|
|
231
|
+
def pause_campaign(
|
|
232
|
+
campaign_id: Annotated[int, typer.Argument(help="Campaign ID to pause")],
|
|
233
|
+
) -> None:
|
|
234
|
+
"""Pause a campaign.
|
|
235
|
+
|
|
236
|
+
Examples:
|
|
237
|
+
asa campaigns pause 123456789
|
|
238
|
+
"""
|
|
239
|
+
client = get_client()
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
with client:
|
|
243
|
+
with spinner("Pausing campaign..."):
|
|
244
|
+
campaign = client.campaigns.update(
|
|
245
|
+
campaign_id,
|
|
246
|
+
data=CampaignUpdate(status=CampaignStatus.PAUSED),
|
|
247
|
+
)
|
|
248
|
+
print_success(f"Campaign '{campaign.name}' paused")
|
|
249
|
+
|
|
250
|
+
except AppleSearchAdsError as e:
|
|
251
|
+
handle_api_error(e)
|
|
252
|
+
raise typer.Exit(1) from None
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@app.command("enable")
|
|
256
|
+
def enable_campaign(
|
|
257
|
+
campaign_id: Annotated[int, typer.Argument(help="Campaign ID to enable")],
|
|
258
|
+
) -> None:
|
|
259
|
+
"""Enable a paused campaign.
|
|
260
|
+
|
|
261
|
+
Examples:
|
|
262
|
+
asa campaigns enable 123456789
|
|
263
|
+
"""
|
|
264
|
+
client = get_client()
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
with client:
|
|
268
|
+
with spinner("Enabling campaign..."):
|
|
269
|
+
campaign = client.campaigns.update(
|
|
270
|
+
campaign_id,
|
|
271
|
+
data=CampaignUpdate(status=CampaignStatus.ENABLED),
|
|
272
|
+
)
|
|
273
|
+
print_success(f"Campaign '{campaign.name}' enabled")
|
|
274
|
+
|
|
275
|
+
except AppleSearchAdsError as e:
|
|
276
|
+
handle_api_error(e)
|
|
277
|
+
raise typer.Exit(1) from None
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@app.command("set-budget")
|
|
281
|
+
def set_budget(
|
|
282
|
+
campaign_id: Annotated[int, typer.Argument(help="Campaign ID")],
|
|
283
|
+
daily_budget: Annotated[
|
|
284
|
+
float | None,
|
|
285
|
+
typer.Option("--daily", "-d", help="Daily budget amount"),
|
|
286
|
+
] = None,
|
|
287
|
+
total_budget: Annotated[
|
|
288
|
+
float | None,
|
|
289
|
+
typer.Option("--total", "-t", help="Total budget amount"),
|
|
290
|
+
] = None,
|
|
291
|
+
currency: Annotated[
|
|
292
|
+
str,
|
|
293
|
+
typer.Option("--currency", "-c", help="Currency code"),
|
|
294
|
+
] = "USD",
|
|
295
|
+
) -> None:
|
|
296
|
+
"""Update campaign budget.
|
|
297
|
+
|
|
298
|
+
Examples:
|
|
299
|
+
asa campaigns set-budget 123456789 --daily 100
|
|
300
|
+
asa campaigns set-budget 123456789 --daily 100 --total 10000
|
|
301
|
+
asa campaigns set-budget 123456789 --daily 100 --currency EUR
|
|
302
|
+
"""
|
|
303
|
+
if daily_budget is None and total_budget is None:
|
|
304
|
+
print_warning("Specify at least --daily or --total budget")
|
|
305
|
+
raise typer.Exit(1)
|
|
306
|
+
|
|
307
|
+
client = get_client()
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
with client:
|
|
311
|
+
update = CampaignUpdate()
|
|
312
|
+
if daily_budget is not None:
|
|
313
|
+
update.daily_budget_amount = Money(amount=str(daily_budget), currency=currency)
|
|
314
|
+
if total_budget is not None:
|
|
315
|
+
update.budget_amount = Money(amount=str(total_budget), currency=currency)
|
|
316
|
+
|
|
317
|
+
with spinner("Updating budget..."):
|
|
318
|
+
campaign = client.campaigns.update(campaign_id, data=update)
|
|
319
|
+
|
|
320
|
+
result_data = {"Campaign": campaign.name}
|
|
321
|
+
if campaign.daily_budget_amount:
|
|
322
|
+
amt = campaign.daily_budget_amount
|
|
323
|
+
result_data["Daily Budget"] = f"{amt.amount} {amt.currency}"
|
|
324
|
+
if campaign.budget_amount:
|
|
325
|
+
amt = campaign.budget_amount
|
|
326
|
+
result_data["Total Budget"] = f"{amt.amount} {amt.currency}"
|
|
327
|
+
|
|
328
|
+
print_result_panel("Budget Updated", result_data)
|
|
329
|
+
|
|
330
|
+
except AppleSearchAdsError as e:
|
|
331
|
+
handle_api_error(e)
|
|
332
|
+
raise typer.Exit(1) from None
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@app.command("delete")
|
|
336
|
+
def delete_campaign(
|
|
337
|
+
campaign_id: Annotated[int, typer.Argument(help="Campaign ID to delete")],
|
|
338
|
+
force: Annotated[
|
|
339
|
+
bool,
|
|
340
|
+
typer.Option("--force", "-f", help="Skip confirmation"),
|
|
341
|
+
] = False,
|
|
342
|
+
) -> None:
|
|
343
|
+
"""Delete a campaign.
|
|
344
|
+
|
|
345
|
+
WARNING: This action cannot be undone.
|
|
346
|
+
|
|
347
|
+
Examples:
|
|
348
|
+
asa campaigns delete 123456789
|
|
349
|
+
asa campaigns delete 123456789 --force
|
|
350
|
+
"""
|
|
351
|
+
client = get_client()
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
with client:
|
|
355
|
+
# Get campaign name for confirmation
|
|
356
|
+
with spinner("Fetching campaign..."):
|
|
357
|
+
campaign = client.campaigns.get(campaign_id)
|
|
358
|
+
|
|
359
|
+
if not force:
|
|
360
|
+
if not confirm_action(f"Are you sure you want to delete campaign '{campaign.name}'?"):
|
|
361
|
+
print_warning("Cancelled")
|
|
362
|
+
raise typer.Exit(0)
|
|
363
|
+
|
|
364
|
+
with spinner("Deleting campaign..."):
|
|
365
|
+
client.campaigns.delete(campaign_id)
|
|
366
|
+
|
|
367
|
+
print_success(f"Campaign '{campaign.name}' deleted")
|
|
368
|
+
|
|
369
|
+
except AppleSearchAdsError as e:
|
|
370
|
+
handle_api_error(e)
|
|
371
|
+
raise typer.Exit(1) from None
|