ump_workflow.serve
module ump-workflow
Serve one workflow bundle as an app speaking UMP’s engine adapter contract (feature workflow-bundles, T7, S6).
Needs the optional serve extra (ump-sdk, FastAPI, uvicorn, httpx); import ump_workflow never imports
this module, so the runtime stays light.
build_app(bundle) gives a FastAPI app from ump_sdk.create_app (/healthz, /v1/meta, problem+json, the
X-UMP-Principal header) with:
GET /v1/metadata:ump_spec.adapter.AdapterMetadata(adapter kindmodel, the bundle’s family, tier A, operationsanswerandfitbuilt in, every other adapter operation unavailable, one estimand per question, aWORKFLOW_INPUT_CONTRACTnote and the trust notes);GET /v1/questions(name, ask, estimand) andGET /v1/contract(the input contract);POST /v1/answer{rows, mode?, allow_refit?, questions?}:OpenBundle.runon the rows, as JSON;POST /v1/fit{rows, allow_refit?, questions?}: the same withmode="refit"(this is the workflow’s own refit body, not the adapter contract’sFitRequest);POST /v1/score,/v1/response_curve,/v1/marginal_return,/v1/decompose,/v1/optimize,/v1/calibrate,/v1/update: 501OPERATION_UNAVAILABLEwith a concretesuggested_action(ump_sdk.mount_unavailable, spec §8).
Trust on a server comes only from trusted_hashes (what a second person’s approval vouches for, see
share.trusted_hashes_from_approval) or the machine’s finprint trust store; there is no flag that trusts code by
hand, and a passed OpenBundle’s trust_code=True is not carried over. Untrusted code is never imported and
answers come from the bundle’s labelled Tier D summary. Request rows are never logged or echoed in an error, and
a request body over max_body_bytes (8 MiB, proposed) is refused with 413 WORKFLOW_REQUEST_TOO_LARGE. Runs are
serialised; a request that waits more than max_wait_seconds (60 s, proposed) for the run before it is refused
with 503 WORKFLOW_BUSY instead of holding a worker thread without limit.
Classes
Section titled “Classes”AnswerRequest
Section titled “AnswerRequest”class
class AnswerRequest(_Body)POST /v1/answer: the input rows as records; mode defaults to the bundle’s (score).
Fields
| Name | Type | Default | Description |
|---|---|---|---|
mode | Literal['score', 'refit'] | None | None |
Source: packages/ump-workflow/src/ump_workflow/serve.py, line 87
FitRequest
Section titled “FitRequest”class
class FitRequest(_Body)POST /v1/fit: an explicit refit on the rows.
Source: packages/ump-workflow/src/ump_workflow/serve.py, line 93
ServedWorkflow
Section titled “ServedWorkflow”class
class ServedWorkflowOne bundle behind the app: its runs are serialised (the module loads once, on first trusted use).
Methods and properties (6)
init constructor
ServedWorkflow( self, bundle: OpenBundle, *, approval: ApprovalTrust | None = None, model_id: str | None = None, max_body_bytes: int = DEFAULT_MAX_BODY_BYTES, max_wait_seconds: float = DEFAULT_MAX_WAIT_SECONDS,) -> Nonename property
name: strquestions method
def questions(self) -> list[dict[str, Any]]contract_words method
def contract_words(self) -> strmetadata method
def metadata(self) -> AdapterMetadatarun method
def run(self, body: _Body, mode: Literal['score', 'refit'] | None) -> dict[str, Any]Source: packages/ump-workflow/src/ump_workflow/serve.py, line 129
Functions
Section titled “Functions”build_app
Section titled “build_app”function
def build_app( bundle: str | Path | OpenBundle | BundleData, *, trusted_hashes: Iterable[str] | None = None, approval: ApprovalTrust | None = None, model_id: str | None = None, port: int | None = None, max_body_bytes: int = DEFAULT_MAX_BODY_BYTES, max_wait_seconds: float = DEFAULT_MAX_WAIT_SECONDS,) -> FastAPIA FastAPI app for one bundle. Trust comes only from trusted_hashes (or approval.hashes when
trusted_hashes is not given) and the machine’s trust store; approval and model_id (from
fetch_shared) also label untrusted answers with the approval’s reason. port is what /v1/meta
reports. max_wait_seconds (proposed) bounds how long a request waits for the run in progress.
Source: packages/ump-workflow/src/ump_workflow/serve.py, line 235
core_client
Section titled “core_client”function
def core_client(url: str | None, principal: str | None) -> CoreClientThe core client serve --artifact uses (a seam for tests).
Source: packages/ump-workflow/src/ump_workflow/serve.py, line 301
fetch_shared
Section titled “fetch_shared”function
def fetch_shared( model_id: str, *, core: CoreClient, dest: str | Path | None = None,) -> tuple[BundleData, ApprovalTrust]Download the bundle a workflow manifest names (its validated version while a revision is pending), rebuild
and verify it at dest (default: a new temp folder, data.root’s parent, which the caller removes when done;
the CLI passes its own folder and removes it when the server stops), and return it with what its approval vouches
for. A
manifest that is not a workflow bundle is WORKFLOW_SHARE_MISMATCH; a wrong blob BUNDLE_TAMPERED
(nothing left behind). An unapproved manifest returns an empty ApprovalTrust with its reason.
Source: packages/ump-workflow/src/ump_workflow/serve.py, line 308
listen
Section titled “listen”function
def listen(host: str, port: int) -> socket.socketA socket bound to host:port (port 0: a free one), so serve knows the port is its own before it
registers with core. OSError when the address is in use or cannot be bound.
The family is uvicorn’s (Config.bind_socket): IPv6 when the host holds a ’:’, else IPv4, so localhost
binds 127.0.0.1 whatever order the resolver lists. A port another process listens on must refuse on every
platform: SO_REUSEADDR (a restart reuses a port in TIME_WAIT) is set on Linux only, where a listening socket
still refuses; on macOS and the BSDs it lets 127.0.0.1:P bind while 0.0.0.0:P listens, and on Windows it lets a
bind take a port in use, so Windows gets SO_EXCLUSIVEADDRUSE instead.
Source: packages/ump-workflow/src/ump_workflow/serve.py, line 353
register
Section titled “register”function
def register(app: FastAPI, *, core: CoreClient, name: str, base_url: str) -> AnyAdd the served bundle to core’s engine registry (POST /v1/engines, kind model) with its metadata, so
s.engines() lists it. Core does not probe: the metadata is sent with the request.
Source: packages/ump-workflow/src/ump_workflow/serve.py, line 345
run_server
Section titled “run_server”function
def run_server( app: FastAPI, *, host: str, port: int, sock: socket.socket | None = None,) -> NoneServe app with uvicorn (request bodies are never logged; the access log has the path only), on sock
when given (from listen), else binding host:port itself.
Source: packages/ump-workflow/src/ump_workflow/serve.py, line 382
Constants
Section titled “Constants”| Name | Type | Value | Description |
|---|---|---|---|
DEFAULT_HOST | '127.0.0.1' | ||
DEFAULT_MAX_BODY_BYTES | 8 * 1024 * 1024 | Proposed request body limit (8 MiB, about a hundred thousand weekly rows of a few columns). | |
DEFAULT_MAX_WAIT_SECONDS | 60.0 | Proposed limit on how long a request waits for the run in progress before 503 WORKFLOW_BUSY. | |
DEFAULT_PORT | SERVE_DEFAULT_PORT | Proposed default port for ump-workflow serve (not in ump_sdk.ports: one app per bundle, so a second bundle on the same machine takes --port). |