amazon-sp-cli 0.2.6__tar.gz → 0.2.8__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 (33) hide show
  1. {amazon_sp_cli-0.2.6/amazon_sp_cli.egg-info → amazon_sp_cli-0.2.8}/PKG-INFO +1 -1
  2. amazon_sp_cli-0.2.8/amazon_sp_cli/__init__.py +3 -0
  3. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/amazon_sp_cli/client.py +69 -2
  4. amazon_sp_cli-0.2.8/amazon_sp_cli/commands/listings.py +191 -0
  5. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/amazon_sp_cli/main.py +2 -0
  6. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8/amazon_sp_cli.egg-info}/PKG-INFO +1 -1
  7. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/amazon_sp_cli.egg-info/SOURCES.txt +2 -0
  8. amazon_sp_cli-0.2.8/tests/test_listings.py +391 -0
  9. amazon_sp_cli-0.2.6/amazon_sp_cli/__init__.py +0 -2
  10. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/LICENSE +0 -0
  11. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/MANIFEST.in +0 -0
  12. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/README.md +0 -0
  13. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/amazon_sp_cli/__main__.py +0 -0
  14. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/amazon_sp_cli/auth.py +0 -0
  15. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/amazon_sp_cli/cli.py +0 -0
  16. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/amazon_sp_cli/commands/__init__.py +0 -0
  17. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/amazon_sp_cli/commands/a_plus.py +0 -0
  18. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/amazon_sp_cli/commands/auth.py +0 -0
  19. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/amazon_sp_cli/commands/pricing.py +0 -0
  20. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/amazon_sp_cli/models/__init__.py +0 -0
  21. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/amazon_sp_cli/models/a_plus.py +0 -0
  22. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/amazon_sp_cli.egg-info/dependency_links.txt +0 -0
  23. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/amazon_sp_cli.egg-info/entry_points.txt +0 -0
  24. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/amazon_sp_cli.egg-info/requires.txt +0 -0
  25. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/amazon_sp_cli.egg-info/top_level.txt +0 -0
  26. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/pyproject.toml +0 -0
  27. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/setup.cfg +0 -0
  28. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/setup.py +0 -0
  29. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/tests/__init__.py +0 -0
  30. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/tests/test_a_plus.py +0 -0
  31. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/tests/test_auth.py +0 -0
  32. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/tests/test_client.py +0 -0
  33. {amazon_sp_cli-0.2.6 → amazon_sp_cli-0.2.8}/tests/test_pricing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amazon-sp-cli
3
- Version: 0.2.6
3
+ Version: 0.2.8
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
@@ -0,0 +1,3 @@
1
+ # Amazon SP-API CLI tool
2
+
3
+ __version__ = "0.2.8"
@@ -9,6 +9,14 @@ from botocore.awsrequest import AWSRequest
9
9
  from botocore.credentials import Credentials
10
10
 
11
11
 
12
+ class SPAPIError(Exception):
13
+ """Raised when SP-API returns an error response."""
14
+
15
+ def __init__(self, message, response_body=None):
16
+ super().__init__(message)
17
+ self.response_body = response_body
18
+
19
+
12
20
  class SPAPIClient:
13
21
  """Client for making signed requests to Amazon SP-API."""
14
22
 
@@ -55,7 +63,17 @@ class SPAPIClient:
55
63
  headers, url, body = self._sign_request(method, path, data)
56
64
 
57
65
  response = requests.request(method, url, headers=headers, data=body)
58
- response.raise_for_status()
66
+ try:
67
+ response.raise_for_status()
68
+ except requests.HTTPError as exc:
69
+ try:
70
+ error_body = response.json()
71
+ except ValueError:
72
+ error_body = None
73
+ raise SPAPIError(
74
+ _format_spapi_error(response.status_code, error_body or response.text),
75
+ response_body=error_body,
76
+ ) from exc
59
77
 
60
78
  return response.json() if response.text else {}
61
79
 
