amazon-sp-cli 0.1.3__tar.gz → 0.1.5__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_sp_cli-0.1.3/amazon_sp_cli.egg-info → amazon_sp_cli-0.1.5}/PKG-INFO +1 -1
- {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/amazon_sp_cli/auth.py +5 -1
- {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/amazon_sp_cli/main.py +140 -8
- {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5/amazon_sp_cli.egg-info}/PKG-INFO +1 -1
- {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/setup.py +1 -1
- {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/LICENSE +0 -0
- {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/MANIFEST.in +0 -0
- {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/README.md +0 -0
- {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/amazon_sp_cli/__init__.py +0 -0
- {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/amazon_sp_cli/__main__.py +0 -0
- {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/amazon_sp_cli/client.py +0 -0
- {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/amazon_sp_cli.egg-info/SOURCES.txt +0 -0
- {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/amazon_sp_cli.egg-info/dependency_links.txt +0 -0
- {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/amazon_sp_cli.egg-info/entry_points.txt +0 -0
- {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/amazon_sp_cli.egg-info/requires.txt +0 -0
- {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/amazon_sp_cli.egg-info/top_level.txt +0 -0
- {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/pyproject.toml +0 -0
- {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/setup.cfg +0 -0
- {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/tests/__init__.py +0 -0
- {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/tests/test_auth.py +0 -0
- {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/tests/test_client.py +0 -0
- {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/tests/test_sale_price.py +0 -0
|
@@ -4,6 +4,7 @@ import json
|
|
|
4
4
|
import os
|
|
5
5
|
import time
|
|
6
6
|
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
7
8
|
|
|
8
9
|
import requests
|
|
9
10
|
import yaml
|
|
@@ -20,11 +21,14 @@ class SPAPIAuth:
|
|
|
20
21
|
self.credentials = self._load_credentials(credentials_path)
|
|
21
22
|
self._ensure_cache_dir()
|
|
22
23
|
|
|
23
|
-
def _load_credentials(self, path: str = None) -> dict:
|
|
24
|
+
def _load_credentials(self, path: str = None) -> Optional[dict]:
|
|
24
25
|
"""Load credentials from YAML file."""
|
|
25
26
|
if path is None:
|
|
26
27
|
path = Path.home() / ".config" / "amazon-sp-cli" / "credentials.yml"
|
|
27
28
|
|
|
29
|
+
if not Path(path).exists():
|
|
30
|
+
return None
|
|
31
|
+
|
|
28
32
|
with open(path, "r") as f:
|
|
29
33
|
config = yaml.safe_load(f)
|
|
30
34
|
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
"""Main CLI entry point for Amazon SP-API CLI."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import os
|
|
4
5
|
from datetime import datetime, timezone
|
|
5
6
|
|
|
6
7
|
import click
|
|
8
|
+
import yaml
|
|
7
9
|
|
|
8
10
|
from .auth import SPAPIAuth
|
|
9
11
|
from .client import SPAPIClient
|
|
10
12
|
|
|
13
|
+
DEFAULT_CREDENTIALS_PATH = os.path.expanduser("~/.config/amazon-sp-cli/credentials.yml")
|
|
14
|
+
|
|
11
15
|
|
|
12
16
|
def _check_path():
|
|
13
17
|
"""Check if the CLI is accessible in PATH and warn once per day."""
|
|
@@ -51,6 +55,18 @@ def _check_path():
|
|
|
51
55
|
print("", file=sys.stderr)
|
|
52
56
|
|
|
53
57
|
|
|
58
|
+
def _ensure_auth_client(ctx):
|
|
59
|
+
"""Lazily create auth and client if not already present."""
|
|
60
|
+
if "client" not in ctx.obj:
|
|
61
|
+
auth = SPAPIAuth(ctx.obj.get("credentials_path"))
|
|
62
|
+
if auth.credentials is None:
|
|
63
|
+
click.echo("Error: No credentials found. Run 'amz-sp auth setup' first.", err=True)
|
|
64
|
+
raise click.Abort()
|
|
65
|
+
ctx.obj["auth"] = auth
|
|
66
|
+
ctx.obj["client"] = SPAPIClient(auth)
|
|
67
|
+
return ctx.obj["auth"], ctx.obj["client"]
|
|
68
|
+
|
|
69
|
+
|
|
54
70
|
@click.group()
|
|
55
71
|
@click.option("--credentials", "-c", help="Path to credentials YAML file")
|
|
56
72
|
@click.pass_context
|
|
@@ -58,8 +74,123 @@ def cli(ctx, credentials):
|
|
|
58
74
|
"""Amazon SP-API CLI - Manage listings, pricing, inventory, and more."""
|
|
59
75
|
_check_path()
|
|
60
76
|
ctx.ensure_object(dict)
|
|
61
|
-
ctx.obj["
|
|
62
|
-
|
|
77
|
+
ctx.obj["credentials_path"] = credentials
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@cli.group()
|
|
81
|
+
def auth():
|
|
82
|
+
"""Authentication commands."""
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@auth.command("setup")
|
|
87
|
+
@click.option("--path", default=DEFAULT_CREDENTIALS_PATH, help="Path to save credentials")
|
|
88
|
+
@click.option("--profile", default="default", help="Credential profile name")
|
|
89
|
+
@click.option("--refresh-token", help="Refresh token")
|
|
90
|
+
@click.option("--client-id", help="Client ID")
|
|
91
|
+
@click.option("--client-secret", help="Client secret")
|
|
92
|
+
@click.option("--aws-access-key-id", help="AWS Access Key ID")
|
|
93
|
+
@click.option("--aws-secret-access-key", help="AWS Secret Access Key")
|
|
94
|
+
@click.option("--seller-id", default="A2GKV2AN9F8YG3", help="Seller ID")
|
|
95
|
+
@click.option("--marketplace-id", default="ATVPDKIKX0DER", help="Marketplace ID")
|
|
96
|
+
@click.pass_context
|
|
97
|
+
def auth_setup(
|
|
98
|
+
ctx,
|
|
99
|
+
path,
|
|
100
|
+
profile,
|
|
101
|
+
refresh_token,
|
|
102
|
+
client_id,
|
|
103
|
+
client_secret,
|
|
104
|
+
aws_access_key_id,
|
|
105
|
+
aws_secret_access_key,
|
|
106
|
+
seller_id,
|
|
107
|
+
marketplace_id,
|
|
108
|
+
):
|
|
109
|
+
"""Set up Amazon SP-API credentials.
|
|
110
|
+
|
|
111
|
+
When flags are omitted, falls back to interactive prompts.
|
|
112
|
+
"""
|
|
113
|
+
click.echo("🔐 Amazon SP-API Credential Setup")
|
|
114
|
+
click.echo("=" * 50)
|
|
115
|
+
click.echo()
|
|
116
|
+
|
|
117
|
+
interactive = not all([refresh_token, client_id, client_secret, aws_access_key_id, aws_secret_access_key])
|
|
118
|
+
if interactive:
|
|
119
|
+
click.echo("You'll need the following from your Amazon Developer account:")
|
|
120
|
+
click.echo(" 1. Refresh Token (from LWA authorization)")
|
|
121
|
+
click.echo(" 2. Client ID (from your app registration)")
|
|
122
|
+
click.echo(" 3. Client Secret (from your app registration)")
|
|
123
|
+
click.echo(" 4. AWS Access Key ID")
|
|
124
|
+
click.echo(" 5. AWS Secret Access Key")
|
|
125
|
+
click.echo()
|
|
126
|
+
|
|
127
|
+
profile = profile or click.prompt("Profile name", default="default")
|
|
128
|
+
refresh_token = refresh_token or click.prompt("Refresh token", hide_input=True)
|
|
129
|
+
client_id = client_id or click.prompt("Client ID")
|
|
130
|
+
client_secret = client_secret or click.prompt("Client secret", hide_input=True)
|
|
131
|
+
aws_access_key_id = aws_access_key_id or click.prompt("AWS Access Key ID")
|
|
132
|
+
aws_secret_access_key = aws_secret_access_key or click.prompt("AWS Secret Access Key", hide_input=True)
|
|
133
|
+
|
|
134
|
+
credentials = {
|
|
135
|
+
"version": "1.0",
|
|
136
|
+
profile: {
|
|
137
|
+
"refresh_token": refresh_token,
|
|
138
|
+
"client_id": client_id,
|
|
139
|
+
"client_secret": client_secret,
|
|
140
|
+
"aws_access_key_id": aws_access_key_id,
|
|
141
|
+
"aws_secret_access_key": aws_secret_access_key,
|
|
142
|
+
"seller_id": seller_id,
|
|
143
|
+
"marketplace_id": marketplace_id,
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# Merge with existing if present
|
|
148
|
+
if os.path.exists(path):
|
|
149
|
+
try:
|
|
150
|
+
with open(path, "r") as f:
|
|
151
|
+
existing = yaml.safe_load(f) or {}
|
|
152
|
+
existing[profile] = credentials[profile]
|
|
153
|
+
credentials = existing
|
|
154
|
+
click.echo(f"\n📝 Merged with existing credentials at {path}")
|
|
155
|
+
except Exception as e:
|
|
156
|
+
click.echo(f"⚠️ Could not read existing file: {e}")
|
|
157
|
+
|
|
158
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
159
|
+
with open(path, "w") as f:
|
|
160
|
+
yaml.dump(credentials, f, default_flow_style=False, sort_keys=False)
|
|
161
|
+
|
|
162
|
+
click.echo(f"✅ Credentials saved to {path}")
|
|
163
|
+
click.echo(f" Profile: {profile}")
|
|
164
|
+
click.echo(f" Seller ID: {seller_id}")
|
|
165
|
+
click.echo(f" Marketplace ID: {marketplace_id}")
|
|
166
|
+
click.echo()
|
|
167
|
+
click.echo("You can now use: python -m amazon_sp_cli.main --profile {profile} get-price <sku>")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@auth.command("show")
|
|
171
|
+
@click.option("--path", default=DEFAULT_CREDENTIALS_PATH, help="Path to credentials file")
|
|
172
|
+
@click.pass_context
|
|
173
|
+
def auth_show(ctx, path):
|
|
174
|
+
"""Show configured profiles (without secrets)."""
|
|
175
|
+
if not os.path.exists(path):
|
|
176
|
+
click.echo(f"❌ No credentials file found at {path}")
|
|
177
|
+
click.echo("Run: python -m amazon_sp_cli.main auth setup")
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
with open(path, "r") as f:
|
|
181
|
+
creds = yaml.safe_load(f) or {}
|
|
182
|
+
|
|
183
|
+
click.echo(f"\n📄 Credentials file: {path}")
|
|
184
|
+
click.echo("-" * 40)
|
|
185
|
+
|
|
186
|
+
for profile, data in creds.items():
|
|
187
|
+
if profile == "version":
|
|
188
|
+
continue
|
|
189
|
+
click.echo(f"Profile: {profile}")
|
|
190
|
+
click.echo(f" Client ID: {data.get('client_id', 'N/A')[:20]}...")
|
|
191
|
+
click.echo(f" Seller ID: {data.get('seller_id', 'N/A')}")
|
|
192
|
+
click.echo(f" Marketplace ID: {data.get('marketplace_id', 'N/A')}")
|
|
193
|
+
click.echo()
|
|
63
194
|
|
|
64
195
|
|
|
65
196
|
@cli.command()
|
|
@@ -67,7 +198,7 @@ def cli(ctx, credentials):
|
|
|
67
198
|
@click.pass_context
|
|
68
199
|
def get_price(ctx, sku):
|
|
69
200
|
"""Get current price for a SKU."""
|
|
70
|
-
client = ctx
|
|
201
|
+
_, client = _ensure_auth_client(ctx)
|
|
71
202
|
try:
|
|
72
203
|
response = client.get_listing(sku)
|
|
73
204
|
attributes = response.get("attributes", {})
|
|
@@ -93,7 +224,7 @@ def get_price(ctx, sku):
|
|
|
93
224
|
@click.pass_context
|
|
94
225
|
def set_price(ctx, sku, price, dry_run):
|
|
95
226
|
"""Set price for a SKU."""
|
|
96
|
-
client = ctx
|
|
227
|
+
_, client = _ensure_auth_client(ctx)
|
|
97
228
|
try:
|
|
98
229
|
mode = "VALIDATION_PREVIEW" if dry_run else None
|
|
99
230
|
response = client.update_price(sku, price, mode)
|
|
@@ -118,7 +249,7 @@ def set_price(ctx, sku, price, dry_run):
|
|
|
118
249
|
@click.pass_context
|
|
119
250
|
def create_discount(ctx, sku, percent, all_variations):
|
|
120
251
|
"""Create discount for a SKU."""
|
|
121
|
-
client = ctx
|
|
252
|
+
_, client = _ensure_auth_client(ctx)
|
|
122
253
|
|
|
123
254
|
try:
|
|
124
255
|
if all_variations:
|
|
@@ -217,7 +348,7 @@ def sale_price(
|
|
|
217
348
|
amz-sp sale-price PAW2603190101 5 --type fixed
|
|
218
349
|
amz-sp sale-price PAW2603190101 15 --start-date 2026-05-01 --end-date 2026-05-31
|
|
219
350
|
"""
|
|
220
|
-
client = ctx
|
|
351
|
+
_, client = _ensure_auth_client(ctx)
|
|
221
352
|
|
|
222
353
|
try:
|
|
223
354
|
# Get current listing info
|
|
@@ -315,7 +446,7 @@ def sale_price(
|
|
|
315
446
|
@click.pass_context
|
|
316
447
|
def check_competitors(ctx, asin):
|
|
317
448
|
"""Check competitor pricing for an ASIN."""
|
|
318
|
-
client = ctx
|
|
449
|
+
_, client = _ensure_auth_client(ctx)
|
|
319
450
|
try:
|
|
320
451
|
response = client.get_catalog_item(asin)
|
|
321
452
|
attributes = response.get("attributes", {})
|
|
@@ -337,7 +468,8 @@ def check_competitors(ctx, asin):
|
|
|
337
468
|
@click.pass_context
|
|
338
469
|
def invalidate(ctx):
|
|
339
470
|
"""Invalidate cached access token."""
|
|
340
|
-
|
|
471
|
+
auth, _ = _ensure_auth_client(ctx)
|
|
472
|
+
auth.invalidate()
|
|
341
473
|
|
|
342
474
|
|
|
343
475
|
if __name__ == "__main__":
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|