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.
- 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-1.1.0.dist-info → truthound_dashboard-1.2.0.dist-info}/METADATA +21 -1
- {truthound_dashboard-1.1.0.dist-info → truthound_dashboard-1.2.0.dist-info}/RECORD +21 -10
- {truthound_dashboard-1.1.0.dist-info → truthound_dashboard-1.2.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.1.0.dist-info → truthound_dashboard-1.2.0.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.1.0.dist-info → truthound_dashboard-1.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -54,6 +54,66 @@ from .schedule import (
|
|
|
54
54
|
ScheduleResponse,
|
|
55
55
|
ScheduleUpdate,
|
|
56
56
|
)
|
|
57
|
+
# Phase 5: Glossary schemas
|
|
58
|
+
from .glossary import (
|
|
59
|
+
CategoryBase,
|
|
60
|
+
CategoryCreate,
|
|
61
|
+
CategoryListResponse,
|
|
62
|
+
CategoryResponse,
|
|
63
|
+
CategorySummary,
|
|
64
|
+
CategoryUpdate,
|
|
65
|
+
RelatedTermSummary,
|
|
66
|
+
RelationshipBase,
|
|
67
|
+
RelationshipCreate,
|
|
68
|
+
RelationshipListResponse,
|
|
69
|
+
RelationshipResponse,
|
|
70
|
+
RelationshipType,
|
|
71
|
+
TermBase,
|
|
72
|
+
TermCreate,
|
|
73
|
+
TermHistoryListResponse,
|
|
74
|
+
TermHistoryResponse,
|
|
75
|
+
TermListItem,
|
|
76
|
+
TermListResponse,
|
|
77
|
+
TermResponse,
|
|
78
|
+
TermStatus,
|
|
79
|
+
TermSummary,
|
|
80
|
+
TermUpdate,
|
|
81
|
+
)
|
|
82
|
+
# Phase 5: Catalog schemas
|
|
83
|
+
from .catalog import (
|
|
84
|
+
AssetBase,
|
|
85
|
+
AssetCreate,
|
|
86
|
+
AssetListItem,
|
|
87
|
+
AssetListResponse,
|
|
88
|
+
AssetResponse,
|
|
89
|
+
AssetType,
|
|
90
|
+
AssetUpdate,
|
|
91
|
+
ColumnBase,
|
|
92
|
+
ColumnCreate,
|
|
93
|
+
ColumnListResponse,
|
|
94
|
+
ColumnResponse,
|
|
95
|
+
ColumnTermMapping,
|
|
96
|
+
ColumnUpdate,
|
|
97
|
+
QualityLevel,
|
|
98
|
+
SensitivityLevel,
|
|
99
|
+
SourceSummary,
|
|
100
|
+
TagBase,
|
|
101
|
+
TagCreate,
|
|
102
|
+
TagResponse,
|
|
103
|
+
)
|
|
104
|
+
# Phase 5: Collaboration schemas
|
|
105
|
+
from .collaboration import (
|
|
106
|
+
ActivityAction,
|
|
107
|
+
ActivityCreate,
|
|
108
|
+
ActivityListResponse,
|
|
109
|
+
ActivityResponse,
|
|
110
|
+
CommentBase,
|
|
111
|
+
CommentCreate,
|
|
112
|
+
CommentListResponse,
|
|
113
|
+
CommentResponse,
|
|
114
|
+
CommentUpdate,
|
|
115
|
+
ResourceType,
|
|
116
|
+
)
|
|
57
117
|
from .schema import (
|
|
58
118
|
ColumnSchema,
|
|
59
119
|
SchemaLearnRequest,
|
|
@@ -147,4 +207,58 @@ __all__ = [
|
|
|
147
207
|
"ScheduleListItem",
|
|
148
208
|
"ScheduleListResponse",
|
|
149
209
|
"ScheduleActionResponse",
|
|
210
|
+
# Phase 5: Glossary
|
|
211
|
+
"TermStatus",
|
|
212
|
+
"RelationshipType",
|
|
213
|
+
"CategoryBase",
|
|
214
|
+
"CategoryCreate",
|
|
215
|
+
"CategoryUpdate",
|
|
216
|
+
"CategorySummary",
|
|
217
|
+
"CategoryResponse",
|
|
218
|
+
"CategoryListResponse",
|
|
219
|
+
"TermBase",
|
|
220
|
+
"TermCreate",
|
|
221
|
+
"TermUpdate",
|
|
222
|
+
"TermSummary",
|
|
223
|
+
"RelatedTermSummary",
|
|
224
|
+
"TermResponse",
|
|
225
|
+
"TermListItem",
|
|
226
|
+
"TermListResponse",
|
|
227
|
+
"RelationshipBase",
|
|
228
|
+
"RelationshipCreate",
|
|
229
|
+
"RelationshipResponse",
|
|
230
|
+
"RelationshipListResponse",
|
|
231
|
+
"TermHistoryResponse",
|
|
232
|
+
"TermHistoryListResponse",
|
|
233
|
+
# Phase 5: Catalog
|
|
234
|
+
"AssetType",
|
|
235
|
+
"SensitivityLevel",
|
|
236
|
+
"QualityLevel",
|
|
237
|
+
"TagBase",
|
|
238
|
+
"TagCreate",
|
|
239
|
+
"TagResponse",
|
|
240
|
+
"ColumnBase",
|
|
241
|
+
"ColumnCreate",
|
|
242
|
+
"ColumnUpdate",
|
|
243
|
+
"ColumnTermMapping",
|
|
244
|
+
"ColumnResponse",
|
|
245
|
+
"ColumnListResponse",
|
|
246
|
+
"SourceSummary",
|
|
247
|
+
"AssetBase",
|
|
248
|
+
"AssetCreate",
|
|
249
|
+
"AssetUpdate",
|
|
250
|
+
"AssetResponse",
|
|
251
|
+
"AssetListItem",
|
|
252
|
+
"AssetListResponse",
|
|
253
|
+
# Phase 5: Collaboration
|
|
254
|
+
"ResourceType",
|
|
255
|
+
"ActivityAction",
|
|
256
|
+
"CommentBase",
|
|
257
|
+
"CommentCreate",
|
|
258
|
+
"CommentUpdate",
|
|
259
|
+
"CommentResponse",
|
|
260
|
+
"CommentListResponse",
|
|
261
|
+
"ActivityResponse",
|
|
262
|
+
"ActivityListResponse",
|
|
263
|
+
"ActivityCreate",
|
|
150
264
|
]
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""Pydantic schemas for Data Catalog API.
|
|
2
|
+
|
|
3
|
+
This module defines request/response schemas for catalog assets,
|
|
4
|
+
columns, and tags.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from enum import Enum
|
|
11
|
+
|
|
12
|
+
from pydantic import Field, field_validator
|
|
13
|
+
|
|
14
|
+
from .base import BaseSchema, IDMixin, ListResponseWrapper, TimestampMixin
|
|
15
|
+
from .glossary import TermSummary
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# =============================================================================
|
|
19
|
+
# Enums
|
|
20
|
+
# =============================================================================
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AssetType(str, Enum):
|
|
24
|
+
"""Type of catalog asset."""
|
|
25
|
+
|
|
26
|
+
TABLE = "table"
|
|
27
|
+
FILE = "file"
|
|
28
|
+
API = "api"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SensitivityLevel(str, Enum):
|
|
32
|
+
"""Sensitivity level for data columns."""
|
|
33
|
+
|
|
34
|
+
PUBLIC = "public"
|
|
35
|
+
INTERNAL = "internal"
|
|
36
|
+
CONFIDENTIAL = "confidential"
|
|
37
|
+
RESTRICTED = "restricted"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class QualityLevel(str, Enum):
|
|
41
|
+
"""Quality level based on score."""
|
|
42
|
+
|
|
43
|
+
UNKNOWN = "unknown"
|
|
44
|
+
POOR = "poor"
|
|
45
|
+
FAIR = "fair"
|
|
46
|
+
GOOD = "good"
|
|
47
|
+
EXCELLENT = "excellent"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# =============================================================================
|
|
51
|
+
# Tag Schemas
|
|
52
|
+
# =============================================================================
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TagBase(BaseSchema):
|
|
56
|
+
"""Base schema for asset tags."""
|
|
57
|
+
|
|
58
|
+
tag_name: str = Field(..., min_length=1, max_length=100, description="Tag name")
|
|
59
|
+
tag_value: str | None = Field(None, max_length=255, description="Tag value")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TagCreate(TagBase):
|
|
63
|
+
"""Schema for creating an asset tag."""
|
|
64
|
+
|
|
65
|
+
@field_validator("tag_name")
|
|
66
|
+
@classmethod
|
|
67
|
+
def validate_tag_name(cls, v: str) -> str:
|
|
68
|
+
"""Validate and normalize tag name."""
|
|
69
|
+
return v.strip().lower()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class TagResponse(BaseSchema, IDMixin):
|
|
73
|
+
"""Response schema for an asset tag."""
|
|
74
|
+
|
|
75
|
+
tag_name: str
|
|
76
|
+
tag_value: str | None
|
|
77
|
+
created_at: datetime
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def from_model(cls, tag: any) -> TagResponse:
|
|
81
|
+
"""Create response from model."""
|
|
82
|
+
return cls(
|
|
83
|
+
id=tag.id,
|
|
84
|
+
tag_name=tag.tag_name,
|
|
85
|
+
tag_value=tag.tag_value,
|
|
86
|
+
created_at=tag.created_at,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# =============================================================================
|
|
91
|
+
# Column Schemas
|
|
92
|
+
# =============================================================================
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ColumnBase(BaseSchema):
|
|
96
|
+
"""Base schema for asset columns."""
|
|
97
|
+
|
|
98
|
+
name: str = Field(..., min_length=1, max_length=255, description="Column name")
|
|
99
|
+
data_type: str | None = Field(None, max_length=100, description="Data type")
|
|
100
|
+
description: str | None = Field(None, description="Column description")
|
|
101
|
+
is_nullable: bool = Field(default=True, description="Whether column allows nulls")
|
|
102
|
+
is_primary_key: bool = Field(default=False, description="Whether column is PK")
|
|
103
|
+
sensitivity_level: SensitivityLevel | None = Field(
|
|
104
|
+
default=SensitivityLevel.PUBLIC,
|
|
105
|
+
description="Data sensitivity level",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ColumnCreate(ColumnBase):
|
|
110
|
+
"""Schema for creating an asset column."""
|
|
111
|
+
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ColumnUpdate(BaseSchema):
|
|
116
|
+
"""Schema for updating an asset column."""
|
|
117
|
+
|
|
118
|
+
name: str | None = Field(None, min_length=1, max_length=255)
|
|
119
|
+
data_type: str | None = None
|
|
120
|
+
description: str | None = None
|
|
121
|
+
is_nullable: bool | None = None
|
|
122
|
+
is_primary_key: bool | None = None
|
|
123
|
+
sensitivity_level: SensitivityLevel | None = None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class ColumnTermMapping(BaseSchema):
|
|
127
|
+
"""Schema for mapping a column to a glossary term."""
|
|
128
|
+
|
|
129
|
+
term_id: str = Field(..., description="Glossary term ID to map")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ColumnResponse(BaseSchema, IDMixin, TimestampMixin):
|
|
133
|
+
"""Response schema for an asset column."""
|
|
134
|
+
|
|
135
|
+
asset_id: str
|
|
136
|
+
name: str
|
|
137
|
+
data_type: str | None
|
|
138
|
+
description: str | None
|
|
139
|
+
is_nullable: bool
|
|
140
|
+
is_primary_key: bool
|
|
141
|
+
term_id: str | None
|
|
142
|
+
term: TermSummary | None = None
|
|
143
|
+
sensitivity_level: SensitivityLevel | None
|
|
144
|
+
is_sensitive: bool = False
|
|
145
|
+
has_term_mapping: bool = False
|
|
146
|
+
|
|
147
|
+
@classmethod
|
|
148
|
+
def from_model(cls, column: any) -> ColumnResponse:
|
|
149
|
+
"""Create response from model."""
|
|
150
|
+
from .glossary import TermStatus
|
|
151
|
+
|
|
152
|
+
term_summary = None
|
|
153
|
+
if column.term:
|
|
154
|
+
term_summary = TermSummary(
|
|
155
|
+
id=column.term.id,
|
|
156
|
+
name=column.term.name,
|
|
157
|
+
status=TermStatus(column.term.status),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
sensitivity = None
|
|
161
|
+
if column.sensitivity_level:
|
|
162
|
+
sensitivity = SensitivityLevel(column.sensitivity_level)
|
|
163
|
+
|
|
164
|
+
return cls(
|
|
165
|
+
id=column.id,
|
|
166
|
+
asset_id=column.asset_id,
|
|
167
|
+
name=column.name,
|
|
168
|
+
data_type=column.data_type,
|
|
169
|
+
description=column.description,
|
|
170
|
+
is_nullable=column.is_nullable,
|
|
171
|
+
is_primary_key=column.is_primary_key,
|
|
172
|
+
term_id=column.term_id,
|
|
173
|
+
term=term_summary,
|
|
174
|
+
sensitivity_level=sensitivity,
|
|
175
|
+
is_sensitive=column.is_sensitive,
|
|
176
|
+
has_term_mapping=column.has_term_mapping,
|
|
177
|
+
created_at=column.created_at,
|
|
178
|
+
updated_at=column.updated_at,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class ColumnListResponse(ListResponseWrapper[ColumnResponse]):
|
|
183
|
+
"""List of columns."""
|
|
184
|
+
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# =============================================================================
|
|
189
|
+
# Asset Schemas
|
|
190
|
+
# =============================================================================
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class SourceSummary(BaseSchema, IDMixin):
|
|
194
|
+
"""Summary schema for data source references."""
|
|
195
|
+
|
|
196
|
+
name: str
|
|
197
|
+
type: str
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class AssetBase(BaseSchema):
|
|
201
|
+
"""Base schema for catalog assets."""
|
|
202
|
+
|
|
203
|
+
name: str = Field(..., min_length=1, max_length=255, description="Asset name")
|
|
204
|
+
asset_type: AssetType = Field(
|
|
205
|
+
default=AssetType.TABLE,
|
|
206
|
+
description="Asset type",
|
|
207
|
+
)
|
|
208
|
+
source_id: str | None = Field(None, description="Linked data source ID")
|
|
209
|
+
description: str | None = Field(None, description="Asset description")
|
|
210
|
+
owner_id: str | None = Field(None, description="Owner identifier")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class AssetCreate(AssetBase):
|
|
214
|
+
"""Schema for creating a catalog asset."""
|
|
215
|
+
|
|
216
|
+
columns: list[ColumnCreate] = Field(
|
|
217
|
+
default_factory=list,
|
|
218
|
+
description="Initial columns to create",
|
|
219
|
+
)
|
|
220
|
+
tags: list[TagCreate] = Field(
|
|
221
|
+
default_factory=list,
|
|
222
|
+
description="Initial tags to add",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
@field_validator("name")
|
|
226
|
+
@classmethod
|
|
227
|
+
def validate_name(cls, v: str) -> str:
|
|
228
|
+
"""Validate asset name."""
|
|
229
|
+
return v.strip()
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class AssetUpdate(BaseSchema):
|
|
233
|
+
"""Schema for updating a catalog asset."""
|
|
234
|
+
|
|
235
|
+
name: str | None = Field(None, min_length=1, max_length=255)
|
|
236
|
+
asset_type: AssetType | None = None
|
|
237
|
+
source_id: str | None = None
|
|
238
|
+
description: str | None = None
|
|
239
|
+
owner_id: str | None = None
|
|
240
|
+
quality_score: float | None = Field(None, ge=0, le=100)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class AssetResponse(BaseSchema, IDMixin, TimestampMixin):
|
|
244
|
+
"""Response schema for a catalog asset."""
|
|
245
|
+
|
|
246
|
+
name: str
|
|
247
|
+
asset_type: AssetType
|
|
248
|
+
source_id: str | None
|
|
249
|
+
source: SourceSummary | None = None
|
|
250
|
+
description: str | None
|
|
251
|
+
owner_id: str | None
|
|
252
|
+
quality_score: float | None
|
|
253
|
+
quality_level: QualityLevel = QualityLevel.UNKNOWN
|
|
254
|
+
column_count: int = 0
|
|
255
|
+
columns: list[ColumnResponse] = Field(default_factory=list)
|
|
256
|
+
tags: list[TagResponse] = Field(default_factory=list)
|
|
257
|
+
|
|
258
|
+
@classmethod
|
|
259
|
+
def from_model(cls, asset: any, include_columns: bool = True) -> AssetResponse:
|
|
260
|
+
"""Create response from model."""
|
|
261
|
+
source_summary = None
|
|
262
|
+
if asset.source:
|
|
263
|
+
source_summary = SourceSummary(
|
|
264
|
+
id=asset.source.id,
|
|
265
|
+
name=asset.source.name,
|
|
266
|
+
type=asset.source.type,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
columns = []
|
|
270
|
+
if include_columns:
|
|
271
|
+
columns = [ColumnResponse.from_model(c) for c in asset.columns]
|
|
272
|
+
|
|
273
|
+
tags = [TagResponse.from_model(t) for t in asset.tags]
|
|
274
|
+
|
|
275
|
+
quality_level = QualityLevel.UNKNOWN
|
|
276
|
+
if asset.quality_score is not None:
|
|
277
|
+
if asset.quality_score >= 90:
|
|
278
|
+
quality_level = QualityLevel.EXCELLENT
|
|
279
|
+
elif asset.quality_score >= 70:
|
|
280
|
+
quality_level = QualityLevel.GOOD
|
|
281
|
+
elif asset.quality_score >= 50:
|
|
282
|
+
quality_level = QualityLevel.FAIR
|
|
283
|
+
else:
|
|
284
|
+
quality_level = QualityLevel.POOR
|
|
285
|
+
|
|
286
|
+
return cls(
|
|
287
|
+
id=asset.id,
|
|
288
|
+
name=asset.name,
|
|
289
|
+
asset_type=AssetType(asset.asset_type),
|
|
290
|
+
source_id=asset.source_id,
|
|
291
|
+
source=source_summary,
|
|
292
|
+
description=asset.description,
|
|
293
|
+
owner_id=asset.owner_id,
|
|
294
|
+
quality_score=asset.quality_score,
|
|
295
|
+
quality_level=quality_level,
|
|
296
|
+
column_count=asset.column_count,
|
|
297
|
+
columns=columns,
|
|
298
|
+
tags=tags,
|
|
299
|
+
created_at=asset.created_at,
|
|
300
|
+
updated_at=asset.updated_at,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class AssetListItem(BaseSchema, IDMixin, TimestampMixin):
|
|
305
|
+
"""List item schema for assets (lighter than full response)."""
|
|
306
|
+
|
|
307
|
+
name: str
|
|
308
|
+
asset_type: AssetType
|
|
309
|
+
source_id: str | None
|
|
310
|
+
source_name: str | None = None
|
|
311
|
+
description: str | None
|
|
312
|
+
owner_id: str | None
|
|
313
|
+
quality_score: float | None
|
|
314
|
+
quality_level: QualityLevel = QualityLevel.UNKNOWN
|
|
315
|
+
column_count: int = 0
|
|
316
|
+
tag_names: list[str] = Field(default_factory=list)
|
|
317
|
+
|
|
318
|
+
@classmethod
|
|
319
|
+
def from_model(cls, asset: any) -> AssetListItem:
|
|
320
|
+
"""Create list item from model."""
|
|
321
|
+
quality_level = QualityLevel.UNKNOWN
|
|
322
|
+
if asset.quality_score is not None:
|
|
323
|
+
if asset.quality_score >= 90:
|
|
324
|
+
quality_level = QualityLevel.EXCELLENT
|
|
325
|
+
elif asset.quality_score >= 70:
|
|
326
|
+
quality_level = QualityLevel.GOOD
|
|
327
|
+
elif asset.quality_score >= 50:
|
|
328
|
+
quality_level = QualityLevel.FAIR
|
|
329
|
+
else:
|
|
330
|
+
quality_level = QualityLevel.POOR
|
|
331
|
+
|
|
332
|
+
return cls(
|
|
333
|
+
id=asset.id,
|
|
334
|
+
name=asset.name,
|
|
335
|
+
asset_type=AssetType(asset.asset_type),
|
|
336
|
+
source_id=asset.source_id,
|
|
337
|
+
source_name=asset.source.name if asset.source else None,
|
|
338
|
+
description=asset.description,
|
|
339
|
+
owner_id=asset.owner_id,
|
|
340
|
+
quality_score=asset.quality_score,
|
|
341
|
+
quality_level=quality_level,
|
|
342
|
+
column_count=asset.column_count,
|
|
343
|
+
tag_names=asset.tag_names,
|
|
344
|
+
created_at=asset.created_at,
|
|
345
|
+
updated_at=asset.updated_at,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class AssetListResponse(ListResponseWrapper[AssetListItem]):
|
|
350
|
+
"""Paginated list of assets."""
|
|
351
|
+
|
|
352
|
+
pass
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Pydantic schemas for Collaboration API.
|
|
2
|
+
|
|
3
|
+
This module defines request/response schemas for comments
|
|
4
|
+
and activity logs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pydantic import Field, field_validator
|
|
14
|
+
|
|
15
|
+
from .base import BaseSchema, IDMixin, ListResponseWrapper, TimestampMixin
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# =============================================================================
|
|
19
|
+
# Enums
|
|
20
|
+
# =============================================================================
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ResourceType(str, Enum):
|
|
24
|
+
"""Type of resource for comments and activities."""
|
|
25
|
+
|
|
26
|
+
TERM = "term"
|
|
27
|
+
CATEGORY = "category"
|
|
28
|
+
ASSET = "asset"
|
|
29
|
+
COLUMN = "column"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ActivityAction(str, Enum):
|
|
33
|
+
"""Type of activity action."""
|
|
34
|
+
|
|
35
|
+
CREATED = "created"
|
|
36
|
+
UPDATED = "updated"
|
|
37
|
+
DELETED = "deleted"
|
|
38
|
+
COMMENTED = "commented"
|
|
39
|
+
STATUS_CHANGED = "status_changed"
|
|
40
|
+
MAPPED = "mapped"
|
|
41
|
+
UNMAPPED = "unmapped"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# =============================================================================
|
|
45
|
+
# Comment Schemas
|
|
46
|
+
# =============================================================================
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CommentBase(BaseSchema):
|
|
50
|
+
"""Base schema for comments."""
|
|
51
|
+
|
|
52
|
+
content: str = Field(..., min_length=1, max_length=10000, description="Comment content")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class CommentCreate(CommentBase):
|
|
56
|
+
"""Schema for creating a comment."""
|
|
57
|
+
|
|
58
|
+
resource_type: ResourceType = Field(..., description="Type of resource")
|
|
59
|
+
resource_id: str = Field(..., description="ID of the resource")
|
|
60
|
+
parent_id: str | None = Field(None, description="Parent comment ID for replies")
|
|
61
|
+
author_id: str | None = Field(None, description="Author identifier")
|
|
62
|
+
|
|
63
|
+
@field_validator("content")
|
|
64
|
+
@classmethod
|
|
65
|
+
def validate_content(cls, v: str) -> str:
|
|
66
|
+
"""Validate comment content."""
|
|
67
|
+
return v.strip()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class CommentUpdate(BaseSchema):
|
|
71
|
+
"""Schema for updating a comment."""
|
|
72
|
+
|
|
73
|
+
content: str = Field(..., min_length=1, max_length=10000)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class CommentResponse(BaseSchema, IDMixin, TimestampMixin):
|
|
77
|
+
"""Response schema for a comment."""
|
|
78
|
+
|
|
79
|
+
resource_type: ResourceType
|
|
80
|
+
resource_id: str
|
|
81
|
+
content: str
|
|
82
|
+
author_id: str | None
|
|
83
|
+
parent_id: str | None
|
|
84
|
+
is_reply: bool = False
|
|
85
|
+
reply_count: int = 0
|
|
86
|
+
replies: list[CommentResponse] = Field(default_factory=list)
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def from_model(cls, comment: any, include_replies: bool = True) -> CommentResponse:
|
|
90
|
+
"""Create response from model."""
|
|
91
|
+
replies = []
|
|
92
|
+
if include_replies and comment.replies:
|
|
93
|
+
replies = [
|
|
94
|
+
CommentResponse.from_model(r, include_replies=False)
|
|
95
|
+
for r in comment.replies
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
return cls(
|
|
99
|
+
id=comment.id,
|
|
100
|
+
resource_type=ResourceType(comment.resource_type),
|
|
101
|
+
resource_id=comment.resource_id,
|
|
102
|
+
content=comment.content,
|
|
103
|
+
author_id=comment.author_id,
|
|
104
|
+
parent_id=comment.parent_id,
|
|
105
|
+
is_reply=comment.is_reply,
|
|
106
|
+
reply_count=comment.reply_count,
|
|
107
|
+
replies=replies,
|
|
108
|
+
created_at=comment.created_at,
|
|
109
|
+
updated_at=comment.updated_at,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class CommentListResponse(ListResponseWrapper[CommentResponse]):
|
|
114
|
+
"""List of comments."""
|
|
115
|
+
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# =============================================================================
|
|
120
|
+
# Activity Schemas
|
|
121
|
+
# =============================================================================
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ActivityResponse(BaseSchema, IDMixin):
|
|
125
|
+
"""Response schema for an activity log entry."""
|
|
126
|
+
|
|
127
|
+
resource_type: ResourceType
|
|
128
|
+
resource_id: str
|
|
129
|
+
action: ActivityAction
|
|
130
|
+
actor_id: str | None
|
|
131
|
+
description: str | None
|
|
132
|
+
metadata: dict[str, Any] | None = None
|
|
133
|
+
created_at: datetime
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def from_model(cls, activity: any) -> ActivityResponse:
|
|
137
|
+
"""Create response from model."""
|
|
138
|
+
return cls(
|
|
139
|
+
id=activity.id,
|
|
140
|
+
resource_type=ResourceType(activity.resource_type),
|
|
141
|
+
resource_id=activity.resource_id,
|
|
142
|
+
action=ActivityAction(activity.action),
|
|
143
|
+
actor_id=activity.actor_id,
|
|
144
|
+
description=activity.description,
|
|
145
|
+
metadata=activity.metadata,
|
|
146
|
+
created_at=activity.created_at,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class ActivityListResponse(ListResponseWrapper[ActivityResponse]):
|
|
151
|
+
"""List of activities."""
|
|
152
|
+
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# =============================================================================
|
|
157
|
+
# Activity Create (Internal use)
|
|
158
|
+
# =============================================================================
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class ActivityCreate(BaseSchema):
|
|
162
|
+
"""Schema for creating an activity (internal use)."""
|
|
163
|
+
|
|
164
|
+
resource_type: ResourceType
|
|
165
|
+
resource_id: str
|
|
166
|
+
action: ActivityAction
|
|
167
|
+
actor_id: str | None = None
|
|
168
|
+
description: str | None = None
|
|
169
|
+
metadata: dict[str, Any] | None = None
|