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.
Files changed (22) hide show
  1. {amazon_sp_cli-0.1.3/amazon_sp_cli.egg-info → amazon_sp_cli-0.1.5}/PKG-INFO +1 -1
  2. {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/amazon_sp_cli/auth.py +5 -1
  3. {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/amazon_sp_cli/main.py +140 -8
  4. {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5/amazon_sp_cli.egg-info}/PKG-INFO +1 -1
  5. {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/setup.py +1 -1
  6. {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/LICENSE +0 -0
  7. {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/MANIFEST.in +0 -0
  8. {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/README.md +0 -0
  9. {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/amazon_sp_cli/__init__.py +0 -0
  10. {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/amazon_sp_cli/__main__.py +0 -0
  11. {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/amazon_sp_cli/client.py +0 -0
  12. {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/amazon_sp_cli.egg-info/SOURCES.txt +0 -0
  13. {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/amazon_sp_cli.egg-info/dependency_links.txt +0 -0
  14. {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/amazon_sp_cli.egg-info/entry_points.txt +0 -0
  15. {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/amazon_sp_cli.egg-info/requires.txt +0 -0
  16. {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/amazon_sp_cli.egg-info/top_level.txt +0 -0
  17. {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/pyproject.toml +0 -0
  18. {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/setup.cfg +0 -0
  19. {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/tests/__init__.py +0 -0
  20. {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/tests/test_auth.py +0 -0
  21. {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/tests/test_client.py +0 -0
  22. {amazon_sp_cli-0.1.3 → amazon_sp_cli-0.1.5}/tests/test_sale_price.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amazon-sp-cli
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: CLI tool for Amazon Selling Partner API (SP-API) operations
5
5
  Home-page: https://github.com/stellaraether/amazon-sp-cli
6
6
  Author: Lunan Li
@@ -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["auth"] = SPAPIAuth(credentials)
62
- ctx.obj["client"] = SPAPIClient(ctx.obj["auth"])
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.obj["client"]
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.obj["client"]
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.obj["client"]
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.obj["client"]
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.obj["client"]
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
- ctx.obj["auth"].invalidate()
471
+ auth, _ = _ensure_auth_client(ctx)
472
+ auth.invalidate()
341
473
 
342
474
 
343
475
  if __name__ == "__main__":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amazon-sp-cli
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: CLI tool for Amazon Selling Partner API (SP-API) operations
5
5
  Home-page: https://github.com/stellaraether/amazon-sp-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-sp-cli",
5
- version="0.1.3",
5
+ version="0.1.5",
6
6
  description="CLI tool for Amazon Selling Partner API (SP-API) operations",
7
7
  author="Lunan Li",
8
8
  author_email="lunan@stellaraether.com",
File without changes
File without changes
File without changes
File without changes