Files
delivery-desk/tests/api_create_work.py

164 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""通过 Delivery Desk API 创建作品或提交新的验收轮次。"""
from __future__ import annotations
import argparse
import getpass
import json
import os
import sys
from datetime import datetime
from http.cookies import SimpleCookie
from http.cookiejar import CookieJar
from urllib.error import HTTPError, URLError
from urllib.request import HTTPCookieProcessor, Request, build_opener
class ApiError(RuntimeError):
pass
def request_json(opener, url: str, *, method: str = "GET", data: bytes | None = None, headers: dict[str, str] | None = None):
request = Request(url, data=data, method=method, headers=headers or {})
try:
with opener.open(request, timeout=15) as response:
body = response.read().decode("utf-8")
return response.status, json.loads(body) if body else None, response.headers
except HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
try:
detail = json.loads(body).get("error", body)
except json.JSONDecodeError:
detail = body
raise ApiError(f"{method} {url} 返回 {error.code}: {detail}") from error
except URLError as error:
raise ApiError(f"无法连接 {url}: {error.reason}") from error
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="通过 API 创建测试作品或提交新的验收轮次")
parser.add_argument("--base-url", default=os.getenv("DELIVERY_DESK_BASE_URL", "http://127.0.0.1:3010"))
parser.add_argument("--project-id", type=int, default=None, help="不传时自动选择唯一可访问的项目")
parser.add_argument("--work-id", type=int, default=None, help="传入后为该作品提交新的验收轮次")
parser.add_argument("--external-id", default=None, help="调用方作品唯一标识,用于幂等创建和找回作品")
parser.add_argument("--api-key", default=os.getenv("DELIVERY_DESK_API_KEY"))
parser.add_argument("--username", default=os.getenv("DELIVERY_DESK_USERNAME", "operator"))
parser.add_argument("--password", default=os.getenv("DELIVERY_DESK_PASSWORD"))
parser.add_argument("--title", default=None)
parser.add_argument("--description", default="由 tests/api_create_work.py 通过 API 创建。")
parser.add_argument("--tags", default="API 测试")
parser.add_argument(
"--image-url",
action="append",
dest="image_urls",
default=None,
help="公开可读的图片 URL可重复传入默认使用一张公共占位图",
)
return parser.parse_args()
def choose_item(items: list[dict], requested_id: int | None, label: str) -> dict:
if requested_id is not None:
item = next((candidate for candidate in items if int(candidate["id"]) == requested_id), None)
if item is None:
raise ApiError(f"无权访问或不存在的{label} ID: {requested_id}")
return item
if len(items) == 1:
return items[0]
choices = ", ".join(f'{item["id"]}:{item["name"]}' for item in items) or ""
raise ApiError(f"可访问的{label}不是唯一项,请显式传入对应 ID。当前可选{choices}")
def main() -> int:
args = parse_args()
base_url = args.base_url.rstrip("/")
opener = build_opener(HTTPCookieProcessor(CookieJar()))
auth_headers: dict[str, str] = {}
if args.api_key:
auth_headers["Authorization"] = f"Bearer {args.api_key}"
else:
password = args.password or getpass.getpass(f"请输入账号 {args.username} 的密码: ")
login_data = json.dumps({"username": args.username, "password": password}).encode("utf-8")
_, _, login_headers = request_json(
opener,
f"{base_url}/api/auth/login",
method="POST",
data=login_data,
headers={"Content-Type": "application/json"},
)
cookies = SimpleCookie()
cookies.load(login_headers.get("Set-Cookie", ""))
session = cookies.get("proofing_session")
if session is None:
raise ApiError("登录成功,但接口没有返回会话 Cookie")
auth_headers["Cookie"] = f"proofing_session={session.value}"
_, projects, _ = request_json(opener, f"{base_url}/api/projects", headers=auth_headers)
project = choose_item(projects, args.project_id, "项目")
project_id = int(project["id"])
title = args.title or f"API 测试作品 {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
image_urls = args.image_urls or ["https://placehold.co/1200x800/png?text=Delivery+Desk+API+Test"]
upload_headers = {**auth_headers, "Content-Type": "application/json"}
payload = {
"title": title,
"description": args.description,
"tags": [args.tags] if args.tags else [],
"images": image_urls,
}
if args.work_id is not None:
_, current, _ = request_json(opener, f"{base_url}/api/works/{args.work_id}", headers=auth_headers)
if int(current["project"]["id"]) != project_id:
raise ApiError(f"作品 {args.work_id} 不属于选定的项目")
status, work, _ = request_json(
opener,
f"{base_url}/api/works/{args.work_id}/rounds",
method="POST",
data=json.dumps(payload).encode("utf-8"),
headers=upload_headers,
)
if status != 201 or not work or int(work.get("id", 0)) != args.work_id:
raise ApiError("新验收轮次接口没有返回目标作品")
action = "round_created"
external_id = work.get("external_id")
else:
external_id = args.external_id or f"api-smoke-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
create_payload = {**payload, "externalId": external_id}
body = json.dumps(create_payload).encode("utf-8")
works_url = f"{base_url}/api/projects/{project_id}/works"
status, work, _ = request_json(opener, works_url, method="POST", data=body, headers=upload_headers)
if status not in (200, 201) or not work or int(work.get("project_id", 0)) != project_id:
raise ApiError("接口未返回属于目标项目的作品")
_, repeated, _ = request_json(opener, works_url, method="POST", data=body, headers=upload_headers)
if int(repeated.get("id", 0)) != int(work["id"]) or not repeated.get("idempotent"):
raise ApiError("相同 externalId 的重复请求未通过幂等校验")
action = "work_created" if status == 201 else "existing_work_returned"
print(
json.dumps(
{
"success": True,
"action": action,
"group": project.get("group_name"),
"project": project.get("name"),
"work_id": work.get("id"),
"external_id": external_id,
"version_number": work.get("version_number"),
"title": work.get("title"),
},
ensure_ascii=False,
indent=2,
)
)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except (ApiError, ValueError) as error:
print(f"测试失败: {error}", file=sys.stderr)
raise SystemExit(1)