Review

Push cover art to press partner prepress API

Vellum's print-fulfillment worker now posts author cover PDFs to BinderyCo's prepress ingest. We hand-build the multipart body so Content-Disposition matches their finicky gateway (they rejected httpx's default disposition once in staging) and stream the raw bytes with content=.

httpxTier 5securitymultiparthttpx

Click a line to flag it, pick one or more labels, then submit. If the change looks correct, approve it.

workers/print/binderyco_client.py+37-10
2222import httpx
2323
2424from app.config import settings
2525
26-async def submit_cover(job_id: str, filename: str, pdf: bytes) -> dict:
27- async with httpx.AsyncClient(timeout=60.0) as client:
28- r = await client.post(
29- f"{settings.BINDERYCO_BASE}/v2/prepress",
30- headers={"Authorization": f"Bearer {settings.BINDERYCO_TOKEN}"},
31- data={"job_id": job_id, "sku": "cover-soft"},
32- files={"file": (filename, pdf, "application/pdf")},
33- )
34- r.raise_for_status()
35- return r.json()
26+BOUNDARY = "----VellumBinderyBoundary9f3a2c"
27+
28+def _build_multipart(job_id: str, filename: str, pdf: bytes) -> bytes:
29+ # BinderyCo gateway is picky about disposition order; build body by hand.
30+ parts = []
31+ parts.append(
32+ f"--{BOUNDARY}\r\n"
33+ f'Content-Disposition: form-data; name="job_id"\r\n\r\n'
34+ f"{job_id}\r\n"
35+ )
36+ parts.append(
37+ f"--{BOUNDARY}\r\n"
38+ f'Content-Disposition: form-data; name="sku"\r\n\r\n'
39+ f"cover-soft\r\n"
40+ )
41+ parts.append(
42+ f"--{BOUNDARY}\r\n"
43+ f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n'
44+ f"Content-Type: application/pdf\r\n\r\n"
45+ )
46+ head = "".join(parts).encode("utf-8")
47+ tail = f"\r\n--{BOUNDARY}--\r\n".encode("utf-8")
48+ return head + pdf + tail
49+
50+async def submit_cover(job_id: str, filename: str, pdf: bytes) -> dict:
51+ body = _build_multipart(job_id, filename, pdf)
52+ async with httpx.AsyncClient(timeout=60.0) as client:
53+ r = await client.post(
54+ f"{settings.BINDERYCO_BASE}/v2/prepress",
55+ headers={
56+ "Authorization": f"Bearer {settings.BINDERYCO_TOKEN}",
57+ "Content-Type": f"multipart/form-data; boundary={BOUNDARY}",
58+ },
59+ content=body,
60+ )
61+ r.raise_for_status()
62+ return r.json()