@@ -64,11 +82,41 @@ class SPAPIClient:
64
82
  path = f"/listings/2021-08-01/items/{self.seller_id}/{sku}"
65
83
  params = {
66
84
  "marketplaceIds": self.marketplace_id,
67
- "includedData": "summaries,attributes",
85
+ "includedData": "summaries,attributes,issues,offers,fulfillmentAvailability",
68
86
  }
69
87
  path += "?" + urlencode(params)
70
88
  return self.request("GET", path)
71
89
 
90
+ def put_listing(
91
+ self, sku: str, product_type: str, attributes: dict, requirements: str = None, mode: str = None
92
+ ) -> dict:
93
+ """Create or fully replace a listing for a SKU."""
94
+ path = f"/listings/2021-08-01/items/{self.seller_id}/{sku}"
95
+ params = {
96
+ "marketplaceIds": self.marketplace_id,
97
+ }
98
+ if mode:
99
+ params["mode"] = mode
100
+ path += "?" + urlencode(params)
101
+
102
+ data = {
103
+ "productType": product_type,
104
+ "attributes": attributes,
105
+ }
106
+ if requirements:
107
+ data["requirements"] = requirements
108
+
109
+ return self.request("PUT", path, data)
110
+
111
+ def delete_listing(self, sku: str) -> dict:
112
+ """Delete a listing for a SKU."""
113
+ path = f"/listings/2021-08-01/items/{self.seller_id}/{sku}"
114
+ params = {
115
+ "marketplaceIds": self.marketplace_id,
116
+ }
117
+ path += "?" + urlencode(params)
118
+ return self.request("DELETE", path)
119
+
72
120
  def update_price(self, sku: str, price: float, mode: str = "VALIDATION_PREVIEW") -> dict:
73
121
  """Update listing price."""
74
122
  path = f"/listings/2021-08-01/items/{self.seller_id}/{sku}"
@@ -189,3 +237,22 @@ class SPAPIClient:
189
237
  params["fileName"] = file_name
190
238
  path += "?" + urlencode(params)
191
239
  return self.request("POST", path)
