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,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