amazon-ads-cli 0.1.0__tar.gz → 0.1.2__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.0 → amazon_ads_cli-0.1.2}/PKG-INFO +1 -1
- {amazon_ads_cli-0.1.0 → amazon_ads_cli-0.1.2}/README.md +25 -5
- amazon_ads_cli-0.1.2/amazon_ads_cli/main.py +420 -0
- {amazon_ads_cli-0.1.0 → amazon_ads_cli-0.1.2}/amazon_ads_cli.egg-info/PKG-INFO +1 -1
- {amazon_ads_cli-0.1.0 → amazon_ads_cli-0.1.2}/setup.py +2 -2
- amazon_ads_cli-0.1.0/amazon_ads_cli/main.py +0 -179
- {amazon_ads_cli-0.1.0 → amazon_ads_cli-0.1.2}/amazon_ads_cli/__init__.py +0 -0
- {amazon_ads_cli-0.1.0 → amazon_ads_cli-0.1.2}/amazon_ads_cli.egg-info/SOURCES.txt +0 -0
- {amazon_ads_cli-0.1.0 → amazon_ads_cli-0.1.2}/amazon_ads_cli.egg-info/dependency_links.txt +0 -0
- {amazon_ads_cli-0.1.0 → amazon_ads_cli-0.1.2}/amazon_ads_cli.egg-info/entry_points.txt +0 -0
- {amazon_ads_cli-0.1.0 → amazon_ads_cli-0.1.2}/amazon_ads_cli.egg-info/requires.txt +0 -0
- {amazon_ads_cli-0.1.0 → amazon_ads_cli-0.1.2}/amazon_ads_cli.egg-info/top_level.txt +0 -0
- {amazon_ads_cli-0.1.0 → amazon_ads_cli-0.1.2}/setup.cfg +0 -0
|
@@ -28,29 +28,49 @@ default:
|
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
30
|
# List campaigns
|
|
31
|
-
|
|
31
|
+
python3 -m amazon_ads_cli.main campaigns list
|
|
32
32
|
|
|
33
33
|
# Pause campaign
|
|
34
|
-
|
|
34
|
+
python3 -m amazon_ads_cli.main campaigns pause <campaign-id>
|
|
35
35
|
|
|
36
36
|
# Enable campaign
|
|
37
|
-
|
|
37
|
+
python3 -m amazon_ads_cli.main campaigns enable <campaign-id>
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
### Keywords
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
43
|
# List keywords for a campaign
|
|
44
|
-
|
|
44
|
+
python3 -m amazon_ads_cli.main keywords list <campaign-id>
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
### Reports
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
50
|
# Get today's performance
|
|
51
|
-
|
|
51
|
+
python3 -m amazon_ads_cli.main report today
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
## Development
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Clone repo
|
|
58
|
+
git clone https://github.com/stellaraether/amazon-ads-cli.git
|
|
59
|
+
cd amazon-ads-cli
|
|
60
|
+
|
|
61
|
+
# Install in editable mode
|
|
62
|
+
pip install -e .
|
|
63
|
+
|
|
64
|
+
# Run locally
|
|
65
|
+
python3 -m amazon_ads_cli.main campaigns list
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Requirements
|
|
69
|
+
|
|
70
|
+
- Python 3.8+
|
|
71
|
+
- `python-amazon-ad-api` library
|
|
72
|
+
- Amazon Advertising API credentials
|
|
73
|
+
|
|
54
74
|
## License
|
|
55
75
|
|
|
56
76
|
MIT
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Amazon Ads CLI - Command line interface for Amazon Advertising API v3."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from ad_api.api import reports, sponsored_products
|
|
9
|
+
from ad_api.base import Marketplaces
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.group()
|
|
13
|
+
@click.option("--profile", "-p", default="default", help="Credential profile")
|
|
14
|
+
@click.pass_context
|
|
15
|
+
def cli(ctx, profile):
|
|
16
|
+
"""Amazon Ads CLI - Manage campaigns, keywords, and reports."""
|
|
17
|
+
ctx.ensure_object(dict)
|
|
18
|
+
ctx.obj["profile"] = profile
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@cli.group()
|
|
22
|
+
def campaigns():
|
|
23
|
+
"""Campaign management commands."""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@campaigns.command("list")
|
|
28
|
+
@click.pass_context
|
|
29
|
+
def list_campaigns(ctx):
|
|
30
|
+
"""List all campaigns."""
|
|
31
|
+
result = sponsored_products.CampaignsV3(marketplace=Marketplaces.NA).list_campaigns(
|
|
32
|
+
body={}
|
|
33
|
+
)
|
|
34
|
+
campaigns = result.payload.get("campaigns", [])
|
|
35
|
+
|
|
36
|
+
click.echo(f"\n{'Campaign':<30} {'State':<10} {'Budget':<10} {'Type'}")
|
|
37
|
+
click.echo("-" * 65)
|
|
38
|
+
for camp in campaigns:
|
|
39
|
+
name = camp["name"][:28]
|
|
40
|
+
state = camp["state"]
|
|
41
|
+
budget = f"${camp['budget']['budget']}"
|
|
42
|
+
ctype = camp.get("targetingType", "N/A")
|
|
43
|
+
click.echo(f"{name:<30} {state:<10} {budget:<10} {ctype}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@campaigns.command("pause")
|
|
47
|
+
@click.argument("campaign-id")
|
|
48
|
+
@click.pass_context
|
|
49
|
+
def pause_campaign(ctx, campaign_id):
|
|
50
|
+
"""Pause a campaign."""
|
|
51
|
+
try:
|
|
52
|
+
sponsored_products.CampaignsV3(marketplace=Marketplaces.NA).edit_campaigns(
|
|
53
|
+
body={"campaigns": [{"campaignId": campaign_id, "state": "PAUSED"}]}
|
|
54
|
+
)
|
|
55
|
+
click.echo(f"✅ Campaign {campaign_id} paused")
|
|
56
|
+
except Exception as e:
|
|
57
|
+
click.echo(f"❌ Error: {e}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@campaigns.command("enable")
|
|
61
|
+
@click.argument("campaign-id")
|
|
62
|
+
@click.pass_context
|
|
63
|
+
def enable_campaign(ctx, campaign_id):
|
|
64
|
+
"""Enable a campaign."""
|
|
65
|
+
try:
|
|
66
|
+
sponsored_products.CampaignsV3(marketplace=Marketplaces.NA).edit_campaigns(
|
|
67
|
+
body={"campaigns": [{"campaignId": campaign_id, "state": "ENABLED"}]}
|
|
68
|
+
)
|
|
69
|
+
click.echo(f"✅ Campaign {campaign_id} enabled")
|
|
70
|
+
except Exception as e:
|
|
71
|
+
click.echo(f"❌ Error: {e}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@campaigns.command("budget")
|
|
75
|
+
@click.argument("campaign-id")
|
|
76
|
+
@click.argument("amount", type=float)
|
|
77
|
+
@click.pass_context
|
|
78
|
+
def set_budget(ctx, campaign_id, amount):
|
|
79
|
+
"""Set campaign daily budget."""
|
|
80
|
+
try:
|
|
81
|
+
sponsored_products.CampaignsV3(marketplace=Marketplaces.NA).edit_campaigns(
|
|
82
|
+
body={
|
|
83
|
+
"campaigns": [
|
|
84
|
+
{
|
|
85
|
+
"campaignId": campaign_id,
|
|
86
|
+
"budget": {"budget": amount, "budgetType": "DAILY"},
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
click.echo(f"✅ Campaign {campaign_id} budget set to ${amount}/day")
|
|
92
|
+
except Exception as e:
|
|
93
|
+
click.echo(f"❌ Error: {e}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@cli.group()
|
|
97
|
+
def keywords():
|
|
98
|
+
"""Keyword management commands."""
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@keywords.command("list")
|
|
103
|
+
@click.argument("campaign-id")
|
|
104
|
+
@click.pass_context
|
|
105
|
+
def list_keywords(ctx, campaign_id):
|
|
106
|
+
"""List keywords for a campaign."""
|
|
107
|
+
result = sponsored_products.KeywordsV3(marketplace=Marketplaces.NA).list_keywords(
|
|
108
|
+
body={}
|
|
109
|
+
)
|
|
110
|
+
keywords = [
|
|
111
|
+
k
|
|
112
|
+
for k in result.payload.get("keywords", [])
|
|
113
|
+
if k.get("campaignId") == campaign_id
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
click.echo(f"\n{'Keyword':<35} {'Match':<10} {'Bid':<8} {'State'}")
|
|
117
|
+
click.echo("-" * 70)
|
|
118
|
+
for kw in keywords:
|
|
119
|
+
text = kw["keywordText"][:33]
|
|
120
|
+
match = kw["matchType"]
|
|
121
|
+
bid = f"${kw['bid']}"
|
|
122
|
+
state = kw["state"]
|
|
123
|
+
click.echo(f"{text:<35} {match:<10} {bid:<8} {state}")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@keywords.command("add")
|
|
127
|
+
@click.argument("campaign-id")
|
|
128
|
+
@click.argument("ad-group-id")
|
|
129
|
+
@click.argument("keyword-text")
|
|
130
|
+
@click.option("--match-type", default="EXACT", help="Match type: EXACT, PHRASE, BROAD")
|
|
131
|
+
@click.option("--bid", default=1.0, help="Bid amount")
|
|
132
|
+
@click.pass_context
|
|
133
|
+
def add_keyword(ctx, campaign_id, ad_group_id, keyword_text, match_type, bid):
|
|
134
|
+
"""Add a keyword to a campaign."""
|
|
135
|
+
try:
|
|
136
|
+
sponsored_products.KeywordsV3(marketplace=Marketplaces.NA).create_keyword(
|
|
137
|
+
body={
|
|
138
|
+
"keywords": [
|
|
139
|
+
{
|
|
140
|
+
"campaignId": campaign_id,
|
|
141
|
+
"adGroupId": ad_group_id,
|
|
142
|
+
"keywordText": keyword_text,
|
|
143
|
+
"matchType": match_type,
|
|
144
|
+
"bid": bid,
|
|
145
|
+
"state": "ENABLED",
|
|
146
|
+
}
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
click.echo(f"✅ Added keyword: {keyword_text} ({match_type}) - ${bid}")
|
|
151
|
+
except Exception as e:
|
|
152
|
+
click.echo(f"❌ Error: {e}")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@keywords.command("bid")
|
|
156
|
+
@click.argument("keyword-id")
|
|
157
|
+
@click.argument("amount", type=float)
|
|
158
|
+
@click.pass_context
|
|
159
|
+
def set_bid(ctx, keyword_id, amount):
|
|
160
|
+
"""Update keyword bid."""
|
|
161
|
+
try:
|
|
162
|
+
sponsored_products.KeywordsV3(marketplace=Marketplaces.NA).edit_keyword(
|
|
163
|
+
keywordId=keyword_id,
|
|
164
|
+
body={"keywords": [{"keywordId": keyword_id, "bid": amount}]},
|
|
165
|
+
)
|
|
166
|
+
click.echo(f"✅ Keyword {keyword_id} bid updated to ${amount}")
|
|
167
|
+
except Exception as e:
|
|
168
|
+
click.echo(f"❌ Error: {e}")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@cli.group()
|
|
172
|
+
def negatives():
|
|
173
|
+
"""Negative keyword management commands."""
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@negatives.command("list")
|
|
178
|
+
@click.argument("campaign-id")
|
|
179
|
+
@click.pass_context
|
|
180
|
+
def list_negatives(ctx, campaign_id):
|
|
181
|
+
"""List negative keywords for a campaign."""
|
|
182
|
+
result = sponsored_products.NegativeKeywordsV3(
|
|
183
|
+
marketplace=Marketplaces.NA
|
|
184
|
+
).list_negative_keywords(body={"campaignIdFilter": {"include": [campaign_id]}})
|
|
185
|
+
negatives = result.payload.get("negativeKeywords", [])
|
|
186
|
+
|
|
187
|
+
click.echo(f"\n{'Negative Keyword':<35} {'Match':<15}")
|
|
188
|
+
click.echo("-" * 55)
|
|
189
|
+
for neg in negatives:
|
|
190
|
+
text = neg["keywordText"][:33]
|
|
191
|
+
match = neg["matchType"]
|
|
192
|
+
click.echo(f"{text:<35} {match:<15}")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@negatives.command("add")
|
|
196
|
+
@click.argument("campaign-id")
|
|
197
|
+
@click.argument("keyword-text")
|
|
198
|
+
@click.option(
|
|
199
|
+
"--match-type",
|
|
200
|
+
default="NEGATIVE_PHRASE",
|
|
201
|
+
help="Match type: NEGATIVE_EXACT, NEGATIVE_PHRASE",
|
|
202
|
+
)
|
|
203
|
+
@click.pass_context
|
|
204
|
+
def add_negative(ctx, campaign_id, keyword_text, match_type):
|
|
205
|
+
"""Add a negative keyword to a campaign."""
|
|
206
|
+
try:
|
|
207
|
+
sponsored_products.NegativeKeywordsV3(
|
|
208
|
+
marketplace=Marketplaces.NA
|
|
209
|
+
).create_negative_keywords(
|
|
210
|
+
body={
|
|
211
|
+
"negativeKeywords": [
|
|
212
|
+
{
|
|
213
|
+
"campaignId": campaign_id,
|
|
214
|
+
"keywordText": keyword_text,
|
|
215
|
+
"matchType": match_type,
|
|
216
|
+
"state": "ENABLED",
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
click.echo(f"✅ Added negative keyword: {keyword_text} ({match_type})")
|
|
222
|
+
except Exception as e:
|
|
223
|
+
click.echo(f"❌ Error: {e}")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@cli.group()
|
|
227
|
+
def report():
|
|
228
|
+
"""Report commands."""
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@report.command("today")
|
|
233
|
+
@click.pass_context
|
|
234
|
+
def report_today(ctx):
|
|
235
|
+
"""Get today's performance report."""
|
|
236
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
237
|
+
|
|
238
|
+
click.echo(f"Requesting report for {today}...")
|
|
239
|
+
|
|
240
|
+
report_body = {
|
|
241
|
+
"name": f"SP_Today_{today}",
|
|
242
|
+
"startDate": today,
|
|
243
|
+
"endDate": today,
|
|
244
|
+
"configuration": {
|
|
245
|
+
"adProduct": "SPONSORED_PRODUCTS",
|
|
246
|
+
"columns": [
|
|
247
|
+
"impressions",
|
|
248
|
+
"clicks",
|
|
249
|
+
"cost",
|
|
250
|
+
"purchases14d",
|
|
251
|
+
"sales14d",
|
|
252
|
+
"campaignName",
|
|
253
|
+
"campaignId",
|
|
254
|
+
],
|
|
255
|
+
"reportTypeId": "spCampaigns",
|
|
256
|
+
"format": "GZIP_JSON",
|
|
257
|
+
"groupBy": ["campaign"],
|
|
258
|
+
"timeUnit": "SUMMARY",
|
|
259
|
+
},
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
# Submit report
|
|
264
|
+
result = reports.Reports(marketplace=Marketplaces.NA).post_report(
|
|
265
|
+
body=report_body
|
|
266
|
+
)
|
|
267
|
+
report_id = result.payload["reportId"]
|
|
268
|
+
|
|
269
|
+
click.echo(f"Report submitted: {report_id}")
|
|
270
|
+
click.echo("Polling for completion...")
|
|
271
|
+
|
|
272
|
+
# Poll
|
|
273
|
+
import time
|
|
274
|
+
|
|
275
|
+
for i in range(20):
|
|
276
|
+
result = reports.Reports(marketplace=Marketplaces.NA).get_report(
|
|
277
|
+
reportId=report_id
|
|
278
|
+
)
|
|
279
|
+
status = result.payload.get("status")
|
|
280
|
+
|
|
281
|
+
if status == "COMPLETED":
|
|
282
|
+
# Download
|
|
283
|
+
import gzip
|
|
284
|
+
|
|
285
|
+
import requests
|
|
286
|
+
|
|
287
|
+
url = result.payload.get("url")
|
|
288
|
+
response = requests.get(url)
|
|
289
|
+
data = gzip.decompress(response.content)
|
|
290
|
+
report_data = json.loads(data)
|
|
291
|
+
|
|
292
|
+
click.echo(
|
|
293
|
+
f"\n{'Campaign':<30} {'Impr':>8} {'Clicks':>7} {'Spend':>8} {'Sales':>8} {'ACOS'}"
|
|
294
|
+
)
|
|
295
|
+
click.echo("-" * 75)
|
|
296
|
+
|
|
297
|
+
for row in report_data:
|
|
298
|
+
camp_name = row.get("campaignName", "N/A")[:28]
|
|
299
|
+
impr = int(row.get("impressions", 0) or 0)
|
|
300
|
+
clicks = int(row.get("clicks", 0) or 0)
|
|
301
|
+
cost = float(row.get("cost", 0) or 0)
|
|
302
|
+
sales = float(row.get("sales14d", 0) or 0)
|
|
303
|
+
acos = (cost / sales * 100) if sales > 0 else 0
|
|
304
|
+
|
|
305
|
+
click.echo(
|
|
306
|
+
f"{camp_name:<30} {impr:>8} {clicks:>7} ${cost:>7.2f} ${sales:>7.2f} {acos:>5.1f}%"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
elif status == "FAILED":
|
|
312
|
+
click.echo(f"❌ Report failed: {result.payload.get('failureReason')}")
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
time.sleep(3)
|
|
316
|
+
|
|
317
|
+
click.echo("⏳ Report still processing...")
|
|
318
|
+
|
|
319
|
+
except Exception as e:
|
|
320
|
+
click.echo(f"❌ Error: {e}")
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@report.command("search-terms")
|
|
324
|
+
@click.option("--days", default=7, help="Number of days to look back")
|
|
325
|
+
@click.pass_context
|
|
326
|
+
def search_terms_report(ctx, days):
|
|
327
|
+
"""Get search term report."""
|
|
328
|
+
end_date = datetime.now().strftime("%Y-%m-%d")
|
|
329
|
+
start_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
|
330
|
+
|
|
331
|
+
click.echo(f"Requesting search term report: {start_date} to {end_date}...")
|
|
332
|
+
|
|
333
|
+
report_body = {
|
|
334
|
+
"name": f"SP_SearchTerms_{start_date}_{end_date}",
|
|
335
|
+
"startDate": start_date,
|
|
336
|
+
"endDate": end_date,
|
|
337
|
+
"configuration": {
|
|
338
|
+
"adProduct": "SPONSORED_PRODUCTS",
|
|
339
|
+
"columns": [
|
|
340
|
+
"impressions",
|
|
341
|
+
"clicks",
|
|
342
|
+
"cost",
|
|
343
|
+
"purchases14d",
|
|
344
|
+
"sales14d",
|
|
345
|
+
"searchTerm",
|
|
346
|
+
"matchType",
|
|
347
|
+
"campaignName",
|
|
348
|
+
"keyword",
|
|
349
|
+
],
|
|
350
|
+
"reportTypeId": "spSearchTerm",
|
|
351
|
+
"format": "GZIP_JSON",
|
|
352
|
+
"groupBy": ["searchTerm"],
|
|
353
|
+
"timeUnit": "SUMMARY",
|
|
354
|
+
},
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
result = reports.Reports(marketplace=Marketplaces.NA).post_report(
|
|
359
|
+
body=report_body
|
|
360
|
+
)
|
|
361
|
+
report_id = result.payload["reportId"]
|
|
362
|
+
|
|
363
|
+
click.echo(f"Report submitted: {report_id}")
|
|
364
|
+
click.echo("Polling for completion...")
|
|
365
|
+
|
|
366
|
+
import time
|
|
367
|
+
|
|
368
|
+
for i in range(30):
|
|
369
|
+
result = reports.Reports(marketplace=Marketplaces.NA).get_report(
|
|
370
|
+
reportId=report_id
|
|
371
|
+
)
|
|
372
|
+
status = result.payload.get("status")
|
|
373
|
+
|
|
374
|
+
if status == "COMPLETED":
|
|
375
|
+
import gzip
|
|
376
|
+
|
|
377
|
+
import requests
|
|
378
|
+
|
|
379
|
+
url = result.payload.get("url")
|
|
380
|
+
response = requests.get(url)
|
|
381
|
+
data = gzip.decompress(response.content)
|
|
382
|
+
report_data = json.loads(data)
|
|
383
|
+
|
|
384
|
+
# Sort by cost (highest first)
|
|
385
|
+
report_data.sort(
|
|
386
|
+
key=lambda x: float(x.get("cost", 0) or 0), reverse=True
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
click.echo(
|
|
390
|
+
f"\n{'Search Term':<40} {'Campaign':<20} {'Spend':>8} {'Sales':>8} {'ACOS'}"
|
|
391
|
+
)
|
|
392
|
+
click.echo("-" * 90)
|
|
393
|
+
|
|
394
|
+
for row in report_data[:20]:
|
|
395
|
+
term = row.get("searchTerm", "N/A")[:38]
|
|
396
|
+
camp = row.get("campaignName", "N/A")[:18]
|
|
397
|
+
cost = float(row.get("cost", 0) or 0)
|
|
398
|
+
sales = float(row.get("sales14d", 0) or 0)
|
|
399
|
+
acos = (cost / sales * 100) if sales > 0 else 0
|
|
400
|
+
|
|
401
|
+
click.echo(
|
|
402
|
+
f"{term:<40} {camp:<20} ${cost:>7.2f} ${sales:>7.2f} {acos:>5.1f}%"
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
elif status == "FAILED":
|
|
408
|
+
click.echo(f"❌ Report failed: {result.payload.get('failureReason')}")
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
time.sleep(5)
|
|
412
|
+
|
|
413
|
+
click.echo("⏳ Report still processing...")
|
|
414
|
+
|
|
415
|
+
except Exception as e:
|
|
416
|
+
click.echo(f"❌ Error: {e}")
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
if __name__ == "__main__":
|
|
420
|
+
cli()
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
from setuptools import
|
|
1
|
+
from setuptools import find_packages, setup
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="amazon-ads-cli",
|
|
5
|
-
version="0.1.
|
|
5
|
+
version="0.1.2",
|
|
6
6
|
description="CLI tool for Amazon Advertising API v3",
|
|
7
7
|
author="Lunan Li",
|
|
8
8
|
author_email="lunan@stellaraether.com",
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Amazon Ads CLI - Command line interface for Amazon Advertising API v3."""
|
|
3
|
-
|
|
4
|
-
import click
|
|
5
|
-
import json
|
|
6
|
-
from datetime import datetime, timedelta
|
|
7
|
-
from ad_api.api import sponsored_products, reports
|
|
8
|
-
from ad_api.base import Marketplaces
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@click.group()
|
|
12
|
-
@click.option('--profile', '-p', default='default', help='Credential profile')
|
|
13
|
-
@click.pass_context
|
|
14
|
-
def cli(ctx, profile):
|
|
15
|
-
"""Amazon Ads CLI - Manage campaigns, keywords, and reports."""
|
|
16
|
-
ctx.ensure_object(dict)
|
|
17
|
-
ctx.obj['profile'] = profile
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@cli.group()
|
|
21
|
-
def campaigns():
|
|
22
|
-
"""Campaign management commands."""
|
|
23
|
-
pass
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@campaigns.command('list')
|
|
27
|
-
@click.pass_context
|
|
28
|
-
def list_campaigns(ctx):
|
|
29
|
-
"""List all campaigns."""
|
|
30
|
-
result = sponsored_products.CampaignsV3(marketplace=Marketplaces.NA).list_campaigns(body={})
|
|
31
|
-
campaigns = result.payload.get('campaigns', [])
|
|
32
|
-
|
|
33
|
-
click.echo(f"\n{'Campaign':<30} {'State':<10} {'Budget':<10} {'Type'}")
|
|
34
|
-
click.echo("-" * 65)
|
|
35
|
-
for camp in campaigns:
|
|
36
|
-
name = camp['name'][:28]
|
|
37
|
-
state = camp['state']
|
|
38
|
-
budget = f"${camp['budget']['budget']}"
|
|
39
|
-
ctype = camp.get('targetingType', 'N/A')
|
|
40
|
-
click.echo(f"{name:<30} {state:<10} {budget:<10} {ctype}")
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@campaigns.command('pause')
|
|
44
|
-
@click.argument('campaign-id')
|
|
45
|
-
@click.pass_context
|
|
46
|
-
def pause_campaign(ctx, campaign_id):
|
|
47
|
-
"""Pause a campaign."""
|
|
48
|
-
try:
|
|
49
|
-
result = sponsored_products.CampaignsV3(marketplace=Marketplaces.NA).edit_campaigns(
|
|
50
|
-
body={"campaigns": [{"campaignId": campaign_id, "state": "PAUSED"}]}
|
|
51
|
-
)
|
|
52
|
-
click.echo(f"✅ Campaign {campaign_id} paused")
|
|
53
|
-
except Exception as e:
|
|
54
|
-
click.echo(f"❌ Error: {e}")
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
@campaigns.command('enable')
|
|
58
|
-
@click.argument('campaign-id')
|
|
59
|
-
@click.pass_context
|
|
60
|
-
def enable_campaign(ctx, campaign_id):
|
|
61
|
-
"""Enable a campaign."""
|
|
62
|
-
try:
|
|
63
|
-
result = sponsored_products.CampaignsV3(marketplace=Marketplaces.NA).edit_campaigns(
|
|
64
|
-
body={"campaigns": [{"campaignId": campaign_id, "state": "ENABLED"}]}
|
|
65
|
-
)
|
|
66
|
-
click.echo(f"✅ Campaign {campaign_id} enabled")
|
|
67
|
-
except Exception as e:
|
|
68
|
-
click.echo(f"❌ Error: {e}")
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
@cli.group()
|
|
72
|
-
def keywords():
|
|
73
|
-
"""Keyword management commands."""
|
|
74
|
-
pass
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
@keywords.command('list')
|
|
78
|
-
@click.argument('campaign-id')
|
|
79
|
-
@click.pass_context
|
|
80
|
-
def list_keywords(ctx, campaign_id):
|
|
81
|
-
"""List keywords for a campaign."""
|
|
82
|
-
result = sponsored_products.KeywordsV3(marketplace=Marketplaces.NA).list_keywords(body={})
|
|
83
|
-
keywords = [k for k in result.payload.get('keywords', []) if k.get('campaignId') == campaign_id]
|
|
84
|
-
|
|
85
|
-
click.echo(f"\n{'Keyword':<35} {'Match':<10} {'Bid':<8} {'State'}")
|
|
86
|
-
click.echo("-" * 70)
|
|
87
|
-
for kw in keywords:
|
|
88
|
-
text = kw['keywordText'][:33]
|
|
89
|
-
match = kw['matchType']
|
|
90
|
-
bid = f"${kw['bid']}"
|
|
91
|
-
state = kw['state']
|
|
92
|
-
click.echo(f"{text:<35} {match:<10} {bid:<8} {state}")
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
@cli.group()
|
|
96
|
-
def report():
|
|
97
|
-
"""Report commands."""
|
|
98
|
-
pass
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
@report.command('today')
|
|
102
|
-
@click.pass_context
|
|
103
|
-
def report_today(ctx):
|
|
104
|
-
"""Get today's performance report."""
|
|
105
|
-
today = datetime.now().strftime('%Y-%m-%d')
|
|
106
|
-
|
|
107
|
-
click.echo(f"Requesting report for {today}...")
|
|
108
|
-
|
|
109
|
-
report_body = {
|
|
110
|
-
"name": f"SP_Today_{today}",
|
|
111
|
-
"startDate": today,
|
|
112
|
-
"endDate": today,
|
|
113
|
-
"configuration": {
|
|
114
|
-
"adProduct": "SPONSORED_PRODUCTS",
|
|
115
|
-
"columns": [
|
|
116
|
-
"impressions", "clicks", "cost",
|
|
117
|
-
"purchases14d", "sales14d",
|
|
118
|
-
"campaignName", "campaignId"
|
|
119
|
-
],
|
|
120
|
-
"reportTypeId": "spCampaigns",
|
|
121
|
-
"format": "GZIP_JSON",
|
|
122
|
-
"groupBy": ["campaign"],
|
|
123
|
-
"timeUnit": "SUMMARY"
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
try:
|
|
128
|
-
# Submit report
|
|
129
|
-
result = reports.Reports(marketplace=Marketplaces.NA).post_report(body=report_body)
|
|
130
|
-
report_id = result.payload['reportId']
|
|
131
|
-
|
|
132
|
-
click.echo(f"Report submitted: {report_id}")
|
|
133
|
-
click.echo("Polling for completion...")
|
|
134
|
-
|
|
135
|
-
# Poll
|
|
136
|
-
import time
|
|
137
|
-
for i in range(20):
|
|
138
|
-
result = reports.Reports(marketplace=Marketplaces.NA).get_report(reportId=report_id)
|
|
139
|
-
status = result.payload.get('status')
|
|
140
|
-
|
|
141
|
-
if status == 'COMPLETED':
|
|
142
|
-
# Download
|
|
143
|
-
import requests
|
|
144
|
-
import gzip
|
|
145
|
-
|
|
146
|
-
url = result.payload.get('url')
|
|
147
|
-
response = requests.get(url)
|
|
148
|
-
data = gzip.decompress(response.content)
|
|
149
|
-
report_data = json.loads(data)
|
|
150
|
-
|
|
151
|
-
click.echo(f"\n{'Campaign':<30} {'Impr':>8} {'Clicks':>7} {'Spend':>8} {'Sales':>8} {'ACOS'}")
|
|
152
|
-
click.echo("-" * 75)
|
|
153
|
-
|
|
154
|
-
for row in report_data:
|
|
155
|
-
camp_name = row.get('campaignName', 'N/A')[:28]
|
|
156
|
-
impr = int(row.get('impressions', 0) or 0)
|
|
157
|
-
clicks = int(row.get('clicks', 0) or 0)
|
|
158
|
-
cost = float(row.get('cost', 0) or 0)
|
|
159
|
-
sales = float(row.get('sales14d', 0) or 0)
|
|
160
|
-
acos = (cost / sales * 100) if sales > 0 else 0
|
|
161
|
-
|
|
162
|
-
click.echo(f"{camp_name:<30} {impr:>8} {clicks:>7} ${cost:>7.2f} ${sales:>7.2f} {acos:>5.1f}%")
|
|
163
|
-
|
|
164
|
-
return
|
|
165
|
-
|
|
166
|
-
elif status == 'FAILED':
|
|
167
|
-
click.echo(f"❌ Report failed: {result.payload.get('failureReason')}")
|
|
168
|
-
return
|
|
169
|
-
|
|
170
|
-
time.sleep(3)
|
|
171
|
-
|
|
172
|
-
click.echo("⏳ Report still processing...")
|
|
173
|
-
|
|
174
|
-
except Exception as e:
|
|
175
|
-
click.echo(f"❌ Error: {e}")
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if __name__ == '__main__':
|
|
179
|
-
cli()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|