Skip to main content

A flexible state machine framework for Python

Project description

State Machine Framework

A flexible state machine framework for Python with clean abstractions, ORM integration support, and type-safe workflow management.

Features

  • Clean State Definitions: Minimal boilerplate with decorator-based validators and hooks
  • Integration Agnostic: Works with any ORM via the adapter pattern
  • Type Safety: Pydantic schema validation and type-checked workflow contexts
  • External Registration: Register validators and hooks in separate modules for clean code organization
  • Workflow Context: Type-safe context management with automatic validation

Installation

pip install state-machine-framework

Quick Start

1. Define Your Models

from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Order(Base):
    __tablename__ = 'orders'

    id = Column(Integer, primary_key=True)
    order_id = Column(String(100), unique=True)
    status = Column(String(50))

2. Define a State Machine

from state_machine_framework import StateMachine

class OrderStateMachine(StateMachine):
    pass

OrderStateMachine.register(
    Order,
    state_field='status',
    identifier_field='order_id'
)

3. Define States

States declare which model field values they represent via model_states, and which states they can transition to via allowed_transitions.

from state_machine_framework import State

class OrderPending(State):
    _state_machine = OrderStateMachine
    is_start_state = True
    allowed_transitions = []  # filled in after all states are defined

    model_states = {Order: 'pending'}

class OrderProcessing(State):
    _state_machine = OrderStateMachine
    model_states = {Order: 'processing'}

class OrderCompleted(State):
    _state_machine = OrderStateMachine
    is_terminal_state = True
    model_states = {Order: 'completed'}

# Wire up the transition graph
OrderPending.allowed_transitions = [OrderProcessing]
OrderProcessing.allowed_transitions = [OrderCompleted]

4. Add Validators

Validators run after pre-transition hooks and before the state update. They should only raise exceptions — never mutate data.

from state_machine_framework import validator

# External registration (requires _state_machine to be set on the state)
@validator(OrderPending, order=1)
def validate_order_amount(state, data, context):
    if data.get('Order', {}).get('amount', 0) <= 0:
        raise ValueError("Order amount must be positive")
    return {'validated': True}

# Inline registration (inside the State class)
class OrderPending(State):
    @validator(order=1)
    def validate_order_amount(self, data, context):
        if data.get('Order', {}).get('amount', 0) <= 0:
            raise ValueError("Order amount must be positive")
        return {'validated': True}

5. Add Hooks

Hooks run after the transition completes. Use them for side effects such as sending notifications or updating external systems.

from state_machine_framework import hook

# External registration
@hook(OrderProcessing, order=1)
def send_confirmation_email(state, instances, context):
    order = instances['Order']
    send_email(order.customer_email)
    return {'email_sent': True}

# Inline registration
class OrderProcessing(State):
    @hook(order=1)
    def send_confirmation_email(self, instances, context):
        order = instances['Order']
        send_email(order.customer_email)
        return {'email_sent': True}

6. Add Pre-Transition Hooks

Pre-transition hooks run before validators. Use them to enrich or transform data or context (e.g. generating IDs, fetching external data).

from state_machine_framework import pre_transition
import uuid

@pre_transition(OrderPending, order=1)
def generate_order_id(state, data, context):
    data['Order']['order_id'] = f"ORD-{uuid.uuid4().hex[:8].upper()}"
    return {'id_generated': True}

7. Implement an ORM Adapter

from state_machine_framework import ORMAdapter
from state_machine_framework.core.exceptions import ObjectNotFound

class SQLAlchemyAdapter(ORMAdapter):
    def __init__(self, session):
        self.session = session

    def create(self, model_cls, **data):
        instance = model_cls(**data)
        self.session.add(instance)
        self.session.flush()
        return instance

    def get(self, model_cls, **filters):
        instance = self.session.query(model_cls).filter_by(**filters).one_or_none()
        if instance is None:
            raise ObjectNotFound(f"{model_cls.__name__} not found: {filters}")
        return instance

    def filter(self, model_cls, **filters):
        return self.session.query(model_cls).filter_by(**filters).all()

    def update(self, model_cls, filters, **updates):
        return self.session.query(model_cls).filter_by(**filters).update(updates)

    def delete(self, model_cls, **filters):
        return self.session.query(model_cls).filter_by(**filters).delete()

    def begin(self):    self.session.begin()
    def commit(self):   self.session.commit()
    def rollback(self): self.session.rollback()

8. Define a Workflow Context

from pydantic import BaseModel, Field
from state_machine_framework import WorkflowContext

class OrderData(BaseModel):
    amount: float = Field(..., gt=0)
    customer_email: str

class OrderWorkflowContext(WorkflowContext):
    def define_requirements(self):
        self.require_value('customer_id', str)
        self.require_model('order', Order, created_in_state=OrderPending)
        self.require_dict('order_data', OrderData)

9. Execute a Workflow

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

