Skip to content

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 kind model, the bundle’s family, tier A, operations answer and fit built in, every other adapter operation unavailable, one estimand per question, a WORKFLOW_INPUT_CONTRACT note and the trust notes);
  • GET /v1/questions (name, ask, estimand) and GET /v1/contract (the input contract);
  • POST /v1/answer {rows, mode?, allow_refit?, questions?}: OpenBundle.run on the rows, as JSON;
  • POST /v1/fit {rows, allow_refit?, questions?}: the same with mode="refit" (this is the workflow’s own refit body, not the adapter contract’s FitRequest);
  • POST /v1/score, /v1/response_curve, /v1/marginal_return, /v1/decompose, /v1/optimize, /v1/calibrate, /v1/update: 501 OPERATION_UNAVAILABLE with a concrete suggested_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.

class

class AnswerRequest(_Body)

POST /v1/answer: the input rows as records; mode defaults to the bundle’s (score).

Fields

NameTypeDefaultDescription
modeLiteral['score', 'refit'] | NoneNone

Source: packages/ump-workflow/src/ump_workflow/serve.py, line 87

class

class FitRequest(_Body)

POST /v1/fit: an explicit refit on the rows.

Source: packages/ump-workflow/src/ump_workflow/serve.py, line 93

class

class ServedWorkflow

One 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,
) -> None

name property

name: str

questions method

def questions(self) -> list[dict[str, Any]]

contract_words method

def contract_words(self) -> str

metadata method

def metadata(self) -> AdapterMetadata

run 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

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,
) -> FastAPI

A 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

function

def core_client(url: str | None, principal: str | None) -> CoreClient

The core client serve --artifact uses (a seam for tests).

Source: packages/ump-workflow/src/ump_workflow/serve.py, line 301

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

function

def listen(host: str, port: int) -> socket.socket

A 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

function

def register(app: FastAPI, *, core: CoreClient, name: str, base_url: str) -> Any

Add 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

function

def run_server(
app: FastAPI,
*,
host: str,
port: int,
sock: socket.socket | None = None,
) -> None

Serve 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

NameTypeValueDescription
DEFAULT_HOST'127.0.0.1'
DEFAULT_MAX_BODY_BYTES8 * 1024 * 1024Proposed request body limit (8 MiB, about a hundred thousand weekly rows of a few columns).
DEFAULT_MAX_WAIT_SECONDS60.0Proposed limit on how long a request waits for the run in progress before 503 WORKFLOW_BUSY.
DEFAULT_PORTSERVE_DEFAULT_PORTProposed 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).