feat: upload_file + prepare_inputs + build_inputs route (parity with @pipelex/sdk)#10
Conversation
Phase 0 of the hosted input-upload & SDK-preparation project: promote the approved contract into this repo's own docs before the code lands. New docs/input-preparation.md is the Python-flavored mirror of the JS SDK contract — upload_file/prepare_inputs, str/Path/bytes sources, signature-driven asset identification, upload-record guarantees, error/capability outcomes, inherited storage policy, and stability across the future endpoint move. Notes the build_inputs parity gap prepare_inputs closes. Pointer added from architecture.md's storage bullet. Docs-only; no code touched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y9RPphjd62DywHtok2fX1q
…@pipelex/sdk)
Add the hosted upload-capability surface, the Python counterpart of @pipelex/sdk's
uploadFile/prepareInputs, plus the build_inputs route the signature step needs:
- client.upload_file(source, *, filename=None, content_type=None) -> UploadRecord —
accepts str/Path paths and raw bytes; record (uri, content_type, size, filename)
assembled client-side (mimetypes for MIME).
- client.prepare_inputs(*, files, pipe_ref=None, inputs) -> PreparedInputs — resolves
the pipe's declared signature via build_inputs(explicit=True), template-guided walk
(file signal = a canonical {url} content dict, mirroring the runtime's
input_normalizer), uploads file-bearing values, returns a copy-on-write rewrite
carrying pipelex-storage:// in `url` plus one UploadRecord per prepared asset.
http(s)/pipelex-storage:// pass through; data URLs and local/byte sources upload;
dedup by source identity; all failures before any run. An unrecognized value at a
file position fails as a typed InputPreparationError.
- client.build_inputs(BuildInputsRequest) -> BuildInputsResponse — closes the
/v1/build/* parity gap the Python SDK had. The response is an is_valid discriminated
union parsed through BuildInputsResponseAdapter (a TypeAdapter), mirroring the repo's
PipelexValidationResultAdapter precedent, so a malformed 200 raises a clean
ValidationError rather than being read as a valid verdict.
- Typed exception family mirrors the JS SDK: InputPreparationError base +
InvalidLocalSource / RejectedAsset / UnsupportedUploadCapability /
UploadAuthentication / UploadTransport.
Scope: prepare_inputs takes the closure as inline files; catalog method_id and opt-in
http(s) ingest deferred (additive). Matrix-derived parity tests + real-client wiring
tests. make agent-check + make agent-test green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y9RPphjd62DywHtok2fX1q
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fe766d63a2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Greptile SummaryThis PR adds hosted file preparation to the Python SDK. The main changes are:
Confidence Score: 4/5Malformed data URLs can bypass the typed preparation error contract, and large uploads can stall the event loop. Incorrect base64 padding exposes a raw decoder exception. File reads and base64 encoding run synchronously in the async upload path. Route parsing, input rewriting, and HTTP error mapping otherwise have focused coverage. pipelex_sdk/prepare_inputs.py and pipelex_sdk/upload.py
What T-Rex did
Important Files Changed
Sequence DiagramsequenceDiagram
participant Caller
participant Client as PipelexAPIClient
participant Build as /v1/build/inputs
participant Prep as prepare_inputs
participant Upload as /v1/upload
Caller->>Client: prepare_inputs(files, pipe_ref, inputs)
Client->>Build: "BuildInputsRequest(explicit=true)"
Build-->>Client: Explicit input template
Client->>Prep: Walk template and input values
loop Each local file value
Prep->>Upload: Base64 UploadInput
Upload-->>Prep: Storage URI
end
Prep-->>Caller: Rewritten inputs and upload records
Prompt To Fix All With AIFix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
pipelex_sdk/prepare_inputs.py:82
**Malformed Base64 Escapes Typed Errors**
When a file input contains an incorrectly padded data URL such as `data:image/png;base64,AQI`, `base64.b64decode()` raises `binascii.Error`. That exception bypasses the promised `InputPreparationError` family, so callers cannot handle all preparation failures through the documented base exception.
### Issue 2 of 2
pipelex_sdk/upload.py:121-122
**Large Uploads Block Event Loop**
A path upload performs the full synchronous file read, then base64-encodes the entire payload before reaching the first `await`. Uploading a file near the documented 50 MiB hosted limit, especially from slow storage, can stall every other coroutine on the same event loop.
Reviews (1): Last reviewed commit: "feat: upload_file + prepare_inputs + bui..." | Re-trigger Greptile |
There was a problem hiding this comment.
All reported issues were addressed across 12 files
You’re at about 98% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
…feedback) Addresses the bot-review findings on PR #10 that are genuine correctness or doc-accuracy issues; deliberately defers the rest. Fixed: - Malformed base64 data URL no longer escapes the typed-error contract. Decode with `validate=True` (junk chars are rejected, not silently discarded into corrupted bytes) and map `binascii.Error` to `InputPreparationError`, so all preparation failures stay within the documented exception family. (Codex, Greptile, cubic all flagged this.) - Percent-encoded binary data URLs keep their exact bytes: decode with `urllib.parse.unquote_to_bytes` instead of `unquote(...).encode("utf-8")`, which corrupted any byte >= 0x80. - docs/input-preparation.md: the URL / storage-URI pass-through is a `prepare_inputs`-level behavior, not a feature of `upload_file` (which treats every string as a filesystem path); removed the `checksum` row that listed a field `UploadRecord` does not have. - docs/architecture.md: upload_file/prepare_inputs are now implemented, not a "planned addition". Deferred (not bugs): offloading the file read/base64 off the event loop (perf nicety on an inherently I/O-bound op), a template-required-per-format validator on BuildInputsValidReport (prepare_inputs already raises a clean typed error), and splitting a P3 two-scenario transport-error test. Regression tests: malformed base64 (bad padding + non-alphabet) raises a typed error and uploads nothing; percent-encoded binary round-trips its exact bytes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019cpRcAL7MEQcsx2hHvGxC2
|
Triaged the automated review feedback (Codex, Greptile, cubic). Pushed fixes for the genuine issues; deferring the rest with rationale. Fixed (commit on this branch):
Added regression tests for both data-URL fixes. Deferred (not bugs — in the spirit of "fix what needs fixing, defer on doubt"):
|
… shape Second pass on PR #10 review-bot feedback — the two remaining substantive items, both promoted from "defer" to "fix now" after independent verification showed each closes a documented JS-parity gap in new code at near-zero cost. - upload_file: offload the synchronous whole-file read via asyncio.to_thread so a large read no longer blocks the event loop before the first await. Matches the JS SDK (which reads off-loop via node:fs/promises) and the repo's async-only invariant. base64 stays inline on purpose — CPython's binascii holds the GIL, so threading it would not free the loop (JS also encodes inline); `_to_asset_bytes`/`_read_path` stay sync. (Greptile P2 + cubic P2.) - BuildInputsValidReport: add a model_validator enforcing the template-present- per-format invariant (inputs for json, inputs_toml for toml, not the other). This makes the adapter's documented malformed-200 guarantee true for the template shape too — an `is_valid: true` body missing its template now raises a clean ValidationError at parse time instead of surfacing one layer down in prepare_inputs with a misleading "got json" message. Mirrors the JS sibling's per-format-required modeling. (cubic P2.) - Tests: assert the read is offloaded (to_thread awaited with _to_asset_bytes, real behavior preserved via wraps); assert the validator rejects the three malformed template shapes and accepts a well-formed toml report. Parametrized the two-scenario transport-error test for per-case attribution (cubic P3). make agent-check + make agent-test green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019cpRcAL7MEQcsx2hHvGxC2
Summary
Adds the hosted upload-capability surface — the Python counterpart of
@pipelex/sdk'suploadFile/prepareInputs— plus thebuild_inputsroute the signature step needs.client.upload_file(source, *, filename=None, content_type=None) -> UploadRecord— acceptsstr/Pathpaths and rawbytes; record (uri,content_type,size,filename) assembled client-side (mimetypesfor MIME).client.prepare_inputs(*, files, pipe_ref=None, inputs) -> PreparedInputs— resolves the pipe's declared signature viabuild_inputs(explicit=True), template-guided walk (file signal = a canonical{url}content dict, mirroring the runtime'sinput_normalizer), uploads file-bearing values, and returns a copy-on-write rewrite carryingpipelex-storage://inurlplus oneUploadRecordper prepared asset.http(s)/pipelex-storage://pass through; data URLs and local/byte sources upload; dedup by source identity; all failures surface before any run. An unrecognized value at a file position fails as a typedInputPreparationError.client.build_inputs(BuildInputsRequest) -> BuildInputsResponse— closes the/v1/build/*parity gap. The response is anis_valid-discriminated union parsed throughBuildInputsResponseAdapter(aTypeAdapter), mirroring the repo'sPipelexValidationResultAdapterprecedent, so a malformed200raises a cleanValidationErrorrather than being read as a valid verdict.InputPreparationErrorbase +InvalidLocalSource/RejectedAsset/UnsupportedUploadCapability/UploadAuthentication/UploadTransport.Scope
prepare_inputstakes the closure as inlinefiles; catalogmethod_idand opt-inhttp(s)ingest are deferred (additive).Testing
Matrix-derived parity tests + real-client wiring tests.
make agent-check+make agent-testgreen.🤖 Generated with Claude Code
Summary by cubic
Adds hosted input preparation to the Python SDK with
client.upload_file,client.prepare_inputs, andclient.build_inputs, matching@pipelex/sdk. Also hardensdata:URL handling and offloads local file reads inupload_fileto avoid blocking the event loop.New Features
client.upload_file(source, filename?, content_type?)— acceptsstr/Path/bytes; returnsUploadRecordwithuri,filename,content_type,size.client.prepare_inputs(files, pipe_ref?, inputs)— resolves the signature viabuild_inputs(explicit=True), walks the template, uploads file-bearing values, and rewrites to canonical content withurlset topipelex-storage://. HTTP(S) and existing storage URIs pass through;data:URLs and local/byte sources upload; dedup by source; copy-on-write; all failures happen before any run.client.build_inputs(request)— adds/v1/build/inputsparity; returns anis_valid-discriminated result; malformed 200s raise aValidationError.InvalidLocalSourceError,RejectedAssetError,UnsupportedUploadCapabilityError,UploadAuthenticationError,UploadTransportError(all extendInputPreparationError).Bug Fixes
data:URL decoding: base64 payloads are validated (validate=True); malformed values raiseInputPreparationErrorand never upload corrupted bytes.data:URLs preserve exact bytes viaunquote_to_bytes; docs clarified that URL/pass-through classification happens inprepare_inputs(strings toupload_fileare treated as paths).upload_filereads local files viaasyncio.to_threadso large reads do not block the event loop.build_inputsvalid reports now enforce that the template field matchesformat(inputsforjson,inputs_tomlfortoml); missing/mismatched templates are treated as malformed 200s and raiseValidationError.Written for commit e292fc3. Summary will update on new commits.