truthound-dashboard 1.1.0__py3-none-any.whl → 1.2.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/catalog.py +343 -0
- truthound_dashboard/api/collaboration.py +148 -0
- truthound_dashboard/api/glossary.py +329 -0
- truthound_dashboard/api/router.py +29 -0
- truthound_dashboard/core/__init__.py +12 -0
- truthound_dashboard/core/phase5/__init__.py +17 -0
- truthound_dashboard/core/phase5/activity.py +144 -0
- truthound_dashboard/core/phase5/catalog.py +868 -0
- truthound_dashboard/core/phase5/collaboration.py +305 -0
- truthound_dashboard/core/phase5/glossary.py +828 -0
- truthound_dashboard/db/__init__.py +37 -0
- truthound_dashboard/db/models.py +693 -0
- truthound_dashboard/schemas/__init__.py +114 -0
- truthound_dashboard/schemas/catalog.py +352 -0
- truthound_dashboard/schemas/collaboration.py +169 -0
- truthound_dashboard/schemas/glossary.py +349 -0
- truthound_dashboard/static/assets/index-BqXVFyqj.js +574 -0
- truthound_dashboard/static/assets/index-o8qHVDte.css +1 -0
- truthound_dashboard/static/assets/logo--IpBiMPK.png +0 -0
- truthound_dashboard/static/assets/unmerged_dictionaries-n_T3wZTf.js +1 -0
- truthound_dashboard/static/favicon.ico +0 -0
- truthound_dashboard/static/index.html +3 -3
- {truthound_dashboard-1.1.0.dist-info → truthound_dashboard-1.2.1.dist-info}/METADATA +21 -1
- {truthound_dashboard-1.1.0.dist-info → truthound_dashboard-1.2.1.dist-info}/RECORD +27 -15
- truthound_dashboard/static/assets/index-BqJMyAHX.js +0 -110
- truthound_dashboard/static/assets/index-DMDxHCTs.js +0 -465
- truthound_dashboard/static/assets/index-Dm2D11TK.css +0 -1
- truthound_dashboard/static/mockServiceWorker.js +0 -349
- {truthound_dashboard-1.1.0.dist-info → truthound_dashboard-1.2.1.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.1.0.dist-info → truthound_dashboard-1.2.1.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.1.0.dist-info → truthound_dashboard-1.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
+
)
|