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
| 22 | 22 | import httpx | |
| 23 | 23 | ||
| 24 | 24 | from app.config import settings | |
| 25 | 25 | ||
| 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() |