ethyca-fides 2.68.1b0__py2.py3-none-any.whl → 2.68.1b2__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.

Files changed (131) hide show
  1. {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/METADATA +1 -1
  2. {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/RECORD +128 -118
  3. fides/_version.py +3 -3
  4. fides/api/alembic/migrations/versions/3baf42d251a6_add_generic_taxonomy_models.py +239 -0
  5. fides/api/api/deps.py +2 -0
  6. fides/api/api/v1/endpoints/generic_overrides.py +64 -167
  7. fides/api/db/base.py +6 -0
  8. fides/api/db/ctl_session.py +3 -0
  9. fides/api/db/session.py +2 -1
  10. fides/api/models/privacy_request/privacy_request.py +15 -0
  11. fides/api/models/taxonomy.py +275 -0
  12. fides/api/schemas/application_config.py +2 -1
  13. fides/api/schemas/privacy_center_config.py +15 -0
  14. fides/api/service/deps.py +5 -0
  15. fides/api/service/privacy_request/request_service.py +6 -1
  16. fides/api/task/conditional_dependencies/evaluator.py +192 -45
  17. fides/api/task/conditional_dependencies/logging_utils.py +196 -0
  18. fides/api/task/conditional_dependencies/operators.py +8 -2
  19. fides/api/task/conditional_dependencies/schemas.py +25 -1
  20. fides/api/task/graph_task.py +9 -2
  21. fides/api/task/manual/manual_task_conditional_evaluation.py +193 -0
  22. fides/api/task/manual/manual_task_graph_task.py +224 -119
  23. fides/api/task/manual/manual_task_utils.py +0 -4
  24. fides/api/tasks/__init__.py +1 -0
  25. fides/api/util/connection_type.py +68 -33
  26. fides/config/database_settings.py +10 -1
  27. fides/data/sample_project/docker-compose.yml +3 -3
  28. fides/service/taxonomy/__init__.py +0 -0
  29. fides/service/taxonomy/handlers/__init__.py +11 -0
  30. fides/service/taxonomy/handlers/base.py +42 -0
  31. fides/service/taxonomy/handlers/legacy_handler.py +95 -0
  32. fides/service/taxonomy/taxonomy_service.py +261 -0
  33. fides/service/taxonomy/utils.py +160 -0
  34. fides/ui-build/static/admin/404.html +1 -1
  35. fides/ui-build/static/admin/_next/static/chunks/pages/{_app-65723cd4b8fc36ac.js → _app-2c10f6b217b7978b.js} +1 -1
  36. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center-58827eb86516931f.js +1 -0
  37. fides/ui-build/static/admin/_next/static/chunks/pages/integrations/{[id]-766e57bcf38b5b1e.js → [id]-4e286a1e501a0c73.js} +1 -1
  38. fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests-709bcb0bc6a5382d.js +1 -0
  39. fides/ui-build/static/admin/_next/static/css/a72179b1754aadd3.css +1 -0
  40. fides/ui-build/static/admin/_next/static/{JLiYN-Wiw1kNc_8IVythJ → qvk5eMANVfwYkdURE7fgG}/_buildManifest.js +1 -1
  41. fides/ui-build/static/admin/add-systems/manual.html +1 -1
  42. fides/ui-build/static/admin/add-systems/multiple.html +1 -1
  43. fides/ui-build/static/admin/add-systems.html +1 -1
  44. fides/ui-build/static/admin/consent/configure/add-vendors.html +1 -1
  45. fides/ui-build/static/admin/consent/configure.html +1 -1
  46. fides/ui-build/static/admin/consent/privacy-experience/[id].html +1 -1
  47. fides/ui-build/static/admin/consent/privacy-experience/new.html +1 -1
  48. fides/ui-build/static/admin/consent/privacy-experience.html +1 -1
  49. fides/ui-build/static/admin/consent/privacy-notices/[id].html +1 -1
  50. fides/ui-build/static/admin/consent/privacy-notices/new.html +1 -1
  51. fides/ui-build/static/admin/consent/privacy-notices.html +1 -1
  52. fides/ui-build/static/admin/consent/properties.html +1 -1
  53. fides/ui-build/static/admin/consent/reporting.html +1 -1
  54. fides/ui-build/static/admin/consent.html +1 -1
  55. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn]/[resourceUrn].html +1 -1
  56. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn].html +1 -1
  57. fides/ui-build/static/admin/data-catalog/[systemId]/projects.html +1 -1
  58. fides/ui-build/static/admin/data-catalog/[systemId]/resources/[resourceUrn].html +1 -1
  59. fides/ui-build/static/admin/data-catalog/[systemId]/resources.html +1 -1
  60. fides/ui-build/static/admin/data-catalog.html +1 -1
  61. fides/ui-build/static/admin/data-discovery/action-center/[monitorId]/[systemId].html +1 -1
  62. fides/ui-build/static/admin/data-discovery/action-center/[monitorId].html +1 -1
  63. fides/ui-build/static/admin/data-discovery/action-center.html +1 -1
  64. fides/ui-build/static/admin/data-discovery/activity.html +1 -1
  65. fides/ui-build/static/admin/data-discovery/detection/[resourceUrn].html +1 -1
  66. fides/ui-build/static/admin/data-discovery/detection.html +1 -1
  67. fides/ui-build/static/admin/data-discovery/discovery/[resourceUrn].html +1 -1
  68. fides/ui-build/static/admin/data-discovery/discovery.html +1 -1
  69. fides/ui-build/static/admin/datamap.html +1 -1
  70. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName]/[...subfieldNames].html +1 -1
  71. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName].html +1 -1
  72. fides/ui-build/static/admin/dataset/[datasetId].html +1 -1
  73. fides/ui-build/static/admin/dataset/new.html +1 -1
  74. fides/ui-build/static/admin/dataset.html +1 -1
  75. fides/ui-build/static/admin/datastore-connection/[id].html +1 -1
  76. fides/ui-build/static/admin/datastore-connection/new.html +1 -1
  77. fides/ui-build/static/admin/datastore-connection.html +1 -1
  78. fides/ui-build/static/admin/index.html +1 -1
  79. fides/ui-build/static/admin/integrations/[id].html +1 -1
  80. fides/ui-build/static/admin/integrations.html +1 -1
  81. fides/ui-build/static/admin/lib/fides-preview.js +1 -1
  82. fides/ui-build/static/admin/lib/fides-tcf.js +2 -2
  83. fides/ui-build/static/admin/lib/fides.js +1 -1
  84. fides/ui-build/static/admin/login/[provider].html +1 -1
  85. fides/ui-build/static/admin/login.html +1 -1
  86. fides/ui-build/static/admin/messaging/[id].html +1 -1
  87. fides/ui-build/static/admin/messaging/add-template.html +1 -1
  88. fides/ui-build/static/admin/messaging.html +1 -1
  89. fides/ui-build/static/admin/poc/ant-components.html +1 -1
  90. fides/ui-build/static/admin/poc/form-experiments/AntForm.html +1 -1
  91. fides/ui-build/static/admin/poc/form-experiments/FormikAntFormItem.html +1 -1
  92. fides/ui-build/static/admin/poc/form-experiments/FormikControlled.html +1 -1
  93. fides/ui-build/static/admin/poc/form-experiments/FormikField.html +1 -1
  94. fides/ui-build/static/admin/poc/form-experiments/FormikSpreadField.html +1 -1
  95. fides/ui-build/static/admin/poc/forms.html +1 -1
  96. fides/ui-build/static/admin/poc/table-migration.html +1 -1
  97. fides/ui-build/static/admin/privacy-requests/[id].html +1 -1
  98. fides/ui-build/static/admin/privacy-requests/configure/messaging.html +1 -1
  99. fides/ui-build/static/admin/privacy-requests/configure/storage.html +1 -1
  100. fides/ui-build/static/admin/privacy-requests/configure.html +1 -1
  101. fides/ui-build/static/admin/privacy-requests.html +1 -1
  102. fides/ui-build/static/admin/properties/[id].html +1 -1
  103. fides/ui-build/static/admin/properties/add-property.html +1 -1
  104. fides/ui-build/static/admin/properties.html +1 -1
  105. fides/ui-build/static/admin/reporting/datamap.html +1 -1
  106. fides/ui-build/static/admin/settings/about/alpha.html +1 -1
  107. fides/ui-build/static/admin/settings/about.html +1 -1
  108. fides/ui-build/static/admin/settings/consent/[configuration_id]/[purpose_id].html +1 -1
  109. fides/ui-build/static/admin/settings/consent.html +1 -1
  110. fides/ui-build/static/admin/settings/custom-fields.html +1 -1
  111. fides/ui-build/static/admin/settings/domain-records.html +1 -1
  112. fides/ui-build/static/admin/settings/domains.html +1 -1
  113. fides/ui-build/static/admin/settings/email-templates.html +1 -1
  114. fides/ui-build/static/admin/settings/locations.html +1 -1
  115. fides/ui-build/static/admin/settings/organization.html +1 -1
  116. fides/ui-build/static/admin/settings/regulations.html +1 -1
  117. fides/ui-build/static/admin/systems/configure/[id]/test-datasets.html +1 -1
  118. fides/ui-build/static/admin/systems/configure/[id].html +1 -1
  119. fides/ui-build/static/admin/systems.html +1 -1
  120. fides/ui-build/static/admin/taxonomy.html +1 -1
  121. fides/ui-build/static/admin/user-management/new.html +1 -1
  122. fides/ui-build/static/admin/user-management/profile/[id].html +1 -1
  123. fides/ui-build/static/admin/user-management.html +1 -1
  124. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center-53a763e49ce34a74.js +0 -1
  125. fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests-f43a988542813110.js +0 -1
  126. fides/ui-build/static/admin/_next/static/css/e1628f15dd5f019b.css +0 -1
  127. {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/WHEEL +0 -0
  128. {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/entry_points.txt +0 -0
  129. {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/licenses/LICENSE +0 -0
  130. {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/top_level.txt +0 -0
  131. /fides/ui-build/static/admin/_next/static/{JLiYN-Wiw1kNc_8IVythJ → qvk5eMANVfwYkdURE7fgG}/_ssgManifest.js +0 -0
@@ -0,0 +1,275 @@
1
+ # pylint: disable=protected-access
2
+ from __future__ import annotations
3
+
4
+ from enum import Enum
5
+ from typing import Any, Dict, List, Optional, Type
6
+
7
+ from sqlalchemy import (
8
+ BOOLEAN,
9
+ Column,
10
+ ForeignKey,
11
+ ForeignKeyConstraint,
12
+ String,
13
+ Text,
14
+ UniqueConstraint,
15
+ )
16
+ from sqlalchemy.ext.associationproxy import association_proxy
17
+ from sqlalchemy.ext.declarative import declared_attr
18
+ from sqlalchemy.orm import RelationshipProperty, Session, relationship
19
+
20
+ from fides.api.common_exceptions import ValidationError
21
+ from fides.api.db.base_class import Base
22
+ from fides.api.models.sql_models import FidesBase # type: ignore[attr-defined]
23
+
24
+ LEGACY_TAXONOMIES = {"data_categories", "data_uses", "data_subjects"}
25
+
26
+
27
+ class TargetType(str, Enum):
28
+ """Enumeration of target types that taxonomies can apply to."""
29
+
30
+ SYSTEM = "system"
31
+ PRIVACY_DECLARATION = "privacy_declaration"
32
+ TAXONOMY = "taxonomy" # For taxonomy-to-taxonomy relationships
33
+
34
+
35
+ class Taxonomy(Base, FidesBase):
36
+ """The SQL model for taxonomy resources.
37
+
38
+ This is a generic taxonomy model that can be used to create any taxonomy.
39
+ For now we seed the database with the legacy taxonomies (data_category, data use, data subject)
40
+ so that these legacy taxonomy types can be used for allowed usage relationships.
41
+ """
42
+
43
+ # Overriding the id definition from Base so we don't treat this as the primary key
44
+ id = Column(
45
+ String(255),
46
+ nullable=False,
47
+ index=False,
48
+ unique=True,
49
+ default=FidesBase.generate_uuid,
50
+ )
51
+
52
+ # The fides_key is inherited from FidesBase and acts as the primary key
53
+
54
+ # This is private to encourage the use of applies_to (see comment below)
55
+ _allowed_usages: RelationshipProperty[List[TaxonomyAllowedUsage]] = relationship(
56
+ "TaxonomyAllowedUsage",
57
+ back_populates="source_taxonomy",
58
+ cascade="all, delete-orphan",
59
+ lazy="selectin",
60
+ )
61
+
62
+ # Association proxy to simplify access to target_type values
63
+ # This allows getting a list of strings while the actual storage
64
+ # is handled through the TaxonomyAllowedUsage model
65
+ # Updates should be done through the create/update methods
66
+ applies_to: List[str] = association_proxy(
67
+ "_allowed_usages",
68
+ "target_type",
69
+ # Allow setting via strings, the relationship backref will set FK
70
+ creator=lambda target_type: TaxonomyAllowedUsage(target_type=target_type),
71
+ )
72
+
73
+ @classmethod
74
+ def create(
75
+ cls: Type["Taxonomy"],
76
+ db: Session,
77
+ *,
78
+ data: Dict[str, Any],
79
+ check_name: bool = True,
80
+ ) -> "Taxonomy":
81
+ """Create a new Taxonomy with proper handling of applies_to."""
82
+ # Disallow creating taxonomies that represent legacy types
83
+ fides_key = data.get("fides_key")
84
+ if fides_key in LEGACY_TAXONOMIES:
85
+ raise ValidationError(
86
+ f"Cannot create taxonomy '{fides_key}'. This is a taxonomy managed by the system."
87
+ )
88
+ applies_to = data.pop("applies_to", [])
89
+
90
+ # Create the taxonomy
91
+ taxonomy: Taxonomy = super().create(db=db, data=data, check_name=check_name)
92
+
93
+ # Reconcile allowed usages if applies_to was provided
94
+ if applies_to:
95
+ taxonomy._reconcile_allowed_usages(db, applies_to)
96
+
97
+ return cls.persist_obj(db, taxonomy)
98
+
99
+ def update(self, db: Session, *, data: Dict[str, Any]) -> "Taxonomy":
100
+ """Update a Taxonomy with proper handling of applies_to."""
101
+ applies_to = data.pop("applies_to", None)
102
+
103
+ # Update the base fields
104
+ super().update(db=db, data=data)
105
+
106
+ # If applies_to was provided, reconcile allowed usages
107
+ if applies_to is not None:
108
+ self._reconcile_allowed_usages(db, applies_to)
109
+
110
+ return self
111
+
112
+ def save(self, db: Session) -> "Taxonomy":
113
+ """Override save to reconcile any direct `applies_to` edits before persisting.
114
+
115
+ This allows callers to mutate `applies_to` via the association proxy and then call save.
116
+ """
117
+ # Ensure no duplicate target types and reconcile relationship objects to the current values
118
+ self._reconcile_allowed_usages(db, list(self.applies_to))
119
+ return super().save(db) # type: ignore[return-value]
120
+
121
+ def _reconcile_allowed_usages(self, db: Session, applies_to: List[str]) -> None:
122
+ """Ensure `_allowed_usages` matches the provided `applies_to` list.
123
+
124
+ - Deletes usages not in the provided list
125
+ - Creates usages missing from the relationship
126
+ - Deduplicates by target_type
127
+ """
128
+ existing_usages = {usage.target_type: usage for usage in self._allowed_usages}
129
+ desired_types = set(applies_to)
130
+
131
+ # Delete usages that should no longer exist
132
+ for target_type in set(existing_usages.keys()) - desired_types:
133
+ # Remove from relationship; delete-orphan cascade will handle DB delete
134
+ self._allowed_usages.remove(existing_usages[target_type])
135
+
136
+ # Add missing usages
137
+ for target_type in desired_types - set(existing_usages.keys()):
138
+ self._allowed_usages.append(TaxonomyAllowedUsage(target_type=target_type))
139
+
140
+ # Deduplicate any accidental duplicates in-memory
141
+ seen: set[str] = set()
142
+ for usage in list(self._allowed_usages):
143
+ if usage.target_type in seen:
144
+ # Remove duplicate from relationship; delete-orphan will handle DB
145
+ self._allowed_usages.remove(usage)
146
+ else:
147
+ seen.add(usage.target_type)
148
+
149
+
150
+ class TaxonomyAllowedUsage(Base):
151
+ """
152
+ The SQL model for taxonomy allowed usage.
153
+ Defines what types of targets a taxonomy can be applied to.
154
+
155
+ target_type can be either:
156
+ - A generic type: "system", "privacy_declaration", "taxonomy"
157
+ - A taxonomy key: "data_categories", "data_uses", etc.
158
+ """
159
+
160
+ @declared_attr
161
+ def __tablename__(self) -> str:
162
+ return "taxonomy_allowed_usage"
163
+
164
+ id = Column(
165
+ String(255),
166
+ nullable=False,
167
+ index=False,
168
+ unique=True,
169
+ default=FidesBase.generate_uuid,
170
+ )
171
+
172
+ source_taxonomy: RelationshipProperty[Taxonomy] = relationship(
173
+ "Taxonomy",
174
+ back_populates="_allowed_usages",
175
+ )
176
+
177
+ source_taxonomy_key: Column[str] = Column(
178
+ String,
179
+ ForeignKey("taxonomy.fides_key", ondelete="CASCADE"),
180
+ primary_key=True,
181
+ )
182
+ target_type: Column[str] = Column(
183
+ String, primary_key=True
184
+ ) # Can be "system", "dataset", OR a taxonomy key like "data_categories"
185
+
186
+
187
+ class TaxonomyElement(Base, FidesBase):
188
+ """
189
+ The SQL model for taxonomy elements.
190
+
191
+ This is a generic taxonomy element model that can be used to create any taxonomy element.
192
+
193
+ As of now the legacy taxonomy elements still exist in their own tables (ctl_data_categories, ctl_data_uses, ctl_data_subjects),
194
+ but we can migrate them to this model in the future if needed.
195
+ """
196
+
197
+ @declared_attr
198
+ def __tablename__(self) -> str:
199
+ return "taxonomy_element"
200
+
201
+ id = Column(
202
+ String(255),
203
+ nullable=False,
204
+ index=False,
205
+ unique=True,
206
+ default=FidesBase.generate_uuid,
207
+ )
208
+
209
+ # Which taxonomy this element belongs to
210
+ taxonomy_type = Column(
211
+ String,
212
+ ForeignKey("taxonomy.fides_key", ondelete="CASCADE"),
213
+ nullable=False,
214
+ index=True,
215
+ )
216
+
217
+ parent_key = Column(
218
+ Text, ForeignKey("taxonomy_element.fides_key", ondelete="RESTRICT"), index=True
219
+ )
220
+ active = Column(BOOLEAN, default=True, nullable=False, index=True)
221
+
222
+ children: RelationshipProperty[List[TaxonomyElement]] = relationship(
223
+ "TaxonomyElement",
224
+ back_populates="parent",
225
+ cascade="save-update, merge, refresh-expire", # intentionally do not cascade deletes
226
+ passive_deletes="all",
227
+ )
228
+
229
+ parent: RelationshipProperty[Optional[TaxonomyElement]] = relationship(
230
+ "TaxonomyElement",
231
+ back_populates="children",
232
+ remote_side="TaxonomyElement.fides_key",
233
+ )
234
+
235
+
236
+ class TaxonomyUsage(Base):
237
+ """
238
+ The SQL model for taxonomy usage.
239
+ Tracks the application of taxonomy elements to other taxonomy elements.
240
+
241
+ Example: Applying a "high" tag (from sensitivity taxonomy) to "user.contact.email" (from data_categories taxonomy).
242
+ """
243
+
244
+ @declared_attr
245
+ def __tablename__(self) -> str:
246
+ return "taxonomy_usage"
247
+
248
+ # The taxonomy element being applied (e.g., risk)
249
+ source_element_key = Column(String, nullable=False, index=True)
250
+
251
+ # The taxonomy element it's being applied to (e.g., a data category)
252
+ target_element_key = Column(String, nullable=False, index=True)
253
+
254
+ # Denormalized taxonomy types for validation and performance
255
+ source_taxonomy = Column(String, nullable=False, index=True)
256
+ target_taxonomy = Column(String, nullable=False, index=True)
257
+
258
+ __table_args__ = (
259
+ # Validate that this type of usage is allowed
260
+ ForeignKeyConstraint(
261
+ ["source_taxonomy", "target_taxonomy"],
262
+ [
263
+ "taxonomy_allowed_usage.source_taxonomy_key",
264
+ "taxonomy_allowed_usage.target_type",
265
+ ],
266
+ ondelete="RESTRICT",
267
+ name="fk_taxonomy_usage_allowed",
268
+ ),
269
+ # Prevent duplicate applications
270
+ UniqueConstraint(
271
+ "source_element_key",
272
+ "target_element_key",
273
+ name="uq_taxonomy_usage",
274
+ ),
275
+ )
@@ -73,7 +73,8 @@ class ExecutionApplicationConfig(FidesSchema):
73
73
  memory_watchdog_enabled: Optional[bool] = None
74
74
  sql_dry_run: Optional[SqlDryRunMode] = None
75
75
 
76
- model_config = ConfigDict(use_enum_values=True, extra="forbid")
76
+ # Allow deprecated / unknown fields (e.g. “safe_mode”) to pass through
77
+ model_config = ConfigDict(use_enum_values=True, extra="ignore")
77
78
 
78
79
 
79
80
  class AdminUIConfig(FidesSchema):
@@ -12,10 +12,22 @@ class CustomIdentity(FidesSchema):
12
12
  label: str
13
13
 
14
14
 
15
+ class LocationIdentityField(FidesSchema):
16
+ """Location field configuration that extends the useful parts of CustomPrivacyRequestField"""
17
+
18
+ label: str
19
+ required: Optional[bool] = True
20
+ default_value: Optional[str] = None
21
+ query_param_key: Optional[str] = None
22
+ ip_geolocation_hint: Optional[bool] = False
23
+ # Note: We intentionally omit 'hidden' field as it doesn't make sense for location identity input
24
+
25
+
15
26
  class IdentityInputs(FidesSchema):
16
27
  name: Optional[RequiredType] = None
17
28
  email: Optional[RequiredType] = None
18
29
  phone: Optional[RequiredType] = None
30
+ location: Optional[Union[RequiredType, LocationIdentityField]] = None
19
31
  model_config = ConfigDict(extra="allow")
20
32
 
21
33
  def __init__(self, **data: Any):
@@ -30,6 +42,9 @@ class IdentityInputs(FidesSchema):
30
42
  f'Custom identity "{field}" must be an instance of CustomIdentity '
31
43
  '(e.g. {"label": "Field label"})'
32
44
  )
45
+ elif field == "location" and isinstance(value, dict):
46
+ # Handle location field as LocationIdentityField
47
+ data[field] = LocationIdentityField(**value)
33
48
  super().__init__(**data)
34
49
 
35
50
 
fides/api/service/deps.py CHANGED
@@ -8,6 +8,7 @@ from fides.service.dataset.dataset_config_service import DatasetConfigService
8
8
  from fides.service.dataset.dataset_service import DatasetService
9
9
  from fides.service.messaging.messaging_service import MessagingService
10
10
  from fides.service.privacy_request.privacy_request_service import PrivacyRequestService
11
+ from fides.service.taxonomy.taxonomy_service import TaxonomyService
11
12
  from fides.service.user.user_service import UserService
12
13
 
13
14
 
@@ -41,3 +42,7 @@ def get_user_service(
41
42
  config_proxy: ConfigProxy = Depends(get_config_proxy),
42
43
  ) -> UserService:
43
44
  return UserService(db, config, config_proxy)
45
+
46
+
47
+ def get_taxonomy_service(db: Session = Depends(get_db)) -> TaxonomyService:
48
+ return TaxonomyService(db)
@@ -191,7 +191,11 @@ def poll_for_exited_privacy_request_tasks(self: DatabaseTask) -> Set[str]:
191
191
  db.query(PrivacyRequest)
192
192
  .filter(
193
193
  PrivacyRequest.status.in_(
194
- [PrivacyRequestStatus.in_processing, PrivacyRequestStatus.approved]
194
+ [
195
+ PrivacyRequestStatus.in_processing,
196
+ PrivacyRequestStatus.approved,
197
+ PrivacyRequestStatus.requires_input,
198
+ ]
195
199
  )
196
200
  )
197
201
  # Only look at Privacy Requests that haven't been deleted
@@ -546,6 +550,7 @@ def requeue_interrupted_tasks(self: DatabaseTask) -> None:
546
550
  [
547
551
  PrivacyRequestStatus.in_processing,
548
552
  PrivacyRequestStatus.approved,
553
+ PrivacyRequestStatus.requires_input,
549
554
  ]
550
555
  )
551
556
  )
@@ -1,15 +1,20 @@
1
- from typing import Any, Union
1
+ from typing import Any, Optional, Union
2
2
 
3
3
  from loguru import logger
4
4
  from sqlalchemy.orm import Session
5
5
 
6
6
  from fides.api.graph.config import FieldPath
7
- from fides.api.task.conditional_dependencies.operators import operator_methods
7
+ from fides.api.task.conditional_dependencies.operators import (
8
+ LOGICAL_OPERATORS,
9
+ OPERATOR_METHODS,
10
+ )
8
11
  from fides.api.task.conditional_dependencies.schemas import (
9
12
  Condition,
13
+ ConditionEvaluationResult,
10
14
  ConditionGroup,
11
15
  ConditionLeaf,
12
- GroupOperator,
16
+ EvaluationResult,
17
+ GroupEvaluationResult,
13
18
  Operator,
14
19
  )
15
20
 
@@ -19,84 +24,226 @@ class ConditionEvaluationError(Exception):
19
24
 
20
25
 
21
26
  class ConditionEvaluator:
22
- """Evaluates nested conditions for manual task creation"""
27
+ """Evaluates nested conditions and returns a boolean result and a detailed evaluation report"""
23
28
 
24
29
  def __init__(self, db: Session):
25
30
  self.db = db
26
31
 
27
- def evaluate_rule(self, rule: Condition, data: Union[dict, Any]) -> bool:
28
- """Evaluate a nested condition rule against input data"""
32
+ def evaluate_rule(
33
+ self, rule: Condition, data: Union[dict, Any]
34
+ ) -> EvaluationResult:
35
+ """Evaluate a nested condition rule against input data and return detailed results
36
+
37
+ Args:
38
+ rule: The condition rule to evaluate
39
+ data: The data to evaluate the condition against
40
+
41
+ Returns:
42
+ evaluation report: A detailed report of the evaluation
43
+ - The field address of the condition
44
+ - The operator used in the condition
45
+ - The expected value of the condition
46
+ - The actual value of the condition
47
+ - The result of the condition evaluation
48
+ - A message describing the condition evaluation
49
+ """
29
50
  if isinstance(rule, ConditionLeaf):
30
- return self._evaluate_leaf_condition(rule, data)
51
+ leaf_result = self._evaluate_leaf_condition(rule, data)
52
+ return leaf_result
31
53
  # ConditionGroup
32
- return self._evaluate_group_condition(rule, data)
54
+ group_result = self._evaluate_group_condition(rule, data)
55
+ return group_result
33
56
 
34
57
  def _evaluate_leaf_condition(
35
58
  self, condition: ConditionLeaf, data: Union[dict, Any]
36
- ) -> bool:
37
- """Evaluate a leaf condition against input data"""
38
- data_value = self._get_nested_value(data, condition.field_address.split("."))
39
- # Apply operator and return result
40
- return self._apply_operator(data_value, condition.operator, condition.value)
59
+ ) -> ConditionEvaluationResult:
60
+ """Evaluate a leaf condition against input data
61
+
62
+ Args:
63
+ condition: The leaf condition to evaluate
64
+ data: The data to evaluate the condition against
65
+
66
+ Returns:
67
+ A detailed evaluation report for the leaf condition
68
+
69
+ Raises:
70
+ ConditionEvaluationError: If there is an issue applying the operator or if an unexpected error occurs.
71
+ """
72
+ # Handle both colon-separated and dot-separated field addresses
73
+ if ":" in condition.field_address:
74
+ # Full field address like "dataset:collection:field" - split on colons
75
+ keys = condition.field_address.split(":")
76
+ else:
77
+ # Relative field path like "field.subfield" - split on dots
78
+ keys = condition.field_address.split(".")
79
+
80
+ data_value = self._get_nested_value(data, keys)
81
+
82
+ # Apply operator and get result
83
+ try:
84
+ result = self._apply_operator(
85
+ data_value, condition.operator, condition.value
86
+ )
87
+ message = f"Condition '{condition.field_address} {condition.operator} {condition.value}' evaluated to {result}"
88
+ except ConditionEvaluationError as e:
89
+ logger.error(
90
+ f"Unexpected error evaluating condition '{condition.field_address} {condition.operator} {condition.value}': {str(e)}"
91
+ )
92
+ raise
93
+
94
+ return ConditionEvaluationResult(
95
+ field_address=condition.field_address,
96
+ operator=condition.operator,
97
+ expected_value=condition.value,
98
+ actual_value=data_value,
99
+ result=result,
100
+ message=message,
101
+ )
41
102
 
