Instrument a computation¶
OCLP records durable computation facts; it does not prescribe a storage convention or an orchestration engine. The Python SDK keeps reusable declarations beside ordinary functions and can, when explicitly activated, turn a declared returned value into a real Artifact and Execution.
The normative field contract is the OCLP specification.
Names, descriptions, and annotations are application-owned metadata¶
Every record-producing SDK declaration requires an application-supplied
name: Computations, Evidence evaluators, Artifact-producing decorators,
materialized computation outputs, runs, ArtifactSets, and materialized
release manifests. The SDK carries that label into the resulting record; it
does not derive one from a function name, port, run ID, digest, timestamp, or
filesystem path.
Use a concise label that reads well in an explorer, such as "Train fold" or
"Validation metrics". The SDK assigns every materialized Core record an
opaque UUID. Inputs and outputs link to those UUIDs, while the run-profile
run_id, parameters, timestamps, and application annotations carry the
domain context. An Artifact representation class used only as an input
declaration (for example, inputs={"table": CsvArtifact}) has no produced
record and needs no name.
An Artifact payload's SHA-256 digest identifies its bytes, not its Core record. Publishing the same bytes again produces a distinct Artifact record UUID; a catalog may still use the payload digest to find equivalent content and share retrieval locations. Mutating any declared record field is never a revision of an existing UUID—it must produce a new record.
Use name for a concise label, description for longer plain-text human
explanation, and annotations for application-specific structured metadata.
For the one-callable/one-record decorators (@computation, @evidence, and
Artifact acquisition decorators), either supply description="..." or opt in
to description_from_docstring=True:
@computation(
name="Prepare report",
description_from_docstring=True,
)
def prepare_report() -> None:
"""Create the normalized report used by the quality gate."""
The SDK calls inspect.getdoc() only for that explicit opt-in. A supplied
description takes precedence, and an opted-in callable with no non-empty
docstring fails while decorators are applied. Ordinary # comments are not
runtime metadata and are never inferred. For multiple output Artifacts, set an
explicit description on each ArtifactType declaration instead.
Declare the computation and its success check¶
An @evidence evaluator defines a reusable, source-bound validation gate. It
returns exactly one outcome—"pass", "fail", or "error"—for one
application-owned rule. The runtime creates the single core Evidence record
from that outcome. Passing Evidence has no Diagnostic; a returned "fail" or
"error" receives a compact SDK Diagnostic with a stable OCLP code and
validation-stage message. An evaluator exception records its exception type and
message as an evaluation-stage Diagnostic. A @computation names an ordinary
callable and can directly require that evaluator—there is no separate
ID/version reference or requirement wrapper.
from oclp import computation, evidence
from oclp.models import PortDefinition
@evidence(name="Normalized report quality")
def normalized_report_quality(report: dict[str, object]) -> str:
title = str(report["title"])
return "pass" if title else "fail"
@computation(
name="Normalize report",
input_ports=(PortDefinition(name="source", media_types=("application/json",)),),
output_ports=(PortDefinition(name="report", media_types=("application/json",)),),
requires=(normalized_report_quality,),
)
def normalize_report(source: dict[str, object]) -> dict[str, object]:
return {"title": str(source["title"]).strip()}
@computation has no application-supplied identity. When a decorated callable
is first observed in an active OclpRun, the SDK emits one fresh opaque UUID
Computation record for that callable. Later invocations of the same callable
in that run reference the same Computation; a later observed run emits a new
Computation record. Every Core record and every RecordReference therefore
remains UUID-only without requiring an application declaration key.
@evidence preserves ordinary call semantics and attaches static metadata.
Separate checks should be separate evaluators and Evidence records—not a
second SDK-only Evidence return type.
@computation does the same unless it declares runtime outputs inside an
active OclpRun; that opt-in path is described below. At publication time the
source actually selected for the run binds both records to exact source
revisions.
from oclp import GitSource, computation_record
source_basis = GitSource(
repository="https://github.com/example/reports.git",
commit="0123456789abcdef0123456789abcdef01234567",
path="src/reports/normalize.py",
)
report_computation = computation_record(normalize_report, source=source_basis)
Calling computation_record(...) directly produces a fresh Computation record.
Normal application code does not need to call it: OclpRun materializes the
record when the decorated callable is first observed.
report_computation.required_evidence contains the exact source-bound evaluator
Implementation. The same binding appears in each resulting Evidence record;
changing the evaluator code or observed source produces a different binding.
Declared input ports match callable parameters¶
When a Python Computation explicitly declares input_ports, each port name
must match a real function parameter (excluding self and cls). The SDK
raises an error when the decorator is applied if they differ. This keeps the
portable Computation interface and the Python call boundary honest:
@computation(
name="Prepare report",
input_ports=(PortDefinition(name="source_snapshot"),),
)
def prepare_report(source_snapshot: pd.DataFrame) -> pd.DataFrame:
return source_snapshot.dropna()
Ordinary arguments become declared Execution parameters¶
Artifact inputs are declared at the decorator boundary. Every other ordinary,
JSON-compatible function argument is inferred as a ParameterDefinition on the
portable Computation record—there is no second parameters= block to keep in
sync. Required arguments remain required; Python defaults become JSON Schema
default values and are recorded as effective values on each Execution.
from oclp import CsvArtifact, JsonArtifact, computation
@computation(
name="Train one temporal fold",
inputs={
"feature_table": CsvArtifact,
"fold_definition": JsonArtifact,
},
outputs={"metrics": JsonArtifact(name="Fold metrics")},
)
def train_fold(
feature_table: pd.DataFrame,
fold_definition: dict[str, object],
*,
fold_number: int,
training_window: Literal["pre-holdout"] = "pre-holdout",
) -> dict[str, object]:
...
This produces parameter definitions equivalent to:
[
{"name": "fold_number", "schema": {"type": "integer"}, "required": true},
{
"name": "training_window",
"schema": {
"type": "string",
"enum": ["pre-holdout"],
"default": "pre-holdout"
},
"required": false
}
]
Within an OclpRun, calling train_fold(..., fold_number=2) publishes an
Execution with parameters: {"fold_number": 2,
"training_window": "pre-holdout"}. The SDK binds Python defaults before it
publishes, so an omitted optional argument is still reproducible. It infers
schemas for JSON-safe built-ins, containers, unions, and Literal values.
Non-JSON implementation plumbing such as pathlib.Path is intentionally not
captured. Make durable content an input Artifact instead; keep a local output
path as runtime-only plumbing.
An Execution then records the Artifact reference at
inputs["source_snapshot"]. If a function currently accepts an intermediate
object such as frame or results rather than the Artifact-oriented input it
claims to consume, it should not declare the mismatched port yet. Migrate that
stage to a real decorator-bound input first; do not use a misleading name just
to make a graph look more complete.
Acquire an external value as an Artifact¶
Use @csv_artifact for an effectful boundary that obtains a durable tabular input instead
of deriving one. Its function body returns a pandas.DataFrame, while its
public decorated call returns a CsvArtifact handle in an active OclpRun.
It does not create a Computation, Execution, or Event.
import pandas as pd
from oclp import CsvArtifact, OclpRun, computation, csv_artifact
@csv_artifact(
name="UCI Bike Sharing hourly source snapshot",
index=False,
lineterminator="\n",
)
def download_source_csv() -> pd.DataFrame:
return fetch_the_external_table()
with OclpRun(...) as observed:
source_snapshot = download_source_csv()
@csv_artifact validates that the function returned a pandas.DataFrame,
serializes it with those options, writes the CSV payload, hashes it, publishes
the Artifact, and returns its CsvArtifact handle. This is appropriate for a
database extract or API client that exposes a table object. If the external
system exposes original bytes, use a byte-oriented Artifact contract instead;
a CSV snapshot is not a claim to preserve the provider's response
byte-for-byte. Artifact-producing decorators require an active OclpRun,
because publishing a durable Artifact requires a configured store.
Adapter-driven Artifact inputs¶
The OCLP protocol records an Artifact reference, while a Python function needs an in-memory value. The SDK bridges those distinct concerns with an adapter:
@computation(
name="Prepare report",
inputs={"source_snapshot": CsvArtifact},
)
def prepare_report(source_snapshot: pd.DataFrame) -> pd.DataFrame:
# Ordinary pandas code; no OCLP loader or location logic here.
return source_snapshot.dropna()
with OclpRun(...) as observed:
source_snapshot = download_source_csv() # CsvArtifact
prepared = prepare_report(source_snapshot) # receives pd.DataFrame
inputs={"source_snapshot": CsvArtifact} is SDK metadata, not a new protocol
record type. It derives the portable input port
{"name": "source_snapshot", "media_types": ["text/csv"]} and requires a
CsvArtifact at the runtime call boundary. The pd.DataFrame annotation still
describes the ordinary in-memory value delivered to the function body. The
registered PandasCsvAdapter verifies the payload SHA-256 digest, reads the
CSV, and supplies the resulting DataFrame. The Execution records the original
Artifact UUID reference in inputs["source_snapshot"].
The annotation describes the value that the function actually receives; the Artifact handle identifies the durable representation. The adapter registry selects a loader from that pair. A representation is therefore not coupled to one in-memory library or type.
| Durable Artifact handle | Durable representation | Requested runtime type | Built-in adapter |
| --- | --- | --- |
| CsvArtifact | UTF-8 text/csv | pd.DataFrame | PandasCsvAdapter |
| ParquetArtifact | application/vnd.apache.parquet | pd.DataFrame | PandasParquetAdapter |
| JsonArtifact | application/json | dict[...] or Mapping[...] | JsonMappingAdapter |
| JsonArtifact | pandas orient="table" JSON | pd.DataFrame | PandasJsonTableAdapter |
Install the optional Parquet implementation with oclp[parquet]. Generic JSON
loads as a mapping when the callable asks for one. Loading JSON as a DataFrame
is deliberately narrower: the adapter validates pandas' table-JSON structure
before it calls pandas. If no single registered adapter supports the Artifact
and annotated target type, the SDK fails before the function body begins. A
port's optional media_types constraint can restrict which durable forms a
Computation accepts.
Exercise multiple source representations¶
The bike-demand reference project includes a small source-format factory to prove that format choice remains at the Artifact boundary. All three decorated acquisition functions fetch the same logical UCI table, but each persists a different byte representation and therefore has its own payload digest. Each materialization also receives a fresh opaque Artifact record UUID:
from oclp import csv_artifact, json_artifact, parquet_artifact
@csv_artifact(name="UCI source (CSV)")
def download_csv() -> pd.DataFrame:
return fetch_the_external_table()
@parquet_artifact(
name="UCI source (Parquet)",
compression="zstd",
)
def download_parquet() -> pd.DataFrame:
return fetch_the_external_table()
@json_artifact(
name="UCI source (pandas table JSON)",
serialization="pandas-table",
)
def download_json() -> pd.DataFrame:
return fetch_the_external_table()
All three decorated calls return handles, not DataFrames. A normal Computation can accept all three forms while preserving a plain pandas signature:
@computation(
name="Inspect source",
input_ports=(
PortDefinition(
name="source_snapshot",
media_types=(
"text/csv",
"application/vnd.apache.parquet",
"application/json",
),
),
),
)
def inspect_source(source_snapshot: pd.DataFrame) -> int:
return len(source_snapshot)
The caller chooses a durable representation; the function remains ordinary pandas code. This is the boundary we want to dogfood: storage format is explicit in one decorator, while the computation consumes its declared in-memory type through a verified adapter.
@parquet_artifact requires the SDK's parquet extra because it uses
pyarrow; its index and compression arguments are serialization policy.
@json_artifact defaults to ordinary JSON-compatible values. Set
serialization="pandas-table" explicitly for a DataFrame's
to_json(orient="table", index=False, date_format="iso") representation.
Both return JsonArtifact; the consumer annotation selects the appropriate
adapter. As with CSV, changing a representation option can change the
immutable payload digest.
Artifact fields: declared intent and derived facts¶
An Artifact record intentionally contains both configuration and observations. The decorator declares the representation the application wants. The SDK then records facts about the exact bytes it actually persisted. Keeping these roles separate prevents a caller from accidentally claiming a digest, size, time, or location that does not match the materialization.
| Artifact field | Source in the current CSV decorator | Why |
|---|---|---|
id |
SDK-generated opaque UUID | Identifies this one immutable Artifact record. It is neither a logical source name nor a payload hash. |
name |
Required name= decorator argument |
Concise human-readable label for people and UIs; the ID and run profile carry run-specific identity. |
kind |
SDK constant: artifact |
The decorator creates one Artifact record. |
media_type |
SDK constant: text/csv |
The concrete decorator defines the persisted representation. |
digest |
SDK SHA-256 of the serialized CSV bytes | Integrity must describe the real payload. |
size |
SDK byte count | It must describe the real payload. |
created_at |
SDK materialization timestamp | It records when this immutable payload was created. |
locations |
Active store's resulting payload URI | A location is a property of a particular storage implementation. |
schema_uri |
Optional schema_uri= decorator argument; default null |
Declare it only when the application publishes a real, versioned schema for the persisted representation. Do not invent a URI merely to label a format. |
profiles, annotations |
Optional decorator arguments | Declare extension-profile bindings and portable application metadata. |
index, lineterminator, na_rep, float_format, date_format, and
columns are CSV serialization options, not OCLP record fields. They still
matter to the contract because changing any of them can change the persisted
bytes and therefore the digest.
@csv_artifact(
name="UCI Bike Sharing hourly source snapshot", # Artifact name
index=False, # do not write DataFrame index
lineterminator="\n", # platform-independent bytes
columns=("timestamp", "temperature", "demand"), # intentional CSV projection
annotations={"source": "UCI"}, # portable application metadata
)
def download_source_csv(dataset_id: int = 275) -> pd.DataFrame:
return fetch_the_external_table()
Every decorated call receives a new opaque Artifact UUID. The function's
dataset_id is domain provenance, so record it in an application annotation
when consumers need it; the Artifact digest validates the persisted bytes.
Two materializations may describe equal bytes while remaining distinct Artifact
records, which preserves their independent creation context.
The active OclpRun is deliberately separate from the decorator: it supplies
the publication context—store, namespace, and run ID—not the Artifact's CSV
serialization policy. The function remains the sole place an application
declares that policy.
Materialize a simple computation output automatically¶
For a straightforward durable output, declare its port and its persistence representation directly on the callable. The port name is no longer disconnected metadata: it names the Artifact produced from the return value. The Python return type and the durable Artifact format are separate decisions.
from pathlib import Path
import pandas as pd
from oclp import CsvArtifact, GitSource, OclpRun, computation
from oclp.publishing import LocalArtifactPublisher
@computation(
name="Fetch report",
outputs={
"source_snapshot": CsvArtifact(
name="Fetched report snapshot",
path="reports/source.csv",
),
},
)
def fetch_report() -> pd.DataFrame:
return pd.DataFrame({"value": [1, 2, 3]})
with LocalArtifactPublisher(
catalog_path=Path("data/oclp/catalog.duckdb"),
record_root=Path("data/oclp"),
payload_root=Path("data/runs/example-run"),
) as publisher:
with OclpRun(
publisher=publisher,
namespace="urn:example",
run_id="example-run",
source=source_basis,
) as observed:
dataframe = fetch_report()
source_snapshot = observed.outputs_for(dataframe)["source_snapshot"]
The wrapper returns the original dataframe; it does not replace application
objects with an OCLP proxy. outputs_for(...) returns an internal resolved
Artifact handle for the materialized source_snapshot, suitable for passing to
a later Computation. The CsvArtifact(...) declaration is the explicit
decision to persist a CSV snapshot, including its name, payload path, schema,
and pandas serialization options. The SDK publishes one source-bound
Computation for the callable in the active run, that Artifact, an Execution whose
outputs["source_snapshot"] references it, and start/publication/terminal
Events. Passing the returned output handle to another declared Computation
records the exact Artifact reference and loads its verified bytes through the
adapter selected by that function's parameter annotation.
An output mapping always needs an explicit concrete Artifact type. CsvArtifact
persists a table snapshot, JsonArtifact persists JSON-compatible data,
BytesArtifact persists bytes, and FileArtifact copies an existing file. A
domain type can own both sides of the boundary: CatBoostModelArtifact, for
example, persists a returned fitted CatBoost model as native .cbm bytes and
loads a verified Artifact into a later CatBoostRegressor parameter. For more
than one direct output, each port is a key in the returned mapping. There is no
separate attribute= selector to keep in sync.
Static annotations describe every Artifact from a declaration. When an
annotation belongs to one particular call, such as a temporal fold number, an
output annotation_factory can declare the Computation parameters it needs by
name. The SDK passes those exact call values and stores the returned
JSON-compatible fields on that one Artifact record. The factory can return
several annotations and its values override static annotations with the same
keys.
@computation(
name="Prepare report",
outputs={
"table": CsvArtifact(name="Prepared report"),
"report_contract": JsonArtifact(name="Report contract"),
},
)
def prepare_report() -> dict[str, object]:
return {
"table": ...,
"report_contract": ...,
}
The same Artifact type names describe both ends of a specialized boundary. A fitted CatBoost model needs no runner-provided path or application-owned serializer:
import pandas as pd
from catboost import CatBoostRegressor
from oclp import CatBoostModelArtifact, CsvArtifact, computation
@computation(
name="Train model",
inputs={"features": CsvArtifact},
outputs={"model": CatBoostModelArtifact(name="Candidate model")},
)
def train_model(features: pd.DataFrame) -> CatBoostRegressor:
model = CatBoostRegressor(verbose=False)
return model.fit(features[["temperature"]], features["demand"])
CatBoostModelArtifact saves the returned model to temporary native .cbm
bytes and publishes those bytes as the Artifact payload. A later computation
can declare inputs={"model": CatBoostModelArtifact} and annotate its normal
Python parameter as CatBoostRegressor; the SDK verifies the digest and loads
the model before it calls the function.
For a port that consumes several Artifacts of one representation, use
many(...). The OCLP record gets cardinality: "many", while the function
receives its normal typed collection after every member is verified and loaded.
import pandas as pd
from oclp import CsvArtifact, JsonArtifact, computation, many
@computation(
name="Aggregate fold predictions",
inputs={"fold_predictions": many(CsvArtifact)},
outputs={"evaluation": JsonArtifact(name="Fold evaluation")},
)
def aggregate_folds(
fold_predictions: tuple[pd.DataFrame, ...],
) -> dict[str, object]:
return {"evaluation": summarize(fold_predictions)}
Required Evidence can remain equally local. An evaluator must accept exactly
one parameter; for a multi-output result that parameter name selects the
returned output it evaluates. OclpRun publishes its Evidence before the
terminal Event and uses a failed terminal status when any required evaluator
fails.
@evidence(name="Quality")
def quality(evaluation: dict[str, float]) -> str:
return "pass" if evaluation["rmse"] <= 1.0 else "fail"
@computation(
name="Evaluate",
outputs={"evaluation": JsonArtifact(name="Evaluation")},
requires=(quality,),
)
def evaluate(...) -> dict[str, object]:
return {"evaluation": {"rmse": 0.8}}
CsvArtifact writes deterministic CSV deliberately, while ParquetArtifact
is available as an optional oclp[parquet] integration. Both
CatBoostModelArtifact and XGBoostModelArtifact are SDK-owned model
integrations rather than application boilerplate. See Artifact formats and
library integrations for their exact native persistence
formats and compatibility rules.
Declare an ArtifactSet output from one Computation¶
When a single Computation materializes several Artifacts that together form one package, declare the collection beside its real output ports. The SDK materializes the member Artifacts, publishes the ArtifactSet, and records the set as an additional output of that same Execution. It does not introduce a second, artificial packaging Computation.
from oclp import ComputationArtifactSet, JsonArtifact, computation
@computation(
name="Evaluate candidate",
outputs={
"model": JsonArtifact(name="Candidate model"),
"evaluation": JsonArtifact(name="Candidate evaluation"),
},
artifact_set=ComputationArtifactSet(
name="Evaluated candidate",
port="candidate_release",
members={
"model": ("model", "model"),
"evaluation": ("evaluation", "validation-report"),
},
),
)
def evaluate_candidate(...) -> dict[str, object]:
return {"model": model, "evaluation": evaluation}
candidate_release is an Execution output bound to the exact ArtifactSet.
Within an active run, retrieve it with
observed.artifact_set_outputs_for(result)["candidate_release"]. Use a
run-level declaration when the collection instead combines outputs from
several child Computations.
Include an output Artifact's UUID in its own payload¶
Most payloads do not need to identify their containing Artifact. When an API
response does, call output_artifact_id("port") inside the observed
Computation. The SDK reserves that UUID before invoking the body and uses it
when it persists the named output, so the payload and Artifact record agree
without application code allocating a second identifier.
from oclp import JsonArtifact, computation, output_artifact_id
@computation(
name="Respond",
outputs={"response": JsonArtifact(name="Response")},
)
def respond() -> dict[str, object]:
return {"response": {"response_id": output_artifact_id("response")}}
Declare a release ArtifactSet across Computations¶
Use @artifact_set outside each real @computation that contributes to a
release. Matching names are resolved at successful observed-context completion.
No release function, Computation, Execution, or Event is fabricated.
from oclp import artifact_set, observe_run, run
@artifact_set(
name="Validated candidate model release",
output_port="model",
role="model",
)
@computation(...)
def train_model(...): ...
@artifact_set(
name="Validated candidate model release",
output_port="evaluation",
member_name="evaluation",
role="validation-report",
)
@computation(...)
def evaluate_candidate(...): ...
@artifact_set(
name="Validated candidate model release",
output_port="features",
role="training-data",
)
@computation(...)
def prepare_features(...): ...
@run(
name="Candidate training",
)
def train_candidate() -> None:
prepared = prepare_features(...)
candidate = train_model(...)
evaluate_candidate(candidate)
with observe_run(train_candidate, publisher=publisher, source=source) as observed:
train_candidate()
model_release = observed.artifact_set("Validated candidate model release")
Each output_port is the ArtifactSet member name by default; use
member_name only where two contributions would otherwise collide. The SDK
requires each declared member to materialize exactly once in the successful
run. It fails when a member is ambiguous; it does not attempt cross-process
aggregation or invent a selection policy.
Publish a dynamic ArtifactSet from exact handles¶
An ArtifactSet is a Core collection record, not a storage format with bytes to serialize and not necessarily the output of a Computation. Publishing a named collection of already-materialized Artifacts is an SDK operation, so it must not fabricate a function, Computation, Execution, or Event merely to make the graph look connected.
from oclp import OclpRun
with OclpRun(...) as observed:
model_release = observed.publish_artifact_set(
name="Validated candidate model release",
members={
"model": (model, "model"),
"evaluation": (evaluation, "validation-report"),
"features": (features, "training-data"),
},
materialize_manifest=True,
manifest_name="Validated candidate release manifest",
)
Each dictionary key is the stable ArtifactSet member name. Its value is a
two-item tuple of an exact ArtifactHandle and an optional semantic role:
dict[str, tuple[ArtifactHandle, str | None]]. The SDK publishes an immutable
ArtifactSet whose members contain those handles' UUID references and
returns an ArtifactSetHandle. Use this imperative API only when the member
list is inherently dynamic and cannot be declared on @computation or @run.
A direct ArtifactSet publication does not copy the Execution-only run
profile: its relationship to a run is derived from the real Artifacts it
collects. No member helper, record-ID construction, or application-owned
publisher call is required.
materialize_manifest=True asks the SDK to persist a deterministic
release-manifest.json sidecar Artifact. The application must supply its
manifest_name; the SDK returns its handle as model_release.manifest. It is
not an ArtifactSet member: the sidecar records the exact digest-bound set
reference through its release-manifest profile, and making it a member would
create a self-content cycle. The JSON
snapshot contains the exact ArtifactSet record plus
the resolved upstream OCLP record closure available in the local publisher:
their exact canonical Artifact, Execution, Computation, Evidence, Event, and
nested ArtifactSet records as applicable. It does not duplicate model or
dataset bytes.
Because the sidecar is outside the collection, its release-manifest profile
contains the final ArtifactSet UUID reference. load_release_manifest(path)
verifies the sidecar payload and returns an ArtifactSetHandle with verified
local handles for its members. The local publisher stores the sidecar under a collision-safe
release/.../release-manifest.json payload path.
Consume a release as one real ArtifactSet input¶
An inference or deployment Computation should consume the release collection,
not smuggle its identity through a string parameter while separately accepting
only the model file. artifact_set_input(...) records the ArtifactSet's exact
reference in Execution.inputs, validates the named members a callable needs,
and leaves the function in control of which verified members it materializes.
from catboost import CatBoostRegressor
from oclp import (
ArtifactSetHandle,
CatBoostModelArtifact,
JsonArtifact,
artifact_set_input,
computation,
)
@computation(
name="Predict demand",
inputs={
"model_release": artifact_set_input(
{
"model": CatBoostModelArtifact,
"feature-contract": JsonArtifact,
}
),
"request": JsonArtifact,
},
)
def predict(model_release: ArtifactSetHandle, request: dict[str, object]) -> float:
model = model_release.load_member("model", CatBoostRegressor)
contract = model_release.load_member("feature-contract", dict[str, object])
return float(model.predict(build_frame(request, contract))[0])
The produced Execution has a real model_release input pointing to the
immutable ArtifactSet. A graph viewer can therefore show the release handoff
without an application-specific naming convention.
Use a real @computation only when release creation itself does work—for
example, evaluating a promotion policy, signing a manifest, or publishing to a
model registry. That Computation may consume or produce other Artifacts; the
final named ArtifactSet remains a direct collection-publication operation.
Publish one execution and its Evidence¶
The optional LocalArtifactPublisher writes immutable payloads, records, and a
DuckDB catalog. Another store can implement the same record contract. Use its
explicit methods when the output has domain-specific naming, profiles,
membership, or serialization that should not be inferred from a function
return value.
from datetime import UTC, datetime
from pathlib import Path
from uuid import uuid4
from oclp import Execution, evaluate_evidence
from oclp.publishing import LocalArtifactPublisher
with LocalArtifactPublisher(
catalog_path=Path("data/oclp/catalog.duckdb"),
record_root=Path("data/oclp"),
payload_root=Path("data/runs/example-run"),
) as publisher:
computation_ref = publisher.publish(report_computation)
source = publisher.json_artifact(
name="Source document",
relative_path="source.json",
value={"title": " Report "},
created_at=datetime.now(UTC),
)
report_value = normalize_report({"title": " Report "})
result = publisher.json_artifact(
name="Normalized report",
relative_path="result.json",
value=report_value,
created_at=datetime.now(UTC),
)
execution = Execution(
computation=computation_ref,
inputs={"source": (source.reference,)},
outputs={"report": (result.reference,)},
)
execution_ref = publisher.publish(execution)
publisher.publish(
evaluate_evidence(
normalized_report_quality,
report_value,
id=str(uuid4()),
subject=execution_ref,
source=source_basis,
observed_at=datetime.now(UTC),
)
)
evaluate_evidence() calls the evaluator and records its exact source-bound
Implementation in the resulting Evidence. A terminal Event can claim
status="succeeded" only after every required evaluator has Evidence about the
exact Execution with outcome="pass".
The SDK evaluates every required evaluator by default, even when an earlier
gate returns "fail" or raises. An evaluator exception is published as
Evidence(outcome="error") with a portable Diagnostic; remaining gates still
run, and the terminal Execution is failed. This gives operators the complete
set of gate results in a single run. The protocol permits other implementation
strategies, but never permits a successful terminal status until all required
evaluators have passed.
Record chronology and validate¶
Publish execution Events explicitly. The optional run profile convention
uses execution-started at sequence 0 and an optional terminal
execution-terminal; it has no request event or nested attempt identity.
from oclp import Event, validate_execution_acceptance
publisher.publish(
Event(
execution=execution_ref,
event_type="execution-terminal",
occurred_at=datetime.now(UTC),
sequence=1,
status="succeeded",
)
)
validate_execution_acceptance(publisher.records())
For complete stores, also call validate_derivation_graph() and
validate_execution_hierarchy(). See the bike-demand example
for a multi-stage CatBoost run that uses these decorators, local Artifacts,
Evidence, execution Events, and MLflow correlation.