API reference¶
Generated from the source. Everything below is exported from the package root:
from morphly import Config, Delete, Entity, Module, Patch, Put, Step, Store, Workflow, Write, module, view
Business objects¶
The types you subclass to describe your domain.
morphly.Entity
¶
Bases: BaseModel
A shared business object, identified by name within its type lineage.
Subclass it to declare a business object. Identity is the (type, name) pair, and
name is frozen: an entity keeps its identity for its whole life, only its other
fields change.
Two pydantic settings are enabled: validate_assignment, so every field write is
validated — including the ones a Patch applies — and
arbitrary_types_allowed, so a field may carry a non-pydantic object such as a
dataframe, a matrix or a solver handle.
Subclasses are a first-class case: a module reading list[Employee] also receives
every Manager in the store.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Stable identifier, unique within the type lineage. Frozen. |
Examples:
class Employee(Entity):
hourly_rate: float
gross: float = 0.0
class Manager(Employee):
bonus_target: float
morphly.Config
¶
Bases: BaseModel
A singleton input: module parameters, global settings, context.
A Config has no name because there is only ever one of each type in play. It is
resolved by type, either from the Step it is bound to or from the
Store.
Examples:
class PayrollPolicy(Config):
overtime_after: float = 35.0
overtime_rate: float = 1.25
social_rate: float = 0.22
Operations¶
What a module returns to declare what it changed.
morphly.Put
¶
Creation marker returned by a module. Annotate as Put[Payslip].
Optional sugar: returning a bare Entity or Config creates or replaces it just the
same. Put exists so a signature can spell out the three verbs — Put[Payslip] |
Patch[Employee] | Delete[Timesheet] — instead of leaving creation implicit in a bare
type sitting in a union.
Like Patch and Delete, it is an intent: nothing
is written until the step has produced all of its operations and they have all been
validated. The object replaces anything stored under the same (type, name).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
E
|
The entity or config to store. |
required |
Examples:
@module
def issue(employees: list[Employee]) -> list[Put[Payslip]]:
return [Put(Payslip(name=e.name, net=e.gross)) for e in employees]
morphly.Patch
¶
Partial update returned by a module. Annotate as Patch[Employee].
A Patch writes only the fields it is given and leaves the rest of the object
untouched — the normal output of a module that computes a few attributes on shared
objects. Returning a whole Entity, bare or inside a Put, means
creation or full replacement instead.
Like Delete, it is an intent: nothing is written until the step
has produced all of its operations and they have all been validated.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
E
|
The entity to update, resolved by type lineage and |
required |
**fields
|
Any
|
Field names and values to write. At least one is required. |
{}
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If no field is given. |
Examples:
@module
def pay(employees: list[Employee]) -> list[Patch[Employee]]:
return [Patch(e, gross=e.hourly_rate * e.contract_hours) for e in employees]
morphly.Delete
¶
Deletion marker returned by a module. Annotate as Delete[Timesheet].
Returning a Delete does not remove anything by itself: it is an intent, applied by
the workflow once the whole step has been validated.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
E
|
The entity to remove. Only its type lineage and |
required |
Examples:
@module
def archive(timesheets: list[Timesheet]) -> list[Delete[Timesheet]]:
return [Delete(t) for t in timesheets if t.processed]
morphly.view
¶
Build an enriched module-local view of a shared object.
Shallow-copies the source fields, adds extra, and validates against cls. Use it
when a module needs to carry its own working fields on a shared entity without
polluting the shared type with them.
The view can be handed straight back inside a Patch: targets are resolved against
the type declared in the return annotation, not the view's own class.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cls
|
type[T]
|
Target model class, usually a subclass of the source class. |
required |
source
|
BaseModel
|
Object to project. |
required |
**extra
|
Any
|
Module-specific fields to add. |
{}
|
Returns:
| Type | Description |
|---|---|
T
|
A new |
Raises:
| Type | Description |
|---|---|
ValidationError
|
If the merged fields do not validate against |
Examples:
class EmployeeWithHours(Employee):
worked: float
@module
def pay(employees: list[Employee], sheets: list[Timesheet]) -> list[Patch[Employee]]:
hours = {s.employee: s.hours for s in sheets}
rich = [view(EmployeeWithHours, e, worked=hours.get(e.name, 0.0)) for e in employees]
return [Patch(e, gross=e.hourly_rate * e.worked) for e in rich]
State¶
morphly.Store
¶
The shared state. Buckets by concrete type, read by type (subclasses included).
Entities are keyed by (concrete type, name) and configs by type. Reading is done
by type and walks the lineage, so all(Employee) returns the Manager instances
too.
The store is a plain Python object: copy.deepcopy(store) is a snapshot and
pickle persists it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*items
|
Entity | Config
|
Entities and configs to load. |
()
|
Examples:
store = Store(
Employee(name="ada", hourly_rate=50.0, contract_hours=35.0),
Manager(name="bob", hourly_rate=60.0, contract_hours=35.0, bonus_target=0.1),
PayrollPolicy(social_rate=0.22),
)
store.all(Employee) # [Employee(name='ada'), Manager(name='bob')]
store.one(PayrollPolicy).social_rate # 0.22
put
¶
Upsert entities (keyed by type and name) and configs (keyed by type).
An entity replaces any object already stored under the same (type, name). For
a partial update, use patch instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*items
|
Entity | Config
|
Entities and configs to store. |
()
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If an item is neither an |
all
¶
Every instance of cls and of its subclasses.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cls
|
type[E]
|
The entity type to read. |
required |
Returns:
| Type | Description |
|---|---|
list[E]
|
The matching entities, in insertion order per bucket. Empty if none. |
one
¶
The single instance of config cls.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cls
|
type[C]
|
The config type to read. |
required |
Returns:
| Type | Description |
|---|---|
C
|
The stored config. |
Raises:
| Type | Description |
|---|---|
LookupError
|
If the store holds zero or several configs of that lineage. |
find
¶
The object named name: exact type first, then the lineage of cls.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cls
|
type[E]
|
The declared type of the object. |
required |
name
|
str
|
Its identifier. |
required |
Returns:
| Type | Description |
|---|---|
E
|
The stored entity. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If no object matches, or if several sibling types hold that name. |
drop
¶
Remove the stored object matching target.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
Entity | type[Entity]
|
An entity with the type lineage and |
required |
name
|
str | None
|
The object's name, when |
None
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
KeyError
|
If no object matches, or if the name is ambiguous. |
patch
¶
patch(
target: Entity | type[Entity],
name_or_fields: str | dict[str, Any],
fields: dict[str, Any] | None = None,
) -> None
Write fields onto the stored object matching target.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
Entity | type[Entity]
|
An entity with the type lineage and |
required |
name_or_fields
|
str | dict[str, Any]
|
Field names and values to write, when |
required |
fields
|
dict[str, Any] | None
|
Field names and values to write, when |
None
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
KeyError
|
If no object matches, or if the name is ambiguous. |
ValueError
|
If a name is not a field of the stored object. |
ValidationError
|
If a value does not validate. |
history
¶
Every write a workflow made to that object, oldest first.
The runtime counterpart of Workflow.to_mermaid:
the graph says which steps may write a type, the history says which ones did.
Recorded by Workflow.run, so a write made by calling
put/patch/drop directly leaves no entry, and configs are not tracked — they
have no name to key them by.
The log outlives the object: a deleted entity keeps its history, ending with its
delete. Reads follow the lineage like all rather than
find, so an ambiguous name returns the writes of every
matching sibling type instead of raising.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
Entity | type[Entity]
|
An entity with the type lineage and |
required |
name
|
str | None
|
The object's name, when |
None
|
Returns:
| Type | Description |
|---|---|
list[Write]
|
The recorded writes. Empty if the object was never written by a workflow. |
Examples:
types
¶
Types currently present, entities and configs.
Returns:
| Type | Description |
|---|---|
set[type]
|
The concrete types held by the store. This is the starting point of |
set[type]
|
morphly.Write
¶
Bases: NamedTuple
One recorded write, as returned by history.
Attributes:
| Name | Type | Description |
|---|---|---|
step |
str
|
Name of the step that wrote. |
action |
str
|
|
fields |
tuple[str, ...]
|
The fields a |
Orchestration¶
morphly.module
¶
Modules and steps: the function signature is the contract.
Module
¶
A function whose annotations declare what it reads, creates and touches.
Built by the @module decorator; you rarely instantiate it
yourself. The annotations are parsed once, at import time, and an unsupported or
missing one fails there rather than mid-run.
| Annotation | Injected |
|---|---|
list[X], X: Entity |
store.all(X), subclasses included |
X, X: Config |
the step's config, else store.one(X) |
The return annotation forms the output contract: Entity and Config types, bare or
inside Put[...], are produced, types inside Patch[...] and Delete[...] are
touched, and None means the module writes nothing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fn
|
Callable[..., Any]
|
The annotated function to wrap. |
required |
Attributes:
| Name | Type | Description |
|---|---|---|
fn |
The wrapped function. |
|
name |
The function's name, used as the default step name. |
|
reads |
list[tuple[str, bool, type]]
|
One |
produces |
list[tuple[str, bool, type]]
|
Entity and config types the module creates or replaces. |
touches |
list[tuple[str, bool, type]]
|
Entity types the module patches or deletes. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If a parameter is unannotated, if an annotation is not
|
__call__
¶
Run the function, returning its operations without applying them.
Useful on its own to unit-test a module: build a Store, call the module, and
assert on the operations it returns — nothing is written.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store
|
Store
|
The state to read from. |
required |
configs
|
tuple[Config, ...]
|
Configs bound to this step, tried before |
()
|
copy_inputs
|
bool
|
Deep-copy every injected value, so the module cannot corrupt the state read by another one. Turn off only when volume demands it. |
True
|
Returns:
| Type | Description |
|---|---|
list[_Op]
|
The operations the module returned, validated against its contract but not |
list[_Op]
|
yet applied. Empty if the module returned |
Raises:
| Type | Description |
|---|---|
TypeError
|
If the module returned an operation its signature does not declare. |
LookupError
|
If a required config is neither bound to the step nor in the store. |
Step
¶
A module plus the configs bound to that occurrence of it.
Configs live on the step, not on the module, so the same module can appear twice in
a workflow with different parameters. A config bound here wins over the one in the
Store.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
module_
|
Module | Callable[..., Any]
|
A |
required |
*configs
|
Config
|
Configs bound to this occurrence. |
()
|
name
|
str | None
|
Step name for |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
module |
The wrapped module. |
|
configs |
The configs bound to this step. |
|
name |
The step name. |
Examples:
Workflow(
Step(withhold, PayrollPolicy(social_rate=0.22), name="withhold_fr"),
Step(withhold, PayrollPolicy(social_rate=0.13), name="withhold_uk"),
)
module
¶
module(fn: Callable[..., Any]) -> Module
Turn an annotated function into a module.
There is no base class to inherit from and nothing to register: the decorator reads the annotations and that is the whole contract. Errors in the contract are raised at import time.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fn
|
Callable[..., Any]
|
A function whose parameters are annotated |
required |
Returns:
| Type | Description |
|---|---|
Module
|
A |
Module
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If the signature does not form a valid contract. |
Examples:
morphly.Module
¶
A function whose annotations declare what it reads, creates and touches.
Built by the @module decorator; you rarely instantiate it
yourself. The annotations are parsed once, at import time, and an unsupported or
missing one fails there rather than mid-run.
| Annotation | Injected |
|---|---|
list[X], X: Entity |
store.all(X), subclasses included |
X, X: Config |
the step's config, else store.one(X) |
The return annotation forms the output contract: Entity and Config types, bare or
inside Put[...], are produced, types inside Patch[...] and Delete[...] are
touched, and None means the module writes nothing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fn
|
Callable[..., Any]
|
The annotated function to wrap. |
required |
Attributes:
| Name | Type | Description |
|---|---|---|
fn |
The wrapped function. |
|
name |
The function's name, used as the default step name. |
|
reads |
list[tuple[str, bool, type]]
|
One |
produces |
list[tuple[str, bool, type]]
|
Entity and config types the module creates or replaces. |
touches |
list[tuple[str, bool, type]]
|
Entity types the module patches or deletes. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If a parameter is unannotated, if an annotation is not
|
__call__
¶
Run the function, returning its operations without applying them.
Useful on its own to unit-test a module: build a Store, call the module, and
assert on the operations it returns — nothing is written.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store
|
Store
|
The state to read from. |
required |
configs
|
tuple[Config, ...]
|
Configs bound to this step, tried before |
()
|
copy_inputs
|
bool
|
Deep-copy every injected value, so the module cannot corrupt the state read by another one. Turn off only when volume demands it. |
True
|
Returns:
| Type | Description |
|---|---|
list[_Op]
|
The operations the module returned, validated against its contract but not |
list[_Op]
|
yet applied. Empty if the module returned |
Raises:
| Type | Description |
|---|---|
TypeError
|
If the module returned an operation its signature does not declare. |
LookupError
|
If a required config is neither bound to the step nor in the store. |
morphly.Step
¶
A module plus the configs bound to that occurrence of it.
Configs live on the step, not on the module, so the same module can appear twice in
a workflow with different parameters. A config bound here wins over the one in the
Store.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
module_
|
Module | Callable[..., Any]
|
A |
required |
*configs
|
Config
|
Configs bound to this occurrence. |
()
|
name
|
str | None
|
Step name for |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
module |
The wrapped module. |
|
configs |
The configs bound to this step. |
|
name |
The step name. |
Examples:
Workflow(
Step(withhold, PayrollPolicy(social_rate=0.22), name="withhold_fr"),
Step(withhold, PayrollPolicy(social_rate=0.13), name="withhold_uk"),
)
morphly.Workflow
¶
An ordered list of steps, checked before it runs.
The order is yours: a workflow is a list, not a scheduler. What it does guarantee is that an inconsistent order — a step reading a type nobody upstream provides — fails in a millisecond instead of three hours in.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*steps
|
Step | Module | Callable[..., Any]
|
|
()
|
Attributes:
| Name | Type | Description |
|---|---|---|
steps |
The steps, in order, with duplicate names suffixed ( |
|
last_run |
_RunCache | None
|
Cache of the most recent |
Examples:
workflow = Workflow(
Step(compute_gross, PayrollPolicy(overtime_after=35.0)),
withhold,
archive_timesheets,
)
workflow.run(store)
check
¶
check(store: Store) -> None
Validate the chaining on types only, without running anything.
Replays the workflow on types: it starts from store.types() and adds, after
each step, the types that step produces. A step reading or touching a type that
is neither in the initial store nor produced upstream is an error.
Called automatically by run; call it yourself to fail
at startup, before loading any data.
A type that is produced, but by a step further down the list, is reported as an ordering problem, with the position to move: the workflow is complete, only its order is wrong, and that is the far more common mistake.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store
|
Store
|
The state the workflow would run on. Only its types are read. |
required |
Raises:
| Type | Description |
|---|---|
LookupError
|
If a step reads or touches a type nobody provides. |
run
¶
run(
store: Store,
*,
copy_inputs: bool = True,
on_step: Callable[[Step, list[_Op], Store], None]
| None = None,
reuse: _RunCache | None = None,
record: bool = False,
atomic: bool = False,
) -> Store
Check, then run every step in order and apply its output.
A step's operations are collected and validated before any of them is written,
so a step never applies halfway. The workflow as a whole is transactional only
with atomic=True.
An exception raised by a module, or while applying its output, is re-raised unchanged with a note naming the step, its position and the state of the store — the context you want when step 12 of 40 fails three hours into a batch.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store
|
Store
|
The state to run on. Mutated in place. |
required |
copy_inputs
|
bool
|
Deep-copy the values injected into each module. Turning this off drops the isolation guarantee — a module mutating an input then affects the shared state. |
True
|
on_step
|
Callable[[Step, list[_Op], Store], None] | None
|
Called as |
None
|
reuse
|
_RunCache | None
|
A previous [ |
None
|
record
|
bool
|
Populate [ |
False
|
atomic
|
bool
|
Restore the store to its exact initial state if any step raises, making the whole run all-or-nothing instead of just each step. Costs one deep copy of the store, taken once before the first step. |
False
|
Returns:
| Type | Description |
|---|---|
Store
|
The same |
Raises:
| Type | Description |
|---|---|
LookupError
|
If |
TypeError
|
If a module returns an operation it did not declare. |
KeyError
|
If a |
ValueError
|
If a |
ValidationError
|
If a written value does not validate. |
explain
¶
One line per step: reads(), then produced and ~touched types.
Returns:
| Type | Description |
|---|---|
str
|
A rendering of the workflow's dataflow, for logs and reviews. Touched types |
str
|
are prefixed with |
Examples:
to_mermaid
¶
Render the workflow's dataflow as a Mermaid flowchart.
Same information as explain, as a graph instead of
a line per step: every edge is a reads/produces/touches relationship
already present in the modules' signatures, so the graph cannot go stale.
Returns:
| Type | Description |
|---|---|
str
|
A |
str
|
for produced types, |
Examples: