amazon-ads-cli 0.1.3__tar.gz → 0.1.4__tar.gz
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.
- {amazon_ads_cli-0.1.3 → amazon_ads_cli-0.1.4}/PKG-INFO +1 -1
- {amazon_ads_cli-0.1.3 → amazon_ads_cli-0.1.4}/amazon_ads_cli/main.py +266 -4
- {amazon_ads_cli-0.1.3 → amazon_ads_cli-0.1.4}/amazon_ads_cli.egg-info/PKG-INFO +1 -1
- {amazon_ads_cli-0.1.3 → amazon_ads_cli-0.1.4}/setup.py +1 -1
- {amazon_ads_cli-0.1.3 → amazon_ads_cli-0.1.4}/LICENSE +0 -0
- {amazon_ads_cli-0.1.3 → amazon_ads_cli-0.1.4}/README.md +0 -0
- {amazon_ads_cli-0.1.3 → amazon_ads_cli-0.1.4}/amazon_ads_cli/__init__.py +0 -0
- {amazon_ads_cli-0.1.3 → amazon_ads_cli-0.1.4}/amazon_ads_cli/__main__.py +0 -0
- {amazon_ads_cli-0.1.3 → amazon_ads_cli-0.1.4}/amazon_ads_cli.egg-info/SOURCES.txt +0 -0
- {amazon_ads_cli-0.1.3 → amazon_ads_cli-0.1.4}/amazon_ads_cli.egg-info/dependency_links.txt +0 -0
- {amazon_ads_cli-0.1.3 → amazon_ads_cli-0.1.4}/amazon_ads_cli.egg-info/entry_points.txt +0 -0
- {amazon_ads_cli-0.1.3 → amazon_ads_cli-0.1.4}/amazon_ads_cli.egg-info/requires.txt +0 -0
- {amazon_ads_cli-0.1.3 → amazon_ads_cli-0.1.4}/amazon_ads_cli.egg-info/top_level.txt +0 -0
- {amazon_ads_cli-0.1.3 → amazon_ads_cli-0.1.4}/setup.cfg +0 -0
|
@@ -153,14 +153,40 @@ def list_campaigns(ctx):
|
|
|
153
153
|
)
|
|
154
154
|
campaigns = result.payload.get("campaigns", [])
|
|
155
155
|
|
|
156
|
-
click.echo(f"\n{'Campaign':<
|
|
157
|
-
click.echo("-" *
|
|
156
|
+
click.echo(f"\n{'ID':<20} {'Campaign':<28} {'State':<10} {'Budget':<10} {'Type'}")
|
|
157
|
+
click.echo("-" * 85)
|
|
158
158
|
for camp in campaigns:
|
|
159
|
-
|
|
159
|
+
cid = camp["campaignId"][:18]
|
|
160
|
+
name = camp["name"][:26]
|
|
160
161
|
state = camp["state"]
|
|
161
162
|
budget = f"${camp['budget']['budget']}"
|
|
162
163
|
ctype = camp.get("targetingType", "N/A")
|
|
163
|
-
click.echo(f"{name:<
|
|
164
|
+
click.echo(f"{cid:<20} {name:<28} {state:<10} {budget:<10} {ctype}")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@campaigns.command("show")
|
|
168
|
+
@click.argument("campaign-id")
|
|
169
|
+
@click.pass_context
|
|
170
|
+
def show_campaign(ctx, campaign_id):
|
|
171
|
+
"""Show full details for a campaign."""
|
|
172
|
+
result = sponsored_products.CampaignsV3(marketplace=Marketplaces.NA).list_campaigns(
|
|
173
|
+
body={"campaignIdFilter": {"include": [campaign_id]}}
|
|
174
|
+
)
|
|
175
|
+
campaigns = result.payload.get("campaigns", [])
|
|
176
|
+
if not campaigns:
|
|
177
|
+
click.echo(f"❌ Campaign {campaign_id} not found")
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
camp = campaigns[0]
|
|
181
|
+
click.echo(f"\n📋 Campaign: {camp['name']}")
|
|
182
|
+
click.echo(f" ID: {camp['campaignId']}")
|
|
183
|
+
click.echo(f" State: {camp['state']}")
|
|
184
|
+
click.echo(
|
|
185
|
+
f" Budget: ${camp['budget']['budget']}/{camp['budget']['budgetType'].lower()}"
|
|
186
|
+
)
|
|
187
|
+
click.echo(f" Type: {camp.get('targetingType', 'N/A')}")
|
|
188
|
+
click.echo(f" Start: {camp.get('startDate', 'N/A')}")
|
|
189
|
+
click.echo(f" End: {camp.get('endDate', 'N/A') or 'No end date'}")
|
|
164
190
|
|
|
165
191
|
|
|
166
192
|
@campaigns.command("pause")
|
|
@@ -213,6 +239,36 @@ def set_budget(ctx, campaign_id, amount):
|
|
|
213
239
|
click.echo(f"❌ Error: {e}")
|
|
214
240
|
|
|
215
241
|
|
|
242
|
+
@cli.group()
|
|
243
|
+
def adgroups():
|
|
244
|
+
"""Ad group management commands."""
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@adgroups.command("list")
|
|
249
|
+
@click.option("--campaign-id", help="Filter by campaign ID")
|
|
250
|
+
@click.pass_context
|
|
251
|
+
def list_adgroups(ctx, campaign_id):
|
|
252
|
+
"""List all ad groups."""
|
|
253
|
+
body = {}
|
|
254
|
+
if campaign_id:
|
|
255
|
+
body["campaignIdFilter"] = {"include": [campaign_id]}
|
|
256
|
+
|
|
257
|
+
result = sponsored_products.AdGroupsV3(marketplace=Marketplaces.NA).list_ad_groups(
|
|
258
|
+
body=body
|
|
259
|
+
)
|
|
260
|
+
ad_groups = result.payload.get("adGroups", [])
|
|
261
|
+
|
|
262
|
+
click.echo(f"\n{'ID':<20} {'Campaign ID':<20} {'Name':<30} {'State'}")
|
|
263
|
+
click.echo("-" * 85)
|
|
264
|
+
for ag in ad_groups:
|
|
265
|
+
ag_id = ag["adGroupId"][:18]
|
|
266
|
+
camp_id = ag["campaignId"][:18]
|
|
267
|
+
name = ag["name"][:28]
|
|
268
|
+
state = ag["state"]
|
|
269
|
+
click.echo(f"{ag_id:<20} {camp_id:<20} {name:<30} {state}")
|
|
270
|
+
|
|
271
|
+
|
|
216
272
|
@cli.group()
|
|
217
273
|
def keywords():
|
|
218
274
|
"""Keyword management commands."""
|
|
@@ -243,6 +299,28 @@ def list_keywords(ctx, campaign_id):
|
|
|
243
299
|
click.echo(f"{text:<35} {match:<10} {bid:<8} {state}")
|
|
244
300
|
|
|
245
301
|
|
|
302
|
+
@keywords.command("list-all")
|
|
303
|
+
@click.pass_context
|
|
304
|
+
def list_all_keywords(ctx):
|
|
305
|
+
"""List all keywords across all campaigns."""
|
|
306
|
+
result = sponsored_products.KeywordsV3(marketplace=Marketplaces.NA).list_keywords(
|
|
307
|
+
body={}
|
|
308
|
+
)
|
|
309
|
+
keywords = result.payload.get("keywords", [])
|
|
310
|
+
|
|
311
|
+
click.echo(
|
|
312
|
+
f"\n{'Campaign ID':<20} {'Keyword':<35} {'Match':<10} {'Bid':<8} {'State'}"
|
|
313
|
+
)
|
|
314
|
+
click.echo("-" * 90)
|
|
315
|
+
for kw in keywords:
|
|
316
|
+
camp_id = kw.get("campaignId", "N/A")[:18]
|
|
317
|
+
text = kw["keywordText"][:33]
|
|
318
|
+
match = kw["matchType"]
|
|
319
|
+
bid = f"${kw['bid']}"
|
|
320
|
+
state = kw["state"]
|
|
321
|
+
click.echo(f"{camp_id:<20} {text:<35} {match:<10} {bid:<8} {state}")
|
|
322
|
+
|
|
323
|
+
|
|
246
324
|
@keywords.command("add")
|
|
247
325
|
@click.argument("campaign-id")
|
|
248
326
|
@click.argument("ad-group-id")
|
|
@@ -317,6 +395,24 @@ def list_negatives(ctx, campaign_id):
|
|
|
317
395
|
click.echo(f"{text:<35} {match:<15}")
|
|
318
396
|
|
|
319
397
|
|
|
398
|
+
@negatives.command("list-all")
|
|
399
|
+
@click.pass_context
|
|
400
|
+
def list_all_negatives(ctx):
|
|
401
|
+
"""List all negative keywords across all campaigns."""
|
|
402
|
+
result = sponsored_products.NegativeKeywordsV3(
|
|
403
|
+
marketplace=Marketplaces.NA
|
|
404
|
+
).list_negative_keywords(body={"stateFilter": {"include": ["ENABLED"]}})
|
|
405
|
+
negatives = result.payload.get("negativeKeywords", [])
|
|
406
|
+
|
|
407
|
+
click.echo(f"\n{'Campaign ID':<20} {'Negative Keyword':<35} {'Match':<15}")
|
|
408
|
+
click.echo("-" * 80)
|
|
409
|
+
for neg in negatives:
|
|
410
|
+
camp_id = neg.get("campaignId", "N/A")[:18]
|
|
411
|
+
text = neg["keywordText"][:33]
|
|
412
|
+
match = neg["matchType"]
|
|
413
|
+
click.echo(f"{camp_id:<20} {text:<35} {match:<15}")
|
|
414
|
+
|
|
415
|
+
|
|
320
416
|
@negatives.command("add")
|
|
321
417
|
@click.argument("campaign-id")
|
|
322
418
|
@click.argument("ad-group-id")
|
|
@@ -366,6 +462,33 @@ def remove_negative(ctx, negative_keyword_id):
|
|
|
366
462
|
click.echo(f"❌ Error: {e}")
|
|
367
463
|
|
|
368
464
|
|
|
465
|
+
@cli.group()
|
|
466
|
+
def targets():
|
|
467
|
+
"""Product target management commands."""
|
|
468
|
+
pass
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
@targets.command("list-all")
|
|
472
|
+
@click.pass_context
|
|
473
|
+
def list_all_targets(ctx):
|
|
474
|
+
"""List all product targets across all campaigns."""
|
|
475
|
+
result = sponsored_products.TargetsV3(
|
|
476
|
+
marketplace=Marketplaces.NA
|
|
477
|
+
).list_product_targets(body={})
|
|
478
|
+
targets_list = result.payload.get("productTargets", [])
|
|
479
|
+
|
|
480
|
+
click.echo(
|
|
481
|
+
f"\n{'Campaign ID':<20} {'Ad Group ID':<20} {'Expression':<40} {'State'}"
|
|
482
|
+
)
|
|
483
|
+
click.echo("-" * 95)
|
|
484
|
+
for t in targets_list:
|
|
485
|
+
camp_id = t.get("campaignId", "N/A")[:18]
|
|
486
|
+
ag_id = t.get("adGroupId", "N/A")[:18]
|
|
487
|
+
expr = str(t.get("expression", []))[:38]
|
|
488
|
+
state = t.get("state", "N/A")
|
|
489
|
+
click.echo(f"{camp_id:<20} {ag_id:<20} {expr:<40} {state}")
|
|
490
|
+
|
|
491
|
+
|
|
369
492
|
@cli.group()
|
|
370
493
|
def report():
|
|
371
494
|
"""Report commands."""
|
|
@@ -463,6 +586,145 @@ def report_today(ctx):
|
|
|
463
586
|
click.echo(f"❌ Error: {e}")
|
|
464
587
|
|
|
465
588
|
|
|
589
|
+
@report.command("status")
|
|
590
|
+
@click.argument("report-id")
|
|
591
|
+
@click.pass_context
|
|
592
|
+
def report_status(ctx, report_id):
|
|
593
|
+
"""Check status of an existing report."""
|
|
594
|
+
try:
|
|
595
|
+
result = reports.Reports(marketplace=Marketplaces.NA).get_report(
|
|
596
|
+
reportId=report_id
|
|
597
|
+
)
|
|
598
|
+
payload = result.payload
|
|
599
|
+
|
|
600
|
+
status = payload.get("status")
|
|
601
|
+
name = payload.get("name", "N/A")
|
|
602
|
+
start = payload.get("startDate", "N/A")
|
|
603
|
+
end = payload.get("endDate", "N/A")
|
|
604
|
+
created = payload.get("createdAt", "N/A")
|
|
605
|
+
updated = payload.get("updatedAt", "N/A")
|
|
606
|
+
|
|
607
|
+
click.echo(f"\n📊 Report: {name}")
|
|
608
|
+
click.echo(f" ID: {report_id}")
|
|
609
|
+
click.echo(f" Status: {status}")
|
|
610
|
+
click.echo(f" Date Range: {start} to {end}")
|
|
611
|
+
click.echo(f" Created: {created}")
|
|
612
|
+
click.echo(f" Updated: {updated}")
|
|
613
|
+
|
|
614
|
+
if status == "COMPLETED":
|
|
615
|
+
size = payload.get("fileSize", "N/A")
|
|
616
|
+
click.echo(f" File Size: {size}")
|
|
617
|
+
click.echo("\n✅ Report is ready. Download with:")
|
|
618
|
+
click.echo(f" amz-ads report download {report_id}")
|
|
619
|
+
elif status == "FAILED":
|
|
620
|
+
reason = payload.get("failureReason", "Unknown")
|
|
621
|
+
click.echo(f"\n❌ Failed: {reason}")
|
|
622
|
+
else:
|
|
623
|
+
click.echo("\n⏳ Still processing. Check again later.")
|
|
624
|
+
|
|
625
|
+
except Exception as e:
|
|
626
|
+
click.echo(f"❌ Error: {e}")
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
@report.command("download")
|
|
630
|
+
@click.argument("report-id")
|
|
631
|
+
@click.option(
|
|
632
|
+
"--format",
|
|
633
|
+
"fmt",
|
|
634
|
+
default="table",
|
|
635
|
+
type=click.Choice(["table", "json", "csv"]),
|
|
636
|
+
help="Output format",
|
|
637
|
+
)
|
|
638
|
+
@click.option("--output", "-o", help="Save to file instead of stdout")
|
|
639
|
+
@click.pass_context
|
|
640
|
+
def report_download(ctx, report_id, fmt, output):
|
|
641
|
+
"""Download a completed report by ID."""
|
|
642
|
+
try:
|
|
643
|
+
result = reports.Reports(marketplace=Marketplaces.NA).get_report(
|
|
644
|
+
reportId=report_id
|
|
645
|
+
)
|
|
646
|
+
status = result.payload.get("status")
|
|
647
|
+
|
|
648
|
+
if status != "COMPLETED":
|
|
649
|
+
click.echo(f"❌ Report is not ready (status: {status})")
|
|
650
|
+
click.echo(f" Check status: amz-ads report status {report_id}")
|
|
651
|
+
return
|
|
652
|
+
|
|
653
|
+
url = result.payload.get("url")
|
|
654
|
+
if not url:
|
|
655
|
+
click.echo("❌ No download URL available")
|
|
656
|
+
return
|
|
657
|
+
|
|
658
|
+
import gzip
|
|
659
|
+
|
|
660
|
+
import requests
|
|
661
|
+
|
|
662
|
+
click.echo("Downloading report...")
|
|
663
|
+
response = requests.get(url)
|
|
664
|
+
data = gzip.decompress(response.content)
|
|
665
|
+
report_data = json.loads(data)
|
|
666
|
+
|
|
667
|
+
if fmt == "json":
|
|
668
|
+
output_text = json.dumps(report_data, indent=2)
|
|
669
|
+
elif fmt == "csv":
|
|
670
|
+
if not report_data:
|
|
671
|
+
click.echo("❌ Report is empty")
|
|
672
|
+
return
|
|
673
|
+
import csv
|
|
674
|
+
import io
|
|
675
|
+
|
|
676
|
+
buf = io.StringIO()
|
|
677
|
+
writer = csv.DictWriter(buf, fieldnames=report_data[0].keys())
|
|
678
|
+
writer.writeheader()
|
|
679
|
+
writer.writerows(report_data)
|
|
680
|
+
output_text = buf.getvalue()
|
|
681
|
+
else:
|
|
682
|
+
# Table format
|
|
683
|
+
if not report_data:
|
|
684
|
+
click.echo("❌ Report is empty")
|
|
685
|
+
return
|
|
686
|
+
|
|
687
|
+
# Detect report type by columns
|
|
688
|
+
columns = report_data[0].keys()
|
|
689
|
+
|
|
690
|
+
if "searchTerm" in columns:
|
|
691
|
+
# Search terms report
|
|
692
|
+
report_data.sort(
|
|
693
|
+
key=lambda x: float(x.get("cost", 0) or 0), reverse=True
|
|
694
|
+
)
|
|
695
|
+
output_text = f"\n{'Search Term':<40} {'Campaign':<20} {'Spend':>8} {'Sales':>8} {'ACOS'}\n"
|
|
696
|
+
output_text += "-" * 90 + "\n"
|
|
697
|
+
for row in report_data[:50]:
|
|
698
|
+
term = row.get("searchTerm", "N/A")[:38]
|
|
699
|
+
camp = row.get("campaignName", "N/A")[:18]
|
|
700
|
+
cost = float(row.get("cost", 0) or 0)
|
|
701
|
+
sales = float(row.get("sales14d", 0) or 0)
|
|
702
|
+
acos = (cost / sales * 100) if sales > 0 else 0
|
|
703
|
+
output_text += f"{term:<40} {camp:<20} ${cost:>7.2f} ${sales:>7.2f} {acos:>5.1f}%\n"
|
|
704
|
+
else:
|
|
705
|
+
# Campaign report
|
|
706
|
+
output_text = f"\n{'Campaign':<30} {'Impr':>8} {'Clicks':>7} {'Spend':>8} {'Sales':>8} {'ACOS'}\n"
|
|
707
|
+
output_text += "-" * 75 + "\n"
|
|
708
|
+
for row in report_data:
|
|
709
|
+
camp_name = row.get("campaignName", "N/A")[:28]
|
|
710
|
+
impr = int(row.get("impressions", 0) or 0)
|
|
711
|
+
clicks = int(row.get("clicks", 0) or 0)
|
|
712
|
+
cost = float(row.get("cost", 0) or 0)
|
|
713
|
+
sales = float(row.get("sales14d", 0) or 0)
|
|
714
|
+
acos = (cost / sales * 100) if sales > 0 else 0
|
|
715
|
+
output_text += f"{camp_name:<30} {impr:>8} {clicks:>7} ${cost:>7.2f} ${sales:>7.2f} {acos:>5.1f}%\n"
|
|
716
|
+
|
|
717
|
+
if output:
|
|
718
|
+
with open(output, "w") as f:
|
|
719
|
+
f.write(output_text)
|
|
720
|
+
click.echo(f"✅ Saved to {output}")
|
|
721
|
+
else:
|
|
722
|
+
click.echo(output_text)
|
|
723
|
+
|
|
724
|
+
except Exception as e:
|
|
725
|
+
click.echo(f"❌ Error: {e}")
|
|
726
|
+
|
|
727
|
+
|
|
466
728
|
@report.command("search-terms")
|
|
467
729
|
@click.option("--days", default=7, help="Number of days to look back")
|
|
468
730
|
@click.pass_context
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|