amazon-sp-cli 0.1.1__tar.gz → 0.1.3__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.1/amazon_sp_cli.egg-info → amazon_sp_cli-0.1.3}/PKG-INFO +1 -1
- amazon_sp_cli-0.1.3/amazon_sp_cli/__main__.py +6 -0
- {amazon_sp_cli-0.1.1 → amazon_sp_cli-0.1.3}/amazon_sp_cli/auth.py +9 -7
- {amazon_sp_cli-0.1.1 → amazon_sp_cli-0.1.3}/amazon_sp_cli/client.py +0 -1
- {amazon_sp_cli-0.1.1 → amazon_sp_cli-0.1.3}/amazon_sp_cli/main.py +169 -0
- {amazon_sp_cli-0.1.1 → amazon_sp_cli-0.1.3/amazon_sp_cli.egg-info}/PKG-INFO +1 -1
- {amazon_sp_cli-0.1.1 → amazon_sp_cli-0.1.3}/amazon_sp_cli.egg-info/SOURCES.txt +3 -1
- {amazon_sp_cli-0.1.1 → amazon_sp_cli-0.1.3}/setup.py +4 -1
- {amazon_sp_cli-0.1.1 → amazon_sp_cli-0.1.3}/tests/test_auth.py +4 -3
- amazon_sp_cli-0.1.3/tests/test_sale_price.py +132 -0
- {amazon_sp_cli-0.1.1 → amazon_sp_cli-0.1.3}/LICENSE +0 -0
- {amazon_sp_cli-0.1.1 → amazon_sp_cli-0.1.3}/MANIFEST.in +0 -0
- {amazon_sp_cli-0.1.1 → amazon_sp_cli-0.1.3}/README.md +0 -0
- {amazon_sp_cli-0.1.1 → amazon_sp_cli-0.1.3}/amazon_sp_cli/__init__.py +0 -0
- {amazon_sp_cli-0.1.1 → amazon_sp_cli-0.1.3}/amazon_sp_cli.egg-info/dependency_links.txt +0 -0
- {amazon_sp_cli-0.1.1 → amazon_sp_cli-0.1.3}/amazon_sp_cli.egg-info/entry_points.txt +0 -0
- {amazon_sp_cli-0.1.1 → amazon_sp_cli-0.1.3}/amazon_sp_cli.egg-info/requires.txt +0 -0
- {amazon_sp_cli-0.1.1 → amazon_sp_cli-0.1.3}/amazon_sp_cli.egg-info/top_level.txt +0 -0
- {amazon_sp_cli-0.1.1 → amazon_sp_cli-0.1.3}/pyproject.toml +0 -0
- {amazon_sp_cli-0.1.1 → amazon_sp_cli-0.1.3}/setup.cfg +0 -0
- {amazon_sp_cli-0.1.1 → amazon_sp_cli-0.1.3}/tests/__init__.py +0 -0
- {amazon_sp_cli-0.1.1 → amazon_sp_cli-0.1.3}/tests/test_client.py +0 -0
|
@@ -13,7 +13,7 @@ class SPAPIAuth:
|
|
|
13
13
|
"""Handles SP-API token refresh and caching."""
|
|
14
14
|
|
|
15
15
|
TOKEN_ENDPOINT = "https://api.amazon.com/auth/o2/token"
|
|
16
|
-
CACHE_FILE = Path.home() / ".config" / "amazon-sp-
|
|
16
|
+
CACHE_FILE = Path.home() / ".config" / "amazon-sp-cli" / "token-cache.json"
|
|
17
17
|
BUFFER_SECONDS = 60
|
|
18
18
|
|
|
19
19
|
def __init__(self, credentials_path: str = None):
|
|
@@ -23,7 +23,7 @@ class SPAPIAuth:
|
|
|
23
23
|
def _load_credentials(self, path: str = None) -> dict:
|
|
24
24
|
"""Load credentials from YAML file."""
|
|
25
25
|
if path is None:
|
|
26
|
-
path = Path.home() / ".config" / "amazon-sp-
|
|
26
|
+
path = Path.home() / ".config" / "amazon-sp-cli" / "credentials.yml"
|
|
27
27
|
|
|
28
28
|
with open(path, "r") as f:
|
|
29
29
|
config = yaml.safe_load(f)
|
|
@@ -95,9 +95,11 @@ class SPAPIAuth:
|
|
|
95
95
|
|
|
96
96
|
def invalidate(self):
|
|
97
97
|
"""Invalidate cached token."""
|
|
98
|
-
self._save_cache(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
self._save_cache(
|
|
99
|
+
{
|
|
100
|
+
"access_token": None,
|
|
101
|
+
"expires_at": 0,
|
|
102
|
+
"refreshed_at": None,
|
|
103
|
+
}
|
|
104
|
+
)
|
|
103
105
|
print("Token cache invalidated.")
|
|
@@ -9,11 +9,54 @@ from .auth import SPAPIAuth
|
|
|
9
9
|
from .client import SPAPIClient
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
def _check_path():
|
|
13
|
+
"""Check if the CLI is accessible in PATH and warn once per day."""
|
|
14
|
+
import shutil
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
if shutil.which("amz-sp"):
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
# Only warn once per day
|
|
22
|
+
flag_file = Path.home() / ".config" / "amazon-sp-cli" / ".path-warned"
|
|
23
|
+
flag_file.parent.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
|
|
25
|
+
now = datetime.now(timezone.utc)
|
|
26
|
+
today = now.strftime("%Y-%m-%d")
|
|
27
|
+
|
|
28
|
+
if flag_file.exists():
|
|
29
|
+
last_warned = flag_file.read_text().strip()
|
|
30
|
+
if last_warned == today:
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
flag_file.write_text(today)
|
|
34
|
+
|
|
35
|
+
print(
|
|
36
|
+
"\n⚠️ Note: 'amz-sp' is not in your PATH.",
|
|
37
|
+
file=sys.stderr,
|
|
38
|
+
)
|
|
39
|
+
print(
|
|
40
|
+
" You can still use: python3 -m amazon_sp_cli",
|
|
41
|
+
file=sys.stderr,
|
|
42
|
+
)
|
|
43
|
+
print(
|
|
44
|
+
" To add to PATH, add this to your shell config:",
|
|
45
|
+
file=sys.stderr,
|
|
46
|
+
)
|
|
47
|
+
print(
|
|
48
|
+
f' export PATH="{sys.prefix}/bin:$PATH"',
|
|
49
|
+
file=sys.stderr,
|
|
50
|
+
)
|
|
51
|
+
print("", file=sys.stderr)
|
|
52
|
+
|
|
53
|
+
|
|
12
54
|
@click.group()
|
|
13
55
|
@click.option("--credentials", "-c", help="Path to credentials YAML file")
|
|
14
56
|
@click.pass_context
|
|
15
57
|
def cli(ctx, credentials):
|
|
16
58
|
"""Amazon SP-API CLI - Manage listings, pricing, inventory, and more."""
|
|
59
|
+
_check_path()
|
|
17
60
|
ctx.ensure_object(dict)
|
|
18
61
|
ctx.obj["auth"] = SPAPIAuth(credentials)
|
|
19
62
|
ctx.obj["client"] = SPAPIClient(ctx.obj["auth"])
|
|
@@ -141,6 +184,132 @@ def create_discount(ctx, sku, percent, all_variations):
|
|
|
141
184
|
raise click.Abort()
|
|
142
185
|
|
|
143
186
|
|
|
187
|
+
@cli.command()
|
|
188
|
+
@click.argument("sku")
|
|
189
|
+
@click.argument("discount", type=float)
|
|
190
|
+
@click.option(
|
|
191
|
+
"--type",
|
|
192
|
+
"discount_type",
|
|
193
|
+
type=click.Choice(["percentage", "fixed"]),
|
|
194
|
+
default="percentage",
|
|
195
|
+
help="Discount type: percentage or fixed amount off",
|
|
196
|
+
)
|
|
197
|
+
@click.option("--start-date", help="Start date (YYYY-MM-DD). Defaults to today")
|
|
198
|
+
@click.option("--end-date", help="End date (YYYY-MM-DD). Defaults to 30 days from start")
|
|
199
|
+
@click.option("--output", "-o", type=click.File("w"), help="Save price adjustment data to file")
|
|
200
|
+
@click.pass_context
|
|
201
|
+
def sale_price(
|
|
202
|
+
ctx,
|
|
203
|
+
sku,
|
|
204
|
+
discount,
|
|
205
|
+
discount_type,
|
|
206
|
+
start_date,
|
|
207
|
+
end_date,
|
|
208
|
+
output,
|
|
209
|
+
):
|
|
210
|
+
"""Generate sale price data for a SKU.
|
|
211
|
+
|
|
212
|
+
SP-API does not support direct sale price creation. This generates
|
|
213
|
+
the feed data you can submit via the Feeds API or use in Seller Central.
|
|
214
|
+
|
|
215
|
+
Examples:
|
|
216
|
+
amz-sp sale-price PAW2603190101 20
|
|
217
|
+
amz-sp sale-price PAW2603190101 5 --type fixed
|
|
218
|
+
amz-sp sale-price PAW2603190101 15 --start-date 2026-05-01 --end-date 2026-05-31
|
|
219
|
+
"""
|
|
220
|
+
client = ctx.obj["client"]
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
# Get current listing info
|
|
224
|
+
response = client.get_listing(sku)
|
|
225
|
+
attributes = response.get("attributes", {})
|
|
226
|
+
list_price = attributes.get("list_price", [{}])[0]
|
|
227
|
+
current_price = list_price.get("value", 0)
|
|
228
|
+
|
|
229
|
+
if not current_price:
|
|
230
|
+
click.echo("Error: Could not get current price for SKU", err=True)
|
|
231
|
+
raise click.Abort()
|
|
232
|
+
|
|
233
|
+
# Calculate dates
|
|
234
|
+
from datetime import timedelta
|
|
235
|
+
|
|
236
|
+
start = datetime.strptime(start_date, "%Y-%m-%d") if start_date else datetime.now(timezone.utc)
|
|
237
|
+
end = datetime.strptime(end_date, "%Y-%m-%d") if end_date else start + timedelta(days=30)
|
|
238
|
+
|
|
239
|
+
# Format for Amazon (ISO 8601)
|
|
240
|
+
start_str = start.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
241
|
+
end_str = end.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
242
|
+
|
|
243
|
+
# Calculate discount values
|
|
244
|
+
if discount_type == "percentage":
|
|
245
|
+
discount_amount = round(current_price * discount / 100, 2)
|
|
246
|
+
discount_display = f"{discount}%"
|
|
247
|
+
else:
|
|
248
|
+
discount_amount = discount
|
|
249
|
+
discount_display = f"${discount}"
|
|
250
|
+
|
|
251
|
+
new_sale_price = max(0, current_price - discount_amount)
|
|
252
|
+
|
|
253
|
+
# Build sale price feed data
|
|
254
|
+
feed = {
|
|
255
|
+
"sku": sku,
|
|
256
|
+
"asin": response.get("summaries", [{}])[0].get("asin"),
|
|
257
|
+
"pricing": {
|
|
258
|
+
"original_price": current_price,
|
|
259
|
+
"sale_price": new_sale_price,
|
|
260
|
+
"discount_amount": discount_amount,
|
|
261
|
+
"discount_display": discount_display,
|
|
262
|
+
},
|
|
263
|
+
"schedule": {
|
|
264
|
+
"start_date": start_str,
|
|
265
|
+
"end_date": end_str,
|
|
266
|
+
"duration_days": (end - start).days,
|
|
267
|
+
},
|
|
268
|
+
"feed_data": {
|
|
269
|
+
"messageId": 1,
|
|
270
|
+
"sku": sku,
|
|
271
|
+
"operationType": "PARTIAL_UPDATE",
|
|
272
|
+
"productType": "PET_TOY",
|
|
273
|
+
"attributes": {
|
|
274
|
+
"list_price": [{"currency": "USD", "value": current_price}],
|
|
275
|
+
"sale_price": [
|
|
276
|
+
{
|
|
277
|
+
"currency": "USD",
|
|
278
|
+
"value": new_sale_price,
|
|
279
|
+
"effective_date": start_str,
|
|
280
|
+
"end_date": end_str,
|
|
281
|
+
}
|
|
282
|
+
],
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
# Output
|
|
288
|
+
output_json = json.dumps(feed, indent=2)
|
|
289
|
+
|
|
290
|
+
if output:
|
|
291
|
+
output.write(output_json)
|
|
292
|
+
click.echo(f"✓ Sale price data saved to {output.name}")
|
|
293
|
+
|
|
294
|
+
click.echo(output_json)
|
|
295
|
+
|
|
296
|
+
# Summary
|
|
297
|
+
click.echo("\n" + "=" * 50)
|
|
298
|
+
click.echo("SALE PRICE SUMMARY")
|
|
299
|
+
click.echo("=" * 50)
|
|
300
|
+
click.echo(f"SKU: {sku}")
|
|
301
|
+
click.echo(f"Discount: {discount_display}")
|
|
302
|
+
click.echo(f"Price: ${current_price} → ${new_sale_price}")
|
|
303
|
+
click.echo(f"Duration: {start_str} to {end_str}")
|
|
304
|
+
click.echo("\n⚠️ SP-API does not support direct sale price creation.")
|
|
305
|
+
click.echo(" Submit the feed_data above via the Feeds API")
|
|
306
|
+
click.echo(" or update manually in Seller Central.")
|
|
307
|
+
|
|
308
|
+
except Exception as e:
|
|
309
|
+
click.echo(f"Error: {e}", err=True)
|
|
310
|
+
raise click.Abort()
|
|
311
|
+
|
|
312
|
+
|
|
144
313
|
@cli.command()
|
|
145
314
|
@click.argument("asin")
|
|
146
315
|
@click.pass_context
|
|
@@ -4,6 +4,7 @@ README.md
|
|
|
4
4
|
pyproject.toml
|
|
5
5
|
setup.py
|
|
6
6
|
amazon_sp_cli/__init__.py
|
|
7
|
+
amazon_sp_cli/__main__.py
|
|
7
8
|
amazon_sp_cli/auth.py
|
|
8
9
|
amazon_sp_cli/client.py
|
|
9
10
|
amazon_sp_cli/main.py
|
|
@@ -15,4 +16,5 @@ amazon_sp_cli.egg-info/requires.txt
|
|
|
15
16
|
amazon_sp_cli.egg-info/top_level.txt
|
|
16
17
|
tests/__init__.py
|
|
17
18
|
tests/test_auth.py
|
|
18
|
-
tests/test_client.py
|
|
19
|
+
tests/test_client.py
|
|
20
|
+
tests/test_sale_price.py
|
|
@@ -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.
|
|
5
|
+
version="0.1.3",
|
|
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",
|
|
@@ -19,6 +19,9 @@ setup(
|
|
|
19
19
|
"amz-sp=amazon_sp_cli.main:cli",
|
|
20
20
|
],
|
|
21
21
|
},
|
|
22
|
+
data_files=[
|
|
23
|
+
("share/amazon-sp-cli", ["README.md"]),
|
|
24
|
+
],
|
|
22
25
|
python_requires=">=3.8",
|
|
23
26
|
classifiers=[
|
|
24
27
|
"Development Status :: 3 - Alpha",
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Tests for SP-API authentication."""
|
|
2
2
|
|
|
3
|
-
import json
|
|
4
3
|
import os
|
|
5
4
|
import tempfile
|
|
6
5
|
from pathlib import Path
|
|
@@ -18,12 +17,14 @@ class TestSPAPIAuth:
|
|
|
18
17
|
def temp_credentials(self):
|
|
19
18
|
"""Create temporary credentials file."""
|
|
20
19
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".yml", delete=False) as f:
|
|
21
|
-
f.write(
|
|
20
|
+
f.write(
|
|
21
|
+
"""
|
|
22
22
|
default:
|
|
23
23
|
refresh_token: "test-refresh-token"
|
|
24
24
|
client_id: "test-client-id"
|
|
25
25
|
client_secret: "test-client-secret"
|
|
26
|
-
"""
|
|
26
|
+
"""
|
|
27
|
+
)
|
|
27
28
|
path = f.name
|
|
28
29
|
yield path
|
|
29
30
|
os.unlink(path)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Tests for sale-price command."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from unittest.mock import Mock, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from click.testing import CliRunner
|
|
8
|
+
|
|
9
|
+
from amazon_sp_cli.main import cli
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestSalePrice:
|
|
13
|
+
"""Test sale-price command."""
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def mock_listing_response(self):
|
|
17
|
+
"""Mock listing response with price."""
|
|
18
|
+
return {
|
|
19
|
+
"summaries": [{"asin": "B09BBL8T4Z", "status": ["ACTIVE"]}],
|
|
20
|
+
"attributes": {"list_price": [{"currency": "USD", "value": 29.99}]},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def runner(self):
|
|
25
|
+
"""Create Click test runner."""
|
|
26
|
+
return CliRunner()
|
|
27
|
+
|
|
28
|
+
@patch("amazon_sp_cli.main.SPAPIAuth")
|
|
29
|
+
@patch("amazon_sp_cli.main.SPAPIClient")
|
|
30
|
+
def test_sale_price_percentage(self, mock_client_class, mock_auth_class, runner, mock_listing_response):
|
|
31
|
+
"""Test generating percentage sale price."""
|
|
32
|
+
mock_client = Mock()
|
|
33
|
+
mock_client.get_listing.return_value = mock_listing_response
|
|
34
|
+
mock_client_class.return_value = mock_client
|
|
35
|
+
|
|
36
|
+
mock_auth = Mock()
|
|
37
|
+
mock_auth_class.return_value = mock_auth
|
|
38
|
+
|
|
39
|
+
result = runner.invoke(cli, ["sale-price", "TEST-SKU", "20"])
|
|
40
|
+
|
|
41
|
+
assert result.exit_code == 0
|
|
42
|
+
assert "20.0%" in result.output
|
|
43
|
+
assert "$29.99" in result.output
|
|
44
|
+
assert "$23.99" in result.output
|
|
45
|
+
assert "feed_data" in result.output
|
|
46
|
+
|
|
47
|
+
@patch("amazon_sp_cli.main.SPAPIAuth")
|
|
48
|
+
@patch("amazon_sp_cli.main.SPAPIClient")
|
|
49
|
+
def test_sale_price_fixed(self, mock_client_class, mock_auth_class, runner, mock_listing_response):
|
|
50
|
+
"""Test generating fixed amount sale price."""
|
|
51
|
+
mock_client = Mock()
|
|
52
|
+
mock_client.get_listing.return_value = mock_listing_response
|
|
53
|
+
mock_client_class.return_value = mock_client
|
|
54
|
+
|
|
55
|
+
mock_auth = Mock()
|
|
56
|
+
mock_auth_class.return_value = mock_auth
|
|
57
|
+
|
|
58
|
+
result = runner.invoke(cli, ["sale-price", "TEST-SKU", "5", "--type", "fixed"])
|
|
59
|
+
|
|
60
|
+
assert result.exit_code == 0
|
|
61
|
+
assert "$5" in result.output
|
|
62
|
+
assert "$24.99" in result.output
|
|
63
|
+
|
|
64
|
+
@patch("amazon_sp_cli.main.SPAPIAuth")
|
|
65
|
+
@patch("amazon_sp_cli.main.SPAPIClient")
|
|
66
|
+
def test_sale_price_with_dates(self, mock_client_class, mock_auth_class, runner, mock_listing_response):
|
|
67
|
+
"""Test generating sale price with custom dates."""
|
|
68
|
+
mock_client = Mock()
|
|
69
|
+
mock_client.get_listing.return_value = mock_listing_response
|
|
70
|
+
mock_client_class.return_value = mock_client
|
|
71
|
+
|
|
72
|
+
mock_auth = Mock()
|
|
73
|
+
mock_auth_class.return_value = mock_auth
|
|
74
|
+
|
|
75
|
+
result = runner.invoke(
|
|
76
|
+
cli,
|
|
77
|
+
[
|
|
78
|
+
"sale-price",
|
|
79
|
+
"TEST-SKU",
|
|
80
|
+
"15",
|
|
81
|
+
"--start-date",
|
|
82
|
+
"2026-05-01",
|
|
83
|
+
"--end-date",
|
|
84
|
+
"2026-06-01",
|
|
85
|
+
],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
assert result.exit_code == 0
|
|
89
|
+
assert "2026-05-01" in result.output
|
|
90
|
+
assert "2026-06-01" in result.output
|
|
91
|
+
|
|
92
|
+
@patch("amazon_sp_cli.main.SPAPIAuth")
|
|
93
|
+
@patch("amazon_sp_cli.main.SPAPIClient")
|
|
94
|
+
def test_sale_price_output_file(self, mock_client_class, mock_auth_class, runner, mock_listing_response):
|
|
95
|
+
"""Test saving sale price data to file."""
|
|
96
|
+
mock_client = Mock()
|
|
97
|
+
mock_client.get_listing.return_value = mock_listing_response
|
|
98
|
+
mock_client_class.return_value = mock_client
|
|
99
|
+
|
|
100
|
+
mock_auth = Mock()
|
|
101
|
+
mock_auth_class.return_value = mock_auth
|
|
102
|
+
|
|
103
|
+
with runner.isolated_filesystem():
|
|
104
|
+
result = runner.invoke(cli, ["sale-price", "TEST-SKU", "10", "-o", "sale-price.json"])
|
|
105
|
+
|
|
106
|
+
assert result.exit_code == 0
|
|
107
|
+
assert "saved to" in result.output
|
|
108
|
+
|
|
109
|
+
# Verify file was created
|
|
110
|
+
with open("sale-price.json") as f:
|
|
111
|
+
data = json.load(f)
|
|
112
|
+
assert data["sku"] == "TEST-SKU"
|
|
113
|
+
assert data["pricing"]["discount_display"] == "10.0%"
|
|
114
|
+
|
|
115
|
+
@patch("amazon_sp_cli.main.SPAPIAuth")
|
|
116
|
+
@patch("amazon_sp_cli.main.SPAPIClient")
|
|
117
|
+
def test_sale_price_no_price(self, mock_client_class, mock_auth_class, runner):
|
|
118
|
+
"""Test error when listing has no price."""
|
|
119
|
+
mock_client = Mock()
|
|
120
|
+
mock_client.get_listing.return_value = {
|
|
121
|
+
"summaries": [{"asin": "B09BBL8T4Z"}],
|
|
122
|
+
"attributes": {},
|
|
123
|
+
}
|
|
124
|
+
mock_client_class.return_value = mock_client
|
|
125
|
+
|
|
126
|
+
mock_auth = Mock()
|
|
127
|
+
mock_auth_class.return_value = mock_auth
|
|
128
|
+
|
|
129
|
+
result = runner.invoke(cli, ["sale-price", "TEST-SKU", "20"])
|
|
130
|
+
|
|
131
|
+
assert result.exit_code != 0
|
|
132
|
+
assert "Error" in result.output
|
|
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
|