240
+
241
+
242
+ def _format_spapi_error(status_code, body):
243
+ """Format an SP-API error into a human-readable string."""
244
+ if isinstance(body, dict):
245
+ parts = [f"SP-API returned {status_code}"]
246
+ for error in body.get("errors", []):
247
+ code = error.get("code", "Unknown")
248
+ message = error.get("message", "")
249
+ parts.append(f" [{code}] {message}")
250
+ for issue in body.get("issues", []):
251
+ code = issue.get("code", "Unknown")
252
+ message = issue.get("message", "")
253
+ severity = issue.get("severity", "")
254
+ parts.append(f" [{code}] ({severity}) {message}")
255
+ if len(parts) == 1:
256
+ parts.append(f" {json.dumps(body)}")
257
+ return "\n".join(parts)
258
+ return f"SP-API returned {status_code}: {body}"
@@ -0,0 +1,191 @@
1
+ """Listing content management commands."""
2
+
3
+ import json
4
+
5
+ import click
6
+
7
+ from ..cli import handle_errors
8
+
9
+
10
+ def _build_attributes(
11
+ title=None,
12
+ description=None,
13
+ bullet_points=None,
14
+ price=None,
15
+ currency="USD",
16
+ condition=None,
17
+ images=None,
18
+ inventory=None,
19
+ shipping_template=None,
20
+ language_tag="en_US",
21
+ attributes_json=None,
22
+ ):
23
+ """Build SP-API attributes dict from CLI options."""
24
+ attributes = {}
25
+
26
+ if title is not None:
27
+ attributes["item_name"] = [{"value": title, "language_tag": language_tag}]
28
+
29
+ if description is not None:
30
+ attributes["product_description"] = [{"value": description, "language_tag": language_tag}]
31
+
32
+ if bullet_points:
33
+ attributes["bullet_point"] = [{"value": bp} for bp in bullet_points]
34
+
35
+ if price is not None:
36
+ attributes["list_price"] = [{"currency": currency, "value": price}]
37
+ attributes["purchasable_price"] = [{"currency": currency, "value": price}]
38
+
39
+ if condition is not None:
40
+ attributes["condition_type"] = [{"value": condition}]
41
+
42
+ if images:
43
+ attributes["main_product_image_locator"] = [{"media_location": images[0]}]
44
+ for idx, img in enumerate(images[1:], start=1):
45
+ if idx > 8:
46
+ break
47
+ attributes[f"other_product_image_locator_{idx}"] = [{"media_location": img}]
48
+
49
+ if inventory is not None:
50
+ attributes["fulfillment_availability"] = [{"quantity": inventory, "fulfillment_channel_code": "DEFAULT"}]
51
+
52
+ if shipping_template is not None:
53
+ attributes["merchant_shipping_group"] = [{"value": shipping_template}]
54
+
55
+ if attributes_json:
56
+ try:
57
+ extra = json.loads(attributes_json)
58
+ except json.JSONDecodeError as exc:
59
+ raise click.BadParameter(f"Invalid JSON in --attributes-json: {exc}")
60
+ if not isinstance(extra, dict):
61
+ raise click.BadParameter("--attributes-json must be a JSON object")
62
+ attributes.update(extra)
63
+
64
+ return attributes
65
+
66
+
67
+ def _check_issues(response):
68
+ """Check response for issues and exit with error if any ERROR severity items exist."""
69
+ issues = response.get("issues", [])
70
+ errors = [i for i in issues if i.get("severity") == "ERROR"]
71
+ if errors:
72
+ click.echo("Validation issues found:", err=True)
73
+ for issue in issues:
74
+ code = issue.get("code", "Unknown")
75
+ message = issue.get("message", "")
76
+ severity = issue.get("severity", "")
77
+ click.echo(f" [{code}] ({severity}) {message}", err=True)
78
+ raise click.Abort()
79
+ return issues
80
+
81
+
82
+ def register_listings_commands(cli_group, ensure_auth_client):
83
+ """Register listing management CLI commands."""
84
+
85
+ @cli_group.command()
86
+ @click.argument("sku")
87
+ @click.pass_context
88
+ @handle_errors
89
+ def get_listing(ctx, sku):
90
+ """Get full listing data for a SKU."""
91
+ _, client = ensure_auth_client(ctx)
92
+ response = client.get_listing(sku)
93
+ click.echo(json.dumps(response, indent=2))
94
+
95
+ @cli_group.command()
96
+ @click.argument("sku")
97
+ @click.option("--product-type", "-p", required=True, help="SP-API product type (e.g., PET_TOY)")
98
+ @click.option(
99
+ "--requirements",
100
+ type=click.Choice(["LISTING", "LISTING_PRODUCT_ONLY", "LISTING_OFFER_ONLY"]),
101
+ default="LISTING",
102
+ help="Requirements level for the update",
103
+ )
104
+ @click.option("--title", help="Item title")
105
+ @click.option("--description", help="Product description")
106
+ @click.option("--bullet-point", multiple=True, help="Bullet point (can be used multiple times)")
107
+ @click.option("--price", type=float, help="List price")
108
+ @click.option("--currency", default="USD", help="Currency code (default USD)")
109
+ @click.option("--condition", help="Condition type (e.g., new_new)")
110
+ @click.option("--image", multiple=True, help="Image URL (first becomes main, rest become other)")
111
+ @click.option("--inventory", type=int, help="Available quantity")
112
+ @click.option("--shipping-template", help="Merchant shipping group name")
113
+ @click.option("--language-tag", default="en_US", help="Language tag for text attributes")
114
+ @click.option(
115
+ "--attributes-json",
116
+ help="Raw JSON string of additional attributes (merged on top of other flags)",
117
+ )
118
+ @click.option("--dry-run", is_flag=True, help="Validate without applying")
119
+ @click.pass_context
120
+ @handle_errors
121
+ def update_listing(
122
+ ctx,
123
+ sku,
124
+ product_type,
125
+ requirements,
126
+ title,
127
+ description,
128
+ bullet_point,
129
+ price,
130
+ currency,
131
+ condition,
132
+ image,
133
+ inventory,
134
+ shipping_template,
135
+ language_tag,
136
+ attributes_json,
137
+ dry_run,
138
+ ):
139
+ """Update listing attributes for a SKU."""
140
+ _, client = ensure_auth_client(ctx)
141
+
142
+ attributes = _build_attributes(
143
+ title=title,
144
+ description=description,
145
+ bullet_points=bullet_point or None,
146
+ price=price,
147
+ currency=currency,
148
+ condition=condition,
149
+ images=image or None,
150
+ inventory=inventory,
151
+ shipping_template=shipping_template,
152
+ language_tag=language_tag,
153
+ attributes_json=attributes_json,
154
+ )
155
+
156
+ if not attributes:
157
+ click.echo("Error: No attributes provided. Use flags or --attributes-json.", err=True)
158
+ raise click.Abort()
159
+
160
+ mode = "VALIDATION_PREVIEW" if dry_run else None
161
+ response = client.put_listing(
162
+ sku,
163
+ product_type=product_type,
164
+ attributes=attributes,
165
+ requirements=requirements,
166
+ mode=mode,
167
+ )
168
+
169
+ issues = _check_issues(response)
170
+ if dry_run and issues:
171
+ click.echo("Validation warnings:")
172
+ for issue in issues:
173
+ code = issue.get("code", "Unknown")
174
+ message = issue.get("message", "")
175
+ severity = issue.get("severity", "")
176
+ click.echo(f" [{code}] ({severity}) {message}")
177
+ click.echo("Validation passed with warnings")
178
+ elif dry_run:
179
+ click.echo("Validation passed")
180
+ else:
181
+ click.echo(json.dumps(response, indent=2))
182
+
183
+ @cli_group.command()
184
+ @click.argument("sku")
185
+ @click.pass_context
186
+ @handle_errors
187
+ def delete_listing(ctx, sku):
188
+ """Delete a listing for a SKU."""
189
+ _, client = ensure_auth_client(ctx)
190
+ response = client.delete_listing(sku)
191
+ click.echo(json.dumps(response, indent=2))
@@ -3,8 +3,10 @@
3
3
  from .cli import _ensure_auth_client, cli
