Django friendly finite state machine support for models through FSMField and the @transition decorator.
—
Exception classes for managing state transition errors including general transition failures, concurrent modification conflicts, and invalid result states.
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")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 handlingRaised 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}")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}")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 FalseAdd 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}
)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 FalsePrevent 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)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