Skip to content

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:

models.py — proposed, not yet available
class Report(models.Model):
    text = fields.TextField()
    state_field = fields.CharEnumField(ReportState, default=ReportState.NEW)
flows.py — proposed, not yet available
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):
        ...
handlers.py — proposed, not yet available
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 bare await 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 an async def transition 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 making commit() 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:

flows.py — works today
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.