truthound-dashboard 1.4.3__py3-none-any.whl → 1.5.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 (205) hide show
  1. truthound_dashboard/api/alerts.py +75 -86
  2. truthound_dashboard/api/anomaly.py +7 -13
  3. truthound_dashboard/api/cross_alerts.py +38 -52
  4. truthound_dashboard/api/drift.py +49 -59
  5. truthound_dashboard/api/drift_monitor.py +234 -79
  6. truthound_dashboard/api/enterprise_sampling.py +498 -0
  7. truthound_dashboard/api/history.py +57 -5
  8. truthound_dashboard/api/lineage.py +3 -48
  9. truthound_dashboard/api/maintenance.py +104 -49
  10. truthound_dashboard/api/mask.py +1 -2
  11. truthound_dashboard/api/middleware.py +2 -1
  12. truthound_dashboard/api/model_monitoring.py +435 -311
  13. truthound_dashboard/api/notifications.py +227 -191
  14. truthound_dashboard/api/notifications_advanced.py +21 -20
  15. truthound_dashboard/api/observability.py +586 -0
  16. truthound_dashboard/api/plugins.py +2 -433
  17. truthound_dashboard/api/profile.py +199 -37
  18. truthound_dashboard/api/quality_reporter.py +701 -0
  19. truthound_dashboard/api/reports.py +7 -16
  20. truthound_dashboard/api/router.py +66 -0
  21. truthound_dashboard/api/rule_suggestions.py +5 -5
  22. truthound_dashboard/api/scan.py +17 -19
  23. truthound_dashboard/api/schedules.py +85 -50
  24. truthound_dashboard/api/schema_evolution.py +6 -6
  25. truthound_dashboard/api/schema_watcher.py +667 -0
  26. truthound_dashboard/api/sources.py +98 -27
  27. truthound_dashboard/api/tiering.py +1323 -0
  28. truthound_dashboard/api/triggers.py +14 -11
  29. truthound_dashboard/api/validations.py +12 -11
  30. truthound_dashboard/api/versioning.py +1 -6
  31. truthound_dashboard/core/__init__.py +129 -3
  32. truthound_dashboard/core/actions/__init__.py +62 -0
  33. truthound_dashboard/core/actions/custom.py +426 -0
  34. truthound_dashboard/core/actions/notifications.py +910 -0
  35. truthound_dashboard/core/actions/storage.py +472 -0
  36. truthound_dashboard/core/actions/webhook.py +281 -0
  37. truthound_dashboard/core/anomaly.py +262 -67
  38. truthound_dashboard/core/anomaly_explainer.py +4 -3
  39. truthound_dashboard/core/backends/__init__.py +67 -0
  40. truthound_dashboard/core/backends/base.py +299 -0
  41. truthound_dashboard/core/backends/errors.py +191 -0
  42. truthound_dashboard/core/backends/factory.py +423 -0
  43. truthound_dashboard/core/backends/mock_backend.py +451 -0
  44. truthound_dashboard/core/backends/truthound_backend.py +718 -0
  45. truthound_dashboard/core/checkpoint/__init__.py +87 -0
  46. truthound_dashboard/core/checkpoint/adapters.py +814 -0
  47. truthound_dashboard/core/checkpoint/checkpoint.py +491 -0
  48. truthound_dashboard/core/checkpoint/runner.py +270 -0
  49. truthound_dashboard/core/connections.py +437 -10
  50. truthound_dashboard/core/converters/__init__.py +14 -0
  51. truthound_dashboard/core/converters/truthound.py +620 -0
  52. truthound_dashboard/core/cross_alerts.py +540 -320
  53. truthound_dashboard/core/datasource_factory.py +1672 -0
  54. truthound_dashboard/core/drift_monitor.py +216 -20
  55. truthound_dashboard/core/enterprise_sampling.py +1291 -0
  56. truthound_dashboard/core/interfaces/__init__.py +225 -0
  57. truthound_dashboard/core/interfaces/actions.py +652 -0
  58. truthound_dashboard/core/interfaces/base.py +247 -0
  59. truthound_dashboard/core/interfaces/checkpoint.py +676 -0
  60. truthound_dashboard/core/interfaces/protocols.py +664 -0
  61. truthound_dashboard/core/interfaces/reporters.py +650 -0
  62. truthound_dashboard/core/interfaces/routing.py +646 -0
  63. truthound_dashboard/core/interfaces/triggers.py +619 -0
  64. truthound_dashboard/core/lineage.py +407 -71
  65. truthound_dashboard/core/model_monitoring.py +431 -3
  66. truthound_dashboard/core/notifications/base.py +4 -0
  67. truthound_dashboard/core/notifications/channels.py +501 -1203
  68. truthound_dashboard/core/notifications/deduplication/__init__.py +81 -115
  69. truthound_dashboard/core/notifications/deduplication/service.py +131 -348
  70. truthound_dashboard/core/notifications/dispatcher.py +202 -11
  71. truthound_dashboard/core/notifications/escalation/__init__.py +119 -106
  72. truthound_dashboard/core/notifications/escalation/engine.py +168 -358
  73. truthound_dashboard/core/notifications/routing/__init__.py +88 -128
  74. truthound_dashboard/core/notifications/routing/engine.py +90 -317
  75. truthound_dashboard/core/notifications/stats_aggregator.py +246 -1
  76. truthound_dashboard/core/notifications/throttling/__init__.py +67 -50
  77. truthound_dashboard/core/notifications/throttling/builder.py +117 -255
  78. truthound_dashboard/core/notifications/truthound_adapter.py +842 -0
  79. truthound_dashboard/core/phase5/collaboration.py +1 -1
  80. truthound_dashboard/core/plugins/lifecycle/__init__.py +0 -13
  81. truthound_dashboard/core/quality_reporter.py +1359 -0
  82. truthound_dashboard/core/report_history.py +0 -6
  83. truthound_dashboard/core/reporters/__init__.py +175 -14
  84. truthound_dashboard/core/reporters/adapters.py +943 -0
  85. truthound_dashboard/core/reporters/base.py +0 -3
  86. truthound_dashboard/core/reporters/builtin/__init__.py +18 -0
  87. truthound_dashboard/core/reporters/builtin/csv_reporter.py +111 -0
  88. truthound_dashboard/core/reporters/builtin/html_reporter.py +270 -0
  89. truthound_dashboard/core/reporters/builtin/json_reporter.py +127 -0
  90. truthound_dashboard/core/reporters/compat.py +266 -0
  91. truthound_dashboard/core/reporters/csv_reporter.py +2 -35
  92. truthound_dashboard/core/reporters/factory.py +526 -0
  93. truthound_dashboard/core/reporters/interfaces.py +745 -0
  94. truthound_dashboard/core/reporters/registry.py +1 -10
  95. truthound_dashboard/core/scheduler.py +165 -0
  96. truthound_dashboard/core/schema_evolution.py +3 -3
  97. truthound_dashboard/core/schema_watcher.py +1528 -0
  98. truthound_dashboard/core/services.py +595 -76
  99. truthound_dashboard/core/store_manager.py +810 -0
  100. truthound_dashboard/core/streaming_anomaly.py +169 -4
  101. truthound_dashboard/core/tiering.py +1309 -0
  102. truthound_dashboard/core/triggers/evaluators.py +178 -8
  103. truthound_dashboard/core/truthound_adapter.py +2620 -197
  104. truthound_dashboard/core/unified_alerts.py +23 -20
  105. truthound_dashboard/db/__init__.py +8 -0
  106. truthound_dashboard/db/database.py +8 -2
  107. truthound_dashboard/db/models.py +944 -25
  108. truthound_dashboard/db/repository.py +2 -0
  109. truthound_dashboard/main.py +11 -0
  110. truthound_dashboard/schemas/__init__.py +177 -16
  111. truthound_dashboard/schemas/base.py +44 -23
  112. truthound_dashboard/schemas/collaboration.py +19 -6
  113. truthound_dashboard/schemas/cross_alerts.py +19 -3
  114. truthound_dashboard/schemas/drift.py +61 -55
  115. truthound_dashboard/schemas/drift_monitor.py +67 -23
  116. truthound_dashboard/schemas/enterprise_sampling.py +653 -0
  117. truthound_dashboard/schemas/lineage.py +0 -33
  118. truthound_dashboard/schemas/mask.py +10 -8
  119. truthound_dashboard/schemas/model_monitoring.py +89 -10
  120. truthound_dashboard/schemas/notifications_advanced.py +13 -0
  121. truthound_dashboard/schemas/observability.py +453 -0
  122. truthound_dashboard/schemas/plugins.py +0 -280
  123. truthound_dashboard/schemas/profile.py +154 -247
  124. truthound_dashboard/schemas/quality_reporter.py +403 -0
  125. truthound_dashboard/schemas/reports.py +2 -2
  126. truthound_dashboard/schemas/rule_suggestion.py +8 -1
  127. truthound_dashboard/schemas/scan.py +4 -24
  128. truthound_dashboard/schemas/schedule.py +11 -3
  129. truthound_dashboard/schemas/schema_watcher.py +727 -0
  130. truthound_dashboard/schemas/source.py +17 -2
  131. truthound_dashboard/schemas/tiering.py +822 -0
  132. truthound_dashboard/schemas/triggers.py +16 -0
  133. truthound_dashboard/schemas/unified_alerts.py +7 -0
  134. truthound_dashboard/schemas/validation.py +0 -13
  135. truthound_dashboard/schemas/validators/base.py +41 -21
  136. truthound_dashboard/schemas/validators/business_rule_validators.py +244 -0
  137. truthound_dashboard/schemas/validators/localization_validators.py +273 -0
  138. truthound_dashboard/schemas/validators/ml_feature_validators.py +308 -0
  139. truthound_dashboard/schemas/validators/profiling_validators.py +275 -0
  140. truthound_dashboard/schemas/validators/referential_validators.py +312 -0
  141. truthound_dashboard/schemas/validators/registry.py +93 -8
  142. truthound_dashboard/schemas/validators/timeseries_validators.py +389 -0
  143. truthound_dashboard/schemas/versioning.py +1 -6
  144. truthound_dashboard/static/index.html +2 -2
  145. truthound_dashboard-1.5.0.dist-info/METADATA +309 -0
  146. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/RECORD +149 -148
  147. truthound_dashboard/core/plugins/hooks/__init__.py +0 -63
  148. truthound_dashboard/core/plugins/hooks/decorators.py +0 -367
  149. truthound_dashboard/core/plugins/hooks/manager.py +0 -403
  150. truthound_dashboard/core/plugins/hooks/protocols.py +0 -265
  151. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +0 -584
  152. truthound_dashboard/core/reporters/junit_reporter.py +0 -233
  153. truthound_dashboard/core/reporters/markdown_reporter.py +0 -207
  154. truthound_dashboard/core/reporters/pdf_reporter.py +0 -209
  155. truthound_dashboard/static/assets/_baseUniq-BcrSP13d.js +0 -1
  156. truthound_dashboard/static/assets/arc-DlYjKwIL.js +0 -1
  157. truthound_dashboard/static/assets/architectureDiagram-VXUJARFQ-Bb2drbQM.js +0 -36
  158. truthound_dashboard/static/assets/blockDiagram-VD42YOAC-BlsPG1CH.js +0 -122
  159. truthound_dashboard/static/assets/c4Diagram-YG6GDRKO-B9JdUoaC.js +0 -10
  160. truthound_dashboard/static/assets/channel-Q6mHF1Hd.js +0 -1
  161. truthound_dashboard/static/assets/chunk-4BX2VUAB-DmyoPVuJ.js +0 -1
  162. truthound_dashboard/static/assets/chunk-55IACEB6-Bcz6Siv8.js +0 -1
  163. truthound_dashboard/static/assets/chunk-B4BG7PRW-Br3G5Rum.js +0 -165
  164. truthound_dashboard/static/assets/chunk-DI55MBZ5-DuM9c23u.js +0 -220
  165. truthound_dashboard/static/assets/chunk-FMBD7UC4-DNU-5mvT.js +0 -15
  166. truthound_dashboard/static/assets/chunk-QN33PNHL-Im2yNcmS.js +0 -1
  167. truthound_dashboard/static/assets/chunk-QZHKN3VN-kZr8XFm1.js +0 -1
  168. truthound_dashboard/static/assets/chunk-TZMSLE5B-Q__360q_.js +0 -1
  169. truthound_dashboard/static/assets/classDiagram-2ON5EDUG-vtixxUyK.js +0 -1
  170. truthound_dashboard/static/assets/classDiagram-v2-WZHVMYZB-vtixxUyK.js +0 -1
  171. truthound_dashboard/static/assets/clone-BOt2LwD0.js +0 -1
  172. truthound_dashboard/static/assets/cose-bilkent-S5V4N54A-CBDw6iac.js +0 -1
  173. truthound_dashboard/static/assets/dagre-6UL2VRFP-XdKqmmY9.js +0 -4
  174. truthound_dashboard/static/assets/diagram-PSM6KHXK-DAZ8nx9V.js +0 -24
  175. truthound_dashboard/static/assets/diagram-QEK2KX5R-BRvDTbGD.js +0 -43
  176. truthound_dashboard/static/assets/diagram-S2PKOQOG-bQcczUkl.js +0 -24
  177. truthound_dashboard/static/assets/erDiagram-Q2GNP2WA-DPje7VMN.js +0 -60
  178. truthound_dashboard/static/assets/flowDiagram-NV44I4VS-B7BVtFVS.js +0 -162
  179. truthound_dashboard/static/assets/ganttDiagram-JELNMOA3-D6WKSS7U.js +0 -267
  180. truthound_dashboard/static/assets/gitGraphDiagram-NY62KEGX-D3vtVd3y.js +0 -65
  181. truthound_dashboard/static/assets/graph-BKgNKZVp.js +0 -1
  182. truthound_dashboard/static/assets/index-C6JSrkHo.css +0 -1
  183. truthound_dashboard/static/assets/index-DkU82VsU.js +0 -1800
  184. truthound_dashboard/static/assets/infoDiagram-WHAUD3N6-DnNCT429.js +0 -2
  185. truthound_dashboard/static/assets/journeyDiagram-XKPGCS4Q-DGiMozqS.js +0 -139
  186. truthound_dashboard/static/assets/kanban-definition-3W4ZIXB7-BV2gUgli.js +0 -89
  187. truthound_dashboard/static/assets/katex-Cu_Erd72.js +0 -261
  188. truthound_dashboard/static/assets/layout-DI2MfQ5G.js +0 -1
  189. truthound_dashboard/static/assets/min-DYdgXVcT.js +0 -1
  190. truthound_dashboard/static/assets/mindmap-definition-VGOIOE7T-C7x4ruxz.js +0 -68
  191. truthound_dashboard/static/assets/pieDiagram-ADFJNKIX-CAJaAB9f.js +0 -30
  192. truthound_dashboard/static/assets/quadrantDiagram-AYHSOK5B-DeqwDI46.js +0 -7
  193. truthound_dashboard/static/assets/requirementDiagram-UZGBJVZJ-e3XDpZIM.js +0 -64
  194. truthound_dashboard/static/assets/sankeyDiagram-TZEHDZUN-CNnAv5Ux.js +0 -10
  195. truthound_dashboard/static/assets/sequenceDiagram-WL72ISMW-Dsne-Of3.js +0 -145
  196. truthound_dashboard/static/assets/stateDiagram-FKZM4ZOC-Ee0sQXyb.js +0 -1
  197. truthound_dashboard/static/assets/stateDiagram-v2-4FDKWEC3-B26KqW_W.js +0 -1
  198. truthound_dashboard/static/assets/timeline-definition-IT6M3QCI-DZYi2yl3.js +0 -61
  199. truthound_dashboard/static/assets/treemap-KMMF4GRG-CY3f8In2.js +0 -128
  200. truthound_dashboard/static/assets/unmerged_dictionaries-Dd7xcPWG.js +0 -1
  201. truthound_dashboard/static/assets/xychartDiagram-PRI3JC2R-CS7fydZZ.js +0 -7
  202. truthound_dashboard-1.4.3.dist-info/METADATA +0 -505
  203. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/WHEEL +0 -0
  204. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/entry_points.txt +0 -0
  205. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -7,15 +7,19 @@ supporting sliding window detection and online learning.
