Tortoise ORM Planned¶
Planned — not implemented yet
open_fsm.contrib.tortoise does not exist. This page describes the API
being designed for it, so the shape can be reviewed before it ships.
Nothing on this page is executable, and no code block here is run by the
test suite.
For what works today, see Binding state to storage.
Same shape, one difference¶
Like the Django integration, this is a flow class wrapping a model
instance — the pattern
Viewflow uses and the one
open-fsm inherited. The model keeps a plain field and knows nothing about the
workflow.
The difference is that Tortoise is async. await instance.save() cannot be
called from @state.on_success(), because the hook is invoked synchronously
from inside the transition call — the whole
lifecycle is synchronous, by
design.
So this integration cannot supply the fourth method the Django one does. It has to answer: when does the row get written?
Proposed API¶
The transition stays synchronous — it decides, it does not persist — and saving
is an explicit await in the caller:
class Report(models.Model):
text = fields.TextField()
state_field = fields.CharEnumField(ReportState, default=ReportState.NEW)
from open_fsm.contrib.tortoise import ModelFlow, ModelState
class ReportFlow(ModelFlow):
state = ModelState(ReportState, default=ReportState.NEW, field='state_field')
@state.transition(source=ReportState.NEW, target=ReportState.APPROVED)
def approve(self):
"""Synchronous: it moves the state and nothing else."""
@state.transition(source=ReportState.NEW, target=ReportState.REJECTED)
def reject(self):
...
async def approve_report(pk):
report = await Report.get(pk=pk)
flow = ReportFlow(report)
flow.approve()
await flow.commit()
return report
ModelState would install the getter and setter over the field, and
deliberately not an on_success that saves. ModelFlow would supply
__init__ and an async def commit() that awaits
instance.save(update_fields=[field]) — so the await stays visible in the
caller, where an async framework expects it.
Under discussion¶
Opinions are welcome on the issue tracker.
commit()versus a bareawait instance.save(...). A helper keeps the field name in one place, but it is thin. It may not be worth a module.- Awaitable transitions. The alternative is
await flow.approve(), which reads better and would let anasync deftransition body do real work. It is also a second transition machine to maintain alongside the synchronous one, and it changes the core rather than wrapping it. - Async conditions. A condition that has to query is the common case in an async codebase, and a synchronous condition cannot await. Resolving the data before the transition and passing it in is the workaround; whether the library should support more than that is open.
- Forgetting to commit. Nothing warns you that
flow.approve()succeeded and the row was never written. A__del__warning, or makingcommit()the only way to read the new state, are both worse than the problem. - Whether this belongs here at all. The "works today" version below needs no library code. This integration may end up being a documentation page and no module — which would be the right outcome.
What to do today¶
This already works, with no library changes:
class ReportFlow(StateEngine):
state = State(ReportState, default=ReportState.NEW)
def __init__(self, report):
self.report = report
@state.getter()
def _get_state(self):
return self.report.state_field
@state.setter()
def _set_state(self, value):
self.report.state_field = value
@state.transition(source=ReportState.NEW, target=ReportState.APPROVED)
def approve(self):
...
async def approve_report(pk):
report = await Report.get(pk=pk)
flow = ReportFlow(report)
flow.approve()
await report.save(update_fields=['state_field'])
Load, decide synchronously, await the write. The machine never touches the event loop, which is the property that makes it safe to use from async code at all.
Related pages¶
- Binding state to storage — the pattern that works now
- Overview → Boundaries — why transitions are synchronous
- Django ORM and SQLAlchemy — the other planned integrations