amazon-sp-cli 0.2.0__tar.gz → 0.2.1__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 (30) hide show
  1. {amazon_sp_cli-0.2.0/amazon_sp_cli.egg-info → amazon_sp_cli-0.2.1}/PKG-INFO +1 -1
  2. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/README.md +28 -27
  3. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/amazon_sp_cli/client.py +25 -16
  4. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/amazon_sp_cli/commands/a_plus.py +24 -18
  5. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/amazon_sp_cli/models/a_plus.py +25 -9
  6. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1/amazon_sp_cli.egg-info}/PKG-INFO +1 -1
  7. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/setup.py +1 -1
  8. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/tests/test_a_plus.py +39 -22
  9. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/LICENSE +0 -0
  10. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/MANIFEST.in +0 -0
  11. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/amazon_sp_cli/__init__.py +0 -0
  12. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/amazon_sp_cli/__main__.py +0 -0
  13. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/amazon_sp_cli/auth.py +0 -0
  14. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/amazon_sp_cli/cli.py +0 -0
  15. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/amazon_sp_cli/commands/__init__.py +0 -0
  16. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/amazon_sp_cli/commands/auth.py +0 -0
  17. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/amazon_sp_cli/commands/pricing.py +0 -0
  18. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/amazon_sp_cli/main.py +0 -0
  19. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/amazon_sp_cli/models/__init__.py +0 -0
  20. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/amazon_sp_cli.egg-info/SOURCES.txt +0 -0
  21. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/amazon_sp_cli.egg-info/dependency_links.txt +0 -0
  22. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/amazon_sp_cli.egg-info/entry_points.txt +0 -0
  23. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/amazon_sp_cli.egg-info/requires.txt +0 -0
  24. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/amazon_sp_cli.egg-info/top_level.txt +0 -0
  25. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/pyproject.toml +0 -0
  26. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/setup.cfg +0 -0
  27. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/tests/__init__.py +0 -0
  28. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/tests/test_auth.py +0 -0
  29. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/tests/test_client.py +0 -0
  30. {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.1}/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.0
3
+ Version: 0.2.1
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
@@ -40,40 +40,23 @@ default:
40
40
 
41
41
  ```bash
42
42
  # Get current price
43
- amz-sp pricing get PAW2603190101-BLU
43
+ amz-sp get-price PAW2603190101-BLU
44
44
 
45
45
  # Set new price
46
- amz-sp pricing set PAW2603190101-BLU 11.99
46
+ amz-sp set-price PAW2603190101-BLU 11.99
47
47
 
48
- # Create discount feed
49
- amz-sp pricing discount PAW2603190101-BLU 23
50
- ```
51
-
52
- ### Listings
53
-
54
- ```bash
55
- # Get listing details
56
- amz-sp listings get PAW2603190101-BLU
48
+ # Create sale price feed JSON
49
+ amz-sp sale-price PAW2603190101-BLU 23
57
50
 
58
- # Update listing
59
- amz-sp listings update PAW2603190101-BLU --data '{...}'
51
+ # Create discount feed JSON (legacy)
52
+ amz-sp create-discount PAW2603190101-BLU 23
60
53
  ```
61
54
 
62
55
  ### Catalog
63
56
 
64
57
  ```bash
65
58
  # Check competitor
66
- amz-sp catalog get B0GW72JGWK
67
- ```
68
-
69
- ### Inventory
70
-
71
- ```bash
72
- # Get FBA inventory
73
- amz-sp inventory list
74
-
75
- # Get specific SKU
76
- amz-sp inventory get PAW2603190101-BLU
59
+ amz-sp check-competitors B0GW72JGWK
77
60
  ```
78
61
 
79
62
  ### A+ Content
@@ -119,13 +102,31 @@ amz-sp a-plus asin remove my-content B123456789
119
102
  git clone https://github.com/stellaraether/amazon-sp-cli.git
120
103
  cd amazon-sp-cli
121
104
 