7
7
  from __future__ import annotations
8
8
 
9
9
  import asyncio
10
+ import logging
11
+ from abc import ABC, abstractmethod
10
12
  from collections import deque
11
13
  from dataclasses import dataclass, field
12
- from datetime import datetime
14
+ from datetime import datetime, timedelta
13
15
  from enum import Enum
14
16
  from typing import Any
15
17
  from uuid import uuid4
16
18
 
17
19
  import numpy as np
18
20
 
21
+ logger = logging.getLogger(__name__)
22
+
19
23
 
20
24
  class StreamingSessionStatus(str, Enum):
21
25
  """Status of a streaming session."""
@@ -130,6 +134,7 @@ class StreamingSession:
130
134
  created_at: datetime
131
135
  started_at: datetime | None = None
132
136
  stopped_at: datetime | None = None
137
+ last_active_at: datetime | None = None
133
138
  config: dict[str, Any] = field(default_factory=dict)
134
139
 
135
140
  # Runtime state (not persisted)
@@ -140,10 +145,16 @@ class StreamingSession:
140
145
  _ema_values: dict[str, float] = field(default_factory=dict)
141
146
 
142
147
  def __post_init__(self) -> None:
143
- """Initialize column statistics."""
148
+ """Initialize column statistics and activity tracking."""
144
149
  for col in self.columns:
