
Linkfox Amazon Store Feeds
- 182 installs
- 64 repo stars
- Updated August 3, 2026
- linkfox-ai/linkfox-skills
Helps with ai & agent building tasks.
About
linkfox-amazon-store-feeds is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- linkfox-amazon-store-feeds
- AI & Agent Building
- AI-coding skill
Linkfox Amazon Store Feeds by the numbers
- 182 all-time installs (skills.sh)
- +35 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #3,035 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/linkfox-ai/linkfox-skills --skill linkfox-amazon-store-feedsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 182 |
|---|---|
| repo stars | ★ 64 |
| Last updated | August 3, 2026 |
| Repository | linkfox-ai/linkfox-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Amazon 店铺 Feeds
本 skill 与 `linkfox-amazon-store-auth` 等同属 Amazon Store 系列:先 `POST /spApi/storeTokens`,再 `POST /spApi/developerProxy` 转发 GET / POST / DELETE。
官方参考索引
| 能力 | 文档 |
|---|---|
| createFeedDocument | createFeedDocument |
| getFeedDocument | getFeedDocument |
| createFeed | createFeed |
| getFeed | getFeed |
| getFeeds | getFeeds |
| cancelFeed | cancelFeed |
---
Prerequisites
1. 依赖 `linkfox-amazon-store-auth`;运行 python scripts/check_auth_dependency.py,exit 42 时需先安装授权 skill。 2. 应用需具备 Feeds 相关角色;feedType 须与上传文件格式匹配(见 Amazon Feed Type Values 文档)。
---
典型工作流
1. `create_feed_document.py` → 得到 feedDocumentId 与 `url`(上传地址)。 2. `upload_feed_document.py` → 对 url PUT 上传 feed 文件(不经 developerProxy)。 3. `create_feed.py` → 传入 inputFeedDocumentId、feedType、marketplaceIds。 4. `get_feed.py` / `get_feeds.py` → 轮询 processingStatus(如 IN_QUEUE、IN_PROGRESS、DONE、FATAL)。 5. 处理完成后用 `get_feed_document.py` 下载 resultFeedDocumentId 对应文档(再按返回 URL 自行下载结果文件)。
---
Current Capabilities
| 能力 | path | method | 脚本 |
|---|---|---|---|
| createFeedDocument | feeds/2021-06-30/documents | POST | create_feed_document.py |
| getFeedDocument | feeds/2021-06-30/documents/{feedDocumentId} | GET | get_feed_document.py |
| createFeed | feeds/2021-06-30/feeds | POST | create_feed.py |
| getFeed | feeds/2021-06-30/feeds/{feedId} | GET | get_feed.py |
| getFeeds | feeds/2021-06-30/feeds | GET | get_feeds.py |
| cancelFeed | feeds/2021-06-30/feeds/{feedId} | DELETE | cancel_feed.py |
| 上传文档内容 | createFeedDocument 返回的 URL | PUT | upload_feed_document.py |
共享模块:`_spapi_feeds_common.py`。
---
Scripts 示例
export LINKFOXAGENT_API_KEY="<your-key>"
python scripts/create_feed_document.py '{"sellerId":"A1...","region":"NA","contentType":"text/tab-separated-values; charset=UTF-8"}'
python scripts/upload_feed_document.py '{"uploadUrl":"<from createFeedDocument>","contentType":"text/tab-separated-values; charset=UTF-8","filePath":"./inventory.tsv"}'
python scripts/create_feed.py '{"sellerId":"A1...","region":"NA","feedType":"POST_FLAT_FILE_INVLOADER_DATA","marketplaceIds":["ATVPDKIKX0DER"],"inputFeedDocumentId":"<feedDocumentId>"}'---
Display Rules
1. 先看 `developerProxy.errcode` / `httpStatus`;createFeedDocument 常为 201,createFeed 常为 202。 2. getFeeds 分页:仅用上一页 `nextToken` 作为下一请求的 `paginationToken` 参数名在 Amazon 侧为 `nextToken`(脚本字段名 nextToken)。 3. upload 失败与 SP-API 网关无关,检查 uploadUrl 是否过期、Content-Type 是否与 createFeedDocument 一致。 4. 网关 path 白名单需包含 `feeds/2021-06-30/` 前缀。
---
Important Limitations
- 本 skill 不代替 Amazon 侧 feed 文件 schema 校验;
feedType、TSV/XML 格式以官方为准。 - 下载 getFeedDocument 返回的 url 内容需另行 HTTP GET(与 upload 类似,不经 developerProxy)。
- 详见 `references/api.md`。
Feedback: skillName:linkfox-amazon-store-feeds。
--- 更多跨境 skill:[LinkFox Skills](https://skill.linkfox.com/)
<!-- LF_LARGE_RESPONSE_BLOCK -->
Handling Large Responses
To avoid overflowing the agent context, persist the response to disk and extract only the fields you need:
python scripts/response_io.py run --script scripts/cancel_feed.py --out-dir <DIR> '<params>'
python scripts/response_io.py read <file> --fields "<paths>" # or --path "<JMESPath>"Pick--out-diroutside any git working tree (e.g./tmp/...on Unix,%TEMP%/...on Windows). Persisted responses may contain PII, pricing, or auth-sensitive data — do not commit them. Files are not auto-deleted; clean up when the task is done.
This skill exposes multiple entry scripts:cancel_feed.py,check_auth_dependency.py,create_feed.py,create_feed_document.py,get_feed.py,get_feed_document.py,get_feeds.py,upload_feed_document.py. Pass--script scripts/<name>.pyto choose the one you need.
run writes the full response to a file and emits only a schema preview + file path. read projects specific fields, with --limit/--offset for slicing and --format json|jsonl|csv|table for output.
When to prefer this pattern — apply your judgment based on the response characteristics, e.g.:
- High field count per record, or fields you don't need
- Batch/paginated results (multiple items per call)
- Long-text fields (descriptions, reviews, HTML, time series)
- Output reused across later steps rather than consumed immediately
For small, single-use responses, calling the main script directly is fine.
⚠️ The preview is a truncated schema + sample, not the full data. Any field-level decision must read from the persisted file via read. <!-- /LF_LARGE_RESPONSE_BLOCK -->
linkfox-amazon-store-feeds — API 参考(Feeds v2021-06-30)
1. 调用链
| 步骤 | 端点 | 说明 |
|---|---|---|
| 1 | POST {BASE}/spApi/storeTokens | {"sellerId","region"} → accessToken |
| 2 | POST {BASE}/spApi/developerProxy | 转发 SP-API(除文档上传/下载 URL 外) |
环境变量:LINKFOXAGENT_API_KEY(必填);STORE_API_BASE_URL / SPAPI_BASE_URL(可选,默认 https://tool-gateway.linkfox.com)。
developerProxy Body
| 字段 | 说明 |
|---|---|
| region | NA / EU / FE |
| path | 如 feeds/2021-06-30/feeds,无前导 / |
| method | GET / POST / DELETE |
| amzAccessToken | storeTokens 返回值 |
| queryString | 无 ? 前缀 |
| body | POST 时为 JSON 字符串 |
| contentType | 一般 application/json |
网关响应:errcode、httpStatus、body(字符串)。成功 HTTP 可能为 200 / 201 / 202 / 204。
---
2. 脚本入参摘要
公共
| 字段 | 必填 | 说明 |
|---|---|---|
| sellerId | 是 | 卖家 ID |
| region | 是 | 区域 |
| skipDepCheck | 否 | 跳过本地 auth 探测 |
create_feed_document.py
| 字段 | 必填 | 说明 |
|---|---|---|
| contentType | 是 | 与将要上传的文件 MIME 一致,如 text/tab-separated-values; charset=UTF-8 |
解析字段:`feedDocument`(含 feedDocumentId、url 等,以 Amazon 为准)。
upload_feed_document.py(不经 developerProxy)
| 字段 | 必填 | 说明 |
|---|---|---|
| uploadUrl | 是 | createFeedDocument 返回的 url |
| contentType | 是 | 与 createFeedDocument 相同 |
| filePath | 三选一 | 本地文件路径 |
| content | 三选一 | UTF-8 字符串内容 |
| contentBase64 | 三选一 | Base64 编码内容 |
create_feed.py
| 字段 | 必填 | 说明 |
|---|---|---|
| feedType | 是 | 如 POST_FLAT_FILE_INVLOADER_DATA |
| marketplaceIds | 是 | 1~25 个站点 ID |
| inputFeedDocumentId | 是 | 已上传内容的文档 ID |
| feedOptions | 否 | 因 feedType 而异的对象 |
get_feed.py / cancel_feed.py
| 字段 | 必填 |
|---|---|
| feedId | 是 |
get_feeds.py
| 字段 | 必填 | 说明 |
|---|---|---|
| feedTypes | 条件 | 与 nextToken 二选一;最多 10 个 |
| nextToken | 条件 | 分页时单独传此参数 |
| marketplaceIds | 否 | 最多 10 个 |
| processingStatuses | 否 | CANCELLED, DONE, FATAL, IN_PROGRESS, IN_QUEUE |
| createdSince / createdUntil | 否 | ISO 8601 |
| pageSize | 否 | 1~100,默认 10 |
get_feed_document.py
| 字段 | 必填 | 说明 |
|---|---|---|
| feedDocumentId | 是 | |
| enableContentEncodingUrlHeader | 否 | boolean;GZIP 时便于客户端自动解压 |
---
3. Feed 标准流程
createFeedDocument → PUT uploadUrl (upload_feed_document.py)
→ createFeed → poll getFeed until DONE/FATAL
→ getFeedDocument(resultFeedDocumentId) → GET 结果 url---
4. 常见 feedType(示例)
以 Feed Type Values 为准,例如:
POST_PRODUCT_DATA/POST_INVENTORY_AVAILABILITY_DATAPOST_FLAT_FILE_INVLOADER_DATAPOST_ORDER_FULFILLMENT_DATA
---
5. 错误与限制
- 403:权限或 feedType 未授权。
- 1005(网关):path 未在白名单,需放行
feeds/2021-06-30/。 - 429:降频;getFeeds 等有独立 usage plan。
- 上传 URL 有时效,过期需重新 createFeedDocument。
---
6. Feedback
上报问题时注明 `skillName`: `linkfox-amazon-store-feeds`,并附 developerProxy JSON(脱敏)。
"""Shared helpers for linkfox-amazon-store-feeds (Feeds API v2021-06-30)."""
from __future__ import annotations
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Any, Iterable, Optional
from urllib.error import HTTPError, URLError
from urllib.parse import quote
from urllib.request import Request, urlopen
REQUIRED_SKILL = "linkfox-amazon-store-auth"
DEPENDENCY_EXIT_CODE = 42
FEEDS_API_VERSION = "2021-06-30"
FEEDS_PATH_PREFIX = f"feeds/{FEEDS_API_VERSION}"
API_BASE_URL = os.environ.get("STORE_API_BASE_URL") or os.environ.get(
"SPAPI_BASE_URL", "https://tool-gateway.linkfox.com"
)
STORE_TOKENS_ENDPOINT = f"{API_BASE_URL.rstrip('/')}/spApi/storeTokens"
DEVELOPER_PROXY_ENDPOINT = f"{API_BASE_URL.rstrip('/')}/spApi/developerProxy"
SUCCESS_HTTP_STATUSES = frozenset({200, 201, 202, 204})
def ensure_auth_skill_available(caller: str = "feeds script") -> None:
here = Path(__file__).resolve().parent
checker = here / "check_auth_dependency.py"
if not checker.exists():
payload = {
"missingSkill": REQUIRED_SKILL,
"reason": f"check_auth_dependency.py not found next to {caller}",
}
print(f"DEPENDENCY_MISSING: {json.dumps(payload, ensure_ascii=False)}", file=sys.stderr)
sys.exit(DEPENDENCY_EXIT_CODE)
try:
result = subprocess.run(
[sys.executable, str(checker)],
capture_output=True,
text=True,
timeout=10,
)
except Exception as exc: # pragma: no cover
payload = {"missingSkill": REQUIRED_SKILL, "reason": str(exc)}
print(f"DEPENDENCY_MISSING: {json.dumps(payload, ensure_ascii=False)}", file=sys.stderr)
sys.exit(DEPENDENCY_EXIT_CODE)
if result.stderr:
sys.stderr.write(result.stderr)
if not result.stderr.endswith("\n"):
sys.stderr.write("\n")
if result.returncode != 0:
sys.exit(DEPENDENCY_EXIT_CODE)
def get_api_key() -> str:
key = os.environ.get("LINKFOXAGENT_API_KEY")
if not key:
print(
"API Key not configured. Set:\n export LINKFOXAGENT_API_KEY=<your-key>",
file=sys.stderr,
)
sys.exit(1)
return key
def call_api(endpoint: str, params: dict, timeout: int = 120) -> dict:
api_key = get_api_key()
data = json.dumps(params).encode("utf-8")
req = Request(
endpoint,
data=data,
headers={
"Authorization": api_key,
"Content-Type": "application/json",
"User-Agent": "LinkFox-Skill/1.0",
},
method="POST",
)
try:
with urlopen(req, timeout=timeout) as response:
return json.loads(response.read().decode("utf-8"))
except HTTPError as e:
body = e.read().decode("utf-8") if e.fp else ""
return {"error": f"HTTP {e.code}: {e.reason}", "details": body}
except URLError as e:
return {"error": f"Connection failed: {e.reason}"}
def get_store_tokens(seller_id: str, region: str) -> dict:
return call_api(STORE_TOKENS_ENDPOINT, {"sellerId": seller_id, "region": region})
def developer_proxy_call(
region: str,
path: str,
method: str,
access_token: str,
*,
query_string: Optional[str] = None,
body: Optional[str] = None,
content_type: str = "application/json",
timeout: int = 120,
) -> dict:
params: dict[str, Any] = {
"region": region,
"path": path,
"method": method,
"amzAccessToken": access_token,
}
if query_string:
params["queryString"] = query_string
if body is not None:
params["body"] = body
params["contentType"] = content_type
return call_api(DEVELOPER_PROXY_ENDPOINT, params, timeout=timeout)
def encode_path_segment(value: str) -> str:
return quote(str(value).strip(), safe="")
def str_list(val: object, name: str) -> list[str]:
if val is None:
return []
if isinstance(val, str):
return [x.strip() for x in val.split(",") if x.strip()]
if isinstance(val, list):
return [str(x).strip() for x in val if str(x).strip()]
print(f"{name} must be a string or string[]", file=sys.stderr)
sys.exit(1)
def norm_marketplace_ids(params: dict) -> list[str]:
mids = params.get("marketplaceIds")
if mids is None:
mid = params.get("marketplaceId")
if mid is not None:
return [str(mid).strip()]
return []
return str_list(mids, "marketplaceIds")
def merge_success_json(
out: dict,
proxy: dict,
result_key: str,
*,
success_http: Iterable[int] = SUCCESS_HTTP_STATUSES,
) -> None:
if proxy.get("errcode") != 200:
return
try:
status = int(proxy.get("httpStatus") or 0)
except (TypeError, ValueError):
return
if status not in success_http:
return
body_raw = proxy.get("body")
if body_raw is None or not str(body_raw).strip():
out[result_key] = None
return
try:
out[result_key] = json.loads(str(body_raw))
except json.JSONDecodeError:
out[result_key] = None
out[f"{result_key}Raw"] = body_raw
def load_cli_params() -> dict:
if len(sys.argv) < 2:
return {}
try:
return json.loads(sys.argv[1])
except json.JSONDecodeError as e:
print(f"Invalid JSON: {e}", file=sys.stderr)
sys.exit(1)
def require_seller_region(params: dict) -> tuple[str, str]:
for f in ("sellerId", "region"):
if f not in params:
print(f"Missing required field: {f}", file=sys.stderr)
sys.exit(1)
return str(params["sellerId"]), str(params["region"])
#!/usr/bin/env python3
"""
Amazon Store — cancelFeed (SP-API Feeds v2021-06-30)
====================================================
DELETE `feeds/2021-06-30/feeds/{feedId}`
官方参考: https://developer-docs.amazon.com/sp-api/reference/cancelfeed
"""
from __future__ import annotations
import json
import sys
from _spapi_feeds_common import (
FEEDS_PATH_PREFIX,
developer_proxy_call,
encode_path_segment,
ensure_auth_skill_available,
get_store_tokens,
load_cli_params,
require_seller_region,
)
def main() -> None:
params = load_cli_params()
if not params:
print(
"Usage: cancel_feed.py '<JSON>'\nRequired: sellerId, region, feedId",
file=sys.stderr,
)
sys.exit(1)
if not params.get("skipDepCheck"):
ensure_auth_skill_available("cancel_feed.py")
if "feedId" not in params:
print("Missing required field: feedId", file=sys.stderr)
sys.exit(1)
seller_id, region = require_seller_region(params)
path = f"{FEEDS_PATH_PREFIX}/feeds/{encode_path_segment(params['feedId'])}"
tokens = get_store_tokens(seller_id, region)
if "error" in tokens or "accessToken" not in tokens:
print(json.dumps(tokens, indent=2, ensure_ascii=False))
sys.exit(1)
proxy = developer_proxy_call(region, path, "DELETE", tokens["accessToken"])
out: dict = {"developerProxy": proxy, "resolvedPath": path}
print(json.dumps(out, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Dependency Check - linkfox-amazon-store-feeds
==============================================
与 `linkfox-amazon-store-listings` / `linkfox-amazon-store-pricing` 相同逻辑:
探测是否已安装 `linkfox-amazon-store-auth`。
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
REQUIRED_SKILL = "linkfox-amazon-store-auth"
_AUTH_SKILL_DIR_ALIASES = ("linkfox-amazon-store-auth", "linkfox-amazon-spapi-auth")
DEPENDENCY_EXIT_CODE = 42
def _split_path_list(raw: str | None) -> list[Path]:
if not raw or not raw.strip():
return []
parts = [p.strip() for p in raw.split(os.pathsep) if p.strip()]
return [Path(p).expanduser() for p in parts]
def candidate_skill_roots() -> list[Path]:
roots: list[Path] = []
for env_var in ("LINKFOX_SKILLS_DIR", "SKILLS_DIR", "CURSOR_SKILLS_DIR"):
p = os.environ.get(env_var)
if p:
roots.append(Path(p).expanduser())
roots.extend(_split_path_list(os.environ.get("HERMES_SKILLS_EXTERNAL_DIRS")))
for env_var in ("OPENCLAW_WORKSPACE", "OPENCLAW_ROOT", "OPENCLAW_WORKDIR"):
ws = os.environ.get(env_var)
if ws:
w = Path(ws).expanduser()
roots.append(w / "skills")
roots.append(w / ".agents" / "skills")
oc_skills = os.environ.get("OPENCLAW_SKILLS_DIR")
if oc_skills:
roots.append(Path(oc_skills).expanduser())
try:
cwd = Path.cwd()
roots.append(cwd / "skills")
roots.append(cwd / ".agents" / "skills")
except OSError:
pass
here = Path(__file__).resolve()
if len(here.parents) >= 3:
roots.append(here.parents[2])
home = Path.home()
roots.extend([
home / ".claude" / "skills",
home / ".cursor" / "skills",
home / ".cursor" / "skills-cursor",
home / ".linkfox" / "skills",
home / ".openclaw" / "skills",
home / ".hermes" / "skills",
])
seen: set[Path] = set()
unique: list[Path] = []
for r in roots:
try:
rr = r.resolve()
except OSError:
rr = r
if rr not in seen:
seen.add(rr)
unique.append(r)
return unique
def _hermes_category_skill_md(hermes_skills_root: Path, skill_dir_name: str) -> Path | None:
if not hermes_skills_root.is_dir():
return None
for category_dir in sorted(hermes_skills_root.iterdir()):
if not category_dir.is_dir():
continue
if category_dir.name.startswith(".") or category_dir.name == ".hub":
continue
candidate = category_dir / skill_dir_name / "SKILL.md"
if candidate.is_file():
return candidate
return None
def _hermes_plugin_skill_md(home: Path, skill_dir_name: str) -> Path | None:
plugins_root = home / ".hermes" / "plugins"
if not plugins_root.is_dir():
return None
for plugin_dir in sorted(plugins_root.iterdir()):
if not plugin_dir.is_dir():
continue
candidate = plugin_dir / "skills" / skill_dir_name / "SKILL.md"
if candidate.is_file():
return candidate
return None
def locate_dependency() -> Path | None:
home = Path.home()
for skill_dir_name in _AUTH_SKILL_DIR_ALIASES:
for root in candidate_skill_roots():
target = root / skill_dir_name / "SKILL.md"
if target.is_file():
return target
found = _hermes_category_skill_md(home / ".hermes" / "skills", skill_dir_name)
if found is not None:
return found
hsh = os.environ.get("HERMES_SKILLS_HOME")
if hsh:
found = _hermes_category_skill_md(Path(hsh).expanduser(), skill_dir_name)
if found is not None:
return found
found = _hermes_plugin_skill_md(home, skill_dir_name)
if found is not None:
return found
return None
def searched_locations_for_report() -> list[str]:
home = Path.home()
out: list[str] = [str(p) for p in candidate_skill_roots()]
out.append(str(home / ".hermes" / "skills"))
out.append(str(home / ".hermes" / "plugins"))
hsh = os.environ.get("HERMES_SKILLS_HOME")
if hsh:
out.append(str(Path(hsh).expanduser()))
seen: set[str] = set()
unique: list[str] = []
for s in out:
if s not in seen:
seen.add(s)
unique.append(s)
return unique
def emit(as_json: bool, ok: bool, payload: dict) -> None:
prefix = "DEPENDENCY_OK:" if ok else "DEPENDENCY_MISSING:"
body = json.dumps(payload, ensure_ascii=False)
if as_json:
out = dict(payload)
out["status"] = "ok" if ok else "missing"
print(json.dumps(out, ensure_ascii=False, indent=2))
print(f"{prefix} {body}", file=sys.stderr)
def main() -> None:
parser = argparse.ArgumentParser(description="Check required dependency skill availability.")
parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON on stdout.")
args = parser.parse_args()
found = locate_dependency()
if found is not None:
emit(
as_json=args.json,
ok=True,
payload={"skill": REQUIRED_SKILL, "skillMdPath": str(found)},
)
sys.exit(0)
payload = {
"missingSkill": REQUIRED_SKILL,
"reason": (
f"linkfox-amazon-store-feeds 依赖 `{REQUIRED_SKILL}`,"
"但在常见 skill 安装路径下未找到其 SKILL.md。"
),
"searchedRoots": searched_locations_for_report(),
"suggestedActions": [
f"If a skill installer tool is available, install '{REQUIRED_SKILL}' immediately.",
"Otherwise ask the user to install from https://skill.linkfox.com/ and retry.",
],
"marketplaceUrl": "https://skill.linkfox.com/",
}
emit(as_json=args.json, ok=False, payload=payload)
sys.exit(DEPENDENCY_EXIT_CODE)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Amazon Store — createFeedDocument (SP-API Feeds v2021-06-30)
============================================================
POST `feeds/2021-06-30/documents`
官方参考: https://developer-docs.amazon.com/sp-api/reference/createfeeddocument
Usage:
python create_feed_document.py '{"sellerId":"A1...","region":"NA","contentType":"text/tab-separated-values; charset=UTF-8"}'
"""
from __future__ import annotations
import json
import sys
from _spapi_feeds_common import (
FEEDS_PATH_PREFIX,
developer_proxy_call,
ensure_auth_skill_available,
get_store_tokens,
load_cli_params,
merge_success_json,
require_seller_region,
)
def main() -> None:
params = load_cli_params()
if not params:
print(
"Usage: create_feed_document.py '<JSON>'\nRequired: sellerId, region, contentType",
file=sys.stderr,
)
sys.exit(1)
if not params.get("skipDepCheck"):
ensure_auth_skill_available("create_feed_document.py")
if "contentType" not in params:
print("Missing required field: contentType", file=sys.stderr)
sys.exit(1)
seller_id, region = require_seller_region(params)
path = f"{FEEDS_PATH_PREFIX}/documents"
body_obj = {"contentType": str(params["contentType"]).strip()}
body_str = json.dumps(body_obj, ensure_ascii=False)
tokens = get_store_tokens(seller_id, region)
if "error" in tokens or "accessToken" not in tokens:
print(json.dumps(tokens, indent=2, ensure_ascii=False))
sys.exit(1)
proxy = developer_proxy_call(
region, path, "POST", tokens["accessToken"], body=body_str
)
out: dict = {
"developerProxy": proxy,
"resolvedPath": path,
"requestBody": body_obj,
}
merge_success_json(out, proxy, "feedDocument")
print(json.dumps(out, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Amazon Store — createFeed (SP-API Feeds v2021-06-30)
====================================================
POST `feeds/2021-06-30/feeds`。须先 createFeedDocument 并上传文档内容。
官方参考: https://developer-docs.amazon.com/sp-api/reference/createfeed
"""
from __future__ import annotations
import json
import sys
from typing import Any, Dict
from _spapi_feeds_common import (
FEEDS_PATH_PREFIX,
developer_proxy_call,
ensure_auth_skill_available,
get_store_tokens,
load_cli_params,
merge_success_json,
norm_marketplace_ids,
require_seller_region,
)
def _build_body(params: dict) -> Dict[str, Any]:
for f in ("feedType", "inputFeedDocumentId"):
if f not in params:
print(f"Missing required field: {f}", file=sys.stderr)
sys.exit(1)
mids = norm_marketplace_ids(params)
if not mids:
print("Missing marketplaceIds (or marketplaceId)", file=sys.stderr)
sys.exit(1)
if len(mids) > 25:
print("marketplaceIds length must be ≤ 25", file=sys.stderr)
sys.exit(1)
body: Dict[str, Any] = {
"feedType": str(params["feedType"]).strip(),
"marketplaceIds": mids,
"inputFeedDocumentId": str(params["inputFeedDocumentId"]).strip(),
}
fo = params.get("feedOptions")
if fo is not None:
if not isinstance(fo, dict):
print("feedOptions must be an object when provided", file=sys.stderr)
sys.exit(1)
body["feedOptions"] = fo
return body
def main() -> None:
params = load_cli_params()
if not params:
print(
"Usage: create_feed.py '<JSON>'\n"
"Required: sellerId, region, feedType, marketplaceIds, inputFeedDocumentId\n"
"Optional: feedOptions",
file=sys.stderr,
)
sys.exit(1)
if not params.get("skipDepCheck"):
ensure_auth_skill_available("create_feed.py")
seller_id, region = require_seller_region(params)
path = f"{FEEDS_PATH_PREFIX}/feeds"
body_obj = _build_body(params)
body_str = json.dumps(body_obj, ensure_ascii=False)
tokens = get_store_tokens(seller_id, region)
if "error" in tokens or "accessToken" not in tokens:
print(json.dumps(tokens, indent=2, ensure_ascii=False))
sys.exit(1)
proxy = developer_proxy_call(
region, path, "POST", tokens["accessToken"], body=body_str
)
out: dict = {
"developerProxy": proxy,
"resolvedPath": path,
"requestBody": body_obj,
}
merge_success_json(out, proxy, "feed")
print(json.dumps(out, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Amazon Store — getFeedDocument (SP-API Feeds v2021-06-30)
=========================================================
GET `feeds/2021-06-30/documents/{feedDocumentId}`
官方参考: https://developer-docs.amazon.com/sp-api/reference/getfeeddocument
"""
from __future__ import annotations
import json
import sys
from urllib.parse import quote
from _spapi_feeds_common import (
FEEDS_PATH_PREFIX,
developer_proxy_call,
encode_path_segment,
ensure_auth_skill_available,
get_store_tokens,
load_cli_params,
merge_success_json,
require_seller_region,
)
def main() -> None:
params = load_cli_params()
if not params:
print(
"Usage: get_feed_document.py '<JSON>'\n"
"Required: sellerId, region, feedDocumentId\n"
"Optional: enableContentEncodingUrlHeader (boolean)",
file=sys.stderr,
)
sys.exit(1)
if not params.get("skipDepCheck"):
ensure_auth_skill_available("get_feed_document.py")
if "feedDocumentId" not in params:
print("Missing required field: feedDocumentId", file=sys.stderr)
sys.exit(1)
seller_id, region = require_seller_region(params)
doc_id = encode_path_segment(params["feedDocumentId"])
path = f"{FEEDS_PATH_PREFIX}/documents/{doc_id}"
qs = None
if "enableContentEncodingUrlHeader" in params:
flag = "true" if params["enableContentEncodingUrlHeader"] else "false"
qs = f"enableContentEncodingUrlHeader={quote(flag, safe='')}"
tokens = get_store_tokens(seller_id, region)
if "error" in tokens or "accessToken" not in tokens:
print(json.dumps(tokens, indent=2, ensure_ascii=False))
sys.exit(1)
proxy = developer_proxy_call(
region, path, "GET", tokens["accessToken"], query_string=qs
)
out: dict = {"developerProxy": proxy, "resolvedPath": path, "queryString": qs}
merge_success_json(out, proxy, "feedDocument")
print(json.dumps(out, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Amazon Store — getFeed (SP-API Feeds v2021-06-30)
==================================================
GET `feeds/2021-06-30/feeds/{feedId}`
官方参考: https://developer-docs.amazon.com/sp-api/reference/getfeed
"""
from __future__ import annotations
import json
import sys
from _spapi_feeds_common import (
FEEDS_PATH_PREFIX,
developer_proxy_call,
encode_path_segment,
ensure_auth_skill_available,
get_store_tokens,
load_cli_params,
merge_success_json,
require_seller_region,
)
def main() -> None:
params = load_cli_params()
if not params:
print(
"Usage: get_feed.py '<JSON>'\nRequired: sellerId, region, feedId",
file=sys.stderr,
)
sys.exit(1)
if not params.get("skipDepCheck"):
ensure_auth_skill_available("get_feed.py")
if "feedId" not in params:
print("Missing required field: feedId", file=sys.stderr)
sys.exit(1)
seller_id, region = require_seller_region(params)
path = f"{FEEDS_PATH_PREFIX}/feeds/{encode_path_segment(params['feedId'])}"
tokens = get_store_tokens(seller_id, region)
if "error" in tokens or "accessToken" not in tokens:
print(json.dumps(tokens, indent=2, ensure_ascii=False))
sys.exit(1)
proxy = developer_proxy_call(region, path, "GET", tokens["accessToken"])
out: dict = {"developerProxy": proxy, "resolvedPath": path}
merge_success_json(out, proxy, "feed")
print(json.dumps(out, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Amazon Store — getFeeds (SP-API Feeds v2021-06-30)
==================================================
GET `feeds/2021-06-30/feeds`。须传 feedTypes 或 nextToken(分页时仅 nextToken)。
官方参考: https://developer-docs.amazon.com/sp-api/reference/getfeeds
"""
from __future__ import annotations
import json
import sys
from urllib.parse import quote
from _spapi_feeds_common import (
FEEDS_PATH_PREFIX,
developer_proxy_call,
ensure_auth_skill_available,
get_store_tokens,
load_cli_params,
merge_success_json,
require_seller_region,
str_list,
)
def _build_query(params: dict) -> str:
parts: list[str] = []
nt = params.get("nextToken")
if nt:
return f"nextToken={quote(str(nt).strip(), safe='')}"
feed_types = str_list(params.get("feedTypes"), "feedTypes")
if not feed_types:
print("Required: feedTypes (array) or nextToken for pagination.", file=sys.stderr)
sys.exit(1)
if len(feed_types) > 10:
print("feedTypes length must be ≤ 10", file=sys.stderr)
sys.exit(1)
def add(k: str, v: str) -> None:
parts.append(f"{k}={quote(v, safe='')}")
for ft in feed_types:
add("feedTypes", ft)
for mid in str_list(params.get("marketplaceIds"), "marketplaceIds"):
add("marketplaceIds", mid)
for ps in str_list(params.get("processingStatuses"), "processingStatuses"):
add("processingStatuses", ps)
if params.get("createdSince"):
add("createdSince", str(params["createdSince"]).strip())
if params.get("createdUntil"):
add("createdUntil", str(params["createdUntil"]).strip())
if params.get("pageSize") is not None:
add("pageSize", str(int(params["pageSize"])))
return "&".join(parts)
def main() -> None:
params = load_cli_params()
if not params:
print(
"Usage: get_feeds.py '<JSON>'\n"
"Required: sellerId, region, and feedTypes OR nextToken\n"
"Optional: marketplaceIds, processingStatuses, createdSince, createdUntil, pageSize",
file=sys.stderr,
)
sys.exit(1)
if not params.get("skipDepCheck"):
ensure_auth_skill_available("get_feeds.py")
seller_id, region = require_seller_region(params)
path = f"{FEEDS_PATH_PREFIX}/feeds"
qs = _build_query(params)
tokens = get_store_tokens(seller_id, region)
if "error" in tokens or "accessToken" not in tokens:
print(json.dumps(tokens, indent=2, ensure_ascii=False))
sys.exit(1)
proxy = developer_proxy_call(
region, path, "GET", tokens["accessToken"], query_string=qs
)
out: dict = {"developerProxy": proxy, "resolvedPath": path, "queryString": qs}
merge_success_json(out, proxy, "feeds")
print(json.dumps(out, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Skill response I/O helper — wraps any main script to persist large API
responses to disk, then offers a `read` subcommand to extract specific fields
from those persisted files. Generic, business-agnostic.
This script is bundled into each skill's scripts/ directory by tools/response_io/sync.py.
The agent must pass --script <path> to identify which main script to execute.
Usage:
python scripts/response_io.py run --script <PATH> --out-dir <DIR> '<json_params>' [--label NAME] [--timeout SEC]
python scripts/response_io.py read <file> (--path "<JMESPath>" | --fields "f1,f2,...") [--limit N] [--offset M] [--format json|jsonl|csv|table]
"""
from __future__ import annotations
import sys
if sys.version_info < (3, 10):
sys.exit(
"Error: Python 3.10+ required (current: "
f"{sys.version_info.major}.{sys.version_info.minor}). "
"Please upgrade Python."
)
import argparse
import csv
import io
import json
import os
import re
import secrets
import subprocess
from datetime import datetime
from pathlib import Path
from typing import Any
# Force UTF-8 stdout/stderr so non-ASCII chars in previews and API responses
# print correctly on Windows (default cp936 / gbk).
for stream in (sys.stdout, sys.stderr):
try:
stream.reconfigure(encoding="utf-8") # type: ignore[attr-defined]
except (AttributeError, OSError):
pass
try:
import jmespath # type: ignore
HAS_JMESPATH = True
except ImportError:
HAS_JMESPATH = False
MAX_STRING_LEN = 120
MAX_DEPTH = 3
SAMPLE_KEY_CAP = 15
RAW_TEXT_PEEK = 500
DEFAULT_TIMEOUT_SEC = 300
# ---------------------------------------------------------------------------
# Shared helpers
# ---------------------------------------------------------------------------
def _err(msg: str, code: int = 1) -> None:
print(msg, file=sys.stderr)
sys.exit(code)
def _resolve_script(script_arg: str) -> Path:
p = Path(script_arg).expanduser()
if not p.is_absolute():
# Resolve relative to the current working directory the agent invoked from.
p = (Path.cwd() / p).resolve()
else:
p = p.resolve()
if not p.is_file():
_err(f"--script path not found: {p}")
return p
def _resolve_skill_name(main_script: Path) -> str:
"""Best-effort skill name extraction for filename prefixing.
main_script lives at <skill_dir>/scripts/<name>.py — return <skill_dir>'s
folder name. Fall back to the script's stem if structure differs.
"""
try:
if main_script.parent.name == "scripts":
return main_script.parents[1].name
except IndexError:
pass
return main_script.stem
def _sanitize_label(label: str) -> str:
"""Allow only safe filename chars in --label to prevent path traversal."""
cleaned = re.sub(r"[^\w\-]", "_", label)
return cleaned[:64] # cap length
def _truncate_string(s: str) -> str:
if len(s) <= MAX_STRING_LEN:
return s
return s[:MAX_STRING_LEN] + f"...(truncated, total {len(s)} chars)"
def _truncate_value(value: Any, depth: int = 0) -> Any:
"""Recursively truncate strings, deep nesting, and large arrays for preview."""
if depth >= MAX_DEPTH:
if isinstance(value, dict):
return f"<truncated nested object, keys: {list(value.keys())[:10]}>"
if isinstance(value, list):
return f"<truncated nested array, length: {len(value)}>"
if isinstance(value, str):
return _truncate_string(value)
return value
if isinstance(value, str):
return _truncate_string(value)
if isinstance(value, dict):
out = {k: _truncate_value(v, depth + 1) for k, v in value.items()}
return out
if isinstance(value, list):
if not value:
return []
truncated = [_truncate_value(value[0], depth + 1)]
if len(value) > 1:
# Note total length on the parent — keep the array type-homogeneous
# so downstream consumers can iterate without special-casing strings.
truncated.append({"_omitted_items": len(value) - 1})
return truncated
return value
def _shape_of(value: Any, top: bool = False) -> Any:
"""Lightweight schema description for the preview block."""
if isinstance(value, dict):
keys = list(value.keys())
out: dict[str, Any] = {"type": "object", "top_keys" if top else "keys": keys}
if top:
for k in keys[:8]:
out[k] = _shape_of(value[k])
return out
if isinstance(value, list):
out = {"type": "array", "length": len(value)}
if value and isinstance(value[0], dict):
out["item_keys"] = list(value[0].keys())
elif value:
out["item_type"] = type(value[0]).__name__
return out
return {"type": type(value).__name__}
def _build_sample(value: Any) -> Any:
"""First-record sample with explicit truncation marker."""
if isinstance(value, list):
if not value:
return {"_truncated_record": True, "_note": "array is empty"}
first = value[0]
if isinstance(first, dict):
sample = {"_truncated_record": True, "_note": f"first of {len(value)} items"}
sample.update(_truncate_value(first, depth=1))
return sample
return {"_truncated_record": True, "_note": f"first of {len(value)} items", "value": _truncate_value(first, depth=1)}
if isinstance(value, dict):
sample = {"_truncated_record": True, "_note": "top-level object (truncated)"}
sample.update(_truncate_value(value, depth=1))
return sample
return {"_truncated_record": True, "value": _truncate_value(value, depth=1)}
def _shrink_preview(preview: dict) -> dict:
"""Cap the sample's value fields when it has many keys.
`shape.*.item_keys` is the single source of truth for the full key list
(always complete, no truncation). The sample only ever shows up to
SAMPLE_KEY_CAP fields with their concrete values, since the agent only
needs a feel for value shapes — for the full menu of available fields,
they read `shape`.
"""
sample = preview.get("sample")
if isinstance(sample, dict):
meta_keys = {"_truncated_record", "_note"}
data_keys = [k for k in sample.keys() if k not in meta_keys]
if len(data_keys) > SAMPLE_KEY_CAP:
kept = data_keys[:SAMPLE_KEY_CAP]
new_sample = {k: v for k, v in sample.items() if k in meta_keys or k in kept}
base_note = sample.get("_note", "")
extra = (
f"showing first {SAMPLE_KEY_CAP} of {len(data_keys)} fields "
f"(see `shape` for the complete key list)"
)
new_sample["_note"] = f"{base_note}; {extra}" if base_note else extra
preview["sample"] = new_sample
return preview
# ---------------------------------------------------------------------------
# `run` subcommand
# ---------------------------------------------------------------------------
def cmd_run(args: argparse.Namespace) -> int:
main_script = _resolve_script(args.script)
skill_name = _resolve_skill_name(main_script)
out_dir = Path(args.out_dir).expanduser().resolve()
try:
out_dir.mkdir(parents=True, exist_ok=True)
except OSError as e:
_err(f"Failed to create --out-dir {out_dir}: {e}")
if not os.access(out_dir, os.W_OK):
_err(f"--out-dir is not writable: {out_dir}")
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
rand = secrets.token_hex(3)
safe_label = _sanitize_label(args.label) if args.label else ""
label_part = f"__{safe_label}" if safe_label else ""
out_file = out_dir / f"{skill_name}__{timestamp}_{rand}{label_part}.json"
# Force the child process to emit UTF-8 regardless of the host console
# encoding (Windows defaults to cp936 / gbk and would otherwise corrupt
# non-ASCII bytes when we read them back).
child_env = os.environ.copy()
child_env["PYTHONIOENCODING"] = "utf-8"
timed_out = False
try:
proc = subprocess.run(
[sys.executable, str(main_script), args.params],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
env=child_env,
timeout=args.timeout,
)
stdout_text = proc.stdout or ""
stderr_text = proc.stderr or ""
returncode = proc.returncode
except subprocess.TimeoutExpired as e:
timed_out = True
stdout_text = (e.stdout.decode("utf-8", errors="replace") if isinstance(e.stdout, bytes) else (e.stdout or "")) or ""
stderr_text = (e.stderr.decode("utf-8", errors="replace") if isinstance(e.stderr, bytes) else (e.stderr or "")) or ""
returncode = 124 # convention for timeout
# Always write the captured stdout to disk, even if not JSON.
try:
out_file.write_text(stdout_text, encoding="utf-8")
except OSError as e:
_err(f"Failed to write output file {out_file}: {e}")
if stderr_text:
sys.stderr.write(stderr_text)
# Try to parse the captured stdout as JSON for the preview.
try:
parsed = json.loads(stdout_text) if stdout_text.strip() else None
format_kind = "json"
except json.JSONDecodeError:
parsed = None
format_kind = "raw_text"
preview: dict[str, Any] = {
"_preview": {
"is_preview": True,
"warning": (
"PREVIEW ONLY — NOT FULL DATA. The full response is saved to `file`. "
"Use `python scripts/response_io.py read <file> --fields '...'` to extract "
"specific fields, or `--path '<JMESPath>'` for complex projections."
),
},
}
# Surface failures prominently so agents don't mistake a stub preview for success.
if returncode != 0 or timed_out:
stderr_snippet = stderr_text[-500:] if stderr_text else ""
preview["_error"] = {
"exit_code": returncode,
"timed_out": timed_out,
"stderr_snippet": stderr_snippet,
"hint": "The wrapped script failed or timed out. The output file may be empty or partial.",
}
preview.update({
"file": str(out_file),
"size_bytes": out_file.stat().st_size,
"skill": skill_name,
"exit_code": returncode,
"format": format_kind,
"label": safe_label or None,
"next_steps_hint": (
"use: python scripts/response_io.py read <file> --fields '...' | --path '...'"
),
})
if format_kind == "json":
preview["shape"] = _shape_of(parsed, top=True)
preview["sample"] = _build_sample(parsed)
else:
peek = stdout_text[:RAW_TEXT_PEEK]
preview["raw_text_peek"] = peek
preview["raw_text_total_chars"] = len(stdout_text)
preview["sample"] = {
"_truncated_record": True,
"_note": f"stdout was not valid JSON; first {RAW_TEXT_PEEK} chars shown above in raw_text_peek",
}
preview = _shrink_preview(preview)
print(json.dumps(preview, ensure_ascii=False, indent=2))
return returncode
# ---------------------------------------------------------------------------
# `read` subcommand
# ---------------------------------------------------------------------------
def _load_json(path: Path) -> Any:
try:
text = path.read_text(encoding="utf-8")
except OSError as e:
_err(f"Failed to read file {path}: {e}")
try:
return json.loads(text)
except json.JSONDecodeError as e:
_err(f"File is not valid JSON: {path}\n{e}")
def _basic_dot_path(data: Any, path: str) -> Any:
"""Pure-stdlib dot-path resolver. No [*] support — callers fall back here only when jmespath is unavailable AND the path has no [*]."""
cur = data
for part in path.split("."):
if isinstance(cur, dict):
cur = cur.get(part)
else:
return None
return cur
def _resolve_field(data: Any, expr: str) -> Any:
if HAS_JMESPATH:
return jmespath.search(expr, data)
if "[" in expr or "*" in expr:
_err(
f"jmespath is required for expression '{expr}'. "
f"Install with: pip install jmespath"
)
return _basic_dot_path(data, expr)
def _project_fields(data: Any, fields: list[str]) -> Any:
"""Run each field expr; if any returns a list, zip them into list-of-dicts."""
resolved: dict[str, Any] = {f: _resolve_field(data, f) for f in fields}
list_lengths = [len(v) for v in resolved.values() if isinstance(v, list)]
if not list_lengths:
return resolved
# All list values must be same length to zip cleanly.
if len(set(list_lengths)) > 1:
# Fallback: return the dict as-is so caller can inspect mismatches.
return resolved
n = list_lengths[0]
rows = []
for i in range(n):
row = {}
for f, v in resolved.items():
row[f] = v[i] if isinstance(v, list) else v
rows.append(row)
return rows
def _apply_slice(value: Any, limit: int | None, offset: int | None) -> Any:
if not isinstance(value, list):
return value
start = offset or 0
end = (start + limit) if limit is not None else None
return value[start:end]
def _format_output(value: Any, fmt: str) -> str:
if fmt == "json":
return json.dumps(value, ensure_ascii=False, indent=2)
if fmt == "jsonl":
if isinstance(value, list):
return "\n".join(json.dumps(item, ensure_ascii=False) for item in value)
return json.dumps(value, ensure_ascii=False)
if fmt in ("csv", "table"):
if not isinstance(value, list) or not value:
_err(f"--format {fmt} requires a non-empty list result")
if not all(isinstance(item, dict) for item in value):
_err(f"--format {fmt} requires list-of-objects, got list of {type(value[0]).__name__}")
keys: list[str] = []
for item in value:
for k in item.keys():
if k not in keys:
keys.append(k)
if fmt == "csv":
buf = io.StringIO()
writer = csv.DictWriter(buf, fieldnames=keys, extrasaction="ignore")
writer.writeheader()
for item in value:
writer.writerow({k: _stringify(item.get(k)) for k in keys})
return buf.getvalue().rstrip("\n")
# table: simple aligned columns
rows = [[_stringify(item.get(k)) for k in keys] for item in value]
widths = [len(k) for k in keys]
for row in rows:
for i, cell in enumerate(row):
widths[i] = max(widths[i], len(cell))
lines = [
" ".join(k.ljust(widths[i]) for i, k in enumerate(keys)),
" ".join("-" * widths[i] for i in range(len(keys))),
]
for row in rows:
lines.append(" ".join(row[i].ljust(widths[i]) for i in range(len(keys))))
return "\n".join(lines)
_err(f"Unknown --format: {fmt}")
return "" # unreachable
def _stringify(v: Any) -> str:
if v is None:
return ""
if isinstance(v, (dict, list)):
return json.dumps(v, ensure_ascii=False)
return str(v)
def cmd_read(args: argparse.Namespace) -> int:
if not args.path and not args.fields:
_err("read: either --path or --fields is required")
if args.path and args.fields:
_err("read: --path and --fields are mutually exclusive")
file_path = Path(args.file).expanduser().resolve()
data = _load_json(file_path)
if args.path:
result = _resolve_field(data, args.path)
else:
fields = [f.strip() for f in args.fields.split(",") if f.strip()]
if not fields:
_err("--fields parsed to empty list")
result = _project_fields(data, fields)
result = _apply_slice(result, args.limit, args.offset)
print(_format_output(result, args.format))
return 0
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main() -> int:
parser = argparse.ArgumentParser(
prog="response_io.py",
description="Persist large skill API responses to disk and read fields on demand.",
)
sub = parser.add_subparsers(dest="cmd", required=True)
p_run = sub.add_parser(
"run",
help="Execute a main script and persist its stdout to a file; "
"print only a lightweight preview to stdout.",
)
p_run.add_argument("params", help="JSON params string passed verbatim to the main script (argv[1]).")
p_run.add_argument("--script", required=True, help="Path to the main script to execute, e.g. scripts/my_api.py")
p_run.add_argument("--out-dir", required=True, help="Directory to write the response file into (created if missing).")
p_run.add_argument("--label", default=None, help="Optional filename suffix; sanitized to safe filename characters.")
p_run.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT_SEC, help=f"Subprocess timeout in seconds (default: {DEFAULT_TIMEOUT_SEC}).")
p_run.set_defaults(func=cmd_run)
p_read = sub.add_parser(
"read",
help="Extract specific fields from a previously persisted response file.",
)
p_read.add_argument("file", help="Path to the persisted JSON response file.")
g = p_read.add_mutually_exclusive_group()
g.add_argument("--path", default=None, help="JMESPath expression, e.g. 'data[*].{asin: asin, title: title}'.")
g.add_argument("--fields", default=None, help="Comma-separated field paths, e.g. 'data[*].asin,data[*].title'.")
p_read.add_argument("--limit", type=int, default=None, help="Take at most N items (when result is a list).")
p_read.add_argument("--offset", type=int, default=None, help="Skip the first M items (when result is a list).")
p_read.add_argument("--format", choices=["json", "jsonl", "csv", "table"], default="json", help="Output format (default: json).")
p_read.set_defaults(func=cmd_read)
args = parser.parse_args()
return args.func(args)
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""
Amazon Store — Upload feed document contents (post createFeedDocument)
======================================================================
createFeedDocument 返回的 **url** 需用 **PUT** 上传 feed 文件内容(不经 developerProxy)。
本脚本使用标准库直接请求 Amazon 预签名 URL。
Usage:
python upload_feed_document.py '{
"uploadUrl": "https://...",
"contentType": "text/tab-separated-values; charset=UTF-8",
"filePath": "/path/to/feed.tsv"
}'
或:
"content": "<raw string>" / "contentBase64": "<base64>"
"""
from __future__ import annotations
import base64
import json
import sys
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
def _load_bytes(params: dict) -> bytes:
if params.get("filePath"):
p = Path(str(params["filePath"]).expanduser())
if not p.is_file():
print(f"filePath not found: {p}", file=sys.stderr)
sys.exit(1)
return p.read_bytes()
if params.get("contentBase64"):
try:
return base64.b64decode(str(params["contentBase64"]))
except Exception as e:
print(f"Invalid contentBase64: {e}", file=sys.stderr)
sys.exit(1)
if "content" in params:
raw = params["content"]
if isinstance(raw, str):
return raw.encode("utf-8")
print("content must be a string", file=sys.stderr)
sys.exit(1)
print("Provide one of: filePath, content, contentBase64", file=sys.stderr)
sys.exit(1)
def main() -> None:
if len(sys.argv) < 2:
print(
"Usage: upload_feed_document.py '<JSON>'\n"
"Required: uploadUrl, contentType, and filePath | content | contentBase64",
file=sys.stderr,
)
sys.exit(1)
try:
params = json.loads(sys.argv[1])
except json.JSONDecodeError as e:
print(f"Invalid JSON: {e}", file=sys.stderr)
sys.exit(1)
for f in ("uploadUrl", "contentType"):
if f not in params:
print(f"Missing required field: {f}", file=sys.stderr)
sys.exit(1)
upload_url = str(params["uploadUrl"]).strip()
content_type = str(params["contentType"]).strip()
payload = _load_bytes(params)
req = Request(
upload_url,
data=payload,
headers={"Content-Type": content_type},
method="PUT",
)
out: dict = {"uploadUrl": upload_url, "bytesUploaded": len(payload)}
try:
with urlopen(req, timeout=300) as resp:
out["httpStatus"] = resp.status
out["success"] = 200 <= resp.status < 300
except HTTPError as e:
body = e.read().decode("utf-8", errors="replace") if e.fp else ""
out["httpStatus"] = e.code
out["success"] = False
out["error"] = f"HTTP {e.code}: {e.reason}"
out["details"] = body[:2000]
except URLError as e:
out["success"] = False
out["error"] = f"Connection failed: {e.reason}"
print(json.dumps(out, indent=2, ensure_ascii=False))
if not out.get("success"):
sys.exit(1)
if __name__ == "__main__":
main()