UN-4010 [FEAT] Support every API deployment request parameter via a generated transport - #27
UN-4010 [FEAT] Support every API deployment request parameter via a generated transport#27chandrasekharan-zipstack wants to merge 31 commits into
Conversation
The HTTP layer is now generated from the committed OpenAPI spec rather than hand-written, so URLs, query names and multipart encoding follow the spec instead of being restated here. tools/gen_sdk.sh regenerates it with a pinned generator; the tree is committed but never hand-edited. The public surface is unchanged on purpose: same constructor, same return dicts, same exceptions. What was deliberately kept rather than rewritten: - The retry policy, verbatim. Attempt counts, Retry-After on 429, exponential jitter, file rewinding, and the sync/async POST distinction are the contract, and nothing about the transport should restate them. - Transport failures are translated to their `requests` equivalents inside the retried call, not around it, so the retry policy still sees the exception types it is configured to retry. `requests` stays a dependency for those classes because callers catch them by name. - Response fields are read from the JSON body, never from a generated response model: a model exists only for the statuses the spec declares, and error bodies are typed too loosely to read. Only the parameters this client sets are sent. The generated builders write every declared default into a request, and sending a default is not the same as omitting it — it pins a value the server would otherwise choose, and the two diverge as soon as the server's default changes. No transport timeout is configured, as before: api_timeout selects a backend execution mode and is not a socket timeout.
The transport changed; the published behaviour must not. These tests compare against the 1.5.3 client vendored under tests/baseline: constructor and method signatures via AST, the request that goes out, the exceptions that come back, and the exact dict each method returns — the last by running both clients over the same responses. Also stop sending the generated fixed multipart boundary. An uploaded file containing those bytes would corrupt the encoding, so the header is dropped and the transport picks a random boundary, as the previous client did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ
requests.ConnectTimeout is both a ConnectionError and a Timeout. Mapping httpx.ConnectTimeout to a plain Timeout — which is all httpx's own hierarchy implies — stops every caller that catches the connection family from catching a connect timeout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ
httpx.ReadTimeout was landing in the TimeoutException catch-all and coming back out as requests.Timeout. Callers that catch requests.ReadTimeout by name stopped matching. The translation table test used pytest.raises, which is subclass-tolerant and passed either way; it now asserts the exact class. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ
|
Backported a fix found by the sibling client's live run: The live round trip for this client is still outstanding — it needs staging credentials. |
The spec is now produced and committed by the backend that serves these endpoints, so this repo tracks that file instead of a copy maintained elsewhere. Regenerating picks up its root `tags` array; the generated tree is otherwise unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ
…_file The deployment accepts twelve request parameters; the client could only send two, and only by way of the constructor. The rest had no argument to travel through, so callers that need a tag, an LLM profile or a HITL queue cannot reach them at all. They are added as keyword-only arguments named exactly as the API names them. Every one defaults to unset and an unset parameter is not sent, so the server still picks its own default and the request is byte-for-byte unchanged for every existing call shape. `timeout` and `include_metadata` fall back to the constructor values when not passed, and a `timeout` passed per request selects the execution mode for that request.
The status endpoint takes include_metadata, include_metrics and include_extracted_text; the client could send only the first, and only via the constructor, so a caller wanting metrics on one poll had nowhere to ask. They are added as keyword-only arguments named exactly as the API names them, each defaulting to unset. An unset parameter is not sent, so the query string is unchanged for every existing call shape and the server still picks its own default. execution_id stays out: it is read from the endpoint URL the server handed back.
The backend's spec now declares the deployment key as a bearer scheme on each operation, describes the error statuses a caller has to branch on, and no longer publishes the MCP endpoints or a request field the deployment does not accept. With no operation left outside the facade, the coverage check compares the declared set whole. Excusing an operation by name kept passing after the spec stopped declaring it, and a green run said nothing about whether the exception still described anything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ
A schema it cannot parse is downgraded to a warning: the endpoint or response it belongs to is dropped, the rest is written, and the run exits 0. Nothing downstream can tell that from a client that never had the operation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ
httpx renders a bool as `true`; urlencoding a Python bool gives `True`, which is what went out before. The service reads both, so nothing breaks either way -- but a caller diffing traffic across the upgrade should see no change, and this is the only field that moved. The parity test could not see it: it stringified our parameters before comparing them with the published ones, which turned `True` into `True` on both sides. It now compares what the transport will actually send. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ
Rebuilding the URL from the spec's path template dropped any prefix the deployment is served under -- an ingress route, an on-prem reverse proxy -- because no route template can carry one. The released client posted to the URL verbatim. The parity test could not see this: it compared against a deployment URL with no prefix, so both sides agreed. It now runs over a prefixed URL, a slash-less one and a mixed-case one, and compares against the released client's URL rather than a constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ
Three httpx failures reached callers as httpx classes, which nothing downstream catches: a redirect loop, an undecodable body, and any future RequestError that is not a TransportError. Two more were translated to a class the released client never raised for them -- requests had no write or pool timeout, and both surfaced as ConnectionError. The class chosen here also decides what gets retried, so an unsendable URL is now MissingSchema rather than a ConnectionError the retry loop would attempt four more times. The parametrised list of failures is replaced by a walk of httpx's own exception tree: a hand-written list is exactly as complete as the day it was written. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ
The transport adds headers no client object holds, so the only place the two can be compared is a socket. Both clients now run against a loopback server and their request heads are diffed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ
The generated tree is committed, so an edit inside it reviews like any other change and then vanishes on the next regeneration -- as does a spec change nobody ran the generator over. Regenerating in CI and diffing is what notices either one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ
Nothing bounds a stalled connection: the transport is untimed, and api_timeout cannot serve as one because the backend reads it as an execution mode -- 0 selects async, and negative values are accepted. A run that stalled for roughly 985 seconds is what this is for. Keyword-only and unset by default, so no released call shape changes and the default behaviour stays exactly what it was. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ
The released client concatenated its base URL with whatever the server handed back, so only a root-relative endpoint worked -- an absolute one became `https://hosthttps://host/...`. Reading the execution id out and rebuilding the route from the spec means all three spellings resolve to the same request, and this is what says so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ
The baseline was pinned by a version string in its own header comment, which an edit to the file can rewrite as easily as the code below it. Every parity test compares against this file, so a weakened baseline weakens all of them silently.
The generated transport is written against one httpx minor series; an upgrade needs a regeneration and a test run, not a resolver decision taken at install time in someone else's environment.
…params A multipart form field carries no null, so a caller passing None got the literal string "None" sent as a tag, an LLM profile id or a queue name for the service to resolve. These are overrides the service defaults when absent, and absent is what None asks for.
Each of these stated what the line below it does, or described a prior state that is no longer there to check against. Keep the reason, drop the narration.
The status URL was built from scheme and host alone, so a deployment served under a path prefix could execute -- the execute call sends the caller's URL verbatim -- and then never poll, losing the result of a paid execution. Documented divergence: the previous release has the same gap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ
The CLI that owns the name depends on this package, so the two always share an environment and the entry point collides on every install. `python -m unstract.clone` is unchanged, and the CLI offers the same command. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ
A 3xx was returned as if it were the answer, which a poll loop reads as a finished execution with no status; the previous transport followed redirects on both verbs. A status endpoint carrying no execution id now fails instead of polling for a blank one. InvalidURL joins the translation table, and the docstring names the two httpx families that stay outside it. Adds the multi-file upload comparison the parity suite never had, and lets the drift gate see a file the generator newly creates. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ
The status endpoint is the service's instruction for reaching one execution. Only its execution_id was being read: any other parameter on it -- a region hint, a signature -- was dropped from every poll, and a deployment URL that does not carry the spec route was polled at a path rebuilt from that route rather than at the endpoint itself. Both end the same way, at a paid execution whose result is never collected. Remaining parameters are now forwarded, and where no path prefix can be derived the endpoint is used as it came. Also pins the exception classes a malformed api_url raises. They differ from the released client's for two inputs; the divergence is deliberate and the test says so.
MissingSchema is a ValueError, so the row that exists to record released parity could not tell the two apart; it asserts the exact class now. The README and a release-notes draft carry the differences a caller can observe, including the console script this branch removed.
It shells out to ruff for post-processing. Finding none, it warns and exits 0, and the warning gate reports that as a spec it could not parse -- a clean regeneration on a runner without a global ruff failed with a message pointing at the wrong thing entirely. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ
The list restated what the code and the compat tests already pin, in a place that goes stale the moment either moves. The console script's removal is the one note a reader needs before running anything, so it stays in the README next to the invocation it changes.
|
| Filename | Overview |
|---|---|
| src/unstract/api_deployments/client.py | Replaces the request transport, adds complete parameter forwarding and connection lifecycle management, and safely constrains status polling to the configured origin. |
| .github/workflows/test.yml | Pins action references to immutable commits and adds a generated-SDK drift check. |
| pyproject.toml | Adds the generated transport dependencies and intentionally removes the top-level console script in favor of module invocation. |
| specs/docstudio-oss.json | Defines the execution and status contracts used to generate the committed transport. |
| tests/test_compat.py | Exercises released-client compatibility, request serialization, exception translation, origin restriction, and API-key rotation. |
Sequence Diagram
sequenceDiagram
participant Caller
participant Client as APIDeploymentsClient
participant Transport as Generated httpx transport
participant API as Deployment API
Caller->>Client: structure_file(files, options)
Client->>Transport: generated multipart request
Transport->>API: POST configured deployment URL
API-->>Client: execution result or status endpoint
alt execution pending
Caller->>Client: check_execution_status(endpoint)
Client->>Client: rebuild URL on configured origin
Client->>Transport: generated status request
Transport->>API: GET status with execution_id
API-->>Caller: normalized execution result
else execution complete
Client-->>Caller: normalized execution result
end
Reviews (5): Last reviewed commit: "fix(client): poll the deployment URLs th..." | Re-trigger Greptile
The status endpoint arrives in the execute reply, and joining it against the base URL let an absolute one replace the host -- so the reply chose where the bearer token was sent. Only its path is taken now. The key is also read per request rather than captured when the transport is built, so assigning `api_key` takes effect on the next call as it did when every call built its own header. Actions in the test workflow are pinned to commit SHAs, matching the clone workflow: a moved tag otherwise runs unreviewed code on the runner. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ
A host can be written without a scheme, and a path beginning `//` is read as one by anything that resolves a reference. None of those spellings escapes the configured origin today; the test says so, so a later change to how the status URL is built cannot quietly let one through. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ
ritwik-g
left a comment
There was a problem hiding this comment.
Standardized review — verdict: REQUEST CHANGES
Critical: 0 · High: 7 · Medium: 11 · Low: 8 · Lenses run: 17/17
Reviewed against a fixed 17-lens rubric (unstract:standard-review, plugin v0.30.1) at d67dda4b, diffed from merge-base 79f8d099 — 31 files, matching GitHub's count. sdk_docstudio/** was treated as generated and reviewed only for whether the facade depends on details regeneration could change.
The engineering here is strong and worth saying plainly: the generator is pinned and version-checked, gen_sdk.sh fails on generator warnings, the generated tree is stamped DO-NOT-EDIT and lint-excluded, the sdk-drift job uses git add -N so a newly-created file cannot slip past the diff, deps are upper-bounded with stated reasons, and the 1190-line parity suite is genuinely load-bearing. The _status_url origin-pinning is careful work.
The High findings cluster in exactly two places: the new parameters this PR exists to add, and the console-script removal.
Things that came out clean — stated so the silence is legible
- A dedicated security pass found no findings at confidence ≥ 8. Notable verified negatives:
follow_redirects=True(client.py:342) is safe because httpx 0.28.1 popsAuthorizationon any non-same-origin redirect — stricter thanrequests, which compares hostname only and preserves the header across an https→http downgrade. Multipart filename injection isn't reachable (ntpath.basename+ httpx's HTML5 escaping).verifydefaultsTrueon both layers and is never disabled. The five added packages all resolve from PyPI with sha256, no typosquats, andh11is at 0.16.0 (at-or-above the request-smuggling fix). - Greptile's open P1 on
client.py:333("cross-origin polling leaks credentials") does not reproduce at HEAD._status_urlstripsschemeandnetlocbeforegeturl(). I testedhttps://attacker.example/steal,//attacker.example/steal,https://user:pw@attacker.example/xandhttp://169.254.169.254/latest/meta-data/— every one resolves back to the configured origin. That is the same mechanism you used to refute the sibling P1 on:339, which Greptile conceded. Your thread is yours to close; flagging only that it's answered. - The two test changes I scrutinised hardest are legitimate.
tests/test_cli_top_level.py's deletion is justified — the wiring those 50 lines exercised survives intests/clone/test_cli.py:49-127through the samemonkeypatch.setattr("unstract.clone.cli.run_clone", …)seam.tests/test_retry.pyhas identical test and assertion counts on both sides (52 and 91); every hunk is either a patch-target move or a fixture gaining?execution_id=. Mutation-checking the parity gate (renaming a public method) produced 33 failures, so it is not warn-only.
Unanchored findings
These have no RIGHT-side line in this diff to attach to.
- [High] [Lens 7]
pyproject.toml(the deleted[project.scripts]block) — a published console script and a public module are removed with no version bump, no deprecation shim, and a release workflow that defaults topatch.unstract = "unstract.cli:main"is gone andsrc/unstract/cli.pyis deleted, sofrom unstract.cli import mainnow raisesImportErrortoo.__version__is untouched at1.5.3and.github/workflows/main.yml:11defaultsversion_bumptopatch— so if the release is cut on the default,1.5.4ships both breaks to everyone pinnedunstract-client~=1.5.3, and a CI pipeline runningunstract clone …getscommand not foundon a routine patch upgrade. Minimally: state the required bump (major, or at leastminor) somewhere the release dispatcher will see it. Better: keep[project.scripts] unstractpointing at a two-line shim that prints "moved to theunstract-clipackage; usepython -m unstract.clone clone …" and exits non-zero, removed next major. - [Medium] [Lens 1] The PR description does not mention the console-script removal. "Can this PR break any existing features" says "Two deliberate behaviour changes, both narrow" and lists only file-handle closing and status-URL rebuilding. This is the most user-visible change in the diff, and anyone generating release notes from the body will miss it.
- [Low] [Lens 1, 16]
README.md:76-77still describes async/sync selection asapi_timeout=0/api_timeout > 0. After this diff the decision is made by the effective per-requesttimeout(client.py:586), which a caller can now override. - PR title — this repo states no title convention in writing (no
CLAUDE.md, noCONTRIBUTING, no PR template), so there is nothing to judge against. - The description says "372 tests"; the suite collects 377. Cosmetic.
- The description notes "A live round trip against a deployment is still outstanding." That round trip is what settles the two open questions below.
Open questions
custom_data— does the deployment echo it back anywhere in the execute or status response, and under which key? No response schema declares it.timeout— is-1meant to be treated as async by this client (widening the branch to<= 0), or is the docstring the thing to narrow? See the finding atclient.py:586; this is a decision, not a defect I should pick for you.
Assumption
specs/docstudio-oss.json here is byte-identical to the copy in Zipstack/unstract#2237 — I diffed them. The response-shape findings raised on that PR therefore apply to the models generated here, and fixing them upstream will move this PR's generated tree.
Lens checklist (17/17)
1 see findings + Unanchored · 2 see findings · 3 see findings · 4 Clean — dedicated security pass plus the redirect/multipart/supply-chain negatives above · 5 N/A — no migrations or persisted state · 6 N/A — no concurrency primitives · 7 see findings · 8 see findings · 9 Clean · 10 see findings · 11 Clean — CI improved here: action SHAs pinned, sdk-drift gate added · 12 N/A · 13 see findings; the deletion and the retry-suite edit were both adjudicated legitimate above · 14 see findings · 15 see findings · 16 see findings · 17 see the gen_sdk.sh provenance finding
Posted as COMMENT, not REQUEST_CHANGES — the merge decision is yours, not the review's.
…eclares it Regenerates the transport from the backend spec that fixes the error bodies and file fields, and reworks the facade around what that spec now declares. Wire correctness: - custom_data went out as a Python repr, which the server's JSON field cannot parse. Non-str values are serialised before the generated encoder sees them. - timeout of -1, the API's own queue-only default, took the non-retrying branch; a 5xx there was reported as a finished execution with no result. The retried path is now taken for anything at or below zero. - Every HTTP error but 401 reduced to an empty `error`. Both shapes the API answers with are read, for execute and for status alike, and neither can crash the parse. - structure_file required an org/API pair in the deployment URL and then threw it away, rejecting short ingress URLs the previous release posted to. - httpx.LocalProtocolError was retried as a connection failure; it is a permanent client-side fault, so it is now raised as InvalidHeader. - Only FileNotFoundError was caught while opening inputs, leaking the handles already opened for any other OSError. Lifecycle and surface: - The pooled transport had no way to be released: adds close() and context manager support, and builds the transport under a lock. - Records the spec's upstream revision, so a stale copy is reviewable. - Documents the accepted differences from the released client rather than leaving them untested, and pins the packaging surface, explicit-None handling, and the error shapes the spec declares. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KjRpEocCvnxGUSkFivgnhk
ritwik-g
left a comment
There was a problem hiding this comment.
FOLLOWUP review — verdict: REQUEST CHANGES (one High remains)
Prior findings: 23 · RESOLVED 18 · PARTIALLY RESOLVED 4 · NOT RESOLVED 1
New findings: 1 High (carried over) · 4 Medium · 3 Low
Scope change: NO — the delta is confined to the files the findings named, plus the spec refresh they asked for.
Verified against 893e6b6 from merge-base 79f8d099. Status here is what the code shows, not what the replies claim — every fix below was exercised, and the ones that mattered were mutation-tested (break the fix, confirm a test goes red, restore).
What genuinely landed
The substantive work is real and I want to be specific about it, because most of it is verified by execution rather than by reading:
custom_datanow emitsb'{"a": 1, "b": "x"}'— valid JSON,json.loadsround-trips. Fixed in the facade, sogen_sdk.shcan't overwrite it. Mutation → 3 failures.timeoutwidened to<= 0, coherent across the branch, both docstrings, the README retry table and the spec default. Measured:-1→3 attempts,0→3,300→1. Mutation → 2 failures.- Error text now surfaces for every non-2xx through both methods — 400/403/404/413 all report the server's
detail, and degenerate bodies (null, bare string, list,{}) report rather than raise. Mutation → 11 failures. - The
AttributeErrorcrash is guarded, and the vendored spec is byte-identical to upstreamZipstack/unstract@eddd4b74(sha256e453d4f7…), with the generated tree regenerated to zero drift under the CI gate's own check. - The README command runs —
python -m unstract.clone clone …parses and stops at the documented--source-keyenv var. - File handles:
IsADirectoryError/PermissionErrornow raiseAPIDeploymentsClientExceptionwith zero leaked handles. LocalProtocolError: 4 attempts → 1, asInvalidHeader.- The packaging test, the explicit-
Noneregression test, the per-partContent-Typeassertion and the lifecycle (close()/__enter__/__exit__under a double-checked lock) are all present and mutation-verified.
That's 18 of 23 fully closed, several with better fixes than I suggested — the _operations() source-level guard and the monkeypatched-warn diagnostics test are both improvements on what I proposed.
Why this still can't be approved
One High is untouched — the console-script removal still ships as a patch release. Details in the inline comment on pyproject.toml. This is the only round-1 finding with no code change at all, and it's the one that reaches users who never read this PR.
Four new Mediums, of which two reach a running deployment: the submit-works-but-poll-fails gap on short ingress URLs, and the 429/409 statuses dropped from the spec while the client still has bespoke Retry-After handling for 429.
Residuals on the partially-resolved four
#4—check_execution_statusstill lets the error text double asextraction_result(client.py:850); the new test asserts onlyresult["error"], so it can't see it.#9— theparsed=Noneclaim is refuted by the dropped statuses above.#18— "returned under each result item's metadata.custom_data" is now specific and falsifiable, which is better, but nothing in this repo backs it:custom_dataappears in the spec exactly once, in the request schema.#22— the accepted-divergence list in the suite docstring reads as closed at three items; at least two more (the<= 0retry boundary, the constructor exception class) are discoverable only inside individual tests. The claim that the<= 0change was declared "in the suite's module docstring" isn't supported —timeoutdoesn't appear in lines 1-18.
Three Lows worth a line each
include_metrics is still sent while metrics was deleted from FileResult — one of the two is wrong. The packaging manifest is blind to top-level modules (I re-created src/unstract/cli.py and the test still passed). And the spec's sha256 is a comment enforced by nothing, where BASELINE_SHA256 gets a real assertion — the exact distinction that comment draws.
CI is green across the board (Greptile, lint, clone-tests, sdk-drift). Once the version question is settled this is close.
Posted as COMMENT. The three sibling PRs in this chain — Zipstack/unstract#2237, unstract-llm-whisperer#722 and llm-whisperer-python-client#35 — came out clean on their followup and I've approved those.
check_execution_status derived the organisation and API name eagerly, so a deployment URL too short to carry them -- an ingress rewrite -- submitted and then failed to poll, stranding an execution already paid for. The route is now derived where it is used, and the branch that reads the endpoint the service returned takes over when there is none. A non-2xx carrying the endpoint's own envelope is an execution state, not a refused request: its reason is no longer invented out of the raw body, and where the envelope puts that reason under the key a success puts the result under, it is reported once rather than as both. The suite's error fixtures are the shapes the service actually builds, the statuses each operation declares are pinned against a manifest so a spec that narrows them fails rather than shrinking the coverage, the vendored spec's recorded provenance is asserted, and the packaging surface check sees a top-level module as well as a package. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KjRpEocCvnxGUSkFivgnhk
|
Round-2 residuals from the review summary, all in #4 residual — error text doubling as #18 residual — the #22 residual — the accepted-divergence list. The module docstring of Correction to an earlier reply on the generated-models thread ( Two backend shapes the spec does not model, found while verifying the fixtures and recorded here for the upstream owner rather than acted on in this repo: the Full suite: 419 passed. |
What
APIDeploymentsClientissues its requests through a transport generated from the API's OpenAPI spec instead of hand-builtrequestscalls, and gains the ten request parameters the deployment accepts that had no argument to travel through.Why
Every parameter the deployment accepts had to be added to this client by hand, so it lagged the API: of the twelve
executeaccepts, the client could send two, and only through the constructor. Generating the transport from the spec the backend now commits (Zipstack/unstract#2237) makes the wire format follow the API rather than a hand-maintained copy of it.How
specs/docstudio-oss.json+tools/gen_sdk.shregeneratesrc/unstract/api_deployments/sdk_docstudio/with a pinned generator. The generated tree is committed, markedlinguist-generated, stamped DO-NOT-EDIT and excluded from lint — regeneration overwrites it wholesale, so fixes belong inclient.pyor in the spec.requestsequivalents inside the retried callable, so callers catchingConnectionError/Timeoutstill work and transport-error retry still counts.api_keytakes effect on the next call as it did when every call built its own header.c291e36forstructure_file,ed89066forcheck_execution_status) are keyword-only, named exactly as the API names them, and unset by default — an unset parameter is not sent, so the request is byte-for-byte unchanged for every existing call shape.timeoutandinclude_metadatastill fall back to the constructor values; a per-requesttimeoutselects the execution mode for that request.execution_idstays out: it is read from the endpoint URL the server handed back.Can this PR break any existing features
Two deliberate behaviour changes, both narrow:
structure_fileare now closed after the request. The previous client leaked them.test_status_url_matches_the_released_clientpins that the resulting request is identical.Everything else is asserted equal to 1.5.3. New parameters are keyword-only and unset by default, so no released call shape changes.
Release
Dispatch
Release Tag and Publish Packagewithversion_bump = minor, which publishes 1.6.0.This PR removes the
unstractconsole script (python -m unstract.clonereplaces it), so the published packaging surface shrinks and a patch release would take the command away from anyone on~=1.5.3. The workflow reads__version__as the last released version and applies the chosen bump, so the source stays at1.5.3and the dispatch input is what decides —patchwould ship 1.5.4 and must not be used.test_a_shrinking_packaging_surface_cannot_ship_as_a_patchfails if a patch release ever lands on this surface.Notes on Testing
372 tests.
tests/test_compat.pycompares this client against 1.5.3, vendored attests/baseline/client_1_5_3.pyand refreshed viatools/refresh_baseline.sh:api_timeout(a backend execution mode, not a socket timeout) never reaches the transportrequestsexception translation, asserted on the exact class rather than with a barepytest.raises— a bare one accepts a superclass, and it hid two real mismatches0,False,"") are sent rather than filtered out as absentapi_keyreaches the next requestThe existing retry suite is unchanged apart from its patch target. A live round trip against a deployment is still outstanding.
Related Issues or PRs
Dependencies Versions
Adds
httpx.requestsstays, as the exception types callers catch. Actions intest.ymlare pinned to commit SHAs, matchingclone-tests.yml.🤖 Generated with Claude Code
https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