122
- # Install in editable mode
123
- pip install -e .
105
+ # One-shot setup (creates venv, installs deps, sets up pre-commit)
106
+ ./setup.sh
107
+ source .venv/bin/activate
108
+
109
+ # Or manually
110
+ python3 -m venv .venv
111
+ source .venv/bin/activate
112
+ pip install -r requirements-dev.txt
113
+ pre-commit install
124
114
 
125
115
  # Run locally
126
- python3 -m amazon_sp_cli.main pricing get PAW2603190101-BLU
116
+ python3 -m amazon_sp_cli get-price PAW2603190101-BLU
127
117
  ```
128
118
 
119
+ ## Releasing
120
+
121
+ This repository uses automated releases. To publish a new version:
122
+
123
+ 1. Open a **standalone PR** that only bumps `version` in `setup.py`.
124
+ 2. The PR must not include code, test, or documentation changes — a CI check enforces this.
125
+ 3. Once the PR is merged to `main`, the `Auto Release` workflow detects the new version, creates a tag (e.g. `v0.2.1`), and pushes it.
126
+ 4. The tag push triggers the `Publish to PyPI` workflow, which runs the full test suite and publishes the package.
127
+
128
+ Do not create tags manually.
129
+
129
130
  ## Requirements
130
131
 
131
132
  - Python 3.8+
@@ -38,7 +38,7 @@ class SPAPIClient:
38
38
 
39
39
  # Create the request
40
40
  url = f"https://{self.HOST}{path}"
41
- body = json.dumps(data) if data else ""
41
+ body = json.dumps(data) if data is not None else ""
42
42
 
43
43
  # Create AWS request for signing
44
44
  request = AWSRequest(method=method, url=url, headers=headers, data=body)
@@ -104,58 +104,68 @@ class SPAPIClient:
104
104
 
105
105
  # --- A+ Content API ---
106
106
 
107
+ def _add_marketplace_param(self, path: str) -> str:
108
+ """Append marketplaceId query param to A+ Content paths."""
109
+ separator = "&" if "?" in path else "?"
110
+ return f"{path}{separator}marketplaceId={self.marketplace_id}"
111
+
107
112
  def create_a_plus_content(self, content_data: dict) -> dict:
108
113
  """Create A+ Content document."""
109
- path = "/aplus/2020-11-01/contentDocuments"
114
+ path = self._add_marketplace_param("/aplus/2020-11-01/contentDocuments")
110
115
  data = {"contentDocument": content_data}
111
116
  return self.request("POST", path, data)
112
117
 
113
118
  def validate_a_plus_content(self, content_data: dict) -> dict:
114
119
  """Validate A+ Content without creating."""
115
120
  path = "/aplus/2020-11-01/contentDocuments"
116
- params = {"mode": "VALIDATION_PREVIEW"}
121
+ params = {"mode": "VALIDATION_PREVIEW", "marketplaceId": self.marketplace_id}
117
122
  path += "?" + urlencode(params)
118
123
  data = {"contentDocument": content_data}
119
124
  return self.request("POST", path, data)
120
125
 
121
126
  def update_a_plus_content(self, content_name: str, content_data: dict) -> dict:
122
127
  """Update existing A+ Content document."""
123
- path = f"/aplus/2020-11-01/contentDocuments/{content_name}"
128
+ path = self._add_marketplace_param(f"/aplus/2020-11-01/contentDocuments/{content_name}")
124
129
  data = {"contentDocument": content_data}
125
130
  return self.request("POST", path, data)
126
131
 
127
132
  def get_a_plus_content(self, content_name: str) -> dict:
128
133
  """Get A+ Content document by name."""
129
134
  path = f"/aplus/2020-11-01/contentDocuments/{content_name}"
135
+ params = {"marketplaceId": self.marketplace_id, "includedDataSet": "CONTENTS"}
136
+ path += "?" + urlencode(params)
130
137
  return self.request("GET", path)
131
138
 
132
139
  def list_a_plus_content(self, **filters) -> dict:
133
140
  """List A+ Content documents."""
134
141
  path = "/aplus/2020-11-01/contentDocuments"
135
- if filters:
136
- path += "?" + urlencode(filters)
142
+ params = {"marketplaceId": self.marketplace_id}
143
+ params.update(filters)
144
+ path += "?" + urlencode(params)
137
145
  return self.request("GET", path)
138
146
 
139
- def delete_a_plus_content(self, content_name: str) -> dict:
140
- """Delete A+ Content document."""
141
- path = f"/aplus/2020-11-01/contentDocuments/{content_name}"
142
- return self.request("DELETE", path)
147
+ def suspend_a_plus_content(self, content_reference_key: str) -> dict:
148
+ """Suspend A+ Content document (API does not support delete)."""
149
+ path = f"/aplus/2020-11-01/contentDocuments/{content_reference_key}/suspendSubmissions"
150
+ params = {"marketplaceId": self.marketplace_id}
151
+ path += "?" + urlencode(params)
152
+ return self.request("POST", path)
143
153
 
144
154
  def get_a_plus_content_asin_relations(self, content_name: str) -> dict:
145
155
  """Get ASIN relations for a content document."""
146
- path = f"/aplus/2020-11-01/contentDocuments/{content_name}/asins"
156
+ path = self._add_marketplace_param(f"/aplus/2020-11-01/contentDocuments/{content_name}/asins")
147
157
  return self.request("GET", path)
148
158
 
149
159
  def post_a_plus_content_asin_relations(self, content_name: str, asin_set: list) -> dict:
150
160
  """Associate ASINs with A+ Content document."""
151
- path = "/aplus/2020-11-01/contentAsinRelations"
161
+ path = self._add_marketplace_param("/aplus/2020-11-01/contentAsinRelations")
152
162
  data = {"contentDocumentName": content_name, "asinSet": asin_set}
153
163
  return self.request("POST", path, data)
154
164
 
155
165
  def delete_a_plus_content_asin_relations(self, content_name: str, asin_set: list) -> dict:
156
166
  """Remove ASIN associations from A+ Content document."""
157
167
  path = "/aplus/2020-11-01/contentAsinRelations"
158
- params = {"contentDocumentName": content_name}
168
+ params = {"contentDocumentName": content_name, "marketplaceId": self.marketplace_id}
159
169
  path += "?" + urlencode(params)
160
170
  data = {"asinSet": asin_set}
161
171
  return self.request("DELETE", path, data)
@@ -169,14 +179,13 @@ class SPAPIClient:
169
179
  resource: str = None,
170
180
  ) -> dict:
171
181
  """Create an upload destination for a file."""
172
- path = f"/uploads/2020-11-01/uploadDestinations/{marketplace_id}"
182
+ path = f"/uploads/2020-11-01/uploadDestinations/{resource}"
173
183
  params = {
184
+ "marketplaceIds": marketplace_id,
174
185
  "contentMD5": content_md5,
175
186
  "contentType": content_type,
176
187
  }
177
188
  if file_name:
178
189
  params["fileName"] = file_name
179
- if resource:
180
- params["resource"] = resource
181
190
  path += "?" + urlencode(params)
182
191
  return self.request("POST", path)
@@ -4,6 +4,7 @@ import base64
4
4
  import hashlib
5
5
  import json
6
6
  import os
7
+ import urllib.parse
7
8
 
8
9
  import click
9
10
  import requests
@@ -80,13 +81,13 @@ def register_a_plus_commands(cli_group, ensure_auth_client):
80
81
  click.echo(json.dumps(response, indent=2))
81
82
 
82
83
  @a_plus.command("get")
83
- @click.argument("content-name")
84
+ @click.argument("content-reference-key")
84
85
  @click.pass_context
85
86
  @handle_errors
86
- def get_content(ctx, content_name):
87
- """Get A+ Content document."""
87
+ def get_content(ctx, content_reference_key):
88
+ """Get A+ Content document by contentReferenceKey."""
88
89
  _, client = ensure_auth_client(ctx)
89
- response = client.get_a_plus_content(content_name)
90
+ response = client.get_a_plus_content(content_reference_key)
90
91
  click.echo(json.dumps(response, indent=2))
91
92
 
92
93
  @a_plus.command("list")
@@ -133,21 +134,23 @@ def register_a_plus_commands(cli_group, ensure_auth_client):
133
134
  if response:
134
135
  click.echo(json.dumps(response, indent=2))
135
136
 
136
- @a_plus.command("delete")
137
- @click.argument("content-name")
138
- @click.confirmation_option(prompt="Delete this A+ Content?")
137
+ @a_plus.command("suspend")
138
+ @click.argument("content-reference-key")
139
+ @click.confirmation_option(prompt="Suspend this A+ Content?")
139
140
  @click.pass_context
140
141
  @handle_errors
141
- def delete_content(ctx, content_name):
142
- """Delete A+ Content document."""
142
+ def suspend_content(ctx, content_reference_key):
143
+ """Suspend A+ Content document (API does not support delete)."""
143
144
  _, client = ensure_auth_client(ctx)
144
- client.delete_a_plus_content(content_name)
145
- click.echo(f"A+ Content deleted: {content_name}")
145
+ client.suspend_a_plus_content(content_reference_key)
146
+ click.echo(f"A+ Content suspended: {content_reference_key}")
146
147
 
147
148
  @a_plus.command("upload-image")
148
149
  @click.argument("file-path", type=click.Path(exists=True))
149
150
  @click.option("--content-type", default="image/jpeg", help="MIME type of the image")
150
- @click.option("--resource", default="aplus", help="Resource type for upload destination")
151
+ @click.option(
152
+ "--resource", default="aplus/2020-11-01/contentDocuments", help="Resource type for upload destination"
153
+ )
151
154
  @click.pass_context
152
155
  @handle_errors
153
156
  def upload_image(ctx, file_path, content_type, resource):
@@ -172,18 +175,21 @@ def register_a_plus_commands(cli_group, ensure_auth_client):
172
175
  payload = response.get("payload", response)
173
176
  upload_destination_id = payload.get("uploadDestinationId")
174
177
  upload_url = payload.get("url")
175
- headers = payload.get("headers", [])
176
178
 
177
179
  if not upload_url:
178
180
  click.echo("Error: No upload URL returned", err=True)
179
181
  raise click.Abort()
180
182
 
181
- upload_headers = {h["name"]: h["value"] for h in headers}
182
- upload_headers.setdefault("Content-Type", content_type)
183
- upload_headers.setdefault("Content-MD5", content_md5)
183
+ parsed = urllib.parse.urlparse(upload_url)
184
+ base_url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
185
+ form_fields = {k: v[0] for k, v in urllib.parse.parse_qs(parsed.query).items()}
184
186
 
185
- put_response = requests.put(upload_url, data=file_data, headers=upload_headers)
186
- put_response.raise_for_status()
187
+ post_response = requests.post(
188
+ base_url,
189
+ data=form_fields,
190
+ files={"File": (file_name, file_data, content_type)},
191
+ )
192
+ post_response.raise_for_status()
187
193
 
188
194
  click.echo(f"Upload successful: {file_name}")
189
195
  click.echo(f"uploadDestinationId: {upload_destination_id}")
@@ -4,12 +4,15 @@
4
4
  class TextComponent:
5
5
  """Text component for A+ Content modules."""
6
6
 
7
- def __init__(self, value: str, decoration: str = "NONE"):
7
+ def __init__(self, value: str, decorator_set: list = None):
8
8
  self.value = value
9
- self.decoration = decoration
9
+ self.decorator_set = decorator_set or []
10
10
 
11
11
  def to_dict(self) -> dict:
12
- return {"value": self.value, "decoration": self.decoration}
12
+ result = {"value": self.value}
13
+ if self.decorator_set:
14
+ result["decoratorSet"] = self.decorator_set
15
+ return result
13
16
 
14
17
 
15
18
  class ImageComponent:
@@ -109,6 +112,16 @@ class StandardComparisonTableModule:
109
112
  return result
110
113
 
111
114
 
115
+ class ParagraphComponent:
116
+ """Paragraph component containing a list of text."""
117
+
118
+ def __init__(self, text_list: list = None):
119
+ self.text_list = text_list or []
120
+
121
+ def to_dict(self) -> dict:
122
+ return {"textList": [t.to_dict() for t in self.text_list]}
123
+
124
+
112
125
  class StandardTextModule:
113
126
  """Standard Text-only module."""
114
127
 
@@ -178,7 +191,7 @@ class ContentModule:
178
191
  self.standard_image_text_overlay = standard_image_text_overlay
179
192
 
180
193
  def to_dict(self) -> dict:
181
- result = {"moduleType": self.module_type}
194
+ result = {"contentModuleType": self.module_type}
182
195
  field_name = self.MODULE_TYPES.get(self.module_type)
183
196
 
184
197
  if field_name == "standardImageText" and self.standard_image_text:
@@ -203,11 +216,11 @@ class ContentModule:
203
216
  issues = []
204
217
 
205
218
  if not self.module_type:
206
- issues.append(f"Module {index + 1}: moduleType is required")
219
+ issues.append(f"Module {index + 1}: contentModuleType is required")
207
220
  return issues
208
221
 
209
222
  if self.module_type not in self.MODULE_TYPES:
210
- issues.append(f"Module {index + 1}: invalid moduleType '{self.module_type}'")
223
+ issues.append(f"Module {index + 1}: invalid contentModuleType '{self.module_type}'")
211
224
  return issues
212
225
 
213
226
  data = self.to_dict()
@@ -224,7 +237,7 @@ class APlusContentDocument:
224
237
  self,
225
238
  name: str,
226
239
  content_type: str = "EBC",
227
- locale: str = "en_US",
240
+ locale: str = "en-US",
228
241
  content_module_list: list = None,
229
242
  ):
230
243
  self.name = name
@@ -262,7 +275,7 @@ def build_content_from_json(name: str, data: dict) -> APlusContentDocument:
262
275
  """Build APlusContentDocument from JSON dict."""
263
276
  content = APlusContentDocument(
264
277
  name=name,
265
- locale=data.get("locale", "en_US"),
278
+ locale=data.get("locale", "en-US"),
266
279
  )
267
280
 
268
281
  for mod_data in data.get("modules", []):
@@ -318,11 +331,14 @@ def build_module_from_json(data: dict) -> ContentModule:
318
331
  ),
319
332
  )
320
333
  elif module_type == "STANDARD_TEXT":
334
+ body = None
335
+ if data.get("body"):
336
+ body = ParagraphComponent(text_list=[TextComponent(data["body"])])
321
337
  return ContentModule(
322
338
  module_type=module_type,
323
339
  standard_text=StandardTextModule(
324
340
  headline=TextComponent(data["headline"]) if data.get("headline") else None,
325
- body=TextComponent(data["body"]) if data.get("body") else None,
341
+ body=body,
326
342
  ),
327
343
  )
328
344
  elif module_type == "STANDARD_IMAGE_TEXT_OVERLAY":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amazon-sp-cli
3
- Version: 0.2.0
3
+ Version: 0.2.1
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.2.0",
5
+ version="0.2.1",
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,17 +19,19 @@ from amazon_sp_cli.models.a_plus import (
19
19
 
20
20
  class TestDataModels:
21
21
  def test_text_component_to_dict(self):
22
- tc = TextComponent("Hello", "BOLD")
23
- assert tc.to_dict() == {"value": "Hello", "decoration": "BOLD"}
22
+ tc = TextComponent("Hello", [{"type": "BOLD"}])
23
+ assert tc.to_dict() == {"value": "Hello", "decoratorSet": [{"type": "BOLD"}]}
24
24
 
25
25
  def test_standard_text_module_to_dict(self):
26
+ from amazon_sp_cli.models.a_plus import ParagraphComponent
27
+
26
28
  mod = StandardTextModule(
27
29
  headline=TextComponent("Headline"),
28
- body=TextComponent("Body text"),
30
+ body=ParagraphComponent(text_list=[TextComponent("Body text")]),
29
31
  )
30
32
  result = mod.to_dict()
31
33
  assert result["headline"]["value"] == "Headline"
32
- assert result["body"]["value"] == "Body text"
34
+ assert result["body"]["textList"][0]["value"] == "Body text"
33
35
 
34
36
  def test_content_module_wrapper(self):
35
37
  mod = ContentModule(
@@ -39,7 +41,7 @@ class TestDataModels:
39
41
  ),
40
42
  )
41
43
  result = mod.to_dict()
42
- assert result["moduleType"] == "STANDARD_TEXT"
44
+ assert result["contentModuleType"] == "STANDARD_TEXT"
43
45
  assert result["standardText"]["headline"]["value"] == "H"
44
46
 
45
47
  def test_document_validation_passes(self):
@@ -64,7 +66,7 @@ class TestDataModels:
64
66
  def test_document_validation_invalid_type(self):
65
67
  doc = APlusContentDocument(name="test")
66
68
  doc.content_module_list = [ContentModule("INVALID_TYPE")]
67
- assert "invalid moduleType" in doc.validate()[0]
69
+ assert "invalid contentModuleType" in doc.validate()[0]
68
70
 
69
71
  def test_document_validation_empty_module(self):
70
72
  doc = APlusContentDocument(name="test")
@@ -88,12 +90,12 @@ class TestDataModels:
88
90
  class TestBuildFromJson:
89
91
  def test_build_content_from_json(self):
90
92
  data = {
91
- "locale": "en_US",
93
+ "locale": "en-US",
92
94
  "modules": [{"moduleType": "STANDARD_TEXT", "headline": "Hello", "body": "World"}],
93
95
  }
94
96
  doc = build_content_from_json("my-doc", data)
95
97
  assert doc.name == "my-doc"
96
- assert doc.locale == "en_US"
98
+ assert doc.locale == "en-US"
97
99
  assert len(doc.content_module_list) == 1
98
100
  assert doc.content_module_list[0].module_type == "STANDARD_TEXT"
99
101
 
@@ -231,17 +233,17 @@ class TestAPlusCLI:
231
233
  @patch("amazon_sp_cli.cli.SPAPIClient")
232
234
  def test_get(self, mock_client_class, mock_auth_class, runner):
233
235
  mock_client = Mock()
234
- mock_client.get_a_plus_content.return_value = {"name": "test-doc"}
236
+ mock_client.get_a_plus_content.return_value = {"contentRecord": {"contentDocument": {"name": "test-doc"}}}
235
237
  mock_client_class.return_value = mock_client
236
238
 
237
239
  mock_auth = Mock()
238
240
  mock_auth_class.return_value = mock_auth
239
241
 
240
- result = runner.invoke(cli, ["a-plus", "get", "test-doc"])
242
+ result = runner.invoke(cli, ["a-plus", "get", "abc123"])
241
243
 
242
244
  assert result.exit_code == 0
243
245
  assert "test-doc" in result.output
244
- mock_client.get_a_plus_content.assert_called_once_with("test-doc")
246
+ mock_client.get_a_plus_content.assert_called_once_with("abc123")
245
247
 
246
248
  @patch("amazon_sp_cli.cli.SPAPIAuth")
247
249
  @patch("amazon_sp_cli.cli.SPAPIClient")
@@ -290,17 +292,17 @@ class TestAPlusCLI:
290
292
 
291
293
  @patch("amazon_sp_cli.cli.SPAPIAuth")
292
294
  @patch("amazon_sp_cli.cli.SPAPIClient")
293
- def test_delete(self, mock_client_class, mock_auth_class, runner):
295
+ def test_suspend(self, mock_client_class, mock_auth_class, runner):
294
296
  mock_client = Mock()
295
297
  mock_client_class.return_value = mock_client
296
298
 
297
299
  mock_auth = Mock()
298
300
  mock_auth_class.return_value = mock_auth
299
301
 
300
- result = runner.invoke(cli, ["a-plus", "delete", "test-doc"], input="y\n")
302
+ result = runner.invoke(cli, ["a-plus", "suspend", "test-doc"], input="y\n")
301
303
 
302
304
  assert result.exit_code == 0
303
- mock_client.delete_a_plus_content.assert_called_once_with("test-doc")
305
+ mock_client.suspend_a_plus_content.assert_called_once_with("test-doc")
304
306
 
305
307
  @patch("amazon_sp_cli.cli.SPAPIAuth")
306
308
  @patch("amazon_sp_cli.cli.SPAPIClient")
@@ -348,24 +350,34 @@ class TestAPlusCLI:
348
350
  assert "B123" in result.output
349
351
  mock_client.get_a_plus_content_asin_relations.assert_called_once_with("test-doc")
350
352
 
351
- @patch("amazon_sp_cli.commands.a_plus.requests.put")
353
+ @patch("amazon_sp_cli.commands.a_plus.requests.post")
352
354
  @patch("amazon_sp_cli.cli.SPAPIAuth")
353
355
  @patch("amazon_sp_cli.cli.SPAPIClient")
354
- def test_upload_image(self, mock_client_class, mock_auth_class, mock_put, runner):
356
+ def test_upload_image(self, mock_client_class, mock_auth_class, mock_post, runner):
355
357
  mock_client = Mock()
356
358
  mock_client.marketplace_id = "ATVPDKIKX0DER"
357
359
  mock_client.create_upload_destination.return_value = {
358
- "uploadDestinationId": "upload-123",
359
- "url": "https://s3.amazonaws.com/presigned-url",
360
- "headers": [{"name": "Content-Type", "value": "image/jpeg"}],
360
+ "payload": {
361
+ "uploadDestinationId": "upload-123",
362
+ "url": (
363
+ "https://aplus-media.s3.amazonaws.com/"
364
+ "?x-amz-date=20251003T113949Z"
365
+ "&x-amz-signature=sig"
366
+ "&acl=private"
367
+ "&key=sc/image.jpg"
368
+ "&x-amz-algorithm=AWS4-HMAC-SHA256"
369
+ "&policy=pol"
370
+ "&x-amz-credential=cred"
371
+ ),
372
+ }
361
373
  }
362
374
  mock_client_class.return_value = mock_client
363
375
 
364
376
  mock_auth = Mock()
365
377
  mock_auth_class.return_value = mock_auth
366
378
 
367
- mock_put.return_value = Mock()
368
- mock_put.return_value.raise_for_status = Mock()
379
+ mock_post.return_value = Mock()
380
+ mock_post.return_value.raise_for_status = Mock()
369
381
 
370
382
  with runner.isolated_filesystem():
371
383
  with open("test-image.jpg", "wb") as f:
@@ -376,4 +388,9 @@ class TestAPlusCLI:
376
388
  assert result.exit_code == 0
377
389
  assert "upload-123" in result.output
378
390
  mock_client.create_upload_destination.assert_called_once()
379
- mock_put.assert_called_once()
391
+ mock_post.assert_called_once()
392
+ call_args = mock_post.call_args
393
+ assert call_args[0][0] == "https://aplus-media.s3.amazonaws.com/"
394
+ assert call_args[1]["data"]["key"] == "sc/image.jpg"
395
+ assert call_args[1]["data"]["acl"] == "private"
396
+ assert "File" in call_args[1]["files"]
File without changes
File without changes
File without changes