""" FSM Model Registry for Model State Management. This module provides a registry system for state models and state choices, allowing the FSM to be decoupled from concrete implementations. """ import logging import typing from typing import Dict, Optional, Type from django.db.models import Model, TextChoices if typing.TYPE_CHECKING: from fsm.state_models import BaseState from fsm.transitions import BaseTransition logger = logging.getLogger(__name__) class StateChoicesRegistry: """ Registry for managing state choices for different entity types. Provides a centralized way to register, discover, and manage state choices for different entity types in the FSM system. """ def __init__(self): self._choices: Dict[str, Type[TextChoices]] = {} def register(self, entity_name: str, choices_class: Type[TextChoices]): """ Register state choices for an entity type. Args: entity_name: Name of the entity (e.g., 'task', 'annotation') choices_class: Django TextChoices class defining valid states """ self._choices[entity_name.lower()] = choices_class def get_choices(self, entity_name: str) -> Optional[Type[TextChoices]]: """ Get state choices for an entity type. Args: entity_name: Name of the entity Returns: Django TextChoices class or None if not found """ return self._choices.get(entity_name.lower()) def list_entities(self) -> list[str]: """Get a list of all registered entity types.""" return list(self._choices.keys()) def clear(self): """ Clear all registered choices. Useful for testing to ensure clean state between tests. """ self._choices.clear() # Global state choices registry instance state_choices_registry = StateChoicesRegistry() def get_state_choices(entity_name: str): """ Get state choices for an entity type. Args: entity_name: Name of the entity Returns: Django TextChoices class or None if not found """ return state_choices_registry.get_choices(entity_name) def register_state_choices(entity_name: str): """ Decorator to register state choices for an entity type. Args: entity_name: Name of the entity type Example: @register_state_choices('task') class TaskStateChoices(models.TextChoices): CREATED = 'CREATED', _('Created') IN_PROGRESS = 'IN_PROGRESS', _('In Progress') COMPLETED = 'COMPLETED', _('Completed') """ def decorator(choices_class: Type[TextChoices]) -> Type[TextChoices]: state_choices_registry.register(entity_name, choices_class) return choices_class return decorator class StateModelRegistry: """ Registry for state models and their configurations. This allows projects to register their state models dynamically without hardcoding them in the FSM framework. """ def __init__(self): self._models: Dict[str, 'BaseState'] = {} def register_model(self, entity_name: str, state_model: 'BaseState'): """ Register a state model for an entity type. Args: entity_name: Name of the entity (e.g., 'task', 'annotation') state_model: The state model class for this entity """ entity_key = entity_name.lower() if entity_key in self._models: logger.debug( 'Overwriting existing state model', extra={ 'event': 'fsm.registry_overwrite', 'entity_type': entity_key, 'previous_model': self._models[entity_key].__name__, 'new_model': state_model.__name__, }, ) self._models[entity_key] = state_model logger.debug( 'Registered state model', extra={ 'event': 'fsm.model_registered', 'entity_type': entity_key, 'model_name': state_model.__name__, }, ) def get_model(self, entity_name: str) -> Optional['BaseState']: """ Get the state model for an entity type. Args: entity_name: Name of the entity Returns: State model class or None if not registered """ return self._models.get(entity_name.lower()) def is_registered(self, entity_name: str) -> bool: """Check if a model is registered for an entity type.""" return entity_name.lower() in self._models def clear(self): """Clear all registered models (useful for testing).""" self._models.clear() logger.debug( 'State model registry cleared', extra={'event': 'fsm.registry_cleared'}, ) def get_all_models(self) -> Dict[str, 'BaseState']: """Get all registered models.""" return self._models.copy() # Global registry instance state_model_registry = StateModelRegistry() def register_state_model(entity_name: str): """ Decorator to register a state model. Args: entity_name: Name of the entity (e.g., 'task', 'annotation') Example: @register_state_model('task') class TaskState(BaseState): @classmethod def get_denormalized_fields(cls, entity): return { 'project_id': entity.project_id, 'priority': entity.priority } """ def decorator(state_model: 'BaseState') -> 'BaseState': state_model_registry.register_model(entity_name, state_model) return state_model return decorator def register_state_model_class(entity_name: str, state_model: 'BaseState'): """ Convenience function to register a state model programmatically. Args: entity_name: Name of the entity (e.g., 'task', 'annotation') state_model: The state model class for this entity """ state_model_registry.register_model(entity_name, state_model) def get_state_model(entity_name: str) -> Optional['BaseState']: """ Convenience function to get a state model. Args: entity_name: Name of the entity Returns: State model class or None if not registered """ return state_model_registry.get_model(entity_name) def get_state_model_for_entity(entity: Model) -> Optional['BaseState']: """Get the state model for an entity.""" entity_name = entity._meta.model_name.lower() return get_state_model(entity_name) class TransitionRegistry: """ Registry for managing declarative transitions. Provides a centralized way to register, discover, and execute transitions for different entity types and state models. """ def __init__(self): self._transitions: Dict[str, Dict[str, 'BaseTransition']] = {} def register(self, entity_name: str, transition_name: str, transition_class: 'BaseTransition'): """ Register a transition class for an entity. Args: entity_name: Name of the entity type (e.g., 'task', 'annotation') transition_name: Name of the transition (e.g., 'start_task', 'submit_annotation') transition_class: The transition class to register """ if entity_name not in self._transitions: self._transitions[entity_name] = {} self._transitions[entity_name][transition_name] = transition_class def get_transition(self, entity_name: str, transition_name: str) -> Optional['BaseTransition']: """ Get a registered transition class. Args: entity_name: Name of the entity type transition_name: Name of the transition Returns: The transition class if found, None otherwise """ return self._transitions.get(entity_name, {}).get(transition_name) def get_transitions_for_entity(self, entity_name: str) -> Dict[str, 'BaseTransition']: """ Get all registered transitions for an entity type. Args: entity_name: Name of the entity type Returns: Dictionary mapping transition names to transition classes """ return self._transitions.get(entity_name, {}).copy() def list_entities(self) -> list[str]: """Get a list of all registered entity types.""" return list(self._transitions.keys()) def clear(self): """ Clear all registered transitions. Useful for testing to ensure clean state between tests. """ self._transitions.clear() # Global transition registry instance transition_registry = TransitionRegistry() def register_state_transition( entity_name: str, transition_name: str, triggers_on_create: bool = False, triggers_on_update: bool = True, triggers_on: list = None, force_state_record: bool = False, ): """ Decorator to register a state transition class with trigger metadata. This decorator not only registers the transition but also configures when it should be triggered based on model changes. Args: entity_name: Name of the entity type (e.g., 'task', 'project') transition_name: Name of the transition (e.g., 'task_created') triggers_on_create: If True, triggers when entity is created triggers_on_update: If True, can trigger on updates (default: True) triggers_on: List of field names that trigger this transition force_state_record: If True, creates state record even if state doesn't change (for audit trails) Example: # Trigger only on creation @register_state_transition('task', 'task_created', triggers_on_create=True) class TaskCreatedTransition(ModelChangeTransition): pass # Trigger when specific fields change @register_state_transition('project', 'project_published', triggers_on=['is_published']) class ProjectPublishedTransition(ModelChangeTransition): pass # Trigger when any of several fields change @register_state_transition('project', 'settings_changed', triggers_on=['maximum_annotations', 'overlap_cohort_percentage']) class ProjectSettingsChangedTransition(ModelChangeTransition): pass """ def decorator(transition_class: 'BaseTransition') -> 'BaseTransition': # Store trigger metadata and transition name on the class transition_class._triggers_on_create = triggers_on_create transition_class._triggers_on_update = triggers_on_update transition_class._trigger_fields = triggers_on or [] transition_class._transition_name = transition_name # Store the registered transition name transition_class._force_state_record = force_state_record # Store whether to force state record creation transition_registry.register(entity_name, transition_name, transition_class) return transition_class return decorator