CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-django-fsm

Django friendly finite state machine support for models through FSMField and the @transition decorator.

Pending
Overview
Eval results
Files

exceptions.mddocs/

Exception Handling

Exception classes for managing state transition errors including general transition failures, concurrent modification conflicts, and invalid result states.

Capabilities

TransitionNotAllowed

Raised when a state transition is not allowed due to invalid source state, unmet conditions, or other transition constraints.

class TransitionNotAllowed(Exception):
    def __init__(self, *args, object=None, method=None, **kwargs):
        """
        Exception raised when transition cannot be executed.
        
        Parameters:
        - *args: Standard exception arguments
        - object: Model instance that failed transition (optional)
        - method: Transition method that was called (optional)
        - **kwargs: Additional exception arguments
        
        Attributes:
        - object: Model instance that failed transition
        - method: Method that was attempted
        """

This exception is raised in several scenarios:

Invalid Source State:

from django_fsm import TransitionNotAllowed

class Order(models.Model):
    state = FSMField(default='pending')
    
    @transition(field=state, source='confirmed', target='shipped')
    def ship(self):
        pass

# This will raise TransitionNotAllowed
order = Order.objects.create()  # state is 'pending'
try:
    order.ship()  # Can only ship from 'confirmed' state
except TransitionNotAllowed as e:
    print(f"Cannot transition: {e}")
    print(f"Object: {e.object}")
    print(f"Method: {e.method}")

Unmet Condition:

def can_ship(instance):
    return instance.payment_confirmed

class Order(models.Model):
    state = FSMField(default='confirmed')
    payment_confirmed = models.BooleanField(default=False)
    
    @transition(field=state, source='confirmed', target='shipped', conditions=[can_ship])
    def ship(self):
        pass

# This will raise TransitionNotAllowed if payment not confirmed
order = Order.objects.create(state='confirmed', payment_confirmed=False)
try:
    order.ship()
except TransitionNotAllowed as e:
    print("Transition conditions not met")

ConcurrentTransition

Raised when a transition cannot be executed because the object has become stale (state has been changed since it was fetched from the database).

class ConcurrentTransition(Exception):
    """
    Raised when transition cannot execute due to concurrent state modifications.
    
    This exception indicates that the object's state was changed by another
    process between when it was loaded and when the transition was attempted.
    """

Usage with ConcurrentTransitionMixin:

from django_fsm import ConcurrentTransition, ConcurrentTransitionMixin
from django.db import transaction

class BankAccount(ConcurrentTransitionMixin, models.Model):
    state = FSMField(default='active')
    balance = models.DecimalField(max_digits=10, decimal_places=2)
    
    @transition(field=state, source='active', target='frozen') 
    def freeze(self):
        pass

def freeze_account(account_id):
    try:
        with transaction.atomic():
            account = BankAccount.objects.get(pk=account_id)
            account.freeze()
            account.save()  # May raise ConcurrentTransition
    except ConcurrentTransition:
        # Another process modified the account
        print("Account was modified by another process")
        # Implement retry logic or error handling

InvalidResultState

Raised when a dynamic state resolution returns an invalid state value that is not in the allowed states list.

class InvalidResultState(Exception):
    """
    Raised when dynamic state resolution produces invalid result.
    
    This occurs when using RETURN_VALUE or GET_STATE with restricted
    allowed states, and the resolved state is not in the allowed list.
    """

Usage with dynamic states:

from django_fsm import RETURN_VALUE, InvalidResultState

def calculate_priority_state():
    # This might return an invalid state
    return 'invalid_priority'

class Task(models.Model):
    state = FSMField(default='new')
    
    @transition(
        field=state, 
        source='new', 
        target=RETURN_VALUE('low', 'medium', 'high')
    )
    def set_priority(self):
        return calculate_priority_state()

try:
    task = Task.objects.create()
    task.set_priority()  # May raise InvalidResultState
except InvalidResultState as e:
    print(f"Invalid state returned: {e}")

Exception Handling Patterns

Basic Exception Handling

Handle different exception types appropriately:

from django_fsm import TransitionNotAllowed, ConcurrentTransition

def safe_transition(instance, transition_method):
    try:
        transition_method()
        instance.save()
        return True, "Success"
    
    except TransitionNotAllowed as e:
        return False, f"Transition not allowed: {e}"
    
    except ConcurrentTransition:
        return False, "Object was modified by another process"
    
    except Exception as e:
        return False, f"Unexpected error: {e}"

# Usage
success, message = safe_transition(order, order.ship)
if success:
    print("Order shipped successfully")
else:
    print(f"Failed to ship order: {message}")

Retry Logic for Concurrent Transitions

Implement retry logic for handling concurrent modifications:

import time
import random
from django_fsm import ConcurrentTransition

