CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-python-statemachine

Python finite state machine library with declarative API for sync and async applications

Pending
Overview
Eval results
Files

mixins-integration.mddocs/

Mixins and Integration

Domain model integration patterns, mixin classes for automatic state machine binding, and framework integration including Django support and model field binding.

Capabilities

MachineMixin Class

Mixin class that enables models to automatically instantiate and manage state machines with configurable binding options.

class MachineMixin:
    """
    Mixin that allows a model to automatically instantiate and assign a StateMachine.
    
    This mixin provides seamless integration between domain models and state machines,
    automatically handling state machine instantiation and event binding.
    
    Class Attributes:
    - state_field_name: The model's field name that holds the state value (default: "state")
    - state_machine_name: Fully qualified name of the StateMachine class for import
    - state_machine_attr: Name of the model attribute that will hold the machine instance (default: "statemachine")
    - bind_events_as_methods: If True, state machine events are bound as model methods (default: False)
    """
    
    state_field_name: str = "state"
    """The model's state field name that will hold the state value."""
    
    state_machine_name: str = None
    """A fully qualified name of the StateMachine class, where it can be imported."""
    
    state_machine_attr: str = "statemachine"
    """Name of the model's attribute that will hold the machine instance."""
    
    bind_events_as_methods: bool = False
    """If True, state machine event triggers will be bound to the model as methods."""
    
    def __init__(self, *args, **kwargs):
        """
        Initialize the mixin and create state machine instance.
        
        Raises:
        - ValueError: If state_machine_name is not set or invalid
        """

Registry Functions

Functions for state machine registration and discovery, particularly useful for framework integration.

# Registry module functions for state machine discovery
def get_machine_cls(qualname: str):
    """
    Get state machine class by fully qualified name.
    
    Parameters:
    - qualname: Fully qualified class name (e.g., "myapp.machines.OrderMachine")
    
    Returns:
    StateMachine class
    
    Raises:
    - ImportError: If the module or class cannot be imported
    """

def register_machine(machine_cls):
    """
    Register a state machine class for discovery.
    
    Parameters:
    - machine_cls: StateMachine class to register
    """

Model Integration Utilities

Utilities for integrating state machines with various model systems and frameworks.

def bind_events_to(machine: StateMachine, target_object: object):
    """
    Bind state machine events as methods on target object.
    
    Parameters:
    - machine: StateMachine instance
    - target_object: Object to bind events to
    
    Creates methods on target_object for each event in the machine.
    """

class Model:
    """
    Base model class with state machine integration support.
    
    Provides foundation for external model integration with
    automatic state field management.
    """
    def __init__(self, **kwargs):
        """Initialize model with optional state field."""

Usage Examples

Basic Model Integration with MachineMixin

from statemachine import StateMachine, State
from statemachine.mixins import MachineMixin

# Define the state machine
class OrderStateMachine(StateMachine):
    pending = State(initial=True, value="pending")
    paid = State(value="paid")
    shipped = State(value="shipped")
    delivered = State(value="delivered", final=True)
    cancelled = State(value="cancelled", final=True)
    
    pay = pending.to(paid)
    ship = paid.to(shipped)
    deliver = shipped.to(delivered)
    cancel = (
        pending.to(cancelled)
        | paid.to(cancelled)
        | shipped.to(cancelled)
    )
    
    def on_enter_paid(self, payment_info: dict = None):
        print(f"Payment received: {payment_info}")
    
    def on_enter_shipped(self, tracking_number: str = None):
        print(f"Order shipped with tracking: {tracking_number}")
    
    def on_enter_delivered(self):
        print("Order delivered successfully!")

# Define the model using MachineMixin
class Order(MachineMixin):
    state_machine_name = "OrderStateMachine"  # Could be full path like "myapp.machines.OrderStateMachine"
    state_field_name = "status"
    bind_events_as_methods = True
    
    def __init__(self, order_id: str, **kwargs):
        self.order_id = order_id
        self.status = "pending"  # Initial state value
        self.customer_email = kwargs.get("customer_email")
        super().__init__(**kwargs)  # Initialize MachineMixin
    
    def send_notification(self, message: str):
        """Custom model method."""
        print(f"Notification to {self.customer_email}: {message}")

# Usage
order = Order("ORD-001", customer_email="customer@example.com")

# Access state machine via configured attribute
print(f"Current state: {order.statemachine.current_state.id}")  # "pending"
print(f"Model status field: {order.status}")  # "pending"

# Use bound event methods (since bind_events_as_methods=True)
order.pay(payment_info={"method": "credit_card", "amount": 99.99})
print(f"After payment - Status: {order.status}")  # "paid"

order.ship(tracking_number="TRK123456")
print(f"After shipping - Status: {order.status}")  # "shipped"

