Metadata-Version: 2.4
Name: stateforge
Version: 0.1.1
Summary: Type-safe async state machines for Python
Project-URL: Homepage, https://github.com/desertaxle/stateforge
Project-URL: Source, https://github.com/desertaxle/stateforge
Author-email: Alexander Streed <ajstreed1@gmail.com>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: async,fsm,state machine,typing
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
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: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: typing-extensions>=4.4; python_version < '3.13'
Description-Content-Type: text/markdown

# stateforge

[![PyPI version](https://img.shields.io/pypi/v/stateforge)](https://pypi.org/project/stateforge/)
[![Python versions](https://img.shields.io/pypi/pyversions/stateforge)](https://pypi.org/project/stateforge/)
[![CI](https://github.com/desertaxle/stateforge/actions/workflows/ci.yml/badge.svg)](https://github.com/desertaxle/stateforge/actions/workflows/ci.yml)
[![Docs](https://img.shields.io/badge/docs-stateforge-blue)](https://desertaxle.github.io/stateforge/)

Type-safe async state machines for Python.

## Installation

```bash
pip install stateforge
```

## Quick Start

```python
import asyncio
from enum import Enum, auto
from stateforge import StateMachine, transition

class State(Enum):
    GREEN = auto()
    YELLOW = auto()
    RED = auto()

class Event(Enum):
    TIMER = auto()

class TrafficLight(StateMachine[State, Event]):
    initial_state = State.GREEN

    @transition(from_=State.GREEN, on=Event.TIMER, to=State.YELLOW)
    async def green_to_yellow(self) -> None:
        pass

    @transition(from_=State.YELLOW, on=Event.TIMER, to=State.RED)
    async def yellow_to_red(self) -> None:
        pass

    @transition(from_=State.RED, on=Event.TIMER, to=State.GREEN)
    async def red_to_green(self) -> None:
        pass

async def main() -> None:
    light = TrafficLight()
    await light.send(Event.TIMER)  # GREEN -> YELLOW
    await light.send(Event.TIMER)  # YELLOW -> RED
    await light.send(Event.TIMER)  # RED -> GREEN
    print(light.state)             # State.GREEN

asyncio.run(main())
```

## Features

### Guards

Attach async guard functions to transitions to conditionally allow or block state changes.

```python
from __future__ import annotations
from stateforge import StateMachine, transition

async def payment_authorized(machine: OrderMachine) -> bool:
    return machine.context is not None and machine.context.amount > 0

class OrderMachine(StateMachine[OrderState, OrderEvent, OrderContext]):
    initial_state = OrderState.PENDING

    @transition(
        from_=OrderState.PENDING,
        on=OrderEvent.CHARGE,
        to=OrderState.PAID,
        guard=payment_authorized,
    )
    async def charge(self) -> None:
        pass
```

### Typed Context

Attach immutable, typed context to your machine using a frozen dataclass as the third generic parameter.

```python
import dataclasses
from dataclasses import dataclass
from stateforge import StateMachine, transition

@dataclass(frozen=True)
class OrderContext:
    order_id: str = ""
    total: float = 0.0

class OrderMachine(StateMachine[OrderState, OrderEvent, OrderContext]):
    initial_state = OrderState.PENDING
    initial_context = OrderContext()

    @transition(from_=OrderState.PENDING, on=OrderEvent.CONFIRM, to=OrderState.CONFIRMED)
    async def confirm(self) -> OrderContext:
        assert self.context is not None
        return dataclasses.replace(self.context, order_id="ORD-001")
```

### Lifecycle Hooks

Override `on_enter_state`, `on_exit_state`, or `on_transition` to run logic around every state change.

```python
from stateforge import StateMachine, transition

class OrderMachine(StateMachine[OrderState, OrderEvent]):
    async def on_enter_state(self, state: OrderState) -> None:
        print(f"Entering {state.name}")

    async def on_exit_state(self, state: OrderState) -> None:
        print(f"Exiting {state.name}")

    async def on_transition(
        self, from_state: OrderState, event: OrderEvent, to_state: OrderState
    ) -> None:
        print(f"{from_state.name} --[{event.name}]--> {to_state.name}")
```

### Event Payloads

Pass keyword arguments to `send()` and they are forwarded directly to the transition handler.

```python
import dataclasses
from stateforge import StateMachine, transition

class OrderMachine(StateMachine[OrderState, OrderEvent, OrderContext]):
    @transition(from_=OrderState.PENDING, on=OrderEvent.CONFIRM, to=OrderState.CONFIRMED)
    async def confirm(self, confirmed_by: str = "system") -> OrderContext:
        assert self.context is not None
        return dataclasses.replace(self.context, confirmed_by=confirmed_by)

# Usage:
# await machine.send(OrderEvent.CONFIRM, confirmed_by="alice@example.com")
```

## Examples

| Example | Features | Run |
|---------|----------|-----|
| [order_fulfillment.py](examples/order_fulfillment.py) | typed context, lifecycle hooks, event payloads | `uv run python examples/order_fulfillment.py` |
| [connection_protocol.py](examples/connection_protocol.py) | guards, ANY sentinel, multi-source transitions | `uv run python examples/connection_protocol.py` |
| [game_character.py](examples/game_character.py) | fast transitions, payload-driven state | `uv run python examples/game_character.py` |
| [ci_pipeline.py](examples/ci_pipeline.py) | sequential stages, guard conditions, on_transition | `uv run python examples/ci_pipeline.py` |
| [kitchen_sink.py](examples/kitchen_sink.py) | all features combined | `uv run python examples/kitchen_sink.py` |

**LLM agent access:** [`/llms.txt`](https://desertaxle.github.io/stateforge/llms.txt) -- structured index | [`/llms-full.txt`](https://desertaxle.github.io/stateforge/llms-full.txt) -- full docs in one request

## License

Apache 2.0 — see [LICENSE](https://github.com/desertaxle/stateforge/blob/main/LICENSE).