def transition_with_retry(instance, transition_method, max_retries=3):
    """
    Execute transition with exponential backoff retry for concurrent conflicts.
    """
    for attempt in range(max_retries):
        try:
            with transaction.atomic():
                # Refresh object to get latest state
                instance.refresh_from_db()
                
                # Attempt transition
                transition_method()
                instance.save()
                return True
                
        except ConcurrentTransition:
            if attempt == max_retries - 1:
                # Final attempt failed
                raise
            
            # Wait before retry with jitter
            delay = 0.1 * (2 ** attempt) + random.uniform(0, 0.1)
            time.sleep(delay)
    
    return False

Custom Exception Context

Add additional context to exceptions:

class OrderTransitionError(TransitionNotAllowed):
    """Custom exception with additional order context."""
    
    def __init__(self, message, order, transition_name, *args, **kwargs):
        super().__init__(message, *args, **kwargs)
        self.order = order
        self.transition_name = transition_name

class Order(models.Model):
    state = FSMField(default='pending')
    
    @transition(field=state, source='confirmed', target='shipped')
    def ship(self):
        if not self.can_ship():
            raise OrderTransitionError(
                f"Order {self.id} cannot be shipped",
                order=self,
                transition_name='ship'
            )

def handle_order_shipping(order):
    try:
        order.ship()
        order.save()
    except OrderTransitionError as e:
        logger.error(
            f"Shipping failed for order {e.order.id}: {e}",
            extra={'order_id': e.order.id, 'transition': e.transition_name}
        )

Exception Logging and Monitoring

Implement comprehensive logging for state machine exceptions:

import logging
from django_fsm import TransitionNotAllowed, ConcurrentTransition

logger = logging.getLogger('fsm_transitions')

def log_transition_error(instance, method_name, exception):
    """Log detailed information about transition failures."""
    logger.error(
        f"FSM transition failed: {exception}",
        extra={
            'model': instance.__class__.__name__,
            'instance_id': instance.pk,
            'current_state': getattr(instance, 'state', None),
            'method': method_name,
            'exception_type': exception.__class__.__name__
        }
    )

def monitored_transition(instance, transition_method, method_name):
    """Execute transition with comprehensive error logging."""
    try:
        transition_method()
        instance.save()
        
        logger.info(
            f"FSM transition successful: {method_name}",
            extra={
                'model': instance.__class__.__name__,
                'instance_id': instance.pk,
                'new_state': getattr(instance, 'state', None),
                'method': method_name
            }
        )
        return True
        
    except (TransitionNotAllowed, ConcurrentTransition) as e:
        log_transition_error(instance, method_name, e)
        return False

Validation and Error Prevention

Prevent exceptions through validation:

from django_fsm import can_proceed, has_transition_perm

def validate_and_execute_transition(instance, transition_method, user=None):
    """
    Validate transition before execution to prevent exceptions.
    """
    # Check if transition is possible
    if not can_proceed(transition_method):
        return False, "Transition not possible from current state"
    
    # Check user permissions if provided
    if user and not has_transition_perm(transition_method, user):
        return False, "User does not have permission for this transition"
    
    # Execute transition
    try:
        transition_method()
        instance.save()
        return True, "Transition successful"
    
    except Exception as e:
        return False, f"Transition failed: {e}"

# Usage in views
def order_action_view(request, order_id, action):
    order = Order.objects.get(pk=order_id)
    
    action_map = {
        'ship': order.ship,
        'cancel': order.cancel,
        'refund': order.refund
    }
    
    method = action_map.get(action)
    if not method:
        return HttpResponseBadRequest("Invalid action")
    
    success, message = validate_and_execute_transition(
        order, method, request.user
    )
    
    if success:
        messages.success(request, message)
    else:
        messages.error(request, message)
    
    return redirect('order_detail', order_id=order.id)

Testing Exception Scenarios

Test exception handling in your state machines:

from django.test import TestCase
from django_fsm import TransitionNotAllowed, ConcurrentTransition

class OrderStateTests(TestCase):
    def test_invalid_transition_raises_exception(self):
        order = Order.objects.create(state='pending')
        
        with self.assertRaises(TransitionNotAllowed) as cm:
            order.ship()  # Can't ship from pending
        
        self.assertEqual(cm.exception.object, order)
        self.assertEqual(cm.exception.method.__name__, 'ship')
    
    def test_concurrent_modification_protection(self):
        order = ConcurrentOrder.objects.create(state='pending')
        
        # Simulate concurrent modification
        ConcurrentOrder.objects.filter(pk=order.pk).update(state='confirmed')
        
        with self.assertRaises(ConcurrentTransition):
            order.ship()
            order.save()

Install with Tessl CLI

npx tessl i tessl/pypi-django-fsm

docs

dynamic-states.md

exceptions.md

field-types.md

index.md

model-mixins.md

signals.md

transitions.md

visualization.md

tile.json