145
150
  self._column_stats[col] = StreamingStatistics()
146
151
  self._ema_values[col] = 0.0
152
+ if self.last_active_at is None:
153
+ self.last_active_at = self.created_at
154
+
155
+ def touch(self) -> None:
156
+ """Update last_active_at to current time."""
157
+ self.last_active_at = datetime.utcnow()
147
158
 
148
159
  def to_dict(self) -> dict[str, Any]:
149
160
  """Convert to dictionary."""
@@ -158,6 +169,7 @@ class StreamingSession:
158
169
  "created_at": self.created_at.isoformat(),
159
170
  "started_at": self.started_at.isoformat() if self.started_at else None,
160
171
  "stopped_at": self.stopped_at.isoformat() if self.stopped_at else None,
172
+ "last_active_at": self.last_active_at.isoformat() if self.last_active_at else None,
161
173
  "config": self.config,
162
174
  "statistics": {col: stats.to_dict() for col, stats in self._column_stats.items()},
163
175
  "total_points": len(self._buffer),
@@ -165,6 +177,89 @@ class StreamingSession:
165
177
  }
166
178
 
167
179
 
180
+ # =============================================================================
181
+ # Session Cleanup Policies
182
+ # =============================================================================
183
+
184
+
185
+ class SessionCleanupPolicy(ABC):
186
+ """Abstract policy for determining if a streaming session should be cleaned up.
187
+
188
+ Implement this interface to define custom cleanup strategies.
189
+ """
190
+
191
+ @abstractmethod
192
+ def should_cleanup(self, session: StreamingSession, now: datetime) -> bool:
193
+ """Determine if a session should be removed.
194
+
195
+ Args:
196
+ session: The streaming session to evaluate.
197
+ now: Current timestamp.
198
+
199
+ Returns:
200
+ True if the session should be cleaned up.
201
+ """
202
+ ...
203
+
204
+
205
+ class IdleTTLPolicy(SessionCleanupPolicy):
206
+ """Remove sessions that have been idle longer than their TTL.
207
+
208
+ Supports per-status TTL configuration so that stopped/error sessions
209
+ are cleaned up faster than active ones.
210
+ """
211
+
212
+ # Default TTL per session status (in seconds)
213
+ DEFAULT_STATUS_TTLS: dict[StreamingSessionStatus, int] = {
214
+ StreamingSessionStatus.STOPPED: 300, # 5 min
215
+ StreamingSessionStatus.ERROR: 600, # 10 min
216
+ StreamingSessionStatus.CREATED: 900, # 15 min
217
+ StreamingSessionStatus.PAUSED: 1800, # 30 min
218
+ StreamingSessionStatus.RUNNING: 3600, # 60 min
219
+ }
220
+
221
+ def __init__(
222
+ self,
223
+ default_ttl_seconds: int = 1800,
224
+ status_ttls: dict[StreamingSessionStatus, int] | None = None,
225
+ ) -> None:
226
+ self._default_ttl = default_ttl_seconds
227
+ self._status_ttls = status_ttls or dict(self.DEFAULT_STATUS_TTLS)
228
+
229
+ def should_cleanup(self, session: StreamingSession, now: datetime) -> bool:
230
+ ttl = self._status_ttls.get(session.status, self._default_ttl)
231
+ reference_time = session.last_active_at or session.created_at
232
+ return (now - reference_time) > timedelta(seconds=ttl)
233
+
234
+
235
+ class CompositeCleanupPolicy(SessionCleanupPolicy):
236
+ """Combine multiple cleanup policies with AND/OR logic.
237
+
238
+ Args:
239
+ policies: List of policies to combine.
240
+ require_all: If True, all policies must agree (AND).
241
+ If False, any policy is sufficient (OR). Default: False.
242
+ """
243
+
244
+ def __init__(
245
+ self,
246
+ policies: list[SessionCleanupPolicy],
247
+ require_all: bool = False,
248
+ ) -> None:
249
+ self._policies = policies
250
+ self._require_all = require_all
251
+
252
+ def should_cleanup(self, session: StreamingSession, now: datetime) -> bool:
253
+ if self._require_all:
254
+ return all(p.should_cleanup(session, now) for p in self._policies)
255
+ return any(p.should_cleanup(session, now) for p in self._policies)
256
+
257
+
258
+ # =============================================================================
259
+ # Streaming Anomaly Detector
260
+ # =============================================================================
261
+
262
+
168
263
  class StreamingAnomalyDetector:
