Repository navigation
release: Merge latest prod-staging changes to prod-stable - #9
Merged
Merged
Conversation
The transport decoded every answer as the JSON envelope, but a few operations, such as a file download or a stream of output, answer with the bytes themselves, and a 206 means nothing without the Content-Range that came with it. _request_bytes returns the payload as a RawResponse with its status and headers, which parse the Content-Range into byte_range and total_size, and _stream_bytes yields it as it arrives. A failure is raised from the envelope as for any other operation. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
An operation whose success response was octet-stream was typed as answering nothing, so its payload was lost, and a header parameter such as Range had no argument of its own. Such an operation returns a RawResponse through the raw transport, and a header parameter is a keyword argument merged over the caller's headers. Each spec has a Makefile target of its own, and the templates take the transport's package from a variable. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
The transport methods of ApiClient were private, so a client outside this package, such as a generated plugin client that takes a transport rather than extending one, had nothing public to call and would have leaned on names it does not own. request, request_no_content, request_bytes, stream_bytes and stream are the public names, and config the configuration a client sends with. The template emits them, and the generated plumbing is regenerated, which changes only the names it calls. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A schema that is only a scalar or an array rendered as an empty model, so a union that named it had no type to carry and a value of it parsed to nothing. The template renders such a schema as a type alias, such as `Names = list[str]`, beside the union aliases. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
Every generated model marked every field optional, request bodies included, though the specification requires many of theirs. A service group request without its services built without complaint and failed at the server, where the error named no field. A schema named as a request now keeps the specification's required fields, so the omission fails at construction. A response stays optional throughout: one shape serves several answers, and a summary or a failed item carries only some fields. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A named map schema rendered as an empty model, a property name that is no identifier rendered as the field name itself, and an operation with several tags was rendered under its first only. A map is now an alias of dict[str, T], such a property is spelled in snake case with the wire name as its alias, or refused when no spelling is an identifier or two render alike, an operation is a method of each tagged class, and literals are rendered escaped. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A specification shape the templates could not render came out as a method that was wrong in silence: an operation without a tag was dropped, a non-JSON body or payload was discarded, and a parameter that was no identifier or clashed with another broke the module. Generation now stops with a message naming the operation or tag for each such shape, a cookie or a tag clash included; a required query or header parameter is a required argument, a path item's headers reach its operations, and _text() spells a non-string one. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
An alias that named another alias came out in alphabetical order, so one that sorted before the alias it named raised a NameError on import. The aliases are now written in passes, each taking those whose named aliases are already written; one left over names itself and is refused. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A query parameter declared with content rather than a schema made generation stop on a nil pointer instead of a message, and a property with a leading underscore became a pydantic private attribute, so its value never reached the field. Such a query parameter is refused with a message naming it, and such a property is spelled without the underscore and keeps the wire name as its alias, as other unsafe names already are. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A property named like a pydantic member broke its model: a model_ member failed at import, and a version 1 method such as json was shadowed with a warning. A query parameter holding objects and a union or enum response rendered what the transport cannot handle. A model_ name gets a field_ prefix and a version 1 method name a trailing underscore, both with the wire name as alias. Such a parameter or response is refused with a message that names the operation. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A schema named like a keyword or an import of the models module rendered an invalid or shadowing class, and a required date-time that the specification allowed to be null was typed as datetime alone, so pydantic refused the null. A schema name is now checked before anything is rendered, and a nullable date-time keeps its None. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A schema named like a builtin the annotations use took its place in every model defined after it, a field named like one broke the annotations of the fields after it, and a parameter named like a helper the method body calls took the place of that helper. Such a schema or parameter is refused with a message naming it, and such a field is spelled with a trailing underscore and keeps the wire name as its alias. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
Which generated models keep their required fields was decided by the schema's name, and nothing told whoever names the schemas, or what follows when a name breaks the convention. The models template and the README now say that a schema whose name contains Request keeps its required fields, every other one has each field optional, and what a request or response named otherwise does. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A named enumeration the specification marked nullable rendered as its Literal alone, so a required request field of that type refused None although the specification allowed it. The other alias forms already kept their None. The enum alias now carries | None when the schema is nullable, as the array, map and union aliases already do. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A query parameter in a style other than form, or one that allows reserved characters, and a path parameter in the label or matrix style rendered as if they were form and simple, so the request went out with a value or a route the specification did not mean. Such a parameter is refused with a message that names it. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A schema that put oneOf or anyOf beside allOf, or beside each other, reached the alias path, where one union keyword is rendered and the rest is dropped, so the type was wider than the wire. Such a schema is refused with a message that names it. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A tag is rendered verbatim in the docstring of its class, and the name checks only saw its snake case, so a tag holding a quote or a backslash escape passed them and left a module that does not parse. The tag itself is now checked before anything is rendered: letters, digits, spaces, dots, dashes and underscores only. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A generated module imported datetime, Any, Literal, Field and AsyncIterator whether it used them or not, so the unused-import check had to be switched off for the whole generated tree, where it would also have caught a stale import of ours. The models template derives each field's name and type first and imports only what they need, adding | None to an optional field once; the resources template imports AsyncIterator, Any and Literal only where a module streams, takes Any or takes an enum. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A request model was detected by its name alone, so a schema that only requests use, such as the image spec a plugin takes, had every field optional when its name lacked Request, and a request missing a field the specification requires failed at the server. A schema that only request bodies reach, directly or through the models they name, is a request model too, as the plugin generator has it; one a response also reaches stays optional. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A nullable query, path or header parameter rendered an argument the transport cannot send as null: it leaves a None value out and spells a None list item as "None". A nullable response schema was typed as a model the transport cannot decode a null into. Such a parameter or response is refused with a message naming it and what to change in the specification, as the plugin generator does. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
The platform and control-plane clients were rendered from an older specification, so a create could not be given annotations, gpus or type: check_spec refused each as unknown, though the API has accepted all three for some time. Both are regenerated from the published specification: ServiceGroupsApi is ServicesApi, PlatformApi gains the audit API, UsersApi loses add_users, request models keep their required fields, as the README now says, and F401 is on again. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A plugin serves an API of its own from inside an instance, which the platform proxies under the instance on the metro running it. The SDK had no client for that API and no way to build its route. The plumbing is the unikraft-cloud-plugin-sandbox-api package, which plugin-sdk renders. SandboxApi, reached as ukc.api.plugins.sandbox, sends it through the SDK's transport, and for_instance() points it at one instance; a plugin named v1 keeps its route. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A handle memoised its lookup as a task and kept it however it ended, so a name not listed yet, or a metro that did not answer, was raised again by every later await. A set kept a failed lookup too. A lookup that failed is made again on the next await, for the handles that only look a resource up, for sets, and for the lookup an operation chained onto such a handle starts from; a create, and the chained operation itself, keep their one outcome. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
The SDK had no client for the sandbox plugin. A caller had to find the instance's UUID, build the plugin's route and drive the plumbing by hand to run a command or move a file. sandbox() and plugin() on an instance handle give a Sandbox or a Plugin, which read the instance only when its UUID is not known. Sandbox runs, follows and signals commands and moves files; output is polled, and a timeout interrupts a command, then waits for it. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
collect() gathered a command's output for the result, so a caller that wanted it as it arrived had to poll the command itself, and with it re-implement the interrupt and the grace a timeout gives. A finished command also stayed in the plugin until deleted. An on_output sink is handed each chunk as it is read, an awaitable it returns awaited before the next, while the result still carries everything. forget drops the plugin's record once the command has ended; a command still running is kept. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
The plugin reads a request body of two mebibytes at most, and a whole file came back in one payload. A caller moving anything larger chunked its writes by hand and held a download in memory. write() sends data longer than a chunk in appended pieces and can create the directories above the file; upload_file() streams a local file the same way. stream() and read_to() hand a file over as it arrives, and examples/sandbox.py walks through the client. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
The metro proxy ends a plugin request after 60s. An unbounded wait for a command's end, and any wait longer than that, was cut off with the proxy's 504 page, so a long command could not be waited for or followed to its end. A wait is sent as waits of at most WAIT_SLICE, thirty seconds, until the command ends or the caller's timeout runs out, each given a read timeout that outlasts it; one the proxy drops is retried as a poll is. The README gains the sandbox section. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
The SDK had no idiomatic images client, and images are reported per metro, so an account-wide view meant asking every metro in scope and merging the answers by hand. Images, reached as scope.images, lists every metro in scope and tags each image with its metro; a partial answer raises with what did arrive. A lookup is a filtered listing, as in the CLI, and one metro's view is that metro's client. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
The images client lists what each metro's nodes have cached, which is not whether an image can be pulled: the cache drops images the registry keeps and keeps ones it has dropped. Callers asked the OCI registry directly, with its token exchange, to find out. Images.find and Images.exists ask the control plane, which answers from the registry itself and needs only the SDK's token. A reference may carry a tag, a digest, a registry host or a scheme, and a bare one means the latest tag. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
The API reports a stop as two integers, a reason bitmask and a code whose meaning depends on it. The Go SDK decodes both; this one handed them over raw, so every caller decoded the platform's image-pull failure and the kernel's out-of-memory for itself. Stop, StopReason and the platform and kernel codes port the Go package; Instance.stop reads them off an instance and describe_stop() puts them in words, as the CLI reports a stop, e.g. "kernel crash: out of memory (ENOMEM)". Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A create that waited for its instance to run raised a bare "API reported an error" when the instance stopped instead. The failed item names the instance and nothing else, so the reason survived only on the stopped instance, which every caller had to read. Instances.create reads that instance and raises InstanceStoppedError with its decoded stop; ResponseError carries an item's state. A create that waits stretches its read timeout past that wait, as wait() does, and raises a lapsed wait as WaitTimeoutError. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A delete answered as soon as it was under way, failed on an instance already gone, and failed at once on one the platform reported busy, as a relay interface is for a while after its instance is deleted. Callers waited and retried by hand. delete() takes timeout_seconds for the API's own wait, missing_ok and retry_busy, which backs off while the API says EBUSY; a wait of -1 leaves the read timeout unbounded. NotFoundError.absent marks a missing resource, and only that not-found is ever forgiven. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
An instance set could be named only by uuid or name, one match per metro. A caller cleaning up everything a job had created had to list by tag and then delete each match itself, or guess at names. each(tags=[...]) locates every instance in scope carrying the tags and hands back the usual set, so `.delete(missing_ok=True)` covers them all. Tags that select nothing are an empty set rather than a failure, so a cleanup can run again. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
Templates had plumbing only. Preparing one meant creating a seed clone, waiting for the template to be listed and deleting the seed by hand, and cloning meant building the create request yourself. Add Templates, reached as scope.templates: prepare makes a template from an instance specification, in the one metro the scope names, and get(...).clone stamps instances out of it; list, each and delete work as for every other resource. examples/templates.py shows it. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
The public surface changed: a dependency on the sandbox plumbing package, which names 0.2.0 as the oldest SDK whose transport it fits, plumbing renamed and removed by the regeneration, and required fields that are now required. The package and `__version__` say 0.2.0, the lock follows, and a changelog records what the release adds, changes and removes. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
The Makefile's help called fmt a format of all sources and misaligned its columns, openapi-gen was passed a variable no template reads, the README linked openapi-gen to the GitHub organisation, and several docstrings and comments were stale. fmt says what it formats, the columns fit the longest target and the dead flag is gone. The README says the Makefile pins openapi-gen, and the stale docstrings and comments are corrected. Signed-off-by: Daniel Vallance <daniel@unikraft.com>
danielvallance
self-requested a review
October 6, 2026 15:02
danielvallance
approved these changes
Oct 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Automated changes by create-pull-request GitHub action