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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amazon-ads-cli
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: CLI tool for Amazon Advertising API v3
5
5
  Home-page: https://github.com/stellaraether/amazon-ads-cli
6
6
  Author: Lunan Li
@@ -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':<30} {'State':<10} {'Budget':<10} {'Type'}")
157
- click.echo("-" * 65)
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
- name = camp["name"][:28]
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:<30} {state:<10} {budget:<10} {ctype}")
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amazon-ads-cli
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: CLI tool for Amazon Advertising API v3
5
5
  Home-page: https://github.com/stellaraether/amazon-ads-cli
6
6
  Author: Lunan Li
@@ -2,7 +2,7 @@ from setuptools import find_packages, setup
2
2
 
3
3
  setup(
4
4
  name="amazon-ads-cli",
5
- version="0.1.3",
5
+ version="0.1.4",
6
6
  description="CLI tool for Amazon Advertising API v3",
7
7
  author="Lunan Li",
8
8
  author_email="lunan@stellaraether.com",
File without changes
File without changes
File without changes