truthound-dashboard 1.0.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/__init__.py +11 -0
- truthound_dashboard/__main__.py +6 -0
- truthound_dashboard/api/__init__.py +15 -0
- truthound_dashboard/api/deps.py +153 -0
- truthound_dashboard/api/drift.py +179 -0
- truthound_dashboard/api/error_handlers.py +287 -0
- truthound_dashboard/api/health.py +78 -0
- truthound_dashboard/api/history.py +62 -0
- truthound_dashboard/api/middleware.py +626 -0
- truthound_dashboard/api/notifications.py +561 -0
- truthound_dashboard/api/profile.py +52 -0
- truthound_dashboard/api/router.py +83 -0
- truthound_dashboard/api/rules.py +277 -0
- truthound_dashboard/api/schedules.py +329 -0
- truthound_dashboard/api/schemas.py +136 -0
- truthound_dashboard/api/sources.py +229 -0
- truthound_dashboard/api/validations.py +125 -0
- truthound_dashboard/cli.py +226 -0
- truthound_dashboard/config.py +132 -0
- truthound_dashboard/core/__init__.py +264 -0
- truthound_dashboard/core/base.py +185 -0
- truthound_dashboard/core/cache.py +479 -0
- truthound_dashboard/core/connections.py +331 -0
- truthound_dashboard/core/encryption.py +409 -0
- truthound_dashboard/core/exceptions.py +627 -0
- truthound_dashboard/core/logging.py +488 -0
- truthound_dashboard/core/maintenance.py +542 -0
- truthound_dashboard/core/notifications/__init__.py +56 -0
- truthound_dashboard/core/notifications/base.py +390 -0
- truthound_dashboard/core/notifications/channels.py +557 -0
- truthound_dashboard/core/notifications/dispatcher.py +453 -0
- truthound_dashboard/core/notifications/events.py +155 -0
- truthound_dashboard/core/notifications/service.py +744 -0
- truthound_dashboard/core/sampling.py +626 -0
- truthound_dashboard/core/scheduler.py +311 -0
- truthound_dashboard/core/services.py +1531 -0
- truthound_dashboard/core/truthound_adapter.py +659 -0
- truthound_dashboard/db/__init__.py +67 -0
- truthound_dashboard/db/base.py +108 -0
- truthound_dashboard/db/database.py +196 -0
- truthound_dashboard/db/models.py +732 -0
- truthound_dashboard/db/repository.py +237 -0
- truthound_dashboard/main.py +309 -0
- truthound_dashboard/schemas/__init__.py +150 -0
- truthound_dashboard/schemas/base.py +96 -0
- truthound_dashboard/schemas/drift.py +118 -0
- truthound_dashboard/schemas/history.py +74 -0
- truthound_dashboard/schemas/profile.py +91 -0
- truthound_dashboard/schemas/rule.py +199 -0
- truthound_dashboard/schemas/schedule.py +88 -0
- truthound_dashboard/schemas/schema.py +121 -0
- truthound_dashboard/schemas/source.py +138 -0
- truthound_dashboard/schemas/validation.py +192 -0
- truthound_dashboard/static/assets/index-BqJMyAHX.js +110 -0
- truthound_dashboard/static/assets/index-DMDxHCTs.js +465 -0
- truthound_dashboard/static/assets/index-Dm2D11TK.css +1 -0
- truthound_dashboard/static/index.html +15 -0
- truthound_dashboard/static/mockServiceWorker.js +349 -0
- truthound_dashboard-1.0.0.dist-info/METADATA +218 -0
- truthound_dashboard-1.0.0.dist-info/RECORD +62 -0
- truthound_dashboard-1.0.0.dist-info/WHEEL +4 -0
- truthound_dashboard-1.0.0.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
"""Notification management API endpoints.
|
|
2
|
+
|
|
3
|
+
This module provides REST API endpoints for managing notification
|
|
4
|
+
channels, rules, and viewing delivery logs.
|
|
5
|
+
|
|
6
|
+
Endpoints:
|
|
7
|
+
Channels:
|
|
8
|
+
GET /notifications/channels - List channels
|
|
9
|
+
POST /notifications/channels - Create channel
|
|
10
|
+
GET /notifications/channels/{id} - Get channel
|
|
11
|
+
PUT /notifications/channels/{id} - Update channel
|
|
12
|
+
DELETE /notifications/channels/{id} - Delete channel
|
|
13
|
+
POST /notifications/channels/{id}/test - Test channel
|
|
14
|
+
GET /notifications/channels/types - Get available types
|
|
15
|
+
|
|
16
|
+
Rules:
|
|
17
|
+
GET /notifications/rules - List rules
|
|
18
|
+
POST /notifications/rules - Create rule
|
|
19
|
+
GET /notifications/rules/{id} - Get rule
|
|
20
|
+
PUT /notifications/rules/{id} - Update rule
|
|
21
|
+
DELETE /notifications/rules/{id} - Delete rule
|
|
22
|
+
GET /notifications/rules/conditions - Get valid conditions
|
|
23
|
+
|
|
24
|
+
Logs:
|
|
25
|
+
GET /notifications/logs - List logs
|
|
26
|
+
GET /notifications/logs/stats - Get statistics
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
34
|
+
from pydantic import BaseModel, Field
|
|
35
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
36
|
+
|
|
37
|
+
from ..api.deps import get_session
|
|
38
|
+
from ..core.notifications.dispatcher import create_dispatcher
|
|
39
|
+
from ..core.notifications.service import (
|
|
40
|
+
NotificationChannelService,
|
|
41
|
+
NotificationLogService,
|
|
42
|
+
NotificationRuleService,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
router = APIRouter(prefix="/notifications")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# =============================================================================
|
|
49
|
+
# Request/Response Schemas
|
|
50
|
+
# =============================================================================
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ChannelCreate(BaseModel):
|
|
54
|
+
"""Request schema for creating a notification channel."""
|
|
55
|
+
|
|
56
|
+
name: str = Field(..., min_length=1, max_length=255)
|
|
57
|
+
type: str = Field(..., description="Channel type (slack, email, webhook)")
|
|
58
|
+
config: dict[str, Any] = Field(..., description="Channel-specific configuration")
|
|
59
|
+
is_active: bool = Field(default=True)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ChannelUpdate(BaseModel):
|
|
63
|
+
"""Request schema for updating a notification channel."""
|
|
64
|
+
|
|
65
|
+
name: str | None = Field(default=None, min_length=1, max_length=255)
|
|
66
|
+
config: dict[str, Any] | None = Field(default=None)
|
|
67
|
+
is_active: bool | None = Field(default=None)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ChannelResponse(BaseModel):
|
|
71
|
+
"""Response schema for a notification channel."""
|
|
72
|
+
|
|
73
|
+
id: str
|
|
74
|
+
name: str
|
|
75
|
+
type: str
|
|
76
|
+
is_active: bool
|
|
77
|
+
config_summary: str
|
|
78
|
+
created_at: str
|
|
79
|
+
updated_at: str
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class RuleCreate(BaseModel):
|
|
83
|
+
"""Request schema for creating a notification rule."""
|
|
84
|
+
|
|
85
|
+
name: str = Field(..., min_length=1, max_length=255)
|
|
86
|
+
condition: str = Field(..., description="Trigger condition type")
|
|
87
|
+
channel_ids: list[str] = Field(..., min_length=1)
|
|
88
|
+
condition_config: dict[str, Any] | None = Field(default=None)
|
|
89
|
+
source_ids: list[str] | None = Field(
|
|
90
|
+
default=None, description="Source IDs to filter (null = all sources)"
|
|
91
|
+
)
|
|
92
|
+
is_active: bool = Field(default=True)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class RuleUpdate(BaseModel):
|
|
96
|
+
"""Request schema for updating a notification rule."""
|
|
97
|
+
|
|
98
|
+
name: str | None = Field(default=None, min_length=1, max_length=255)
|
|
99
|
+
condition: str | None = Field(default=None)
|
|
100
|
+
channel_ids: list[str] | None = Field(default=None)
|
|
101
|
+
condition_config: dict[str, Any] | None = Field(default=None)
|
|
102
|
+
source_ids: list[str] | None = Field(default=None)
|
|
103
|
+
is_active: bool | None = Field(default=None)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class RuleResponse(BaseModel):
|
|
107
|
+
"""Response schema for a notification rule."""
|
|
108
|
+
|
|
109
|
+
id: str
|
|
110
|
+
name: str
|
|
111
|
+
condition: str
|
|
112
|
+
condition_config: dict[str, Any] | None
|
|
113
|
+
channel_ids: list[str]
|
|
114
|
+
source_ids: list[str] | None
|
|
115
|
+
is_active: bool
|
|
116
|
+
created_at: str
|
|
117
|
+
updated_at: str
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class LogResponse(BaseModel):
|
|
121
|
+
"""Response schema for a notification log."""
|
|
122
|
+
|
|
123
|
+
id: str
|
|
124
|
+
channel_id: str
|
|
125
|
+
rule_id: str | None
|
|
126
|
+
event_type: str
|
|
127
|
+
status: str
|
|
128
|
+
message_preview: str
|
|
129
|
+
error_message: str | None
|
|
130
|
+
created_at: str
|
|
131
|
+
sent_at: str | None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# =============================================================================
|
|
135
|
+
# Channel Endpoints
|
|
136
|
+
# =============================================================================
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@router.get("/channels/types")
|
|
140
|
+
async def get_channel_types(
|
|
141
|
+
session: AsyncSession = Depends(get_session),
|
|
142
|
+
) -> dict[str, Any]:
|
|
143
|
+
"""Get available notification channel types with their configuration schemas."""
|
|
144
|
+
service = NotificationChannelService(session)
|
|
145
|
+
return {
|
|
146
|
+
"success": True,
|
|
147
|
+
"data": service.get_available_types(),
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@router.get("/channels")
|
|
152
|
+
async def list_channels(
|
|
153
|
+
offset: int = Query(default=0, ge=0),
|
|
154
|
+
limit: int = Query(default=50, ge=1, le=100),
|
|
155
|
+
active_only: bool = Query(default=False),
|
|
156
|
+
channel_type: str | None = Query(default=None),
|
|
157
|
+
session: AsyncSession = Depends(get_session),
|
|
158
|
+
) -> dict[str, Any]:
|
|
159
|
+
"""List notification channels."""
|
|
160
|
+
service = NotificationChannelService(session)
|
|
161
|
+
channels = await service.list(
|
|
162
|
+
offset=offset,
|
|
163
|
+
limit=limit,
|
|
164
|
+
active_only=active_only,
|
|
165
|
+
channel_type=channel_type,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
"success": True,
|
|
170
|
+
"data": [
|
|
171
|
+
{
|
|
172
|
+
"id": c.id,
|
|
173
|
+
"name": c.name,
|
|
174
|
+
"type": c.type,
|
|
175
|
+
"is_active": c.is_active,
|
|
176
|
+
"config_summary": c.get_config_summary(),
|
|
177
|
+
"created_at": c.created_at.isoformat(),
|
|
178
|
+
"updated_at": c.updated_at.isoformat(),
|
|
179
|
+
}
|
|
180
|
+
for c in channels
|
|
181
|
+
],
|
|
182
|
+
"count": len(channels),
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@router.post("/channels")
|
|
187
|
+
async def create_channel(
|
|
188
|
+
request: ChannelCreate,
|
|
189
|
+
session: AsyncSession = Depends(get_session),
|
|
190
|
+
) -> dict[str, Any]:
|
|
191
|
+
"""Create a new notification channel."""
|
|
192
|
+
service = NotificationChannelService(session)
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
channel = await service.create(
|
|
196
|
+
name=request.name,
|
|
197
|
+
channel_type=request.type,
|
|
198
|
+
config=request.config,
|
|
199
|
+
is_active=request.is_active,
|
|
200
|
+
)
|
|
201
|
+
await session.commit()
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
"success": True,
|
|
205
|
+
"data": {
|
|
206
|
+
"id": channel.id,
|
|
207
|
+
"name": channel.name,
|
|
208
|
+
"type": channel.type,
|
|
209
|
+
"is_active": channel.is_active,
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
except ValueError as e:
|
|
213
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@router.get("/channels/{channel_id}")
|
|
217
|
+
async def get_channel(
|
|
218
|
+
channel_id: str,
|
|
219
|
+
session: AsyncSession = Depends(get_session),
|
|
220
|
+
) -> dict[str, Any]:
|
|
221
|
+
"""Get a notification channel by ID."""
|
|
222
|
+
service = NotificationChannelService(session)
|
|
223
|
+
channel = await service.get_by_id(channel_id)
|
|
224
|
+
|
|
225
|
+
if channel is None:
|
|
226
|
+
raise HTTPException(status_code=404, detail="Channel not found")
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
"success": True,
|
|
230
|
+
"data": {
|
|
231
|
+
"id": channel.id,
|
|
232
|
+
"name": channel.name,
|
|
233
|
+
"type": channel.type,
|
|
234
|
+
"is_active": channel.is_active,
|
|
235
|
+
"config_summary": channel.get_config_summary(),
|
|
236
|
+
"created_at": channel.created_at.isoformat(),
|
|
237
|
+
"updated_at": channel.updated_at.isoformat(),
|
|
238
|
+
},
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@router.put("/channels/{channel_id}")
|
|
243
|
+
async def update_channel(
|
|
244
|
+
channel_id: str,
|
|
245
|
+
request: ChannelUpdate,
|
|
246
|
+
session: AsyncSession = Depends(get_session),
|
|
247
|
+
) -> dict[str, Any]:
|
|
248
|
+
"""Update a notification channel."""
|
|
249
|
+
service = NotificationChannelService(session)
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
channel = await service.update(
|
|
253
|
+
channel_id,
|
|
254
|
+
name=request.name,
|
|
255
|
+
config=request.config,
|
|
256
|
+
is_active=request.is_active,
|
|
257
|
+
)
|
|
258
|
+
await session.commit()
|
|
259
|
+
|
|
260
|
+
if channel is None:
|
|
261
|
+
raise HTTPException(status_code=404, detail="Channel not found")
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
"success": True,
|
|
265
|
+
"data": {
|
|
266
|
+
"id": channel.id,
|
|
267
|
+
"name": channel.name,
|
|
268
|
+
"type": channel.type,
|
|
269
|
+
"is_active": channel.is_active,
|
|
270
|
+
},
|
|
271
|
+
}
|
|
272
|
+
except ValueError as e:
|
|
273
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@router.delete("/channels/{channel_id}")
|
|
277
|
+
async def delete_channel(
|
|
278
|
+
channel_id: str,
|
|
279
|
+
session: AsyncSession = Depends(get_session),
|
|
280
|
+
) -> dict[str, Any]:
|
|
281
|
+
"""Delete a notification channel."""
|
|
282
|
+
service = NotificationChannelService(session)
|
|
283
|
+
deleted = await service.delete(channel_id)
|
|
284
|
+
await session.commit()
|
|
285
|
+
|
|
286
|
+
if not deleted:
|
|
287
|
+
raise HTTPException(status_code=404, detail="Channel not found")
|
|
288
|
+
|
|
289
|
+
return {"success": True}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@router.post("/channels/{channel_id}/test")
|
|
293
|
+
async def test_channel(
|
|
294
|
+
channel_id: str,
|
|
295
|
+
session: AsyncSession = Depends(get_session),
|
|
296
|
+
) -> dict[str, Any]:
|
|
297
|
+
"""Send a test notification to a channel."""
|
|
298
|
+
dispatcher = create_dispatcher(session)
|
|
299
|
+
result = await dispatcher.test_channel(channel_id)
|
|
300
|
+
await session.commit()
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
"success": result.success,
|
|
304
|
+
"message": "Test notification sent" if result.success else "Test failed",
|
|
305
|
+
"error": result.error,
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# =============================================================================
|
|
310
|
+
# Rule Endpoints
|
|
311
|
+
# =============================================================================
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@router.get("/rules/conditions")
|
|
315
|
+
async def get_rule_conditions(
|
|
316
|
+
session: AsyncSession = Depends(get_session),
|
|
317
|
+
) -> dict[str, Any]:
|
|
318
|
+
"""Get valid notification rule conditions."""
|
|
319
|
+
service = NotificationRuleService(session)
|
|
320
|
+
return {
|
|
321
|
+
"success": True,
|
|
322
|
+
"data": service.get_valid_conditions(),
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@router.get("/rules")
|
|
327
|
+
async def list_rules(
|
|
328
|
+
offset: int = Query(default=0, ge=0),
|
|
329
|
+
limit: int = Query(default=50, ge=1, le=100),
|
|
330
|
+
active_only: bool = Query(default=False),
|
|
331
|
+
condition: str | None = Query(default=None),
|
|
332
|
+
session: AsyncSession = Depends(get_session),
|
|
333
|
+
) -> dict[str, Any]:
|
|
334
|
+
"""List notification rules."""
|
|
335
|
+
service = NotificationRuleService(session)
|
|
336
|
+
rules = await service.list(
|
|
337
|
+
offset=offset,
|
|
338
|
+
limit=limit,
|
|
339
|
+
active_only=active_only,
|
|
340
|
+
condition=condition,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
"success": True,
|
|
345
|
+
"data": [
|
|
346
|
+
{
|
|
347
|
+
"id": r.id,
|
|
348
|
+
"name": r.name,
|
|
349
|
+
"condition": r.condition,
|
|
350
|
+
"condition_config": r.condition_config,
|
|
351
|
+
"channel_ids": r.channel_ids,
|
|
352
|
+
"source_ids": r.source_ids,
|
|
353
|
+
"is_active": r.is_active,
|
|
354
|
+
"created_at": r.created_at.isoformat(),
|
|
355
|
+
"updated_at": r.updated_at.isoformat(),
|
|
356
|
+
}
|
|
357
|
+
for r in rules
|
|
358
|
+
],
|
|
359
|
+
"count": len(rules),
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@router.post("/rules")
|
|
364
|
+
async def create_rule(
|
|
365
|
+
request: RuleCreate,
|
|
366
|
+
session: AsyncSession = Depends(get_session),
|
|
367
|
+
) -> dict[str, Any]:
|
|
368
|
+
"""Create a new notification rule."""
|
|
369
|
+
service = NotificationRuleService(session)
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
rule = await service.create(
|
|
373
|
+
name=request.name,
|
|
374
|
+
condition=request.condition,
|
|
375
|
+
channel_ids=request.channel_ids,
|
|
376
|
+
condition_config=request.condition_config,
|
|
377
|
+
source_ids=request.source_ids,
|
|
378
|
+
is_active=request.is_active,
|
|
379
|
+
)
|
|
380
|
+
await session.commit()
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
"success": True,
|
|
384
|
+
"data": {
|
|
385
|
+
"id": rule.id,
|
|
386
|
+
"name": rule.name,
|
|
387
|
+
"condition": rule.condition,
|
|
388
|
+
},
|
|
389
|
+
}
|
|
390
|
+
except ValueError as e:
|
|
391
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@router.get("/rules/{rule_id}")
|
|
395
|
+
async def get_rule(
|
|
396
|
+
rule_id: str,
|
|
397
|
+
session: AsyncSession = Depends(get_session),
|
|
398
|
+
) -> dict[str, Any]:
|
|
399
|
+
"""Get a notification rule by ID."""
|
|
400
|
+
service = NotificationRuleService(session)
|
|
401
|
+
rule = await service.get_by_id(rule_id)
|
|
402
|
+
|
|
403
|
+
if rule is None:
|
|
404
|
+
raise HTTPException(status_code=404, detail="Rule not found")
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
"success": True,
|
|
408
|
+
"data": {
|
|
409
|
+
"id": rule.id,
|
|
410
|
+
"name": rule.name,
|
|
411
|
+
"condition": rule.condition,
|
|
412
|
+
"condition_config": rule.condition_config,
|
|
413
|
+
"channel_ids": rule.channel_ids,
|
|
414
|
+
"source_ids": rule.source_ids,
|
|
415
|
+
"is_active": rule.is_active,
|
|
416
|
+
"created_at": rule.created_at.isoformat(),
|
|
417
|
+
"updated_at": rule.updated_at.isoformat(),
|
|
418
|
+
},
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
@router.put("/rules/{rule_id}")
|
|
423
|
+
async def update_rule(
|
|
424
|
+
rule_id: str,
|
|
425
|
+
request: RuleUpdate,
|
|
426
|
+
session: AsyncSession = Depends(get_session),
|
|
427
|
+
) -> dict[str, Any]:
|
|
428
|
+
"""Update a notification rule."""
|
|
429
|
+
service = NotificationRuleService(session)
|
|
430
|
+
|
|
431
|
+
try:
|
|
432
|
+
rule = await service.update(
|
|
433
|
+
rule_id,
|
|
434
|
+
name=request.name,
|
|
435
|
+
condition=request.condition,
|
|
436
|
+
channel_ids=request.channel_ids,
|
|
437
|
+
condition_config=request.condition_config,
|
|
438
|
+
source_ids=request.source_ids,
|
|
439
|
+
is_active=request.is_active,
|
|
440
|
+
)
|
|
441
|
+
await session.commit()
|
|
442
|
+
|
|
443
|
+
if rule is None:
|
|
444
|
+
raise HTTPException(status_code=404, detail="Rule not found")
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
"success": True,
|
|
448
|
+
"data": {
|
|
449
|
+
"id": rule.id,
|
|
450
|
+
"name": rule.name,
|
|
451
|
+
"condition": rule.condition,
|
|
452
|
+
},
|
|
453
|
+
}
|
|
454
|
+
except ValueError as e:
|
|
455
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
@router.delete("/rules/{rule_id}")
|
|
459
|
+
async def delete_rule(
|
|
460
|
+
rule_id: str,
|
|
461
|
+
session: AsyncSession = Depends(get_session),
|
|
462
|
+
) -> dict[str, Any]:
|
|
463
|
+
"""Delete a notification rule."""
|
|
464
|
+
service = NotificationRuleService(session)
|
|
465
|
+
deleted = await service.delete(rule_id)
|
|
466
|
+
await session.commit()
|
|
467
|
+
|
|
468
|
+
if not deleted:
|
|
469
|
+
raise HTTPException(status_code=404, detail="Rule not found")
|
|
470
|
+
|
|
471
|
+
return {"success": True}
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# =============================================================================
|
|
475
|
+
# Log Endpoints
|
|
476
|
+
# =============================================================================
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@router.get("/logs")
|
|
480
|
+
async def list_logs(
|
|
481
|
+
offset: int = Query(default=0, ge=0),
|
|
482
|
+
limit: int = Query(default=50, ge=1, le=100),
|
|
483
|
+
channel_id: str | None = Query(default=None),
|
|
484
|
+
status: str | None = Query(default=None),
|
|
485
|
+
hours: int | None = Query(default=None, ge=1, le=168),
|
|
486
|
+
session: AsyncSession = Depends(get_session),
|
|
487
|
+
) -> dict[str, Any]:
|
|
488
|
+
"""List notification delivery logs."""
|
|
489
|
+
service = NotificationLogService(session)
|
|
490
|
+
logs = await service.list(
|
|
491
|
+
offset=offset,
|
|
492
|
+
limit=limit,
|
|
493
|
+
channel_id=channel_id,
|
|
494
|
+
status=status,
|
|
495
|
+
hours=hours,
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
"success": True,
|
|
500
|
+
"data": [
|
|
501
|
+
{
|
|
502
|
+
"id": log.id,
|
|
503
|
+
"channel_id": log.channel_id,
|
|
504
|
+
"rule_id": log.rule_id,
|
|
505
|
+
"event_type": log.event_type,
|
|
506
|
+
"status": log.status,
|
|
507
|
+
"message_preview": (
|
|
508
|
+
log.message[:100] + "..." if len(log.message) > 100 else log.message
|
|
509
|
+
),
|
|
510
|
+
"error_message": log.error_message,
|
|
511
|
+
"created_at": log.created_at.isoformat(),
|
|
512
|
+
"sent_at": log.sent_at.isoformat() if log.sent_at else None,
|
|
513
|
+
}
|
|
514
|
+
for log in logs
|
|
515
|
+
],
|
|
516
|
+
"count": len(logs),
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
@router.get("/logs/stats")
|
|
521
|
+
async def get_log_stats(
|
|
522
|
+
hours: int = Query(default=24, ge=1, le=168),
|
|
523
|
+
session: AsyncSession = Depends(get_session),
|
|
524
|
+
) -> dict[str, Any]:
|
|
525
|
+
"""Get notification delivery statistics."""
|
|
526
|
+
service = NotificationLogService(session)
|
|
527
|
+
stats = await service.get_stats(hours=hours)
|
|
528
|
+
|
|
529
|
+
return {
|
|
530
|
+
"success": True,
|
|
531
|
+
"data": stats,
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
@router.get("/logs/{log_id}")
|
|
536
|
+
async def get_log(
|
|
537
|
+
log_id: str,
|
|
538
|
+
session: AsyncSession = Depends(get_session),
|
|
539
|
+
) -> dict[str, Any]:
|
|
540
|
+
"""Get a notification log by ID."""
|
|
541
|
+
service = NotificationLogService(session)
|
|
542
|
+
log = await service.get_by_id(log_id)
|
|
543
|
+
|
|
544
|
+
if log is None:
|
|
545
|
+
raise HTTPException(status_code=404, detail="Log not found")
|
|
546
|
+
|
|
547
|
+
return {
|
|
548
|
+
"success": True,
|
|
549
|
+
"data": {
|
|
550
|
+
"id": log.id,
|
|
551
|
+
"channel_id": log.channel_id,
|
|
552
|
+
"rule_id": log.rule_id,
|
|
553
|
+
"event_type": log.event_type,
|
|
554
|
+
"event_data": log.event_data,
|
|
555
|
+
"status": log.status,
|
|
556
|
+
"message": log.message,
|
|
557
|
+
"error_message": log.error_message,
|
|
558
|
+
"created_at": log.created_at.isoformat(),
|
|
559
|
+
"sent_at": log.sent_at.isoformat() if log.sent_at else None,
|
|
560
|
+
},
|
|
561
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Profile API endpoints.
|
|
2
|
+
|
|
3
|
+
This module provides endpoints for data profiling.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
|
|
10
|
+
from fastapi import APIRouter, HTTPException, Path
|
|
11
|
+
|
|
12
|
+
from truthound_dashboard.schemas import ProfileResponse
|
|
13
|
+
|
|
14
|
+
from .deps import ProfileServiceDep, SourceServiceDep
|
|
15
|
+
|
|
16
|
+
router = APIRouter()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@router.post(
|
|
20
|
+
"/sources/{source_id}/profile",
|
|
21
|
+
response_model=ProfileResponse,
|
|
22
|
+
summary="Profile source",
|
|
23
|
+
description="Run data profiling on a source",
|
|
24
|
+
)
|
|
25
|
+
async def profile_source(
|
|
26
|
+
service: ProfileServiceDep,
|
|
27
|
+
source_service: SourceServiceDep,
|
|
28
|
+
source_id: Annotated[str, Path(description="Source ID to profile")],
|
|
29
|
+
) -> ProfileResponse:
|
|
30
|
+
"""Run data profiling on a source.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
service: Injected profile service.
|
|
34
|
+
source_service: Injected source service.
|
|
35
|
+
source_id: Source to profile.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Profiling result with column statistics.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
HTTPException: 404 if source not found.
|
|
42
|
+
"""
|
|
43
|
+
# Verify source exists
|
|
44
|
+
source = await source_service.get_by_id(source_id)
|
|
45
|
+
if source is None:
|
|
46
|
+
raise HTTPException(status_code=404, detail="Source not found")
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
result = await service.profile_source(source_id)
|
|
50
|
+
return ProfileResponse.from_result(result)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""API router configuration.
|
|
2
|
+
|
|
3
|
+
This module configures the main API router and includes all sub-routers.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter
|
|
7
|
+
|
|
8
|
+
from . import (
|
|
9
|
+
drift,
|
|
10
|
+
health,
|
|
11
|
+
history,
|
|
12
|
+
notifications,
|
|
13
|
+
profile,
|
|
14
|
+
rules,
|
|
15
|
+
schedules,
|
|
16
|
+
schemas,
|
|
17
|
+
sources,
|
|
18
|
+
validations,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
api_router = APIRouter()
|
|
22
|
+
|
|
23
|
+
# Health endpoints (no prefix)
|
|
24
|
+
api_router.include_router(
|
|
25
|
+
health.router,
|
|
26
|
+
tags=["health"],
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Source management
|
|
30
|
+
api_router.include_router(
|
|
31
|
+
sources.router,
|
|
32
|
+
prefix="/sources",
|
|
33
|
+
tags=["sources"],
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Schema management
|
|
37
|
+
api_router.include_router(
|
|
38
|
+
schemas.router,
|
|
39
|
+
tags=["schemas"],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Rules management
|
|
43
|
+
api_router.include_router(
|
|
44
|
+
rules.router,
|
|
45
|
+
tags=["rules"],
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Validation endpoints
|
|
49
|
+
api_router.include_router(
|
|
50
|
+
validations.router,
|
|
51
|
+
prefix="/validations",
|
|
52
|
+
tags=["validations"],
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Profiling endpoints
|
|
56
|
+
api_router.include_router(
|
|
57
|
+
profile.router,
|
|
58
|
+
tags=["profiling"],
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# History endpoints (Phase 2)
|
|
62
|
+
api_router.include_router(
|
|
63
|
+
history.router,
|
|
64
|
+
tags=["history"],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Drift detection endpoints (Phase 2)
|
|
68
|
+
api_router.include_router(
|
|
69
|
+
drift.router,
|
|
70
|
+
tags=["drift"],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Schedule management endpoints (Phase 2)
|
|
74
|
+
api_router.include_router(
|
|
75
|
+
schedules.router,
|
|
76
|
+
tags=["schedules"],
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Notification management endpoints (Phase 3)
|
|
80
|
+
api_router.include_router(
|
|
81
|
+
notifications.router,
|
|
82
|
+
tags=["notifications"],
|
|
83
|
+
)
|