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/keywords.py
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
"""Keyword CLI commands."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Any
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from asa_api_client.exceptions import AppleSearchAdsError
|
|
7
|
+
from asa_api_client.models import (
|
|
8
|
+
KeywordCreate,
|
|
9
|
+
KeywordMatchType,
|
|
10
|
+
KeywordStatus,
|
|
11
|
+
KeywordUpdate,
|
|
12
|
+
Money,
|
|
13
|
+
NegativeKeywordCreate,
|
|
14
|
+
Selector,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from asa_api_cli.utils import (
|
|
18
|
+
OutputFormat,
|
|
19
|
+
confirm_action,
|
|
20
|
+
enum_value,
|
|
21
|
+
format_money,
|
|
22
|
+
get_client,
|
|
23
|
+
handle_api_error,
|
|
24
|
+
output_data,
|
|
25
|
+
print_json,
|
|
26
|
+
print_result_panel,
|
|
27
|
+
print_success,
|
|
28
|
+
print_warning,
|
|
29
|
+
spinner,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
app = typer.Typer(help="Manage keywords")
|
|
33
|
+
|
|
34
|
+
KEYWORD_COLUMNS = [
|
|
35
|
+
"id",
|
|
36
|
+
"text",
|
|
37
|
+
"match_type",
|
|
38
|
+
"status",
|
|
39
|
+
"bid",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
KEYWORD_COLUMN_LABELS = {
|
|
43
|
+
"id": "ID",
|
|
44
|
+
"match_type": "Match Type",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def keyword_to_dict(keyword: object) -> dict[str, Any]:
|
|
49
|
+
"""Convert keyword to display dictionary."""
|
|
50
|
+
return {
|
|
51
|
+
"id": keyword.id, # type: ignore
|
|
52
|
+
"text": keyword.text, # type: ignore
|
|
53
|
+
"match_type": enum_value(keyword.match_type), # type: ignore
|
|
54
|
+
"status": enum_value(keyword.status), # type: ignore
|
|
55
|
+
"bid": format_money(
|
|
56
|
+
keyword.bid_amount.amount if keyword.bid_amount else None, # type: ignore
|
|
57
|
+
keyword.bid_amount.currency if keyword.bid_amount else None, # type: ignore
|
|
58
|
+
),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.command("list")
|
|
63
|
+
def list_keywords(
|
|
64
|
+
campaign_id: Annotated[int, typer.Argument(help="Campaign ID")],
|
|
65
|
+
ad_group_id: Annotated[int, typer.Argument(help="Ad Group ID")],
|
|
66
|
+
status: Annotated[
|
|
67
|
+
KeywordStatus | None,
|
|
68
|
+
typer.Option("--status", "-s", help="Filter by status"),
|
|
69
|
+
] = None,
|
|
70
|
+
match_type: Annotated[
|
|
71
|
+
KeywordMatchType | None,
|
|
72
|
+
typer.Option("--match-type", "-m", help="Filter by match type"),
|
|
73
|
+
] = None,
|
|
74
|
+
limit: Annotated[
|
|
75
|
+
int,
|
|
76
|
+
typer.Option("--limit", "-l", help="Maximum number of results"),
|
|
77
|
+
] = 100,
|
|
78
|
+
format: Annotated[
|
|
79
|
+
OutputFormat,
|
|
80
|
+
typer.Option("--format", "-f", help="Output format"),
|
|
81
|
+
] = OutputFormat.TABLE,
|
|
82
|
+
) -> None:
|
|
83
|
+
"""List targeting keywords in an ad group.
|
|
84
|
+
|
|
85
|
+
Examples:
|
|
86
|
+
asa keywords list 123 456
|
|
87
|
+
asa keywords list 123 456 --status ACTIVE
|
|
88
|
+
asa keywords list 123 456 --match-type EXACT
|
|
89
|
+
asa keywords list 123 456 --format json
|
|
90
|
+
"""
|
|
91
|
+
client = get_client()
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
with client:
|
|
95
|
+
with spinner("Fetching keywords..."):
|
|
96
|
+
selector = Selector().limit(limit)
|
|
97
|
+
|
|
98
|
+
if status:
|
|
99
|
+
selector = selector.where("status", "==", status.value)
|
|
100
|
+
if match_type:
|
|
101
|
+
selector = selector.where("matchType", "==", match_type.value)
|
|
102
|
+
|
|
103
|
+
if selector.conditions:
|
|
104
|
+
keywords = client.campaigns(campaign_id).ad_groups(ad_group_id).keywords.find(selector)
|
|
105
|
+
else:
|
|
106
|
+
keywords = client.campaigns(campaign_id).ad_groups(ad_group_id).keywords.list(limit=limit)
|
|
107
|
+
|
|
108
|
+
if not keywords.data:
|
|
109
|
+
print_warning("No keywords found")
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
data = [keyword_to_dict(k) for k in keywords]
|
|
113
|
+
output_data(
|
|
114
|
+
data,
|
|
115
|
+
KEYWORD_COLUMNS,
|
|
116
|
+
format,
|
|
117
|
+
title=f"Keywords ({keywords.total_results} total)",
|
|
118
|
+
column_labels=KEYWORD_COLUMN_LABELS,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
except AppleSearchAdsError as e:
|
|
122
|
+
handle_api_error(e)
|
|
123
|
+
raise typer.Exit(1) from None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@app.command("get")
|
|
127
|
+
def get_keyword(
|
|
128
|
+
campaign_id: Annotated[int, typer.Argument(help="Campaign ID")],
|
|
129
|
+
ad_group_id: Annotated[int, typer.Argument(help="Ad Group ID")],
|
|
130
|
+
keyword_id: Annotated[int, typer.Argument(help="Keyword ID")],
|
|
131
|
+
format: Annotated[
|
|
132
|
+
OutputFormat,
|
|
133
|
+
typer.Option("--format", "-f", help="Output format"),
|
|
134
|
+
] = OutputFormat.JSON,
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Get details for a specific keyword.
|
|
137
|
+
|
|
138
|
+
Examples:
|
|
139
|
+
asa keywords get 123 456 789
|
|
140
|
+
"""
|
|
141
|
+
client = get_client()
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
with client:
|
|
145
|
+
with spinner("Fetching keyword..."):
|
|
146
|
+
keyword = client.campaigns(campaign_id).ad_groups(ad_group_id).keywords.get(keyword_id)
|
|
147
|
+
|
|
148
|
+
if format == OutputFormat.JSON:
|
|
149
|
+
print_json(keyword, title=f"Keyword {keyword_id}")
|
|
150
|
+
else:
|
|
151
|
+
data = [keyword_to_dict(keyword)]
|
|
152
|
+
output_data(data, KEYWORD_COLUMNS, format, column_labels=KEYWORD_COLUMN_LABELS)
|
|
153
|
+
|
|
154
|
+
except AppleSearchAdsError as e:
|
|
155
|
+
handle_api_error(e)
|
|
156
|
+
raise typer.Exit(1) from None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@app.command("add")
|
|
160
|
+
def add_keyword(
|
|
161
|
+
campaign_id: Annotated[int, typer.Argument(help="Campaign ID")],
|
|
162
|
+
ad_group_id: Annotated[int, typer.Argument(help="Ad Group ID")],
|
|
163
|
+
text: Annotated[str, typer.Argument(help="Keyword text")],
|
|
164
|
+
match_type: Annotated[
|
|
165
|
+
KeywordMatchType,
|
|
166
|
+
typer.Option("--match-type", "-m", help="Match type"),
|
|
167
|
+
] = KeywordMatchType.EXACT,
|
|
168
|
+
bid: Annotated[
|
|
169
|
+
float | None,
|
|
170
|
+
typer.Option("--bid", "-b", help="Bid amount (uses ad group default if not specified)"),
|
|
171
|
+
] = None,
|
|
172
|
+
currency: Annotated[
|
|
173
|
+
str,
|
|
174
|
+
typer.Option("--currency", "-c", help="Currency code"),
|
|
175
|
+
] = "USD",
|
|
176
|
+
) -> None:
|
|
177
|
+
"""Add a targeting keyword.
|
|
178
|
+
|
|
179
|
+
Examples:
|
|
180
|
+
asa keywords add 123 456 "productivity app"
|
|
181
|
+
asa keywords add 123 456 "todo list" --match-type BROAD
|
|
182
|
+
asa keywords add 123 456 "task manager" --bid 2.50
|
|
183
|
+
"""
|
|
184
|
+
client = get_client()
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
with client:
|
|
188
|
+
create_data = KeywordCreate(
|
|
189
|
+
text=text,
|
|
190
|
+
match_type=match_type,
|
|
191
|
+
)
|
|
192
|
+
if bid is not None:
|
|
193
|
+
create_data.bid_amount = Money(amount=str(bid), currency=currency)
|
|
194
|
+
|
|
195
|
+
with spinner("Adding keyword..."):
|
|
196
|
+
keyword = client.campaigns(campaign_id).ad_groups(ad_group_id).keywords.create(create_data)
|
|
197
|
+
|
|
198
|
+
print_result_panel(
|
|
199
|
+
"Keyword Added",
|
|
200
|
+
{
|
|
201
|
+
"ID": str(keyword.id),
|
|
202
|
+
"Text": keyword.text,
|
|
203
|
+
"Match Type": keyword.match_type.value,
|
|
204
|
+
},
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
except AppleSearchAdsError as e:
|
|
208
|
+
handle_api_error(e)
|
|
209
|
+
raise typer.Exit(1) from None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@app.command("pause")
|
|
213
|
+
def pause_keyword(
|
|
214
|
+
campaign_id: Annotated[int, typer.Argument(help="Campaign ID")],
|
|
215
|
+
ad_group_id: Annotated[int, typer.Argument(help="Ad Group ID")],
|
|
216
|
+
keyword_id: Annotated[int, typer.Argument(help="Keyword ID to pause")],
|
|
217
|
+
) -> None:
|
|
218
|
+
"""Pause a keyword.
|
|
219
|
+
|
|
220
|
+
Examples:
|
|
221
|
+
asa keywords pause 123 456 789
|
|
222
|
+
"""
|
|
223
|
+
client = get_client()
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
with client:
|
|
227
|
+
with spinner("Pausing keyword..."):
|
|
228
|
+
keyword = (
|
|
229
|
+
client.campaigns(campaign_id)
|
|
230
|
+
.ad_groups(ad_group_id)
|
|
231
|
+
.keywords.update(keyword_id, data=KeywordUpdate(status=KeywordStatus.PAUSED))
|
|
232
|
+
)
|
|
233
|
+
print_success(f"Keyword '{keyword.text}' paused")
|
|
234
|
+
|
|
235
|
+
except AppleSearchAdsError as e:
|
|
236
|
+
handle_api_error(e)
|
|
237
|
+
raise typer.Exit(1) from None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@app.command("enable")
|
|
241
|
+
def enable_keyword(
|
|
242
|
+
campaign_id: Annotated[int, typer.Argument(help="Campaign ID")],
|
|
243
|
+
ad_group_id: Annotated[int, typer.Argument(help="Ad Group ID")],
|
|
244
|
+
keyword_id: Annotated[int, typer.Argument(help="Keyword ID to enable")],
|
|
245
|
+
) -> None:
|
|
246
|
+
"""Enable a paused keyword.
|
|
247
|
+
|
|
248
|
+
Examples:
|
|
249
|
+
asa keywords enable 123 456 789
|
|
250
|
+
"""
|
|
251
|
+
client = get_client()
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
with client:
|
|
255
|
+
with spinner("Enabling keyword..."):
|
|
256
|
+
keyword = (
|
|
257
|
+
client.campaigns(campaign_id)
|
|
258
|
+
.ad_groups(ad_group_id)
|
|
259
|
+
.keywords.update(keyword_id, data=KeywordUpdate(status=KeywordStatus.ACTIVE))
|
|
260
|
+
)
|
|
261
|
+
print_success(f"Keyword '{keyword.text}' enabled")
|
|
262
|
+
|
|
263
|
+
except AppleSearchAdsError as e:
|
|
264
|
+
handle_api_error(e)
|
|
265
|
+
raise typer.Exit(1) from None
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@app.command("set-bid")
|
|
269
|
+
def set_keyword_bid(
|
|
270
|
+
campaign_id: Annotated[int, typer.Argument(help="Campaign ID")],
|
|
271
|
+
ad_group_id: Annotated[int, typer.Argument(help="Ad Group ID")],
|
|
272
|
+
keyword_id: Annotated[int, typer.Argument(help="Keyword ID")],
|
|
273
|
+
bid: Annotated[float, typer.Argument(help="New bid amount")],
|
|
274
|
+
currency: Annotated[
|
|
275
|
+
str,
|
|
276
|
+
typer.Option("--currency", "-c", help="Currency code"),
|
|
277
|
+
] = "USD",
|
|
278
|
+
) -> None:
|
|
279
|
+
"""Set the bid for a keyword.
|
|
280
|
+
|
|
281
|
+
Examples:
|
|
282
|
+
asa keywords set-bid 123 456 789 3.00
|
|
283
|
+
asa keywords set-bid 123 456 789 2.50 --currency EUR
|
|
284
|
+
"""
|
|
285
|
+
client = get_client()
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
with client:
|
|
289
|
+
with spinner("Updating keyword bid..."):
|
|
290
|
+
keyword = (
|
|
291
|
+
client.campaigns(campaign_id)
|
|
292
|
+
.ad_groups(ad_group_id)
|
|
293
|
+
.keywords.update(
|
|
294
|
+
keyword_id,
|
|
295
|
+
data=KeywordUpdate(bid_amount=Money(amount=str(bid), currency=currency)),
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
print_result_panel(
|
|
300
|
+
"Keyword Bid Updated",
|
|
301
|
+
{
|
|
302
|
+
"Keyword": keyword.text,
|
|
303
|
+
"Bid": f"{keyword.bid_amount.amount} {keyword.bid_amount.currency}",
|
|
304
|
+
},
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
except AppleSearchAdsError as e:
|
|
308
|
+
handle_api_error(e)
|
|
309
|
+
raise typer.Exit(1) from None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@app.command("delete")
|
|
313
|
+
def delete_keyword(
|
|
314
|
+
campaign_id: Annotated[int, typer.Argument(help="Campaign ID")],
|
|
315
|
+
ad_group_id: Annotated[int, typer.Argument(help="Ad Group ID")],
|
|
316
|
+
keyword_id: Annotated[int, typer.Argument(help="Keyword ID to delete")],
|
|
317
|
+
force: Annotated[
|
|
318
|
+
bool,
|
|
319
|
+
typer.Option("--force", "-f", help="Skip confirmation"),
|
|
320
|
+
] = False,
|
|
321
|
+
) -> None:
|
|
322
|
+
"""Delete a keyword.
|
|
323
|
+
|
|
324
|
+
Examples:
|
|
325
|
+
asa keywords delete 123 456 789
|
|
326
|
+
asa keywords delete 123 456 789 --force
|
|
327
|
+
"""
|
|
328
|
+
client = get_client()
|
|
329
|
+
|
|
330
|
+
try:
|
|
331
|
+
with client:
|
|
332
|
+
with spinner("Fetching keyword..."):
|
|
333
|
+
keyword = client.campaigns(campaign_id).ad_groups(ad_group_id).keywords.get(keyword_id)
|
|
334
|
+
|
|
335
|
+
if not force:
|
|
336
|
+
if not confirm_action(f"Are you sure you want to delete keyword '{keyword.text}'?"):
|
|
337
|
+
print_warning("Cancelled")
|
|
338
|
+
raise typer.Exit(0)
|
|
339
|
+
|
|
340
|
+
with spinner("Deleting keyword..."):
|
|
341
|
+
client.campaigns(campaign_id).ad_groups(ad_group_id).keywords.delete(keyword_id)
|
|
342
|
+
|
|
343
|
+
print_success(f"Keyword '{keyword.text}' deleted")
|
|
344
|
+
|
|
345
|
+
except AppleSearchAdsError as e:
|
|
346
|
+
handle_api_error(e)
|
|
347
|
+
raise typer.Exit(1) from None
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
# Negative keywords subcommand
|
|
351
|
+
negatives_app = typer.Typer(help="Manage negative keywords")
|
|
352
|
+
app.add_typer(negatives_app, name="negatives")
|
|
353
|
+
|
|
354
|
+
NEGATIVE_KEYWORD_COLUMNS = ["id", "text", "match_type", "status"]
|
|
355
|
+
NEGATIVE_KEYWORD_COLUMN_LABELS = {
|
|
356
|
+
"id": "ID",
|
|
357
|
+
"match_type": "Match Type",
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@negatives_app.command("list")
|
|
362
|
+
def list_negatives(
|
|
363
|
+
campaign_id: Annotated[int, typer.Argument(help="Campaign ID")],
|
|
364
|
+
ad_group_id: Annotated[
|
|
365
|
+
int | None,
|
|
366
|
+
typer.Option("--ad-group", "-a", help="Ad Group ID (for ad group level negatives)"),
|
|
367
|
+
] = None,
|
|
368
|
+
limit: Annotated[
|
|
369
|
+
int,
|
|
370
|
+
typer.Option("--limit", "-l", help="Maximum number of results"),
|
|
371
|
+
] = 100,
|
|
372
|
+
format: Annotated[
|
|
373
|
+
OutputFormat,
|
|
374
|
+
typer.Option("--format", "-f", help="Output format"),
|
|
375
|
+
] = OutputFormat.TABLE,
|
|
376
|
+
) -> None:
|
|
377
|
+
"""List negative keywords.
|
|
378
|
+
|
|
379
|
+
Examples:
|
|
380
|
+
asa keywords negatives list 123 # Campaign level
|
|
381
|
+
asa keywords negatives list 123 --ad-group 456 # Ad group level
|
|
382
|
+
"""
|
|
383
|
+
client = get_client()
|
|
384
|
+
|
|
385
|
+
try:
|
|
386
|
+
with client:
|
|
387
|
+
level = "Ad Group" if ad_group_id else "Campaign"
|
|
388
|
+
|
|
389
|
+
with spinner(f"Fetching {level.lower()} negative keywords..."):
|
|
390
|
+
if ad_group_id:
|
|
391
|
+
negatives = client.campaigns(campaign_id).ad_groups(ad_group_id).negative_keywords.list(limit=limit)
|
|
392
|
+
else:
|
|
393
|
+
negatives = client.campaigns(campaign_id).negative_keywords.list(limit=limit)
|
|
394
|
+
|
|
395
|
+
if not negatives.data:
|
|
396
|
+
print_warning(f"No {level.lower()} negative keywords found")
|
|
397
|
+
return
|
|
398
|
+
|
|
399
|
+
data = [
|
|
400
|
+
{
|
|
401
|
+
"id": n.id,
|
|
402
|
+
"text": n.text,
|
|
403
|
+
"match_type": n.match_type.value,
|
|
404
|
+
"status": n.status.value,
|
|
405
|
+
}
|
|
406
|
+
for n in negatives
|
|
407
|
+
]
|
|
408
|
+
output_data(
|
|
409
|
+
data,
|
|
410
|
+
NEGATIVE_KEYWORD_COLUMNS,
|
|
411
|
+
format,
|
|
412
|
+
title=f"{level} Negative Keywords ({negatives.total_results} total)",
|
|
413
|
+
column_labels=NEGATIVE_KEYWORD_COLUMN_LABELS,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
except AppleSearchAdsError as e:
|
|
417
|
+
handle_api_error(e)
|
|
418
|
+
raise typer.Exit(1) from None
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@negatives_app.command("add")
|
|
422
|
+
def add_negative(
|
|
423
|
+
campaign_id: Annotated[int, typer.Argument(help="Campaign ID")],
|
|
424
|
+
text: Annotated[str, typer.Argument(help="Keyword text to exclude")],
|
|
425
|
+
ad_group_id: Annotated[
|
|
426
|
+
int | None,
|
|
427
|
+
typer.Option("--ad-group", "-a", help="Ad Group ID (for ad group level negative)"),
|
|
428
|
+
] = None,
|
|
429
|
+
match_type: Annotated[
|
|
430
|
+
KeywordMatchType,
|
|
431
|
+
typer.Option("--match-type", "-m", help="Match type"),
|
|
432
|
+
] = KeywordMatchType.EXACT,
|
|
433
|
+
) -> None:
|
|
434
|
+
"""Add a negative keyword.
|
|
435
|
+
|
|
436
|
+
Examples:
|
|
437
|
+
asa keywords negatives add 123 "free" # Campaign level
|
|
438
|
+
asa keywords negatives add 123 "cheap" --ad-group 456 # Ad group level
|
|
439
|
+
asa keywords negatives add 123 "competitor" --match-type BROAD
|
|
440
|
+
"""
|
|
441
|
+
client = get_client()
|
|
442
|
+
|
|
443
|
+
try:
|
|
444
|
+
with client:
|
|
445
|
+
create_data = NegativeKeywordCreate(
|
|
446
|
+
text=text,
|
|
447
|
+
match_type=match_type,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
level = "ad group" if ad_group_id else "campaign"
|
|
451
|
+
|
|
452
|
+
with spinner(f"Adding negative keyword at {level} level..."):
|
|
453
|
+
if ad_group_id:
|
|
454
|
+
negative = (
|
|
455
|
+
client.campaigns(campaign_id).ad_groups(ad_group_id).negative_keywords.create(create_data)
|
|
456
|
+
)
|
|
457
|
+
else:
|
|
458
|
+
negative = client.campaigns(campaign_id).negative_keywords.create(create_data)
|
|
459
|
+
|
|
460
|
+
print_result_panel(
|
|
461
|
+
"Negative Keyword Added",
|
|
462
|
+
{
|
|
463
|
+
"Level": level.title(),
|
|
464
|
+
"Text": negative.text,
|
|
465
|
+
"Match Type": negative.match_type.value,
|
|
466
|
+
},
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
except AppleSearchAdsError as e:
|
|
470
|
+
handle_api_error(e)
|
|
471
|
+
raise typer.Exit(1) from None
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@negatives_app.command("delete")
|
|
475
|
+
def delete_negative(
|
|
476
|
+
campaign_id: Annotated[int, typer.Argument(help="Campaign ID")],
|
|
477
|
+
keyword_id: Annotated[int, typer.Argument(help="Negative keyword ID to delete")],
|
|
478
|
+
ad_group_id: Annotated[
|
|
479
|
+
int | None,
|
|
480
|
+
typer.Option("--ad-group", "-a", help="Ad Group ID (for ad group level negative)"),
|
|
481
|
+
] = None,
|
|
482
|
+
force: Annotated[
|
|
483
|
+
bool,
|
|
484
|
+
typer.Option("--force", "-f", help="Skip confirmation"),
|
|
485
|
+
] = False,
|
|
486
|
+
) -> None:
|
|
487
|
+
"""Delete a negative keyword.
|
|
488
|
+
|
|
489
|
+
Examples:
|
|
490
|
+
asa keywords negatives delete 123 789 # Campaign level
|
|
491
|
+
asa keywords negatives delete 123 789 --ad-group 456 # Ad group level
|
|
492
|
+
"""
|
|
493
|
+
client = get_client()
|
|
494
|
+
|
|
495
|
+
try:
|
|
496
|
+
with client:
|
|
497
|
+
if not force:
|
|
498
|
+
if not confirm_action("Are you sure you want to delete this negative keyword?"):
|
|
499
|
+
print_warning("Cancelled")
|
|
500
|
+
raise typer.Exit(0)
|
|
501
|
+
|
|
502
|
+
level = "ad group" if ad_group_id else "campaign"
|
|
503
|
+
|
|
504
|
+
with spinner(f"Deleting {level} negative keyword..."):
|
|
505
|
+
if ad_group_id:
|
|
506
|
+
client.campaigns(campaign_id).ad_groups(ad_group_id).negative_keywords.delete(keyword_id)
|
|
507
|
+
else:
|
|
508
|
+
client.campaigns(campaign_id).negative_keywords.delete(keyword_id)
|
|
509
|
+
|
|
510
|
+
print_success("Negative keyword deleted")
|
|
511
|
+
|
|
512
|
+
except AppleSearchAdsError as e:
|
|
513
|
+
handle_api_error(e)
|
|
514
|
+
raise typer.Exit(1) from None
|
asa_api_cli/main.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Main CLI application."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from asa_api_cli import ad_groups, auth, brand, campaigns, keywords, optimize, reports
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(
|
|
11
|
+
name="asa",
|
|
12
|
+
help="Apple Search Ads API CLI - Manage campaigns, ad groups, keywords, and reports.",
|
|
13
|
+
rich_markup_mode="rich",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
# Register sub-commands
|
|
19
|
+
app.add_typer(auth.app, name="auth", help="Authentication commands")
|
|
20
|
+
app.add_typer(brand.app, name="brand", help="Create brand protection campaigns")
|
|
21
|
+
app.add_typer(campaigns.app, name="campaigns", help="Manage campaigns")
|
|
22
|
+
app.add_typer(ad_groups.app, name="ad-groups", help="Manage ad groups")
|
|
23
|
+
app.add_typer(keywords.app, name="keywords", help="Manage keywords")
|
|
24
|
+
app.add_typer(reports.app, name="reports", help="Generate reports")
|
|
25
|
+
app.add_typer(optimize.app, name="optimize", help="Optimization tools")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def version_callback(value: bool) -> None:
|
|
29
|
+
"""Show version and exit."""
|
|
30
|
+
if value:
|
|
31
|
+
from asa_api_client import __version__ as api_version
|
|
32
|
+
|
|
33
|
+
from asa_api_cli import __version__ as cli_version
|
|
34
|
+
|
|
35
|
+
console.print(f"asa-api-cli {cli_version} (asa-api-client {api_version})")
|
|
36
|
+
raise typer.Exit()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.callback(invoke_without_command=True)
|
|
40
|
+
def main(
|
|
41
|
+
ctx: typer.Context,
|
|
42
|
+
version: Annotated[
|
|
43
|
+
bool | None,
|
|
44
|
+
typer.Option(
|
|
45
|
+
"--version",
|
|
46
|
+
"-v",
|
|
47
|
+
help="Show version and exit",
|
|
48
|
+
callback=version_callback,
|
|
49
|
+
is_eager=True,
|
|
50
|
+
),
|
|
51
|
+
] = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Apple Search Ads API CLI.
|
|
54
|
+
|
|
55
|
+
Manage your Apple Search Ads campaigns, ad groups, keywords,
|
|
56
|
+
and generate performance reports from the command line.
|
|
57
|
+
|
|
58
|
+
Set up authentication using environment variables:
|
|
59
|
+
|
|
60
|
+
export ASA_CLIENT_ID="SEARCHADS.your-client-id"
|
|
61
|
+
export ASA_TEAM_ID="YOUR_TEAM_ID"
|
|
62
|
+
export ASA_KEY_ID="YOUR_KEY_ID"
|
|
63
|
+
export ASA_ORG_ID="123456"
|
|
64
|
+
export ASA_PRIVATE_KEY_PATH="/path/to/private-key.pem"
|
|
65
|
+
|
|
66
|
+
Or test your credentials:
|
|
67
|
+
|
|
68
|
+
asa auth test
|
|
69
|
+
"""
|
|
70
|
+
if ctx.invoked_subcommand is None and not version:
|
|
71
|
+
console.print(ctx.get_help())
|
|
72
|
+
raise typer.Exit()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
app()
|