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.
- 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.0.dist-info → truthound_dashboard-1.4.0.dist-info}/METADATA +142 -18
- truthound_dashboard-1.4.0.dist-info/RECORD +239 -0
- truthound_dashboard/static/assets/index-BCA8H1hO.js +0 -574
- truthound_dashboard/static/assets/index-BNsSQ2fN.css +0 -1
- truthound_dashboard/static/assets/unmerged_dictionaries-CsJWCRx9.js +0 -1
- truthound_dashboard-1.3.0.dist-info/RECORD +0 -110
- {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1363 @@
|
|
|
1
|
+
"""Pydantic schemas for advanced notification features.
|
|
2
|
+
|
|
3
|
+
This module provides schemas for:
|
|
4
|
+
- Routing rules (11 rule types + combinators)
|
|
5
|
+
- Deduplication configuration (4 strategies, 6 policies)
|
|
6
|
+
- Throttling configuration
|
|
7
|
+
- Escalation policies and incidents
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from typing import Any, Literal
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
17
|
+
|
|
18
|
+
from .base import BaseSchema, IDMixin, ListResponseWrapper, TimestampMixin
|
|
19
|
+
from ..core.validation_limits import (
|
|
20
|
+
get_deduplication_limits,
|
|
21
|
+
get_escalation_limits,
|
|
22
|
+
get_throttling_limits,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# =============================================================================
|
|
27
|
+
# Enums
|
|
28
|
+
# =============================================================================
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RuleType(str, Enum):
|
|
32
|
+
"""Available routing rule types."""
|
|
33
|
+
|
|
34
|
+
SEVERITY = "severity"
|
|
35
|
+
ISSUE_COUNT = "issue_count"
|
|
36
|
+
PASS_RATE = "pass_rate"
|
|
37
|
+
TIME_WINDOW = "time_window"
|
|
38
|
+
TAG = "tag"
|
|
39
|
+
DATA_ASSET = "data_asset"
|
|
40
|
+
METADATA = "metadata"
|
|
41
|
+
STATUS = "status"
|
|
42
|
+
ERROR = "error"
|
|
43
|
+
ALWAYS = "always"
|
|
44
|
+
NEVER = "never"
|
|
45
|
+
# Combinators
|
|
46
|
+
ALL_OF = "all_of"
|
|
47
|
+
ANY_OF = "any_of"
|
|
48
|
+
NOT = "not"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class DeduplicationStrategy(str, Enum):
|
|
52
|
+
"""Deduplication window strategies."""
|
|
53
|
+
|
|
54
|
+
SLIDING = "sliding"
|
|
55
|
+
TUMBLING = "tumbling"
|
|
56
|
+
SESSION = "session"
|
|
57
|
+
ADAPTIVE = "adaptive"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class DeduplicationPolicy(str, Enum):
|
|
61
|
+
"""Deduplication policies."""
|
|
62
|
+
|
|
63
|
+
NONE = "none"
|
|
64
|
+
BASIC = "basic"
|
|
65
|
+
SEVERITY = "severity"
|
|
66
|
+
ISSUE_BASED = "issue_based"
|
|
67
|
+
STRICT = "strict"
|
|
68
|
+
CUSTOM = "custom"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class EscalationState(str, Enum):
|
|
72
|
+
"""Escalation incident states."""
|
|
73
|
+
|
|
74
|
+
PENDING = "pending"
|
|
75
|
+
TRIGGERED = "triggered"
|
|
76
|
+
ACKNOWLEDGED = "acknowledged"
|
|
77
|
+
ESCALATED = "escalated"
|
|
78
|
+
RESOLVED = "resolved"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TargetType(str, Enum):
|
|
82
|
+
"""Escalation target types."""
|
|
83
|
+
|
|
84
|
+
USER = "user"
|
|
85
|
+
GROUP = "group"
|
|
86
|
+
ONCALL = "oncall"
|
|
87
|
+
CHANNEL = "channel"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# =============================================================================
|
|
91
|
+
# Routing Rules Schemas
|
|
92
|
+
# =============================================================================
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class CombinatorType(str, Enum):
|
|
96
|
+
"""Available combinator types for combining rules."""
|
|
97
|
+
|
|
98
|
+
ALL_OF = "all_of"
|
|
99
|
+
ANY_OF = "any_of"
|
|
100
|
+
NOT = "not"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class RuleConfig(BaseModel):
|
|
104
|
+
"""Base configuration for a routing rule."""
|
|
105
|
+
|
|
106
|
+
type: str = Field(..., description="Rule type")
|
|
107
|
+
params: dict[str, Any] = Field(default_factory=dict, description="Rule parameters")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class NestedRuleConfig(BaseModel):
|
|
111
|
+
"""Nested rule configuration supporting combinators.
|
|
112
|
+
|
|
113
|
+
Supports both simple rules and combinator rules (all_of, any_of, not).
|
|
114
|
+
For simple rules, use `type` and `params`.
|
|
115
|
+
For combinator rules, use `type` and `rules` (for all_of/any_of) or `rule` (for not).
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
type: str = Field(..., description="Rule type or combinator type")
|
|
119
|
+
params: dict[str, Any] = Field(
|
|
120
|
+
default_factory=dict,
|
|
121
|
+
description="Rule parameters (for non-combinator rules)",
|
|
122
|
+
)
|
|
123
|
+
rules: list["NestedRuleConfig"] | None = Field(
|
|
124
|
+
None,
|
|
125
|
+
description="Nested rules for all_of/any_of combinators",
|
|
126
|
+
)
|
|
127
|
+
rule: "NestedRuleConfig | None" = Field(
|
|
128
|
+
None,
|
|
129
|
+
description="Nested rule for 'not' combinator",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def is_combinator(self) -> bool:
|
|
133
|
+
"""Check if this is a combinator rule."""
|
|
134
|
+
return self.type in (
|
|
135
|
+
CombinatorType.ALL_OF.value,
|
|
136
|
+
CombinatorType.ANY_OF.value,
|
|
137
|
+
CombinatorType.NOT.value,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# Rebuild for self-referencing model
|
|
142
|
+
NestedRuleConfig.model_rebuild()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class RuleValidationResult(BaseModel):
|
|
146
|
+
"""Result of rule configuration validation."""
|
|
147
|
+
|
|
148
|
+
valid: bool = Field(..., description="Whether the configuration is valid")
|
|
149
|
+
errors: list[str] = Field(
|
|
150
|
+
default_factory=list,
|
|
151
|
+
description="List of validation errors",
|
|
152
|
+
)
|
|
153
|
+
warnings: list[str] = Field(
|
|
154
|
+
default_factory=list,
|
|
155
|
+
description="List of validation warnings",
|
|
156
|
+
)
|
|
157
|
+
rule_count: int = Field(
|
|
158
|
+
default=0,
|
|
159
|
+
description="Total number of rules (including nested)",
|
|
160
|
+
)
|
|
161
|
+
max_depth: int = Field(
|
|
162
|
+
default=0,
|
|
163
|
+
description="Maximum nesting depth",
|
|
164
|
+
)
|
|
165
|
+
circular_paths: list[str] = Field(
|
|
166
|
+
default_factory=list,
|
|
167
|
+
description="Paths of detected circular references (if any)",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class RoutingRuleBase(BaseSchema):
|
|
172
|
+
"""Base routing rule schema."""
|
|
173
|
+
|
|
174
|
+
name: str = Field(..., description="Rule name", min_length=1, max_length=255)
|
|
175
|
+
rule_config: dict[str, Any] = Field(..., description="Rule configuration JSON")
|
|
176
|
+
actions: list[str] = Field(..., description="Channel IDs to notify", min_length=1)
|
|
177
|
+
priority: int = Field(default=0, description="Priority (higher = more important)")
|
|
178
|
+
is_active: bool = Field(default=True, description="Whether rule is active")
|
|
179
|
+
stop_on_match: bool = Field(default=False, description="Stop processing after match")
|
|
180
|
+
metadata: dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class RoutingRuleCreate(RoutingRuleBase):
|
|
184
|
+
"""Schema for creating a routing rule."""
|
|
185
|
+
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class RoutingRuleUpdate(BaseSchema):
|
|
190
|
+
"""Schema for updating a routing rule."""
|
|
191
|
+
|
|
192
|
+
name: str | None = Field(None, description="Rule name", min_length=1, max_length=255)
|
|
193
|
+
rule_config: dict[str, Any] | None = Field(None, description="Rule configuration JSON")
|
|
194
|
+
actions: list[str] | None = Field(None, description="Channel IDs to notify", min_length=1)
|
|
195
|
+
priority: int | None = Field(None, description="Priority")
|
|
196
|
+
is_active: bool | None = Field(None, description="Whether rule is active")
|
|
197
|
+
stop_on_match: bool | None = Field(None, description="Stop processing after match")
|
|
198
|
+
metadata: dict[str, Any] | None = Field(None, description="Additional metadata")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class RoutingRuleResponse(RoutingRuleBase, IDMixin, TimestampMixin):
|
|
202
|
+
"""Schema for routing rule response."""
|
|
203
|
+
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class RoutingRuleListResponse(ListResponseWrapper):
|
|
208
|
+
"""Schema for routing rule list response."""
|
|
209
|
+
|
|
210
|
+
items: list[RoutingRuleResponse]
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class RuleTypeInfo(BaseModel):
|
|
214
|
+
"""Information about a rule type."""
|
|
215
|
+
|
|
216
|
+
type: str = Field(..., description="Rule type identifier")
|
|
217
|
+
name: str = Field(..., description="Human-readable name")
|
|
218
|
+
description: str = Field(..., description="Rule description")
|
|
219
|
+
param_schema: dict[str, Any] = Field(..., description="Parameter schema")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class RuleTypesResponse(BaseModel):
|
|
223
|
+
"""Response for available rule types."""
|
|
224
|
+
|
|
225
|
+
rule_types: list[RuleTypeInfo]
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# =============================================================================
|
|
229
|
+
# Deduplication Schemas
|
|
230
|
+
# =============================================================================
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class DeduplicationConfigBase(BaseSchema):
|
|
234
|
+
"""Base deduplication configuration schema.
|
|
235
|
+
|
|
236
|
+
Validation:
|
|
237
|
+
- window_seconds: Must be between 1 and 86400 seconds (24 hours).
|
|
238
|
+
These limits are configurable via environment variables:
|
|
239
|
+
- TRUTHOUND_DEDUP_WINDOW_MIN
|
|
240
|
+
- TRUTHOUND_DEDUP_WINDOW_MAX
|
|
241
|
+
|
|
242
|
+
DoS Prevention:
|
|
243
|
+
- Minimum window prevents excessive memory churn from tiny windows.
|
|
244
|
+
- Maximum window prevents memory exhaustion from excessively long windows.
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
name: str = Field(..., description="Configuration name", min_length=1, max_length=255)
|
|
248
|
+
strategy: DeduplicationStrategy = Field(
|
|
249
|
+
default=DeduplicationStrategy.SLIDING,
|
|
250
|
+
description="Window strategy",
|
|
251
|
+
)
|
|
252
|
+
policy: DeduplicationPolicy = Field(
|
|
253
|
+
default=DeduplicationPolicy.BASIC,
|
|
254
|
+
description="Deduplication policy",
|
|
255
|
+
)
|
|
256
|
+
window_seconds: int = Field(
|
|
257
|
+
default=300,
|
|
258
|
+
description="Window duration in seconds (1-86400, configurable via env vars)",
|
|
259
|
+
ge=1,
|
|
260
|
+
le=86400,
|
|
261
|
+
)
|
|
262
|
+
is_active: bool = Field(default=True, description="Whether config is active")
|
|
263
|
+
|
|
264
|
+
@field_validator("window_seconds")
|
|
265
|
+
@classmethod
|
|
266
|
+
def validate_window_seconds(cls, v: int) -> int:
|
|
267
|
+
"""Validate window_seconds against configurable limits.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
v: Window duration in seconds.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Validated window duration.
|
|
274
|
+
|
|
275
|
+
Raises:
|
|
276
|
+
ValueError: If window_seconds is outside allowed range.
|
|
277
|
+
"""
|
|
278
|
+
limits = get_deduplication_limits()
|
|
279
|
+
valid, error = limits.validate_window_seconds(v)
|
|
280
|
+
if not valid:
|
|
281
|
+
raise ValueError(error)
|
|
282
|
+
return v
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class DeduplicationConfigCreate(DeduplicationConfigBase):
|
|
286
|
+
"""Schema for creating deduplication config."""
|
|
287
|
+
|
|
288
|
+
pass
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class DeduplicationConfigUpdate(BaseSchema):
|
|
292
|
+
"""Schema for updating deduplication config."""
|
|
293
|
+
|
|
294
|
+
name: str | None = Field(None, description="Configuration name")
|
|
295
|
+
strategy: DeduplicationStrategy | None = Field(None, description="Window strategy")
|
|
296
|
+
policy: DeduplicationPolicy | None = Field(None, description="Deduplication policy")
|
|
297
|
+
window_seconds: int | None = Field(None, description="Window duration in seconds")
|
|
298
|
+
is_active: bool | None = Field(None, description="Whether config is active")
|
|
299
|
+
|
|
300
|
+
@field_validator("window_seconds")
|
|
301
|
+
@classmethod
|
|
302
|
+
def validate_window_seconds(cls, v: int | None) -> int | None:
|
|
303
|
+
"""Validate window_seconds against configurable limits.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
v: Window duration in seconds or None.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
Validated window duration or None.
|
|
310
|
+
|
|
311
|
+
Raises:
|
|
312
|
+
ValueError: If window_seconds is outside allowed range.
|
|
313
|
+
"""
|
|
314
|
+
if v is None:
|
|
315
|
+
return v
|
|
316
|
+
limits = get_deduplication_limits()
|
|
317
|
+
valid, error = limits.validate_window_seconds(v)
|
|
318
|
+
if not valid:
|
|
319
|
+
raise ValueError(error)
|
|
320
|
+
return v
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class DeduplicationConfigResponse(DeduplicationConfigBase, IDMixin, TimestampMixin):
|
|
324
|
+
"""Schema for deduplication config response."""
|
|
325
|
+
|
|
326
|
+
pass
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class DeduplicationConfigListResponse(ListResponseWrapper):
|
|
330
|
+
"""Schema for deduplication config list response."""
|
|
331
|
+
|
|
332
|
+
items: list[DeduplicationConfigResponse]
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class DeduplicationStats(BaseModel):
|
|
336
|
+
"""Deduplication statistics."""
|
|
337
|
+
|
|
338
|
+
total_received: int = Field(default=0, description="Total notifications received")
|
|
339
|
+
total_deduplicated: int = Field(default=0, description="Total notifications deduplicated")
|
|
340
|
+
total_passed: int = Field(default=0, description="Total notifications passed through")
|
|
341
|
+
dedup_rate: float = Field(default=0.0, description="Deduplication rate percentage")
|
|
342
|
+
active_fingerprints: int = Field(default=0, description="Active fingerprints in window")
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
# =============================================================================
|
|
346
|
+
# Throttling Schemas
|
|
347
|
+
# =============================================================================
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class ThrottlingConfigBase(BaseSchema):
|
|
351
|
+
"""Base throttling configuration schema.
|
|
352
|
+
|
|
353
|
+
Validation:
|
|
354
|
+
- per_minute: Must be between 1 and 10000 (configurable).
|
|
355
|
+
- per_hour: Must be between 1 and 100000 (configurable).
|
|
356
|
+
- per_day: Must be between 1 and 1000000 (configurable).
|
|
357
|
+
- burst_allowance: Must be between 1.0 and 10.0 (configurable).
|
|
358
|
+
|
|
359
|
+
DoS Prevention:
|
|
360
|
+
- Upper limits prevent unreasonable rate limits that could
|
|
361
|
+
consume excessive memory or processing resources.
|
|
362
|
+
- Lower limits ensure at least one notification is allowed.
|
|
363
|
+
|
|
364
|
+
Environment Variables:
|
|
365
|
+
- TRUTHOUND_THROTTLE_PER_MINUTE_MAX
|
|
366
|
+
- TRUTHOUND_THROTTLE_PER_HOUR_MAX
|
|
367
|
+
- TRUTHOUND_THROTTLE_PER_DAY_MAX
|
|
368
|
+
- TRUTHOUND_THROTTLE_BURST_MIN
|
|
369
|
+
- TRUTHOUND_THROTTLE_BURST_MAX
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
name: str = Field(..., description="Configuration name", min_length=1, max_length=255)
|
|
373
|
+
per_minute: int | None = Field(
|
|
374
|
+
None,
|
|
375
|
+
description="Max notifications per minute (1-10000, configurable)",
|
|
376
|
+
ge=1,
|
|
377
|
+
)
|
|
378
|
+
per_hour: int | None = Field(
|
|
379
|
+
None,
|
|
380
|
+
description="Max notifications per hour (1-100000, configurable)",
|
|
381
|
+
ge=1,
|
|
382
|
+
)
|
|
383
|
+
per_day: int | None = Field(
|
|
384
|
+
None,
|
|
385
|
+
description="Max notifications per day (1-1000000, configurable)",
|
|
386
|
+
ge=1,
|
|
387
|
+
)
|
|
388
|
+
burst_allowance: float = Field(
|
|
389
|
+
default=1.5,
|
|
390
|
+
description="Burst allowance factor (1.0-10.0, configurable)",
|
|
391
|
+
ge=1.0,
|
|
392
|
+
le=10.0,
|
|
393
|
+
)
|
|
394
|
+
channel_id: str | None = Field(None, description="Channel ID (null = global)")
|
|
395
|
+
is_active: bool = Field(default=True, description="Whether config is active")
|
|
396
|
+
|
|
397
|
+
@field_validator("per_minute")
|
|
398
|
+
@classmethod
|
|
399
|
+
def validate_per_minute(cls, v: int | None) -> int | None:
|
|
400
|
+
"""Validate per_minute against configurable limits.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
v: Max notifications per minute or None.
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
Validated value or None.
|
|
407
|
+
|
|
408
|
+
Raises:
|
|
409
|
+
ValueError: If value exceeds maximum limit.
|
|
410
|
+
"""
|
|
411
|
+
if v is None:
|
|
412
|
+
return v
|
|
413
|
+
limits = get_throttling_limits()
|
|
414
|
+
if v > limits.per_minute_max:
|
|
415
|
+
raise ValueError(
|
|
416
|
+
f"per_minute must not exceed {limits.per_minute_max}, got {v}"
|
|
417
|
+
)
|
|
418
|
+
return v
|
|
419
|
+
|
|
420
|
+
@field_validator("per_hour")
|
|
421
|
+
@classmethod
|
|
422
|
+
def validate_per_hour(cls, v: int | None) -> int | None:
|
|
423
|
+
"""Validate per_hour against configurable limits.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
v: Max notifications per hour or None.
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
Validated value or None.
|
|
430
|
+
|
|
431
|
+
Raises:
|
|
432
|
+
ValueError: If value exceeds maximum limit.
|
|
433
|
+
"""
|
|
434
|
+
if v is None:
|
|
435
|
+
return v
|
|
436
|
+
limits = get_throttling_limits()
|
|
437
|
+
if v > limits.per_hour_max:
|
|
438
|
+
raise ValueError(
|
|
439
|
+
f"per_hour must not exceed {limits.per_hour_max}, got {v}"
|
|
440
|
+
)
|
|
441
|
+
return v
|
|
442
|
+
|
|
443
|
+
@field_validator("per_day")
|
|
444
|
+
@classmethod
|
|
445
|
+
def validate_per_day(cls, v: int | None) -> int | None:
|
|
446
|
+
"""Validate per_day against configurable limits.
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
v: Max notifications per day or None.
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
Validated value or None.
|
|
453
|
+
|
|
454
|
+
Raises:
|
|
455
|
+
ValueError: If value exceeds maximum limit.
|
|
456
|
+
"""
|
|
457
|
+
if v is None:
|
|
458
|
+
return v
|
|
459
|
+
limits = get_throttling_limits()
|
|
460
|
+
if v > limits.per_day_max:
|
|
461
|
+
raise ValueError(
|
|
462
|
+
f"per_day must not exceed {limits.per_day_max}, got {v}"
|
|
463
|
+
)
|
|
464
|
+
return v
|
|
465
|
+
|
|
466
|
+
@field_validator("burst_allowance")
|
|
467
|
+
@classmethod
|
|
468
|
+
def validate_burst_allowance(cls, v: float) -> float:
|
|
469
|
+
"""Validate burst_allowance against configurable limits.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
v: Burst allowance factor.
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
Validated burst allowance.
|
|
476
|
+
|
|
477
|
+
Raises:
|
|
478
|
+
ValueError: If value is outside allowed range.
|
|
479
|
+
"""
|
|
480
|
+
limits = get_throttling_limits()
|
|
481
|
+
valid, error = limits.validate_burst_allowance(v)
|
|
482
|
+
if not valid:
|
|
483
|
+
raise ValueError(error)
|
|
484
|
+
return v
|
|
485
|
+
|
|
486
|
+
@model_validator(mode="after")
|
|
487
|
+
def validate_at_least_one_limit(self) -> "ThrottlingConfigBase":
|
|
488
|
+
"""Ensure at least one rate limit is specified.
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
Validated model instance.
|
|
492
|
+
|
|
493
|
+
Raises:
|
|
494
|
+
ValueError: If no rate limits are specified.
|
|
495
|
+
"""
|
|
496
|
+
if (
|
|
497
|
+
self.per_minute is None
|
|
498
|
+
and self.per_hour is None
|
|
499
|
+
and self.per_day is None
|
|
500
|
+
):
|
|
501
|
+
raise ValueError(
|
|
502
|
+
"At least one rate limit must be specified: per_minute, per_hour, or per_day"
|
|
503
|
+
)
|
|
504
|
+
return self
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
class ThrottlingConfigCreate(ThrottlingConfigBase):
|
|
508
|
+
"""Schema for creating throttling config."""
|
|
509
|
+
|
|
510
|
+
pass
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
class ThrottlingConfigUpdate(BaseSchema):
|
|
514
|
+
"""Schema for updating throttling config."""
|
|
515
|
+
|
|
516
|
+
name: str | None = Field(None, description="Configuration name")
|
|
517
|
+
per_minute: int | None = Field(None, description="Max notifications per minute", ge=1)
|
|
518
|
+
per_hour: int | None = Field(None, description="Max notifications per hour", ge=1)
|
|
519
|
+
per_day: int | None = Field(None, description="Max notifications per day", ge=1)
|
|
520
|
+
burst_allowance: float | None = Field(None, description="Burst allowance factor")
|
|
521
|
+
channel_id: str | None = Field(None, description="Channel ID")
|
|
522
|
+
is_active: bool | None = Field(None, description="Whether config is active")
|
|
523
|
+
|
|
524
|
+
@field_validator("per_minute")
|
|
525
|
+
@classmethod
|
|
526
|
+
def validate_per_minute(cls, v: int | None) -> int | None:
|
|
527
|
+
"""Validate per_minute against configurable limits."""
|
|
528
|
+
if v is None:
|
|
529
|
+
return v
|
|
530
|
+
limits = get_throttling_limits()
|
|
531
|
+
if v > limits.per_minute_max:
|
|
532
|
+
raise ValueError(
|
|
533
|
+
f"per_minute must not exceed {limits.per_minute_max}, got {v}"
|
|
534
|
+
)
|
|
535
|
+
return v
|
|
536
|
+
|
|
537
|
+
@field_validator("per_hour")
|
|
538
|
+
@classmethod
|
|
539
|
+
def validate_per_hour(cls, v: int | None) -> int | None:
|
|
540
|
+
"""Validate per_hour against configurable limits."""
|
|
541
|
+
if v is None:
|
|
542
|
+
return v
|
|
543
|
+
limits = get_throttling_limits()
|
|
544
|
+
if v > limits.per_hour_max:
|
|
545
|
+
raise ValueError(
|
|
546
|
+
f"per_hour must not exceed {limits.per_hour_max}, got {v}"
|
|
547
|
+
)
|
|
548
|
+
return v
|
|
549
|
+
|
|
550
|
+
@field_validator("per_day")
|
|
551
|
+
@classmethod
|
|
552
|
+
def validate_per_day(cls, v: int | None) -> int | None:
|
|
553
|
+
"""Validate per_day against configurable limits."""
|
|
554
|
+
if v is None:
|
|
555
|
+
return v
|
|
556
|
+
limits = get_throttling_limits()
|
|
557
|
+
if v > limits.per_day_max:
|
|
558
|
+
raise ValueError(
|
|
559
|
+
f"per_day must not exceed {limits.per_day_max}, got {v}"
|
|
560
|
+
)
|
|
561
|
+
return v
|
|
562
|
+
|
|
563
|
+
@field_validator("burst_allowance")
|
|
564
|
+
@classmethod
|
|
565
|
+
def validate_burst_allowance(cls, v: float | None) -> float | None:
|
|
566
|
+
"""Validate burst_allowance against configurable limits."""
|
|
567
|
+
if v is None:
|
|
568
|
+
return v
|
|
569
|
+
limits = get_throttling_limits()
|
|
570
|
+
valid, error = limits.validate_burst_allowance(v)
|
|
571
|
+
if not valid:
|
|
572
|
+
raise ValueError(error)
|
|
573
|
+
return v
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
class ThrottlingConfigResponse(ThrottlingConfigBase, IDMixin, TimestampMixin):
|
|
577
|
+
"""Schema for throttling config response."""
|
|
578
|
+
|
|
579
|
+
pass
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
class ThrottlingConfigListResponse(ListResponseWrapper):
|
|
583
|
+
"""Schema for throttling config list response."""
|
|
584
|
+
|
|
585
|
+
items: list[ThrottlingConfigResponse]
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
class ThrottlingStats(BaseModel):
|
|
589
|
+
"""Throttling statistics."""
|
|
590
|
+
|
|
591
|
+
total_received: int = Field(default=0, description="Total notifications received")
|
|
592
|
+
total_throttled: int = Field(default=0, description="Total notifications throttled")
|
|
593
|
+
total_passed: int = Field(default=0, description="Total notifications passed through")
|
|
594
|
+
throttle_rate: float = Field(default=0.0, description="Throttle rate percentage")
|
|
595
|
+
current_window_count: int = Field(default=0, description="Count in current window")
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
# =============================================================================
|
|
599
|
+
# Escalation Schemas
|
|
600
|
+
# =============================================================================
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
class EscalationTargetBase(BaseModel):
|
|
604
|
+
"""Escalation target schema."""
|
|
605
|
+
|
|
606
|
+
type: TargetType = Field(..., description="Target type")
|
|
607
|
+
identifier: str = Field(..., description="Target identifier (email, group name, etc.)")
|
|
608
|
+
channel: str = Field(..., description="Notification channel ID")
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
class EscalationLevelBase(BaseModel):
|
|
612
|
+
"""Escalation level schema.
|
|
613
|
+
|
|
614
|
+
Validation:
|
|
615
|
+
- level: Must be at least 1.
|
|
616
|
+
- delay_minutes: Must be between 0 and 10080 (7 days, configurable).
|
|
617
|
+
- targets: At least one target must be specified.
|
|
618
|
+
|
|
619
|
+
DoS Prevention:
|
|
620
|
+
- Maximum delay prevents excessively long escalation windows.
|
|
621
|
+
- Configurable via TRUTHOUND_ESCALATION_DELAY_MAX environment variable.
|
|
622
|
+
"""
|
|
623
|
+
|
|
624
|
+
level: int = Field(..., description="Level number (1 = first)", ge=1)
|
|
625
|
+
delay_minutes: int = Field(
|
|
626
|
+
...,
|
|
627
|
+
description="Delay before escalating to next level (0-10080 minutes, configurable)",
|
|
628
|
+
ge=0,
|
|
629
|
+
)
|
|
630
|
+
targets: list[EscalationTargetBase] = Field(
|
|
631
|
+
...,
|
|
632
|
+
description="Targets to notify at this level",
|
|
633
|
+
min_length=1,
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
@field_validator("delay_minutes")
|
|
637
|
+
@classmethod
|
|
638
|
+
def validate_delay_minutes(cls, v: int) -> int:
|
|
639
|
+
"""Validate delay_minutes against configurable limits.
|
|
640
|
+
|
|
641
|
+
Args:
|
|
642
|
+
v: Delay in minutes.
|
|
643
|
+
|
|
644
|
+
Returns:
|
|
645
|
+
Validated delay value.
|
|
646
|
+
|
|
647
|
+
Raises:
|
|
648
|
+
ValueError: If delay exceeds maximum limit.
|
|
649
|
+
"""
|
|
650
|
+
limits = get_escalation_limits()
|
|
651
|
+
valid, error = limits.validate_delay_minutes(v)
|
|
652
|
+
if not valid:
|
|
653
|
+
raise ValueError(error)
|
|
654
|
+
return v
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
class EscalationPolicyBase(BaseSchema):
|
|
658
|
+
"""Base escalation policy schema.
|
|
659
|
+
|
|
660
|
+
Validation:
|
|
661
|
+
- levels: Must have at least 1 level and at most 20 levels (configurable).
|
|
662
|
+
- max_escalations: Must be between 1 and 100 (configurable).
|
|
663
|
+
|
|
664
|
+
DoS Prevention:
|
|
665
|
+
- Maximum levels prevents excessive escalation chains.
|
|
666
|
+
- Maximum escalations prevents infinite retry loops.
|
|
667
|
+
|
|
668
|
+
Environment Variables:
|
|
669
|
+
- TRUTHOUND_ESCALATION_MAX_LEVELS
|
|
670
|
+
- TRUTHOUND_ESCALATION_MAX_ESCALATIONS_MIN
|
|
671
|
+
- TRUTHOUND_ESCALATION_MAX_ESCALATIONS_MAX
|
|
672
|
+
"""
|
|
673
|
+
|
|
674
|
+
name: str = Field(..., description="Policy name", min_length=1, max_length=255)
|
|
675
|
+
description: str = Field(default="", description="Policy description")
|
|
676
|
+
levels: list[EscalationLevelBase] = Field(
|
|
677
|
+
...,
|
|
678
|
+
description="Escalation levels (1-20 levels, configurable)",
|
|
679
|
+
min_length=1,
|
|
680
|
+
)
|
|
681
|
+
auto_resolve_on_success: bool = Field(
|
|
682
|
+
default=True,
|
|
683
|
+
description="Auto-resolve when validation passes",
|
|
684
|
+
)
|
|
685
|
+
max_escalations: int = Field(
|
|
686
|
+
default=3,
|
|
687
|
+
description="Maximum escalation attempts (1-100, configurable)",
|
|
688
|
+
ge=1,
|
|
689
|
+
)
|
|
690
|
+
is_active: bool = Field(default=True, description="Whether policy is active")
|
|
691
|
+
|
|
692
|
+
@field_validator("levels")
|
|
693
|
+
@classmethod
|
|
694
|
+
def validate_levels(cls, v: list[EscalationLevelBase]) -> list[EscalationLevelBase]:
|
|
695
|
+
"""Validate escalation levels against configurable limits.
|
|
696
|
+
|
|
697
|
+
Args:
|
|
698
|
+
v: List of escalation levels.
|
|
699
|
+
|
|
700
|
+
Returns:
|
|
701
|
+
Validated levels list.
|
|
702
|
+
|
|
703
|
+
Raises:
|
|
704
|
+
ValueError: If too many levels or invalid level numbers.
|
|
705
|
+
"""
|
|
706
|
+
limits = get_escalation_limits()
|
|
707
|
+
if len(v) > limits.max_levels:
|
|
708
|
+
raise ValueError(
|
|
709
|
+
f"Cannot have more than {limits.max_levels} escalation levels, "
|
|
710
|
+
f"got {len(v)}"
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
# Validate level numbers are sequential and start from 1
|
|
714
|
+
level_numbers = [level.level for level in v]
|
|
715
|
+
expected_levels = list(range(1, len(v) + 1))
|
|
716
|
+
if sorted(level_numbers) != expected_levels:
|
|
717
|
+
raise ValueError(
|
|
718
|
+
f"Level numbers must be sequential starting from 1, "
|
|
719
|
+
f"got {sorted(level_numbers)}"
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
return v
|
|
723
|
+
|
|
724
|
+
@field_validator("max_escalations")
|
|
725
|
+
@classmethod
|
|
726
|
+
def validate_max_escalations(cls, v: int) -> int:
|
|
727
|
+
"""Validate max_escalations against configurable limits.
|
|
728
|
+
|
|
729
|
+
Args:
|
|
730
|
+
v: Maximum escalation attempts.
|
|
731
|
+
|
|
732
|
+
Returns:
|
|
733
|
+
Validated max_escalations value.
|
|
734
|
+
|
|
735
|
+
Raises:
|
|
736
|
+
ValueError: If value is outside allowed range.
|
|
737
|
+
"""
|
|
738
|
+
limits = get_escalation_limits()
|
|
739
|
+
valid, error = limits.validate_max_escalations(v)
|
|
740
|
+
if not valid:
|
|
741
|
+
raise ValueError(error)
|
|
742
|
+
return v
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
class EscalationPolicyCreate(EscalationPolicyBase):
|
|
746
|
+
"""Schema for creating escalation policy."""
|
|
747
|
+
|
|
748
|
+
pass
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
class EscalationPolicyUpdate(BaseSchema):
|
|
752
|
+
"""Schema for updating escalation policy."""
|
|
753
|
+
|
|
754
|
+
name: str | None = Field(None, description="Policy name")
|
|
755
|
+
description: str | None = Field(None, description="Policy description")
|
|
756
|
+
levels: list[EscalationLevelBase] | None = Field(None, description="Escalation levels")
|
|
757
|
+
auto_resolve_on_success: bool | None = Field(None, description="Auto-resolve on success")
|
|
758
|
+
max_escalations: int | None = Field(None, description="Maximum escalation attempts", ge=1)
|
|
759
|
+
is_active: bool | None = Field(None, description="Whether policy is active")
|
|
760
|
+
|
|
761
|
+
@field_validator("levels")
|
|
762
|
+
@classmethod
|
|
763
|
+
def validate_levels(
|
|
764
|
+
cls, v: list[EscalationLevelBase] | None
|
|
765
|
+
) -> list[EscalationLevelBase] | None:
|
|
766
|
+
"""Validate escalation levels against configurable limits."""
|
|
767
|
+
if v is None:
|
|
768
|
+
return v
|
|
769
|
+
limits = get_escalation_limits()
|
|
770
|
+
if len(v) > limits.max_levels:
|
|
771
|
+
raise ValueError(
|
|
772
|
+
f"Cannot have more than {limits.max_levels} escalation levels, "
|
|
773
|
+
f"got {len(v)}"
|
|
774
|
+
)
|
|
775
|
+
if len(v) > 0:
|
|
776
|
+
level_numbers = [level.level for level in v]
|
|
777
|
+
expected_levels = list(range(1, len(v) + 1))
|
|
778
|
+
if sorted(level_numbers) != expected_levels:
|
|
779
|
+
raise ValueError(
|
|
780
|
+
f"Level numbers must be sequential starting from 1, "
|
|
781
|
+
f"got {sorted(level_numbers)}"
|
|
782
|
+
)
|
|
783
|
+
return v
|
|
784
|
+
|
|
785
|
+
@field_validator("max_escalations")
|
|
786
|
+
@classmethod
|
|
787
|
+
def validate_max_escalations(cls, v: int | None) -> int | None:
|
|
788
|
+
"""Validate max_escalations against configurable limits."""
|
|
789
|
+
if v is None:
|
|
790
|
+
return v
|
|
791
|
+
limits = get_escalation_limits()
|
|
792
|
+
valid, error = limits.validate_max_escalations(v)
|
|
793
|
+
if not valid:
|
|
794
|
+
raise ValueError(error)
|
|
795
|
+
return v
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
class EscalationPolicyResponse(EscalationPolicyBase, IDMixin, TimestampMixin):
|
|
799
|
+
"""Schema for escalation policy response."""
|
|
800
|
+
|
|
801
|
+
pass
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
class EscalationPolicyListResponse(ListResponseWrapper):
|
|
805
|
+
"""Schema for escalation policy list response."""
|
|
806
|
+
|
|
807
|
+
items: list[EscalationPolicyResponse]
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
class EscalationEventBase(BaseModel):
|
|
811
|
+
"""Escalation event (state transition history)."""
|
|
812
|
+
|
|
813
|
+
from_state: str | None = Field(None, description="Previous state")
|
|
814
|
+
to_state: str = Field(..., description="New state")
|
|
815
|
+
actor: str | None = Field(None, description="Who triggered the transition")
|
|
816
|
+
message: str = Field(default="", description="Event message")
|
|
817
|
+
timestamp: datetime = Field(..., description="When the event occurred")
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
class EscalationIncidentBase(BaseSchema):
|
|
821
|
+
"""Base escalation incident schema."""
|
|
822
|
+
|
|
823
|
+
policy_id: str = Field(..., description="Escalation policy ID")
|
|
824
|
+
incident_ref: str = Field(..., description="External reference (e.g., validation ID)")
|
|
825
|
+
state: EscalationState = Field(
|
|
826
|
+
default=EscalationState.PENDING,
|
|
827
|
+
description="Current state",
|
|
828
|
+
)
|
|
829
|
+
current_level: int = Field(default=1, description="Current escalation level")
|
|
830
|
+
escalation_count: int = Field(default=0, description="Number of escalations")
|
|
831
|
+
context: dict[str, Any] = Field(default_factory=dict, description="Incident context")
|
|
832
|
+
acknowledged_by: str | None = Field(None, description="Who acknowledged")
|
|
833
|
+
acknowledged_at: datetime | None = Field(None, description="When acknowledged")
|
|
834
|
+
resolved_by: str | None = Field(None, description="Who resolved")
|
|
835
|
+
resolved_at: datetime | None = Field(None, description="When resolved")
|
|
836
|
+
next_escalation_at: datetime | None = Field(None, description="Next escalation time")
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
class EscalationIncidentResponse(EscalationIncidentBase, IDMixin, TimestampMixin):
|
|
840
|
+
"""Schema for escalation incident response."""
|
|
841
|
+
|
|
842
|
+
events: list[EscalationEventBase] = Field(
|
|
843
|
+
default_factory=list,
|
|
844
|
+
description="State transition history",
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
class EscalationIncidentListResponse(ListResponseWrapper):
|
|
849
|
+
"""Schema for escalation incident list response."""
|
|
850
|
+
|
|
851
|
+
items: list[EscalationIncidentResponse]
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
class AcknowledgeRequest(BaseModel):
|
|
855
|
+
"""Request to acknowledge an incident."""
|
|
856
|
+
|
|
857
|
+
actor: str = Field(..., description="Who is acknowledging")
|
|
858
|
+
message: str = Field(default="", description="Acknowledgement message")
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
class ResolveRequest(BaseModel):
|
|
862
|
+
"""Request to resolve an incident."""
|
|
863
|
+
|
|
864
|
+
actor: str | None = Field(None, description="Who is resolving (null for auto)")
|
|
865
|
+
message: str = Field(default="", description="Resolution message")
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
class EscalationStats(BaseModel):
|
|
869
|
+
"""Escalation statistics."""
|
|
870
|
+
|
|
871
|
+
total_incidents: int = Field(default=0, description="Total incidents")
|
|
872
|
+
by_state: dict[str, int] = Field(default_factory=dict, description="Count by state")
|
|
873
|
+
active_count: int = Field(default=0, description="Active (non-resolved) incidents")
|
|
874
|
+
total_policies: int = Field(default=0, description="Total policies")
|
|
875
|
+
avg_resolution_time_minutes: float | None = Field(
|
|
876
|
+
None,
|
|
877
|
+
description="Average resolution time in minutes",
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
# =============================================================================
|
|
882
|
+
# Enhanced Stats with Time Range and Caching
|
|
883
|
+
# =============================================================================
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
class TimeRangeFilter(BaseModel):
|
|
887
|
+
"""Time range filter for stats queries."""
|
|
888
|
+
|
|
889
|
+
start_time: datetime | None = Field(
|
|
890
|
+
None,
|
|
891
|
+
description="Start of time range (inclusive)",
|
|
892
|
+
)
|
|
893
|
+
end_time: datetime | None = Field(
|
|
894
|
+
None,
|
|
895
|
+
description="End of time range (exclusive)",
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
class CacheInfo(BaseModel):
|
|
900
|
+
"""Cache information for stats response."""
|
|
901
|
+
|
|
902
|
+
cached: bool = Field(default=False, description="Whether result was served from cache")
|
|
903
|
+
cached_at: datetime | None = Field(None, description="When result was cached")
|
|
904
|
+
ttl_seconds: int | None = Field(None, description="Cache TTL in seconds")
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
class EscalationStatsEnhanced(BaseModel):
|
|
908
|
+
"""Enhanced escalation statistics with time range and caching info."""
|
|
909
|
+
|
|
910
|
+
total_incidents: int = Field(default=0, description="Total incidents")
|
|
911
|
+
by_state: dict[str, int] = Field(default_factory=dict, description="Count by state")
|
|
912
|
+
active_count: int = Field(default=0, description="Active (non-resolved) incidents")
|
|
913
|
+
total_policies: int = Field(default=0, description="Total policies")
|
|
914
|
+
avg_resolution_time_minutes: float | None = Field(
|
|
915
|
+
None,
|
|
916
|
+
description="Average resolution time in minutes",
|
|
917
|
+
)
|
|
918
|
+
time_range: TimeRangeFilter | None = Field(
|
|
919
|
+
None,
|
|
920
|
+
description="Time range filter applied",
|
|
921
|
+
)
|
|
922
|
+
cache_info: CacheInfo | None = Field(
|
|
923
|
+
None,
|
|
924
|
+
description="Cache information",
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
class DeduplicationStatsEnhanced(BaseModel):
|
|
929
|
+
"""Enhanced deduplication statistics with aggregated config data."""
|
|
930
|
+
|
|
931
|
+
# Runtime metrics (from in-memory metrics collector)
|
|
932
|
+
total_received: int = Field(default=0, description="Total notifications received")
|
|
933
|
+
total_deduplicated: int = Field(default=0, description="Total notifications deduplicated")
|
|
934
|
+
total_passed: int = Field(default=0, description="Total notifications passed through")
|
|
935
|
+
dedup_rate: float = Field(default=0.0, description="Deduplication rate percentage")
|
|
936
|
+
active_fingerprints: int = Field(default=0, description="Active fingerprints in window")
|
|
937
|
+
# Config aggregates (from database)
|
|
938
|
+
total_configs: int = Field(default=0, description="Total deduplication configs")
|
|
939
|
+
active_configs: int = Field(default=0, description="Active configs count")
|
|
940
|
+
by_strategy: dict[str, int] = Field(
|
|
941
|
+
default_factory=dict,
|
|
942
|
+
description="Count of configs by strategy",
|
|
943
|
+
)
|
|
944
|
+
by_policy: dict[str, int] = Field(
|
|
945
|
+
default_factory=dict,
|
|
946
|
+
description="Count of configs by policy",
|
|
947
|
+
)
|
|
948
|
+
avg_window_seconds: float = Field(
|
|
949
|
+
default=0.0,
|
|
950
|
+
description="Average window duration in seconds",
|
|
951
|
+
)
|
|
952
|
+
time_range: TimeRangeFilter | None = Field(
|
|
953
|
+
None,
|
|
954
|
+
description="Time range filter applied",
|
|
955
|
+
)
|
|
956
|
+
cache_info: CacheInfo | None = Field(
|
|
957
|
+
None,
|
|
958
|
+
description="Cache information",
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
class ThrottlingStatsEnhanced(BaseModel):
|
|
963
|
+
"""Enhanced throttling statistics with aggregated config data."""
|
|
964
|
+
|
|
965
|
+
# Runtime metrics (from in-memory metrics collector)
|
|
966
|
+
total_received: int = Field(default=0, description="Total notifications received")
|
|
967
|
+
total_throttled: int = Field(default=0, description="Total notifications throttled")
|
|
968
|
+
total_passed: int = Field(default=0, description="Total notifications passed through")
|
|
969
|
+
throttle_rate: float = Field(default=0.0, description="Throttle rate percentage")
|
|
970
|
+
current_window_count: int = Field(default=0, description="Count in current window")
|
|
971
|
+
# Config aggregates (from database)
|
|
972
|
+
total_configs: int = Field(default=0, description="Total throttling configs")
|
|
973
|
+
active_configs: int = Field(default=0, description="Active configs count")
|
|
974
|
+
configs_with_per_minute: int = Field(
|
|
975
|
+
default=0,
|
|
976
|
+
description="Configs with per-minute limits",
|
|
977
|
+
)
|
|
978
|
+
configs_with_per_hour: int = Field(
|
|
979
|
+
default=0,
|
|
980
|
+
description="Configs with per-hour limits",
|
|
981
|
+
)
|
|
982
|
+
configs_with_per_day: int = Field(
|
|
983
|
+
default=0,
|
|
984
|
+
description="Configs with per-day limits",
|
|
985
|
+
)
|
|
986
|
+
avg_burst_allowance: float = Field(
|
|
987
|
+
default=0.0,
|
|
988
|
+
description="Average burst allowance",
|
|
989
|
+
)
|
|
990
|
+
time_range: TimeRangeFilter | None = Field(
|
|
991
|
+
None,
|
|
992
|
+
description="Time range filter applied",
|
|
993
|
+
)
|
|
994
|
+
cache_info: CacheInfo | None = Field(
|
|
995
|
+
None,
|
|
996
|
+
description="Cache information",
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
class StatsCacheStatus(BaseModel):
|
|
1001
|
+
"""Stats cache status information."""
|
|
1002
|
+
|
|
1003
|
+
total_entries: int = Field(default=0, description="Total cache entries")
|
|
1004
|
+
valid_entries: int = Field(default=0, description="Valid (non-expired) entries")
|
|
1005
|
+
expired_entries: int = Field(default=0, description="Expired entries")
|
|
1006
|
+
max_entries: int = Field(default=100, description="Maximum cache entries")
|
|
1007
|
+
default_ttl_seconds: int = Field(default=30, description="Default TTL in seconds")
|
|
1008
|
+
total_hits: int = Field(default=0, description="Total cache hits")
|
|
1009
|
+
total_misses: int = Field(default=0, description="Total cache misses")
|
|
1010
|
+
hit_rate: float = Field(default=0.0, description="Cache hit rate")
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
# =============================================================================
|
|
1014
|
+
# Escalation Scheduler Schemas
|
|
1015
|
+
# =============================================================================
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
class EscalationSchedulerStatus(BaseModel):
|
|
1019
|
+
"""Status of the escalation scheduler service."""
|
|
1020
|
+
|
|
1021
|
+
running: bool = Field(..., description="Whether scheduler is running")
|
|
1022
|
+
enabled: bool = Field(..., description="Whether scheduler is enabled")
|
|
1023
|
+
check_interval_seconds: int = Field(..., description="Check interval in seconds")
|
|
1024
|
+
last_check_at: str | None = Field(None, description="Last check timestamp (ISO format)")
|
|
1025
|
+
next_check_at: str | None = Field(None, description="Next check timestamp (ISO format)")
|
|
1026
|
+
check_count: int = Field(default=0, description="Total checks performed")
|
|
1027
|
+
escalation_count: int = Field(default=0, description="Total escalations processed")
|
|
1028
|
+
error_count: int = Field(default=0, description="Total errors encountered")
|
|
1029
|
+
handlers: list[str] = Field(default_factory=list, description="Registered handler types")
|
|
1030
|
+
strategy: str = Field(default="time_based", description="Active escalation strategy")
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
class EscalationSchedulerConfigRequest(BaseModel):
|
|
1034
|
+
"""Request to update scheduler configuration.
|
|
1035
|
+
|
|
1036
|
+
Validation:
|
|
1037
|
+
- check_interval_seconds: Must be between 10 and 3600 seconds (configurable).
|
|
1038
|
+
- max_escalations_per_check: Must be between 1 and 1000.
|
|
1039
|
+
|
|
1040
|
+
DoS Prevention:
|
|
1041
|
+
- Minimum interval prevents excessive CPU usage from too-frequent checks.
|
|
1042
|
+
- Maximum interval ensures timely escalation processing.
|
|
1043
|
+
|
|
1044
|
+
Environment Variables:
|
|
1045
|
+
- TRUTHOUND_ESCALATION_CHECK_INTERVAL_MIN
|
|
1046
|
+
- TRUTHOUND_ESCALATION_CHECK_INTERVAL_MAX
|
|
1047
|
+
"""
|
|
1048
|
+
|
|
1049
|
+
check_interval_seconds: int | None = Field(
|
|
1050
|
+
None,
|
|
1051
|
+
description="Check interval in seconds (10-3600, configurable)",
|
|
1052
|
+
ge=10,
|
|
1053
|
+
le=3600,
|
|
1054
|
+
)
|
|
1055
|
+
max_escalations_per_check: int | None = Field(
|
|
1056
|
+
None,
|
|
1057
|
+
description="Maximum escalations per check (1-1000)",
|
|
1058
|
+
ge=1,
|
|
1059
|
+
le=1000,
|
|
1060
|
+
)
|
|
1061
|
+
enabled: bool | None = Field(None, description="Enable/disable scheduler")
|
|
1062
|
+
|
|
1063
|
+
@field_validator("check_interval_seconds")
|
|
1064
|
+
@classmethod
|
|
1065
|
+
def validate_check_interval(cls, v: int | None) -> int | None:
|
|
1066
|
+
"""Validate check_interval_seconds against configurable limits.
|
|
1067
|
+
|
|
1068
|
+
Args:
|
|
1069
|
+
v: Check interval in seconds or None.
|
|
1070
|
+
|
|
1071
|
+
Returns:
|
|
1072
|
+
Validated interval value or None.
|
|
1073
|
+
|
|
1074
|
+
Raises:
|
|
1075
|
+
ValueError: If interval is outside allowed range.
|
|
1076
|
+
"""
|
|
1077
|
+
if v is None:
|
|
1078
|
+
return v
|
|
1079
|
+
limits = get_escalation_limits()
|
|
1080
|
+
valid, error = limits.validate_check_interval(v)
|
|
1081
|
+
if not valid:
|
|
1082
|
+
raise ValueError(error)
|
|
1083
|
+
return v
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
class EscalationSchedulerAction(BaseModel):
|
|
1087
|
+
"""Response for scheduler control actions."""
|
|
1088
|
+
|
|
1089
|
+
success: bool = Field(..., description="Whether action succeeded")
|
|
1090
|
+
message: str = Field(..., description="Status message")
|
|
1091
|
+
action: str = Field(..., description="Action performed")
|
|
1092
|
+
timestamp: str = Field(..., description="Action timestamp (ISO format)")
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
class TriggerCheckResponse(BaseModel):
|
|
1096
|
+
"""Response for triggered manual check."""
|
|
1097
|
+
|
|
1098
|
+
success: bool = Field(..., description="Whether check succeeded")
|
|
1099
|
+
message: str = Field(..., description="Status message")
|
|
1100
|
+
escalations_processed: int = Field(default=0, description="Number of escalations processed")
|
|
1101
|
+
timestamp: str = Field(..., description="Check timestamp (ISO format)")
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
# =============================================================================
|
|
1105
|
+
# Combined Stats
|
|
1106
|
+
# =============================================================================
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
class AdvancedNotificationStats(BaseModel):
|
|
1110
|
+
"""Combined statistics for all advanced notification features."""
|
|
1111
|
+
|
|
1112
|
+
routing: dict[str, int] = Field(
|
|
1113
|
+
default_factory=dict,
|
|
1114
|
+
description="Routing statistics",
|
|
1115
|
+
)
|
|
1116
|
+
deduplication: DeduplicationStats = Field(
|
|
1117
|
+
default_factory=DeduplicationStats,
|
|
1118
|
+
description="Deduplication statistics",
|
|
1119
|
+
)
|
|
1120
|
+
throttling: ThrottlingStats = Field(
|
|
1121
|
+
default_factory=ThrottlingStats,
|
|
1122
|
+
description="Throttling statistics",
|
|
1123
|
+
)
|
|
1124
|
+
escalation: EscalationStats = Field(
|
|
1125
|
+
default_factory=EscalationStats,
|
|
1126
|
+
description="Escalation statistics",
|
|
1127
|
+
)
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
# =============================================================================
|
|
1131
|
+
# Config Import/Export Schemas
|
|
1132
|
+
# =============================================================================
|
|
1133
|
+
|
|
1134
|
+
|
|
1135
|
+
class NotificationConfigBundle(BaseModel):
|
|
1136
|
+
"""Bundle of notification configurations for import/export.
|
|
1137
|
+
|
|
1138
|
+
Contains all notification configurations in a portable format
|
|
1139
|
+
that can be exported as JSON or YAML and imported later.
|
|
1140
|
+
"""
|
|
1141
|
+
|
|
1142
|
+
version: str = Field(
|
|
1143
|
+
default="1.0",
|
|
1144
|
+
description="Bundle schema version for compatibility checking",
|
|
1145
|
+
)
|
|
1146
|
+
exported_at: datetime = Field(
|
|
1147
|
+
...,
|
|
1148
|
+
description="Timestamp when the bundle was exported",
|
|
1149
|
+
)
|
|
1150
|
+
routing_rules: list[RoutingRuleResponse] = Field(
|
|
1151
|
+
default_factory=list,
|
|
1152
|
+
description="List of routing rule configurations",
|
|
1153
|
+
)
|
|
1154
|
+
deduplication_configs: list[DeduplicationConfigResponse] = Field(
|
|
1155
|
+
default_factory=list,
|
|
1156
|
+
description="List of deduplication configurations",
|
|
1157
|
+
)
|
|
1158
|
+
throttling_configs: list[ThrottlingConfigResponse] = Field(
|
|
1159
|
+
default_factory=list,
|
|
1160
|
+
description="List of throttling configurations",
|
|
1161
|
+
)
|
|
1162
|
+
escalation_policies: list[EscalationPolicyResponse] = Field(
|
|
1163
|
+
default_factory=list,
|
|
1164
|
+
description="List of escalation policy configurations",
|
|
1165
|
+
)
|
|
1166
|
+
|
|
1167
|
+
|
|
1168
|
+
class ConfigImportItem(BaseModel):
|
|
1169
|
+
"""Single item to import with conflict resolution."""
|
|
1170
|
+
|
|
1171
|
+
config_type: Literal["routing_rule", "deduplication", "throttling", "escalation"] = Field(
|
|
1172
|
+
...,
|
|
1173
|
+
description="Type of configuration being imported",
|
|
1174
|
+
)
|
|
1175
|
+
config_id: str = Field(
|
|
1176
|
+
...,
|
|
1177
|
+
description="ID of the configuration (for conflict detection)",
|
|
1178
|
+
)
|
|
1179
|
+
action: Literal["create", "skip", "overwrite"] = Field(
|
|
1180
|
+
default="create",
|
|
1181
|
+
description="Action to take for this config",
|
|
1182
|
+
)
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
class ConfigImportRequest(BaseModel):
|
|
1186
|
+
"""Request to import notification configurations.
|
|
1187
|
+
|
|
1188
|
+
Supports selective import with conflict resolution options.
|
|
1189
|
+
"""
|
|
1190
|
+
|
|
1191
|
+
bundle: NotificationConfigBundle = Field(
|
|
1192
|
+
...,
|
|
1193
|
+
description="Configuration bundle to import",
|
|
1194
|
+
)
|
|
1195
|
+
conflict_resolution: Literal["skip", "overwrite", "rename"] = Field(
|
|
1196
|
+
default="skip",
|
|
1197
|
+
description="Default conflict resolution strategy",
|
|
1198
|
+
)
|
|
1199
|
+
selected_items: list[ConfigImportItem] | None = Field(
|
|
1200
|
+
None,
|
|
1201
|
+
description="Specific items to import (null = import all)",
|
|
1202
|
+
)
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
class ConfigImportConflict(BaseModel):
|
|
1206
|
+
"""Conflict detected during import preview."""
|
|
1207
|
+
|
|
1208
|
+
config_type: Literal["routing_rule", "deduplication", "throttling", "escalation"] = Field(
|
|
1209
|
+
...,
|
|
1210
|
+
description="Type of configuration with conflict",
|
|
1211
|
+
)
|
|
1212
|
+
config_id: str = Field(
|
|
1213
|
+
...,
|
|
1214
|
+
description="ID of the conflicting configuration",
|
|
1215
|
+
)
|
|
1216
|
+
config_name: str = Field(
|
|
1217
|
+
...,
|
|
1218
|
+
description="Name of the conflicting configuration",
|
|
1219
|
+
)
|
|
1220
|
+
existing_name: str = Field(
|
|
1221
|
+
...,
|
|
1222
|
+
description="Name of the existing configuration",
|
|
1223
|
+
)
|
|
1224
|
+
suggested_action: Literal["skip", "overwrite", "rename"] = Field(
|
|
1225
|
+
default="skip",
|
|
1226
|
+
description="Suggested resolution action",
|
|
1227
|
+
)
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
class ConfigImportPreview(BaseModel):
|
|
1231
|
+
"""Preview of import operation before execution."""
|
|
1232
|
+
|
|
1233
|
+
total_configs: int = Field(
|
|
1234
|
+
default=0,
|
|
1235
|
+
description="Total configurations in bundle",
|
|
1236
|
+
)
|
|
1237
|
+
new_configs: int = Field(
|
|
1238
|
+
default=0,
|
|
1239
|
+
description="Number of new configurations to create",
|
|
1240
|
+
)
|
|
1241
|
+
conflicts: list[ConfigImportConflict] = Field(
|
|
1242
|
+
default_factory=list,
|
|
1243
|
+
description="List of detected conflicts",
|
|
1244
|
+
)
|
|
1245
|
+
routing_rules_count: int = Field(default=0, description="Routing rules to import")
|
|
1246
|
+
deduplication_configs_count: int = Field(default=0, description="Deduplication configs to import")
|
|
1247
|
+
throttling_configs_count: int = Field(default=0, description="Throttling configs to import")
|
|
1248
|
+
escalation_policies_count: int = Field(default=0, description="Escalation policies to import")
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
class ConfigImportResult(BaseModel):
|
|
1252
|
+
"""Result of configuration import operation."""
|
|
1253
|
+
|
|
1254
|
+
success: bool = Field(
|
|
1255
|
+
...,
|
|
1256
|
+
description="Whether the import was successful",
|
|
1257
|
+
)
|
|
1258
|
+
message: str = Field(
|
|
1259
|
+
...,
|
|
1260
|
+
description="Summary message",
|
|
1261
|
+
)
|
|
1262
|
+
created_count: int = Field(
|
|
1263
|
+
default=0,
|
|
1264
|
+
description="Number of configurations created",
|
|
1265
|
+
)
|
|
1266
|
+
skipped_count: int = Field(
|
|
1267
|
+
default=0,
|
|
1268
|
+
description="Number of configurations skipped",
|
|
1269
|
+
)
|
|
1270
|
+
overwritten_count: int = Field(
|
|
1271
|
+
default=0,
|
|
1272
|
+
description="Number of configurations overwritten",
|
|
1273
|
+
)
|
|
1274
|
+
errors: list[str] = Field(
|
|
1275
|
+
default_factory=list,
|
|
1276
|
+
description="List of errors encountered",
|
|
1277
|
+
)
|
|
1278
|
+
created_ids: dict[str, list[str]] = Field(
|
|
1279
|
+
default_factory=dict,
|
|
1280
|
+
description="IDs of created configurations by type",
|
|
1281
|
+
)
|
|
1282
|
+
|
|
1283
|
+
|
|
1284
|
+
class ConfigExportRequest(BaseModel):
|
|
1285
|
+
"""Request parameters for config export."""
|
|
1286
|
+
|
|
1287
|
+
format: Literal["json", "yaml"] = Field(
|
|
1288
|
+
default="json",
|
|
1289
|
+
description="Export format (json or yaml)",
|
|
1290
|
+
)
|
|
1291
|
+
include_routing_rules: bool = Field(
|
|
1292
|
+
default=True,
|
|
1293
|
+
description="Include routing rules in export",
|
|
1294
|
+
)
|
|
1295
|
+
include_deduplication: bool = Field(
|
|
1296
|
+
default=True,
|
|
1297
|
+
description="Include deduplication configs in export",
|
|
1298
|
+
)
|
|
1299
|
+
include_throttling: bool = Field(
|
|
1300
|
+
default=True,
|
|
1301
|
+
description="Include throttling configs in export",
|
|
1302
|
+
)
|
|
1303
|
+
include_escalation: bool = Field(
|
|
1304
|
+
default=True,
|
|
1305
|
+
description="Include escalation policies in export",
|
|
1306
|
+
)
|
|
1307
|
+
|
|
1308
|
+
|
|
1309
|
+
# =============================================================================
|
|
1310
|
+
# Expression Validation Schemas
|
|
1311
|
+
# =============================================================================
|
|
1312
|
+
|
|
1313
|
+
|
|
1314
|
+
class ExpressionValidateRequest(BaseModel):
|
|
1315
|
+
"""Request to validate a Python-like expression.
|
|
1316
|
+
|
|
1317
|
+
This schema is used to validate expressions before saving routing rules.
|
|
1318
|
+
"""
|
|
1319
|
+
|
|
1320
|
+
expression: str = Field(
|
|
1321
|
+
...,
|
|
1322
|
+
description="Python-like expression to validate",
|
|
1323
|
+
min_length=1,
|
|
1324
|
+
max_length=4096,
|
|
1325
|
+
)
|
|
1326
|
+
timeout_seconds: float = Field(
|
|
1327
|
+
default=1.0,
|
|
1328
|
+
description="Maximum evaluation time in seconds",
|
|
1329
|
+
ge=0.1,
|
|
1330
|
+
le=10.0,
|
|
1331
|
+
)
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
class ExpressionValidateResponse(BaseModel):
|
|
1335
|
+
"""Response from expression validation.
|
|
1336
|
+
|
|
1337
|
+
Contains validation results and optionally a preview evaluation result.
|
|
1338
|
+
"""
|
|
1339
|
+
|
|
1340
|
+
valid: bool = Field(
|
|
1341
|
+
...,
|
|
1342
|
+
description="Whether the expression is syntactically valid",
|
|
1343
|
+
)
|
|
1344
|
+
error: str | None = Field(
|
|
1345
|
+
default=None,
|
|
1346
|
+
description="Error message if validation failed",
|
|
1347
|
+
)
|
|
1348
|
+
error_line: int | None = Field(
|
|
1349
|
+
default=None,
|
|
1350
|
+
description="Line number where error occurred (1-indexed)",
|
|
1351
|
+
)
|
|
1352
|
+
preview_result: bool | None = Field(
|
|
1353
|
+
default=None,
|
|
1354
|
+
description="Preview evaluation result with sample data",
|
|
1355
|
+
)
|
|
1356
|
+
preview_error: str | None = Field(
|
|
1357
|
+
default=None,
|
|
1358
|
+
description="Error during preview evaluation",
|
|
1359
|
+
)
|
|
1360
|
+
warnings: list[str] = Field(
|
|
1361
|
+
default_factory=list,
|
|
1362
|
+
description="Non-fatal warnings about the expression",
|
|
1363
|
+
)
|