Bin
2025-12-17 21f0498f62ada55651f4d232327e15fc47f498b1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
"""
Utility functions for working with the declarative transition system.
 
This module provides helper functions to make it easier to integrate
the new Pydantic-based transition system with existing Label Studio code.
"""
 
import logging
from typing import Any, Dict, List, Type
 
from django.db.models import Model
from fsm.registry import transition_registry
from fsm.state_manager import get_state_manager
from fsm.transitions import BaseTransition, TransitionValidationError
 
logger = logging.getLogger(__name__)
 
StateManager = get_state_manager()
 
 
def get_available_transitions(entity: Model, user=None, validate: bool = False) -> Dict[str, Type[BaseTransition]]:
    """
    Get available transitions for an entity.
 
    Args:
        entity: The entity to get transitions for
        user: User context for validation (only used when validate=True)
        validate: Whether to validate each transition against current state.
                 When False, returns all registered transitions for the entity type.
                 When True, filters to only transitions valid from current state (may be expensive).
 
    Returns:
        Dictionary mapping transition names to transition classes.
        When validate=False: All registered transitions for the entity type.
        When validate=True: Only transitions valid for the current state.
    """
    entity_name = entity._meta.model_name.lower()
    available = transition_registry.get_transitions_for_entity(entity_name)
 
    if not validate:
        return available
 
    valid_transitions = {}
 
    for name, transition_class in available.items():
        try:
            # Get current state information using potentially overridden StateManager
            current_state_object = StateManager.get_current_state_object(entity)
            current_state = current_state_object.state if current_state_object else None
 
            # Build minimal context for validation
            from .transitions import TransitionContext
 
            # Get target state from instance (may need entity context)
            # For validation purposes, we try to create with minimal/default data
            try:
                temp_instance = transition_class()
                # Create minimal context for dynamic target_state computation
                minimal_context = TransitionContext(
                    entity=entity,
                    current_user=user,
                    current_state_object=current_state_object,
                    current_state=current_state,
                    target_state=None,  # Will be computed
                    organization_id=getattr(entity, 'organization_id', None),
                )
                target_state = temp_instance.get_target_state(minimal_context)
            except (TypeError, ValueError):
                # Can't instantiate without required data - include in results
                # since we can't validate state transitions, we assume they're available
                valid_transitions[name] = transition_class
                continue
 
            context = TransitionContext(
                entity=entity,
                current_user=user,
                current_state_object=current_state_object,
                current_state=current_state,
                target_state=target_state,
                organization_id=getattr(entity, 'organization_id', None),
            )
 
            # Use class-level validation that doesn't require an instance
            if transition_class.can_transition_from_state(context):
                valid_transitions[name] = transition_class
 
        except TransitionValidationError:
            # Transition is not valid for current state/context - this is expected
            continue
        except Exception as e:
            # Unexpected error during validation - this should be investigated
            logger.warning(
                'Unexpected error validating transition',
                extra={
                    'event': 'fsm.transition_validation_error',
                    'transition_name': name,
                    'entity_type': entity._meta.model_name,
                    'error': str(e),
                },
                exc_info=True,
            )
            continue
 
    return valid_transitions
 
 
def create_transition_from_dict(transition_class: Type[BaseTransition], data: Dict[str, Any]) -> BaseTransition:
    """
    Create a transition instance from a dictionary of data.
 
    This handles Pydantic validation and provides clear error messages.
 
    Args:
        transition_class: The transition class to instantiate
        data: Dictionary of transition data
 
    Returns:
        Validated transition instance
 
    Raises:
        ValueError: If data validation fails
    """
    try:
        return transition_class(**data)
    except Exception as e:
        raise ValueError(f'Failed to create {transition_class.__name__}: {e}')
 
 
def get_transition_schema(transition_class: Type[BaseTransition]) -> Dict[str, Any]:
    """
    Get the JSON schema for a transition class.
 
    Useful for generating API documentation or frontend forms.
 
    Args:
        transition_class: The transition class
 
    Returns:
        JSON schema dictionary
    """
    return transition_class.model_json_schema()
 
 
def validate_transition_data(transition_class: Type[BaseTransition], data: Dict[str, Any]) -> Dict[str, List[str]]:
    """
    Validate transition data without creating an instance.
 
    Args:
        transition_class: The transition class
        data: Data to validate
 
    Returns:
        Dictionary of field names to error messages (empty if valid)
    """
    try:
        transition_class(**data)
        return {}
    except Exception as e:
        # Parse Pydantic validation errors
        errors = {}
        if hasattr(e, 'errors'):
            for error in e.errors():
                field = '.'.join(str(loc) for loc in error['loc'])
                if field not in errors:
                    errors[field] = []
                errors[field].append(error['msg'])
        else:
            errors['__root__'] = [str(e)]
        return errors
 
 
def get_entity_state_flow(entity: Model) -> List[Dict[str, Any]]:
    """
    Get a summary of the state flow for an entity type.
 
    This analyzes all registered transitions and builds a flow diagram.
 
    Args:
        entity: Example entity instance
 
    Returns:
        List of state flow information
    """
    entity_name = entity._meta.model_name.lower()
    transitions = transition_registry.get_transitions_for_entity(entity_name)
 
    # Build state flow information
    states = set()
    flows = []
 
    for transition_name, transition_class in transitions.items():
        # Create instance to get target state
        try:
            from .transitions import TransitionContext
 
            transition = transition_class()
            # Create minimal context for dynamic target_state computation
            minimal_context = (
                TransitionContext(
                    entity=entity,
                    current_user=None,
                    current_state_object=None,
                    current_state=None,
                    target_state=None,  # Will be computed
                    organization_id=getattr(entity, 'organization_id', None),
                )
                if hasattr(entity, 'pk')
                else None
            )
            target = transition.get_target_state(minimal_context)
            states.add(target)
 
            flows.append(
                {
                    'transition_name': transition_name,
                    'transition_class': transition_class.__name__,
                    'target_state': target,
                    'description': transition_class.__doc__ or '',
                    'fields': list(transition_class.model_fields.keys()),
                }
            )
        except Exception:
            continue
 
    return flows