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/utils.py
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
"""Shared utilities for CLI commands."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Iterator
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from datetime import date
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, TypeVar
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from asa_api_client import AppleSearchAdsClient
|
|
13
|
+
from asa_api_client.exceptions import AppleSearchAdsError, ConfigurationError
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
from rich.progress import (
|
|
17
|
+
BarColumn,
|
|
18
|
+
Progress,
|
|
19
|
+
SpinnerColumn,
|
|
20
|
+
TaskProgressColumn,
|
|
21
|
+
TextColumn,
|
|
22
|
+
TimeElapsedColumn,
|
|
23
|
+
)
|
|
24
|
+
from rich.syntax import Syntax
|
|
25
|
+
from rich.table import Table
|
|
26
|
+
from rich.text import Text
|
|
27
|
+
from rich.theme import Theme
|
|
28
|
+
|
|
29
|
+
# Custom theme for consistent styling
|
|
30
|
+
ASA_THEME = Theme(
|
|
31
|
+
{
|
|
32
|
+
"info": "cyan",
|
|
33
|
+
"success": "green",
|
|
34
|
+
"warning": "yellow",
|
|
35
|
+
"error": "red bold",
|
|
36
|
+
"highlight": "magenta",
|
|
37
|
+
"muted": "dim",
|
|
38
|
+
"header": "bold cyan",
|
|
39
|
+
"value": "white",
|
|
40
|
+
"label": "blue",
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
console = Console(theme=ASA_THEME)
|
|
45
|
+
error_console = Console(stderr=True, theme=ASA_THEME)
|
|
46
|
+
|
|
47
|
+
T = TypeVar("T")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def enum_value(v: Enum | str | bool) -> str:
|
|
51
|
+
"""Get the value from an enum, string, or bool.
|
|
52
|
+
|
|
53
|
+
Since API models use `EnumType | str | bool` for forward compatibility,
|
|
54
|
+
this helper safely extracts the string value.
|
|
55
|
+
"""
|
|
56
|
+
if isinstance(v, Enum):
|
|
57
|
+
return str(v.value)
|
|
58
|
+
elif isinstance(v, bool):
|
|
59
|
+
return "OPT_IN" if v else "OPT_OUT"
|
|
60
|
+
return str(v)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class OutputFormat(str, Enum):
|
|
64
|
+
"""Output format options."""
|
|
65
|
+
|
|
66
|
+
TABLE = "table"
|
|
67
|
+
JSON = "json"
|
|
68
|
+
CSV = "csv"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_client() -> AppleSearchAdsClient:
|
|
72
|
+
"""Get an authenticated client from environment variables.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
An authenticated AppleSearchAdsClient.
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
typer.Exit: If configuration is invalid.
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
return AppleSearchAdsClient.from_env()
|
|
82
|
+
except ConfigurationError as e:
|
|
83
|
+
print_error("Configuration Error", e.message)
|
|
84
|
+
error_console.print()
|
|
85
|
+
print_info_panel(
|
|
86
|
+
"Required Environment Variables",
|
|
87
|
+
"• ASA_CLIENT_ID\n• ASA_TEAM_ID\n• ASA_KEY_ID\n• ASA_ORG_ID\n• ASA_PRIVATE_KEY_PATH (or ASA_PRIVATE_KEY)",
|
|
88
|
+
)
|
|
89
|
+
raise typer.Exit(1) from None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def handle_api_error(e: AppleSearchAdsError) -> None:
|
|
93
|
+
"""Handle and display an API error.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
e: The exception to handle.
|
|
97
|
+
"""
|
|
98
|
+
from asa_api_client.exceptions import ValidationError
|
|
99
|
+
|
|
100
|
+
details_parts = []
|
|
101
|
+
|
|
102
|
+
# Show request info if available (for debugging)
|
|
103
|
+
if e.response_body and "_request" in e.response_body:
|
|
104
|
+
details_parts.append(f"Request: {e.response_body['_request']}")
|
|
105
|
+
|
|
106
|
+
if e.status_code:
|
|
107
|
+
details_parts.append(f"Status code: {e.status_code}")
|
|
108
|
+
|
|
109
|
+
# Show field errors for validation errors
|
|
110
|
+
if isinstance(e, ValidationError) and e.field_errors:
|
|
111
|
+
for field, errors in e.field_errors.items():
|
|
112
|
+
for err in errors:
|
|
113
|
+
details_parts.append(f" {field}: {err}")
|
|
114
|
+
|
|
115
|
+
# Show response body if available (for debugging)
|
|
116
|
+
if e.response_body:
|
|
117
|
+
import json
|
|
118
|
+
|
|
119
|
+
# Remove internal _request field before displaying
|
|
120
|
+
body_copy = {k: v for k, v in e.response_body.items() if not k.startswith("_")}
|
|
121
|
+
body_str = json.dumps(body_copy, indent=2)
|
|
122
|
+
if len(body_str) < 500: # Only show if not too long
|
|
123
|
+
details_parts.append(f"Response: {body_str}")
|
|
124
|
+
|
|
125
|
+
details = "\n".join(details_parts) if details_parts else None
|
|
126
|
+
print_error("API Error", e.message, details=details)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ============================================================================
|
|
130
|
+
# Styled Output Functions
|
|
131
|
+
# ============================================================================
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def print_success(message: str, details: str | None = None) -> None:
|
|
135
|
+
"""Print a success message with a checkmark.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
message: The success message.
|
|
139
|
+
details: Optional additional details.
|
|
140
|
+
"""
|
|
141
|
+
text = Text()
|
|
142
|
+
text.append("✓ ", style="success")
|
|
143
|
+
text.append(message)
|
|
144
|
+
if details:
|
|
145
|
+
text.append(f"\n {details}", style="muted")
|
|
146
|
+
console.print(text)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def print_error(title: str, message: str, details: str | None = None) -> None:
|
|
150
|
+
"""Print an error message in a styled panel.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
title: Error title.
|
|
154
|
+
message: Error message.
|
|
155
|
+
details: Optional additional details.
|
|
156
|
+
"""
|
|
157
|
+
content = Text(message)
|
|
158
|
+
if details:
|
|
159
|
+
content.append(f"\n{details}", style="muted")
|
|
160
|
+
|
|
161
|
+
panel = Panel(
|
|
162
|
+
content,
|
|
163
|
+
title=f"[error]✗ {title}[/error]",
|
|
164
|
+
border_style="red",
|
|
165
|
+
padding=(0, 1),
|
|
166
|
+
)
|
|
167
|
+
error_console.print(panel)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def print_warning(message: str) -> None:
|
|
171
|
+
"""Print a warning message.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
message: The warning message.
|
|
175
|
+
"""
|
|
176
|
+
text = Text()
|
|
177
|
+
text.append("⚠ ", style="warning")
|
|
178
|
+
text.append(message, style="warning")
|
|
179
|
+
console.print(text)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def print_info(message: str) -> None:
|
|
183
|
+
"""Print an info message.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
message: The info message.
|
|
187
|
+
"""
|
|
188
|
+
text = Text()
|
|
189
|
+
text.append("ℹ ", style="info")
|
|
190
|
+
text.append(message)
|
|
191
|
+
console.print(text)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def print_info_panel(title: str, content: str) -> None:
|
|
195
|
+
"""Print information in a styled panel.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
title: Panel title.
|
|
199
|
+
content: Panel content.
|
|
200
|
+
"""
|
|
201
|
+
panel = Panel(
|
|
202
|
+
content,
|
|
203
|
+
title=f"[info]{title}[/info]",
|
|
204
|
+
border_style="cyan",
|
|
205
|
+
padding=(0, 1),
|
|
206
|
+
)
|
|
207
|
+
console.print(panel)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def print_result_panel(title: str, data: dict[str, Any]) -> None:
|
|
211
|
+
"""Print a result in a styled panel with key-value pairs.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
title: Panel title.
|
|
215
|
+
data: Dictionary of key-value pairs to display.
|
|
216
|
+
"""
|
|
217
|
+
lines = []
|
|
218
|
+
for key, value in data.items():
|
|
219
|
+
lines.append(f"[label]{key}:[/label] [value]{value}[/value]")
|
|
220
|
+
|
|
221
|
+
panel = Panel(
|
|
222
|
+
"\n".join(lines),
|
|
223
|
+
title=f"[success]{title}[/success]",
|
|
224
|
+
border_style="green",
|
|
225
|
+
padding=(0, 1),
|
|
226
|
+
)
|
|
227
|
+
console.print(panel)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# ============================================================================
|
|
231
|
+
# Table Output
|
|
232
|
+
# ============================================================================
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def print_table(
|
|
236
|
+
data: list[dict[str, Any]],
|
|
237
|
+
columns: list[str],
|
|
238
|
+
title: str | None = None,
|
|
239
|
+
column_labels: dict[str, str] | None = None,
|
|
240
|
+
) -> None:
|
|
241
|
+
"""Print data as a rich table.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
data: List of dictionaries to display.
|
|
245
|
+
columns: Column names to include.
|
|
246
|
+
title: Optional table title.
|
|
247
|
+
column_labels: Optional mapping of column names to display labels.
|
|
248
|
+
"""
|
|
249
|
+
table = Table(
|
|
250
|
+
title=title,
|
|
251
|
+
show_header=True,
|
|
252
|
+
header_style="header",
|
|
253
|
+
border_style="muted",
|
|
254
|
+
row_styles=["", "dim"],
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
labels = column_labels or {}
|
|
258
|
+
for col in columns:
|
|
259
|
+
label = labels.get(col, col.replace("_", " ").title())
|
|
260
|
+
table.add_column(label)
|
|
261
|
+
|
|
262
|
+
for row in data:
|
|
263
|
+
table.add_row(*[str(row.get(col, "")) for col in columns])
|
|
264
|
+
|
|
265
|
+
console.print(table)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# ============================================================================
|
|
269
|
+
# JSON Output with Syntax Highlighting
|
|
270
|
+
# ============================================================================
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def print_json(data: Any, title: str | None = None) -> None:
|
|
274
|
+
"""Print data as syntax-highlighted JSON.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
data: Data to print as JSON.
|
|
278
|
+
title: Optional title to display above the JSON.
|
|
279
|
+
"""
|
|
280
|
+
if hasattr(data, "model_dump"):
|
|
281
|
+
data = data.model_dump(by_alias=True, exclude_none=True)
|
|
282
|
+
elif isinstance(data, list) and data and hasattr(data[0], "model_dump"):
|
|
283
|
+
data = [item.model_dump(by_alias=True, exclude_none=True) for item in data]
|
|
284
|
+
|
|
285
|
+
json_str = json.dumps(data, indent=2, default=str)
|
|
286
|
+
|
|
287
|
+
syntax = Syntax(
|
|
288
|
+
json_str,
|
|
289
|
+
"json",
|
|
290
|
+
theme="monokai",
|
|
291
|
+
line_numbers=False,
|
|
292
|
+
word_wrap=True,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
if title:
|
|
296
|
+
panel = Panel(syntax, title=f"[info]{title}[/info]", border_style="cyan")
|
|
297
|
+
console.print(panel)
|
|
298
|
+
else:
|
|
299
|
+
console.print(syntax)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def print_csv(data: list[dict[str, Any]], columns: list[str]) -> None:
|
|
303
|
+
"""Print data as CSV.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
data: List of dictionaries to print.
|
|
307
|
+
columns: Column names to include.
|
|
308
|
+
"""
|
|
309
|
+
import csv
|
|
310
|
+
import sys
|
|
311
|
+
|
|
312
|
+
writer = csv.DictWriter(sys.stdout, fieldnames=columns, extrasaction="ignore")
|
|
313
|
+
writer.writeheader()
|
|
314
|
+
writer.writerows(data)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def output_data(
|
|
318
|
+
data: list[dict[str, Any]],
|
|
319
|
+
columns: list[str],
|
|
320
|
+
format: OutputFormat,
|
|
321
|
+
title: str | None = None,
|
|
322
|
+
column_labels: dict[str, str] | None = None,
|
|
323
|
+
) -> None:
|
|
324
|
+
"""Output data in the specified format.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
data: Data to output.
|
|
328
|
+
columns: Column names for table/CSV output.
|
|
329
|
+
format: Output format.
|
|
330
|
+
title: Optional title for table output.
|
|
331
|
+
column_labels: Optional mapping of column names to display labels.
|
|
332
|
+
"""
|
|
333
|
+
if format == OutputFormat.JSON:
|
|
334
|
+
print_json(data, title)
|
|
335
|
+
elif format == OutputFormat.CSV:
|
|
336
|
+
print_csv(data, columns)
|
|
337
|
+
else:
|
|
338
|
+
print_table(data, columns, title, column_labels)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# ============================================================================
|
|
342
|
+
# Progress Indicators
|
|
343
|
+
# ============================================================================
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@contextmanager
|
|
347
|
+
def spinner(message: str) -> Iterator[None]:
|
|
348
|
+
"""Show a spinner while executing a block.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
message: Message to display with the spinner.
|
|
352
|
+
|
|
353
|
+
Yields:
|
|
354
|
+
None
|
|
355
|
+
|
|
356
|
+
Example:
|
|
357
|
+
with spinner("Loading campaigns..."):
|
|
358
|
+
campaigns = client.campaigns.list()
|
|
359
|
+
"""
|
|
360
|
+
with console.status(f"[info]{message}[/info]", spinner="dots"):
|
|
361
|
+
yield
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def create_progress() -> Progress:
|
|
365
|
+
"""Create a progress bar for iteration.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
A configured Progress instance.
|
|
369
|
+
|
|
370
|
+
Example:
|
|
371
|
+
with create_progress() as progress:
|
|
372
|
+
task = progress.add_task("Loading...", total=100)
|
|
373
|
+
for item in items:
|
|
374
|
+
progress.advance(task)
|
|
375
|
+
"""
|
|
376
|
+
return Progress(
|
|
377
|
+
SpinnerColumn(),
|
|
378
|
+
TextColumn("[progress.description]{task.description}"),
|
|
379
|
+
BarColumn(),
|
|
380
|
+
TaskProgressColumn(),
|
|
381
|
+
TimeElapsedColumn(),
|
|
382
|
+
console=console,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def iterate_with_progress(
|
|
387
|
+
items: Iterator[T],
|
|
388
|
+
total: int | None = None,
|
|
389
|
+
description: str = "Processing...",
|
|
390
|
+
) -> Iterator[T]:
|
|
391
|
+
"""Iterate over items with a progress bar.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
items: Iterator to wrap.
|
|
395
|
+
total: Total number of items (if known).
|
|
396
|
+
description: Progress bar description.
|
|
397
|
+
|
|
398
|
+
Yields:
|
|
399
|
+
Items from the iterator.
|
|
400
|
+
|
|
401
|
+
Example:
|
|
402
|
+
for campaign in iterate_with_progress(client.campaigns.iter_all(), total=100):
|
|
403
|
+
process(campaign)
|
|
404
|
+
"""
|
|
405
|
+
with create_progress() as progress:
|
|
406
|
+
task = progress.add_task(description, total=total)
|
|
407
|
+
for item in items:
|
|
408
|
+
yield item
|
|
409
|
+
progress.advance(task)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
# ============================================================================
|
|
413
|
+
# Utility Functions
|
|
414
|
+
# ============================================================================
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def parse_date(value: str) -> date:
|
|
418
|
+
"""Parse a date string in YYYY-MM-DD format.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
value: Date string to parse.
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
Parsed date object.
|
|
425
|
+
|
|
426
|
+
Raises:
|
|
427
|
+
typer.BadParameter: If the date format is invalid.
|
|
428
|
+
"""
|
|
429
|
+
try:
|
|
430
|
+
return date.fromisoformat(value)
|
|
431
|
+
except ValueError:
|
|
432
|
+
raise typer.BadParameter(f"Invalid date format: '{value}'. Use YYYY-MM-DD format.") from None
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def save_to_file(content: str, path: Path) -> None:
|
|
436
|
+
"""Save content to a file.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
content: Content to save.
|
|
440
|
+
path: File path.
|
|
441
|
+
"""
|
|
442
|
+
path.write_text(content)
|
|
443
|
+
print_success(f"Saved to {path}")
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def format_number(value: int | float | None) -> str:
|
|
447
|
+
"""Format a number with thousands separators.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
value: Number to format.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
Formatted string or "-" if None.
|
|
454
|
+
"""
|
|
455
|
+
if value is None:
|
|
456
|
+
return "-"
|
|
457
|
+
if isinstance(value, float):
|
|
458
|
+
return f"{value:,.2f}"
|
|
459
|
+
return f"{value:,}"
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def format_money(amount: str | None, currency: str | None = None) -> str:
|
|
463
|
+
"""Format a monetary value.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
amount: The amount string.
|
|
467
|
+
currency: Optional currency code.
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
Formatted money string.
|
|
471
|
+
"""
|
|
472
|
+
if amount is None:
|
|
473
|
+
return "-"
|
|
474
|
+
if currency:
|
|
475
|
+
return f"{amount} {currency}"
|
|
476
|
+
return amount
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def format_percent(value: float | None) -> str:
|
|
480
|
+
"""Format a value as a percentage.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
value: Value between 0 and 1.
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
Formatted percentage string.
|
|
487
|
+
"""
|
|
488
|
+
if value is None:
|
|
489
|
+
return "-"
|
|
490
|
+
return f"{value:.2%}"
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def confirm_action(message: str, default: bool = False) -> bool:
|
|
494
|
+
"""Ask for confirmation with styled prompt.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
message: Confirmation message.
|
|
498
|
+
default: Default value if user just presses Enter.
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
True if confirmed, False otherwise.
|
|
502
|
+
"""
|
|
503
|
+
return typer.confirm(message, default=default)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: asa-api-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Command-line interface for Apple Search Ads campaign management
|
|
5
|
+
Project-URL: Homepage, https://github.com/SamPetherbridge/asa-api-cli
|
|
6
|
+
Project-URL: Repository, https://github.com/SamPetherbridge/asa-api-cli
|
|
7
|
+
Project-URL: Issues, https://github.com/SamPetherbridge/asa-api-cli/issues
|
|
8
|
+
Author-email: Sam Petherbridge <hello@peth.me>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: advertising,apple,campaign-management,cli,search-ads
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.13
|
|
21
|
+
Requires-Dist: asa-api-client>=0.1.0
|
|
22
|
+
Requires-Dist: rich>=13.9.0
|
|
23
|
+
Requires-Dist: typer>=0.13.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mypy>=1.13.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-cov>=6.0.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8.3.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.8.0; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# ASA API CLI
|
|
33
|
+
|
|
34
|
+
[](https://pypi.org/project/asa-api-cli/)
|
|
35
|
+
[](https://pypi.org/project/asa-api-cli/)
|
|
36
|
+
[](https://github.com/SamPetherbridge/asa-api-cli/blob/main/LICENSE)
|
|
37
|
+
[](https://github.com/SamPetherbridge/asa-api-cli/actions/workflows/ci.yml)
|
|
38
|
+
|
|
39
|
+
A command-line interface for managing Apple Search Ads campaigns.
|
|
40
|
+
|
|
41
|
+
Built on top of [asa-api-client](https://github.com/SamPetherbridge/asa-api-client).
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
Using [uv](https://docs.astral.sh/uv/) (recommended):
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uv tool install asa-api-cli
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Using pip:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install asa-api-cli
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Using pipx:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pipx install asa-api-cli
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Setup
|
|
64
|
+
|
|
65
|
+
Set up your Apple Search Ads API credentials:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
export ASA_CLIENT_ID="SEARCHADS.your-client-id"
|
|
69
|
+
export ASA_TEAM_ID="SEARCHADS.your-team-id"
|
|
70
|
+
export ASA_KEY_ID="your-key-id"
|
|
71
|
+
export ASA_ORG_ID="123456"
|
|
72
|
+
export ASA_PRIVATE_KEY_PATH="/path/to/private-key.pem"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Or use a `.env` file in your working directory.
|
|
76
|
+
|
|
77
|
+
Test your credentials:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
asa auth test
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Commands
|
|
84
|
+
|
|
85
|
+
### Campaigns
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# List all enabled campaigns
|
|
89
|
+
asa campaigns list
|
|
90
|
+
|
|
91
|
+
# List all campaigns (including paused)
|
|
92
|
+
asa campaigns list --all
|
|
93
|
+
|
|
94
|
+
# List with 7-day spend data
|
|
95
|
+
asa campaigns list --with-spend
|
|
96
|
+
|
|
97
|
+
# Get a specific campaign
|
|
98
|
+
asa campaigns get 123456789
|
|
99
|
+
|
|
100
|
+
# Output as JSON
|
|
101
|
+
asa campaigns list --json
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Ad Groups
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# List ad groups in a campaign
|
|
108
|
+
asa ad-groups list 123456789
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Keywords
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# List keywords in an ad group
|
|
115
|
+
asa keywords list 123456789 987654321
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Reports
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# Campaign performance report
|
|
122
|
+
asa reports campaigns --start 2024-01-01 --end 2024-01-31
|
|
123
|
+
|
|
124
|
+
# Keyword report
|
|
125
|
+
asa reports keywords 123456789 --start 2024-01-01 --end 2024-01-31
|
|
126
|
+
|
|
127
|
+
# Export to CSV
|
|
128
|
+
asa reports campaigns --start 2024-01-01 --end 2024-01-31 --output report.csv
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Brand Campaigns
|
|
132
|
+
|
|
133
|
+
Create brand protection campaigns for your app:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# Interactive mode (recommended)
|
|
137
|
+
asa brand
|
|
138
|
+
|
|
139
|
+
# With arguments
|
|
140
|
+
asa brand "My App Name" -v "My App" -c US -c GB
|
|
141
|
+
|
|
142
|
+
# All standard countries
|
|
143
|
+
asa brand "My App Name" --country all
|
|
144
|
+
|
|
145
|
+
# Preview without creating
|
|
146
|
+
asa brand "My App Name" -c US --dry-run
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Campaign Expansion
|
|
150
|
+
|
|
151
|
+
Expand existing campaigns to new markets:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
# Interactive mode
|
|
155
|
+
asa optimize expand
|
|
156
|
+
|
|
157
|
+
# With filters
|
|
158
|
+
asa optimize expand --type Generic --match EM
|
|
159
|
+
|
|
160
|
+
# Preview
|
|
161
|
+
asa optimize expand --dry-run
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Bid Optimization
|
|
165
|
+
|
|
166
|
+
Check and fix bid discrepancies:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
# Check bids
|
|
170
|
+
asa optimize bid-check
|
|
171
|
+
|
|
172
|
+
# Auto-fix discrepancies
|
|
173
|
+
asa optimize bid-check --auto-fix
|
|
174
|
+
|
|
175
|
+
# With threshold
|
|
176
|
+
asa optimize bid-check --threshold 0.05
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Shell Completion
|
|
180
|
+
|
|
181
|
+
Install shell completion for your shell:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
# Bash
|
|
185
|
+
asa --install-completion bash
|
|
186
|
+
|
|
187
|
+
# Zsh
|
|
188
|
+
asa --install-completion zsh
|
|
189
|
+
|
|
190
|
+
# Fish
|
|
191
|
+
asa --install-completion fish
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
MIT License - Copyright (c) 2025 Peth Pty Ltd
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
asa_api_cli/__init__.py,sha256=vBki45z9Mf1zMH14tLj5W6OBcttjxeHWVBM2DyWOLOE,311
|
|
2
|
+
asa_api_cli/ad_groups.py,sha256=frVqei3uyq_eSgBgp7KORPm5xrxUHJAqIh3_P4ds_pI,8226
|
|
3
|
+
asa_api_cli/auth.py,sha256=mmMugQWnFGvHnjjdyTk5_bCUmF7D9EZbgDpm0FIwAKw,6382
|
|
4
|
+
asa_api_cli/brand.py,sha256=rJglbYAQ17H0Nu7SkfykRTaXO0IlBVuroH-flqEIQuM,26467
|
|
5
|
+
asa_api_cli/campaigns.py,sha256=lvqxqMMxcvR2W8TDFNotZzub5P1c3239euIs0kKftek,11555
|
|
6
|
+
asa_api_cli/keywords.py,sha256=-a0kUF96ynrVF04Gxd44p8gHWFU6d8Z-OfAZs6V_3BQ,16324
|
|
7
|
+
asa_api_cli/main.py,sha256=t3JXLjN1YfApB6Vsai8RoOS_4YoDu149VxuL4dIuApo,2231
|
|
8
|
+
asa_api_cli/optimize.py,sha256=_FldTB8vzUCedxzxChxLn_4MK3ykFYsx7i9uPe1IAzY,36743
|
|
9
|
+
asa_api_cli/reports.py,sha256=pB656hOVeyja7PJKWdCW3H6Yju1bChJhdarVuJi0xPo,13911
|
|
10
|
+
asa_api_cli/utils.py,sha256=DredVaT9T8Xjjfmno7JIpybZE3Epgbs2itKjFwYIvnw,13166
|
|
11
|
+
asa_api_cli-0.1.0.dist-info/METADATA,sha256=r-9DWQT6ZkgT50NvBDJK0hgCOefnI-38cCoz-x_22E4,4353
|
|
12
|
+
asa_api_cli-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
13
|
+
asa_api_cli-0.1.0.dist-info/entry_points.txt,sha256=1NAsJc-p7DGzsOv467Os0iuyCPxelaOdJi1CGREtDz4,40
|
|
14
|
+
asa_api_cli-0.1.0.dist-info/licenses/LICENSE,sha256=KB9suX2BucB1i3r7FT6wxU5ELrvgB95zgAHfzFFUAw4,1069
|
|
15
|
+
asa_api_cli-0.1.0.dist-info/RECORD,,
|