from dascore.workflow import Task
class Scale(Task):
"""Multiply patch data by a factor."""
factor: float = 1.0
def run(self, patch):
"""Scale a patch."""
return patch.update(data=patch.data * self.factor)
scale = Scale(factor=2)Workflow
A workflow records processing as objects rather than only as a script. Its basic unit is an immutable Task: one operation and its parameters. DASCore records tasks and fingerprints; graph construction and scheduling belong to tools such as Dask and Ray.
The workflow module is new. Patch functions expose task objects, but DASCore processing does not yet route through a workflow graph.
Tasks
Subclass Task, declare operation parameters as fields, and implement run:
Task parameters are frozen. Arrays are marked read-only without being copied, so copy a buffer before passing it if other code will continue writing to it.
Because parameters are the model fields, task equality describes the operation rather than object identity. Calling a task supplies only its runtime inputs; stored parameters remain unchanged.
Fingerprints
Each task has a deterministic fingerprint derived from its package, class, version, and parameters. Equivalent tasks compare equal and share a fingerprint.
assert Scale(factor=2) == Scale(factor=2)
assert Scale(factor=2).fingerprint == scale.fingerprintSet a task class’s __version__ when the same parameters acquire different behavior. Module paths and function source are not fingerprinted, so moving code within a package does not change existing identities.
Fingerprints are stable across processes and machines for supported parameter types. Packages are responsible for increasing __version__ when behavior changes without a parameter change.
Tasks from functions
The task decorator converts a function signature into a task class. Leading parameters are runtime inputs; remaining parameters become stored fields.
from dascore.workflow import task
@task
def scale_number(number, factor=1):
"""Scale a number."""
return number * factor
operation = scale_number(factor=3)
assert operation.run(2) == 6
assert operation(2) == 6Use inputs= when a function takes zero or multiple runtime inputs.
@task(inputs=2)
def add_numbers(left, right, offset=0):
"""Add two runtime inputs and a stored offset."""
return left + right + offset
assert add_numbers(offset=1)(2, 3) == 6Patch operations
Functions decorated with dc.patch_function expose .op(...), which binds arguments and defaults into a PatchOp.
import dascore as dc
patch = dc.get_example_patch()
normalize = dc.proc.normalize.op("time")
assert normalize(patch).equals(patch.normalize("time"))
print(normalize.fingerprint)0ff4a26903e46565
Registry names distinguish DASCore operations from similarly named plugin operations. fingerprint_call computes the same identity without constructing an operation. Ordinary patch calls do not fingerprint arguments unless provenance recording needs the operation.
from dascore.workflow import fingerprint_call
assert fingerprint_call(dc.proc.normalize, (), {"dim": "time"}) == normalize.fingerprint
print(normalize.to_dict()){'object_type': 'Normalize', 'version': '1.0', 'params': {'dim': 'time', 'norm': 'l2', 'window': None, 'samples': {'$bool': False}}}
Advanced operations may use PatchProcessor to separate metadata derivation from the array kernel or register kernels for other array libraries; these details are documented in the workflow API.
Separating derive_meta from kernel lets planners inspect coordinate and metadata changes without touching an array. Registered kernels can then execute the same named operation on another array implementation without changing its fingerprint.
Saving and loading
Tasks serialize to JSON or YAML and load through registered task names. Loading does not import arbitrary paths from the file, so the package defining a custom task must already be imported.
import tempfile
from pathlib import Path
path = Path(tempfile.mkdtemp()) / "operation.yaml"
normalize.save(path)
loaded = Task.load(path)
assert loaded == normalize.yaml and .yml select YAML; .json and suffixless paths select JSON. Unsupported suffixes are rejected rather than guessed. Custom task classes must live at module level under a unique package registry name.
Patch provenance
Tasks describe operations; Patch.attrs records their effect on data. patch_id identifies source data, while processing_id incorporates each operation fingerprint. See Patch identity for persistence and lookup rules.
Workflow objects do not schedule execution. Use Spool.map, Dask, Ray, or another executor to run operations over many patches.
This boundary keeps saved operations portable: orchestration tools decide when and where work runs, while DASCore provides stable operation and patch identities.