4
4
  from .commands.a_plus import register_a_plus_commands
5
5
  from .commands.auth import register_auth_commands
6
+ from .commands.listings import register_listings_commands
6
7
  from .commands.pricing import register_pricing_commands
7
8
 
8
9
  register_auth_commands(cli)
10
+ register_listings_commands(cli, _ensure_auth_client)
9
11
  register_pricing_commands(cli, _ensure_auth_client)
10
12
  register_a_plus_commands(cli, _ensure_auth_client)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amazon-sp-cli
3
- Version: 0.2.6
3
+ Version: 0.2.8
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
@@ -18,6 +18,7 @@ amazon_sp_cli.egg-info/top_level.txt
18
18
  amazon_sp_cli/commands/__init__.py
19
19
  amazon_sp_cli/commands/a_plus.py
20
20
  amazon_sp_cli/commands/auth.py
21
+ amazon_sp_cli/commands/listings.py
21
22
  amazon_sp_cli/commands/pricing.py
22
23
  amazon_sp_cli/models/__init__.py
23
24
  amazon_sp_cli/models/a_plus.py
@@ -25,4 +26,5 @@ tests/__init__.py
25
26
  tests/test_a_plus.py
26
27
  tests/test_auth.py
27
28
  tests/test_client.py
29
+ tests/test_listings.py
28
30
  tests/test_pricing.py