169
264
  """Real-time streaming anomaly detection service.
170
265
 
@@ -173,12 +268,80 @@ class StreamingAnomalyDetector:
173
268
  - Multiple algorithms (Z-score, EMA, etc.)
174
269
  - Online learning / model updates
175
270
  - Alert callbacks for real-time notifications
271
+ - Automatic session cleanup via configurable policies
176
272
  """
177
273
 
178
- def __init__(self) -> None:
179
- """Initialize the streaming detector."""
274
+ def __init__(
275
+ self,
276
+ cleanup_policy: SessionCleanupPolicy | None = None,
277
+ cleanup_interval_seconds: int = 60,
278
+ ) -> None:
279
+ """Initialize the streaming detector.
280
+
281
+ Args:
282
+ cleanup_policy: Policy for automatic session cleanup.
283
+ Defaults to IdleTTLPolicy.
284
+ cleanup_interval_seconds: How often to run cleanup (in seconds).
285
+ """
180
286
  self._sessions: dict[str, StreamingSession] = {}
181
287
  self._lock = asyncio.Lock()
288
+ self._cleanup_policy = cleanup_policy or IdleTTLPolicy()
289
+ self._cleanup_interval = cleanup_interval_seconds
290
+ self._cleanup_task: asyncio.Task[None] | None = None
291
+
292
+ # =========================================================================
293
+ # Lifecycle
294
+ # =========================================================================
295
+
296
+ async def start(self) -> None:
297
+ """Start the background session cleanup task."""
298
+ if self._cleanup_task is None:
299
+ self._cleanup_task = asyncio.create_task(self._cleanup_loop())
300
+ logger.info("Streaming session cleanup task started")
301
+
302
+ async def stop(self) -> None:
303
+ """Stop the background session cleanup task."""
304
+ if self._cleanup_task is not None:
305
+ self._cleanup_task.cancel()
306
+ try:
307
+ await self._cleanup_task
308
+ except asyncio.CancelledError:
309
+ pass
310
+ self._cleanup_task = None
311
+ logger.info("Streaming session cleanup task stopped")
312
+
313
+ async def _cleanup_loop(self) -> None:
314
+ """Background loop that periodically removes expired sessions."""
315
+ while True:
316
+ try:
317
+ await asyncio.sleep(self._cleanup_interval)
318
+ removed = await self._cleanup_expired_sessions()
319
+ if removed:
320
+ logger.info(
321
+ "Cleaned up %d expired streaming session(s)", removed
322
+ )
323
+ except asyncio.CancelledError:
324
+ break
325
+ except Exception:
326
+ logger.exception("Error during streaming session cleanup")
327
+
328
+ async def _cleanup_expired_sessions(self) -> int:
329
+ """Remove sessions that match the cleanup policy.
330
+
331
+ Returns:
332
+ Number of sessions removed.
333
+ """
334
+ now = datetime.utcnow()
335
+ async with self._lock:
336
+ to_remove = [
337
+ sid
338
+ for sid, session in self._sessions.items()
339
+ if self._cleanup_policy.should_cleanup(session, now)
340
+ ]
341
+ for sid in to_remove:
342
+ logger.debug("Removing expired streaming session %s", sid)
343
+ del self._sessions[sid]
344
+ return len(to_remove)
182
345
 
183
346
  # =========================================================================
184
347
  # Session Management
@@ -244,6 +407,7 @@ class StreamingAnomalyDetector:
244
407
 
245
408
  session.status = StreamingSessionStatus.RUNNING
246
409
  session.started_at = datetime.utcnow()
410
+ session.touch()
247
411
 
248
412
  return session
249
413
 
@@ -333,6 +497,7 @@ class StreamingAnomalyDetector:
333
497
  if session.status != StreamingSessionStatus.RUNNING:
334
498
  raise ValueError(f"Session '{session_id}' is not running")
335
499
 
500
+ session.touch()
336
501
  timestamp = timestamp or datetime.utcnow()
337
502
 
338
503
  # Store data point in buffer