amazon-sp-cli 0.2.0__tar.gz → 0.2.2__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.2.0/amazon_sp_cli.egg-info → amazon_sp_cli-0.2.2}/PKG-INFO +1 -1
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/README.md +28 -27
- amazon_sp_cli-0.2.2/amazon_sp_cli/__init__.py +2 -0
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/amazon_sp_cli/client.py +25 -16
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/amazon_sp_cli/commands/a_plus.py +24 -18
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/amazon_sp_cli/models/a_plus.py +25 -9
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2/amazon_sp_cli.egg-info}/PKG-INFO +1 -1
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/setup.py +1 -1
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/tests/test_a_plus.py +39 -22
- amazon_sp_cli-0.2.0/amazon_sp_cli/__init__.py +0 -2
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/LICENSE +0 -0
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/MANIFEST.in +0 -0
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/amazon_sp_cli/__main__.py +0 -0
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/amazon_sp_cli/auth.py +0 -0
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/amazon_sp_cli/cli.py +0 -0
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/amazon_sp_cli/commands/__init__.py +0 -0
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/amazon_sp_cli/commands/auth.py +0 -0
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/amazon_sp_cli/commands/pricing.py +0 -0
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/amazon_sp_cli/main.py +0 -0
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/amazon_sp_cli/models/__init__.py +0 -0
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/amazon_sp_cli.egg-info/SOURCES.txt +0 -0
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/amazon_sp_cli.egg-info/dependency_links.txt +0 -0
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/amazon_sp_cli.egg-info/entry_points.txt +0 -0
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/amazon_sp_cli.egg-info/requires.txt +0 -0
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/amazon_sp_cli.egg-info/top_level.txt +0 -0
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/pyproject.toml +0 -0
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/setup.cfg +0 -0
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/tests/__init__.py +0 -0
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/tests/test_auth.py +0 -0
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/tests/test_client.py +0 -0
- {amazon_sp_cli-0.2.0 → amazon_sp_cli-0.2.2}/tests/test_pricing.py +0 -0
|
@@ -40,40 +40,23 @@ default:
|
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
42
|
# Get current price
|
|
43
|
-
amz-sp
|
|
43
|
+
amz-sp get-price PAW2603190101-BLU
|
|
44
44
|
|
|
45
45
|
# Set new price
|
|
46
|
-
amz-sp
|
|
46
|
+
amz-sp set-price PAW2603190101-BLU 11.99
|
|
47
47
|
|
|
48
|
-
# Create
|
|
49
|
-
amz-sp
|
|
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
|
-
#
|
|
59
|
-
amz-sp
|
|
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
|
|
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
|
-
#
|
|
123
|
-
|
|
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
|
|
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
|
-
|
|
136
|
-
|
|
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
|
|
140
|
-
"""
|
|
141
|
-
path = f"/aplus/2020-11-01/contentDocuments/{
|
|
142
|
-
|
|
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/{
|
|
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-
|
|
84
|
+
@click.argument("content-reference-key")
|
|
84
85
|
@click.pass_context
|
|
85
86
|
@handle_errors
|
|
86
|
-
def get_content(ctx,
|
|
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(
|
|
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("
|
|
137
|
-
@click.argument("content-
|
|
138
|
-
@click.confirmation_option(prompt="
|
|
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
|
|
142
|
-
"""
|
|
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.
|
|
145
|
-
click.echo(f"A+ Content
|
|
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(
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
186
|
-
|
|
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,
|
|
7
|
+
def __init__(self, value: str, decorator_set: list = None):
|
|
8
8
|
self.value = value
|
|
9
|
-
self.
|
|
9
|
+
self.decorator_set = decorator_set or []
|
|
10
10
|
|
|
11
11
|
def to_dict(self) -> dict:
|
|
12
|
-
|
|
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 = {"
|
|
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}:
|
|
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
|
|
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 = "
|
|
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", "
|
|
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=
|
|
341
|
+
body=body,
|
|
326
342
|
),
|
|
327
343
|
)
|
|
328
344
|
elif module_type == "STANDARD_IMAGE_TEXT_OVERLAY":
|
|
@@ -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", "
|
|
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["
|
|
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
|
|
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": "
|
|
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 == "
|
|
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", "
|
|
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("
|
|
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
|
|
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", "
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
-
"
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
368
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
File without changes
|