Units & Flow¶
A Unit is the atomic building block of PyUoW. A Flow is a chain of units composed with the >> operator.
Every unit is generic over two type variables:
CONTEXT— the context type the unit operates on (aBaseContext[Params]).OUT— the final output type of the flow this unit belongs to.
The unit hierarchy¶
BaseUnit
│
FlowUnit
├── ConditionalUnit -- branches on a boolean
├── RunUnit -- side-effects, falls through
└── FinalUnit -- terminates with a Result
└── ErrorUnit -- pre-loaded with an exception
All four are exported from pyuow (sync) and pyuow.aio (async). The sync and async surface is identical except for async def.
ConditionalUnit¶
Branches on a boolean. Override condition(...) to return True (continue to the next unit) or False (call the on_failure unit).
from pyuow import ConditionalUnit, ErrorUnit
class IsAuthenticated(ConditionalUnit[Ctx, Out]):
def condition(self, ctx: Ctx) -> bool:
return ctx.user is not None
flow = (
IsAuthenticated(on_failure=ErrorUnit(exc=PermissionError("not signed in")))
>> NextUnit()
>> FinalUnit()
).build()
The on_failure slot is required at construction and must itself be a FlowUnit — typically an ErrorUnit or another terminal unit.
RunUnit¶
Does side-effects and falls through to the next unit unconditionally. Override run(...) — it returns None.
from pyuow import RunUnit
class SendEmail(RunUnit[Ctx, Out]):
def __init__(self, *, mailer: Mailer) -> None:
super().__init__()
self._mailer = mailer
def run(self, ctx: Ctx) -> None:
self._mailer.send(ctx.params.email, "Welcome!")
If run() raises, the surrounding unit catches the exception and returns Result.error(exc) — subsequent units are skipped.
FinalUnit¶
Terminates the flow. Override finish(...) to return the Result.
from pyuow import FinalUnit, Result
class Success(FinalUnit[Ctx, OrderConfirmation]):
def finish(self, ctx: Ctx) -> Result[OrderConfirmation]:
return Result.ok(OrderConfirmation(id=ctx.order_id))
A FinalUnit must be the last unit in a flow. Chaining anything after it raises FinalUnitError.
ErrorUnit¶
A pre-baked FinalUnit that always returns Result.error(exc). Useful as on_failure for a ConditionalUnit.
from pyuow import ErrorUnit
cond = MyCondition(on_failure=ErrorUnit(exc=ValueError("invalid input")))
Composing with >>¶
The __rshift__ operator chains units. The return value is the right-hand unit — chained calls work because every call returns the chained unit again. .build() returns the root of the chain ready to execute.
Build-time validation¶
.build() walks the chain and ensures it terminates in a FinalUnit. If a non-final unit has no successor, you get:
from pyuow import FlowNotTerminatedError
bad_flow = StepA().build()
# raises FlowNotTerminatedError: StepA
This catches "I forgot to add the terminal step" before any context is ever passed in.
Re-using units¶
A unit instance can be used in only one chain. Re-chaining the same instance into a second flow raises CannotReassignUnitError:
shared = StepA()
flow_a = shared >> Done() # OK
flow_b = OtherStep() >> shared # raises CannotReassignUnitError
Create a fresh instance per flow.
Async flow¶
Everything above has an async twin. Import from pyuow.aio:
from pyuow.aio import (
ConditionalUnit,
ErrorUnit,
FinalUnit,
RunUnit,
)
class CheckStock(ConditionalUnit[Ctx, Out]):
async def condition(self, ctx: Ctx) -> bool:
return await inventory.has(ctx.params.sku)
class Reserve(RunUnit[Ctx, Out]):
async def run(self, ctx: Ctx) -> None:
await inventory.reserve(ctx.params.sku)
class Confirm(FinalUnit[Ctx, Out]):
async def finish(self, ctx: Ctx) -> Result[Out]:
return Result.ok(Out(...))
flow = (
CheckStock(on_failure=ErrorUnit(exc=RuntimeError("no stock")))
>> Reserve()
>> Confirm()
).build()
result = await flow(context)
The operator chaining, .build() validation, and error semantics are identical to the sync version.
Reference¶
pyuow.unit— sync unitspyuow.aio— async units