engine = create_engine('sqlite:///orders.db')
session = sessionmaker(bind=engine)()

adapter = SQLAlchemyAdapter(session)

with OrderWorkflowContext(
    OrderStateMachine,
    customer_id='CUST123',
    order_data={'amount': 99.99, 'customer_email': 'user@example.com'},
    _orm_adapter=adapter,
) as workflow:
    workflow.transition_to(OrderPending, Order={'amount': 99.99, 'customer_email': 'user@example.com'})
    workflow.transition_to(OrderProcessing)
    workflow.transition_to(OrderCompleted)

    print(workflow.get_history_summary())

Package Structure

state_machine_framework/
├── __init__.py              # Main package exports
├── core/
│   ├── __init__.py
│   ├── state.py             # State base class and metaclass
│   ├── state_machine.py     # StateMachine base class
│   └── exceptions.py        # Custom exceptions
├── decorators/
│   ├── __init__.py
│   └── hooks.py             # Decorators: pre_transition, validator, hook, transition
├── orm/
│   ├── __init__.py
│   └── base.py              # Abstract ORM adapter interface
└── workflow/
    ├── __init__.py
    ├── context.py           # WorkflowContext implementation
    └── requirements.py      # Context requirement classes

Key Concepts

States

States represent points in your workflow. Each state declares:

  • model_states: maps model class → state field value at this state
  • allowed_transitions: list of State classes this state can transition to (empty = unrestricted)
  • is_start_state / is_terminal_state: lifecycle flags
class MyState(State):
    _state_machine = MyStateMachine
    is_start_state = True
    allowed_transitions = [NextState]
    model_states = {MyModel: 'my_value'}

Validators

Validators run after pre-transition hooks and before the state update. They receive (data, context) and must only raise exceptions — never modify data.

@validator(MyState, order=1)
def validate_something(state, data, context):
    if not some_condition(data):
        raise ValueError("Validation failed")
    return {'result': 'validated'}

Hooks

Hooks run after the transition completes. They receive (instances, context) where instances maps model name strings to ORM instances.

@hook(MyState, order=1)
def do_something(state, instances, context):
    my_model = instances['MyModel']
    notify(my_model)
    return {'action': 'completed'}

Pre-Transition Hooks

Pre-transition hooks run before validators. They receive (data, context) and are allowed to mutate both, making them suitable for data enrichment.

@pre_transition(MyState, order=1)
def enrich_data(state, data, context):
    data['MyModel']['computed_field'] = compute_value(data)
    return {'enriched': True}

Workflow Context

Workflow contexts provide type-safe, validated context management:

class MyWorkflowContext(WorkflowContext):
    def define_requirements(self):
        # Require a simple typed value
        self.require_value('user_id', str, required=True)

        # Require a model instance (created during workflow at the given state)
        self.require_model('order', Order, created_in_state=OrderPending)

        # Require a dict validated against a Pydantic schema
        self.require_dict('order_data', OrderDataSchema, required=True)

Custom Transitions

Use @transition to override the default state-field update logic:

class MyState(State):
    @transition
    def apply(self, instances, context):
        for instance in instances.values():
            instance.status = 'custom'
            instance.save()

Examples

See the examples/ directory for a complete vending machine implementation demonstrating:

  • ORM adapter implementation
  • Workflow context with validation
  • External validator and hook registration
  • Pydantic schema validation
  • Complete transaction workflow

To run the example:

cd examples/vending_machine
python main.py

Requirements

  • Python 3.10+
  • pydantic >= 2.0

License

MIT License - see LICENSE file for details

Contributing

Contributions are welcome! Please feel free to reach out or open an issue.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

state_machine_framework-0.2.0.tar.gz (23.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

state_machine_framework-0.2.0-py3-none-any.whl (25.1 kB view details)

Uploaded Python 3

File details

Details for the file state_machine_framework-0.2.0.tar.gz.

File metadata

  • Download URL: state_machine_framework-0.2.0.tar.gz
  • Upload date:
  • Size: 23.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.8.9

File hashes

Hashes for state_machine_framework-0.2.0.tar.gz
Algorithm Hash digest
SHA256 c61a02a8a9632972052b193a32162cf08b0be76a1f4bbfc2680aea2020a97a1a
MD5 3e7b4e753c4542f37da409b579c44c50
BLAKE2b-256 a17066dd19a21cf3ed86ad1e9feed2d7a59a64483bb02ba7590d5a9a1b4b3df0

See more details on using hashes here.

File details

Details for the file state_machine_framework-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for state_machine_framework-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7df18679be0aadf0861fe2c115dfb43890a654d64999aa9b761e3703450957fe
MD5 d4190bc5fb77b81ee45b153ddf6a6da0
BLAKE2b-256 69c8fcea3ef5981c84302c10f7c00537f6dec07842af55db5e6370eb43294fc3

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page