truthound-dashboard 1.1.0__py3-none-any.whl → 1.2.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.
@@ -0,0 +1,343 @@
1
+ """Catalog API endpoints.
2
+
3
+ This module provides REST API endpoints for managing data catalog
4
+ assets, columns, and tags.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Annotated
10
+
11
+ from fastapi import APIRouter, Depends, HTTPException, Query, status
12
+
13
+ from truthound_dashboard.core.phase5 import CatalogService
14
+ from truthound_dashboard.schemas import (
15
+ AssetCreate,
16
+ AssetListItem,
17
+ AssetListResponse,
18
+ AssetResponse,
19
+ AssetUpdate,
20
+ ColumnCreate,
21
+ ColumnListResponse,
22
+ ColumnResponse,
23
+ ColumnTermMapping,
24
+ ColumnUpdate,
25
+ MessageResponse,
26
+ TagCreate,
27
+ TagResponse,
28
+ )
29
+
30
+ from .deps import SessionDep
31
+
32
+ router = APIRouter()
33
+
34
+
35
+ # =============================================================================
36
+ # Dependencies
37
+ # =============================================================================
38
+
39
+
40
+ async def get_catalog_service(session: SessionDep) -> CatalogService:
41
+ """Get catalog service dependency."""
42
+ return CatalogService(session)
43
+
44
+
45
+ CatalogServiceDep = Annotated[CatalogService, Depends(get_catalog_service)]
46
+
47
+
48
+ # =============================================================================
49
+ # Asset Endpoints
50
+ # =============================================================================
51
+
52
+
53
+ @router.get("/assets", response_model=AssetListResponse)
54
+ async def list_assets(
55
+ service: CatalogServiceDep,
56
+ search: Annotated[str | None, Query(description="Search query")] = None,
57
+ asset_type: Annotated[str | None, Query(description="Filter by type")] = None,
58
+ source_id: Annotated[str | None, Query(description="Filter by data source")] = None,
59
+ offset: Annotated[int, Query(ge=0)] = 0,
60
+ limit: Annotated[int, Query(ge=1, le=100)] = 100,
61
+ ) -> AssetListResponse:
62
+ """List catalog assets with optional filters.
63
+
64
+ - **search**: Search in asset name and description
65
+ - **asset_type**: Filter by type (table, file, api)
66
+ - **source_id**: Filter by linked data source
67
+ """
68
+ assets, total = await service.list_assets(
69
+ query=search,
70
+ asset_type=asset_type,
71
+ source_id=source_id,
72
+ offset=offset,
73
+ limit=limit,
74
+ )
75
+ return AssetListResponse(
76
+ data=[AssetListItem.from_model(a) for a in assets],
77
+ total=total,
78
+ offset=offset,
79
+ limit=limit,
80
+ )
81
+
82
+
83
+ @router.post("/assets", response_model=AssetResponse, status_code=status.HTTP_201_CREATED)
84
+ async def create_asset(
85
+ service: CatalogServiceDep,
86
+ data: AssetCreate,
87
+ ) -> AssetResponse:
88
+ """Create a new catalog asset."""
89
+ try:
90
+ columns_data = [c.model_dump() for c in data.columns] if data.columns else None
91
+ tags_data = [t.model_dump() for t in data.tags] if data.tags else None
92
+
93
+ asset = await service.create_asset(
94
+ name=data.name,
95
+ asset_type=data.asset_type.value,
96
+ source_id=data.source_id,
97
+ description=data.description,
98
+ owner_id=data.owner_id,
99
+ columns=columns_data,
100
+ tags=tags_data,
101
+ )
102
+ return AssetResponse.from_model(asset)
103
+ except ValueError as e:
104
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
105
+
106
+
107
+ @router.get("/assets/{asset_id}", response_model=AssetResponse)
108
+ async def get_asset(
109
+ service: CatalogServiceDep,
110
+ asset_id: str,
111
+ ) -> AssetResponse:
112
+ """Get a catalog asset by ID."""
113
+ asset = await service.get_asset(asset_id)
114
+ if not asset:
115
+ raise HTTPException(
116
+ status_code=status.HTTP_404_NOT_FOUND,
117
+ detail=f"Asset '{asset_id}' not found",
118
+ )
119
+ return AssetResponse.from_model(asset)
120
+
121
+
122
+ @router.put("/assets/{asset_id}", response_model=AssetResponse)
123
+ async def update_asset(
124
+ service: CatalogServiceDep,
125
+ asset_id: str,
126
+ data: AssetUpdate,
127
+ ) -> AssetResponse:
128
+ """Update a catalog asset."""
129
+ try:
130
+ asset = await service.update_asset(
131
+ asset_id,
132
+ name=data.name,
133
+ asset_type=data.asset_type.value if data.asset_type else None,
134
+ source_id=data.source_id,
135
+ description=data.description,
136
+ owner_id=data.owner_id,
137
+ quality_score=data.quality_score,
138
+ )
139
+ if not asset:
140
+ raise HTTPException(
141
+ status_code=status.HTTP_404_NOT_FOUND,
142
+ detail=f"Asset '{asset_id}' not found",
143
+ )
144
+ return AssetResponse.from_model(asset)
145
+ except ValueError as e:
146
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
147
+
148
+
149
+ @router.delete("/assets/{asset_id}", response_model=MessageResponse)
150
+ async def delete_asset(
151
+ service: CatalogServiceDep,
152
+ asset_id: str,
153
+ ) -> MessageResponse:
154
+ """Delete a catalog asset."""
155
+ deleted = await service.delete_asset(asset_id)
156
+ if not deleted:
157
+ raise HTTPException(
158
+ status_code=status.HTTP_404_NOT_FOUND,
159
+ detail=f"Asset '{asset_id}' not found",
160
+ )
161
+ return MessageResponse(message="Asset deleted successfully")
162
+
163
+
164
+ # =============================================================================
165
+ # Column Endpoints
166
+ # =============================================================================
167
+
168
+
169
+ @router.get("/assets/{asset_id}/columns", response_model=ColumnListResponse)
170
+ async def get_asset_columns(
171
+ service: CatalogServiceDep,
172
+ asset_id: str,
173
+ ) -> ColumnListResponse:
174
+ """Get columns for an asset."""
175
+ # Verify asset exists
176
+ asset = await service.get_asset(asset_id)
177
+ if not asset:
178
+ raise HTTPException(
179
+ status_code=status.HTTP_404_NOT_FOUND,
180
+ detail=f"Asset '{asset_id}' not found",
181
+ )
182
+
183
+ columns = await service.get_columns(asset_id)
184
+ return ColumnListResponse(
185
+ data=[ColumnResponse.from_model(c) for c in columns],
186
+ total=len(columns),
187
+ )
188
+
189
+
190
+ @router.post("/assets/{asset_id}/columns", response_model=ColumnResponse, status_code=status.HTTP_201_CREATED)
191
+ async def create_column(
192
+ service: CatalogServiceDep,
193
+ asset_id: str,
194
+ data: ColumnCreate,
195
+ ) -> ColumnResponse:
196
+ """Add a column to an asset."""
197
+ try:
198
+ column = await service.create_column(
199
+ asset_id,
200
+ name=data.name,
201
+ data_type=data.data_type,
202
+ description=data.description,
203
+ is_nullable=data.is_nullable,
204
+ is_primary_key=data.is_primary_key,
205
+ sensitivity_level=data.sensitivity_level.value if data.sensitivity_level else None,
206
+ )
207
+ return ColumnResponse.from_model(column)
208
+ except ValueError as e:
209
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
210
+
211
+
212
+ @router.put("/columns/{column_id}", response_model=ColumnResponse)
213
+ async def update_column(
214
+ service: CatalogServiceDep,
215
+ column_id: str,
216
+ data: ColumnUpdate,
217
+ ) -> ColumnResponse:
218
+ """Update a column."""
219
+ column = await service.update_column(
220
+ column_id,
221
+ name=data.name,
222
+ data_type=data.data_type,
223
+ description=data.description,
224
+ is_nullable=data.is_nullable,
225
+ is_primary_key=data.is_primary_key,
226
+ sensitivity_level=data.sensitivity_level.value if data.sensitivity_level else None,
227
+ )
228
+ if not column:
229
+ raise HTTPException(
230
+ status_code=status.HTTP_404_NOT_FOUND,
231
+ detail=f"Column '{column_id}' not found",
232
+ )
233
+ return ColumnResponse.from_model(column)
234
+
235
+
236
+ @router.delete("/columns/{column_id}", response_model=MessageResponse)
237
+ async def delete_column(
238
+ service: CatalogServiceDep,
239
+ column_id: str,
240
+ ) -> MessageResponse:
241
+ """Delete a column."""
242
+ deleted = await service.delete_column(column_id)
243
+ if not deleted:
244
+ raise HTTPException(
245
+ status_code=status.HTTP_404_NOT_FOUND,
246
+ detail=f"Column '{column_id}' not found",
247
+ )
248
+ return MessageResponse(message="Column deleted successfully")
249
+
250
+
251
+ # =============================================================================
252
+ # Column-Term Mapping Endpoints
253
+ # =============================================================================
254
+
255
+
256
+ @router.put("/columns/{column_id}/term", response_model=ColumnResponse)
257
+ async def map_column_to_term(
258
+ service: CatalogServiceDep,
259
+ column_id: str,
260
+ data: ColumnTermMapping,
261
+ ) -> ColumnResponse:
262
+ """Map a column to a glossary term."""
263
+ try:
264
+ column = await service.map_column_to_term(column_id, data.term_id)
265
+ if not column:
266
+ raise HTTPException(
267
+ status_code=status.HTTP_404_NOT_FOUND,
268
+ detail=f"Column '{column_id}' not found",
269
+ )
270
+ return ColumnResponse.from_model(column)
271
+ except ValueError as e:
272
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
273
+
274
+
275
+ @router.delete("/columns/{column_id}/term", response_model=ColumnResponse)
276
+ async def unmap_column_from_term(
277
+ service: CatalogServiceDep,
278
+ column_id: str,
279
+ ) -> ColumnResponse:
280
+ """Remove term mapping from a column."""
281
+ column = await service.unmap_column_from_term(column_id)
282
+ if not column:
283
+ raise HTTPException(
284
+ status_code=status.HTTP_404_NOT_FOUND,
285
+ detail=f"Column '{column_id}' not found",
286
+ )
287
+ return ColumnResponse.from_model(column)
288
+
289
+
290
+ # =============================================================================
291
+ # Tag Endpoints
292
+ # =============================================================================
293
+
294
+
295
+ @router.get("/assets/{asset_id}/tags")
296
+ async def get_asset_tags(
297
+ service: CatalogServiceDep,
298
+ asset_id: str,
299
+ ) -> list[TagResponse]:
300
+ """Get tags for an asset."""
301
+ # Verify asset exists
302
+ asset = await service.get_asset(asset_id)
303
+ if not asset:
304
+ raise HTTPException(
305
+ status_code=status.HTTP_404_NOT_FOUND,
306
+ detail=f"Asset '{asset_id}' not found",
307
+ )
308
+
309
+ tags = await service.get_tags(asset_id)
310
+ return [TagResponse.from_model(t) for t in tags]
311
+
312
+
313
+ @router.post("/assets/{asset_id}/tags", response_model=TagResponse, status_code=status.HTTP_201_CREATED)
314
+ async def add_tag(
315
+ service: CatalogServiceDep,
316
+ asset_id: str,
317
+ data: TagCreate,
318
+ ) -> TagResponse:
319
+ """Add a tag to an asset."""
320
+ try:
321
+ tag = await service.add_tag(
322
+ asset_id,
323
+ tag_name=data.tag_name,
324
+ tag_value=data.tag_value,
325
+ )
326
+ return TagResponse.from_model(tag)
327
+ except ValueError as e:
328
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
329
+
330
+
331
+ @router.delete("/tags/{tag_id}", response_model=MessageResponse)
332
+ async def remove_tag(
333
+ service: CatalogServiceDep,
334
+ tag_id: str,
335
+ ) -> MessageResponse:
336
+ """Remove a tag."""
337
+ deleted = await service.remove_tag(tag_id)
338
+ if not deleted:
339
+ raise HTTPException(
340
+ status_code=status.HTTP_404_NOT_FOUND,
341
+ detail=f"Tag '{tag_id}' not found",
342
+ )
343
+ return MessageResponse(message="Tag removed successfully")
@@ -0,0 +1,148 @@
1
+ """Collaboration API endpoints.
2
+
3
+ This module provides REST API endpoints for managing comments
4
+ and activity feeds.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Annotated
10
+
11
+ from fastapi import APIRouter, Depends, HTTPException, Query, status
12
+
13
+ from truthound_dashboard.core.phase5 import CollaborationService
14
+ from truthound_dashboard.schemas import (
15
+ ActivityListResponse,
16
+ ActivityResponse,
17
+ CommentCreate,
18
+ CommentListResponse,
19
+ CommentResponse,
20
+ CommentUpdate,
21
+ MessageResponse,
22
+ )
23
+
24
+ from .deps import SessionDep
25
+
26
+ router = APIRouter()
27
+
28
+
29
+ # =============================================================================
30
+ # Dependencies
31
+ # =============================================================================
32
+
33
+
34
+ async def get_collaboration_service(session: SessionDep) -> CollaborationService:
35
+ """Get collaboration service dependency."""
36
+ return CollaborationService(session)
37
+
38
+
39
+ CollaborationServiceDep = Annotated[CollaborationService, Depends(get_collaboration_service)]
40
+
41
+
42
+ # =============================================================================
43
+ # Comment Endpoints
44
+ # =============================================================================
45
+
46
+
47
+ @router.get("/comments", response_model=CommentListResponse)
48
+ async def get_comments(
49
+ service: CollaborationServiceDep,
50
+ resource_type: Annotated[str, Query(description="Resource type (term, asset, column)")],
51
+ resource_id: Annotated[str, Query(description="Resource ID")],
52
+ limit: Annotated[int, Query(ge=1, le=100)] = 100,
53
+ ) -> CommentListResponse:
54
+ """Get comments for a resource.
55
+
56
+ - **resource_type**: Type of resource (term, asset, column)
57
+ - **resource_id**: ID of the resource
58
+ """
59
+ comments, total = await service.get_comments(
60
+ resource_type,
61
+ resource_id,
62
+ limit=limit,
63
+ )
64
+ return CommentListResponse(
65
+ data=[CommentResponse.from_model(c) for c in comments],
66
+ total=total,
67
+ )
68
+
69
+
70
+ @router.post("/comments", response_model=CommentResponse, status_code=status.HTTP_201_CREATED)
71
+ async def create_comment(
72
+ service: CollaborationServiceDep,
73
+ data: CommentCreate,
74
+ ) -> CommentResponse:
75
+ """Create a new comment."""
76
+ try:
77
+ comment = await service.create_comment(
78
+ resource_type=data.resource_type.value,
79
+ resource_id=data.resource_id,
80
+ content=data.content,
81
+ author_id=data.author_id,
82
+ parent_id=data.parent_id,
83
+ )
84
+ return CommentResponse.from_model(comment)
85
+ except ValueError as e:
86
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
87
+
88
+
89
+ @router.put("/comments/{comment_id}", response_model=CommentResponse)
90
+ async def update_comment(
91
+ service: CollaborationServiceDep,
92
+ comment_id: str,
93
+ data: CommentUpdate,
94
+ ) -> CommentResponse:
95
+ """Update a comment."""
96
+ comment = await service.update_comment(
97
+ comment_id,
98
+ content=data.content,
99
+ )
100
+ if not comment:
101
+ raise HTTPException(
102
+ status_code=status.HTTP_404_NOT_FOUND,
103
+ detail=f"Comment '{comment_id}' not found",
104
+ )
105
+ return CommentResponse.from_model(comment)
106
+
107
+
108
+ @router.delete("/comments/{comment_id}", response_model=MessageResponse)
109
+ async def delete_comment(
110
+ service: CollaborationServiceDep,
111
+ comment_id: str,
112
+ ) -> MessageResponse:
113
+ """Delete a comment."""
114
+ deleted = await service.delete_comment(comment_id)
115
+ if not deleted:
116
+ raise HTTPException(
117
+ status_code=status.HTTP_404_NOT_FOUND,
118
+ detail=f"Comment '{comment_id}' not found",
119
+ )
120
+ return MessageResponse(message="Comment deleted successfully")
121
+
122
+
123
+ # =============================================================================
124
+ # Activity Endpoints
125
+ # =============================================================================
126
+
127
+
128
+ @router.get("/activities", response_model=ActivityListResponse)
129
+ async def get_activities(
130
+ service: CollaborationServiceDep,
131
+ resource_type: Annotated[str | None, Query(description="Filter by resource type")] = None,
132
+ resource_id: Annotated[str | None, Query(description="Filter by resource ID")] = None,
133
+ limit: Annotated[int, Query(ge=1, le=100)] = 50,
134
+ ) -> ActivityListResponse:
135
+ """Get activity feed.
136
+
137
+ - **resource_type**: Optional filter by type (term, asset, column)
138
+ - **resource_id**: Optional filter by resource ID (requires resource_type)
139
+ """
140
+ activities = await service.get_activities(
141
+ resource_type=resource_type,
142
+ resource_id=resource_id,
143
+ limit=limit,
144
+ )
145
+ return ActivityListResponse(
146
+ data=[ActivityResponse.from_model(a) for a in activities],
147
+ total=len(activities),
148
+ )