truthound-dashboard 1.4.4__py3-none-any.whl → 1.5.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 +75 -86
- truthound_dashboard/api/anomaly.py +7 -13
- truthound_dashboard/api/cross_alerts.py +38 -52
- truthound_dashboard/api/drift.py +49 -59
- truthound_dashboard/api/drift_monitor.py +234 -79
- truthound_dashboard/api/enterprise_sampling.py +498 -0
- truthound_dashboard/api/history.py +57 -5
- truthound_dashboard/api/lineage.py +3 -48
- truthound_dashboard/api/maintenance.py +104 -49
- truthound_dashboard/api/mask.py +1 -2
- truthound_dashboard/api/middleware.py +2 -1
- truthound_dashboard/api/model_monitoring.py +435 -311
- truthound_dashboard/api/notifications.py +227 -191
- truthound_dashboard/api/notifications_advanced.py +21 -20
- truthound_dashboard/api/observability.py +586 -0
- truthound_dashboard/api/plugins.py +2 -433
- truthound_dashboard/api/profile.py +199 -37
- truthound_dashboard/api/quality_reporter.py +701 -0
- truthound_dashboard/api/reports.py +7 -16
- truthound_dashboard/api/router.py +66 -0
- truthound_dashboard/api/rule_suggestions.py +5 -5
- truthound_dashboard/api/scan.py +17 -19
- truthound_dashboard/api/schedules.py +85 -50
- truthound_dashboard/api/schema_evolution.py +6 -6
- truthound_dashboard/api/schema_watcher.py +667 -0
- truthound_dashboard/api/sources.py +98 -27
- truthound_dashboard/api/tiering.py +1323 -0
- truthound_dashboard/api/triggers.py +14 -11
- truthound_dashboard/api/validations.py +12 -11
- truthound_dashboard/api/versioning.py +1 -6
- truthound_dashboard/core/__init__.py +129 -3
- truthound_dashboard/core/actions/__init__.py +62 -0
- truthound_dashboard/core/actions/custom.py +426 -0
- truthound_dashboard/core/actions/notifications.py +910 -0
- truthound_dashboard/core/actions/storage.py +472 -0
- truthound_dashboard/core/actions/webhook.py +281 -0
- truthound_dashboard/core/anomaly.py +262 -67
- truthound_dashboard/core/anomaly_explainer.py +4 -3
- truthound_dashboard/core/backends/__init__.py +67 -0
- truthound_dashboard/core/backends/base.py +299 -0
- truthound_dashboard/core/backends/errors.py +191 -0
- truthound_dashboard/core/backends/factory.py +423 -0
- truthound_dashboard/core/backends/mock_backend.py +451 -0
- truthound_dashboard/core/backends/truthound_backend.py +718 -0
- truthound_dashboard/core/checkpoint/__init__.py +87 -0
- truthound_dashboard/core/checkpoint/adapters.py +814 -0
- truthound_dashboard/core/checkpoint/checkpoint.py +491 -0
- truthound_dashboard/core/checkpoint/runner.py +270 -0
- truthound_dashboard/core/connections.py +645 -23
- truthound_dashboard/core/converters/__init__.py +14 -0
- truthound_dashboard/core/converters/truthound.py +620 -0
- truthound_dashboard/core/cross_alerts.py +540 -320
- truthound_dashboard/core/datasource_factory.py +1672 -0
- truthound_dashboard/core/drift_monitor.py +216 -20
- truthound_dashboard/core/enterprise_sampling.py +1291 -0
- truthound_dashboard/core/interfaces/__init__.py +225 -0
- truthound_dashboard/core/interfaces/actions.py +652 -0
- truthound_dashboard/core/interfaces/base.py +247 -0
- truthound_dashboard/core/interfaces/checkpoint.py +676 -0
- truthound_dashboard/core/interfaces/protocols.py +664 -0
- truthound_dashboard/core/interfaces/reporters.py +650 -0
- truthound_dashboard/core/interfaces/routing.py +646 -0
- truthound_dashboard/core/interfaces/triggers.py +619 -0
- truthound_dashboard/core/lineage.py +407 -71
- truthound_dashboard/core/model_monitoring.py +431 -3
- truthound_dashboard/core/notifications/base.py +4 -0
- truthound_dashboard/core/notifications/channels.py +501 -1203
- truthound_dashboard/core/notifications/deduplication/__init__.py +81 -115
- truthound_dashboard/core/notifications/deduplication/service.py +131 -348
- truthound_dashboard/core/notifications/dispatcher.py +202 -11
- truthound_dashboard/core/notifications/escalation/__init__.py +119 -106
- truthound_dashboard/core/notifications/escalation/engine.py +168 -358
- truthound_dashboard/core/notifications/routing/__init__.py +88 -128
- truthound_dashboard/core/notifications/routing/engine.py +90 -317
- truthound_dashboard/core/notifications/stats_aggregator.py +246 -1
- truthound_dashboard/core/notifications/throttling/__init__.py +67 -50
- truthound_dashboard/core/notifications/throttling/builder.py +117 -255
- truthound_dashboard/core/notifications/truthound_adapter.py +842 -0
- truthound_dashboard/core/phase5/collaboration.py +1 -1
- truthound_dashboard/core/plugins/lifecycle/__init__.py +0 -13
- truthound_dashboard/core/quality_reporter.py +1359 -0
- truthound_dashboard/core/report_history.py +0 -6
- truthound_dashboard/core/reporters/__init__.py +175 -14
- truthound_dashboard/core/reporters/adapters.py +943 -0
- truthound_dashboard/core/reporters/base.py +0 -3
- truthound_dashboard/core/reporters/builtin/__init__.py +18 -0
- truthound_dashboard/core/reporters/builtin/csv_reporter.py +111 -0
- truthound_dashboard/core/reporters/builtin/html_reporter.py +270 -0
- truthound_dashboard/core/reporters/builtin/json_reporter.py +127 -0
- truthound_dashboard/core/reporters/compat.py +266 -0
- truthound_dashboard/core/reporters/csv_reporter.py +2 -35
- truthound_dashboard/core/reporters/factory.py +526 -0
- truthound_dashboard/core/reporters/interfaces.py +745 -0
- truthound_dashboard/core/reporters/registry.py +1 -10
- truthound_dashboard/core/scheduler.py +165 -0
- truthound_dashboard/core/schema_evolution.py +3 -3
- truthound_dashboard/core/schema_watcher.py +1528 -0
- truthound_dashboard/core/services.py +595 -76
- truthound_dashboard/core/store_manager.py +810 -0
- truthound_dashboard/core/streaming_anomaly.py +169 -4
- truthound_dashboard/core/tiering.py +1309 -0
- truthound_dashboard/core/triggers/evaluators.py +178 -8
- truthound_dashboard/core/truthound_adapter.py +2620 -197
- truthound_dashboard/core/unified_alerts.py +23 -20
- truthound_dashboard/db/__init__.py +8 -0
- truthound_dashboard/db/database.py +8 -2
- truthound_dashboard/db/models.py +944 -25
- truthound_dashboard/db/repository.py +2 -0
- truthound_dashboard/main.py +15 -0
- truthound_dashboard/schemas/__init__.py +177 -16
- truthound_dashboard/schemas/base.py +44 -23
- truthound_dashboard/schemas/collaboration.py +19 -6
- truthound_dashboard/schemas/cross_alerts.py +19 -3
- truthound_dashboard/schemas/drift.py +61 -55
- truthound_dashboard/schemas/drift_monitor.py +67 -23
- truthound_dashboard/schemas/enterprise_sampling.py +653 -0
- truthound_dashboard/schemas/lineage.py +0 -33
- truthound_dashboard/schemas/mask.py +10 -8
- truthound_dashboard/schemas/model_monitoring.py +89 -10
- truthound_dashboard/schemas/notifications_advanced.py +13 -0
- truthound_dashboard/schemas/observability.py +453 -0
- truthound_dashboard/schemas/plugins.py +0 -280
- truthound_dashboard/schemas/profile.py +154 -247
- truthound_dashboard/schemas/quality_reporter.py +403 -0
- truthound_dashboard/schemas/reports.py +2 -2
- truthound_dashboard/schemas/rule_suggestion.py +8 -1
- truthound_dashboard/schemas/scan.py +4 -24
- truthound_dashboard/schemas/schedule.py +11 -3
- truthound_dashboard/schemas/schema_watcher.py +727 -0
- truthound_dashboard/schemas/source.py +17 -2
- truthound_dashboard/schemas/tiering.py +822 -0
- truthound_dashboard/schemas/triggers.py +16 -0
- truthound_dashboard/schemas/unified_alerts.py +7 -0
- truthound_dashboard/schemas/validation.py +0 -13
- truthound_dashboard/schemas/validators/base.py +41 -21
- truthound_dashboard/schemas/validators/business_rule_validators.py +244 -0
- truthound_dashboard/schemas/validators/localization_validators.py +273 -0
- truthound_dashboard/schemas/validators/ml_feature_validators.py +308 -0
- truthound_dashboard/schemas/validators/profiling_validators.py +275 -0
- truthound_dashboard/schemas/validators/referential_validators.py +312 -0
- truthound_dashboard/schemas/validators/registry.py +93 -8
- truthound_dashboard/schemas/validators/timeseries_validators.py +389 -0
- truthound_dashboard/schemas/versioning.py +1 -6
- truthound_dashboard/static/index.html +2 -2
- truthound_dashboard-1.5.1.dist-info/METADATA +312 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/RECORD +149 -148
- truthound_dashboard/core/plugins/hooks/__init__.py +0 -63
- truthound_dashboard/core/plugins/hooks/decorators.py +0 -367
- truthound_dashboard/core/plugins/hooks/manager.py +0 -403
- truthound_dashboard/core/plugins/hooks/protocols.py +0 -265
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +0 -584
- truthound_dashboard/core/reporters/junit_reporter.py +0 -233
- truthound_dashboard/core/reporters/markdown_reporter.py +0 -207
- truthound_dashboard/core/reporters/pdf_reporter.py +0 -209
- truthound_dashboard/static/assets/_baseUniq-BcrSP13d.js +0 -1
- truthound_dashboard/static/assets/arc-DlYjKwIL.js +0 -1
- truthound_dashboard/static/assets/architectureDiagram-VXUJARFQ-Bb2drbQM.js +0 -36
- truthound_dashboard/static/assets/blockDiagram-VD42YOAC-BlsPG1CH.js +0 -122
- truthound_dashboard/static/assets/c4Diagram-YG6GDRKO-B9JdUoaC.js +0 -10
- truthound_dashboard/static/assets/channel-Q6mHF1Hd.js +0 -1
- truthound_dashboard/static/assets/chunk-4BX2VUAB-DmyoPVuJ.js +0 -1
- truthound_dashboard/static/assets/chunk-55IACEB6-Bcz6Siv8.js +0 -1
- truthound_dashboard/static/assets/chunk-B4BG7PRW-Br3G5Rum.js +0 -165
- truthound_dashboard/static/assets/chunk-DI55MBZ5-DuM9c23u.js +0 -220
- truthound_dashboard/static/assets/chunk-FMBD7UC4-DNU-5mvT.js +0 -15
- truthound_dashboard/static/assets/chunk-QN33PNHL-Im2yNcmS.js +0 -1
- truthound_dashboard/static/assets/chunk-QZHKN3VN-kZr8XFm1.js +0 -1
- truthound_dashboard/static/assets/chunk-TZMSLE5B-Q__360q_.js +0 -1
- truthound_dashboard/static/assets/classDiagram-2ON5EDUG-vtixxUyK.js +0 -1
- truthound_dashboard/static/assets/classDiagram-v2-WZHVMYZB-vtixxUyK.js +0 -1
- truthound_dashboard/static/assets/clone-BOt2LwD0.js +0 -1
- truthound_dashboard/static/assets/cose-bilkent-S5V4N54A-CBDw6iac.js +0 -1
- truthound_dashboard/static/assets/dagre-6UL2VRFP-XdKqmmY9.js +0 -4
- truthound_dashboard/static/assets/diagram-PSM6KHXK-DAZ8nx9V.js +0 -24
- truthound_dashboard/static/assets/diagram-QEK2KX5R-BRvDTbGD.js +0 -43
- truthound_dashboard/static/assets/diagram-S2PKOQOG-bQcczUkl.js +0 -24
- truthound_dashboard/static/assets/erDiagram-Q2GNP2WA-DPje7VMN.js +0 -60
- truthound_dashboard/static/assets/flowDiagram-NV44I4VS-B7BVtFVS.js +0 -162
- truthound_dashboard/static/assets/ganttDiagram-JELNMOA3-D6WKSS7U.js +0 -267
- truthound_dashboard/static/assets/gitGraphDiagram-NY62KEGX-D3vtVd3y.js +0 -65
- truthound_dashboard/static/assets/graph-BKgNKZVp.js +0 -1
- truthound_dashboard/static/assets/index-C6JSrkHo.css +0 -1
- truthound_dashboard/static/assets/index-DkU82VsU.js +0 -1800
- truthound_dashboard/static/assets/infoDiagram-WHAUD3N6-DnNCT429.js +0 -2
- truthound_dashboard/static/assets/journeyDiagram-XKPGCS4Q-DGiMozqS.js +0 -139
- truthound_dashboard/static/assets/kanban-definition-3W4ZIXB7-BV2gUgli.js +0 -89
- truthound_dashboard/static/assets/katex-Cu_Erd72.js +0 -261
- truthound_dashboard/static/assets/layout-DI2MfQ5G.js +0 -1
- truthound_dashboard/static/assets/min-DYdgXVcT.js +0 -1
- truthound_dashboard/static/assets/mindmap-definition-VGOIOE7T-C7x4ruxz.js +0 -68
- truthound_dashboard/static/assets/pieDiagram-ADFJNKIX-CAJaAB9f.js +0 -30
- truthound_dashboard/static/assets/quadrantDiagram-AYHSOK5B-DeqwDI46.js +0 -7
- truthound_dashboard/static/assets/requirementDiagram-UZGBJVZJ-e3XDpZIM.js +0 -64
- truthound_dashboard/static/assets/sankeyDiagram-TZEHDZUN-CNnAv5Ux.js +0 -10
- truthound_dashboard/static/assets/sequenceDiagram-WL72ISMW-Dsne-Of3.js +0 -145
- truthound_dashboard/static/assets/stateDiagram-FKZM4ZOC-Ee0sQXyb.js +0 -1
- truthound_dashboard/static/assets/stateDiagram-v2-4FDKWEC3-B26KqW_W.js +0 -1
- truthound_dashboard/static/assets/timeline-definition-IT6M3QCI-DZYi2yl3.js +0 -61
- truthound_dashboard/static/assets/treemap-KMMF4GRG-CY3f8In2.js +0 -128
- truthound_dashboard/static/assets/unmerged_dictionaries-Dd7xcPWG.js +0 -1
- truthound_dashboard/static/assets/xychartDiagram-PRI3JC2R-CS7fydZZ.js +0 -7
- truthound_dashboard-1.4.4.dist-info/METADATA +0 -507
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
This module provides REST API endpoints for managing notification
|
|
4
4
|
channels, rules, and viewing delivery logs.
|
|
5
5
|
|
|
6
|
+
API Design: Direct Response Style
|
|
7
|
+
- Single resources return the resource directly
|
|
8
|
+
- List endpoints return data array with count
|
|
9
|
+
- Errors are handled via HTTPException
|
|
10
|
+
- Success is indicated by HTTP status codes (200, 201, 204)
|
|
11
|
+
|
|
6
12
|
Endpoints:
|
|
7
13
|
Channels:
|
|
8
14
|
GET /notifications/channels - List channels
|
|
@@ -79,6 +85,13 @@ class ChannelResponse(BaseModel):
|
|
|
79
85
|
updated_at: str
|
|
80
86
|
|
|
81
87
|
|
|
88
|
+
class ChannelListResponse(BaseModel):
|
|
89
|
+
"""Response schema for channel list."""
|
|
90
|
+
|
|
91
|
+
data: list[ChannelResponse]
|
|
92
|
+
count: int
|
|
93
|
+
|
|
94
|
+
|
|
82
95
|
class RuleCreate(BaseModel):
|
|
83
96
|
"""Request schema for creating a notification rule."""
|
|
84
97
|
|
|
@@ -117,6 +130,13 @@ class RuleResponse(BaseModel):
|
|
|
117
130
|
updated_at: str
|
|
118
131
|
|
|
119
132
|
|
|
133
|
+
class RuleListResponse(BaseModel):
|
|
134
|
+
"""Response schema for rule list."""
|
|
135
|
+
|
|
136
|
+
data: list[RuleResponse]
|
|
137
|
+
count: int
|
|
138
|
+
|
|
139
|
+
|
|
120
140
|
class LogResponse(BaseModel):
|
|
121
141
|
"""Response schema for a notification log."""
|
|
122
142
|
|
|
@@ -131,31 +151,64 @@ class LogResponse(BaseModel):
|
|
|
131
151
|
sent_at: str | None
|
|
132
152
|
|
|
133
153
|
|
|
154
|
+
class LogDetailResponse(BaseModel):
|
|
155
|
+
"""Detailed response schema for a notification log."""
|
|
156
|
+
|
|
157
|
+
id: str
|
|
158
|
+
channel_id: str
|
|
159
|
+
rule_id: str | None
|
|
160
|
+
event_type: str
|
|
161
|
+
event_data: dict[str, Any] | None
|
|
162
|
+
status: str
|
|
163
|
+
message: str
|
|
164
|
+
error_message: str | None
|
|
165
|
+
created_at: str
|
|
166
|
+
sent_at: str | None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class LogListResponse(BaseModel):
|
|
170
|
+
"""Response schema for log list."""
|
|
171
|
+
|
|
172
|
+
data: list[LogResponse]
|
|
173
|
+
count: int
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class TestChannelResponse(BaseModel):
|
|
177
|
+
"""Response schema for channel test."""
|
|
178
|
+
|
|
179
|
+
success: bool
|
|
180
|
+
message: str
|
|
181
|
+
error: str | None = None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class MessageResponse(BaseModel):
|
|
185
|
+
"""Simple message response."""
|
|
186
|
+
|
|
187
|
+
message: str
|
|
188
|
+
|
|
189
|
+
|
|
134
190
|
# =============================================================================
|
|
135
191
|
# Channel Endpoints
|
|
136
192
|
# =============================================================================
|
|
137
193
|
|
|
138
194
|
|
|
139
|
-
@router.get("/channels/types")
|
|
195
|
+
@router.get("/channels/types", response_model=list[dict[str, Any]])
|
|
140
196
|
async def get_channel_types(
|
|
141
197
|
session: AsyncSession = Depends(get_session),
|
|
142
|
-
) -> dict[str, Any]:
|
|
198
|
+
) -> list[dict[str, Any]]:
|
|
143
199
|
"""Get available notification channel types with their configuration schemas."""
|
|
144
200
|
service = NotificationChannelService(session)
|
|
145
|
-
return
|
|
146
|
-
"success": True,
|
|
147
|
-
"data": service.get_available_types(),
|
|
148
|
-
}
|
|
201
|
+
return service.get_available_types()
|
|
149
202
|
|
|
150
203
|
|
|
151
|
-
@router.get("/channels")
|
|
204
|
+
@router.get("/channels", response_model=ChannelListResponse)
|
|
152
205
|
async def list_channels(
|
|
153
206
|
offset: int = Query(default=0, ge=0),
|
|
154
207
|
limit: int = Query(default=50, ge=1, le=100),
|
|
155
208
|
active_only: bool = Query(default=False),
|
|
156
209
|
channel_type: str | None = Query(default=None),
|
|
157
210
|
session: AsyncSession = Depends(get_session),
|
|
158
|
-
) ->
|
|
211
|
+
) -> ChannelListResponse:
|
|
159
212
|
"""List notification channels."""
|
|
160
213
|
service = NotificationChannelService(session)
|
|
161
214
|
channels = await service.list(
|
|
@@ -165,29 +218,27 @@ async def list_channels(
|
|
|
165
218
|
channel_type=channel_type,
|
|
166
219
|
)
|
|
167
220
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
@router.post("/channels")
|
|
221
|
+
data = [
|
|
222
|
+
ChannelResponse(
|
|
223
|
+
id=c.id,
|
|
224
|
+
name=c.name,
|
|
225
|
+
type=c.type,
|
|
226
|
+
is_active=c.is_active,
|
|
227
|
+
config_summary=c.get_config_summary(),
|
|
228
|
+
created_at=c.created_at.isoformat(),
|
|
229
|
+
updated_at=c.updated_at.isoformat(),
|
|
230
|
+
)
|
|
231
|
+
for c in channels
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
return ChannelListResponse(data=data, count=len(data))
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@router.post("/channels", response_model=ChannelResponse, status_code=201)
|
|
187
238
|
async def create_channel(
|
|
188
239
|
request: ChannelCreate,
|
|
189
240
|
session: AsyncSession = Depends(get_session),
|
|
190
|
-
) ->
|
|
241
|
+
) -> ChannelResponse:
|
|
191
242
|
"""Create a new notification channel."""
|
|
192
243
|
service = NotificationChannelService(session)
|
|
193
244
|
|
|
@@ -200,24 +251,24 @@ async def create_channel(
|
|
|
200
251
|
)
|
|
201
252
|
await session.commit()
|
|
202
253
|
|
|
203
|
-
return
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
254
|
+
return ChannelResponse(
|
|
255
|
+
id=channel.id,
|
|
256
|
+
name=channel.name,
|
|
257
|
+
type=channel.type,
|
|
258
|
+
is_active=channel.is_active,
|
|
259
|
+
config_summary=channel.get_config_summary(),
|
|
260
|
+
created_at=channel.created_at.isoformat(),
|
|
261
|
+
updated_at=channel.updated_at.isoformat(),
|
|
262
|
+
)
|
|
212
263
|
except ValueError as e:
|
|
213
264
|
raise HTTPException(status_code=400, detail=str(e))
|
|
214
265
|
|
|
215
266
|
|
|
216
|
-
@router.get("/channels/{channel_id}")
|
|
267
|
+
@router.get("/channels/{channel_id}", response_model=ChannelResponse)
|
|
217
268
|
async def get_channel(
|
|
218
269
|
channel_id: str,
|
|
219
270
|
session: AsyncSession = Depends(get_session),
|
|
220
|
-
) ->
|
|
271
|
+
) -> ChannelResponse:
|
|
221
272
|
"""Get a notification channel by ID."""
|
|
222
273
|
service = NotificationChannelService(session)
|
|
223
274
|
channel = await service.get_by_id(channel_id)
|
|
@@ -225,26 +276,23 @@ async def get_channel(
|
|
|
225
276
|
if channel is None:
|
|
226
277
|
raise HTTPException(status_code=404, detail="Channel not found")
|
|
227
278
|
|
|
228
|
-
return
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
@router.put("/channels/{channel_id}")
|
|
279
|
+
return ChannelResponse(
|
|
280
|
+
id=channel.id,
|
|
281
|
+
name=channel.name,
|
|
282
|
+
type=channel.type,
|
|
283
|
+
is_active=channel.is_active,
|
|
284
|
+
config_summary=channel.get_config_summary(),
|
|
285
|
+
created_at=channel.created_at.isoformat(),
|
|
286
|
+
updated_at=channel.updated_at.isoformat(),
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@router.put("/channels/{channel_id}", response_model=ChannelResponse)
|
|
243
291
|
async def update_channel(
|
|
244
292
|
channel_id: str,
|
|
245
293
|
request: ChannelUpdate,
|
|
246
294
|
session: AsyncSession = Depends(get_session),
|
|
247
|
-
) ->
|
|
295
|
+
) -> ChannelResponse:
|
|
248
296
|
"""Update a notification channel."""
|
|
249
297
|
service = NotificationChannelService(session)
|
|
250
298
|
|
|
@@ -260,24 +308,24 @@ async def update_channel(
|
|
|
260
308
|
if channel is None:
|
|
261
309
|
raise HTTPException(status_code=404, detail="Channel not found")
|
|
262
310
|
|
|
263
|
-
return
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
311
|
+
return ChannelResponse(
|
|
312
|
+
id=channel.id,
|
|
313
|
+
name=channel.name,
|
|
314
|
+
type=channel.type,
|
|
315
|
+
is_active=channel.is_active,
|
|
316
|
+
config_summary=channel.get_config_summary(),
|
|
317
|
+
created_at=channel.created_at.isoformat(),
|
|
318
|
+
updated_at=channel.updated_at.isoformat(),
|
|
319
|
+
)
|
|
272
320
|
except ValueError as e:
|
|
273
321
|
raise HTTPException(status_code=400, detail=str(e))
|
|
274
322
|
|
|
275
323
|
|
|
276
|
-
@router.delete("/channels/{channel_id}")
|
|
324
|
+
@router.delete("/channels/{channel_id}", response_model=MessageResponse)
|
|
277
325
|
async def delete_channel(
|
|
278
326
|
channel_id: str,
|
|
279
327
|
session: AsyncSession = Depends(get_session),
|
|
280
|
-
) ->
|
|
328
|
+
) -> MessageResponse:
|
|
281
329
|
"""Delete a notification channel."""
|
|
282
330
|
service = NotificationChannelService(session)
|
|
283
331
|
deleted = await service.delete(channel_id)
|
|
@@ -286,24 +334,24 @@ async def delete_channel(
|
|
|
286
334
|
if not deleted:
|
|
287
335
|
raise HTTPException(status_code=404, detail="Channel not found")
|
|
288
336
|
|
|
289
|
-
return
|
|
337
|
+
return MessageResponse(message="Channel deleted")
|
|
290
338
|
|
|
291
339
|
|
|
292
|
-
@router.post("/channels/{channel_id}/test")
|
|
340
|
+
@router.post("/channels/{channel_id}/test", response_model=TestChannelResponse)
|
|
293
341
|
async def test_channel(
|
|
294
342
|
channel_id: str,
|
|
295
343
|
session: AsyncSession = Depends(get_session),
|
|
296
|
-
) ->
|
|
344
|
+
) -> TestChannelResponse:
|
|
297
345
|
"""Send a test notification to a channel."""
|
|
298
346
|
dispatcher = create_dispatcher(session)
|
|
299
347
|
result = await dispatcher.test_channel(channel_id)
|
|
300
348
|
await session.commit()
|
|
301
349
|
|
|
302
|
-
return
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
350
|
+
return TestChannelResponse(
|
|
351
|
+
success=result.success,
|
|
352
|
+
message="Test notification sent" if result.success else "Test failed",
|
|
353
|
+
error=result.error,
|
|
354
|
+
)
|
|
307
355
|
|
|
308
356
|
|
|
309
357
|
# =============================================================================
|
|
@@ -311,26 +359,23 @@ async def test_channel(
|
|
|
311
359
|
# =============================================================================
|
|
312
360
|
|
|
313
361
|
|
|
314
|
-
@router.get("/rules/conditions")
|
|
362
|
+
@router.get("/rules/conditions", response_model=list[dict[str, Any]])
|
|
315
363
|
async def get_rule_conditions(
|
|
316
364
|
session: AsyncSession = Depends(get_session),
|
|
317
|
-
) -> dict[str, Any]:
|
|
365
|
+
) -> list[dict[str, Any]]:
|
|
318
366
|
"""Get valid notification rule conditions."""
|
|
319
367
|
service = NotificationRuleService(session)
|
|
320
|
-
return
|
|
321
|
-
"success": True,
|
|
322
|
-
"data": service.get_valid_conditions(),
|
|
323
|
-
}
|
|
368
|
+
return service.get_valid_conditions()
|
|
324
369
|
|
|
325
370
|
|
|
326
|
-
@router.get("/rules")
|
|
371
|
+
@router.get("/rules", response_model=RuleListResponse)
|
|
327
372
|
async def list_rules(
|
|
328
373
|
offset: int = Query(default=0, ge=0),
|
|
329
374
|
limit: int = Query(default=50, ge=1, le=100),
|
|
330
375
|
active_only: bool = Query(default=False),
|
|
331
376
|
condition: str | None = Query(default=None),
|
|
332
377
|
session: AsyncSession = Depends(get_session),
|
|
333
|
-
) ->
|
|
378
|
+
) -> RuleListResponse:
|
|
334
379
|
"""List notification rules."""
|
|
335
380
|
service = NotificationRuleService(session)
|
|
336
381
|
rules = await service.list(
|
|
@@ -340,31 +385,29 @@ async def list_rules(
|
|
|
340
385
|
condition=condition,
|
|
341
386
|
)
|
|
342
387
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
@router.post("/rules")
|
|
388
|
+
data = [
|
|
389
|
+
RuleResponse(
|
|
390
|
+
id=r.id,
|
|
391
|
+
name=r.name,
|
|
392
|
+
condition=r.condition,
|
|
393
|
+
condition_config=r.condition_config,
|
|
394
|
+
channel_ids=r.channel_ids,
|
|
395
|
+
source_ids=r.source_ids,
|
|
396
|
+
is_active=r.is_active,
|
|
397
|
+
created_at=r.created_at.isoformat(),
|
|
398
|
+
updated_at=r.updated_at.isoformat(),
|
|
399
|
+
)
|
|
400
|
+
for r in rules
|
|
401
|
+
]
|
|
402
|
+
|
|
403
|
+
return RuleListResponse(data=data, count=len(data))
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@router.post("/rules", response_model=RuleResponse, status_code=201)
|
|
364
407
|
async def create_rule(
|
|
365
408
|
request: RuleCreate,
|
|
366
409
|
session: AsyncSession = Depends(get_session),
|
|
367
|
-
) ->
|
|
410
|
+
) -> RuleResponse:
|
|
368
411
|
"""Create a new notification rule."""
|
|
369
412
|
service = NotificationRuleService(session)
|
|
370
413
|
|
|
@@ -379,23 +422,26 @@ async def create_rule(
|
|
|
379
422
|
)
|
|
380
423
|
await session.commit()
|
|
381
424
|
|
|
382
|
-
return
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
425
|
+
return RuleResponse(
|
|
426
|
+
id=rule.id,
|
|
427
|
+
name=rule.name,
|
|
428
|
+
condition=rule.condition,
|
|
429
|
+
condition_config=rule.condition_config,
|
|
430
|
+
channel_ids=rule.channel_ids,
|
|
431
|
+
source_ids=rule.source_ids,
|
|
432
|
+
is_active=rule.is_active,
|
|
433
|
+
created_at=rule.created_at.isoformat(),
|
|
434
|
+
updated_at=rule.updated_at.isoformat(),
|
|
435
|
+
)
|
|
390
436
|
except ValueError as e:
|
|
391
437
|
raise HTTPException(status_code=400, detail=str(e))
|
|
392
438
|
|
|
393
439
|
|
|
394
|
-
@router.get("/rules/{rule_id}")
|
|
440
|
+
@router.get("/rules/{rule_id}", response_model=RuleResponse)
|
|
395
441
|
async def get_rule(
|
|
396
442
|
rule_id: str,
|
|
397
443
|
session: AsyncSession = Depends(get_session),
|
|
398
|
-
) ->
|
|
444
|
+
) -> RuleResponse:
|
|
399
445
|
"""Get a notification rule by ID."""
|
|
400
446
|
service = NotificationRuleService(session)
|
|
401
447
|
rule = await service.get_by_id(rule_id)
|
|
@@ -403,28 +449,25 @@ async def get_rule(
|
|
|
403
449
|
if rule is None:
|
|
404
450
|
raise HTTPException(status_code=404, detail="Rule not found")
|
|
405
451
|
|
|
406
|
-
return
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
@router.put("/rules/{rule_id}")
|
|
452
|
+
return RuleResponse(
|
|
453
|
+
id=rule.id,
|
|
454
|
+
name=rule.name,
|
|
455
|
+
condition=rule.condition,
|
|
456
|
+
condition_config=rule.condition_config,
|
|
457
|
+
channel_ids=rule.channel_ids,
|
|
458
|
+
source_ids=rule.source_ids,
|
|
459
|
+
is_active=rule.is_active,
|
|
460
|
+
created_at=rule.created_at.isoformat(),
|
|
461
|
+
updated_at=rule.updated_at.isoformat(),
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
@router.put("/rules/{rule_id}", response_model=RuleResponse)
|
|
423
466
|
async def update_rule(
|
|
424
467
|
rule_id: str,
|
|
425
468
|
request: RuleUpdate,
|
|
426
469
|
session: AsyncSession = Depends(get_session),
|
|
427
|
-
) ->
|
|
470
|
+
) -> RuleResponse:
|
|
428
471
|
"""Update a notification rule."""
|
|
429
472
|
service = NotificationRuleService(session)
|
|
430
473
|
|
|
@@ -443,23 +486,26 @@ async def update_rule(
|
|
|
443
486
|
if rule is None:
|
|
444
487
|
raise HTTPException(status_code=404, detail="Rule not found")
|
|
445
488
|
|
|
446
|
-
return
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
489
|
+
return RuleResponse(
|
|
490
|
+
id=rule.id,
|
|
491
|
+
name=rule.name,
|
|
492
|
+
condition=rule.condition,
|
|
493
|
+
condition_config=rule.condition_config,
|
|
494
|
+
channel_ids=rule.channel_ids,
|
|
495
|
+
source_ids=rule.source_ids,
|
|
496
|
+
is_active=rule.is_active,
|
|
497
|
+
created_at=rule.created_at.isoformat(),
|
|
498
|
+
updated_at=rule.updated_at.isoformat(),
|
|
499
|
+
)
|
|
454
500
|
except ValueError as e:
|
|
455
501
|
raise HTTPException(status_code=400, detail=str(e))
|
|
456
502
|
|
|
457
503
|
|
|
458
|
-
@router.delete("/rules/{rule_id}")
|
|
504
|
+
@router.delete("/rules/{rule_id}", response_model=MessageResponse)
|
|
459
505
|
async def delete_rule(
|
|
460
506
|
rule_id: str,
|
|
461
507
|
session: AsyncSession = Depends(get_session),
|
|
462
|
-
) ->
|
|
508
|
+
) -> MessageResponse:
|
|
463
509
|
"""Delete a notification rule."""
|
|
464
510
|
service = NotificationRuleService(session)
|
|
465
511
|
deleted = await service.delete(rule_id)
|
|
@@ -468,7 +514,7 @@ async def delete_rule(
|
|
|
468
514
|
if not deleted:
|
|
469
515
|
raise HTTPException(status_code=404, detail="Rule not found")
|
|
470
516
|
|
|
471
|
-
return
|
|
517
|
+
return MessageResponse(message="Rule deleted")
|
|
472
518
|
|
|
473
519
|
|
|
474
520
|
# =============================================================================
|
|
@@ -476,7 +522,7 @@ async def delete_rule(
|
|
|
476
522
|
# =============================================================================
|
|
477
523
|
|
|
478
524
|
|
|
479
|
-
@router.get("/logs")
|
|
525
|
+
@router.get("/logs", response_model=LogListResponse)
|
|
480
526
|
async def list_logs(
|
|
481
527
|
offset: int = Query(default=0, ge=0),
|
|
482
528
|
limit: int = Query(default=50, ge=1, le=100),
|
|
@@ -484,7 +530,7 @@ async def list_logs(
|
|
|
484
530
|
status: str | None = Query(default=None),
|
|
485
531
|
hours: int | None = Query(default=None, ge=1, le=168),
|
|
486
532
|
session: AsyncSession = Depends(get_session),
|
|
487
|
-
) ->
|
|
533
|
+
) -> LogListResponse:
|
|
488
534
|
"""List notification delivery logs."""
|
|
489
535
|
service = NotificationLogService(session)
|
|
490
536
|
logs = await service.list(
|
|
@@ -495,48 +541,41 @@ async def list_logs(
|
|
|
495
541
|
hours=hours,
|
|
496
542
|
)
|
|
497
543
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
"
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
@router.get("/logs/stats")
|
|
544
|
+
data = [
|
|
545
|
+
LogResponse(
|
|
546
|
+
id=log.id,
|
|
547
|
+
channel_id=log.channel_id,
|
|
548
|
+
rule_id=log.rule_id,
|
|
549
|
+
event_type=log.event_type,
|
|
550
|
+
status=log.status,
|
|
551
|
+
message_preview=(
|
|
552
|
+
log.message[:100] + "..." if len(log.message) > 100 else log.message
|
|
553
|
+
),
|
|
554
|
+
error_message=log.error_message,
|
|
555
|
+
created_at=log.created_at.isoformat(),
|
|
556
|
+
sent_at=log.sent_at.isoformat() if log.sent_at else None,
|
|
557
|
+
)
|
|
558
|
+
for log in logs
|
|
559
|
+
]
|
|
560
|
+
|
|
561
|
+
return LogListResponse(data=data, count=len(data))
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
@router.get("/logs/stats", response_model=dict[str, Any])
|
|
521
565
|
async def get_log_stats(
|
|
522
566
|
hours: int = Query(default=24, ge=1, le=168),
|
|
523
567
|
session: AsyncSession = Depends(get_session),
|
|
524
568
|
) -> dict[str, Any]:
|
|
525
569
|
"""Get notification delivery statistics."""
|
|
526
570
|
service = NotificationLogService(session)
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
return {
|
|
530
|
-
"success": True,
|
|
531
|
-
"data": stats,
|
|
532
|
-
}
|
|
571
|
+
return await service.get_stats(hours=hours)
|
|
533
572
|
|
|
534
573
|
|
|
535
|
-
@router.get("/logs/{log_id}")
|
|
574
|
+
@router.get("/logs/{log_id}", response_model=LogDetailResponse)
|
|
536
575
|
async def get_log(
|
|
537
576
|
log_id: str,
|
|
538
577
|
session: AsyncSession = Depends(get_session),
|
|
539
|
-
) ->
|
|
578
|
+
) -> LogDetailResponse:
|
|
540
579
|
"""Get a notification log by ID."""
|
|
541
580
|
service = NotificationLogService(session)
|
|
542
581
|
log = await service.get_by_id(log_id)
|
|
@@ -544,18 +583,15 @@ async def get_log(
|
|
|
544
583
|
if log is None:
|
|
545
584
|
raise HTTPException(status_code=404, detail="Log not found")
|
|
546
585
|
|
|
547
|
-
return
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
"sent_at": log.sent_at.isoformat() if log.sent_at else None,
|
|
560
|
-
},
|
|
561
|
-
}
|
|
586
|
+
return LogDetailResponse(
|
|
587
|
+
id=log.id,
|
|
588
|
+
channel_id=log.channel_id,
|
|
589
|
+
rule_id=log.rule_id,
|
|
590
|
+
event_type=log.event_type,
|
|
591
|
+
event_data=log.event_data,
|
|
592
|
+
status=log.status,
|
|
593
|
+
message=log.message,
|
|
594
|
+
error_message=log.error_message,
|
|
595
|
+
created_at=log.created_at.isoformat(),
|
|
596
|
+
sent_at=log.sent_at.isoformat() if log.sent_at else None,
|
|
597
|
+
)
|