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
truthound_dashboard/db/models.py
CHANGED
|
@@ -12,6 +12,17 @@ Models:
|
|
|
12
12
|
- NotificationChannel: Notification channel configuration
|
|
13
13
|
- NotificationRule: Notification trigger rules
|
|
14
14
|
- NotificationLog: Notification delivery log
|
|
15
|
+
|
|
16
|
+
Phase 5 Models:
|
|
17
|
+
- GlossaryCategory: Business term categories
|
|
18
|
+
- GlossaryTerm: Business glossary terms
|
|
19
|
+
- TermRelationship: Relationships between terms
|
|
20
|
+
- TermHistory: Term change history
|
|
21
|
+
- CatalogAsset: Data catalog assets
|
|
22
|
+
- AssetColumn: Asset column metadata
|
|
23
|
+
- AssetTag: Asset tags
|
|
24
|
+
- Comment: Comments on resources
|
|
25
|
+
- Activity: Activity log
|
|
15
26
|
"""
|
|
16
27
|
|
|
17
28
|
from __future__ import annotations
|
|
@@ -19,11 +30,14 @@ from __future__ import annotations
|
|
|
19
30
|
from datetime import datetime
|
|
20
31
|
from typing import Any
|
|
21
32
|
|
|
33
|
+
from enum import Enum
|
|
34
|
+
|
|
22
35
|
from sqlalchemy import (
|
|
23
36
|
JSON,
|
|
24
37
|
Boolean,
|
|
25
38
|
DateTime,
|
|
26
39
|
Enum as SQLEnum,
|
|
40
|
+
Float,
|
|
27
41
|
ForeignKey,
|
|
28
42
|
Index,
|
|
29
43
|
Integer,
|
|
@@ -35,6 +49,66 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
35
49
|
from .base import Base, TimestampMixin, UUIDMixin
|
|
36
50
|
|
|
37
51
|
|
|
52
|
+
# =============================================================================
|
|
53
|
+
# Phase 5: Enums
|
|
54
|
+
# =============================================================================
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TermStatus(str, Enum):
|
|
58
|
+
"""Status of a glossary term."""
|
|
59
|
+
|
|
60
|
+
DRAFT = "draft"
|
|
61
|
+
APPROVED = "approved"
|
|
62
|
+
DEPRECATED = "deprecated"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class RelationshipType(str, Enum):
|
|
66
|
+
"""Type of relationship between terms."""
|
|
67
|
+
|
|
68
|
+
SYNONYM = "synonym"
|
|
69
|
+
RELATED = "related"
|
|
70
|
+
PARENT = "parent"
|
|
71
|
+
CHILD = "child"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class AssetType(str, Enum):
|
|
75
|
+
"""Type of catalog asset."""
|
|
76
|
+
|
|
77
|
+
TABLE = "table"
|
|
78
|
+
FILE = "file"
|
|
79
|
+
API = "api"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class SensitivityLevel(str, Enum):
|
|
83
|
+
"""Sensitivity level for data columns."""
|
|
84
|
+
|
|
85
|
+
PUBLIC = "public"
|
|
86
|
+
INTERNAL = "internal"
|
|
87
|
+
CONFIDENTIAL = "confidential"
|
|
88
|
+
RESTRICTED = "restricted"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ResourceType(str, Enum):
|
|
92
|
+
"""Type of resource for comments and activities."""
|
|
93
|
+
|
|
94
|
+
TERM = "term"
|
|
95
|
+
CATEGORY = "category"
|
|
96
|
+
ASSET = "asset"
|
|
97
|
+
COLUMN = "column"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class ActivityAction(str, Enum):
|
|
101
|
+
"""Type of activity action."""
|
|
102
|
+
|
|
103
|
+
CREATED = "created"
|
|
104
|
+
UPDATED = "updated"
|
|
105
|
+
DELETED = "deleted"
|
|
106
|
+
COMMENTED = "commented"
|
|
107
|
+
STATUS_CHANGED = "status_changed"
|
|
108
|
+
MAPPED = "mapped"
|
|
109
|
+
UNMAPPED = "unmapped"
|
|
110
|
+
|
|
111
|
+
|
|
38
112
|
class Source(Base, UUIDMixin, TimestampMixin):
|
|
39
113
|
"""Data source model.
|
|
40
114
|
|
|
@@ -98,6 +172,12 @@ class Source(Base, UUIDMixin, TimestampMixin):
|
|
|
98
172
|
cascade="all, delete-orphan",
|
|
99
173
|
lazy="selectin",
|
|
100
174
|
)
|
|
175
|
+
# Phase 5: Catalog assets linked to this source
|
|
176
|
+
assets: Mapped[list[CatalogAsset]] = relationship(
|
|
177
|
+
"CatalogAsset",
|
|
178
|
+
back_populates="source",
|
|
179
|
+
lazy="selectin",
|
|
180
|
+
)
|
|
101
181
|
|
|
102
182
|
@property
|
|
103
183
|
def source_path(self) -> str | None:
|
|
@@ -730,3 +810,616 @@ class NotificationLog(Base, UUIDMixin):
|
|
|
730
810
|
self.status = "failed"
|
|
731
811
|
self.error_message = error
|
|
732
812
|
self.sent_at = datetime.utcnow()
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
# =============================================================================
|
|
816
|
+
# Phase 5: Business Glossary Models
|
|
817
|
+
# =============================================================================
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
class GlossaryCategory(Base, UUIDMixin, TimestampMixin):
|
|
821
|
+
"""Business glossary category model.
|
|
822
|
+
|
|
823
|
+
Provides hierarchical categorization for business terms.
|
|
824
|
+
Categories can be nested (parent-child relationships).
|
|
825
|
+
|
|
826
|
+
Attributes:
|
|
827
|
+
id: Unique identifier (UUID).
|
|
828
|
+
name: Category name (unique).
|
|
829
|
+
description: Optional category description.
|
|
830
|
+
parent_id: Optional parent category ID for hierarchy.
|
|
831
|
+
"""
|
|
832
|
+
|
|
833
|
+
__tablename__ = "glossary_categories"
|
|
834
|
+
|
|
835
|
+
name: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
|
|
836
|
+
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
837
|
+
parent_id: Mapped[str | None] = mapped_column(
|
|
838
|
+
String(36),
|
|
839
|
+
ForeignKey("glossary_categories.id", ondelete="SET NULL"),
|
|
840
|
+
nullable=True,
|
|
841
|
+
index=True,
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
# Relationships
|
|
845
|
+
parent: Mapped[GlossaryCategory | None] = relationship(
|
|
846
|
+
"GlossaryCategory",
|
|
847
|
+
remote_side="GlossaryCategory.id",
|
|
848
|
+
back_populates="children",
|
|
849
|
+
)
|
|
850
|
+
children: Mapped[list[GlossaryCategory]] = relationship(
|
|
851
|
+
"GlossaryCategory",
|
|
852
|
+
back_populates="parent",
|
|
853
|
+
lazy="selectin",
|
|
854
|
+
)
|
|
855
|
+
terms: Mapped[list[GlossaryTerm]] = relationship(
|
|
856
|
+
"GlossaryTerm",
|
|
857
|
+
back_populates="category",
|
|
858
|
+
lazy="selectin",
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
@property
|
|
862
|
+
def term_count(self) -> int:
|
|
863
|
+
"""Get number of terms in this category."""
|
|
864
|
+
return len(self.terms)
|
|
865
|
+
|
|
866
|
+
@property
|
|
867
|
+
def full_path(self) -> str:
|
|
868
|
+
"""Get full category path (e.g., 'Parent > Child')."""
|
|
869
|
+
if self.parent:
|
|
870
|
+
return f"{self.parent.full_path} > {self.name}"
|
|
871
|
+
return self.name
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
class GlossaryTerm(Base, UUIDMixin, TimestampMixin):
|
|
875
|
+
"""Business glossary term model.
|
|
876
|
+
|
|
877
|
+
Represents a business term with its definition, status, and relationships.
|
|
878
|
+
|
|
879
|
+
Attributes:
|
|
880
|
+
id: Unique identifier (UUID).
|
|
881
|
+
name: Term name (unique).
|
|
882
|
+
definition: Term definition (required).
|
|
883
|
+
category_id: Optional category ID.
|
|
884
|
+
status: Term status (draft, approved, deprecated).
|
|
885
|
+
owner_id: Optional owner identifier.
|
|
886
|
+
"""
|
|
887
|
+
|
|
888
|
+
__tablename__ = "glossary_terms"
|
|
889
|
+
|
|
890
|
+
# Composite index for efficient search queries
|
|
891
|
+
__table_args__ = (
|
|
892
|
+
Index("idx_glossary_terms_name_status", "name", "status"),
|
|
893
|
+
Index("idx_glossary_terms_category", "category_id"),
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
name: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
|
|
897
|
+
definition: Mapped[str] = mapped_column(Text, nullable=False)
|
|
898
|
+
category_id: Mapped[str | None] = mapped_column(
|
|
899
|
+
String(36),
|
|
900
|
+
ForeignKey("glossary_categories.id", ondelete="SET NULL"),
|
|
901
|
+
nullable=True,
|
|
902
|
+
index=True,
|
|
903
|
+
)
|
|
904
|
+
status: Mapped[str] = mapped_column(
|
|
905
|
+
String(20),
|
|
906
|
+
nullable=False,
|
|
907
|
+
default=TermStatus.DRAFT.value,
|
|
908
|
+
index=True,
|
|
909
|
+
)
|
|
910
|
+
owner_id: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
911
|
+
|
|
912
|
+
# Relationships
|
|
913
|
+
category: Mapped[GlossaryCategory | None] = relationship(
|
|
914
|
+
"GlossaryCategory",
|
|
915
|
+
back_populates="terms",
|
|
916
|
+
)
|
|
917
|
+
history: Mapped[list[TermHistory]] = relationship(
|
|
918
|
+
"TermHistory",
|
|
919
|
+
back_populates="term",
|
|
920
|
+
cascade="all, delete-orphan",
|
|
921
|
+
lazy="selectin",
|
|
922
|
+
order_by="desc(TermHistory.changed_at)",
|
|
923
|
+
)
|
|
924
|
+
# Relationships where this term is the source
|
|
925
|
+
outgoing_relationships: Mapped[list[TermRelationship]] = relationship(
|
|
926
|
+
"TermRelationship",
|
|
927
|
+
foreign_keys="TermRelationship.source_term_id",
|
|
928
|
+
back_populates="source_term",
|
|
929
|
+
cascade="all, delete-orphan",
|
|
930
|
+
lazy="selectin",
|
|
931
|
+
)
|
|
932
|
+
# Relationships where this term is the target
|
|
933
|
+
incoming_relationships: Mapped[list[TermRelationship]] = relationship(
|
|
934
|
+
"TermRelationship",
|
|
935
|
+
foreign_keys="TermRelationship.target_term_id",
|
|
936
|
+
back_populates="target_term",
|
|
937
|
+
cascade="all, delete-orphan",
|
|
938
|
+
lazy="selectin",
|
|
939
|
+
)
|
|
940
|
+
# Columns mapped to this term
|
|
941
|
+
mapped_columns: Mapped[list[AssetColumn]] = relationship(
|
|
942
|
+
"AssetColumn",
|
|
943
|
+
back_populates="term",
|
|
944
|
+
lazy="selectin",
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
@property
|
|
948
|
+
def synonyms(self) -> list[GlossaryTerm]:
|
|
949
|
+
"""Get all synonym terms."""
|
|
950
|
+
result = []
|
|
951
|
+
for rel in self.outgoing_relationships:
|
|
952
|
+
if rel.relationship_type == RelationshipType.SYNONYM.value:
|
|
953
|
+
result.append(rel.target_term)
|
|
954
|
+
for rel in self.incoming_relationships:
|
|
955
|
+
if rel.relationship_type == RelationshipType.SYNONYM.value:
|
|
956
|
+
result.append(rel.source_term)
|
|
957
|
+
return result
|
|
958
|
+
|
|
959
|
+
@property
|
|
960
|
+
def related_terms(self) -> list[GlossaryTerm]:
|
|
961
|
+
"""Get all related terms (non-synonym relationships)."""
|
|
962
|
+
result = []
|
|
963
|
+
for rel in self.outgoing_relationships:
|
|
964
|
+
if rel.relationship_type == RelationshipType.RELATED.value:
|
|
965
|
+
result.append(rel.target_term)
|
|
966
|
+
for rel in self.incoming_relationships:
|
|
967
|
+
if rel.relationship_type == RelationshipType.RELATED.value:
|
|
968
|
+
result.append(rel.source_term)
|
|
969
|
+
return result
|
|
970
|
+
|
|
971
|
+
@property
|
|
972
|
+
def is_approved(self) -> bool:
|
|
973
|
+
"""Check if term is approved."""
|
|
974
|
+
return self.status == TermStatus.APPROVED.value
|
|
975
|
+
|
|
976
|
+
@property
|
|
977
|
+
def is_deprecated(self) -> bool:
|
|
978
|
+
"""Check if term is deprecated."""
|
|
979
|
+
return self.status == TermStatus.DEPRECATED.value
|
|
980
|
+
|
|
981
|
+
def approve(self) -> None:
|
|
982
|
+
"""Approve this term."""
|
|
983
|
+
self.status = TermStatus.APPROVED.value
|
|
984
|
+
|
|
985
|
+
def deprecate(self) -> None:
|
|
986
|
+
"""Mark this term as deprecated."""
|
|
987
|
+
self.status = TermStatus.DEPRECATED.value
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
class TermRelationship(Base, UUIDMixin):
|
|
991
|
+
"""Relationship between glossary terms.
|
|
992
|
+
|
|
993
|
+
Represents directional relationships between terms such as
|
|
994
|
+
synonyms, related terms, or parent-child relationships.
|
|
995
|
+
|
|
996
|
+
Attributes:
|
|
997
|
+
id: Unique identifier (UUID).
|
|
998
|
+
source_term_id: Source term ID.
|
|
999
|
+
target_term_id: Target term ID.
|
|
1000
|
+
relationship_type: Type of relationship.
|
|
1001
|
+
created_at: When the relationship was created.
|
|
1002
|
+
"""
|
|
1003
|
+
|
|
1004
|
+
__tablename__ = "term_relationships"
|
|
1005
|
+
|
|
1006
|
+
# Unique constraint to prevent duplicate relationships
|
|
1007
|
+
__table_args__ = (
|
|
1008
|
+
Index(
|
|
1009
|
+
"idx_term_relationships_unique",
|
|
1010
|
+
"source_term_id",
|
|
1011
|
+
"target_term_id",
|
|
1012
|
+
"relationship_type",
|
|
1013
|
+
unique=True,
|
|
1014
|
+
),
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
source_term_id: Mapped[str] = mapped_column(
|
|
1018
|
+
String(36),
|
|
1019
|
+
ForeignKey("glossary_terms.id", ondelete="CASCADE"),
|
|
1020
|
+
nullable=False,
|
|
1021
|
+
index=True,
|
|
1022
|
+
)
|
|
1023
|
+
target_term_id: Mapped[str] = mapped_column(
|
|
1024
|
+
String(36),
|
|
1025
|
+
ForeignKey("glossary_terms.id", ondelete="CASCADE"),
|
|
1026
|
+
nullable=False,
|
|
1027
|
+
index=True,
|
|
1028
|
+
)
|
|
1029
|
+
relationship_type: Mapped[str] = mapped_column(
|
|
1030
|
+
String(20),
|
|
1031
|
+
nullable=False,
|
|
1032
|
+
index=True,
|
|
1033
|
+
)
|
|
1034
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
1035
|
+
DateTime,
|
|
1036
|
+
default=datetime.utcnow,
|
|
1037
|
+
nullable=False,
|
|
1038
|
+
)
|
|
1039
|
+
|
|
1040
|
+
# Relationships
|
|
1041
|
+
source_term: Mapped[GlossaryTerm] = relationship(
|
|
1042
|
+
"GlossaryTerm",
|
|
1043
|
+
foreign_keys=[source_term_id],
|
|
1044
|
+
back_populates="outgoing_relationships",
|
|
1045
|
+
)
|
|
1046
|
+
target_term: Mapped[GlossaryTerm] = relationship(
|
|
1047
|
+
"GlossaryTerm",
|
|
1048
|
+
foreign_keys=[target_term_id],
|
|
1049
|
+
back_populates="incoming_relationships",
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
class TermHistory(Base, UUIDMixin):
|
|
1054
|
+
"""History of changes to glossary terms.
|
|
1055
|
+
|
|
1056
|
+
Tracks all modifications to term fields for auditing.
|
|
1057
|
+
|
|
1058
|
+
Attributes:
|
|
1059
|
+
id: Unique identifier (UUID).
|
|
1060
|
+
term_id: Reference to the term.
|
|
1061
|
+
field_name: Name of the changed field.
|
|
1062
|
+
old_value: Previous value (as string).
|
|
1063
|
+
new_value: New value (as string).
|
|
1064
|
+
changed_by: User who made the change.
|
|
1065
|
+
changed_at: When the change occurred.
|
|
1066
|
+
"""
|
|
1067
|
+
|
|
1068
|
+
__tablename__ = "term_history"
|
|
1069
|
+
|
|
1070
|
+
# Index for efficient history queries
|
|
1071
|
+
__table_args__ = (
|
|
1072
|
+
Index("idx_term_history_term_changed", "term_id", "changed_at"),
|
|
1073
|
+
)
|
|
1074
|
+
|
|
1075
|
+
term_id: Mapped[str] = mapped_column(
|
|
1076
|
+
String(36),
|
|
1077
|
+
ForeignKey("glossary_terms.id", ondelete="CASCADE"),
|
|
1078
|
+
nullable=False,
|
|
1079
|
+
index=True,
|
|
1080
|
+
)
|
|
1081
|
+
field_name: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
1082
|
+
old_value: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
1083
|
+
new_value: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
1084
|
+
changed_by: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
1085
|
+
changed_at: Mapped[datetime] = mapped_column(
|
|
1086
|
+
DateTime,
|
|
1087
|
+
default=datetime.utcnow,
|
|
1088
|
+
nullable=False,
|
|
1089
|
+
)
|
|
1090
|
+
|
|
1091
|
+
# Relationships
|
|
1092
|
+
term: Mapped[GlossaryTerm] = relationship(
|
|
1093
|
+
"GlossaryTerm",
|
|
1094
|
+
back_populates="history",
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
# =============================================================================
|
|
1099
|
+
# Phase 5: Data Catalog Models
|
|
1100
|
+
# =============================================================================
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
class CatalogAsset(Base, UUIDMixin, TimestampMixin):
|
|
1104
|
+
"""Data catalog asset model.
|
|
1105
|
+
|
|
1106
|
+
Represents a data asset (table, file, API) in the catalog.
|
|
1107
|
+
|
|
1108
|
+
Attributes:
|
|
1109
|
+
id: Unique identifier (UUID).
|
|
1110
|
+
name: Asset name.
|
|
1111
|
+
asset_type: Type of asset (table, file, api).
|
|
1112
|
+
source_id: Optional reference to data source.
|
|
1113
|
+
description: Optional asset description.
|
|
1114
|
+
owner_id: Optional owner identifier.
|
|
1115
|
+
quality_score: Computed quality score (0-100).
|
|
1116
|
+
"""
|
|
1117
|
+
|
|
1118
|
+
__tablename__ = "catalog_assets"
|
|
1119
|
+
|
|
1120
|
+
# Indexes for efficient queries
|
|
1121
|
+
__table_args__ = (
|
|
1122
|
+
Index("idx_catalog_assets_type", "asset_type"),
|
|
1123
|
+
Index("idx_catalog_assets_source", "source_id"),
|
|
1124
|
+
Index("idx_catalog_assets_name_type", "name", "asset_type"),
|
|
1125
|
+
)
|
|
1126
|
+
|
|
1127
|
+
name: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
|
|
1128
|
+
asset_type: Mapped[str] = mapped_column(
|
|
1129
|
+
String(20),
|
|
1130
|
+
nullable=False,
|
|
1131
|
+
default=AssetType.TABLE.value,
|
|
1132
|
+
)
|
|
1133
|
+
source_id: Mapped[str | None] = mapped_column(
|
|
1134
|
+
String(36),
|
|
1135
|
+
ForeignKey("sources.id", ondelete="SET NULL"),
|
|
1136
|
+
nullable=True,
|
|
1137
|
+
index=True,
|
|
1138
|
+
)
|
|
1139
|
+
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
1140
|
+
owner_id: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
1141
|
+
quality_score: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
1142
|
+
|
|
1143
|
+
# Relationships
|
|
1144
|
+
source: Mapped[Source | None] = relationship(
|
|
1145
|
+
"Source",
|
|
1146
|
+
back_populates="assets",
|
|
1147
|
+
)
|
|
1148
|
+
columns: Mapped[list[AssetColumn]] = relationship(
|
|
1149
|
+
"AssetColumn",
|
|
1150
|
+
back_populates="asset",
|
|
1151
|
+
cascade="all, delete-orphan",
|
|
1152
|
+
lazy="selectin",
|
|
1153
|
+
order_by="AssetColumn.name",
|
|
1154
|
+
)
|
|
1155
|
+
tags: Mapped[list[AssetTag]] = relationship(
|
|
1156
|
+
"AssetTag",
|
|
1157
|
+
back_populates="asset",
|
|
1158
|
+
cascade="all, delete-orphan",
|
|
1159
|
+
lazy="selectin",
|
|
1160
|
+
)
|
|
1161
|
+
|
|
1162
|
+
@property
|
|
1163
|
+
def column_count(self) -> int:
|
|
1164
|
+
"""Get number of columns."""
|
|
1165
|
+
return len(self.columns)
|
|
1166
|
+
|
|
1167
|
+
@property
|
|
1168
|
+
def tag_names(self) -> list[str]:
|
|
1169
|
+
"""Get list of tag names."""
|
|
1170
|
+
return [tag.tag_name for tag in self.tags]
|
|
1171
|
+
|
|
1172
|
+
@property
|
|
1173
|
+
def quality_level(self) -> str:
|
|
1174
|
+
"""Get quality level based on score."""
|
|
1175
|
+
if self.quality_score is None:
|
|
1176
|
+
return "unknown"
|
|
1177
|
+
if self.quality_score >= 90:
|
|
1178
|
+
return "excellent"
|
|
1179
|
+
if self.quality_score >= 70:
|
|
1180
|
+
return "good"
|
|
1181
|
+
if self.quality_score >= 50:
|
|
1182
|
+
return "fair"
|
|
1183
|
+
return "poor"
|
|
1184
|
+
|
|
1185
|
+
def update_quality_score(self, score: float) -> None:
|
|
1186
|
+
"""Update the quality score."""
|
|
1187
|
+
self.quality_score = min(100.0, max(0.0, score))
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
class AssetColumn(Base, UUIDMixin, TimestampMixin):
|
|
1191
|
+
"""Asset column metadata model.
|
|
1192
|
+
|
|
1193
|
+
Represents a column within a data asset with optional term mapping.
|
|
1194
|
+
|
|
1195
|
+
Attributes:
|
|
1196
|
+
id: Unique identifier (UUID).
|
|
1197
|
+
asset_id: Reference to parent asset.
|
|
1198
|
+
name: Column name.
|
|
1199
|
+
data_type: Column data type.
|
|
1200
|
+
description: Optional column description.
|
|
1201
|
+
is_nullable: Whether column allows null values.
|
|
1202
|
+
is_primary_key: Whether column is a primary key.
|
|
1203
|
+
term_id: Optional mapped glossary term ID.
|
|
1204
|
+
sensitivity_level: Data sensitivity classification.
|
|
1205
|
+
"""
|
|
1206
|
+
|
|
1207
|
+
__tablename__ = "asset_columns"
|
|
1208
|
+
|
|
1209
|
+
# Indexes for efficient queries
|
|
1210
|
+
__table_args__ = (
|
|
1211
|
+
Index("idx_asset_columns_asset", "asset_id"),
|
|
1212
|
+
Index("idx_asset_columns_term", "term_id"),
|
|
1213
|
+
Index("idx_asset_columns_sensitivity", "sensitivity_level"),
|
|
1214
|
+
)
|
|
1215
|
+
|
|
1216
|
+
asset_id: Mapped[str] = mapped_column(
|
|
1217
|
+
String(36),
|
|
1218
|
+
ForeignKey("catalog_assets.id", ondelete="CASCADE"),
|
|
1219
|
+
nullable=False,
|
|
1220
|
+
index=True,
|
|
1221
|
+
)
|
|
1222
|
+
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
1223
|
+
data_type: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
1224
|
+
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
1225
|
+
is_nullable: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
|
1226
|
+
is_primary_key: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
1227
|
+
term_id: Mapped[str | None] = mapped_column(
|
|
1228
|
+
String(36),
|
|
1229
|
+
ForeignKey("glossary_terms.id", ondelete="SET NULL"),
|
|
1230
|
+
nullable=True,
|
|
1231
|
+
index=True,
|
|
1232
|
+
)
|
|
1233
|
+
sensitivity_level: Mapped[str | None] = mapped_column(
|
|
1234
|
+
String(20),
|
|
1235
|
+
nullable=True,
|
|
1236
|
+
default=SensitivityLevel.PUBLIC.value,
|
|
1237
|
+
)
|
|
1238
|
+
|
|
1239
|
+
# Relationships
|
|
1240
|
+
asset: Mapped[CatalogAsset] = relationship(
|
|
1241
|
+
"CatalogAsset",
|
|
1242
|
+
back_populates="columns",
|
|
1243
|
+
)
|
|
1244
|
+
term: Mapped[GlossaryTerm | None] = relationship(
|
|
1245
|
+
"GlossaryTerm",
|
|
1246
|
+
back_populates="mapped_columns",
|
|
1247
|
+
)
|
|
1248
|
+
|
|
1249
|
+
@property
|
|
1250
|
+
def is_sensitive(self) -> bool:
|
|
1251
|
+
"""Check if column contains sensitive data."""
|
|
1252
|
+
if self.sensitivity_level is None:
|
|
1253
|
+
return False
|
|
1254
|
+
return self.sensitivity_level in (
|
|
1255
|
+
SensitivityLevel.CONFIDENTIAL.value,
|
|
1256
|
+
SensitivityLevel.RESTRICTED.value,
|
|
1257
|
+
)
|
|
1258
|
+
|
|
1259
|
+
@property
|
|
1260
|
+
def has_term_mapping(self) -> bool:
|
|
1261
|
+
"""Check if column is mapped to a term."""
|
|
1262
|
+
return self.term_id is not None
|
|
1263
|
+
|
|
1264
|
+
def map_to_term(self, term_id: str) -> None:
|
|
1265
|
+
"""Map this column to a glossary term."""
|
|
1266
|
+
self.term_id = term_id
|
|
1267
|
+
|
|
1268
|
+
def unmap_term(self) -> None:
|
|
1269
|
+
"""Remove term mapping from this column."""
|
|
1270
|
+
self.term_id = None
|
|
1271
|
+
|
|
1272
|
+
|
|
1273
|
+
class AssetTag(Base, UUIDMixin):
|
|
1274
|
+
"""Tag for catalog assets.
|
|
1275
|
+
|
|
1276
|
+
Provides flexible tagging for assets with optional values.
|
|
1277
|
+
|
|
1278
|
+
Attributes:
|
|
1279
|
+
id: Unique identifier (UUID).
|
|
1280
|
+
asset_id: Reference to parent asset.
|
|
1281
|
+
tag_name: Tag name/key.
|
|
1282
|
+
tag_value: Optional tag value.
|
|
1283
|
+
created_at: When the tag was created.
|
|
1284
|
+
"""
|
|
1285
|
+
|
|
1286
|
+
__tablename__ = "asset_tags"
|
|
1287
|
+
|
|
1288
|
+
# Unique constraint to prevent duplicate tags
|
|
1289
|
+
__table_args__ = (
|
|
1290
|
+
Index("idx_asset_tags_asset", "asset_id"),
|
|
1291
|
+
Index("idx_asset_tags_name", "tag_name"),
|
|
1292
|
+
Index(
|
|
1293
|
+
"idx_asset_tags_unique",
|
|
1294
|
+
"asset_id",
|
|
1295
|
+
"tag_name",
|
|
1296
|
+
unique=True,
|
|
1297
|
+
),
|
|
1298
|
+
)
|
|
1299
|
+
|
|
1300
|
+
asset_id: Mapped[str] = mapped_column(
|
|
1301
|
+
String(36),
|
|
1302
|
+
ForeignKey("catalog_assets.id", ondelete="CASCADE"),
|
|
1303
|
+
nullable=False,
|
|
1304
|
+
index=True,
|
|
1305
|
+
)
|
|
1306
|
+
tag_name: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
1307
|
+
tag_value: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
1308
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
1309
|
+
DateTime,
|
|
1310
|
+
default=datetime.utcnow,
|
|
1311
|
+
nullable=False,
|
|
1312
|
+
)
|
|
1313
|
+
|
|
1314
|
+
# Relationships
|
|
1315
|
+
asset: Mapped[CatalogAsset] = relationship(
|
|
1316
|
+
"CatalogAsset",
|
|
1317
|
+
back_populates="tags",
|
|
1318
|
+
)
|
|
1319
|
+
|
|
1320
|
+
|
|
1321
|
+
# =============================================================================
|
|
1322
|
+
# Phase 5: Collaboration Models
|
|
1323
|
+
# =============================================================================
|
|
1324
|
+
|
|
1325
|
+
|
|
1326
|
+
class Comment(Base, UUIDMixin, TimestampMixin):
|
|
1327
|
+
"""Comment on resources (terms, assets, columns).
|
|
1328
|
+
|
|
1329
|
+
Supports threaded comments with replies.
|
|
1330
|
+
|
|
1331
|
+
Attributes:
|
|
1332
|
+
id: Unique identifier (UUID).
|
|
1333
|
+
resource_type: Type of resource being commented on.
|
|
1334
|
+
resource_id: ID of the resource.
|
|
1335
|
+
content: Comment content.
|
|
1336
|
+
author_id: Optional author identifier.
|
|
1337
|
+
parent_id: Optional parent comment ID for replies.
|
|
1338
|
+
"""
|
|
1339
|
+
|
|
1340
|
+
__tablename__ = "comments"
|
|
1341
|
+
|
|
1342
|
+
# Indexes for efficient queries
|
|
1343
|
+
__table_args__ = (
|
|
1344
|
+
Index("idx_comments_resource", "resource_type", "resource_id"),
|
|
1345
|
+
Index("idx_comments_parent", "parent_id"),
|
|
1346
|
+
)
|
|
1347
|
+
|
|
1348
|
+
resource_type: Mapped[str] = mapped_column(String(20), nullable=False, index=True)
|
|
1349
|
+
resource_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
|
|
1350
|
+
content: Mapped[str] = mapped_column(Text, nullable=False)
|
|
1351
|
+
author_id: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
1352
|
+
parent_id: Mapped[str | None] = mapped_column(
|
|
1353
|
+
String(36),
|
|
1354
|
+
ForeignKey("comments.id", ondelete="CASCADE"),
|
|
1355
|
+
nullable=True,
|
|
1356
|
+
index=True,
|
|
1357
|
+
)
|
|
1358
|
+
|
|
1359
|
+
# Relationships
|
|
1360
|
+
parent: Mapped[Comment | None] = relationship(
|
|
1361
|
+
"Comment",
|
|
1362
|
+
remote_side="Comment.id",
|
|
1363
|
+
back_populates="replies",
|
|
1364
|
+
)
|
|
1365
|
+
replies: Mapped[list[Comment]] = relationship(
|
|
1366
|
+
"Comment",
|
|
1367
|
+
back_populates="parent",
|
|
1368
|
+
cascade="all, delete-orphan",
|
|
1369
|
+
lazy="selectin",
|
|
1370
|
+
order_by="Comment.created_at",
|
|
1371
|
+
)
|
|
1372
|
+
|
|
1373
|
+
@property
|
|
1374
|
+
def is_reply(self) -> bool:
|
|
1375
|
+
"""Check if this is a reply to another comment."""
|
|
1376
|
+
return self.parent_id is not None
|
|
1377
|
+
|
|
1378
|
+
@property
|
|
1379
|
+
def reply_count(self) -> int:
|
|
1380
|
+
"""Get number of direct replies."""
|
|
1381
|
+
return len(self.replies)
|
|
1382
|
+
|
|
1383
|
+
|
|
1384
|
+
class Activity(Base, UUIDMixin):
|
|
1385
|
+
"""Activity log for tracking changes.
|
|
1386
|
+
|
|
1387
|
+
Records all significant actions on resources for audit trail.
|
|
1388
|
+
|
|
1389
|
+
Attributes:
|
|
1390
|
+
id: Unique identifier (UUID).
|
|
1391
|
+
resource_type: Type of resource.
|
|
1392
|
+
resource_id: ID of the resource.
|
|
1393
|
+
action: Type of action performed.
|
|
1394
|
+
actor_id: User who performed the action.
|
|
1395
|
+
description: Human-readable description.
|
|
1396
|
+
metadata: Additional action metadata as JSON.
|
|
1397
|
+
created_at: When the activity occurred.
|
|
1398
|
+
"""
|
|
1399
|
+
|
|
1400
|
+
__tablename__ = "activities"
|
|
1401
|
+
|
|
1402
|
+
# Indexes for efficient queries
|
|
1403
|
+
__table_args__ = (
|
|
1404
|
+
Index("idx_activities_resource", "resource_type", "resource_id"),
|
|
1405
|
+
Index("idx_activities_action", "action"),
|
|
1406
|
+
Index("idx_activities_created", "created_at"),
|
|
1407
|
+
)
|
|
1408
|
+
|
|
1409
|
+
resource_type: Mapped[str] = mapped_column(String(20), nullable=False, index=True)
|
|
1410
|
+
resource_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
|
|
1411
|
+
action: Mapped[str] = mapped_column(String(30), nullable=False, index=True)
|
|
1412
|
+
actor_id: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
1413
|
+
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
1414
|
+
metadata: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
|
|
1415
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
1416
|
+
DateTime,
|
|
1417
|
+
default=datetime.utcnow,
|
|
1418
|
+
nullable=False,
|
|
1419
|
+
index=True,
|
|
1420
|
+
)
|
|
1421
|
+
|
|
1422
|
+
@property
|
|
1423
|
+
def resource_key(self) -> str:
|
|
1424
|
+
"""Get unique resource key."""
|
|
1425
|
+
return f"{self.resource_type}:{self.resource_id}"
|