@@ -0,0 +1,391 @@
1
+ """Tests for listing commands."""
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 TestGetListing:
13
+ """Test get-listing command."""
14
+
15
+ @pytest.fixture
16
+ def mock_listing_response(self):
17
+ """Mock full listing response."""
18
+ return {
19
+ "summaries": [{"asin": "B09BBL8T4Z", "status": ["ACTIVE"]}],
20
+ "attributes": {
21
+ "item_name": [{"value": "Test Product", "language_tag": "en_US"}],
22
+ "list_price": [{"currency": "USD", "value": 29.99}],
23
+ },
24
+ "issues": [],
25
+ }
26
+
27
+ @pytest.fixture
28
+ def runner(self):
29
+ """Create Click test runner."""
30
+ return CliRunner()
31
+
32
+ @patch("amazon_sp_cli.cli.SPAPIAuth")
33
+ @patch("amazon_sp_cli.cli.SPAPIClient")
34
+ def test_get_listing(self, mock_client_class, mock_auth_class, runner, mock_listing_response):
35
+ """Test fetching a listing."""
36
+ mock_client = Mock()
37
+ mock_client.get_listing.return_value = mock_listing_response
38
+ mock_client_class.return_value = mock_client
39
+
40
+ mock_auth = Mock()
41
+ mock_auth_class.return_value = mock_auth
42
+
43
+ result = runner.invoke(cli, ["get-listing", "TEST-SKU"])
44
+
45
+ assert result.exit_code == 0
46
+ output = json.loads(result.output)
47
+ assert output["summaries"][0]["asin"] == "B09BBL8T4Z"
48
+ assert output["attributes"]["item_name"][0]["value"] == "Test Product"
49
+
50
+
51
+ class TestUpdateListing:
52
+ """Test update-listing command."""
53
+
54
+ @pytest.fixture
55
+ def runner(self):
56
+ """Create Click test runner."""
57
+ return CliRunner()
58
+
59
+ @patch("amazon_sp_cli.cli.SPAPIAuth")
60
+ @patch("amazon_sp_cli.cli.SPAPIClient")
61
+ def test_update_listing_with_title(self, mock_client_class, mock_auth_class, runner):
62
+ """Test updating a listing title."""
63
+ mock_client = Mock()
64
+ mock_client.put_listing.return_value = {"sku": "TEST-SKU", "status": "ACCEPTED"}
65
+ mock_client_class.return_value = mock_client
66
+
67
+ mock_auth = Mock()
68
+ mock_auth_class.return_value = mock_auth
69
+
70
+ result = runner.invoke(
71
+ cli,
72
+ [
73
+ "update-listing",
74
+ "TEST-SKU",
75
+ "--product-type",
76
+ "PET_TOY",
77
+ "--title",
78
+ "New Title",
79
+ ],
80
+ )
81
+
82
+ assert result.exit_code == 0
83
+ mock_client.put_listing.assert_called_once()
84
+ call_args = mock_client.put_listing.call_args
85
+ assert call_args.kwargs["product_type"] == "PET_TOY"
86
+ assert call_args.kwargs["attributes"]["item_name"] == [{"value": "New Title", "language_tag": "en_US"}]
87
+
88
+ @patch("amazon_sp_cli.cli.SPAPIAuth")
89
+ @patch("amazon_sp_cli.cli.SPAPIClient")
90
+ def test_update_listing_with_multiple_flags(self, mock_client_class, mock_auth_class, runner):
91
+ """Test updating multiple attributes at once."""
92
+ mock_client = Mock()
93
+ mock_client.put_listing.return_value = {"sku": "TEST-SKU", "status": "ACCEPTED"}
94
+ mock_client_class.return_value = mock_client
95
+
96
+ mock_auth = Mock()
97
+ mock_auth_class.return_value = mock_auth
98
+
99
+ result = runner.invoke(
100
+ cli,
101
+ [
102
+ "update-listing",
103
+ "TEST-SKU",
104
+ "--product-type",
105
+ "PET_TOY",
106
+ "--title",
107
+ "New Title",
108
+ "--description",
109
+ "New Description",
110
+ "--bullet-point",
111
+ "Point 1",
112
+ "--bullet-point",
113
+ "Point 2",
114
+ "--price",
115
+ "19.99",
116
+ "--condition",
117
+ "new_new",
118
+ "--inventory",
119
+ "100",
120
+ "--shipping-template",
121
+ "Std Template",
122
+ ],
123
+ )
124
+
125
+ assert result.exit_code == 0
126
+ attrs = mock_client.put_listing.call_args.kwargs["attributes"]
127
+ assert attrs["item_name"] == [{"value": "New Title", "language_tag": "en_US"}]
128
+ assert attrs["product_description"] == [{"value": "New Description", "language_tag": "en_US"}]
129
+ assert attrs["bullet_point"] == [{"value": "Point 1"}, {"value": "Point 2"}]
130
+ assert attrs["list_price"] == [{"currency": "USD", "value": 19.99}]
131
+ assert attrs["purchasable_price"] == [{"currency": "USD", "value": 19.99}]
132
+ assert attrs["condition_type"] == [{"value": "new_new"}]
133
+ assert attrs["fulfillment_availability"] == [{"quantity": 100, "fulfillment_channel_code": "DEFAULT"}]
134
+ assert attrs["merchant_shipping_group"] == [{"value": "Std Template"}]
135
+
136
+ @patch("amazon_sp_cli.cli.SPAPIAuth")
137
+ @patch("amazon_sp_cli.cli.SPAPIClient")
138
+ def test_update_listing_with_images(self, mock_client_class, mock_auth_class, runner):
139
+ """Test updating images."""
140
+ mock_client = Mock()
141
+ mock_client.put_listing.return_value = {"sku": "TEST-SKU", "status": "ACCEPTED"}
142
+ mock_client_class.return_value = mock_client
143
+
144
+ mock_auth = Mock()
145
+ mock_auth_class.return_value = mock_auth
146
+
147
+ result = runner.invoke(
148
+ cli,
149
+ [
150
+ "update-listing",
151
+ "TEST-SKU",
152
+ "--product-type",
153
+ "PET_TOY",
154
+ "--image",
155
+ "https://example.com/main.jpg",
156
+ "--image",
157
+ "https://example.com/other1.jpg",
158
+ "--image",
159
+ "https://example.com/other2.jpg",
160
+ ],
161
+ )
162
+
163
+ assert result.exit_code == 0
164
+ attrs = mock_client.put_listing.call_args.kwargs["attributes"]
165
+ assert attrs["main_product_image_locator"] == [{"media_location": "https://example.com/main.jpg"}]
166
+ assert attrs["other_product_image_locator_1"] == [{"media_location": "https://example.com/other1.jpg"}]
167
+ assert attrs["other_product_image_locator_2"] == [{"media_location": "https://example.com/other2.jpg"}]
168
+
169
+ @patch("amazon_sp_cli.cli.SPAPIAuth")
170
+ @patch("amazon_sp_cli.cli.SPAPIClient")
171
+ def test_update_listing_with_attributes_json(self, mock_client_class, mock_auth_class, runner):
172
+ """Test updating with raw attributes JSON."""
173
+ mock_client = Mock()
174
+ mock_client.put_listing.return_value = {"sku": "TEST-SKU", "status": "ACCEPTED"}
175
+ mock_client_class.return_value = mock_client
176
+
177
+ mock_auth = Mock()
178
+ mock_auth_class.return_value = mock_auth
179
+
180
+ result = runner.invoke(
181
+ cli,
182
+ [
183
+ "update-listing",
184
+ "TEST-SKU",
185
+ "--product-type",
186
+ "PET_TOY",
187
+ "--attributes-json",
188
+ '{"item_name": [{"value": "JSON Title", "language_tag": "en_US"}]}',
189
+ ],
190
+ )
191
+
192
+ assert result.exit_code == 0
193
+ attrs = mock_client.put_listing.call_args.kwargs["attributes"]
194
+ assert attrs["item_name"] == [{"value": "JSON Title", "language_tag": "en_US"}]
195
+
196
+ @patch("amazon_sp_cli.cli.SPAPIAuth")
197
+ @patch("amazon_sp_cli.cli.SPAPIClient")
198
+ def test_update_listing_flags_override_json(self, mock_client_class, mock_auth_class, runner):
199
+ """Test that CLI flags override --attributes-json for same keys."""
200
+ mock_client = Mock()
201
+ mock_client.put_listing.return_value = {"sku": "TEST-SKU", "status": "ACCEPTED"}
202
+ mock_client_class.return_value = mock_client
203
+
204
+ mock_auth = Mock()
205
+ mock_auth_class.return_value = mock_auth
206
+
207
+ result = runner.invoke(
208
+ cli,
209
+ [
210
+ "update-listing",
211
+ "TEST-SKU",
212
+ "--product-type",
213
+ "PET_TOY",
214
+ "--title",
215
+ "Flag Title",
216
+ "--attributes-json",
217
+ '{"item_name": [{"value": "JSON Title", "language_tag": "en_US"}]}',
218
+ ],
219
+ )
220
+
221
+ assert result.exit_code == 0
222
+ attrs = mock_client.put_listing.call_args.kwargs["attributes"]
223
+ # flags are built first, then attributes_json is merged on top
224
+ assert attrs["item_name"] == [{"value": "JSON Title", "language_tag": "en_US"}]
225
+
226
+ @patch("amazon_sp_cli.cli.SPAPIAuth")
227
+ @patch("amazon_sp_cli.cli.SPAPIClient")
228
+ def test_update_listing_dry_run(self, mock_client_class, mock_auth_class, runner):
229
+ """Test dry-run mode."""
230
+ mock_client = Mock()
231
+ mock_client.put_listing.return_value = {"sku": "TEST-SKU", "issues": []}
232
+ mock_client_class.return_value = mock_client
233
+
234
+ mock_auth = Mock()
235
+ mock_auth_class.return_value = mock_auth
236
+
237
+ result = runner.invoke(
238
+ cli,
239
+ [
240
+ "update-listing",
241
+ "TEST-SKU",
242
+ "--product-type",
243
+ "PET_TOY",
244
+ "--title",
245
+ "New Title",
246
+ "--dry-run",
247
+ ],
248
+ )
249
+
250
+ assert result.exit_code == 0
251
+ assert "Validation passed" in result.output
252
+ assert mock_client.put_listing.call_args.kwargs["mode"] == "VALIDATION_PREVIEW"
253
+
254
+ @patch("amazon_sp_cli.cli.SPAPIAuth")
255
+ @patch("amazon_sp_cli.cli.SPAPIClient")
256
+ def test_update_listing_with_issues(self, mock_client_class, mock_auth_class, runner):
257
+ """Test that ERROR issues cause failure."""
258
+ mock_client = Mock()
259
+ mock_client.put_listing.return_value = {
260
+ "sku": "TEST-SKU",
261
+ "issues": [
262
+ {
263
+ "code": "99022",
264
+ "message": "Invalid attribute",
265
+ "severity": "ERROR",
266
+ }
267
+ ],
268
+ }
269
+ mock_client_class.return_value = mock_client
270
+
271
+ mock_auth = Mock()
272
+ mock_auth_class.return_value = mock_auth
273
+
274
+ result = runner.invoke(
275
+ cli,
276
+ [
277
+ "update-listing",
278
+ "TEST-SKU",
279
+ "--product-type",
280
+ "PET_TOY",
281
+ "--title",
282
+ "New Title",
283
+ ],
284
+ )
285
+
286
+ assert result.exit_code != 0
287
+ assert "Validation issues found" in result.output
288
+
289
+ @patch("amazon_sp_cli.cli.SPAPIAuth")
290
+ @patch("amazon_sp_cli.cli.SPAPIClient")
291
+ def test_update_listing_no_attributes(self, mock_client_class, mock_auth_class, runner):
292
+ """Test error when no attributes are provided."""
293
+ mock_client = Mock()
294
+ mock_client_class.return_value = mock_client
295
+
296
+ mock_auth = Mock()
297
+ mock_auth_class.return_value = mock_auth
298
+
299
+ result = runner.invoke(
300
+ cli,
301
+ [
302
+ "update-listing",
303
+ "TEST-SKU",
304
+ "--product-type",
305
+ "PET_TOY",
306
+ ],
307
+ )
308
+
309
+ assert result.exit_code != 0
310
+ assert "No attributes provided" in result.output
311
+
312
+ @patch("amazon_sp_cli.cli.SPAPIAuth")
313
+ @patch("amazon_sp_cli.cli.SPAPIClient")
314
+ def test_update_listing_invalid_json(self, mock_client_class, mock_auth_class, runner):
315
+ """Test error with invalid attributes JSON."""
316
+ mock_client = Mock()
317
+ mock_client_class.return_value = mock_client
318
+
319
+ mock_auth = Mock()
320
+ mock_auth_class.return_value = mock_auth
321
+
322
+ result = runner.invoke(
323
+ cli,
324
+ [
325
+ "update-listing",
326
+ "TEST-SKU",
327
+ "--product-type",
328
+ "PET_TOY",
329
+ "--attributes-json",
330
+ "not json",
331
+ ],
332
+ )
333
+
334
+ assert result.exit_code != 0
335
+ assert "Invalid JSON" in result.output
336
+
337
+ @patch("amazon_sp_cli.cli.SPAPIAuth")
338
+ @patch("amazon_sp_cli.cli.SPAPIClient")
339
+ def test_update_listing_requirements_option(self, mock_client_class, mock_auth_class, runner):
340
+ """Test requirements option is passed through."""
341
+ mock_client = Mock()
342
+ mock_client.put_listing.return_value = {"sku": "TEST-SKU", "status": "ACCEPTED"}
343
+ mock_client_class.return_value = mock_client
344
+
345
+ mock_auth = Mock()
346
+ mock_auth_class.return_value = mock_auth
347
+
348
+ result = runner.invoke(
349
+ cli,
350
+ [
351
+ "update-listing",
352
+ "TEST-SKU",
353
+ "--product-type",
354
+ "PET_TOY",
355
+ "--requirements",
356
+ "LISTING_OFFER_ONLY",
357
+ "--price",
358
+ "9.99",
359
+ ],
360
+ )
361
+
362
+ assert result.exit_code == 0
363
+ call_args = mock_client.put_listing.call_args
364
+ assert call_args.kwargs["requirements"] == "LISTING_OFFER_ONLY"
365
+
366
+
367
+ class TestDeleteListing:
368
+ """Test delete-listing command."""
369
+
370
+ @pytest.fixture
371
+ def runner(self):
372
+ """Create Click test runner."""
373
+ return CliRunner()
374
+
375
+ @patch("amazon_sp_cli.cli.SPAPIAuth")
376
+ @patch("amazon_sp_cli.cli.SPAPIClient")
377
+ def test_delete_listing(self, mock_client_class, mock_auth_class, runner):
378
+ """Test deleting a listing."""
379
+ mock_client = Mock()
380
+ mock_client.delete_listing.return_value = {"sku": "TEST-SKU", "status": "ACCEPTED"}
381
+ mock_client_class.return_value = mock_client
382
+
383
+ mock_auth = Mock()
384
+ mock_auth_class.return_value = mock_auth
385
+
386
+ result = runner.invoke(cli, ["delete-listing", "TEST-SKU"])
387
+
388
+ assert result.exit_code == 0
389
+ mock_client.delete_listing.assert_called_once_with("TEST-SKU")
390
+ output = json.loads(result.output)
391
+ assert output["status"] == "ACCEPTED"
@@ -1,2 +0,0 @@
1
- # Amazon SP-API CLI
2
- __version__ = "0.2.6"
File without changes
File without changes
File without changes
File without changes
File without changes