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.
Files changed (205) hide show
  1. truthound_dashboard/api/alerts.py +75 -86
  2. truthound_dashboard/api/anomaly.py +7 -13
  3. truthound_dashboard/api/cross_alerts.py +38 -52
  4. truthound_dashboard/api/drift.py +49 -59
  5. truthound_dashboard/api/drift_monitor.py +234 -79
  6. truthound_dashboard/api/enterprise_sampling.py +498 -0
  7. truthound_dashboard/api/history.py +57 -5
  8. truthound_dashboard/api/lineage.py +3 -48
  9. truthound_dashboard/api/maintenance.py +104 -49
  10. truthound_dashboard/api/mask.py +1 -2
  11. truthound_dashboard/api/middleware.py +2 -1
  12. truthound_dashboard/api/model_monitoring.py +435 -311
  13. truthound_dashboard/api/notifications.py +227 -191
  14. truthound_dashboard/api/notifications_advanced.py +21 -20
  15. truthound_dashboard/api/observability.py +586 -0
  16. truthound_dashboard/api/plugins.py +2 -433
  17. truthound_dashboard/api/profile.py +199 -37
  18. truthound_dashboard/api/quality_reporter.py +701 -0
  19. truthound_dashboard/api/reports.py +7 -16
  20. truthound_dashboard/api/router.py +66 -0
  21. truthound_dashboard/api/rule_suggestions.py +5 -5
  22. truthound_dashboard/api/scan.py +17 -19
  23. truthound_dashboard/api/schedules.py +85 -50
  24. truthound_dashboard/api/schema_evolution.py +6 -6
  25. truthound_dashboard/api/schema_watcher.py +667 -0
  26. truthound_dashboard/api/sources.py +98 -27
  27. truthound_dashboard/api/tiering.py +1323 -0
  28. truthound_dashboard/api/triggers.py +14 -11
  29. truthound_dashboard/api/validations.py +12 -11
  30. truthound_dashboard/api/versioning.py +1 -6
  31. truthound_dashboard/core/__init__.py +129 -3
  32. truthound_dashboard/core/actions/__init__.py +62 -0
  33. truthound_dashboard/core/actions/custom.py +426 -0
  34. truthound_dashboard/core/actions/notifications.py +910 -0
  35. truthound_dashboard/core/actions/storage.py +472 -0
  36. truthound_dashboard/core/actions/webhook.py +281 -0
  37. truthound_dashboard/core/anomaly.py +262 -67
  38. truthound_dashboard/core/anomaly_explainer.py +4 -3
  39. truthound_dashboard/core/backends/__init__.py +67 -0
  40. truthound_dashboard/core/backends/base.py +299 -0
  41. truthound_dashboard/core/backends/errors.py +191 -0
  42. truthound_dashboard/core/backends/factory.py +423 -0
  43. truthound_dashboard/core/backends/mock_backend.py +451 -0
  44. truthound_dashboard/core/backends/truthound_backend.py +718 -0
  45. truthound_dashboard/core/checkpoint/__init__.py +87 -0
  46. truthound_dashboard/core/checkpoint/adapters.py +814 -0
  47. truthound_dashboard/core/checkpoint/checkpoint.py +491 -0
  48. truthound_dashboard/core/checkpoint/runner.py +270 -0
  49. truthound_dashboard/core/connections.py +645 -23
  50. truthound_dashboard/core/converters/__init__.py +14 -0
  51. truthound_dashboard/core/converters/truthound.py +620 -0
  52. truthound_dashboard/core/cross_alerts.py +540 -320
  53. truthound_dashboard/core/datasource_factory.py +1672 -0
  54. truthound_dashboard/core/drift_monitor.py +216 -20
  55. truthound_dashboard/core/enterprise_sampling.py +1291 -0
  56. truthound_dashboard/core/interfaces/__init__.py +225 -0
  57. truthound_dashboard/core/interfaces/actions.py +652 -0
  58. truthound_dashboard/core/interfaces/base.py +247 -0
  59. truthound_dashboard/core/interfaces/checkpoint.py +676 -0
  60. truthound_dashboard/core/interfaces/protocols.py +664 -0
  61. truthound_dashboard/core/interfaces/reporters.py +650 -0
  62. truthound_dashboard/core/interfaces/routing.py +646 -0
  63. truthound_dashboard/core/interfaces/triggers.py +619 -0
  64. truthound_dashboard/core/lineage.py +407 -71
  65. truthound_dashboard/core/model_monitoring.py +431 -3
  66. truthound_dashboard/core/notifications/base.py +4 -0
  67. truthound_dashboard/core/notifications/channels.py +501 -1203
  68. truthound_dashboard/core/notifications/deduplication/__init__.py +81 -115
  69. truthound_dashboard/core/notifications/deduplication/service.py +131 -348
  70. truthound_dashboard/core/notifications/dispatcher.py +202 -11
  71. truthound_dashboard/core/notifications/escalation/__init__.py +119 -106
  72. truthound_dashboard/core/notifications/escalation/engine.py +168 -358
  73. truthound_dashboard/core/notifications/routing/__init__.py +88 -128
  74. truthound_dashboard/core/notifications/routing/engine.py +90 -317
  75. truthound_dashboard/core/notifications/stats_aggregator.py +246 -1
  76. truthound_dashboard/core/notifications/throttling/__init__.py +67 -50
  77. truthound_dashboard/core/notifications/throttling/builder.py +117 -255
  78. truthound_dashboard/core/notifications/truthound_adapter.py +842 -0
  79. truthound_dashboard/core/phase5/collaboration.py +1 -1
  80. truthound_dashboard/core/plugins/lifecycle/__init__.py +0 -13
  81. truthound_dashboard/core/quality_reporter.py +1359 -0
  82. truthound_dashboard/core/report_history.py +0 -6
  83. truthound_dashboard/core/reporters/__init__.py +175 -14
  84. truthound_dashboard/core/reporters/adapters.py +943 -0
  85. truthound_dashboard/core/reporters/base.py +0 -3
  86. truthound_dashboard/core/reporters/builtin/__init__.py +18 -0
  87. truthound_dashboard/core/reporters/builtin/csv_reporter.py +111 -0
  88. truthound_dashboard/core/reporters/builtin/html_reporter.py +270 -0
  89. truthound_dashboard/core/reporters/builtin/json_reporter.py +127 -0
  90. truthound_dashboard/core/reporters/compat.py +266 -0
  91. truthound_dashboard/core/reporters/csv_reporter.py +2 -35
  92. truthound_dashboard/core/reporters/factory.py +526 -0
  93. truthound_dashboard/core/reporters/interfaces.py +745 -0
  94. truthound_dashboard/core/reporters/registry.py +1 -10
  95. truthound_dashboard/core/scheduler.py +165 -0
  96. truthound_dashboard/core/schema_evolution.py +3 -3
  97. truthound_dashboard/core/schema_watcher.py +1528 -0
  98. truthound_dashboard/core/services.py +595 -76
  99. truthound_dashboard/core/store_manager.py +810 -0
  100. truthound_dashboard/core/streaming_anomaly.py +169 -4
  101. truthound_dashboard/core/tiering.py +1309 -0
  102. truthound_dashboard/core/triggers/evaluators.py +178 -8
  103. truthound_dashboard/core/truthound_adapter.py +2620 -197
  104. truthound_dashboard/core/unified_alerts.py +23 -20
  105. truthound_dashboard/db/__init__.py +8 -0
  106. truthound_dashboard/db/database.py +8 -2
  107. truthound_dashboard/db/models.py +944 -25
  108. truthound_dashboard/db/repository.py +2 -0
  109. truthound_dashboard/main.py +15 -0
  110. truthound_dashboard/schemas/__init__.py +177 -16
  111. truthound_dashboard/schemas/base.py +44 -23
  112. truthound_dashboard/schemas/collaboration.py +19 -6
  113. truthound_dashboard/schemas/cross_alerts.py +19 -3
  114. truthound_dashboard/schemas/drift.py +61 -55
  115. truthound_dashboard/schemas/drift_monitor.py +67 -23
  116. truthound_dashboard/schemas/enterprise_sampling.py +653 -0
  117. truthound_dashboard/schemas/lineage.py +0 -33
  118. truthound_dashboard/schemas/mask.py +10 -8
  119. truthound_dashboard/schemas/model_monitoring.py +89 -10
  120. truthound_dashboard/schemas/notifications_advanced.py +13 -0
  121. truthound_dashboard/schemas/observability.py +453 -0
  122. truthound_dashboard/schemas/plugins.py +0 -280
  123. truthound_dashboard/schemas/profile.py +154 -247
  124. truthound_dashboard/schemas/quality_reporter.py +403 -0
  125. truthound_dashboard/schemas/reports.py +2 -2
  126. truthound_dashboard/schemas/rule_suggestion.py +8 -1
  127. truthound_dashboard/schemas/scan.py +4 -24
  128. truthound_dashboard/schemas/schedule.py +11 -3
  129. truthound_dashboard/schemas/schema_watcher.py +727 -0
  130. truthound_dashboard/schemas/source.py +17 -2
  131. truthound_dashboard/schemas/tiering.py +822 -0
  132. truthound_dashboard/schemas/triggers.py +16 -0
  133. truthound_dashboard/schemas/unified_alerts.py +7 -0
  134. truthound_dashboard/schemas/validation.py +0 -13
  135. truthound_dashboard/schemas/validators/base.py +41 -21
  136. truthound_dashboard/schemas/validators/business_rule_validators.py +244 -0
  137. truthound_dashboard/schemas/validators/localization_validators.py +273 -0
  138. truthound_dashboard/schemas/validators/ml_feature_validators.py +308 -0
  139. truthound_dashboard/schemas/validators/profiling_validators.py +275 -0
  140. truthound_dashboard/schemas/validators/referential_validators.py +312 -0
  141. truthound_dashboard/schemas/validators/registry.py +93 -8
  142. truthound_dashboard/schemas/validators/timeseries_validators.py +389 -0
  143. truthound_dashboard/schemas/versioning.py +1 -6
  144. truthound_dashboard/static/index.html +2 -2
  145. truthound_dashboard-1.5.1.dist-info/METADATA +312 -0
  146. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/RECORD +149 -148
  147. truthound_dashboard/core/plugins/hooks/__init__.py +0 -63
  148. truthound_dashboard/core/plugins/hooks/decorators.py +0 -367
  149. truthound_dashboard/core/plugins/hooks/manager.py +0 -403
  150. truthound_dashboard/core/plugins/hooks/protocols.py +0 -265
  151. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +0 -584
  152. truthound_dashboard/core/reporters/junit_reporter.py +0 -233
  153. truthound_dashboard/core/reporters/markdown_reporter.py +0 -207
  154. truthound_dashboard/core/reporters/pdf_reporter.py +0 -209
  155. truthound_dashboard/static/assets/_baseUniq-BcrSP13d.js +0 -1
  156. truthound_dashboard/static/assets/arc-DlYjKwIL.js +0 -1
  157. truthound_dashboard/static/assets/architectureDiagram-VXUJARFQ-Bb2drbQM.js +0 -36
  158. truthound_dashboard/static/assets/blockDiagram-VD42YOAC-BlsPG1CH.js +0 -122
  159. truthound_dashboard/static/assets/c4Diagram-YG6GDRKO-B9JdUoaC.js +0 -10
  160. truthound_dashboard/static/assets/channel-Q6mHF1Hd.js +0 -1
  161. truthound_dashboard/static/assets/chunk-4BX2VUAB-DmyoPVuJ.js +0 -1
  162. truthound_dashboard/static/assets/chunk-55IACEB6-Bcz6Siv8.js +0 -1
  163. truthound_dashboard/static/assets/chunk-B4BG7PRW-Br3G5Rum.js +0 -165
  164. truthound_dashboard/static/assets/chunk-DI55MBZ5-DuM9c23u.js +0 -220
  165. truthound_dashboard/static/assets/chunk-FMBD7UC4-DNU-5mvT.js +0 -15
  166. truthound_dashboard/static/assets/chunk-QN33PNHL-Im2yNcmS.js +0 -1
  167. truthound_dashboard/static/assets/chunk-QZHKN3VN-kZr8XFm1.js +0 -1
  168. truthound_dashboard/static/assets/chunk-TZMSLE5B-Q__360q_.js +0 -1
  169. truthound_dashboard/static/assets/classDiagram-2ON5EDUG-vtixxUyK.js +0 -1
  170. truthound_dashboard/static/assets/classDiagram-v2-WZHVMYZB-vtixxUyK.js +0 -1
  171. truthound_dashboard/static/assets/clone-BOt2LwD0.js +0 -1
  172. truthound_dashboard/static/assets/cose-bilkent-S5V4N54A-CBDw6iac.js +0 -1
  173. truthound_dashboard/static/assets/dagre-6UL2VRFP-XdKqmmY9.js +0 -4
  174. truthound_dashboard/static/assets/diagram-PSM6KHXK-DAZ8nx9V.js +0 -24
  175. truthound_dashboard/static/assets/diagram-QEK2KX5R-BRvDTbGD.js +0 -43
  176. truthound_dashboard/static/assets/diagram-S2PKOQOG-bQcczUkl.js +0 -24
  177. truthound_dashboard/static/assets/erDiagram-Q2GNP2WA-DPje7VMN.js +0 -60
  178. truthound_dashboard/static/assets/flowDiagram-NV44I4VS-B7BVtFVS.js +0 -162
  179. truthound_dashboard/static/assets/ganttDiagram-JELNMOA3-D6WKSS7U.js +0 -267
  180. truthound_dashboard/static/assets/gitGraphDiagram-NY62KEGX-D3vtVd3y.js +0 -65
  181. truthound_dashboard/static/assets/graph-BKgNKZVp.js +0 -1
  182. truthound_dashboard/static/assets/index-C6JSrkHo.css +0 -1
  183. truthound_dashboard/static/assets/index-DkU82VsU.js +0 -1800
  184. truthound_dashboard/static/assets/infoDiagram-WHAUD3N6-DnNCT429.js +0 -2
  185. truthound_dashboard/static/assets/journeyDiagram-XKPGCS4Q-DGiMozqS.js +0 -139
  186. truthound_dashboard/static/assets/kanban-definition-3W4ZIXB7-BV2gUgli.js +0 -89
  187. truthound_dashboard/static/assets/katex-Cu_Erd72.js +0 -261
  188. truthound_dashboard/static/assets/layout-DI2MfQ5G.js +0 -1
  189. truthound_dashboard/static/assets/min-DYdgXVcT.js +0 -1
  190. truthound_dashboard/static/assets/mindmap-definition-VGOIOE7T-C7x4ruxz.js +0 -68
  191. truthound_dashboard/static/assets/pieDiagram-ADFJNKIX-CAJaAB9f.js +0 -30
  192. truthound_dashboard/static/assets/quadrantDiagram-AYHSOK5B-DeqwDI46.js +0 -7
  193. truthound_dashboard/static/assets/requirementDiagram-UZGBJVZJ-e3XDpZIM.js +0 -64
  194. truthound_dashboard/static/assets/sankeyDiagram-TZEHDZUN-CNnAv5Ux.js +0 -10
  195. truthound_dashboard/static/assets/sequenceDiagram-WL72ISMW-Dsne-Of3.js +0 -145
  196. truthound_dashboard/static/assets/stateDiagram-FKZM4ZOC-Ee0sQXyb.js +0 -1
  197. truthound_dashboard/static/assets/stateDiagram-v2-4FDKWEC3-B26KqW_W.js +0 -1
  198. truthound_dashboard/static/assets/timeline-definition-IT6M3QCI-DZYi2yl3.js +0 -61
  199. truthound_dashboard/static/assets/treemap-KMMF4GRG-CY3f8In2.js +0 -128
  200. truthound_dashboard/static/assets/unmerged_dictionaries-Dd7xcPWG.js +0 -1
  201. truthound_dashboard/static/assets/xychartDiagram-PRI3JC2R-CS7fydZZ.js +0 -7
  202. truthound_dashboard-1.4.4.dist-info/METADATA +0 -507
  203. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/WHEEL +0 -0
  204. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/entry_points.txt +0 -0
  205. {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
- ) -> dict[str, Any]:
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
- 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")
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
- ) -> dict[str, Any]:
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
- "success": True,
205
- "data": {
206
- "id": channel.id,
207
- "name": channel.name,
208
- "type": channel.type,
209
- "is_active": channel.is_active,
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
- ) -> dict[str, Any]:
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
- "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}")
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
- ) -> dict[str, Any]:
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
- "success": True,
265
- "data": {
266
- "id": channel.id,
267
- "name": channel.name,
268
- "type": channel.type,
269
- "is_active": channel.is_active,
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
- ) -> dict[str, Any]:
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 {"success": True}
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
- ) -> dict[str, Any]:
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
- "success": result.success,
304
- "message": "Test notification sent" if result.success else "Test failed",
305
- "error": result.error,
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
- ) -> dict[str, Any]:
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
- 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")
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
- ) -> dict[str, Any]:
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
- "success": True,
384
- "data": {
385
- "id": rule.id,
386
- "name": rule.name,
387
- "condition": rule.condition,
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
- ) -> dict[str, Any]:
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
- "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}")
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
- ) -> dict[str, Any]:
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
- "success": True,
448
- "data": {
449
- "id": rule.id,
450
- "name": rule.name,
451
- "condition": rule.condition,
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
- ) -> dict[str, Any]:
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 {"success": True}
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
- ) -> dict[str, Any]:
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
- 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")
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
- stats = await service.get_stats(hours=hours)
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
- ) -> dict[str, Any]:
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
- "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
- }
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
+ )