42
103
  def _evaluate_group_condition(
43
104
  self, group: ConditionGroup, data: Union[dict, Any]
44
- ) -> bool:
45
- """Evaluate a group condition against input data"""
105
+ ) -> GroupEvaluationResult:
106
+ """Evaluate a group condition against input data
107
+
108
+ Args:
109
+ group: The group condition to evaluate
110
+ data: The data to evaluate the condition against
111
+
112
+ Returns:
113
+ A detailed evaluation report for the group condition
114
+
115
+ Raises:
116
+ ConditionEvaluationError: If there is an issue evaluating the group condition (e.g., from evaluate_rule calls)
117
+ """
118
+ try:
119
+ operator_func = LOGICAL_OPERATORS[group.logical_operator]
120
+ except KeyError as e:
121
+ raise ConditionEvaluationError(
122
+ f"Unknown logical operator: {group.logical_operator}"
123
+ ) from e
124
+
46
125
  results = [
47
126
  self.evaluate_rule(condition, data) for condition in group.conditions
48
127
  ]
128
+ group_result = operator_func([r.result for r in results])
129
+
130
+ return GroupEvaluationResult(
131
+ logical_operator=group.logical_operator,
132
+ condition_results=results,
133
+ result=group_result,
134
+ )
49
135
 
50
- logical_operators = {GroupOperator.and_: all, GroupOperator.or_: any}
51
- operator_func = logical_operators.get(group.logical_operator)
136
+ def _get_nested_value_from_fides_reference_structure(
137
+ self, data: Any, keys: list[str]
138
+ ) -> Optional[Any]:
139
+ """Get nested value from Fides reference structure
52
140
 
53
- if operator_func is None:
54
- logger.warning(f"Unknown logical operator: {group.logical_operator}")
55
- return False
141
+ Args:
142
+ data: The Fides reference structure to get the nested value from
143
+ keys: The keys to for the specific nested value in the data
56
144
 
57
- return operator_func(results)
145
+ Returns:
146
+ The nested value from the data or None if not a Fides reference structure
147
+
148
+ Raises:
149
+ AttributeError: If the data does not have a get_field_value method
150
+ ValueError: If the keys are not valid for the Fides reference structure
151
+ """
152
+ if hasattr(data, "get_field_value"):
153
+ try:
154
+ field_path = FieldPath(*keys) if len(keys) > 1 else FieldPath(keys[0])
155
+ return data.get_field_value(field_path)
156
+ except (AttributeError, ValueError):
157
+ logger.debug(
158
+ f"Fides reference structure does not have a get_field_value method: {data}"
159
+ )
160
+ raise
161
+ raise ConditionEvaluationError(
162
+ f"Data does not have a get_field_value method: {data}"
163
+ )
164
+
165
+ def _get_nested_value_from_dict(self, data: dict, keys: list[str]) -> Optional[Any]:
166
+ """Get nested value from dictionary. This is the fallback and will return None if the key is not found.
167
+ When the data is missing the None value will work with exists/not_exists operations and correctly evaluate to False
168
+ for other operations like eq, not_eq, etc.
169
+
170
+ Args:
171
+ data: The dictionary to get the nested value from
172
+ keys: The keys to for the specific nested value in the data
173
+
174
+ Returns:
175
+ The nested value from the data
176
+
177
+ Raises:
178
+ KeyError: If the keys are not valid for the dictionary
179
+ """
180
+ current: Any = data
181
+ for key in keys:
182
+ if not isinstance(current, dict):
183
+ return None
184
+ current = current.get(key)
185
+ if current is None:
186
+ return None
187
+ return current
58
188
 
