truthound-dashboard 1.3.1__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.1.dist-info → truthound_dashboard-1.4.0.dist-info}/METADATA +142 -22
  162. truthound_dashboard-1.4.0.dist-info/RECORD +239 -0
  163. truthound_dashboard/static/assets/index-BZG20KuF.js +0 -586
  164. truthound_dashboard/static/assets/index-D_HyZ3pb.css +0 -1
  165. truthound_dashboard/static/assets/unmerged_dictionaries-CtpqQBm0.js +0 -1
  166. truthound_dashboard-1.3.1.dist-info/RECORD +0 -110
  167. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/WHEEL +0 -0
  168. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/entry_points.txt +0 -0
  169. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,309 @@
1
+ """Versioning API endpoints.
2
+
3
+ Provides endpoints for validation result version management:
4
+ - List versions for a source
5
+ - Get specific version details
6
+ - Compare two versions
7
+ - Get version history chain
8
+ - Create new versions
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from typing import Annotated
15
+
16
+ from fastapi import APIRouter, HTTPException, Path, Query
17
+
18
+ from truthound_dashboard.core.versioning import (
19
+ VersioningStrategy,
20
+ get_version_manager,
21
+ )
22
+ from truthound_dashboard.schemas.versioning import (
23
+ CreateVersionRequest,
24
+ CreateVersionResponse,
25
+ RollbackAvailabilityResponse,
26
+ RollbackRequest,
27
+ RollbackResponse,
28
+ VersionCompareRequest,
29
+ VersionDiffResponse,
30
+ VersionHistoryResponse,
31
+ VersionInfoResponse,
32
+ VersionListResponse,
33
+ )
34
+ from truthound_dashboard.api.deps import ValidationServiceDep
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+ router = APIRouter(prefix="/versions", tags=["versioning"])
39
+
40
+
41
+ def _version_info_to_response(version_info) -> VersionInfoResponse:
42
+ """Convert VersionInfo dataclass to response model."""
43
+ return VersionInfoResponse(
44
+ version_id=version_info.version_id,
45
+ version_number=version_info.version_number,
46
+ validation_id=version_info.validation_id,
47
+ source_id=version_info.source_id,
48
+ strategy=version_info.strategy.value,
49
+ created_at=version_info.created_at,
50
+ parent_version_id=version_info.parent_version_id,
51
+ metadata=version_info.metadata,
52
+ content_hash=version_info.content_hash,
53
+ )
54
+
55
+
56
+ @router.get(
57
+ "/sources/{source_id}",
58
+ response_model=VersionListResponse,
59
+ summary="List versions for a source",
60
+ description="Get all validation result versions for a data source, ordered by creation time (newest first).",
61
+ )
62
+ async def list_source_versions(
63
+ source_id: Annotated[str, Path(description="Source ID")],
64
+ limit: Annotated[int, Query(ge=1, le=100, description="Maximum versions to return")] = 20,
65
+ ) -> VersionListResponse:
66
+ """List all versions for a source."""
67
+ manager = get_version_manager()
68
+ versions = await manager.list_versions(source_id=source_id, limit=limit)
69
+
70
+ return VersionListResponse(
71
+ success=True,
72
+ data=[_version_info_to_response(v) for v in versions],
73
+ total=len(versions),
74
+ source_id=source_id,
75
+ )
76
+
77
+
78
+ @router.get(
79
+ "/{version_id}",
80
+ response_model=VersionInfoResponse,
81
+ summary="Get version details",
82
+ description="Get detailed information about a specific version.",
83
+ )
84
+ async def get_version(
85
+ version_id: Annotated[str, Path(description="Version ID")],
86
+ ) -> VersionInfoResponse:
87
+ """Get a specific version by ID."""
88
+ manager = get_version_manager()
89
+ version = await manager.get_version(version_id)
90
+
91
+ if not version:
92
+ raise HTTPException(status_code=404, detail=f"Version not found: {version_id}")
93
+
94
+ return _version_info_to_response(version)
95
+
96
+
97
+ @router.get(
98
+ "/sources/{source_id}/latest",
99
+ response_model=VersionInfoResponse,
100
+ summary="Get latest version",
101
+ description="Get the most recent version for a data source.",
102
+ )
103
+ async def get_latest_version(
104
+ source_id: Annotated[str, Path(description="Source ID")],
105
+ ) -> VersionInfoResponse:
106
+ """Get the latest version for a source."""
107
+ manager = get_version_manager()
108
+ version = await manager.get_latest_version(source_id)
109
+
110
+ if not version:
111
+ raise HTTPException(
112
+ status_code=404,
113
+ detail=f"No versions found for source: {source_id}"
114
+ )
115
+
116
+ return _version_info_to_response(version)
117
+
118
+
119
+ @router.post(
120
+ "/compare",
121
+ response_model=VersionDiffResponse,
122
+ summary="Compare two versions",
123
+ description="Compare two validation result versions and get detailed differences.",
124
+ )
125
+ async def compare_versions(
126
+ request: VersionCompareRequest,
127
+ validation_service: ValidationServiceDep,
128
+ ) -> VersionDiffResponse:
129
+ """Compare two versions and return differences."""
130
+ manager = get_version_manager()
131
+
132
+ # Get versions first to check they exist
133
+ from_version = await manager.get_version(request.from_version_id)
134
+ to_version = await manager.get_version(request.to_version_id)
135
+
136
+ if not from_version:
137
+ raise HTTPException(
138
+ status_code=404,
139
+ detail=f"From version not found: {request.from_version_id}"
140
+ )
141
+ if not to_version:
142
+ raise HTTPException(
143
+ status_code=404,
144
+ detail=f"To version not found: {request.to_version_id}"
145
+ )
146
+
147
+ # Try to get validation results for comparison
148
+ from_result = None
149
+ to_result = None
150
+
151
+ try:
152
+ from_validation = await validation_service.get_validation(from_version.validation_id)
153
+ if from_validation and from_validation.result_json:
154
+ from_result = from_validation.result_json
155
+ except Exception:
156
+ pass
157
+
158
+ try:
159
+ to_validation = await validation_service.get_validation(to_version.validation_id)
160
+ if to_validation and to_validation.result_json:
161
+ to_result = to_validation.result_json
162
+ except Exception:
163
+ pass
164
+
165
+ # Compare versions
166
+ diff = await manager.compare_versions(
167
+ from_version_id=request.from_version_id,
168
+ to_version_id=request.to_version_id,
169
+ from_result=from_result,
170
+ to_result=to_result,
171
+ )
172
+
173
+ return VersionDiffResponse(
174
+ from_version=_version_info_to_response(diff.from_version),
175
+ to_version=_version_info_to_response(diff.to_version),
176
+ issues_added=diff.issues_added,
177
+ issues_removed=diff.issues_removed,
178
+ issues_changed=diff.issues_changed,
179
+ summary_changes=diff.summary_changes,
180
+ has_changes=diff.has_changes,
181
+ )
182
+
183
+
184
+ @router.get(
185
+ "/{version_id}/history",
186
+ response_model=VersionHistoryResponse,
187
+ summary="Get version history",
188
+ description="Get the version history chain starting from a specific version.",
189
+ )
190
+ async def get_version_history(
191
+ version_id: Annotated[str, Path(description="Starting version ID")],
192
+ depth: Annotated[int, Query(ge=1, le=50, description="Maximum history depth")] = 10,
193
+ ) -> VersionHistoryResponse:
194
+ """Get version history chain."""
195
+ manager = get_version_manager()
196
+
197
+ # Check version exists
198
+ version = await manager.get_version(version_id)
199
+ if not version:
200
+ raise HTTPException(status_code=404, detail=f"Version not found: {version_id}")
201
+
202
+ history = await manager.get_version_history(version_id=version_id, depth=depth)
203
+
204
+ return VersionHistoryResponse(
205
+ success=True,
206
+ data=[_version_info_to_response(v) for v in history],
207
+ depth=len(history),
208
+ )
209
+
210
+
211
+ @router.post(
212
+ "/",
213
+ response_model=CreateVersionResponse,
214
+ summary="Create a version",
215
+ description="Create a new version for a validation result.",
216
+ )
217
+ async def create_version(
218
+ request: CreateVersionRequest,
219
+ validation_service: ValidationServiceDep,
220
+ ) -> CreateVersionResponse:
221
+ """Create a new version for a validation."""
222
+ # Get validation to verify it exists
223
+ validation = await validation_service.get_validation(request.validation_id)
224
+ if not validation:
225
+ raise HTTPException(
226
+ status_code=404,
227
+ detail=f"Validation not found: {request.validation_id}"
228
+ )
229
+
230
+ manager = get_version_manager()
231
+
232
+ # Parse strategy
233
+ strategy = None
234
+ if request.strategy:
235
+ strategy = VersioningStrategy(request.strategy)
236
+
237
+ # Create version
238
+ version = await manager.create_version(
239
+ validation_id=request.validation_id,
240
+ source_id=validation.source_id,
241
+ result_json=validation.result_json,
242
+ strategy=strategy,
243
+ metadata=request.metadata,
244
+ )
245
+
246
+ return CreateVersionResponse(
247
+ success=True,
248
+ data=_version_info_to_response(version),
249
+ message=f"Created version {version.version_number} for validation {request.validation_id}",
250
+ )
251
+
252
+
253
+ @router.get(
254
+ "/sources/{source_id}/rollback-availability",
255
+ response_model=RollbackAvailabilityResponse,
256
+ summary="Check rollback availability",
257
+ description="Check if rollback is available for a source and list available targets.",
258
+ )
259
+ async def check_rollback_availability(
260
+ source_id: Annotated[str, Path(description="Source ID")],
261
+ ) -> RollbackAvailabilityResponse:
262
+ """Check if rollback is available for a source."""
263
+ manager = get_version_manager()
264
+ availability = await manager.can_rollback(source_id)
265
+
266
+ return RollbackAvailabilityResponse(
267
+ success=True,
268
+ can_rollback=availability["can_rollback"],
269
+ current_version_id=availability["current_version_id"],
270
+ available_versions=availability["available_versions"],
271
+ rollback_targets=availability["rollback_targets"],
272
+ )
273
+
274
+
275
+ @router.post(
276
+ "/sources/{source_id}/rollback",
277
+ response_model=RollbackResponse,
278
+ summary="Rollback to a previous version",
279
+ description="Rollback a source to a previous validation result version.",
280
+ )
281
+ async def rollback_to_version(
282
+ source_id: Annotated[str, Path(description="Source ID")],
283
+ request: RollbackRequest,
284
+ ) -> RollbackResponse:
285
+ """Rollback to a previous version."""
286
+ manager = get_version_manager()
287
+
288
+ # Perform rollback
289
+ result = await manager.rollback_to_version(
290
+ source_id=source_id,
291
+ target_version_id=request.target_version_id,
292
+ create_new_validation=request.create_new_validation,
293
+ )
294
+
295
+ if not result.success:
296
+ raise HTTPException(
297
+ status_code=400,
298
+ detail=result.message,
299
+ )
300
+
301
+ return RollbackResponse(
302
+ success=result.success,
303
+ source_id=result.source_id,
304
+ from_version=_version_info_to_response(result.from_version) if result.from_version else None,
305
+ to_version=_version_info_to_response(result.to_version) if result.to_version else None,
306
+ new_validation_id=result.new_validation_id,
307
+ message=result.message,
308
+ rolled_back_at=result.rolled_back_at,
309
+ )
@@ -0,0 +1,301 @@
1
+ """WebSocket API endpoints.
2
+
3
+ This module provides WebSocket endpoints for real-time updates.
4
+
5
+ Endpoints:
6
+ WebSocket /ws/notifications/incidents - Real-time escalation incident updates
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import logging
13
+ import uuid
14
+ from typing import Any
15
+
16
+ from fastapi import APIRouter, Query, WebSocket, WebSocketDisconnect
17
+
18
+ from ..core.websocket import (
19
+ WebSocketManager,
20
+ WebSocketMessage,
21
+ WebSocketMessageType,
22
+ get_websocket_manager,
23
+ )
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ router = APIRouter()
28
+
29
+ # Room name for incident updates
30
+ INCIDENTS_ROOM = "incidents"
31
+
32
+
33
+ @router.websocket("/ws/notifications/incidents")
34
+ async def websocket_incidents(
35
+ websocket: WebSocket,
36
+ token: str | None = Query(default=None, description="Optional authentication token"),
37
+ ) -> None:
38
+ """WebSocket endpoint for real-time escalation incident updates.
39
+
40
+ This endpoint allows clients to receive real-time updates about escalation
41
+ incidents, including creation, state changes, and resolution.
42
+
43
+ Query Parameters:
44
+ token: Optional authentication token for secure connections.
45
+
46
+ Message Types Received:
47
+ - ping: Client heartbeat, server responds with pong
48
+ - pong: Response to server ping
49
+
50
+ Message Types Sent:
51
+ - connected: Sent when connection is established
52
+ - ping: Server heartbeat
53
+ - pong: Response to client ping
54
+ - incident_created: New incident created
55
+ - incident_updated: Incident updated
56
+ - incident_state_changed: Incident state changed
57
+ - incident_acknowledged: Incident acknowledged
58
+ - incident_escalated: Incident escalated to next level
59
+ - incident_resolved: Incident resolved
60
+
61
+ Example Client Connection (JavaScript):
62
+ const ws = new WebSocket('ws://localhost:8765/api/v1/ws/notifications/incidents');
63
+
64
+ ws.onmessage = (event) => {
65
+ const message = JSON.parse(event.data);
66
+ switch (message.type) {
67
+ case 'incident_created':
68
+ handleNewIncident(message.data);
69
+ break;
70
+ case 'incident_state_changed':
71
+ handleStateChange(message.data);
72
+ break;
73
+ // ... handle other message types
74
+ }
75
+ };
76
+
77
+ // Send ping to keep connection alive
78
+ setInterval(() => {
79
+ ws.send(JSON.stringify({ type: 'ping' }));
80
+ }, 30000);
81
+ """
82
+ manager = get_websocket_manager()
83
+ connection_id = str(uuid.uuid4())
84
+
85
+ # Optional: Validate token if provided
86
+ # In production, implement proper authentication
87
+ if token:
88
+ logger.debug(f"WebSocket connection with token: {token[:8]}...")
89
+
90
+ try:
91
+ # Accept connection and register
92
+ connection = await manager.connect(
93
+ websocket=websocket,
94
+ connection_id=connection_id,
95
+ token=token,
96
+ )
97
+
98
+ # Join the incidents room for broadcast updates
99
+ await manager.join_room(connection, INCIDENTS_ROOM)
100
+
101
+ logger.info(
102
+ f"Client connected to incidents WebSocket: {connection_id}"
103
+ )
104
+
105
+ # Handle incoming messages
106
+ while True:
107
+ try:
108
+ data = await websocket.receive_json()
109
+ await manager.handle_client_message(connection, data)
110
+ except WebSocketDisconnect:
111
+ break
112
+ except Exception as e:
113
+ logger.warning(
114
+ f"Error receiving message from {connection_id}: {e}"
115
+ )
116
+ # Send error message
117
+ await connection.send_message(
118
+ WebSocketMessage(
119
+ type=WebSocketMessageType.ERROR,
120
+ data={"error": str(e)},
121
+ )
122
+ )
123
+
124
+ except WebSocketDisconnect:
125
+ logger.info(f"Client disconnected from incidents WebSocket: {connection_id}")
126
+ except Exception as e:
127
+ logger.error(f"WebSocket error for {connection_id}: {e}")
128
+ finally:
129
+ await manager.disconnect(connection_id, reason="Connection closed")
130
+
131
+
132
+ async def broadcast_incident_event(
133
+ event_type: WebSocketMessageType,
134
+ incident_data: dict[str, Any],
135
+ ) -> int:
136
+ """Broadcast an incident event to all connected clients.
137
+
138
+ Args:
139
+ event_type: Type of the event.
140
+ incident_data: Incident data to broadcast.
141
+
142
+ Returns:
143
+ Number of clients that received the message.
144
+ """
145
+ manager = get_websocket_manager()
146
+
147
+ message = WebSocketMessage(
148
+ type=event_type,
149
+ data=incident_data,
150
+ )
151
+
152
+ return await manager.broadcast_to_room(INCIDENTS_ROOM, message)
153
+
154
+
155
+ async def notify_incident_created(
156
+ incident_id: str,
157
+ incident_ref: str,
158
+ policy_id: str,
159
+ state: str,
160
+ current_level: int,
161
+ context: dict[str, Any] | None = None,
162
+ ) -> int:
163
+ """Notify clients about a new incident.
164
+
165
+ Args:
166
+ incident_id: ID of the new incident.
167
+ incident_ref: External reference.
168
+ policy_id: Associated policy ID.
169
+ state: Initial state.
170
+ current_level: Initial escalation level.
171
+ context: Optional incident context.
172
+
173
+ Returns:
174
+ Number of clients notified.
175
+ """
176
+ return await broadcast_incident_event(
177
+ WebSocketMessageType.INCIDENT_CREATED,
178
+ {
179
+ "incident_id": incident_id,
180
+ "incident_ref": incident_ref,
181
+ "policy_id": policy_id,
182
+ "state": state,
183
+ "current_level": current_level,
184
+ "context": context or {},
185
+ },
186
+ )
187
+
188
+
189
+ async def notify_incident_state_changed(
190
+ incident_id: str,
191
+ incident_ref: str,
192
+ policy_id: str,
193
+ from_state: str,
194
+ to_state: str,
195
+ current_level: int,
196
+ actor: str | None = None,
197
+ message: str | None = None,
198
+ ) -> int:
199
+ """Notify clients about an incident state change.
200
+
201
+ Args:
202
+ incident_id: ID of the incident.
203
+ incident_ref: External reference.
204
+ policy_id: Associated policy ID.
205
+ from_state: Previous state.
206
+ to_state: New state.
207
+ current_level: Current escalation level.
208
+ actor: Who triggered the change.
209
+ message: Optional message.
210
+
211
+ Returns:
212
+ Number of clients notified.
213
+ """
214
+ # Determine specific event type based on new state
215
+ if to_state == "acknowledged":
216
+ event_type = WebSocketMessageType.INCIDENT_ACKNOWLEDGED
217
+ elif to_state == "resolved":
218
+ event_type = WebSocketMessageType.INCIDENT_RESOLVED
219
+ elif to_state == "escalated":
220
+ event_type = WebSocketMessageType.INCIDENT_ESCALATED
221
+ else:
222
+ event_type = WebSocketMessageType.INCIDENT_STATE_CHANGED
223
+
224
+ return await broadcast_incident_event(
225
+ event_type,
226
+ {
227
+ "incident_id": incident_id,
228
+ "incident_ref": incident_ref,
229
+ "policy_id": policy_id,
230
+ "from_state": from_state,
231
+ "to_state": to_state,
232
+ "current_level": current_level,
233
+ "actor": actor,
234
+ "message": message,
235
+ },
236
+ )
237
+
238
+
239
+ async def notify_incident_updated(
240
+ incident_id: str,
241
+ incident_ref: str,
242
+ policy_id: str,
243
+ state: str,
244
+ current_level: int,
245
+ changes: dict[str, Any] | None = None,
246
+ ) -> int:
247
+ """Notify clients about an incident update.
248
+
249
+ Args:
250
+ incident_id: ID of the incident.
251
+ incident_ref: External reference.
252
+ policy_id: Associated policy ID.
253
+ state: Current state.
254
+ current_level: Current escalation level.
255
+ changes: Optional dictionary of changed fields.
256
+
257
+ Returns:
258
+ Number of clients notified.
259
+ """
260
+ return await broadcast_incident_event(
261
+ WebSocketMessageType.INCIDENT_UPDATED,
262
+ {
263
+ "incident_id": incident_id,
264
+ "incident_ref": incident_ref,
265
+ "policy_id": policy_id,
266
+ "state": state,
267
+ "current_level": current_level,
268
+ "changes": changes or {},
269
+ },
270
+ )
271
+
272
+
273
+ async def notify_incident_resolved(
274
+ incident_id: str,
275
+ incident_ref: str,
276
+ policy_id: str,
277
+ resolved_by: str | None = None,
278
+ message: str | None = None,
279
+ ) -> int:
280
+ """Notify clients about an incident resolution.
281
+
282
+ Args:
283
+ incident_id: ID of the incident.
284
+ incident_ref: External reference.
285
+ policy_id: Associated policy ID.
286
+ resolved_by: Who resolved it.
287
+ message: Resolution message.
288
+
289
+ Returns:
290
+ Number of clients notified.
291
+ """
292
+ return await broadcast_incident_event(
293
+ WebSocketMessageType.INCIDENT_RESOLVED,
294
+ {
295
+ "incident_id": incident_id,
296
+ "incident_ref": incident_ref,
297
+ "policy_id": policy_id,
298
+ "resolved_by": resolved_by,
299
+ "message": message,
300
+ },
301
+ )
@@ -117,6 +117,20 @@ from .sampling import (
117
117
  get_sampler,
118
118
  reset_sampler,
119
119
  )
120
+ from .validation_limits import (
121
+ DeduplicationLimits,
122
+ EscalationLimits,
123
+ ThrottlingLimits,
124
+ TimeWindowLimits,
125
+ ValidationLimitError,
126
+ clear_limits_cache,
127
+ get_deduplication_limits,
128
+ get_escalation_limits,
129
+ get_throttling_limits,
130
+ get_time_window_limits,
131
+ validate_positive_float,
132
+ validate_positive_int,
133
+ )
120
134
  from .scheduler import (
121
135
  ValidationScheduler,
122
136
  get_scheduler,
@@ -281,4 +295,17 @@ __all__ = [
281
295
  "CatalogService",
282
296
  "CollaborationService",
283
297
  "ActivityLogger",
298
+ # Validation Limits (DoS Prevention)
299
+ "ValidationLimitError",
300
+ "DeduplicationLimits",
301
+ "ThrottlingLimits",
302
+ "EscalationLimits",
303
+ "TimeWindowLimits",
304
+ "get_deduplication_limits",
305
+ "get_throttling_limits",
306
+ "get_escalation_limits",
307
+ "get_time_window_limits",
308
+ "clear_limits_cache",
309
+ "validate_positive_int",
310
+ "validate_positive_float",
284
311
  ]