truthound-dashboard 1.3.1__py3-none-any.whl → 1.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- truthound_dashboard/api/alerts.py +258 -0
- truthound_dashboard/api/anomaly.py +1302 -0
- truthound_dashboard/api/cross_alerts.py +352 -0
- truthound_dashboard/api/deps.py +143 -0
- truthound_dashboard/api/drift_monitor.py +540 -0
- truthound_dashboard/api/lineage.py +1151 -0
- truthound_dashboard/api/maintenance.py +363 -0
- truthound_dashboard/api/middleware.py +373 -1
- truthound_dashboard/api/model_monitoring.py +805 -0
- truthound_dashboard/api/notifications_advanced.py +2452 -0
- truthound_dashboard/api/plugins.py +2096 -0
- truthound_dashboard/api/profile.py +211 -14
- truthound_dashboard/api/reports.py +853 -0
- truthound_dashboard/api/router.py +147 -0
- truthound_dashboard/api/rule_suggestions.py +310 -0
- truthound_dashboard/api/schema_evolution.py +231 -0
- truthound_dashboard/api/sources.py +47 -3
- truthound_dashboard/api/triggers.py +190 -0
- truthound_dashboard/api/validations.py +13 -0
- truthound_dashboard/api/validators.py +333 -4
- truthound_dashboard/api/versioning.py +309 -0
- truthound_dashboard/api/websocket.py +301 -0
- truthound_dashboard/core/__init__.py +27 -0
- truthound_dashboard/core/anomaly.py +1395 -0
- truthound_dashboard/core/anomaly_explainer.py +633 -0
- truthound_dashboard/core/cache.py +206 -0
- truthound_dashboard/core/cached_services.py +422 -0
- truthound_dashboard/core/charts.py +352 -0
- truthound_dashboard/core/connections.py +1069 -42
- truthound_dashboard/core/cross_alerts.py +837 -0
- truthound_dashboard/core/drift_monitor.py +1477 -0
- truthound_dashboard/core/drift_sampling.py +669 -0
- truthound_dashboard/core/i18n/__init__.py +42 -0
- truthound_dashboard/core/i18n/detector.py +173 -0
- truthound_dashboard/core/i18n/messages.py +564 -0
- truthound_dashboard/core/lineage.py +971 -0
- truthound_dashboard/core/maintenance.py +443 -5
- truthound_dashboard/core/model_monitoring.py +1043 -0
- truthound_dashboard/core/notifications/channels.py +1020 -1
- truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
- truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
- truthound_dashboard/core/notifications/deduplication/service.py +400 -0
- truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
- truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
- truthound_dashboard/core/notifications/dispatcher.py +43 -0
- truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
- truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
- truthound_dashboard/core/notifications/escalation/engine.py +429 -0
- truthound_dashboard/core/notifications/escalation/models.py +336 -0
- truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
- truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
- truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
- truthound_dashboard/core/notifications/events.py +49 -0
- truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
- truthound_dashboard/core/notifications/metrics/base.py +528 -0
- truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
- truthound_dashboard/core/notifications/routing/__init__.py +169 -0
- truthound_dashboard/core/notifications/routing/combinators.py +184 -0
- truthound_dashboard/core/notifications/routing/config.py +375 -0
- truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
- truthound_dashboard/core/notifications/routing/engine.py +382 -0
- truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
- truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
- truthound_dashboard/core/notifications/routing/rules.py +625 -0
- truthound_dashboard/core/notifications/routing/validator.py +678 -0
- truthound_dashboard/core/notifications/service.py +2 -0
- truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
- truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
- truthound_dashboard/core/notifications/throttling/builder.py +311 -0
- truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
- truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
- truthound_dashboard/core/openlineage.py +1028 -0
- truthound_dashboard/core/plugins/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/extractor.py +703 -0
- truthound_dashboard/core/plugins/docs/renderers.py +804 -0
- truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
- truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
- truthound_dashboard/core/plugins/hooks/manager.py +403 -0
- truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
- truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
- truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
- truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
- truthound_dashboard/core/plugins/loader.py +504 -0
- truthound_dashboard/core/plugins/registry.py +810 -0
- truthound_dashboard/core/plugins/reporter_executor.py +588 -0
- truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
- truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
- truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
- truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
- truthound_dashboard/core/plugins/sandbox.py +617 -0
- truthound_dashboard/core/plugins/security/__init__.py +68 -0
- truthound_dashboard/core/plugins/security/analyzer.py +535 -0
- truthound_dashboard/core/plugins/security/policies.py +311 -0
- truthound_dashboard/core/plugins/security/protocols.py +296 -0
- truthound_dashboard/core/plugins/security/signing.py +842 -0
- truthound_dashboard/core/plugins/security.py +446 -0
- truthound_dashboard/core/plugins/validator_executor.py +401 -0
- truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
- truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
- truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
- truthound_dashboard/core/plugins/versioning/semver.py +266 -0
- truthound_dashboard/core/profile_comparison.py +601 -0
- truthound_dashboard/core/report_history.py +570 -0
- truthound_dashboard/core/reporters/__init__.py +57 -0
- truthound_dashboard/core/reporters/base.py +296 -0
- truthound_dashboard/core/reporters/csv_reporter.py +155 -0
- truthound_dashboard/core/reporters/html_reporter.py +598 -0
- truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
- truthound_dashboard/core/reporters/i18n/base.py +494 -0
- truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
- truthound_dashboard/core/reporters/json_reporter.py +160 -0
- truthound_dashboard/core/reporters/junit_reporter.py +233 -0
- truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
- truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
- truthound_dashboard/core/reporters/registry.py +272 -0
- truthound_dashboard/core/rule_generator.py +2088 -0
- truthound_dashboard/core/scheduler.py +822 -12
- truthound_dashboard/core/schema_evolution.py +858 -0
- truthound_dashboard/core/services.py +152 -9
- truthound_dashboard/core/statistics.py +718 -0
- truthound_dashboard/core/streaming_anomaly.py +883 -0
- truthound_dashboard/core/triggers/__init__.py +45 -0
- truthound_dashboard/core/triggers/base.py +226 -0
- truthound_dashboard/core/triggers/evaluators.py +609 -0
- truthound_dashboard/core/triggers/factory.py +363 -0
- truthound_dashboard/core/unified_alerts.py +870 -0
- truthound_dashboard/core/validation_limits.py +509 -0
- truthound_dashboard/core/versioning.py +709 -0
- truthound_dashboard/core/websocket/__init__.py +59 -0
- truthound_dashboard/core/websocket/manager.py +512 -0
- truthound_dashboard/core/websocket/messages.py +130 -0
- truthound_dashboard/db/__init__.py +30 -0
- truthound_dashboard/db/models.py +3375 -3
- truthound_dashboard/main.py +22 -0
- truthound_dashboard/schemas/__init__.py +396 -1
- truthound_dashboard/schemas/anomaly.py +1258 -0
- truthound_dashboard/schemas/base.py +4 -0
- truthound_dashboard/schemas/cross_alerts.py +334 -0
- truthound_dashboard/schemas/drift_monitor.py +890 -0
- truthound_dashboard/schemas/lineage.py +428 -0
- truthound_dashboard/schemas/maintenance.py +154 -0
- truthound_dashboard/schemas/model_monitoring.py +374 -0
- truthound_dashboard/schemas/notifications_advanced.py +1363 -0
- truthound_dashboard/schemas/openlineage.py +704 -0
- truthound_dashboard/schemas/plugins.py +1293 -0
- truthound_dashboard/schemas/profile.py +420 -34
- truthound_dashboard/schemas/profile_comparison.py +242 -0
- truthound_dashboard/schemas/reports.py +285 -0
- truthound_dashboard/schemas/rule_suggestion.py +434 -0
- truthound_dashboard/schemas/schema_evolution.py +164 -0
- truthound_dashboard/schemas/source.py +117 -2
- truthound_dashboard/schemas/triggers.py +511 -0
- truthound_dashboard/schemas/unified_alerts.py +223 -0
- truthound_dashboard/schemas/validation.py +25 -1
- truthound_dashboard/schemas/validators/__init__.py +11 -0
- truthound_dashboard/schemas/validators/base.py +151 -0
- truthound_dashboard/schemas/versioning.py +152 -0
- truthound_dashboard/static/index.html +2 -2
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/METADATA +147 -23
- truthound_dashboard-1.4.1.dist-info/RECORD +239 -0
- truthound_dashboard/static/assets/index-BZG20KuF.js +0 -586
- truthound_dashboard/static/assets/index-D_HyZ3pb.css +0 -1
- truthound_dashboard/static/assets/unmerged_dictionaries-CtpqQBm0.js +0 -1
- truthound_dashboard-1.3.1.dist-info/RECORD +0 -110
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"""Plugin State Machine.
|
|
2
|
+
|
|
3
|
+
This module provides the state machine implementation
|
|
4
|
+
for plugin lifecycle management.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from typing import Any, Callable, Awaitable
|
|
15
|
+
|
|
16
|
+
from .states import (
|
|
17
|
+
PluginState,
|
|
18
|
+
StateTransition,
|
|
19
|
+
StateTransitionError,
|
|
20
|
+
is_valid_transition,
|
|
21
|
+
get_valid_transitions,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Callback types
|
|
28
|
+
SyncCallback = Callable[["PluginStateMachine", StateTransition], None]
|
|
29
|
+
AsyncCallback = Callable[["PluginStateMachine", StateTransition], Awaitable[None]]
|
|
30
|
+
TransitionCallback = SyncCallback | AsyncCallback
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class PluginContext:
|
|
35
|
+
"""Context for plugin state machine.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
plugin_id: Plugin identifier.
|
|
39
|
+
version: Plugin version.
|
|
40
|
+
path: Plugin installation path.
|
|
41
|
+
config: Plugin configuration.
|
|
42
|
+
state_data: State-specific data.
|
|
43
|
+
error: Last error message.
|
|
44
|
+
last_error_at: When last error occurred.
|
|
45
|
+
retry_count: Number of retry attempts.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
plugin_id: str
|
|
49
|
+
version: str = ""
|
|
50
|
+
path: str = ""
|
|
51
|
+
config: dict[str, Any] = field(default_factory=dict)
|
|
52
|
+
state_data: dict[str, Any] = field(default_factory=dict)
|
|
53
|
+
error: str | None = None
|
|
54
|
+
last_error_at: datetime | None = None
|
|
55
|
+
retry_count: int = 0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PluginStateMachine:
|
|
59
|
+
"""State machine for managing plugin lifecycle.
|
|
60
|
+
|
|
61
|
+
This class provides:
|
|
62
|
+
- State transition management
|
|
63
|
+
- Transition callbacks (before/after)
|
|
64
|
+
- State history tracking
|
|
65
|
+
- Error recovery and rollback
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
plugin_id: str,
|
|
71
|
+
initial_state: PluginState = PluginState.DISCOVERED,
|
|
72
|
+
context: PluginContext | None = None,
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Initialize the state machine.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
plugin_id: Plugin identifier.
|
|
78
|
+
initial_state: Initial state.
|
|
79
|
+
context: Optional plugin context.
|
|
80
|
+
"""
|
|
81
|
+
self.plugin_id = plugin_id
|
|
82
|
+
self._state = initial_state
|
|
83
|
+
self._previous_state: PluginState | None = None
|
|
84
|
+
self._state_entered_at = datetime.utcnow()
|
|
85
|
+
self._history: list[StateTransition] = []
|
|
86
|
+
self._context = context or PluginContext(plugin_id=plugin_id)
|
|
87
|
+
|
|
88
|
+
# Callbacks
|
|
89
|
+
self._before_callbacks: dict[PluginState, list[TransitionCallback]] = {}
|
|
90
|
+
self._after_callbacks: dict[PluginState, list[TransitionCallback]] = {}
|
|
91
|
+
self._on_any_transition: list[TransitionCallback] = []
|
|
92
|
+
|
|
93
|
+
# Locking
|
|
94
|
+
self._lock = asyncio.Lock()
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def state(self) -> PluginState:
|
|
98
|
+
"""Get current state."""
|
|
99
|
+
return self._state
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def previous_state(self) -> PluginState | None:
|
|
103
|
+
"""Get previous state."""
|
|
104
|
+
return self._previous_state
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def context(self) -> PluginContext:
|
|
108
|
+
"""Get plugin context."""
|
|
109
|
+
return self._context
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def history(self) -> list[StateTransition]:
|
|
113
|
+
"""Get state transition history."""
|
|
114
|
+
return self._history.copy()
|
|
115
|
+
|
|
116
|
+
def can_transition_to(self, target_state: PluginState) -> bool:
|
|
117
|
+
"""Check if transition to target state is possible.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
target_state: Target state.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
True if transition is valid.
|
|
124
|
+
"""
|
|
125
|
+
return is_valid_transition(self._state, target_state)
|
|
126
|
+
|
|
127
|
+
def get_available_transitions(self) -> list[PluginState]:
|
|
128
|
+
"""Get states that can be transitioned to from current state.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
List of valid target states.
|
|
132
|
+
"""
|
|
133
|
+
return get_valid_transitions(self._state)
|
|
134
|
+
|
|
135
|
+
def transition(
|
|
136
|
+
self,
|
|
137
|
+
target_state: PluginState,
|
|
138
|
+
trigger: str = "",
|
|
139
|
+
metadata: dict[str, Any] | None = None,
|
|
140
|
+
) -> StateTransition:
|
|
141
|
+
"""Transition to a new state synchronously.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
target_state: Target state.
|
|
145
|
+
trigger: What triggered the transition.
|
|
146
|
+
metadata: Additional metadata.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
StateTransition record.
|
|
150
|
+
|
|
151
|
+
Raises:
|
|
152
|
+
StateTransitionError: If transition is invalid.
|
|
153
|
+
"""
|
|
154
|
+
if not is_valid_transition(self._state, target_state):
|
|
155
|
+
raise StateTransitionError(self._state, target_state)
|
|
156
|
+
|
|
157
|
+
# Calculate time in previous state
|
|
158
|
+
now = datetime.utcnow()
|
|
159
|
+
duration_ms = (now - self._state_entered_at).total_seconds() * 1000
|
|
160
|
+
|
|
161
|
+
# Create transition record
|
|
162
|
+
transition = StateTransition(
|
|
163
|
+
from_state=self._state,
|
|
164
|
+
to_state=target_state,
|
|
165
|
+
timestamp=now,
|
|
166
|
+
trigger=trigger,
|
|
167
|
+
duration_ms=duration_ms,
|
|
168
|
+
metadata=metadata or {},
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Execute before callbacks
|
|
172
|
+
self._execute_before_callbacks(transition)
|
|
173
|
+
|
|
174
|
+
# Perform transition
|
|
175
|
+
self._previous_state = self._state
|
|
176
|
+
self._state = target_state
|
|
177
|
+
self._state_entered_at = now
|
|
178
|
+
self._history.append(transition)
|
|
179
|
+
|
|
180
|
+
# Execute after callbacks
|
|
181
|
+
self._execute_after_callbacks(transition)
|
|
182
|
+
|
|
183
|
+
logger.info(
|
|
184
|
+
f"Plugin {self.plugin_id}: {transition.from_state.value} -> "
|
|
185
|
+
f"{transition.to_state.value} (trigger: {trigger})"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return transition
|
|
189
|
+
|
|
190
|
+
async def transition_async(
|
|
191
|
+
self,
|
|
192
|
+
target_state: PluginState,
|
|
193
|
+
trigger: str = "",
|
|
194
|
+
metadata: dict[str, Any] | None = None,
|
|
195
|
+
) -> StateTransition:
|
|
196
|
+
"""Transition to a new state asynchronously.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
target_state: Target state.
|
|
200
|
+
trigger: What triggered the transition.
|
|
201
|
+
metadata: Additional metadata.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
StateTransition record.
|
|
205
|
+
|
|
206
|
+
Raises:
|
|
207
|
+
StateTransitionError: If transition is invalid.
|
|
208
|
+
"""
|
|
209
|
+
async with self._lock:
|
|
210
|
+
if not is_valid_transition(self._state, target_state):
|
|
211
|
+
raise StateTransitionError(self._state, target_state)
|
|
212
|
+
|
|
213
|
+
# Calculate time in previous state
|
|
214
|
+
now = datetime.utcnow()
|
|
215
|
+
duration_ms = (now - self._state_entered_at).total_seconds() * 1000
|
|
216
|
+
|
|
217
|
+
# Create transition record
|
|
218
|
+
transition = StateTransition(
|
|
219
|
+
from_state=self._state,
|
|
220
|
+
to_state=target_state,
|
|
221
|
+
timestamp=now,
|
|
222
|
+
trigger=trigger,
|
|
223
|
+
duration_ms=duration_ms,
|
|
224
|
+
metadata=metadata or {},
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Execute before callbacks
|
|
228
|
+
await self._execute_before_callbacks_async(transition)
|
|
229
|
+
|
|
230
|
+
# Perform transition
|
|
231
|
+
self._previous_state = self._state
|
|
232
|
+
self._state = target_state
|
|
233
|
+
self._state_entered_at = now
|
|
234
|
+
self._history.append(transition)
|
|
235
|
+
|
|
236
|
+
# Execute after callbacks
|
|
237
|
+
await self._execute_after_callbacks_async(transition)
|
|
238
|
+
|
|
239
|
+
logger.info(
|
|
240
|
+
f"Plugin {self.plugin_id}: {transition.from_state.value} -> "
|
|
241
|
+
f"{transition.to_state.value} (trigger: {trigger})"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return transition
|
|
245
|
+
|
|
246
|
+
def fail(self, error: str, trigger: str = "error") -> StateTransition:
|
|
247
|
+
"""Transition to FAILED state.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
error: Error message.
|
|
251
|
+
trigger: What triggered the failure.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
StateTransition record.
|
|
255
|
+
"""
|
|
256
|
+
self._context.error = error
|
|
257
|
+
self._context.last_error_at = datetime.utcnow()
|
|
258
|
+
|
|
259
|
+
return self.transition(
|
|
260
|
+
PluginState.FAILED,
|
|
261
|
+
trigger=trigger,
|
|
262
|
+
metadata={"error": error},
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def rollback(self, trigger: str = "rollback") -> StateTransition | None:
|
|
266
|
+
"""Rollback to previous state if possible.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
trigger: What triggered the rollback.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
StateTransition record, or None if rollback not possible.
|
|
273
|
+
"""
|
|
274
|
+
if self._previous_state is None:
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
if not is_valid_transition(self._state, self._previous_state):
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
return self.transition(
|
|
281
|
+
self._previous_state,
|
|
282
|
+
trigger=trigger,
|
|
283
|
+
metadata={"rollback": True},
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def on_before(
|
|
287
|
+
self,
|
|
288
|
+
state: PluginState,
|
|
289
|
+
callback: TransitionCallback,
|
|
290
|
+
) -> None:
|
|
291
|
+
"""Register a callback to run before entering a state.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
state: Target state.
|
|
295
|
+
callback: Callback function.
|
|
296
|
+
"""
|
|
297
|
+
if state not in self._before_callbacks:
|
|
298
|
+
self._before_callbacks[state] = []
|
|
299
|
+
self._before_callbacks[state].append(callback)
|
|
300
|
+
|
|
301
|
+
def on_after(
|
|
302
|
+
self,
|
|
303
|
+
state: PluginState,
|
|
304
|
+
callback: TransitionCallback,
|
|
305
|
+
) -> None:
|
|
306
|
+
"""Register a callback to run after entering a state.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
state: Target state.
|
|
310
|
+
callback: Callback function.
|
|
311
|
+
"""
|
|
312
|
+
if state not in self._after_callbacks:
|
|
313
|
+
self._after_callbacks[state] = []
|
|
314
|
+
self._after_callbacks[state].append(callback)
|
|
315
|
+
|
|
316
|
+
def on_transition(self, callback: TransitionCallback) -> None:
|
|
317
|
+
"""Register a callback for any transition.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
callback: Callback function.
|
|
321
|
+
"""
|
|
322
|
+
self._on_any_transition.append(callback)
|
|
323
|
+
|
|
324
|
+
def _execute_before_callbacks(self, transition: StateTransition) -> None:
|
|
325
|
+
"""Execute before callbacks synchronously."""
|
|
326
|
+
callbacks = self._before_callbacks.get(transition.to_state, [])
|
|
327
|
+
for callback in callbacks:
|
|
328
|
+
try:
|
|
329
|
+
result = callback(self, transition)
|
|
330
|
+
if asyncio.iscoroutine(result):
|
|
331
|
+
# Run async callback synchronously
|
|
332
|
+
asyncio.get_event_loop().run_until_complete(result)
|
|
333
|
+
except Exception as e:
|
|
334
|
+
logger.error(f"Before callback error: {e}")
|
|
335
|
+
|
|
336
|
+
def _execute_after_callbacks(self, transition: StateTransition) -> None:
|
|
337
|
+
"""Execute after callbacks synchronously."""
|
|
338
|
+
callbacks = self._after_callbacks.get(transition.to_state, [])
|
|
339
|
+
callbacks.extend(self._on_any_transition)
|
|
340
|
+
for callback in callbacks:
|
|
341
|
+
try:
|
|
342
|
+
result = callback(self, transition)
|
|
343
|
+
if asyncio.iscoroutine(result):
|
|
344
|
+
asyncio.get_event_loop().run_until_complete(result)
|
|
345
|
+
except Exception as e:
|
|
346
|
+
logger.error(f"After callback error: {e}")
|
|
347
|
+
|
|
348
|
+
async def _execute_before_callbacks_async(
|
|
349
|
+
self, transition: StateTransition
|
|
350
|
+
) -> None:
|
|
351
|
+
"""Execute before callbacks asynchronously."""
|
|
352
|
+
callbacks = self._before_callbacks.get(transition.to_state, [])
|
|
353
|
+
for callback in callbacks:
|
|
354
|
+
try:
|
|
355
|
+
result = callback(self, transition)
|
|
356
|
+
if asyncio.iscoroutine(result):
|
|
357
|
+
await result
|
|
358
|
+
except Exception as e:
|
|
359
|
+
logger.error(f"Before callback error: {e}")
|
|
360
|
+
|
|
361
|
+
async def _execute_after_callbacks_async(
|
|
362
|
+
self, transition: StateTransition
|
|
363
|
+
) -> None:
|
|
364
|
+
"""Execute after callbacks asynchronously."""
|
|
365
|
+
callbacks = self._after_callbacks.get(transition.to_state, [])
|
|
366
|
+
callbacks.extend(self._on_any_transition)
|
|
367
|
+
for callback in callbacks:
|
|
368
|
+
try:
|
|
369
|
+
result = callback(self, transition)
|
|
370
|
+
if asyncio.iscoroutine(result):
|
|
371
|
+
await result
|
|
372
|
+
except Exception as e:
|
|
373
|
+
logger.error(f"After callback error: {e}")
|
|
374
|
+
|
|
375
|
+
def get_state_info(self) -> dict[str, Any]:
|
|
376
|
+
"""Get information about current state.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
State information dictionary.
|
|
380
|
+
"""
|
|
381
|
+
return {
|
|
382
|
+
"plugin_id": self.plugin_id,
|
|
383
|
+
"state": self._state.value,
|
|
384
|
+
"previous_state": self._previous_state.value if self._previous_state else None,
|
|
385
|
+
"state_entered_at": self._state_entered_at.isoformat(),
|
|
386
|
+
"available_transitions": [s.value for s in self.get_available_transitions()],
|
|
387
|
+
"history_length": len(self._history),
|
|
388
|
+
"context": {
|
|
389
|
+
"version": self._context.version,
|
|
390
|
+
"error": self._context.error,
|
|
391
|
+
"retry_count": self._context.retry_count,
|
|
392
|
+
},
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def create_state_machine(
|
|
397
|
+
plugin_id: str,
|
|
398
|
+
version: str = "",
|
|
399
|
+
initial_state: PluginState = PluginState.DISCOVERED,
|
|
400
|
+
) -> PluginStateMachine:
|
|
401
|
+
"""Create a new plugin state machine.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
plugin_id: Plugin identifier.
|
|
405
|
+
version: Plugin version.
|
|
406
|
+
initial_state: Initial state.
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
Configured PluginStateMachine.
|
|
410
|
+
"""
|
|
411
|
+
context = PluginContext(
|
|
412
|
+
plugin_id=plugin_id,
|
|
413
|
+
version=version,
|
|
414
|
+
)
|
|
415
|
+
return PluginStateMachine(
|
|
416
|
+
plugin_id=plugin_id,
|
|
417
|
+
initial_state=initial_state,
|
|
418
|
+
context=context,
|
|
419
|
+
)
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Plugin Lifecycle States.
|
|
2
|
+
|
|
3
|
+
This module defines the state machine states and transitions
|
|
4
|
+
for plugin lifecycle management.
|
|
5
|
+
|
|
6
|
+
State Diagram:
|
|
7
|
+
DISCOVERED --> LOADING --> LOADED --> ACTIVATING --> ACTIVE
|
|
8
|
+
| | | | |
|
|
9
|
+
v v v v v
|
|
10
|
+
FAILED FAILED FAILED FAILED DEACTIVATING
|
|
11
|
+
|
|
|
12
|
+
v
|
|
13
|
+
UNLOADING --> UNLOADED
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
from enum import Enum
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PluginState(str, Enum):
|
|
25
|
+
"""Plugin lifecycle states."""
|
|
26
|
+
|
|
27
|
+
# Initial state when plugin is found
|
|
28
|
+
DISCOVERED = "discovered"
|
|
29
|
+
|
|
30
|
+
# Plugin is being loaded (reading files, parsing)
|
|
31
|
+
LOADING = "loading"
|
|
32
|
+
|
|
33
|
+
# Plugin code is loaded but not active
|
|
34
|
+
LOADED = "loaded"
|
|
35
|
+
|
|
36
|
+
# Plugin is being activated (initializing)
|
|
37
|
+
ACTIVATING = "activating"
|
|
38
|
+
|
|
39
|
+
# Plugin is active and running
|
|
40
|
+
ACTIVE = "active"
|
|
41
|
+
|
|
42
|
+
# Plugin is being deactivated
|
|
43
|
+
DEACTIVATING = "deactivating"
|
|
44
|
+
|
|
45
|
+
# Plugin is being unloaded
|
|
46
|
+
UNLOADING = "unloading"
|
|
47
|
+
|
|
48
|
+
# Plugin has been unloaded
|
|
49
|
+
UNLOADED = "unloaded"
|
|
50
|
+
|
|
51
|
+
# Plugin failed to load or activate
|
|
52
|
+
FAILED = "failed"
|
|
53
|
+
|
|
54
|
+
# Plugin is being reloaded (hot reload)
|
|
55
|
+
RELOADING = "reloading"
|
|
56
|
+
|
|
57
|
+
# Plugin is being upgraded to new version
|
|
58
|
+
UPGRADING = "upgrading"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class StateTransitionError(Exception):
|
|
62
|
+
"""Raised when an invalid state transition is attempted."""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
current_state: PluginState,
|
|
67
|
+
target_state: PluginState,
|
|
68
|
+
message: str | None = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Initialize the error.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
current_state: Current plugin state.
|
|
74
|
+
target_state: Attempted target state.
|
|
75
|
+
message: Optional error message.
|
|
76
|
+
"""
|
|
77
|
+
self.current_state = current_state
|
|
78
|
+
self.target_state = target_state
|
|
79
|
+
msg = message or f"Cannot transition from {current_state.value} to {target_state.value}"
|
|
80
|
+
super().__init__(msg)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class StateTransition:
|
|
85
|
+
"""Represents a state transition event.
|
|
86
|
+
|
|
87
|
+
Attributes:
|
|
88
|
+
from_state: Previous state.
|
|
89
|
+
to_state: New state.
|
|
90
|
+
timestamp: When the transition occurred.
|
|
91
|
+
trigger: What triggered the transition.
|
|
92
|
+
duration_ms: Time spent in previous state.
|
|
93
|
+
metadata: Additional transition metadata.
|
|
94
|
+
error: Error message if transition failed.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
from_state: PluginState
|
|
98
|
+
to_state: PluginState
|
|
99
|
+
timestamp: datetime = field(default_factory=datetime.utcnow)
|
|
100
|
+
trigger: str = ""
|
|
101
|
+
duration_ms: float = 0
|
|
102
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
103
|
+
error: str | None = None
|
|
104
|
+
|
|
105
|
+
def to_dict(self) -> dict[str, Any]:
|
|
106
|
+
"""Convert to dictionary."""
|
|
107
|
+
return {
|
|
108
|
+
"from_state": self.from_state.value,
|
|
109
|
+
"to_state": self.to_state.value,
|
|
110
|
+
"timestamp": self.timestamp.isoformat(),
|
|
111
|
+
"trigger": self.trigger,
|
|
112
|
+
"duration_ms": self.duration_ms,
|
|
113
|
+
"metadata": self.metadata,
|
|
114
|
+
"error": self.error,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# Valid state transitions
|
|
119
|
+
VALID_TRANSITIONS: dict[PluginState, set[PluginState]] = {
|
|
120
|
+
PluginState.DISCOVERED: {
|
|
121
|
+
PluginState.LOADING,
|
|
122
|
+
PluginState.FAILED,
|
|
123
|
+
PluginState.UNLOADED,
|
|
124
|
+
},
|
|
125
|
+
PluginState.LOADING: {
|
|
126
|
+
PluginState.LOADED,
|
|
127
|
+
PluginState.FAILED,
|
|
128
|
+
},
|
|
129
|
+
PluginState.LOADED: {
|
|
130
|
+
PluginState.ACTIVATING,
|
|
131
|
+
PluginState.UNLOADING,
|
|
132
|
+
PluginState.FAILED,
|
|
133
|
+
},
|
|
134
|
+
PluginState.ACTIVATING: {
|
|
135
|
+
PluginState.ACTIVE,
|
|
136
|
+
PluginState.FAILED,
|
|
137
|
+
PluginState.LOADED, # Rollback on failure
|
|
138
|
+
},
|
|
139
|
+
PluginState.ACTIVE: {
|
|
140
|
+
PluginState.DEACTIVATING,
|
|
141
|
+
PluginState.RELOADING,
|
|
142
|
+
PluginState.UPGRADING,
|
|
143
|
+
PluginState.FAILED,
|
|
144
|
+
},
|
|
145
|
+
PluginState.DEACTIVATING: {
|
|
146
|
+
PluginState.LOADED,
|
|
147
|
+
PluginState.UNLOADING,
|
|
148
|
+
PluginState.FAILED,
|
|
149
|
+
},
|
|
150
|
+
PluginState.UNLOADING: {
|
|
151
|
+
PluginState.UNLOADED,
|
|
152
|
+
PluginState.FAILED,
|
|
153
|
+
},
|
|
154
|
+
PluginState.UNLOADED: {
|
|
155
|
+
PluginState.DISCOVERED, # Re-install
|
|
156
|
+
PluginState.LOADING, # Re-load
|
|
157
|
+
},
|
|
158
|
+
PluginState.FAILED: {
|
|
159
|
+
PluginState.DISCOVERED, # Retry discovery
|
|
160
|
+
PluginState.LOADING, # Retry load
|
|
161
|
+
PluginState.UNLOADING, # Unload failed plugin
|
|
162
|
+
},
|
|
163
|
+
PluginState.RELOADING: {
|
|
164
|
+
PluginState.ACTIVE,
|
|
165
|
+
PluginState.FAILED,
|
|
166
|
+
PluginState.LOADED, # Rollback
|
|
167
|
+
},
|
|
168
|
+
PluginState.UPGRADING: {
|
|
169
|
+
PluginState.ACTIVE,
|
|
170
|
+
PluginState.FAILED,
|
|
171
|
+
PluginState.LOADED, # Rollback
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def is_valid_transition(from_state: PluginState, to_state: PluginState) -> bool:
|
|
177
|
+
"""Check if a state transition is valid.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
from_state: Current state.
|
|
181
|
+
to_state: Target state.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
True if transition is valid.
|
|
185
|
+
"""
|
|
186
|
+
valid_targets = VALID_TRANSITIONS.get(from_state, set())
|
|
187
|
+
return to_state in valid_targets
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def get_valid_transitions(state: PluginState) -> list[PluginState]:
|
|
191
|
+
"""Get valid target states from a given state.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
state: Current state.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
List of valid target states.
|
|
198
|
+
"""
|
|
199
|
+
return list(VALID_TRANSITIONS.get(state, []))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def can_activate(state: PluginState) -> bool:
|
|
203
|
+
"""Check if plugin can be activated from current state.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
state: Current state.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
True if activation is possible.
|
|
210
|
+
"""
|
|
211
|
+
return state in {PluginState.LOADED, PluginState.FAILED}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def can_deactivate(state: PluginState) -> bool:
|
|
215
|
+
"""Check if plugin can be deactivated from current state.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
state: Current state.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
True if deactivation is possible.
|
|
222
|
+
"""
|
|
223
|
+
return state == PluginState.ACTIVE
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def is_running(state: PluginState) -> bool:
|
|
227
|
+
"""Check if plugin is in a running state.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
state: Current state.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
True if plugin is running.
|
|
234
|
+
"""
|
|
235
|
+
return state == PluginState.ACTIVE
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def is_transitioning(state: PluginState) -> bool:
|
|
239
|
+
"""Check if plugin is in a transitional state.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
state: Current state.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
True if plugin is transitioning.
|
|
246
|
+
"""
|
|
247
|
+
return state in {
|
|
248
|
+
PluginState.LOADING,
|
|
249
|
+
PluginState.ACTIVATING,
|
|
250
|
+
PluginState.DEACTIVATING,
|
|
251
|
+
PluginState.UNLOADING,
|
|
252
|
+
PluginState.RELOADING,
|
|
253
|
+
PluginState.UPGRADING,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def is_terminal(state: PluginState) -> bool:
|
|
258
|
+
"""Check if plugin is in a terminal state.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
state: Current state.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
True if plugin is in terminal state.
|
|
265
|
+
"""
|
|
266
|
+
return state in {PluginState.UNLOADED, PluginState.FAILED}
|