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,403 @@
|
|
|
1
|
+
"""Hook Manager Implementation.
|
|
2
|
+
|
|
3
|
+
This module provides the main hook manager that coordinates
|
|
4
|
+
hook registration and execution.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
import time
|
|
12
|
+
import uuid
|
|
13
|
+
from collections import defaultdict
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from .protocols import (
|
|
17
|
+
HookContext,
|
|
18
|
+
HookHandler,
|
|
19
|
+
HookPriority,
|
|
20
|
+
HookRegistration,
|
|
21
|
+
HookResult,
|
|
22
|
+
HookType,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class HookManager:
|
|
29
|
+
"""Manages hook registration and execution.
|
|
30
|
+
|
|
31
|
+
This class provides:
|
|
32
|
+
- Hook registration with priority ordering
|
|
33
|
+
- Synchronous and asynchronous execution
|
|
34
|
+
- Conditional execution based on context
|
|
35
|
+
- Result aggregation
|
|
36
|
+
- Error handling and recovery
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self) -> None:
|
|
40
|
+
"""Initialize the hook manager."""
|
|
41
|
+
self._registrations: dict[str, HookRegistration] = {}
|
|
42
|
+
self._hooks: dict[HookType, list[str]] = defaultdict(list)
|
|
43
|
+
self._lock = asyncio.Lock()
|
|
44
|
+
|
|
45
|
+
def register(
|
|
46
|
+
self,
|
|
47
|
+
hook_type: HookType | str,
|
|
48
|
+
handler: HookHandler,
|
|
49
|
+
plugin_id: str | None = None,
|
|
50
|
+
priority: HookPriority | int = HookPriority.NORMAL,
|
|
51
|
+
conditions: dict[str, Any] | None = None,
|
|
52
|
+
handler_name: str | None = None,
|
|
53
|
+
metadata: dict[str, Any] | None = None,
|
|
54
|
+
) -> str:
|
|
55
|
+
"""Register a hook handler.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
hook_type: Type of hook to register for.
|
|
59
|
+
handler: Handler function.
|
|
60
|
+
plugin_id: ID of the registering plugin.
|
|
61
|
+
priority: Execution priority (lower = earlier).
|
|
62
|
+
conditions: Conditions for executing the handler.
|
|
63
|
+
handler_name: Optional handler name.
|
|
64
|
+
metadata: Additional metadata.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Registration ID for later unregistration.
|
|
68
|
+
"""
|
|
69
|
+
if isinstance(hook_type, str):
|
|
70
|
+
hook_type = HookType(hook_type)
|
|
71
|
+
|
|
72
|
+
if isinstance(priority, int) and not isinstance(priority, HookPriority):
|
|
73
|
+
# Convert int to closest priority
|
|
74
|
+
for p in HookPriority:
|
|
75
|
+
if priority <= p.value:
|
|
76
|
+
priority = p
|
|
77
|
+
break
|
|
78
|
+
else:
|
|
79
|
+
priority = HookPriority.LOWEST
|
|
80
|
+
|
|
81
|
+
registration_id = str(uuid.uuid4())
|
|
82
|
+
registration = HookRegistration(
|
|
83
|
+
hook_type=hook_type,
|
|
84
|
+
handler=handler,
|
|
85
|
+
plugin_id=plugin_id,
|
|
86
|
+
handler_name=handler_name or getattr(handler, "__name__", "unknown"),
|
|
87
|
+
priority=priority,
|
|
88
|
+
conditions=conditions or {},
|
|
89
|
+
metadata=metadata or {},
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
self._registrations[registration_id] = registration
|
|
93
|
+
self._hooks[hook_type].append(registration_id)
|
|
94
|
+
|
|
95
|
+
# Sort by priority
|
|
96
|
+
self._hooks[hook_type].sort(
|
|
97
|
+
key=lambda rid: self._registrations[rid].priority.value
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
logger.debug(
|
|
101
|
+
f"Registered hook handler '{registration.handler_name}' "
|
|
102
|
+
f"for {hook_type.value} with priority {priority.value}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return registration_id
|
|
106
|
+
|
|
107
|
+
def unregister(self, registration_id: str) -> bool:
|
|
108
|
+
"""Unregister a hook handler.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
registration_id: Registration ID to remove.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
True if successfully unregistered.
|
|
115
|
+
"""
|
|
116
|
+
if registration_id not in self._registrations:
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
registration = self._registrations[registration_id]
|
|
120
|
+
hook_type = registration.hook_type
|
|
121
|
+
|
|
122
|
+
if registration_id in self._hooks[hook_type]:
|
|
123
|
+
self._hooks[hook_type].remove(registration_id)
|
|
124
|
+
|
|
125
|
+
del self._registrations[registration_id]
|
|
126
|
+
|
|
127
|
+
logger.debug(
|
|
128
|
+
f"Unregistered hook handler '{registration.handler_name}' "
|
|
129
|
+
f"from {hook_type.value}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
def unregister_plugin(self, plugin_id: str) -> int:
|
|
135
|
+
"""Unregister all handlers for a plugin.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
plugin_id: Plugin ID.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Number of handlers unregistered.
|
|
142
|
+
"""
|
|
143
|
+
to_remove = [
|
|
144
|
+
rid
|
|
145
|
+
for rid, reg in self._registrations.items()
|
|
146
|
+
if reg.plugin_id == plugin_id
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
for rid in to_remove:
|
|
150
|
+
self.unregister(rid)
|
|
151
|
+
|
|
152
|
+
logger.debug(f"Unregistered {len(to_remove)} handlers for plugin {plugin_id}")
|
|
153
|
+
|
|
154
|
+
return len(to_remove)
|
|
155
|
+
|
|
156
|
+
def get_handlers(self, hook_type: HookType | str) -> list[HookRegistration]:
|
|
157
|
+
"""Get all registered handlers for a hook type.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
hook_type: Type of hook.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
List of registrations, sorted by priority.
|
|
164
|
+
"""
|
|
165
|
+
if isinstance(hook_type, str):
|
|
166
|
+
hook_type = HookType(hook_type)
|
|
167
|
+
|
|
168
|
+
return [
|
|
169
|
+
self._registrations[rid]
|
|
170
|
+
for rid in self._hooks.get(hook_type, [])
|
|
171
|
+
if rid in self._registrations
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
def execute(
|
|
175
|
+
self,
|
|
176
|
+
hook_type: HookType | str,
|
|
177
|
+
data: dict[str, Any] | None = None,
|
|
178
|
+
metadata: dict[str, Any] | None = None,
|
|
179
|
+
stop_on_cancel: bool = True,
|
|
180
|
+
stop_on_error: bool = False,
|
|
181
|
+
) -> tuple[HookContext, list[HookResult]]:
|
|
182
|
+
"""Execute all handlers for a hook type synchronously.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
hook_type: Type of hook.
|
|
186
|
+
data: Data to pass to handlers.
|
|
187
|
+
metadata: Additional metadata.
|
|
188
|
+
stop_on_cancel: Stop if a handler cancels the operation.
|
|
189
|
+
stop_on_error: Stop if a handler raises an error.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Tuple of (final context, list of results).
|
|
193
|
+
"""
|
|
194
|
+
if isinstance(hook_type, str):
|
|
195
|
+
hook_type = HookType(hook_type)
|
|
196
|
+
|
|
197
|
+
context = HookContext(
|
|
198
|
+
hook_type=hook_type,
|
|
199
|
+
data=data or {},
|
|
200
|
+
metadata=metadata or {},
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
handlers = self.get_handlers(hook_type)
|
|
204
|
+
results: list[HookResult] = []
|
|
205
|
+
|
|
206
|
+
for registration in handlers:
|
|
207
|
+
if not registration.enabled:
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
if not registration.matches_conditions(context):
|
|
211
|
+
results.append(
|
|
212
|
+
HookResult(
|
|
213
|
+
success=True,
|
|
214
|
+
plugin_id=registration.plugin_id,
|
|
215
|
+
handler_name=registration.handler_name,
|
|
216
|
+
skipped=True,
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
continue
|
|
220
|
+
|
|
221
|
+
result = self._execute_handler(registration, context)
|
|
222
|
+
results.append(result)
|
|
223
|
+
context.results.append(result)
|
|
224
|
+
|
|
225
|
+
if not result.success and stop_on_error:
|
|
226
|
+
logger.warning(
|
|
227
|
+
f"Hook execution stopped due to error in "
|
|
228
|
+
f"'{registration.handler_name}': {result.error}"
|
|
229
|
+
)
|
|
230
|
+
break
|
|
231
|
+
|
|
232
|
+
if context.cancelled and stop_on_cancel:
|
|
233
|
+
logger.debug(
|
|
234
|
+
f"Hook execution cancelled by '{registration.handler_name}'"
|
|
235
|
+
)
|
|
236
|
+
break
|
|
237
|
+
|
|
238
|
+
return context, results
|
|
239
|
+
|
|
240
|
+
async def execute_async(
|
|
241
|
+
self,
|
|
242
|
+
hook_type: HookType | str,
|
|
243
|
+
data: dict[str, Any] | None = None,
|
|
244
|
+
metadata: dict[str, Any] | None = None,
|
|
245
|
+
stop_on_cancel: bool = True,
|
|
246
|
+
stop_on_error: bool = False,
|
|
247
|
+
) -> tuple[HookContext, list[HookResult]]:
|
|
248
|
+
"""Execute all handlers for a hook type asynchronously.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
hook_type: Type of hook.
|
|
252
|
+
data: Data to pass to handlers.
|
|
253
|
+
metadata: Additional metadata.
|
|
254
|
+
stop_on_cancel: Stop if a handler cancels the operation.
|
|
255
|
+
stop_on_error: Stop if a handler raises an error.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Tuple of (final context, list of results).
|
|
259
|
+
"""
|
|
260
|
+
async with self._lock:
|
|
261
|
+
return self.execute(
|
|
262
|
+
hook_type,
|
|
263
|
+
data,
|
|
264
|
+
metadata,
|
|
265
|
+
stop_on_cancel,
|
|
266
|
+
stop_on_error,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def _execute_handler(
|
|
270
|
+
self,
|
|
271
|
+
registration: HookRegistration,
|
|
272
|
+
context: HookContext,
|
|
273
|
+
) -> HookResult:
|
|
274
|
+
"""Execute a single handler.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
registration: Handler registration.
|
|
278
|
+
context: Hook context.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Handler result.
|
|
282
|
+
"""
|
|
283
|
+
start_time = time.perf_counter()
|
|
284
|
+
context.plugin_id = registration.plugin_id
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
handler_result = registration.handler(context)
|
|
288
|
+
|
|
289
|
+
execution_time = (time.perf_counter() - start_time) * 1000
|
|
290
|
+
|
|
291
|
+
if handler_result is None:
|
|
292
|
+
return HookResult(
|
|
293
|
+
success=True,
|
|
294
|
+
plugin_id=registration.plugin_id,
|
|
295
|
+
handler_name=registration.handler_name,
|
|
296
|
+
execution_time_ms=execution_time,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
handler_result.execution_time_ms = execution_time
|
|
300
|
+
handler_result.plugin_id = registration.plugin_id
|
|
301
|
+
handler_result.handler_name = registration.handler_name
|
|
302
|
+
|
|
303
|
+
return handler_result
|
|
304
|
+
|
|
305
|
+
except Exception as e:
|
|
306
|
+
execution_time = (time.perf_counter() - start_time) * 1000
|
|
307
|
+
logger.error(
|
|
308
|
+
f"Error in hook handler '{registration.handler_name}': {e}",
|
|
309
|
+
exc_info=True,
|
|
310
|
+
)
|
|
311
|
+
return HookResult(
|
|
312
|
+
success=False,
|
|
313
|
+
plugin_id=registration.plugin_id,
|
|
314
|
+
handler_name=registration.handler_name,
|
|
315
|
+
execution_time_ms=execution_time,
|
|
316
|
+
error=str(e),
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
def create_context(
|
|
320
|
+
self,
|
|
321
|
+
hook_type: HookType | str,
|
|
322
|
+
data: dict[str, Any] | None = None,
|
|
323
|
+
metadata: dict[str, Any] | None = None,
|
|
324
|
+
) -> HookContext:
|
|
325
|
+
"""Create a hook context.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
hook_type: Type of hook.
|
|
329
|
+
data: Context data.
|
|
330
|
+
metadata: Additional metadata.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
New HookContext.
|
|
334
|
+
"""
|
|
335
|
+
if isinstance(hook_type, str):
|
|
336
|
+
hook_type = HookType(hook_type)
|
|
337
|
+
|
|
338
|
+
return HookContext(
|
|
339
|
+
hook_type=hook_type,
|
|
340
|
+
data=data or {},
|
|
341
|
+
metadata=metadata or {},
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
def list_hooks(self) -> dict[str, list[dict[str, Any]]]:
|
|
345
|
+
"""List all registered hooks.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
Dict mapping hook types to list of handler info.
|
|
349
|
+
"""
|
|
350
|
+
result: dict[str, list[dict[str, Any]]] = {}
|
|
351
|
+
|
|
352
|
+
for hook_type in HookType:
|
|
353
|
+
handlers = self.get_handlers(hook_type)
|
|
354
|
+
if handlers:
|
|
355
|
+
result[hook_type.value] = [
|
|
356
|
+
{
|
|
357
|
+
"handler_name": h.handler_name,
|
|
358
|
+
"plugin_id": h.plugin_id,
|
|
359
|
+
"priority": h.priority.value,
|
|
360
|
+
"enabled": h.enabled,
|
|
361
|
+
"conditions": h.conditions,
|
|
362
|
+
}
|
|
363
|
+
for h in handlers
|
|
364
|
+
]
|
|
365
|
+
|
|
366
|
+
return result
|
|
367
|
+
|
|
368
|
+
def enable_handler(self, registration_id: str) -> bool:
|
|
369
|
+
"""Enable a handler.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
registration_id: Registration ID.
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
True if successful.
|
|
376
|
+
"""
|
|
377
|
+
if registration_id in self._registrations:
|
|
378
|
+
self._registrations[registration_id].enabled = True
|
|
379
|
+
return True
|
|
380
|
+
return False
|
|
381
|
+
|
|
382
|
+
def disable_handler(self, registration_id: str) -> bool:
|
|
383
|
+
"""Disable a handler.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
registration_id: Registration ID.
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
True if successful.
|
|
390
|
+
"""
|
|
391
|
+
if registration_id in self._registrations:
|
|
392
|
+
self._registrations[registration_id].enabled = False
|
|
393
|
+
return True
|
|
394
|
+
return False
|
|
395
|
+
|
|
396
|
+
def clear(self) -> None:
|
|
397
|
+
"""Clear all registrations."""
|
|
398
|
+
self._registrations.clear()
|
|
399
|
+
self._hooks.clear()
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# Global hook manager instance
|
|
403
|
+
hook_manager = HookManager()
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""Hook System Protocol Definitions.
|
|
2
|
+
|
|
3
|
+
This module defines the core protocols and data types for the
|
|
4
|
+
plugin hook system.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import Any, Callable, Protocol
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HookType(str, Enum):
|
|
16
|
+
"""Types of hooks available in the system."""
|
|
17
|
+
|
|
18
|
+
# Validation hooks
|
|
19
|
+
BEFORE_VALIDATION = "before_validation"
|
|
20
|
+
AFTER_VALIDATION = "after_validation"
|
|
21
|
+
ON_ISSUE_FOUND = "on_issue_found"
|
|
22
|
+
|
|
23
|
+
# Profiling hooks
|
|
24
|
+
BEFORE_PROFILE = "before_profile"
|
|
25
|
+
AFTER_PROFILE = "after_profile"
|
|
26
|
+
|
|
27
|
+
# Report hooks
|
|
28
|
+
ON_REPORT_GENERATE = "on_report_generate"
|
|
29
|
+
BEFORE_REPORT_SEND = "before_report_send"
|
|
30
|
+
AFTER_REPORT_SEND = "after_report_send"
|
|
31
|
+
|
|
32
|
+
# Error hooks
|
|
33
|
+
ON_ERROR = "on_error"
|
|
34
|
+
ON_RETRY = "on_retry"
|
|
35
|
+
|
|
36
|
+
# Plugin lifecycle hooks
|
|
37
|
+
ON_PLUGIN_LOAD = "on_plugin_load"
|
|
38
|
+
ON_PLUGIN_UNLOAD = "on_plugin_unload"
|
|
39
|
+
ON_PLUGIN_ENABLE = "on_plugin_enable"
|
|
40
|
+
ON_PLUGIN_DISABLE = "on_plugin_disable"
|
|
41
|
+
|
|
42
|
+
# Data source hooks
|
|
43
|
+
ON_SOURCE_CONNECT = "on_source_connect"
|
|
44
|
+
ON_SOURCE_DISCONNECT = "on_source_disconnect"
|
|
45
|
+
|
|
46
|
+
# Schema hooks
|
|
47
|
+
ON_SCHEMA_CHANGE = "on_schema_change"
|
|
48
|
+
ON_SCHEMA_DRIFT = "on_schema_drift"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class HookPriority(int, Enum):
|
|
52
|
+
"""Hook execution priority (lower = earlier)."""
|
|
53
|
+
|
|
54
|
+
HIGHEST = 0
|
|
55
|
+
HIGH = 25
|
|
56
|
+
NORMAL = 50
|
|
57
|
+
LOW = 75
|
|
58
|
+
LOWEST = 100
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class HookContext:
|
|
63
|
+
"""Context passed to hook handlers.
|
|
64
|
+
|
|
65
|
+
Attributes:
|
|
66
|
+
hook_type: Type of hook being executed.
|
|
67
|
+
plugin_id: ID of the plugin that registered this hook.
|
|
68
|
+
timestamp: When the hook was triggered.
|
|
69
|
+
data: Hook-specific data.
|
|
70
|
+
metadata: Additional metadata.
|
|
71
|
+
results: Results from previous handlers in the chain.
|
|
72
|
+
cancelled: Whether the operation should be cancelled.
|
|
73
|
+
modified_data: Data modified by handlers.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
hook_type: HookType
|
|
77
|
+
plugin_id: str | None = None
|
|
78
|
+
timestamp: datetime = field(default_factory=datetime.utcnow)
|
|
79
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
80
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
81
|
+
results: list["HookResult"] = field(default_factory=list)
|
|
82
|
+
cancelled: bool = False
|
|
83
|
+
modified_data: dict[str, Any] = field(default_factory=dict)
|
|
84
|
+
|
|
85
|
+
def cancel(self) -> None:
|
|
86
|
+
"""Cancel the current operation."""
|
|
87
|
+
self.cancelled = True
|
|
88
|
+
|
|
89
|
+
def modify(self, key: str, value: Any) -> None:
|
|
90
|
+
"""Modify data that will be passed to the next handler.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
key: Data key to modify.
|
|
94
|
+
value: New value.
|
|
95
|
+
"""
|
|
96
|
+
self.modified_data[key] = value
|
|
97
|
+
|
|
98
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
99
|
+
"""Get data from context, preferring modified data.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
key: Data key.
|
|
103
|
+
default: Default value if not found.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Value from modified_data or data.
|
|
107
|
+
"""
|
|
108
|
+
if key in self.modified_data:
|
|
109
|
+
return self.modified_data[key]
|
|
110
|
+
return self.data.get(key, default)
|
|
111
|
+
|
|
112
|
+
def to_dict(self) -> dict[str, Any]:
|
|
113
|
+
"""Convert to dictionary."""
|
|
114
|
+
return {
|
|
115
|
+
"hook_type": self.hook_type.value,
|
|
116
|
+
"plugin_id": self.plugin_id,
|
|
117
|
+
"timestamp": self.timestamp.isoformat(),
|
|
118
|
+
"data": self.data,
|
|
119
|
+
"metadata": self.metadata,
|
|
120
|
+
"cancelled": self.cancelled,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class HookResult:
|
|
126
|
+
"""Result from a hook handler.
|
|
127
|
+
|
|
128
|
+
Attributes:
|
|
129
|
+
success: Whether the handler executed successfully.
|
|
130
|
+
plugin_id: ID of the plugin that handled this hook.
|
|
131
|
+
handler_name: Name of the handler function.
|
|
132
|
+
execution_time_ms: Execution time in milliseconds.
|
|
133
|
+
data: Data returned by the handler.
|
|
134
|
+
error: Error message if failed.
|
|
135
|
+
skipped: Whether the handler was skipped.
|
|
136
|
+
modified_keys: Keys that were modified by this handler.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
success: bool
|
|
140
|
+
plugin_id: str | None = None
|
|
141
|
+
handler_name: str = ""
|
|
142
|
+
execution_time_ms: float = 0
|
|
143
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
144
|
+
error: str | None = None
|
|
145
|
+
skipped: bool = False
|
|
146
|
+
modified_keys: list[str] = field(default_factory=list)
|
|
147
|
+
|
|
148
|
+
def to_dict(self) -> dict[str, Any]:
|
|
149
|
+
"""Convert to dictionary."""
|
|
150
|
+
return {
|
|
151
|
+
"success": self.success,
|
|
152
|
+
"plugin_id": self.plugin_id,
|
|
153
|
+
"handler_name": self.handler_name,
|
|
154
|
+
"execution_time_ms": self.execution_time_ms,
|
|
155
|
+
"data": self.data,
|
|
156
|
+
"error": self.error,
|
|
157
|
+
"skipped": self.skipped,
|
|
158
|
+
"modified_keys": self.modified_keys,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# Type alias for hook handlers
|
|
163
|
+
HookHandler = Callable[[HookContext], HookResult | None]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass
|
|
167
|
+
class HookRegistration:
|
|
168
|
+
"""Registration information for a hook handler.
|
|
169
|
+
|
|
170
|
+
Attributes:
|
|
171
|
+
hook_type: Type of hook.
|
|
172
|
+
handler: Handler function.
|
|
173
|
+
plugin_id: ID of the registering plugin.
|
|
174
|
+
handler_name: Name of the handler.
|
|
175
|
+
priority: Execution priority.
|
|
176
|
+
enabled: Whether the handler is enabled.
|
|
177
|
+
conditions: Conditions for executing the handler.
|
|
178
|
+
metadata: Additional metadata.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
hook_type: HookType
|
|
182
|
+
handler: HookHandler
|
|
183
|
+
plugin_id: str | None = None
|
|
184
|
+
handler_name: str = ""
|
|
185
|
+
priority: HookPriority = HookPriority.NORMAL
|
|
186
|
+
enabled: bool = True
|
|
187
|
+
conditions: dict[str, Any] = field(default_factory=dict)
|
|
188
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
189
|
+
|
|
190
|
+
def __post_init__(self) -> None:
|
|
191
|
+
"""Extract handler name if not provided."""
|
|
192
|
+
if not self.handler_name and hasattr(self.handler, "__name__"):
|
|
193
|
+
self.handler_name = self.handler.__name__
|
|
194
|
+
|
|
195
|
+
def matches_conditions(self, context: HookContext) -> bool:
|
|
196
|
+
"""Check if conditions match the context.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
context: Hook context to check against.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
True if all conditions are satisfied.
|
|
203
|
+
"""
|
|
204
|
+
if not self.conditions:
|
|
205
|
+
return True
|
|
206
|
+
|
|
207
|
+
for key, expected in self.conditions.items():
|
|
208
|
+
actual = context.data.get(key) or context.metadata.get(key)
|
|
209
|
+
if actual != expected:
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
return True
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class HookRegistry(Protocol):
|
|
216
|
+
"""Protocol for hook registries."""
|
|
217
|
+
|
|
218
|
+
def register(
|
|
219
|
+
self,
|
|
220
|
+
hook_type: HookType,
|
|
221
|
+
handler: HookHandler,
|
|
222
|
+
plugin_id: str | None = None,
|
|
223
|
+
priority: HookPriority = HookPriority.NORMAL,
|
|
224
|
+
conditions: dict[str, Any] | None = None,
|
|
225
|
+
) -> str:
|
|
226
|
+
"""Register a hook handler.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
hook_type: Type of hook.
|
|
230
|
+
handler: Handler function.
|
|
231
|
+
plugin_id: ID of the registering plugin.
|
|
232
|
+
priority: Execution priority.
|
|
233
|
+
conditions: Conditions for executing.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Registration ID.
|
|
237
|
+
"""
|
|
238
|
+
...
|
|
239
|
+
|
|
240
|
+
def unregister(self, registration_id: str) -> bool:
|
|
241
|
+
"""Unregister a hook handler.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
registration_id: Registration ID.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
True if successfully unregistered.
|
|
248
|
+
"""
|
|
249
|
+
...
|
|
250
|
+
|
|
251
|
+
def execute(
|
|
252
|
+
self,
|
|
253
|
+
hook_type: HookType,
|
|
254
|
+
context: HookContext,
|
|
255
|
+
) -> list[HookResult]:
|
|
256
|
+
"""Execute all handlers for a hook type.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
hook_type: Type of hook.
|
|
260
|
+
context: Hook context.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
List of results from handlers.
|
|
264
|
+
"""
|
|
265
|
+
...
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Plugin Lifecycle Management.
|
|
2
|
+
|
|
3
|
+
This module provides:
|
|
4
|
+
- State machine for plugin lifecycle
|
|
5
|
+
- Hot reload capability
|
|
6
|
+
- File watching for auto-reload
|
|
7
|
+
- Graceful shutdown and rollback
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from .states import (
|
|
13
|
+
PluginState,
|
|
14
|
+
StateTransition,
|
|
15
|
+
StateTransitionError,
|
|
16
|
+
)
|
|
17
|
+
from .machine import (
|
|
18
|
+
PluginStateMachine,
|
|
19
|
+
create_state_machine,
|
|
20
|
+
)
|
|
21
|
+
from .hot_reload import (
|
|
22
|
+
HotReloadManager,
|
|
23
|
+
FileWatcher,
|
|
24
|
+
ReloadResult,
|
|
25
|
+
ReloadStrategy,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
# States
|
|
30
|
+
"PluginState",
|
|
31
|
+
"StateTransition",
|
|
32
|
+
"StateTransitionError",
|
|
33
|
+
# Machine
|
|
34
|
+
"PluginStateMachine",
|
|
35
|
+
"create_state_machine",
|
|
36
|
+
# Hot Reload
|
|
37
|
+
"HotReloadManager",
|
|
38
|
+
"FileWatcher",
|
|
39
|
+
"ReloadResult",
|
|
40
|
+
"ReloadStrategy",
|
|
41
|
+
]
|