order.deliver()
print(f"Final status: {order.status}")  # "delivered"

Advanced Model Integration with Custom State Field

import json
from datetime import datetime

class AdvancedOrder(MachineMixin):
    state_machine_name = "OrderStateMachine"
    state_field_name = "workflow_state"
    state_machine_attr = "workflow"
    
    def __init__(self, order_id: str, **kwargs):
        self.order_id = order_id
        self.workflow_state = "pending"
        self.created_at = datetime.now()
        self.state_history = []
        super().__init__(**kwargs)
        
        # Add state change logging
        self.workflow.add_callback(
            self.log_state_change,
            group=CallbackGroup.ACTIONS
        )
    
    def log_state_change(self, event: str, source: State, target: State, **kwargs):
        """Log all state changes."""
        self.state_history.append({
            "timestamp": datetime.now().isoformat(),
            "event": event,
            "from": source.id,
            "to": target.id,
            "metadata": kwargs
        })
        print(f"State change logged: {source.id} -> {target.id} via {event}")
    
    def get_state_history(self) -> str:
        """Get formatted state history."""
        return json.dumps(self.state_history, indent=2)
    
    def current_state_info(self) -> dict:
        """Get current state information."""
        return {
            "state": self.workflow_state,
            "state_name": self.workflow.current_state.name,
            "is_final": self.workflow.current_state.final,
            "allowed_events": [e.id for e in self.workflow.allowed_events]
        }

# Usage
order = AdvancedOrder("ORD-002")
print("Initial state:", order.current_state_info())

order.workflow.send("pay", payment_method="paypal", amount=75.50)
order.workflow.send("ship", carrier="UPS", tracking="1Z999AA1234567890")
order.workflow.send("deliver")

print("\nFinal state:", order.current_state_info())
print("\nState history:")
print(order.get_state_history())

Django Model Integration

# Django model example (requires Django)
try:
    from django.db import models
    from statemachine.mixins import MachineMixin
    
    class DjangoOrder(models.Model, MachineMixin):
        # Django model fields
        order_id = models.CharField(max_length=50, unique=True)
        customer_email = models.EmailField()
        total_amount = models.DecimalField(max_digits=10, decimal_places=2)
        status = models.CharField(max_length=20, default="pending")
        created_at = models.DateTimeField(auto_now_add=True)
        updated_at = models.DateTimeField(auto_now=True)
        
        # MachineMixin configuration
        state_machine_name = "myapp.machines.OrderStateMachine"
        state_field_name = "status"
        bind_events_as_methods = True
        
        class Meta:
            db_table = "orders"
        
        def save(self, *args, **kwargs):
            """Override save to sync state machine state."""
            if hasattr(self, 'statemachine'):
                # Ensure model field matches state machine state
                self.status = self.statemachine.current_state.value
            super().save(*args, **kwargs)
        
        def __str__(self):
            return f"Order {self.order_id} - {self.status}"
    
    # Usage in Django views/services
    def process_payment(order_id: str, payment_data: dict):
        """Service function to process order payment."""
        order = DjangoOrder.objects.get(order_id=order_id)
        
        try:
            # Use bound event method
            order.pay(payment_info=payment_data)
            order.save()  # Persist state change
            return {"success": True, "new_state": order.status}
        except TransitionNotAllowed as e:
            return {"success": False, "error": str(e)}

except ImportError:
    print("Django not available - skipping Django example")

SQLAlchemy Integration

# SQLAlchemy model example
try:
    from sqlalchemy import Column, Integer, String, DateTime, create_engine
    from sqlalchemy.ext.declarative import declarative_base
    from sqlalchemy.orm import sessionmaker
    from datetime import datetime
    
    Base = declarative_base()
    
    class SQLAlchemyOrder(Base, MachineMixin):
        __tablename__ = "orders"
        
        # SQLAlchemy columns
        id = Column(Integer, primary_key=True)
        order_id = Column(String(50), unique=True, nullable=False)
        customer_email = Column(String(255), nullable=False)
        status = Column(String(20), default="pending")
        created_at = Column(DateTime, default=datetime.now)
        updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
        
        # MachineMixin configuration
        state_machine_name = "OrderStateMachine"
        state_field_name = "status"
        
        def __init__(self, order_id: str, customer_email: str, **kwargs):
            self.order_id = order_id
            self.customer_email = customer_email
            super().__init__(**kwargs)
        
        def update_state(self, session):
            """Helper to update and persist state changes."""
            if hasattr(self, 'statemachine'):
                self.status = self.statemachine.current_state.value
                self.updated_at = datetime.now()
                session.commit()
    
    # Usage
    def create_order_with_workflow():
        engine = create_engine("sqlite:///orders.db")
        Base.metadata.create_all(engine)
        Session = sessionmaker(bind=engine)
        session = Session()
        
        # Create new order
        order = SQLAlchemyOrder(
            order_id="ORD-003",
            customer_email="customer@example.com"
        )
        session.add(order)
        session.commit()
        
        # Process through workflow
        order.statemachine.send("pay", payment_method="stripe")
        order.update_state(session)
        
        order.statemachine.send("ship", carrier="FedEx")
        order.update_state(session)
        
        print(f"Order {order.order_id} status: {order.status}")
        session.close()

