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/reports.py
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
"""Report CLI commands."""
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated, Any
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from asa_api_client.exceptions import AppleSearchAdsError
|
|
10
|
+
from asa_api_client.models import GranularityType
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from asa_api_cli.utils import (
|
|
15
|
+
console,
|
|
16
|
+
format_number,
|
|
17
|
+
format_percent,
|
|
18
|
+
get_client,
|
|
19
|
+
handle_api_error,
|
|
20
|
+
parse_date,
|
|
21
|
+
print_error,
|
|
22
|
+
print_success,
|
|
23
|
+
print_warning,
|
|
24
|
+
spinner,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
app = typer.Typer(help="Generate performance reports")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def format_report_money(spend: object | None) -> str:
|
|
31
|
+
"""Format money value for display."""
|
|
32
|
+
if spend is None:
|
|
33
|
+
return "-"
|
|
34
|
+
return f"{spend.amount} {spend.currency}" # type: ignore
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def report_row_to_dict(row: object) -> dict[str, Any]:
|
|
38
|
+
"""Convert report row to display dictionary."""
|
|
39
|
+
result: dict[str, Any] = {}
|
|
40
|
+
|
|
41
|
+
# Metadata
|
|
42
|
+
if row.metadata: # type: ignore
|
|
43
|
+
meta = row.metadata # type: ignore
|
|
44
|
+
if meta.campaign_name:
|
|
45
|
+
result["campaign"] = meta.campaign_name
|
|
46
|
+
if meta.ad_group_name:
|
|
47
|
+
result["ad_group"] = meta.ad_group_name
|
|
48
|
+
if meta.keyword:
|
|
49
|
+
result["keyword"] = meta.keyword
|
|
50
|
+
if meta.search_term_text:
|
|
51
|
+
result["search_term"] = meta.search_term_text
|
|
52
|
+
if meta.country_or_region:
|
|
53
|
+
result["country"] = meta.country_or_region
|
|
54
|
+
|
|
55
|
+
# Metrics
|
|
56
|
+
if row.total: # type: ignore
|
|
57
|
+
total = row.total # type: ignore
|
|
58
|
+
result["impressions"] = total.impressions
|
|
59
|
+
result["taps"] = total.taps
|
|
60
|
+
result["installs"] = total.installs
|
|
61
|
+
result["ttr"] = total.ttr
|
|
62
|
+
result["conv_rate"] = total.conversion_rate
|
|
63
|
+
result["spend"] = total.local_spend.amount if total.local_spend else None
|
|
64
|
+
result["avg_cpt"] = total.avg_cpt.amount if total.avg_cpt else None
|
|
65
|
+
result["avg_cpa"] = total.avg_cpa.amount if total.avg_cpa else None
|
|
66
|
+
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def print_report_table(
|
|
71
|
+
data: list[dict[str, Any]],
|
|
72
|
+
columns: list[str],
|
|
73
|
+
title: str,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Print report data as a styled table."""
|
|
76
|
+
table = Table(
|
|
77
|
+
title=title,
|
|
78
|
+
show_header=True,
|
|
79
|
+
header_style="header",
|
|
80
|
+
border_style="muted",
|
|
81
|
+
row_styles=["", "dim"],
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Column headers
|
|
85
|
+
column_labels = {
|
|
86
|
+
"campaign": "Campaign",
|
|
87
|
+
"ad_group": "Ad Group",
|
|
88
|
+
"keyword": "Keyword",
|
|
89
|
+
"search_term": "Search Term",
|
|
90
|
+
"country": "Country",
|
|
91
|
+
"impressions": "Impressions",
|
|
92
|
+
"taps": "Taps",
|
|
93
|
+
"installs": "Installs",
|
|
94
|
+
"ttr": "TTR",
|
|
95
|
+
"conv_rate": "Conv Rate",
|
|
96
|
+
"spend": "Spend",
|
|
97
|
+
"avg_cpt": "Avg CPT",
|
|
98
|
+
"avg_cpa": "Avg CPA",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for col in columns:
|
|
102
|
+
table.add_column(column_labels.get(col, col))
|
|
103
|
+
|
|
104
|
+
for row in data:
|
|
105
|
+
values = []
|
|
106
|
+
for col in columns:
|
|
107
|
+
value = row.get(col)
|
|
108
|
+
if col in ("ttr", "conv_rate"):
|
|
109
|
+
values.append(format_percent(value))
|
|
110
|
+
elif col in ("impressions", "taps", "installs"):
|
|
111
|
+
values.append(format_number(value))
|
|
112
|
+
elif col in ("spend", "avg_cpt", "avg_cpa"):
|
|
113
|
+
values.append(str(value) if value else "-")
|
|
114
|
+
else:
|
|
115
|
+
values.append(str(value) if value else "-")
|
|
116
|
+
table.add_row(*values)
|
|
117
|
+
|
|
118
|
+
console.print(table)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def print_grand_totals(report: object) -> None:
|
|
122
|
+
"""Print grand totals in a styled panel."""
|
|
123
|
+
if not report.grand_totals or not report.grand_totals.total: # type: ignore
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
total = report.grand_totals.total # type: ignore
|
|
127
|
+
|
|
128
|
+
lines = []
|
|
129
|
+
lines.append(f"[label]Impressions:[/label] [value]{format_number(total.impressions)}[/value]")
|
|
130
|
+
lines.append(f"[label]Taps:[/label] [value]{format_number(total.taps)}[/value]")
|
|
131
|
+
lines.append(f"[label]Installs:[/label] [value]{format_number(total.installs)}[/value]")
|
|
132
|
+
|
|
133
|
+
if total.ttr is not None:
|
|
134
|
+
lines.append(f"[label]TTR:[/label] [value]{format_percent(total.ttr)}[/value]")
|
|
135
|
+
if total.conversion_rate is not None:
|
|
136
|
+
lines.append(f"[label]Conv Rate:[/label] [value]{format_percent(total.conversion_rate)}[/value]")
|
|
137
|
+
if total.local_spend:
|
|
138
|
+
spend = total.local_spend
|
|
139
|
+
lines.append(f"[label]Total Spend:[/label] [value]{spend.amount} {spend.currency}[/value]")
|
|
140
|
+
|
|
141
|
+
panel = Panel(
|
|
142
|
+
"\n".join(lines),
|
|
143
|
+
title="[info]Grand Totals[/info]",
|
|
144
|
+
border_style="cyan",
|
|
145
|
+
padding=(0, 1),
|
|
146
|
+
)
|
|
147
|
+
console.print()
|
|
148
|
+
console.print(panel)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def save_report(
|
|
152
|
+
data: list[dict[str, Any]],
|
|
153
|
+
output: Path,
|
|
154
|
+
columns: list[str],
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Save report to file."""
|
|
157
|
+
suffix = output.suffix.lower()
|
|
158
|
+
|
|
159
|
+
if suffix == ".json":
|
|
160
|
+
output.write_text(json.dumps(data, indent=2, default=str))
|
|
161
|
+
elif suffix == ".csv":
|
|
162
|
+
with output.open("w", newline="") as f:
|
|
163
|
+
writer = csv.DictWriter(f, fieldnames=columns, extrasaction="ignore")
|
|
164
|
+
writer.writeheader()
|
|
165
|
+
writer.writerows(data)
|
|
166
|
+
else:
|
|
167
|
+
print_error("Unsupported Format", f"File format '{suffix}' is not supported", "Supported formats: .json, .csv")
|
|
168
|
+
raise typer.Exit(1)
|
|
169
|
+
|
|
170
|
+
print_success(f"Report saved to {output}")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@app.command("campaigns")
|
|
174
|
+
def campaign_report(
|
|
175
|
+
start: Annotated[
|
|
176
|
+
str,
|
|
177
|
+
typer.Option("--start", "-s", help="Start date (YYYY-MM-DD)"),
|
|
178
|
+
],
|
|
179
|
+
end: Annotated[
|
|
180
|
+
str,
|
|
181
|
+
typer.Option("--end", "-e", help="End date (YYYY-MM-DD)"),
|
|
182
|
+
],
|
|
183
|
+
campaign_ids: Annotated[
|
|
184
|
+
list[int] | None,
|
|
185
|
+
typer.Option("--campaign", "-c", help="Filter by campaign ID(s)"),
|
|
186
|
+
] = None,
|
|
187
|
+
granularity: Annotated[
|
|
188
|
+
GranularityType,
|
|
189
|
+
typer.Option("--granularity", "-g", help="Time granularity"),
|
|
190
|
+
] = GranularityType.DAILY,
|
|
191
|
+
output: Annotated[
|
|
192
|
+
Path | None,
|
|
193
|
+
typer.Option("--output", "-o", help="Save to file (JSON or CSV)"),
|
|
194
|
+
] = None,
|
|
195
|
+
) -> None:
|
|
196
|
+
"""Generate a campaign performance report.
|
|
197
|
+
|
|
198
|
+
Examples:
|
|
199
|
+
asa reports campaigns --start 2024-01-01 --end 2024-01-31
|
|
200
|
+
asa reports campaigns -s 2024-01-01 -e 2024-01-31 --campaign 123
|
|
201
|
+
asa reports campaigns -s 2024-01-01 -e 2024-01-31 -o report.csv
|
|
202
|
+
asa reports campaigns -s 2024-01-01 -e 2024-01-31 --granularity MONTHLY
|
|
203
|
+
"""
|
|
204
|
+
start_date = parse_date(start)
|
|
205
|
+
end_date = parse_date(end)
|
|
206
|
+
|
|
207
|
+
client = get_client()
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
with client:
|
|
211
|
+
with spinner("Generating campaign report..."):
|
|
212
|
+
report = client.reports.campaigns(
|
|
213
|
+
start_date=start_date,
|
|
214
|
+
end_date=end_date,
|
|
215
|
+
campaign_ids=campaign_ids,
|
|
216
|
+
granularity=granularity,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if not report.row:
|
|
220
|
+
print_warning("No data found for the specified period")
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
data = [report_row_to_dict(row) for row in report.row]
|
|
224
|
+
columns = ["campaign", "impressions", "taps", "installs", "ttr", "conv_rate", "spend"]
|
|
225
|
+
|
|
226
|
+
if output:
|
|
227
|
+
save_report(data, output, columns)
|
|
228
|
+
else:
|
|
229
|
+
print_report_table(
|
|
230
|
+
data,
|
|
231
|
+
columns,
|
|
232
|
+
f"Campaign Report ({start} to {end})",
|
|
233
|
+
)
|
|
234
|
+
print_grand_totals(report)
|
|
235
|
+
|
|
236
|
+
except AppleSearchAdsError as e:
|
|
237
|
+
handle_api_error(e)
|
|
238
|
+
raise typer.Exit(1) from None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@app.command("ad-groups")
|
|
242
|
+
def ad_group_report(
|
|
243
|
+
campaign_id: Annotated[int, typer.Argument(help="Campaign ID")],
|
|
244
|
+
start: Annotated[
|
|
245
|
+
str,
|
|
246
|
+
typer.Option("--start", "-s", help="Start date (YYYY-MM-DD)"),
|
|
247
|
+
],
|
|
248
|
+
end: Annotated[
|
|
249
|
+
str,
|
|
250
|
+
typer.Option("--end", "-e", help="End date (YYYY-MM-DD)"),
|
|
251
|
+
],
|
|
252
|
+
granularity: Annotated[
|
|
253
|
+
GranularityType,
|
|
254
|
+
typer.Option("--granularity", "-g", help="Time granularity"),
|
|
255
|
+
] = GranularityType.DAILY,
|
|
256
|
+
output: Annotated[
|
|
257
|
+
Path | None,
|
|
258
|
+
typer.Option("--output", "-o", help="Save to file (JSON or CSV)"),
|
|
259
|
+
] = None,
|
|
260
|
+
) -> None:
|
|
261
|
+
"""Generate an ad group performance report.
|
|
262
|
+
|
|
263
|
+
Examples:
|
|
264
|
+
asa reports ad-groups 123 --start 2024-01-01 --end 2024-01-31
|
|
265
|
+
asa reports ad-groups 123 -s 2024-01-01 -e 2024-01-31 -o report.csv
|
|
266
|
+
"""
|
|
267
|
+
start_date = parse_date(start)
|
|
268
|
+
end_date = parse_date(end)
|
|
269
|
+
|
|
270
|
+
client = get_client()
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
with client:
|
|
274
|
+
with spinner("Generating ad group report..."):
|
|
275
|
+
report = client.reports.ad_groups(
|
|
276
|
+
campaign_id=campaign_id,
|
|
277
|
+
start_date=start_date,
|
|
278
|
+
end_date=end_date,
|
|
279
|
+
granularity=granularity,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
if not report.row:
|
|
283
|
+
print_warning("No data found for the specified period")
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
data = [report_row_to_dict(row) for row in report.row]
|
|
287
|
+
columns = ["ad_group", "impressions", "taps", "installs", "ttr", "conv_rate", "spend"]
|
|
288
|
+
|
|
289
|
+
if output:
|
|
290
|
+
save_report(data, output, columns)
|
|
291
|
+
else:
|
|
292
|
+
print_report_table(
|
|
293
|
+
data,
|
|
294
|
+
columns,
|
|
295
|
+
f"Ad Group Report ({start} to {end})",
|
|
296
|
+
)
|
|
297
|
+
print_grand_totals(report)
|
|
298
|
+
|
|
299
|
+
except AppleSearchAdsError as e:
|
|
300
|
+
handle_api_error(e)
|
|
301
|
+
raise typer.Exit(1) from None
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@app.command("keywords")
|
|
305
|
+
def keyword_report(
|
|
306
|
+
campaign_id: Annotated[int, typer.Argument(help="Campaign ID")],
|
|
307
|
+
start: Annotated[
|
|
308
|
+
str,
|
|
309
|
+
typer.Option("--start", "-s", help="Start date (YYYY-MM-DD)"),
|
|
310
|
+
],
|
|
311
|
+
end: Annotated[
|
|
312
|
+
str,
|
|
313
|
+
typer.Option("--end", "-e", help="End date (YYYY-MM-DD)"),
|
|
314
|
+
],
|
|
315
|
+
ad_group_ids: Annotated[
|
|
316
|
+
list[int] | None,
|
|
317
|
+
typer.Option("--ad-group", "-a", help="Filter by ad group ID(s)"),
|
|
318
|
+
] = None,
|
|
319
|
+
granularity: Annotated[
|
|
320
|
+
GranularityType,
|
|
321
|
+
typer.Option("--granularity", "-g", help="Time granularity"),
|
|
322
|
+
] = GranularityType.DAILY,
|
|
323
|
+
output: Annotated[
|
|
324
|
+
Path | None,
|
|
325
|
+
typer.Option("--output", "-o", help="Save to file (JSON or CSV)"),
|
|
326
|
+
] = None,
|
|
327
|
+
) -> None:
|
|
328
|
+
"""Generate a keyword performance report.
|
|
329
|
+
|
|
330
|
+
Examples:
|
|
331
|
+
asa reports keywords 123 --start 2024-01-01 --end 2024-01-31
|
|
332
|
+
asa reports keywords 123 -s 2024-01-01 -e 2024-01-31 --ad-group 456
|
|
333
|
+
asa reports keywords 123 -s 2024-01-01 -e 2024-01-31 -o keywords.csv
|
|
334
|
+
"""
|
|
335
|
+
start_date = parse_date(start)
|
|
336
|
+
end_date = parse_date(end)
|
|
337
|
+
|
|
338
|
+
client = get_client()
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
with client:
|
|
342
|
+
with spinner("Generating keyword report..."):
|
|
343
|
+
report = client.reports.keywords(
|
|
344
|
+
campaign_id=campaign_id,
|
|
345
|
+
start_date=start_date,
|
|
346
|
+
end_date=end_date,
|
|
347
|
+
ad_group_ids=ad_group_ids,
|
|
348
|
+
granularity=granularity,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
if not report.row:
|
|
352
|
+
print_warning("No data found for the specified period")
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
data = [report_row_to_dict(row) for row in report.row]
|
|
356
|
+
columns = ["keyword", "impressions", "taps", "installs", "ttr", "conv_rate", "spend", "avg_cpt"]
|
|
357
|
+
|
|
358
|
+
if output:
|
|
359
|
+
save_report(data, output, columns)
|
|
360
|
+
else:
|
|
361
|
+
print_report_table(
|
|
362
|
+
data,
|
|
363
|
+
columns,
|
|
364
|
+
f"Keyword Report ({start} to {end})",
|
|
365
|
+
)
|
|
366
|
+
print_grand_totals(report)
|
|
367
|
+
|
|
368
|
+
except AppleSearchAdsError as e:
|
|
369
|
+
handle_api_error(e)
|
|
370
|
+
raise typer.Exit(1) from None
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@app.command("search-terms")
|
|
374
|
+
def search_term_report(
|
|
375
|
+
campaign_id: Annotated[int, typer.Argument(help="Campaign ID")],
|
|
376
|
+
start: Annotated[
|
|
377
|
+
str,
|
|
378
|
+
typer.Option("--start", "-s", help="Start date (YYYY-MM-DD)"),
|
|
379
|
+
],
|
|
380
|
+
end: Annotated[
|
|
381
|
+
str,
|
|
382
|
+
typer.Option("--end", "-e", help="End date (YYYY-MM-DD)"),
|
|
383
|
+
],
|
|
384
|
+
ad_group_id: Annotated[
|
|
385
|
+
int | None,
|
|
386
|
+
typer.Option("--ad-group", "-a", help="Filter by ad group ID"),
|
|
387
|
+
] = None,
|
|
388
|
+
granularity: Annotated[
|
|
389
|
+
GranularityType,
|
|
390
|
+
typer.Option("--granularity", "-g", help="Time granularity"),
|
|
391
|
+
] = GranularityType.DAILY,
|
|
392
|
+
output: Annotated[
|
|
393
|
+
Path | None,
|
|
394
|
+
typer.Option("--output", "-o", help="Save to file (JSON or CSV)"),
|
|
395
|
+
] = None,
|
|
396
|
+
) -> None:
|
|
397
|
+
"""Generate a search term performance report.
|
|
398
|
+
|
|
399
|
+
Shows actual search queries that triggered your ads.
|
|
400
|
+
|
|
401
|
+
Examples:
|
|
402
|
+
asa reports search-terms 123 --start 2024-01-01 --end 2024-01-31
|
|
403
|
+
asa reports search-terms 123 -s 2024-01-01 -e 2024-01-31 -o terms.csv
|
|
404
|
+
"""
|
|
405
|
+
start_date = parse_date(start)
|
|
406
|
+
end_date = parse_date(end)
|
|
407
|
+
|
|
408
|
+
client = get_client()
|
|
409
|
+
|
|
410
|
+
try:
|
|
411
|
+
with client:
|
|
412
|
+
with spinner("Generating search term report..."):
|
|
413
|
+
report = client.reports.search_terms(
|
|
414
|
+
campaign_id=campaign_id,
|
|
415
|
+
start_date=start_date,
|
|
416
|
+
end_date=end_date,
|
|
417
|
+
ad_group_id=ad_group_id,
|
|
418
|
+
granularity=granularity,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
if not report.row:
|
|
422
|
+
print_warning("No data found for the specified period")
|
|
423
|
+
return
|
|
424
|
+
|
|
425
|
+
data = [report_row_to_dict(row) for row in report.row]
|
|
426
|
+
columns = ["search_term", "impressions", "taps", "installs", "ttr", "conv_rate", "spend"]
|
|
427
|
+
|
|
428
|
+
if output:
|
|
429
|
+
save_report(data, output, columns)
|
|
430
|
+
else:
|
|
431
|
+
print_report_table(
|
|
432
|
+
data,
|
|
433
|
+
columns,
|
|
434
|
+
f"Search Term Report ({start} to {end})",
|
|
435
|
+
)
|
|
436
|
+
print_grand_totals(report)
|
|
437
|
+
|
|
438
|
+
except AppleSearchAdsError as e:
|
|
439
|
+
handle_api_error(e)
|
|
440
|
+
raise typer.Exit(1) from None
|