truthound-dashboard 1.3.0__py3-none-any.whl → 1.4.0__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.
Files changed (169) hide show
  1. truthound_dashboard/api/alerts.py +258 -0
  2. truthound_dashboard/api/anomaly.py +1302 -0
  3. truthound_dashboard/api/cross_alerts.py +352 -0
  4. truthound_dashboard/api/deps.py +143 -0
  5. truthound_dashboard/api/drift_monitor.py +540 -0
  6. truthound_dashboard/api/lineage.py +1151 -0
  7. truthound_dashboard/api/maintenance.py +363 -0
  8. truthound_dashboard/api/middleware.py +373 -1
  9. truthound_dashboard/api/model_monitoring.py +805 -0
  10. truthound_dashboard/api/notifications_advanced.py +2452 -0
  11. truthound_dashboard/api/plugins.py +2096 -0
  12. truthound_dashboard/api/profile.py +211 -14
  13. truthound_dashboard/api/reports.py +853 -0
  14. truthound_dashboard/api/router.py +147 -0
  15. truthound_dashboard/api/rule_suggestions.py +310 -0
  16. truthound_dashboard/api/schema_evolution.py +231 -0
  17. truthound_dashboard/api/sources.py +47 -3
  18. truthound_dashboard/api/triggers.py +190 -0
  19. truthound_dashboard/api/validations.py +13 -0
  20. truthound_dashboard/api/validators.py +333 -4
  21. truthound_dashboard/api/versioning.py +309 -0
  22. truthound_dashboard/api/websocket.py +301 -0
  23. truthound_dashboard/core/__init__.py +27 -0
  24. truthound_dashboard/core/anomaly.py +1395 -0
  25. truthound_dashboard/core/anomaly_explainer.py +633 -0
  26. truthound_dashboard/core/cache.py +206 -0
  27. truthound_dashboard/core/cached_services.py +422 -0
  28. truthound_dashboard/core/charts.py +352 -0
  29. truthound_dashboard/core/connections.py +1069 -42
  30. truthound_dashboard/core/cross_alerts.py +837 -0
  31. truthound_dashboard/core/drift_monitor.py +1477 -0
  32. truthound_dashboard/core/drift_sampling.py +669 -0
  33. truthound_dashboard/core/i18n/__init__.py +42 -0
  34. truthound_dashboard/core/i18n/detector.py +173 -0
  35. truthound_dashboard/core/i18n/messages.py +564 -0
  36. truthound_dashboard/core/lineage.py +971 -0
  37. truthound_dashboard/core/maintenance.py +443 -5
  38. truthound_dashboard/core/model_monitoring.py +1043 -0
  39. truthound_dashboard/core/notifications/channels.py +1020 -1
  40. truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
  41. truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
  42. truthound_dashboard/core/notifications/deduplication/service.py +400 -0
  43. truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
  44. truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
  45. truthound_dashboard/core/notifications/dispatcher.py +43 -0
  46. truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
  47. truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
  48. truthound_dashboard/core/notifications/escalation/engine.py +429 -0
  49. truthound_dashboard/core/notifications/escalation/models.py +336 -0
  50. truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
  51. truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
  52. truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
  53. truthound_dashboard/core/notifications/events.py +49 -0
  54. truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
  55. truthound_dashboard/core/notifications/metrics/base.py +528 -0
  56. truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
  57. truthound_dashboard/core/notifications/routing/__init__.py +169 -0
  58. truthound_dashboard/core/notifications/routing/combinators.py +184 -0
  59. truthound_dashboard/core/notifications/routing/config.py +375 -0
  60. truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
  61. truthound_dashboard/core/notifications/routing/engine.py +382 -0
  62. truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
  63. truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
  64. truthound_dashboard/core/notifications/routing/rules.py +625 -0
  65. truthound_dashboard/core/notifications/routing/validator.py +678 -0
  66. truthound_dashboard/core/notifications/service.py +2 -0
  67. truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
  68. truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
  69. truthound_dashboard/core/notifications/throttling/builder.py +311 -0
  70. truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
  71. truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
  72. truthound_dashboard/core/openlineage.py +1028 -0
  73. truthound_dashboard/core/plugins/__init__.py +39 -0
  74. truthound_dashboard/core/plugins/docs/__init__.py +39 -0
  75. truthound_dashboard/core/plugins/docs/extractor.py +703 -0
  76. truthound_dashboard/core/plugins/docs/renderers.py +804 -0
  77. truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
  78. truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
  79. truthound_dashboard/core/plugins/hooks/manager.py +403 -0
  80. truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
  81. truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
  82. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
  83. truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
  84. truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
  85. truthound_dashboard/core/plugins/loader.py +504 -0
  86. truthound_dashboard/core/plugins/registry.py +810 -0
  87. truthound_dashboard/core/plugins/reporter_executor.py +588 -0
  88. truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
  89. truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
  90. truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
  91. truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
  92. truthound_dashboard/core/plugins/sandbox.py +617 -0
  93. truthound_dashboard/core/plugins/security/__init__.py +68 -0
  94. truthound_dashboard/core/plugins/security/analyzer.py +535 -0
  95. truthound_dashboard/core/plugins/security/policies.py +311 -0
  96. truthound_dashboard/core/plugins/security/protocols.py +296 -0
  97. truthound_dashboard/core/plugins/security/signing.py +842 -0
  98. truthound_dashboard/core/plugins/security.py +446 -0
  99. truthound_dashboard/core/plugins/validator_executor.py +401 -0
  100. truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
  101. truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
  102. truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
  103. truthound_dashboard/core/plugins/versioning/semver.py +266 -0
  104. truthound_dashboard/core/profile_comparison.py +601 -0
  105. truthound_dashboard/core/report_history.py +570 -0
  106. truthound_dashboard/core/reporters/__init__.py +57 -0
  107. truthound_dashboard/core/reporters/base.py +296 -0
  108. truthound_dashboard/core/reporters/csv_reporter.py +155 -0
  109. truthound_dashboard/core/reporters/html_reporter.py +598 -0
  110. truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
  111. truthound_dashboard/core/reporters/i18n/base.py +494 -0
  112. truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
  113. truthound_dashboard/core/reporters/json_reporter.py +160 -0
  114. truthound_dashboard/core/reporters/junit_reporter.py +233 -0
  115. truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
  116. truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
  117. truthound_dashboard/core/reporters/registry.py +272 -0
  118. truthound_dashboard/core/rule_generator.py +2088 -0
  119. truthound_dashboard/core/scheduler.py +822 -12
  120. truthound_dashboard/core/schema_evolution.py +858 -0
  121. truthound_dashboard/core/services.py +152 -9
  122. truthound_dashboard/core/statistics.py +718 -0
  123. truthound_dashboard/core/streaming_anomaly.py +883 -0
  124. truthound_dashboard/core/triggers/__init__.py +45 -0
  125. truthound_dashboard/core/triggers/base.py +226 -0
  126. truthound_dashboard/core/triggers/evaluators.py +609 -0
  127. truthound_dashboard/core/triggers/factory.py +363 -0
  128. truthound_dashboard/core/unified_alerts.py +870 -0
  129. truthound_dashboard/core/validation_limits.py +509 -0
  130. truthound_dashboard/core/versioning.py +709 -0
  131. truthound_dashboard/core/websocket/__init__.py +59 -0
  132. truthound_dashboard/core/websocket/manager.py +512 -0
  133. truthound_dashboard/core/websocket/messages.py +130 -0
  134. truthound_dashboard/db/__init__.py +30 -0
  135. truthound_dashboard/db/models.py +3375 -3
  136. truthound_dashboard/main.py +22 -0
  137. truthound_dashboard/schemas/__init__.py +396 -1
  138. truthound_dashboard/schemas/anomaly.py +1258 -0
  139. truthound_dashboard/schemas/base.py +4 -0
  140. truthound_dashboard/schemas/cross_alerts.py +334 -0
  141. truthound_dashboard/schemas/drift_monitor.py +890 -0
  142. truthound_dashboard/schemas/lineage.py +428 -0
  143. truthound_dashboard/schemas/maintenance.py +154 -0
  144. truthound_dashboard/schemas/model_monitoring.py +374 -0
  145. truthound_dashboard/schemas/notifications_advanced.py +1363 -0
  146. truthound_dashboard/schemas/openlineage.py +704 -0
  147. truthound_dashboard/schemas/plugins.py +1293 -0
  148. truthound_dashboard/schemas/profile.py +420 -34
  149. truthound_dashboard/schemas/profile_comparison.py +242 -0
  150. truthound_dashboard/schemas/reports.py +285 -0
  151. truthound_dashboard/schemas/rule_suggestion.py +434 -0
  152. truthound_dashboard/schemas/schema_evolution.py +164 -0
  153. truthound_dashboard/schemas/source.py +117 -2
  154. truthound_dashboard/schemas/triggers.py +511 -0
  155. truthound_dashboard/schemas/unified_alerts.py +223 -0
  156. truthound_dashboard/schemas/validation.py +25 -1
  157. truthound_dashboard/schemas/validators/__init__.py +11 -0
  158. truthound_dashboard/schemas/validators/base.py +151 -0
  159. truthound_dashboard/schemas/versioning.py +152 -0
  160. truthound_dashboard/static/index.html +2 -2
  161. {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/METADATA +142 -18
  162. truthound_dashboard-1.4.0.dist-info/RECORD +239 -0
  163. truthound_dashboard/static/assets/index-BCA8H1hO.js +0 -574
  164. truthound_dashboard/static/assets/index-BNsSQ2fN.css +0 -1
  165. truthound_dashboard/static/assets/unmerged_dictionaries-CsJWCRx9.js +0 -1
  166. truthound_dashboard-1.3.0.dist-info/RECORD +0 -110
  167. {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/WHEEL +0 -0
  168. {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/entry_points.txt +0 -0
  169. {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.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
+ ]