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,336 @@
|
|
|
1
|
+
"""Data models for escalation system.
|
|
2
|
+
|
|
3
|
+
This module defines the core data structures for the escalation
|
|
4
|
+
system including policies, levels, targets, and incidents.
|
|
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
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EscalationState(str, Enum):
|
|
16
|
+
"""Escalation incident states.
|
|
17
|
+
|
|
18
|
+
State transitions:
|
|
19
|
+
PENDING -> TRIGGERED -> ESCALATED -> RESOLVED
|
|
20
|
+
| ^
|
|
21
|
+
v |
|
|
22
|
+
ACKNOWLEDGED ------+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# Initial state, waiting for trigger
|
|
26
|
+
PENDING = "pending"
|
|
27
|
+
|
|
28
|
+
# Escalation started, first level notified
|
|
29
|
+
TRIGGERED = "triggered"
|
|
30
|
+
|
|
31
|
+
# Someone acknowledged the alert
|
|
32
|
+
ACKNOWLEDGED = "acknowledged"
|
|
33
|
+
|
|
34
|
+
# Moved to next level
|
|
35
|
+
ESCALATED = "escalated"
|
|
36
|
+
|
|
37
|
+
# Issue resolved, escalation stopped
|
|
38
|
+
RESOLVED = "resolved"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TargetType(str, Enum):
|
|
42
|
+
"""Types of escalation targets."""
|
|
43
|
+
|
|
44
|
+
# Individual user
|
|
45
|
+
USER = "user"
|
|
46
|
+
|
|
47
|
+
# Group of users
|
|
48
|
+
GROUP = "group"
|
|
49
|
+
|
|
50
|
+
# On-call rotation
|
|
51
|
+
ONCALL = "oncall"
|
|
52
|
+
|
|
53
|
+
# Channel (Slack channel, email list, etc.)
|
|
54
|
+
CHANNEL = "channel"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class EscalationTarget:
|
|
59
|
+
"""A target for escalation notifications.
|
|
60
|
+
|
|
61
|
+
Attributes:
|
|
62
|
+
type: Type of target (user, group, oncall, channel).
|
|
63
|
+
identifier: Target identifier (username, group name, etc.).
|
|
64
|
+
channel: Notification channel type to use.
|
|
65
|
+
channel_id: Optional specific channel ID.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
type: TargetType
|
|
69
|
+
identifier: str
|
|
70
|
+
channel: str
|
|
71
|
+
channel_id: str | None = None
|
|
72
|
+
|
|
73
|
+
def to_dict(self) -> dict[str, Any]:
|
|
74
|
+
"""Serialize to dictionary."""
|
|
75
|
+
return {
|
|
76
|
+
"type": self.type.value,
|
|
77
|
+
"identifier": self.identifier,
|
|
78
|
+
"channel": self.channel,
|
|
79
|
+
"channel_id": self.channel_id,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def from_dict(cls, data: dict[str, Any]) -> "EscalationTarget":
|
|
84
|
+
"""Create from dictionary."""
|
|
85
|
+
return cls(
|
|
86
|
+
type=TargetType(data["type"]),
|
|
87
|
+
identifier=data["identifier"],
|
|
88
|
+
channel=data["channel"],
|
|
89
|
+
channel_id=data.get("channel_id"),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class EscalationLevel:
|
|
95
|
+
"""A level in the escalation policy.
|
|
96
|
+
|
|
97
|
+
Attributes:
|
|
98
|
+
level: Level number (1 = first, 2 = second, etc.).
|
|
99
|
+
delay_minutes: Minutes to wait before escalating to this level.
|
|
100
|
+
targets: List of targets to notify at this level.
|
|
101
|
+
message_template: Optional custom message template.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
level: int
|
|
105
|
+
delay_minutes: int
|
|
106
|
+
targets: list[EscalationTarget]
|
|
107
|
+
message_template: str | None = None
|
|
108
|
+
|
|
109
|
+
def to_dict(self) -> dict[str, Any]:
|
|
110
|
+
"""Serialize to dictionary."""
|
|
111
|
+
return {
|
|
112
|
+
"level": self.level,
|
|
113
|
+
"delay_minutes": self.delay_minutes,
|
|
114
|
+
"targets": [t.to_dict() for t in self.targets],
|
|
115
|
+
"message_template": self.message_template,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def from_dict(cls, data: dict[str, Any]) -> "EscalationLevel":
|
|
120
|
+
"""Create from dictionary."""
|
|
121
|
+
return cls(
|
|
122
|
+
level=data["level"],
|
|
123
|
+
delay_minutes=data["delay_minutes"],
|
|
124
|
+
targets=[EscalationTarget.from_dict(t) for t in data.get("targets", [])],
|
|
125
|
+
message_template=data.get("message_template"),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass
|
|
130
|
+
class EscalationPolicy:
|
|
131
|
+
"""An escalation policy definition.
|
|
132
|
+
|
|
133
|
+
Attributes:
|
|
134
|
+
id: Unique policy identifier.
|
|
135
|
+
name: Human-readable policy name.
|
|
136
|
+
description: Policy description.
|
|
137
|
+
levels: Ordered list of escalation levels.
|
|
138
|
+
auto_resolve_on_success: Auto-resolve when validation succeeds.
|
|
139
|
+
max_escalations: Maximum escalation attempts per incident.
|
|
140
|
+
is_active: Whether policy is active.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
name: str
|
|
144
|
+
levels: list[EscalationLevel]
|
|
145
|
+
id: str | None = None
|
|
146
|
+
description: str = ""
|
|
147
|
+
auto_resolve_on_success: bool = True
|
|
148
|
+
max_escalations: int = 3
|
|
149
|
+
is_active: bool = True
|
|
150
|
+
|
|
151
|
+
def get_level(self, level_num: int) -> EscalationLevel | None:
|
|
152
|
+
"""Get level by number."""
|
|
153
|
+
for level in self.levels:
|
|
154
|
+
if level.level == level_num:
|
|
155
|
+
return level
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
def get_next_level(self, current_level: int) -> EscalationLevel | None:
|
|
159
|
+
"""Get the next escalation level."""
|
|
160
|
+
for level in self.levels:
|
|
161
|
+
if level.level == current_level + 1:
|
|
162
|
+
return level
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def max_level(self) -> int:
|
|
167
|
+
"""Get highest level number."""
|
|
168
|
+
if not self.levels:
|
|
169
|
+
return 0
|
|
170
|
+
return max(level.level for level in self.levels)
|
|
171
|
+
|
|
172
|
+
def to_dict(self) -> dict[str, Any]:
|
|
173
|
+
"""Serialize to dictionary."""
|
|
174
|
+
return {
|
|
175
|
+
"id": self.id,
|
|
176
|
+
"name": self.name,
|
|
177
|
+
"description": self.description,
|
|
178
|
+
"levels": [level.to_dict() for level in self.levels],
|
|
179
|
+
"auto_resolve_on_success": self.auto_resolve_on_success,
|
|
180
|
+
"max_escalations": self.max_escalations,
|
|
181
|
+
"is_active": self.is_active,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@classmethod
|
|
185
|
+
def from_dict(cls, data: dict[str, Any]) -> "EscalationPolicy":
|
|
186
|
+
"""Create from dictionary."""
|
|
187
|
+
return cls(
|
|
188
|
+
id=data.get("id"),
|
|
189
|
+
name=data["name"],
|
|
190
|
+
description=data.get("description", ""),
|
|
191
|
+
levels=[EscalationLevel.from_dict(l) for l in data.get("levels", [])],
|
|
192
|
+
auto_resolve_on_success=data.get("auto_resolve_on_success", True),
|
|
193
|
+
max_escalations=data.get("max_escalations", 3),
|
|
194
|
+
is_active=data.get("is_active", True),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@dataclass
|
|
199
|
+
class EscalationIncident:
|
|
200
|
+
"""An active escalation incident.
|
|
201
|
+
|
|
202
|
+
Attributes:
|
|
203
|
+
id: Unique incident identifier.
|
|
204
|
+
policy_id: ID of the escalation policy.
|
|
205
|
+
incident_ref: External reference (e.g., validation_id).
|
|
206
|
+
state: Current incident state.
|
|
207
|
+
current_level: Current escalation level (0 = not yet triggered).
|
|
208
|
+
context: Incident context data.
|
|
209
|
+
acknowledged_by: Who acknowledged the incident.
|
|
210
|
+
resolved_by: Who resolved the incident.
|
|
211
|
+
created_at: When incident was created.
|
|
212
|
+
updated_at: When incident was last updated.
|
|
213
|
+
next_escalation_at: When next escalation will occur.
|
|
214
|
+
escalation_count: Number of times escalated.
|
|
215
|
+
events: History of state changes.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
policy_id: str
|
|
219
|
+
incident_ref: str
|
|
220
|
+
id: str | None = None
|
|
221
|
+
state: EscalationState = EscalationState.PENDING
|
|
222
|
+
current_level: int = 0
|
|
223
|
+
context: dict[str, Any] = field(default_factory=dict)
|
|
224
|
+
acknowledged_by: str | None = None
|
|
225
|
+
acknowledged_at: datetime | None = None
|
|
226
|
+
resolved_by: str | None = None
|
|
227
|
+
resolved_at: datetime | None = None
|
|
228
|
+
created_at: datetime = field(default_factory=datetime.utcnow)
|
|
229
|
+
updated_at: datetime = field(default_factory=datetime.utcnow)
|
|
230
|
+
next_escalation_at: datetime | None = None
|
|
231
|
+
escalation_count: int = 0
|
|
232
|
+
events: list["EscalationEvent"] = field(default_factory=list)
|
|
233
|
+
|
|
234
|
+
def to_dict(self) -> dict[str, Any]:
|
|
235
|
+
"""Serialize to dictionary."""
|
|
236
|
+
return {
|
|
237
|
+
"id": self.id,
|
|
238
|
+
"policy_id": self.policy_id,
|
|
239
|
+
"incident_ref": self.incident_ref,
|
|
240
|
+
"state": self.state.value,
|
|
241
|
+
"current_level": self.current_level,
|
|
242
|
+
"context": self.context,
|
|
243
|
+
"acknowledged_by": self.acknowledged_by,
|
|
244
|
+
"acknowledged_at": self.acknowledged_at.isoformat() if self.acknowledged_at else None,
|
|
245
|
+
"resolved_by": self.resolved_by,
|
|
246
|
+
"resolved_at": self.resolved_at.isoformat() if self.resolved_at else None,
|
|
247
|
+
"created_at": self.created_at.isoformat(),
|
|
248
|
+
"updated_at": self.updated_at.isoformat(),
|
|
249
|
+
"next_escalation_at": self.next_escalation_at.isoformat() if self.next_escalation_at else None,
|
|
250
|
+
"escalation_count": self.escalation_count,
|
|
251
|
+
"events": [e.to_dict() for e in self.events],
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@classmethod
|
|
255
|
+
def from_dict(cls, data: dict[str, Any]) -> "EscalationIncident":
|
|
256
|
+
"""Create from dictionary."""
|
|
257
|
+
return cls(
|
|
258
|
+
id=data.get("id"),
|
|
259
|
+
policy_id=data["policy_id"],
|
|
260
|
+
incident_ref=data["incident_ref"],
|
|
261
|
+
state=EscalationState(data.get("state", "pending")),
|
|
262
|
+
current_level=data.get("current_level", 0),
|
|
263
|
+
context=data.get("context", {}),
|
|
264
|
+
acknowledged_by=data.get("acknowledged_by"),
|
|
265
|
+
acknowledged_at=datetime.fromisoformat(data["acknowledged_at"]) if data.get("acknowledged_at") else None,
|
|
266
|
+
resolved_by=data.get("resolved_by"),
|
|
267
|
+
resolved_at=datetime.fromisoformat(data["resolved_at"]) if data.get("resolved_at") else None,
|
|
268
|
+
created_at=datetime.fromisoformat(data["created_at"]) if data.get("created_at") else datetime.utcnow(),
|
|
269
|
+
updated_at=datetime.fromisoformat(data["updated_at"]) if data.get("updated_at") else datetime.utcnow(),
|
|
270
|
+
next_escalation_at=datetime.fromisoformat(data["next_escalation_at"]) if data.get("next_escalation_at") else None,
|
|
271
|
+
escalation_count=data.get("escalation_count", 0),
|
|
272
|
+
events=[EscalationEvent.from_dict(e) for e in data.get("events", [])],
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@dataclass
|
|
277
|
+
class EscalationEvent:
|
|
278
|
+
"""A state change event in an escalation incident.
|
|
279
|
+
|
|
280
|
+
Attributes:
|
|
281
|
+
from_state: Previous state.
|
|
282
|
+
to_state: New state.
|
|
283
|
+
level: Escalation level at time of event.
|
|
284
|
+
actor: Who/what caused the transition.
|
|
285
|
+
message: Event message.
|
|
286
|
+
timestamp: When event occurred.
|
|
287
|
+
metadata: Additional event data.
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
from_state: EscalationState
|
|
291
|
+
to_state: EscalationState
|
|
292
|
+
level: int = 0
|
|
293
|
+
actor: str | None = None
|
|
294
|
+
message: str = ""
|
|
295
|
+
timestamp: datetime = field(default_factory=datetime.utcnow)
|
|
296
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
297
|
+
|
|
298
|
+
def to_dict(self) -> dict[str, Any]:
|
|
299
|
+
"""Serialize to dictionary."""
|
|
300
|
+
return {
|
|
301
|
+
"from_state": self.from_state.value,
|
|
302
|
+
"to_state": self.to_state.value,
|
|
303
|
+
"level": self.level,
|
|
304
|
+
"actor": self.actor,
|
|
305
|
+
"message": self.message,
|
|
306
|
+
"timestamp": self.timestamp.isoformat(),
|
|
307
|
+
"metadata": self.metadata,
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
@classmethod
|
|
311
|
+
def from_dict(cls, data: dict[str, Any]) -> "EscalationEvent":
|
|
312
|
+
"""Create from dictionary."""
|
|
313
|
+
return cls(
|
|
314
|
+
from_state=EscalationState(data["from_state"]),
|
|
315
|
+
to_state=EscalationState(data["to_state"]),
|
|
316
|
+
level=data.get("level", 0),
|
|
317
|
+
actor=data.get("actor"),
|
|
318
|
+
message=data.get("message", ""),
|
|
319
|
+
timestamp=datetime.fromisoformat(data["timestamp"]) if data.get("timestamp") else datetime.utcnow(),
|
|
320
|
+
metadata=data.get("metadata", {}),
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@dataclass
|
|
325
|
+
class StateTransition:
|
|
326
|
+
"""Defines a valid state transition.
|
|
327
|
+
|
|
328
|
+
Attributes:
|
|
329
|
+
from_states: Valid source states.
|
|
330
|
+
to_state: Target state.
|
|
331
|
+
requires_actor: Whether actor info is required.
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
from_states: list[EscalationState]
|
|
335
|
+
to_state: EscalationState
|
|
336
|
+
requires_actor: bool = False
|