except ImportError:
    print("SQLAlchemy not available - skipping SQLAlchemy example")

Custom Integration Pattern

from abc import ABC, abstractmethod

class StateMachineModel(ABC):
    """Abstract base class for state machine model integration."""
    
    def __init__(self, machine_class, initial_state_value=None, **kwargs):
        self.machine_class = machine_class
        self._state_value = initial_state_value or self.get_initial_state_value()
        self._machine = None
        self.initialize_machine(**kwargs)
    
    @abstractmethod
    def get_initial_state_value(self):
        """Get the initial state value for this model."""
        pass
    
    @abstractmethod
    def persist_state(self):
        """Persist the current state to storage."""
        pass
    
    def initialize_machine(self, **kwargs):
        """Initialize the state machine instance."""
        self._machine = self.machine_class(
            model=self,
            state_field="_state_value",
            **kwargs
        )
    
    @property
    def machine(self):
        """Get the state machine instance."""
        return self._machine
    
    def send_event(self, event: str, *args, **kwargs):
        """Send event and persist state change."""
        try:
            result = self._machine.send(event, *args, **kwargs)
            self.persist_state()
            return result
        except Exception as e:
            # Optionally log error or handle rollback
            raise

class FileBasedOrder(StateMachineModel):
    """Example model that persists state to file."""
    
    def __init__(self, order_id: str, **kwargs):
        self.order_id = order_id
        self.filename = f"order_{order_id}.json"
        super().__init__(OrderStateMachine, **kwargs)
    
    def get_initial_state_value(self):
        """Load state from file or return default."""
        try:
            with open(self.filename, 'r') as f:
                data = json.load(f)
                return data.get('state', 'pending')
        except FileNotFoundError:
            return 'pending'
    
    def persist_state(self):
        """Save current state to file."""
        data = {
            'order_id': self.order_id,
            'state': self._state_value,
            'timestamp': datetime.now().isoformat()
        }
        with open(self.filename, 'w') as f:
            json.dump(data, f)
        print(f"State persisted: {self._state_value}")

# Usage
order = FileBasedOrder("ORD-004")
print(f"Initial state: {order.machine.current_state.id}")

order.send_event("pay", payment_method="bitcoin")
order.send_event("ship", carrier="DHL")
order.send_event("deliver")

print(f"Final state: {order.machine.current_state.id}")

Event Binding Patterns

class EventBoundModel(MachineMixin):
    state_machine_name = "OrderStateMachine"
    bind_events_as_methods = True
    
    def __init__(self, order_id: str, **kwargs):
        self.order_id = order_id
        self.state = "pending"
        self.notifications = []
        super().__init__(**kwargs)
        
        # Add custom event handling
        self.setup_event_notifications()
    
    def setup_event_notifications(self):
        """Setup automatic notifications for events."""
        # Override bound event methods to add notifications
        original_pay = self.pay
        original_ship = self.ship
        original_deliver = self.deliver
        original_cancel = self.cancel
        
        def notify_pay(*args, **kwargs):
            result = original_pay(*args, **kwargs)
            self.notifications.append("Payment confirmation sent")
            return result
        
        def notify_ship(*args, **kwargs):
            result = original_ship(*args, **kwargs)
            self.notifications.append("Shipping notification sent")
            return result
        
        def notify_deliver(*args, **kwargs):
            result = original_deliver(*args, **kwargs)
            self.notifications.append("Delivery confirmation sent")
            return result
        
        def notify_cancel(*args, **kwargs):
            result = original_cancel(*args, **kwargs)
            self.notifications.append("Cancellation notice sent")
            return result
        
        # Replace bound methods with notification versions
        self.pay = notify_pay
        self.ship = notify_ship
        self.deliver = notify_deliver
        self.cancel = notify_cancel
    
    def get_notifications(self):
        """Get all notifications sent."""
        return self.notifications

# Usage
order = EventBoundModel("ORD-005")
order.pay(payment_method="visa")
order.ship(tracking_number="ABC123")
order.deliver()

print("Notifications sent:")
for notification in order.get_notifications():
    print(f"- {notification}")

Install with Tessl CLI

npx tessl i tessl/pypi-python-statemachine

docs

actions-callbacks.md

core-statemachine.md

diagrams.md

events-transitions.md

exceptions.md

index.md

mixins-integration.md

utilities.md

tile.json