ethyca-fides 2.71.0rc4__py2.py3-none-any.whl → 2.71.1b0__py2.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.
Potentially problematic release.
This version of ethyca-fides might be problematic. Click here for more details.
- {ethyca_fides-2.71.0rc4.dist-info → ethyca_fides-2.71.1b0.dist-info}/METADATA +2 -2
- {ethyca_fides-2.71.0rc4.dist-info → ethyca_fides-2.71.1b0.dist-info}/RECORD +108 -107
- fides/_version.py +3 -3
- fides/api/alembic/migrations/versions/918aefc950c9_create_digest_conditional_dependencies.py +125 -0
- fides/api/api/v1/endpoints/generic_overrides.py +9 -3
- fides/api/db/base.py +1 -1
- fides/api/models/conditional_dependency/conditional_dependency_base.py +253 -24
- fides/api/models/digest/__init__.py +5 -1
- fides/api/models/digest/conditional_dependencies.py +267 -1
- fides/api/models/digest/digest_config.py +34 -9
- fides/api/models/fides_user.py +9 -0
- fides/api/models/manual_task/conditional_dependency.py +16 -18
- fides/api/task/manual/manual_task_conditional_evaluation.py +1 -1
- fides/service/dataset/dataset_service.py +39 -0
- fides/service/privacy_request/privacy_request_service.py +103 -48
- fides/ui-build/static/admin/404.html +1 -1
- fides/ui-build/static/admin/_next/static/{kdnucJIsIefS6ViqY-8w3 → IPOgh7BMBX7b_r8-scpgv}/_buildManifest.js +1 -1
- fides/ui-build/static/admin/_next/static/chunks/{3585-efd5d41f08e180c4.js → 3585-f728d32fda6f1ac1.js} +1 -1
- fides/ui-build/static/admin/add-systems/manual.html +1 -1
- fides/ui-build/static/admin/add-systems/multiple.html +1 -1
- fides/ui-build/static/admin/add-systems.html +1 -1
- fides/ui-build/static/admin/consent/configure/add-vendors.html +1 -1
- fides/ui-build/static/admin/consent/configure.html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience/[id].html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience/new.html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience.html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices/[id].html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices/new.html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices.html +1 -1
- fides/ui-build/static/admin/consent/properties.html +1 -1
- fides/ui-build/static/admin/consent/reporting.html +1 -1
- fides/ui-build/static/admin/consent.html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn]/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects.html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/resources/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/resources.html +1 -1
- fides/ui-build/static/admin/data-catalog.html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center/[monitorId]/[systemId].html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center/[monitorId].html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center.html +1 -1
- fides/ui-build/static/admin/data-discovery/activity.html +1 -1
- fides/ui-build/static/admin/data-discovery/detection/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-discovery/detection.html +1 -1
- fides/ui-build/static/admin/data-discovery/discovery/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-discovery/discovery.html +1 -1
- fides/ui-build/static/admin/datamap.html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId]/[collectionName]/[...subfieldNames].html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId]/[collectionName].html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId].html +1 -1
- fides/ui-build/static/admin/dataset/new.html +1 -1
- fides/ui-build/static/admin/dataset.html +1 -1
- fides/ui-build/static/admin/datastore-connection/[id].html +1 -1
- fides/ui-build/static/admin/datastore-connection/new.html +1 -1
- fides/ui-build/static/admin/datastore-connection.html +1 -1
- fides/ui-build/static/admin/index.html +1 -1
- fides/ui-build/static/admin/integrations/[id].html +1 -1
- fides/ui-build/static/admin/integrations.html +1 -1
- fides/ui-build/static/admin/login/[provider].html +1 -1
- fides/ui-build/static/admin/login.html +1 -1
- fides/ui-build/static/admin/messaging/[id].html +1 -1
- fides/ui-build/static/admin/messaging/add-template.html +1 -1
- fides/ui-build/static/admin/messaging.html +1 -1
- fides/ui-build/static/admin/poc/ant-components.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/AntForm.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikAntFormItem.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikControlled.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikField.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikSpreadField.html +1 -1
- fides/ui-build/static/admin/poc/forms.html +1 -1
- fides/ui-build/static/admin/poc/table-migration.html +1 -1
- fides/ui-build/static/admin/privacy-requests/[id].html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure/storage.html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure.html +1 -1
- fides/ui-build/static/admin/privacy-requests.html +1 -1
- fides/ui-build/static/admin/properties/[id].html +1 -1
- fides/ui-build/static/admin/properties/add-property.html +1 -1
- fides/ui-build/static/admin/properties.html +1 -1
- fides/ui-build/static/admin/reporting/datamap.html +1 -1
- fides/ui-build/static/admin/settings/about/alpha.html +1 -1
- fides/ui-build/static/admin/settings/about.html +1 -1
- fides/ui-build/static/admin/settings/consent/[configuration_id]/[purpose_id].html +1 -1
- fides/ui-build/static/admin/settings/consent.html +1 -1
- fides/ui-build/static/admin/settings/custom-fields/[id].html +1 -1
- fides/ui-build/static/admin/settings/custom-fields/new.html +1 -1
- fides/ui-build/static/admin/settings/custom-fields.html +1 -1
- fides/ui-build/static/admin/settings/domain-records.html +1 -1
- fides/ui-build/static/admin/settings/domains.html +1 -1
- fides/ui-build/static/admin/settings/email-templates.html +1 -1
- fides/ui-build/static/admin/settings/locations.html +1 -1
- fides/ui-build/static/admin/settings/messaging-providers/[key].html +1 -1
- fides/ui-build/static/admin/settings/messaging-providers/new.html +1 -1
- fides/ui-build/static/admin/settings/messaging-providers.html +1 -1
- fides/ui-build/static/admin/settings/organization.html +1 -1
- fides/ui-build/static/admin/settings/privacy-requests.html +1 -1
- fides/ui-build/static/admin/settings/regulations.html +1 -1
- fides/ui-build/static/admin/systems/configure/[id]/test-datasets.html +1 -1
- fides/ui-build/static/admin/systems/configure/[id].html +1 -1
- fides/ui-build/static/admin/systems.html +1 -1
- fides/ui-build/static/admin/taxonomy.html +1 -1
- fides/ui-build/static/admin/user-management/new.html +1 -1
- fides/ui-build/static/admin/user-management/profile/[id].html +1 -1
- fides/ui-build/static/admin/user-management.html +1 -1
- {ethyca_fides-2.71.0rc4.dist-info → ethyca_fides-2.71.1b0.dist-info}/WHEEL +0 -0
- {ethyca_fides-2.71.0rc4.dist-info → ethyca_fides-2.71.1b0.dist-info}/entry_points.txt +0 -0
- {ethyca_fides-2.71.0rc4.dist-info → ethyca_fides-2.71.1b0.dist-info}/licenses/LICENSE +0 -0
- {ethyca_fides-2.71.0rc4.dist-info → ethyca_fides-2.71.1b0.dist-info}/top_level.txt +0 -0
- /fides/ui-build/static/admin/_next/static/{kdnucJIsIefS6ViqY-8w3 → IPOgh7BMBX7b_r8-scpgv}/_ssgManifest.js +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
-
from typing import Any, Optional
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Optional, Union
|
|
3
3
|
|
|
4
4
|
from sqlalchemy import Column, Integer, String
|
|
5
5
|
from sqlalchemy.dialects.postgresql import JSONB
|
|
@@ -13,21 +13,80 @@ from fides.api.task.conditional_dependencies.schemas import (
|
|
|
13
13
|
ConditionLeaf,
|
|
14
14
|
)
|
|
15
15
|
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from sqlalchemy.orm.relationships import RelationshipProperty
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ConditionalDependencyError(Exception):
|
|
21
|
+
"""Exception for conditional dependency errors."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, message: str):
|
|
24
|
+
self.message = message
|
|
25
|
+
super().__init__(self.message)
|
|
26
|
+
|
|
16
27
|
|
|
17
28
|
class ConditionalDependencyType(str, Enum):
|
|
18
|
-
"""Shared enum for conditional dependency node types.
|
|
29
|
+
"""Shared enum for conditional dependency node types.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
leaf: Individual condition (field_address + operator + value)
|
|
33
|
+
group: Collection of conditions with logical operator (AND/OR)
|
|
34
|
+
"""
|
|
19
35
|
|
|
20
36
|
leaf = "leaf"
|
|
21
37
|
group = "group"
|
|
22
38
|
|
|
23
39
|
|
|
24
40
|
class ConditionalDependencyBase(Base):
|
|
25
|
-
"""Abstract base class for all conditional dependency models.
|
|
41
|
+
"""Abstract base class for all conditional dependency models.
|
|
42
|
+
|
|
43
|
+
This class provides a common structure for building hierarchical condition trees
|
|
44
|
+
that can be evaluated to determine when certain actions should be taken.
|
|
45
|
+
|
|
46
|
+
Architecture:
|
|
47
|
+
- Tree Structure: Supports parent-child relationships for complex logic
|
|
48
|
+
- Two Node Types: 'leaf' (individual conditions) and 'group' (logical operators)
|
|
49
|
+
- Flexible Schema: Uses JSONB for dynamic value storage
|
|
50
|
+
- Ordered Evaluation: sort_order ensures predictable condition processing
|
|
51
|
+
|
|
52
|
+
Concrete Implementations:
|
|
53
|
+
- ManualTaskConditionalDependency: Single-type hierarchy for manual tasks
|
|
54
|
+
- Single-type hierarchy means one condition tree per manual task, this condition
|
|
55
|
+
may be a nested group of conditions or a single leaf condition.
|
|
56
|
+
- DigestCondition: Multi-type hierarchy with digest_condition_type separation
|
|
57
|
+
- Multi-type hierarchy means one digest_config can have multiple independent
|
|
58
|
+
condition trees, each with a different digest_condition_type (RECEIVER, CONTENT, PRIORITY)
|
|
59
|
+
- Within each tree, all nodes must have the same digest_condition_type
|
|
60
|
+
- This enables separate condition logic for different aspects of digest processing
|
|
61
|
+
|
|
62
|
+
Usage Pattern:
|
|
63
|
+
1. Inherit from this base class
|
|
64
|
+
2. Define your table name with @declared_attr
|
|
65
|
+
3. Add foreign key relationships (parent_id, entity_id)
|
|
66
|
+
4. Implement get_root_condition() classmethod
|
|
67
|
+
5. Add any domain-specific columns
|
|
68
|
+
|
|
69
|
+
Example Tree Structure:
|
|
70
|
+
Root Group (AND)
|
|
71
|
+
├── Leaf: user.role == "admin"
|
|
72
|
+
├── Leaf: request.priority >= 3
|
|
73
|
+
└── Child Group (OR)
|
|
74
|
+
├── Leaf: user.department == "security"
|
|
75
|
+
└── Leaf: user.department == "compliance"
|
|
76
|
+
|
|
77
|
+
Note:
|
|
78
|
+
- This is a SQLAlchemy abstract model (__abstract__ = True)
|
|
79
|
+
- No database table is created for this base class
|
|
80
|
+
- Subclasses must implement get_root_condition()
|
|
81
|
+
- The 'children' relationship must be defined in concrete subclasses
|
|
82
|
+
"""
|
|
26
83
|
|
|
27
84
|
__abstract__ = True
|
|
28
85
|
|
|
29
86
|
# Tree structure - parent_id defined in concrete classes for proper foreign keys
|
|
30
|
-
condition_type = Column(
|
|
87
|
+
condition_type = Column(
|
|
88
|
+
EnumColumn(ConditionalDependencyType), nullable=False, index=True
|
|
89
|
+
)
|
|
31
90
|
|
|
32
91
|
# Condition details (for leaf nodes)
|
|
33
92
|
field_address = Column(String(255), nullable=True) # For leaf conditions
|
|
@@ -36,47 +95,217 @@ class ConditionalDependencyBase(Base):
|
|
|
36
95
|
logical_operator = Column(String, nullable=True) # 'and' or 'or' for groups
|
|
37
96
|
|
|
38
97
|
# Ordering
|
|
39
|
-
sort_order = Column(Integer, nullable=False, default=0)
|
|
98
|
+
sort_order = Column(Integer, nullable=False, default=0, index=True)
|
|
99
|
+
|
|
100
|
+
def to_correct_condition_type(self) -> Union[ConditionLeaf, ConditionGroup]:
|
|
101
|
+
"""Convert this database model to the correct condition type."""
|
|
102
|
+
if self.condition_type == ConditionalDependencyType.leaf:
|
|
103
|
+
return self.to_condition_leaf()
|
|
104
|
+
return self.to_condition_group()
|
|
40
105
|
|
|
41
106
|
def to_condition_leaf(self) -> ConditionLeaf:
|
|
42
|
-
"""Convert
|
|
43
|
-
|
|
44
|
-
|
|
107
|
+
"""Convert this database model to a ConditionLeaf schema object.
|
|
108
|
+
|
|
109
|
+
This method transforms a leaf-type conditional dependency from its database
|
|
110
|
+
representation into a structured ConditionLeaf object that can be used for
|
|
111
|
+
evaluation and serialization.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
ConditionLeaf: Schema object containing field_address, operator, and value
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
ValueError: If this condition is not a leaf type (i.e., it's a group)
|
|
118
|
+
|
|
119
|
+
Example:
|
|
120
|
+
>>> condition = SomeConcreteConditionalDependency(
|
|
121
|
+
... condition_type="leaf",
|
|
122
|
+
... field_address="user.role",
|
|
123
|
+
... operator="eq",
|
|
124
|
+
... value="admin"
|
|
125
|
+
... )
|
|
126
|
+
>>> leaf = condition.to_condition_leaf()
|
|
127
|
+
>>> print(leaf.field_address) # "user.role"
|
|
128
|
+
"""
|
|
129
|
+
if self.condition_type != ConditionalDependencyType.leaf:
|
|
130
|
+
raise ValueError(
|
|
131
|
+
f"Cannot convert {self.condition_type} condition to leaf. "
|
|
132
|
+
f"Only conditions with condition_type='leaf' can be converted to ConditionLeaf. "
|
|
133
|
+
f"This condition has type '{self.condition_type}' and should be converted using to_condition_group()."
|
|
134
|
+
)
|
|
45
135
|
|
|
46
136
|
return ConditionLeaf(
|
|
47
137
|
field_address=self.field_address, operator=self.operator, value=self.value
|
|
48
138
|
)
|
|
49
139
|
|
|
50
140
|
def to_condition_group(self) -> ConditionGroup:
|
|
51
|
-
"""Convert
|
|
52
|
-
|
|
53
|
-
|
|
141
|
+
"""Convert this database model to a ConditionGroup schema object.
|
|
142
|
+
|
|
143
|
+
This method transforms a group-type conditional dependency from its database
|
|
144
|
+
representation into a structured ConditionGroup object. It recursively processes
|
|
145
|
+
all child conditions, maintaining the tree structure and sort order.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
ConditionGroup: Schema object containing logical_operator and child conditions
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
ValueError: If this condition is not a group type (i.e., it's a leaf)
|
|
152
|
+
AttributeError: If the 'children' relationship is not properly defined
|
|
153
|
+
|
|
154
|
+
Example:
|
|
155
|
+
>>> # Assume we have a group with two leaf children
|
|
156
|
+
>>> group_condition = SomeConcreteConditionalDependency(
|
|
157
|
+
... condition_type="group",
|
|
158
|
+
... logical_operator="and"
|
|
159
|
+
... )
|
|
160
|
+
>>> condition_group = group_condition.to_condition_group()
|
|
161
|
+
>>> print(condition_group.logical_operator) # "and"
|
|
162
|
+
>>> print(len(condition_group.conditions)) # 2
|
|
163
|
+
"""
|
|
164
|
+
if self.condition_type != ConditionalDependencyType.group:
|
|
165
|
+
raise ValueError(
|
|
166
|
+
f"Cannot convert {self.condition_type} condition to group. "
|
|
167
|
+
f"Only conditions with condition_type='group' can be converted to ConditionGroup. "
|
|
168
|
+
f"This condition has type '{self.condition_type}' and should be converted using to_condition_leaf()."
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Recursively build children - note: 'children' must be defined in concrete classes
|
|
172
|
+
try:
|
|
173
|
+
children_list = [child for child in self.children] # type: ignore[attr-defined]
|
|
174
|
+
except AttributeError:
|
|
175
|
+
raise AttributeError(
|
|
176
|
+
f"The 'children' relationship is not defined on {self.__class__.__name__}. "
|
|
177
|
+
f"Concrete subclasses must define a 'children' relationship for group conditions to work properly."
|
|
178
|
+
)
|
|
54
179
|
|
|
55
|
-
# Recursively build children
|
|
56
180
|
child_conditions = []
|
|
57
|
-
children_list = [child for child in self.children] # type: ignore[attr-defined]
|
|
58
181
|
for child in sorted(children_list, key=lambda x: x.sort_order):
|
|
59
|
-
if child.condition_type ==
|
|
182
|
+
if child.condition_type == ConditionalDependencyType.leaf:
|
|
60
183
|
child_conditions.append(child.to_condition_leaf())
|
|
61
|
-
|
|
184
|
+
elif child.condition_type == ConditionalDependencyType.group:
|
|
62
185
|
child_conditions.append(child.to_condition_group())
|
|
186
|
+
else:
|
|
187
|
+
raise ValueError(
|
|
188
|
+
f"Unknown condition_type '{child.condition_type}' found in child condition. "
|
|
189
|
+
f"Expected '{ConditionalDependencyType.leaf}' or '{ConditionalDependencyType.group}'."
|
|
190
|
+
)
|
|
63
191
|
|
|
64
192
|
return ConditionGroup(
|
|
65
193
|
logical_operator=self.logical_operator, conditions=child_conditions
|
|
66
194
|
)
|
|
67
195
|
|
|
68
196
|
@classmethod
|
|
69
|
-
def get_root_condition(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
197
|
+
def get_root_condition(cls, db: Session, **kwargs: Any) -> Optional[Condition]:
|
|
198
|
+
"""Get the root condition tree for a parent entity.
|
|
199
|
+
|
|
200
|
+
This abstract method must be implemented by concrete subclasses to define
|
|
201
|
+
how to retrieve the root condition node for their specific use case.
|
|
202
|
+
The root condition represents the top-level node in a condition tree.
|
|
203
|
+
|
|
204
|
+
Implementation Guidelines:
|
|
205
|
+
1. Query for conditions with parent_id=None for the given parent entity
|
|
206
|
+
2. Return None if no root condition exists
|
|
207
|
+
3. Convert the database model to a Condition schema object
|
|
208
|
+
4. Handle any domain-specific filtering or validation
|
|
73
209
|
|
|
74
210
|
Args:
|
|
75
|
-
db:
|
|
76
|
-
|
|
77
|
-
|
|
211
|
+
db: SQLAlchemy database session for querying
|
|
212
|
+
**kwargs: Keyword arguments specific to each implementation.
|
|
213
|
+
Examples:
|
|
214
|
+
- manual_task_id: ID of the manual task (for single-type hierarchies)
|
|
215
|
+
- digest_config_id: ID of the digest config (for multi-type hierarchies)
|
|
216
|
+
- digest_condition_type: Type of digest condition (for multi-type hierarchies)
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Optional[Condition]: Root condition tree (ConditionLeaf or ConditionGroup) or None
|
|
220
|
+
if no conditions exist for the specified criteria
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
NotImplementedError: If called on the base class directly
|
|
224
|
+
|
|
225
|
+
Example Implementation:
|
|
226
|
+
>>> @classmethod
|
|
227
|
+
>>> def get_root_condition(cls, db: Session, *, manual_task_id: str) -> Optional[Condition]:
|
|
228
|
+
... root = db.query(cls).filter(
|
|
229
|
+
... cls.manual_task_id == manual_task_id,
|
|
230
|
+
... cls.parent_id.is_(None)
|
|
231
|
+
... ).first()
|
|
232
|
+
... if not root:
|
|
233
|
+
... return None
|
|
234
|
+
... return root.to_condition_leaf() if root.condition_type == 'leaf' else root.to_condition_group()
|
|
235
|
+
"""
|
|
236
|
+
raise NotImplementedError(
|
|
237
|
+
f"Subclasses of {cls.__name__} must implement get_root_condition(). "
|
|
238
|
+
f"This method should query for the root condition (parent_id=None) "
|
|
239
|
+
f"and return it as a Condition schema object, or None if not found. "
|
|
240
|
+
f"See the docstring for implementation guidelines and examples."
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def get_depth(self) -> int:
|
|
244
|
+
"""Calculate the depth of this node in the condition tree.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
int: Depth level (0 for root, 1 for direct children, etc.)
|
|
248
|
+
|
|
249
|
+
Note:
|
|
250
|
+
Requires the 'parent' relationship to be defined in concrete classes.
|
|
251
|
+
"""
|
|
252
|
+
depth = 0
|
|
253
|
+
current = self
|
|
254
|
+
try:
|
|
255
|
+
while hasattr(current, "parent") and current.parent is not None: # type: ignore[attr-defined]
|
|
256
|
+
depth += 1
|
|
257
|
+
current = current.parent # type: ignore[attr-defined]
|
|
258
|
+
except AttributeError:
|
|
259
|
+
# If parent relationship not defined, we can't calculate depth
|
|
260
|
+
pass
|
|
261
|
+
return depth
|
|
262
|
+
|
|
263
|
+
def get_tree_summary(self) -> str:
|
|
264
|
+
"""Generate a human-readable summary of this condition tree.
|
|
78
265
|
|
|
79
266
|
Returns:
|
|
80
|
-
|
|
267
|
+
str: Multi-line string representation of the condition tree structure
|
|
268
|
+
|
|
269
|
+
Example:
|
|
270
|
+
>>> print(condition.get_tree_summary())
|
|
271
|
+
Group (AND) [depth: 0, order: 0]
|
|
272
|
+
├── Leaf: user.role == "admin" [depth: 1, order: 0]
|
|
273
|
+
├── Leaf: request.priority >= 3 [depth: 1, order: 1]
|
|
274
|
+
└── Group (OR) [depth: 1, order: 2]
|
|
275
|
+
├── Leaf: user.dept == "security" [depth: 2, order: 0]
|
|
276
|
+
└── Leaf: user.dept == "compliance" [depth: 2, order: 1]
|
|
81
277
|
"""
|
|
82
|
-
|
|
278
|
+
|
|
279
|
+
def _build_tree_lines(
|
|
280
|
+
node: "ConditionalDependencyBase", prefix: str = "", is_last: bool = True
|
|
281
|
+
) -> list[str]:
|
|
282
|
+
lines = []
|
|
283
|
+
|
|
284
|
+
# Current node info
|
|
285
|
+
if node.condition_type == ConditionalDependencyType.leaf:
|
|
286
|
+
node_desc = f"Leaf: {node.field_address} {node.operator} {node.value}"
|
|
287
|
+
else:
|
|
288
|
+
node_desc = f"Group ({node.logical_operator.upper() if node.logical_operator else 'UNKNOWN'})"
|
|
289
|
+
|
|
290
|
+
depth = node.get_depth()
|
|
291
|
+
connector = "└── " if is_last else "├── "
|
|
292
|
+
lines.append(
|
|
293
|
+
f"{prefix}{connector}{node_desc} [depth: {depth}, order: {node.sort_order}]"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# Add children if this is a group
|
|
297
|
+
if node.condition_type == ConditionalDependencyType.group:
|
|
298
|
+
try:
|
|
299
|
+
children = sorted([child for child in node.children], key=lambda x: x.sort_order) # type: ignore[attr-defined]
|
|
300
|
+
for i, child in enumerate(children):
|
|
301
|
+
is_last_child = i == len(children) - 1
|
|
302
|
+
child_prefix = prefix + (" " if is_last else "│ ")
|
|
303
|
+
lines.extend(
|
|
304
|
+
_build_tree_lines(child, child_prefix, is_last_child)
|
|
305
|
+
)
|
|
306
|
+
except AttributeError:
|
|
307
|
+
lines.append(f"{prefix} [children relationship not defined]")
|
|
308
|
+
|
|
309
|
+
return lines
|
|
310
|
+
|
|
311
|
+
return "\n".join(_build_tree_lines(self))
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
"""Digest models package."""
|
|
2
2
|
|
|
3
|
-
from fides.api.models.digest.conditional_dependencies import
|
|
3
|
+
from fides.api.models.digest.conditional_dependencies import (
|
|
4
|
+
DigestCondition,
|
|
5
|
+
DigestConditionType,
|
|
6
|
+
)
|
|
4
7
|
from fides.api.models.digest.digest_config import DigestConfig, DigestType
|
|
5
8
|
|
|
6
9
|
__all__ = [
|
|
7
10
|
"DigestConfig",
|
|
8
11
|
"DigestType",
|
|
12
|
+
"DigestCondition",
|
|
9
13
|
"DigestConditionType",
|
|
10
14
|
]
|
|
@@ -1,9 +1,275 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Optional, Union
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import Column, ForeignKey, Index, String, text
|
|
5
|
+
from sqlalchemy.ext.declarative import declared_attr
|
|
6
|
+
from sqlalchemy.orm import Session, relationship
|
|
7
|
+
|
|
8
|
+
from fides.api.db.base_class import FidesBase
|
|
9
|
+
from fides.api.db.util import EnumColumn
|
|
10
|
+
from fides.api.models.conditional_dependency.conditional_dependency_base import (
|
|
11
|
+
ConditionalDependencyBase,
|
|
12
|
+
ConditionalDependencyError,
|
|
13
|
+
)
|
|
14
|
+
from fides.api.task.conditional_dependencies.schemas import (
|
|
15
|
+
Condition,
|
|
16
|
+
ConditionGroup,
|
|
17
|
+
ConditionLeaf,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from fides.api.models.digest.digest_config import DigestConfig
|
|
2
22
|
|
|
3
23
|
|
|
4
24
|
class DigestConditionType(str, Enum):
|
|
5
|
-
"""Types of digest conditions - each can have their own tree.
|
|
25
|
+
"""Types of digest conditions - each can have their own tree.
|
|
26
|
+
|
|
27
|
+
Types:
|
|
28
|
+
- RECEIVER: Conditions that determine who gets the digest
|
|
29
|
+
- CONTENT: Conditions that determine what gets included in the digest
|
|
30
|
+
- PRIORITY: Conditions that determine what is considered high priority for the digest
|
|
31
|
+
- This could be used to determine sending the digest at a different time or how
|
|
32
|
+
often it should be sent. It could also be used to format content.
|
|
33
|
+
- Example:
|
|
34
|
+
- DSRs that are due within the next week
|
|
35
|
+
- Privacy requests that are due within the next week
|
|
36
|
+
- Privacy requests for certain geographic regions
|
|
37
|
+
"""
|
|
6
38
|
|
|
7
39
|
RECEIVER = "receiver"
|
|
8
40
|
CONTENT = "content"
|
|
9
41
|
PRIORITY = "priority"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class DigestCondition(ConditionalDependencyBase):
|
|
45
|
+
"""Digest conditional dependencies - multi-type hierarchies.
|
|
46
|
+
|
|
47
|
+
- Multi-type hierarchy means one digest_config can have multiple independent
|
|
48
|
+
condition trees, each with a different digest_condition_type (RECEIVER, CONTENT, PRIORITY)
|
|
49
|
+
- Within each tree, all nodes must have the same digest_condition_type
|
|
50
|
+
- This enables separate condition logic for different aspects of digest processing
|
|
51
|
+
|
|
52
|
+
Ensures that all conditions within the same tree have the same digest_condition_type.
|
|
53
|
+
This prevents logical errors where different condition types are mixed in a single
|
|
54
|
+
condition tree structure.
|
|
55
|
+
|
|
56
|
+
Example Tree Structure:
|
|
57
|
+
DigestConfig (e.g., "Weekly Privacy Digest")
|
|
58
|
+
├── RECEIVER Dependency Condition Tree (who gets the digest)
|
|
59
|
+
│ └── Group (AND)
|
|
60
|
+
│ ├── Leaf: user.role == "admin"
|
|
61
|
+
│ └── Leaf: user.department == "privacy"
|
|
62
|
+
├── CONTENT Dependency Condition Tree (what gets included)
|
|
63
|
+
│ └── Group (OR)
|
|
64
|
+
│ ├── Leaf: task.priority == "high"
|
|
65
|
+
│ └── Leaf: task.overdue == true
|
|
66
|
+
└── PRIORITY Dependency Condition Tree (when to send)
|
|
67
|
+
└── Leaf: task.count >= 5
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
@declared_attr
|
|
71
|
+
def __tablename__(cls) -> str:
|
|
72
|
+
return "digest_condition"
|
|
73
|
+
|
|
74
|
+
# We need to redefine it here so that self-referential relationships
|
|
75
|
+
# can properly reference the `id` column instead of the built-in Python function.
|
|
76
|
+
id = Column(String(255), primary_key=True, default=FidesBase.generate_uuid)
|
|
77
|
+
|
|
78
|
+
# Foreign key relationships
|
|
79
|
+
digest_config_id = Column(
|
|
80
|
+
String(255),
|
|
81
|
+
ForeignKey("digest_config.id", ondelete="CASCADE"),
|
|
82
|
+
nullable=False,
|
|
83
|
+
index=True,
|
|
84
|
+
)
|
|
85
|
+
parent_id = Column(
|
|
86
|
+
String(255),
|
|
87
|
+
ForeignKey("digest_condition.id", ondelete="CASCADE"),
|
|
88
|
+
nullable=True,
|
|
89
|
+
index=True,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Digest-specific: condition category
|
|
93
|
+
digest_condition_type = Column(
|
|
94
|
+
EnumColumn(DigestConditionType), nullable=False, index=True
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Relationships
|
|
98
|
+
digest_config = relationship("DigestConfig", back_populates="conditions")
|
|
99
|
+
parent = relationship(
|
|
100
|
+
"DigestCondition",
|
|
101
|
+
remote_side=[id],
|
|
102
|
+
back_populates="children",
|
|
103
|
+
foreign_keys=[parent_id],
|
|
104
|
+
)
|
|
105
|
+
children = relationship(
|
|
106
|
+
"DigestCondition",
|
|
107
|
+
back_populates="parent",
|
|
108
|
+
cascade="all, delete-orphan",
|
|
109
|
+
foreign_keys=[parent_id],
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Ensure only one root condition per digest_condition_type per digest_config
|
|
113
|
+
__table_args__ = (
|
|
114
|
+
Index(
|
|
115
|
+
"ix_digest_condition_unique_root_per_type",
|
|
116
|
+
"digest_config_id",
|
|
117
|
+
"digest_condition_type",
|
|
118
|
+
unique=True,
|
|
119
|
+
postgresql_where=text("parent_id IS NULL"),
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def _validate_condition_type_consistency(db: Session, data: dict[str, Any]) -> None:
|
|
125
|
+
"""Validate that a condition's digest_condition_type matches its parent's type.
|
|
126
|
+
|
|
127
|
+
Since each parent was validated when created, checking against the immediate parent
|
|
128
|
+
is sufficient to ensure tree-wide consistency.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
db: Database session for querying
|
|
132
|
+
data: Dictionary containing condition data to validate (must include both parent_id and digest_condition_type)
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
ValueError: If parent doesn't exist or digest_condition_type doesn't match parent's type
|
|
136
|
+
"""
|
|
137
|
+
parent_id = data.get("parent_id")
|
|
138
|
+
digest_condition_type = data.get("digest_condition_type")
|
|
139
|
+
|
|
140
|
+
if not parent_id:
|
|
141
|
+
# Root condition - no validation needed
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
# Get the parent condition
|
|
145
|
+
parent = (
|
|
146
|
+
db.query(DigestCondition).filter(DigestCondition.id == parent_id).first()
|
|
147
|
+
)
|
|
148
|
+
if not parent:
|
|
149
|
+
raise ValueError(f"Parent condition with id '{parent_id}' does not exist")
|
|
150
|
+
|
|
151
|
+
# Validate that the new condition matches the parent's digest_condition_type
|
|
152
|
+
if parent.digest_condition_type != digest_condition_type:
|
|
153
|
+
raise ValueError(
|
|
154
|
+
f"Cannot create condition with type '{digest_condition_type}' under parent "
|
|
155
|
+
f"with type '{parent.digest_condition_type}'. All conditions in the same tree "
|
|
156
|
+
f"must have the same digest_condition_type."
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def create(
|
|
161
|
+
cls,
|
|
162
|
+
db: Session,
|
|
163
|
+
*,
|
|
164
|
+
data: dict[str, Any],
|
|
165
|
+
check_name: bool = True,
|
|
166
|
+
) -> "DigestCondition":
|
|
167
|
+
"""Create a new DigestCondition with validation."""
|
|
168
|
+
# Validate condition type consistency
|
|
169
|
+
cls._validate_condition_type_consistency(db, data)
|
|
170
|
+
try:
|
|
171
|
+
return super().create(db=db, data=data, check_name=check_name)
|
|
172
|
+
except Exception as e:
|
|
173
|
+
raise ConditionalDependencyError(str(e))
|
|
174
|
+
|
|
175
|
+
def update(self, db: Session, *, data: dict[str, Any]) -> "DigestCondition":
|
|
176
|
+
"""Update DigestCondition with validation."""
|
|
177
|
+
# Ensure validation data includes current values for fields not being updated
|
|
178
|
+
validation_data = {
|
|
179
|
+
"parent_id": data.get("parent_id", self.parent_id),
|
|
180
|
+
"digest_condition_type": data.get(
|
|
181
|
+
"digest_condition_type", self.digest_condition_type
|
|
182
|
+
),
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# Validate before updating
|
|
186
|
+
self._validate_condition_type_consistency(db, validation_data)
|
|
187
|
+
return super().update(db=db, data=data) # type: ignore[return-value]
|
|
188
|
+
|
|
189
|
+
def save(self, db: Session) -> "DigestCondition":
|
|
190
|
+
"""Save DigestCondition with validation."""
|
|
191
|
+
# Extract current object data for validation
|
|
192
|
+
data = {
|
|
193
|
+
"parent_id": self.parent_id,
|
|
194
|
+
"digest_condition_type": self.digest_condition_type,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# Validate before saving (only if this has a parent)
|
|
198
|
+
if self.parent_id:
|
|
199
|
+
self._validate_condition_type_consistency(db, data)
|
|
200
|
+
return super().save(db=db) # type: ignore[return-value]
|
|
201
|
+
|
|
202
|
+
@classmethod
|
|
203
|
+
def get_root_condition(
|
|
204
|
+
cls,
|
|
205
|
+
db: Session,
|
|
206
|
+
**kwargs: Any,
|
|
207
|
+
) -> Optional[Union[ConditionLeaf, ConditionGroup]]:
|
|
208
|
+
"""Get the root condition tree for a specific digest condition type.
|
|
209
|
+
|
|
210
|
+
Implementation of the abstract base method for DigestCondition's multi-type hierarchy.
|
|
211
|
+
Each digest_config can have separate condition trees for RECEIVER, CONTENT, and PRIORITY
|
|
212
|
+
types. This method retrieves the root of one specific tree.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
db: SQLAlchemy database session for querying
|
|
216
|
+
**kwargs: Keyword arguments containing:
|
|
217
|
+
digest_config_id: ID of the digest config
|
|
218
|
+
digest_condition_type: DigestConditionType enum value
|
|
219
|
+
Must be one of: RECEIVER, CONTENT, PRIORITY
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Optional[Union[ConditionLeaf, ConditionGroup]]: Root condition tree for the specified
|
|
223
|
+
type, or None if no conditions exist
|
|
224
|
+
|
|
225
|
+
Raises:
|
|
226
|
+
ValueError: If required parameters are missing
|
|
227
|
+
|
|
228
|
+
Example:
|
|
229
|
+
>>> # Get receiver conditions for a digest
|
|
230
|
+
>>> receiver_conditions = DigestCondition.get_root_condition(
|
|
231
|
+
... db, digest_config_id=digest_config.id,
|
|
232
|
+
... digest_condition_type=DigestConditionType.RECEIVER
|
|
233
|
+
... )
|
|
234
|
+
>>> # Get content conditions for the same digest
|
|
235
|
+
>>> content_conditions = DigestCondition.get_root_condition(
|
|
236
|
+
... db, digest_config_id=digest_config.id,
|
|
237
|
+
... digest_condition_type=DigestConditionType.CONTENT
|
|
238
|
+
... )
|
|
239
|
+
"""
|
|
240
|
+
digest_config_id = kwargs.get("digest_config_id")
|
|
241
|
+
digest_condition_type = kwargs.get("digest_condition_type")
|
|
242
|
+
|
|
243
|
+
if not digest_config_id or not digest_condition_type:
|
|
244
|
+
raise ValueError(
|
|
245
|
+
"digest_config_id and digest_condition_type are required keyword arguments"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
root = (
|
|
249
|
+
db.query(cls)
|
|
250
|
+
.filter(
|
|
251
|
+
cls.digest_config_id == digest_config_id,
|
|
252
|
+
cls.digest_condition_type == digest_condition_type,
|
|
253
|
+
cls.parent_id.is_(None),
|
|
254
|
+
)
|
|
255
|
+
.one_or_none()
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if not root:
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
return root.to_correct_condition_type()
|
|
262
|
+
|
|
263
|
+
@classmethod
|
|
264
|
+
def get_all_root_conditions(
|
|
265
|
+
cls, db: Session, digest_config_id: str
|
|
266
|
+
) -> dict[DigestConditionType, Optional[Condition]]:
|
|
267
|
+
"""Get root conditions for all digest condition types"""
|
|
268
|
+
return {
|
|
269
|
+
condition_type: cls.get_root_condition(
|
|
270
|
+
db,
|
|
271
|
+
digest_config_id=digest_config_id,
|
|
272
|
+
digest_condition_type=condition_type,
|
|
273
|
+
)
|
|
274
|
+
for condition_type in DigestConditionType
|
|
275
|
+
}
|