Metadata-Version: 2.4
Name: open-fsm
Version: 1.0.0
Summary: An open, lightweight, ORM-agnostic finite state machine for Python.
Keywords: fsm,finite-state-machine,state-machine,workflow,transitions,viewflow
Author: Izcar J. Munoz Torrez
Author-email: Izcar J. Munoz Torrez <izcarmt95@gmail.com>
License-Expression: AGPL-3.0-or-later
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Project-URL: Homepage, https://open-byte.github.io/open-fsm/
Project-URL: Documentation, https://open-byte.github.io/open-fsm/
Project-URL: Repository, https://github.com/open-byte/open-fsm
Project-URL: Issues, https://github.com/open-byte/open-fsm/issues
Description-Content-Type: text/markdown

# Open Finite State Machine (open-fsm)

An open, lightweight, ORM-agnostic finite state machine for Python.

> [!IMPORTANT]
> `open-fsm` is a fork of the finite state machine that lives inside [Viewflow](https://github.com/viewflow/viewflow).
>
> Big thanks to Mikhail Podgurskiy for writing it and maintaining it for so many years.
>
> Viewflow is a full workflow framework, built around Django. Of all of it, the FSM is the only part many projects actually need — and taking it means taking the framework along with it.
>
> `open-fsm` extracts exactly that piece and nothing else: the same declarative API, on a plain Python class, with any ORM or none at all. Fully typed, zero dependencies.

## Documentation

**[open-byte.github.io/open-fsm](https://open-byte.github.io/open-fsm/)** — guides, reference and a worked tutorial. Every example on the site is executed by the test suite.

| | |
| --- | --- |
| [Getting Started](https://open-byte.github.io/open-fsm/getting-started/) | Build a machine from an empty file |
| [Transitions](https://open-byte.github.io/open-fsm/guides/transitions/) | Sources, targets, wildcards and labels |
| [Conditions](https://open-byte.github.io/open-fsm/guides/conditions/) | Refusals that explain themselves |
| [Dataclasses](https://open-byte.github.io/open-fsm/guides/dataclasses/) | Declaring the field on a generated class |
| [Binding state to storage](https://open-byte.github.io/open-fsm/guides/persistence/) | Keeping the state in a database row |
| [Errors](https://open-byte.github.io/open-fsm/reference/errors/) | Every exception and what triggers it |

Runnable versions of the examples live in [`examples/README.md`](examples/README.md).

## Quick start

Declare a `State` field on any plain class, then mark the methods that move between states:

```python
from enum import Enum

from open_fsm import State, StateEngine


class ReviewState(str, Enum):
    DRAFT = "DRAFT"
    IN_REVIEW = "IN_REVIEW"
    APPROVED = "APPROVED"
    REJECTED = "REJECTED"
    PUBLISHED = "PUBLISHED"
    ARCHIVED = "ARCHIVED"


class Article(StateEngine):
    state = State(ReviewState, default=ReviewState.DRAFT)

    def __init__(self, title, body=""):
        self.title = title
        self.body = body

    @state.transition(
        source=ReviewState.DRAFT,
        target=ReviewState.IN_REVIEW,
        conditions=[lambda article: bool(article.body)],
    )
    def submit(self):
        ...

    @state.transition(source=ReviewState.IN_REVIEW, target=ReviewState.APPROVED)
    def approve(self):
        ...

    @state.transition(source=ReviewState.IN_REVIEW, target=ReviewState.REJECTED)
    def reject(self):
        ...

    @state.transition(source=ReviewState.APPROVED, target=ReviewState.PUBLISHED)
    def publish(self):
        ...

    @state.transition(source=State.ANY, target=ReviewState.ARCHIVED)
    def archive(self):
        ...
```

Five transitions. `submit`, `approve`, `reject` and `publish` each move between two specific states; `archive` is declared with `State.ANY`, so it is reachable from anywhere.

Calling a transition method runs its body and moves the state:

```python
article = Article("Hello", body="...")
article.state                   # ReviewState.DRAFT

article.submit()
article.approve()
article.publish()
article.state                   # ReviewState.PUBLISHED
```

A transition that does not exist from the current state is refused, and the state is never assignable by hand:

```python
article.publish.can_proceed()   # False
article.publish()               # NoTransition: Publish :: no transition from "PUBLISHED"

article.state = ReviewState.DRAFT
# AttributeError: Direct state modification is not allowed
```

### Asking what is possible

Inheriting `StateEngine` gives every instance three introspection methods:

```python
draft = Article("No body yet")

[t.slug for t in draft.get_outgoing_transitions()]    # ['archive', 'submit']
[t.slug for t in draft.get_available_transitions()]   # ['archive']
```

`submit` leaves `DRAFT`, so it is *outgoing* — but its condition (a non-empty body) is unmet, so it is not *available*. `get_transitions()` returns the whole machine, regardless of the current state.

The same three exist as module-level functions, for classes that cannot inherit the mixin. Those also take a state, so you can ask about one the flow is not in:

```python
from open_fsm import get_outgoing_transitions

[t.slug for t in get_outgoing_transitions(draft, ReviewState.IN_REVIEW)]
# ['approve', 'archive', 'reject']
```

## Features

* ORM-agnostic
* Lightweight and reusable
* Framework-independent core
* Designed to support multiple ORMs
* Fully typed, ships a PEP 561 `py.typed` marker
* Based on Viewflow's FSM implementation

## ORM Integrations

Binding a machine to stored state works today with `@state.getter()`, `@state.setter()` and `@state.on_success()` — see [Binding state to storage](https://open-byte.github.io/open-fsm/guides/persistence/).

Dedicated wrappers are the next thing to build. Their proposed APIs are written up so the shape can be reviewed before they ship:

* [Django ORM](https://open-byte.github.io/open-fsm/integrations/django/) — planned
* [Tortoise ORM](https://open-byte.github.io/open-fsm/integrations/tortoise/) — planned
* [SQLAlchemy](https://open-byte.github.io/open-fsm/integrations/sqlalchemy/) — planned

## License

This project is licensed under the **GNU Affero General Public License v3.0 (AGPL-3.0)**.

See [LICENSE](LICENSE) for the full license text.

## Attribution

This project is based on and derived from the FSM implementation originally developed as part of Viewflow.
