DataPoints¶
DataPoints are a typed contract for what one unit produces and what another consumes. Instead of passing arbitrary values through the context, units declare their contributions and dependencies as typed specs, and the framework verifies them at runtime.
The pattern shines when a flow has several independent units that share data through the context — DataPoints catch missing/mismatched producers and consumers before they cause action-at-a-distance bugs.
The shape¶
BaseDataPointSpec[VALUE]— a typed key. Has anameand aref_type. Calling the spec creates a typed container:spec(value)returnsBaseDataPointContainer(spec, value).BaseDataPointContainer[VALUE]—(spec, value)pair; what flows through the context's storage.ProducesDataPoints— mixin for units that write datapoints. Declares_produces: set[Spec]. Useself.to(producer).add(spec(value), ...).ConsumesDataPoints— mixin for units that read datapoints. Declares_consumes: set[Spec]. Useself.out_of(consumer)[spec].InMemoryDataPointContext— aBaseContextthat doubles as producer + consumer; stores datapoints in a dict keyed by their spec.
A complete example¶
from dataclasses import dataclass
from pyuow import BaseParams, ConditionalUnit, ErrorUnit, FinalUnit, Result, RunUnit
from pyuow.context.datapoint.in_memory import InMemoryDataPointContext
from pyuow.datapoint import (
BaseDataPointSpec,
ConsumesDataPoints,
ProducesDataPoints,
)
# 1. Define the params and context
@dataclass(frozen=True)
class CheckoutParams(BaseParams):
user_id: str
cart_id: str
@dataclass(frozen=True)
class CheckoutCtx(InMemoryDataPointContext[CheckoutParams]):
params: CheckoutParams
# 2. Declare typed datapoint specs
CartTotal = BaseDataPointSpec("cart_total", int)
TaxAmount = BaseDataPointSpec("tax_amount", int)
# 3. A producer unit
class ComputeCartTotal(RunUnit[CheckoutCtx, str], ProducesDataPoints):
_produces = {CartTotal}
def run(self, ctx: CheckoutCtx) -> None:
total = self._lookup(ctx.params.cart_id)
self.to(ctx).add(CartTotal(total))
# 4. A consumer + producer unit
class ApplyTax(RunUnit[CheckoutCtx, str], ConsumesDataPoints, ProducesDataPoints):
_consumes = {CartTotal}
_produces = {TaxAmount}
def run(self, ctx: CheckoutCtx) -> None:
total = self.out_of(ctx)[CartTotal]
self.to(ctx).add(TaxAmount(total // 10))
# 5. A consumer terminal
class Summarise(FinalUnit[CheckoutCtx, str], ConsumesDataPoints):
_consumes = {CartTotal, TaxAmount}
def finish(self, ctx: CheckoutCtx) -> Result[str]:
dp = self.out_of(ctx)
return Result.ok(f"total={dp[CartTotal]} tax={dp[TaxAmount]}")
# 6. Compose
flow = (
ComputeCartTotal()
>> ApplyTax()
>> Summarise()
).build()
When the flow runs, the context's in-memory store collects datapoints as they're added. The framework checks at consume-time that every declared _consumes entry actually exists. If anything is missing, you get DataPointIsNotProducedError with the missing specs — never KeyError deep in business code.
What gets enforced¶
| Mistake | Exception |
|---|---|
| A consumer requests a spec no producer wrote | DataPointIsNotProducedError |
A producer writes a spec it did not declare in _produces |
DataPointIsNotDeclaredError |
| Two producers write the same spec to the same context | DataPointCannotBeOverriddenError |
| Wrong value type passed to a spec call | TypeError (at spec(value) time) |
The first two errors come from the ProducesDataPoints / ConsumesDataPoints mixins. The third comes from InMemoryDataPointContext rejecting duplicates. The fourth is the spec itself — it isinstance-checks value against ref_type.
Async variant¶
The async twins live under pyuow.context.datapoint.aio.in_memory and pyuow.datapoint.aio:
from pyuow.aio import RunUnit
from pyuow.context.datapoint.aio.in_memory import InMemoryDataPointContext
from pyuow.datapoint.aio import ConsumesDataPoints, ProducesDataPoints
class ComputeCartTotal(RunUnit[CheckoutCtx, str], ProducesDataPoints):
_produces = {CartTotal}
async def run(self, ctx: CheckoutCtx) -> None:
total = await self._lookup(ctx.params.cart_id)
await self.to(ctx).add(CartTotal(total))
class Summarise(FinalUnit[CheckoutCtx, str], ConsumesDataPoints):
_consumes = {CartTotal}
async def finish(self, ctx: CheckoutCtx) -> Result[str]:
dp = await self.out_of(ctx)
return Result.ok(f"total={dp[CartTotal]}")
The sync and async producer/consumer mixins have identical method names; the only difference is await.
When to use DataPoints¶
Use them when:
- A flow has 3+ units that share computed state.
- You want explicit contracts between units rather than ad-hoc context attributes.
- You catch missing/unused producer-consumer pairs at runtime before they cause incorrect behaviour deeper down.
Skip them when:
- The flow is short and the shared state is obvious from context attributes.
- The state is naturally tied to the params, not to intermediate computation.