59
189
  def _get_nested_value(self, data: Union[dict, Any], keys: list[str]) -> Any:
60
- """Get nested value from data using dot notation
190
+ """Get nested value from data using dot notation or colon notation
61
191
 
62
192
  Supports both simple dictionary access and Fides reference structures:
63
193
  - Simple dict: data["user"]["name"]
64
194
  - Fides FieldAddress: data.get_field_value(FieldAddress("dataset", "collection", "field_address"))
65
195
  - Fides Collection: data.get_field_value(FieldPath("field_address", "subfield"))
196
+
197
+ Also supports full field addresses with dataset:collection:field format
198
+
199
+ Args:
200
+ data: The data to get the nested value from
201
+ keys: The keys to for the specific nested value in the data
202
+
203
+ Returns:
204
+ The nested value from the data
205
+
206
+ Raises:
207
+ KeyError: If the keys are not valid for the dictionary
66
208
  """
67
209
  if not keys:
68
210
  return data
69
211
 
70
- current = data
71
-
72
212
  # Try Fides reference structures first
73
- if hasattr(current, "get_field_value"):
74
- try:
75
- field_path = FieldPath(*keys) if len(keys) > 1 else FieldPath(keys[0])
76
- return current.get_field_value(field_path)
77
- except (AttributeError, ValueError):
78
- pass
79
-
80
- # Fall back to dictionary access
81
- for key in keys:
82
- if not isinstance(current, dict):
83
- current = current.get(key, {}) if hasattr(current, "get") else None
84
- else:
85
- current = current.get(key, {})
213
+ try:
214
+ return self._get_nested_value_from_fides_reference_structure(data, keys)
215
+ except (AttributeError, ValueError, ConditionEvaluationError):
216
+ pass
86
217
 
87
- return current if current != {} else None
218
+ # Fall back to dictionary access for all path types
219
+ return self._get_nested_value_from_dict(data, keys)
88
220
 
89
221
  def _apply_operator(
90
222
  self, data_value: Any, operator: Operator, user_input_value: Any
91
223
  ) -> bool:
92
- """Apply operator to actual and expected values"""
224
+ """Apply operator to actual and expected values
225
+ The operator is validated in the ConditionLeaf and ConditionGroup schemas,
226
+ so we don't need to validate it here.
93
227
 
228
+ Args:
229
+ data_value: The actual value to evaluate
230
+ operator: The operator to apply
231
+ user_input_value: The expected value to evaluate against
232
+
233
+ Returns:
234
+ The result of the operator applied to the actual and expected values
235
+ """
94
236
  # Get the method for the operator and execute it
95
- operator_method = operator_methods.get(operator)
96
- if operator_method is None:
97
- logger.warning(f"Unknown operator: {operator}")
98
- raise ConditionEvaluationError(f"Unknown operator: {operator}")
99
237
  try:
238
+ operator_method = OPERATOR_METHODS[operator]
100
239
  return operator_method(data_value, user_input_value)
101
- except (TypeError, ValueError) as e:
102
- raise ConditionEvaluationError(f"Error evaluating condition: {e}") from e
240
+ except KeyError as e:
241
+ # Unknown operator
242
+ logger.error(f"Unknown operator: {operator}")
243
+ raise ConditionEvaluationError(f"Unknown operator: {operator}") from e
244
+ except Exception as e:
245
+ # Log unexpected errors but still raise them
246
+ logger.error(f"Unexpected error in operator {operator}: {e}")
247
+ raise ConditionEvaluationError(
248
+ f"Unexpected error evaluating condition: {e}"
249
+ ) from e