ethyca-fides 2.71.0rc3__py2.py3-none-any.whl → 2.71.1__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 (185) hide show
  1. {ethyca_fides-2.71.0rc3.dist-info → ethyca_fides-2.71.1.dist-info}/METADATA +1 -1
  2. {ethyca_fides-2.71.0rc3.dist-info → ethyca_fides-2.71.1.dist-info}/RECORD +168 -153
  3. fides/_version.py +3 -3
  4. fides/api/alembic/migrations/versions/3efe14d4469a_adds_new_experience_configs_for_vendor_.py +79 -0
  5. fides/api/alembic/migrations/versions/4bfbeff34611_add_polling_status.py +68 -0
  6. fides/api/alembic/migrations/versions/7db29f9cd77b_create_new_sub_request_table.py +95 -0
  7. fides/api/alembic/migrations/versions/918aefc950c9_create_digest_conditional_dependencies.py +125 -0
  8. fides/api/alembic/migrations/versions/9caf76161e55_make_user_assigned_data_uses_nullable_.py +64 -0
  9. fides/api/alembic/migrations/versions/b97e92b038d2_add_digest_execution_model.py +117 -0
  10. fides/api/alembic/migrations/versions/f108fa05c579_adds_optional_duration_field_to_assets.py +28 -0
  11. fides/api/common_exceptions.py +4 -0
  12. fides/api/db/base.py +1 -1
  13. fides/api/main.py +2 -2
  14. fides/api/models/asset.py +14 -1
  15. fides/api/models/attachment.py +1 -0
  16. fides/api/models/conditional_dependency/conditional_dependency_base.py +253 -24
  17. fides/api/models/detection_discovery/core.py +57 -3
  18. fides/api/models/digest/__init__.py +7 -1
  19. fides/api/models/digest/conditional_dependencies.py +267 -1
  20. fides/api/models/digest/digest_config.py +44 -10
  21. fides/api/models/digest/digest_execution.py +132 -0
  22. fides/api/models/event_audit.py +8 -0
  23. fides/api/models/fides_user.py +9 -0
  24. fides/api/models/manual_task/conditional_dependency.py +16 -18
  25. fides/api/models/privacy_experience.py +10 -0
  26. fides/api/models/privacy_notice.py +139 -20
  27. fides/api/models/privacy_request/request_task.py +98 -1
  28. fides/api/models/worker_task.py +8 -0
  29. fides/api/schemas/saas/async_polling_configuration.py +81 -0
  30. fides/api/schemas/saas/saas_config.py +10 -3
  31. fides/api/schemas/saas/strategy_configuration.py +0 -12
  32. fides/api/service/async_dsr/handlers/__init__.py +0 -0
  33. fides/api/service/async_dsr/handlers/polling_attachment_handler.py +155 -0
  34. fides/api/service/async_dsr/handlers/polling_request_handler.py +88 -0
  35. fides/api/service/async_dsr/handlers/polling_response_handler.py +261 -0
  36. fides/api/service/async_dsr/handlers/polling_sub_request_handler.py +123 -0
  37. fides/api/service/async_dsr/strategies/__init__.py +0 -0
  38. fides/api/service/async_dsr/strategies/async_dsr_strategy.py +52 -0
  39. fides/api/service/async_dsr/strategies/async_dsr_strategy_callback.py +199 -0
  40. fides/api/service/async_dsr/strategies/async_dsr_strategy_factory.py +72 -0
  41. fides/api/service/async_dsr/strategies/async_dsr_strategy_polling.py +678 -0
  42. fides/api/service/async_dsr/utils.py +130 -0
  43. fides/api/service/connectors/fides/fides_client.py +63 -1
  44. fides/api/service/connectors/query_configs/saas_query_config.py +4 -5
  45. fides/api/service/connectors/saas_connector.py +77 -69
  46. fides/api/service/privacy_request/attachment_handling.py +9 -2
  47. fides/api/service/privacy_request/request_runner_service.py +9 -83
  48. fides/api/service/privacy_request/request_service.py +47 -74
  49. fides/api/service/saas_request/saas_request_override_factory.py +66 -1
  50. fides/api/task/execute_request_tasks.py +5 -2
  51. fides/api/task/filter_results.py +35 -2
  52. fides/api/task/graph_task.py +34 -2
  53. fides/api/task/manual/manual_task_conditional_evaluation.py +1 -1
  54. fides/config/execution_settings.py +7 -3
  55. fides/ui-build/static/admin/404.html +1 -1
  56. fides/ui-build/static/admin/_next/static/-sJd4KUm81_d189v12Jmo/_buildManifest.js +1 -0
  57. fides/ui-build/static/admin/_next/static/chunks/155-c1ae010c664e2245.js +1 -0
  58. fides/ui-build/static/admin/_next/static/chunks/1817-1ad037b7d6d2f6d2.js +1 -0
  59. fides/ui-build/static/admin/_next/static/chunks/5279-12c9cbdc67ad7b14.js +1 -0
  60. fides/ui-build/static/admin/_next/static/chunks/6277-182efc294d413f64.js +1 -0
  61. fides/ui-build/static/admin/_next/static/chunks/7079-bbc7b856802a4834.js +1 -0
  62. fides/ui-build/static/admin/_next/static/chunks/pages/add-systems/{manual-75e99306393938e8.js → manual-4ec03eed67572861.js} +1 -1
  63. fides/ui-build/static/admin/_next/static/chunks/pages/consent/privacy-experience/{[id]-fd41ffaff543e05a.js → [id]-e1e2fd704ac2d71d.js} +1 -1
  64. fides/ui-build/static/admin/_next/static/chunks/pages/consent/privacy-experience/{new-e74cb5ea87f15b40.js → new-a5e738a234dadc7e.js} +1 -1
  65. fides/ui-build/static/admin/_next/static/chunks/pages/consent/privacy-notices/{[id]-9c23fbe813c997d0.js → [id]-5fc78b78a51c239c.js} +1 -1
  66. fides/ui-build/static/admin/_next/static/chunks/pages/consent/privacy-notices/{new-0e5e38bbcfe59fd2.js → new-b79bcb93b5f4c734.js} +1 -1
  67. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center/[monitorId]/[systemId]-29c1fb777bd464e0.js +1 -0
  68. fides/ui-build/static/admin/_next/static/chunks/pages/integrations/[id]-153eb88ab4e7dc6d.js +1 -0
  69. fides/ui-build/static/admin/_next/static/chunks/pages/integrations-f682b1def859931e.js +1 -0
  70. fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/[id]-febf156d2977f3ac.js +1 -0
  71. fides/ui-build/static/admin/_next/static/chunks/pages/settings/consent-4d658222ec800511.js +1 -0
  72. fides/ui-build/static/admin/_next/static/chunks/pages/systems/configure/{[id]-547c6ef0ad52b85d.js → [id]-4d470bbf199a2f9c.js} +1 -1
  73. fides/ui-build/static/admin/_next/static/css/f38242c11f7fea64.css +1 -0
  74. fides/ui-build/static/admin/add-systems/manual.html +1 -1
  75. fides/ui-build/static/admin/add-systems/multiple.html +1 -1
  76. fides/ui-build/static/admin/add-systems.html +1 -1
  77. fides/ui-build/static/admin/consent/configure/add-vendors.html +1 -1
  78. fides/ui-build/static/admin/consent/configure.html +1 -1
  79. fides/ui-build/static/admin/consent/privacy-experience/[id].html +1 -1
  80. fides/ui-build/static/admin/consent/privacy-experience/new.html +1 -1
  81. fides/ui-build/static/admin/consent/privacy-experience.html +1 -1
  82. fides/ui-build/static/admin/consent/privacy-notices/[id].html +1 -1
  83. fides/ui-build/static/admin/consent/privacy-notices/new.html +1 -1
  84. fides/ui-build/static/admin/consent/privacy-notices.html +1 -1
  85. fides/ui-build/static/admin/consent/properties.html +1 -1
  86. fides/ui-build/static/admin/consent/reporting.html +1 -1
  87. fides/ui-build/static/admin/consent.html +1 -1
  88. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn]/[resourceUrn].html +1 -1
  89. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn].html +1 -1
  90. fides/ui-build/static/admin/data-catalog/[systemId]/projects.html +1 -1
  91. fides/ui-build/static/admin/data-catalog/[systemId]/resources/[resourceUrn].html +1 -1
  92. fides/ui-build/static/admin/data-catalog/[systemId]/resources.html +1 -1
  93. fides/ui-build/static/admin/data-catalog.html +1 -1
  94. fides/ui-build/static/admin/data-discovery/action-center/[monitorId]/[systemId].html +1 -1
  95. fides/ui-build/static/admin/data-discovery/action-center/[monitorId].html +1 -1
  96. fides/ui-build/static/admin/data-discovery/action-center.html +1 -1
  97. fides/ui-build/static/admin/data-discovery/activity.html +1 -1
  98. fides/ui-build/static/admin/data-discovery/detection/[resourceUrn].html +1 -1
  99. fides/ui-build/static/admin/data-discovery/detection.html +1 -1
  100. fides/ui-build/static/admin/data-discovery/discovery/[resourceUrn].html +1 -1
  101. fides/ui-build/static/admin/data-discovery/discovery.html +1 -1
  102. fides/ui-build/static/admin/datamap.html +1 -1
  103. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName]/[...subfieldNames].html +1 -1
  104. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName].html +1 -1
  105. fides/ui-build/static/admin/dataset/[datasetId].html +1 -1
  106. fides/ui-build/static/admin/dataset/new.html +1 -1
  107. fides/ui-build/static/admin/dataset.html +1 -1
  108. fides/ui-build/static/admin/datastore-connection/[id].html +1 -1
  109. fides/ui-build/static/admin/datastore-connection/new.html +1 -1
  110. fides/ui-build/static/admin/datastore-connection.html +1 -1
  111. fides/ui-build/static/admin/index.html +1 -1
  112. fides/ui-build/static/admin/integrations/[id].html +1 -1
  113. fides/ui-build/static/admin/integrations.html +1 -1
  114. fides/ui-build/static/admin/lib/fides-headless.js +1 -1
  115. fides/ui-build/static/admin/lib/fides-preview.js +1 -1
  116. fides/ui-build/static/admin/lib/fides-tcf.js +3 -3
  117. fides/ui-build/static/admin/lib/fides.js +3 -3
  118. fides/ui-build/static/admin/login/[provider].html +1 -1
  119. fides/ui-build/static/admin/login.html +1 -1
  120. fides/ui-build/static/admin/messaging/[id].html +1 -1
  121. fides/ui-build/static/admin/messaging/add-template.html +1 -1
  122. fides/ui-build/static/admin/messaging.html +1 -1
  123. fides/ui-build/static/admin/poc/ant-components.html +1 -1
  124. fides/ui-build/static/admin/poc/form-experiments/AntForm.html +1 -1
  125. fides/ui-build/static/admin/poc/form-experiments/FormikAntFormItem.html +1 -1
  126. fides/ui-build/static/admin/poc/form-experiments/FormikControlled.html +1 -1
  127. fides/ui-build/static/admin/poc/form-experiments/FormikField.html +1 -1
  128. fides/ui-build/static/admin/poc/form-experiments/FormikSpreadField.html +1 -1
  129. fides/ui-build/static/admin/poc/forms.html +1 -1
  130. fides/ui-build/static/admin/poc/table-migration.html +1 -1
  131. fides/ui-build/static/admin/privacy-requests/[id].html +1 -1
  132. fides/ui-build/static/admin/privacy-requests/configure/storage.html +1 -1
  133. fides/ui-build/static/admin/privacy-requests/configure.html +1 -1
  134. fides/ui-build/static/admin/privacy-requests.html +1 -1
  135. fides/ui-build/static/admin/properties/[id].html +1 -1
  136. fides/ui-build/static/admin/properties/add-property.html +1 -1
  137. fides/ui-build/static/admin/properties.html +1 -1
  138. fides/ui-build/static/admin/reporting/datamap.html +1 -1
  139. fides/ui-build/static/admin/settings/about/alpha.html +1 -1
  140. fides/ui-build/static/admin/settings/about.html +1 -1
  141. fides/ui-build/static/admin/settings/consent/[configuration_id]/[purpose_id].html +1 -1
  142. fides/ui-build/static/admin/settings/consent.html +1 -1
  143. fides/ui-build/static/admin/settings/custom-fields/[id].html +1 -1
  144. fides/ui-build/static/admin/settings/custom-fields/new.html +1 -1
  145. fides/ui-build/static/admin/settings/custom-fields.html +1 -1
  146. fides/ui-build/static/admin/settings/domain-records.html +1 -1
  147. fides/ui-build/static/admin/settings/domains.html +1 -1
  148. fides/ui-build/static/admin/settings/email-templates.html +1 -1
  149. fides/ui-build/static/admin/settings/locations.html +1 -1
  150. fides/ui-build/static/admin/settings/messaging-providers/[key].html +1 -1
  151. fides/ui-build/static/admin/settings/messaging-providers/new.html +1 -1
  152. fides/ui-build/static/admin/settings/messaging-providers.html +1 -1
  153. fides/ui-build/static/admin/settings/organization.html +1 -1
  154. fides/ui-build/static/admin/settings/privacy-requests.html +1 -1
  155. fides/ui-build/static/admin/settings/regulations.html +1 -1
  156. fides/ui-build/static/admin/systems/configure/[id]/test-datasets.html +1 -1
  157. fides/ui-build/static/admin/systems/configure/[id].html +1 -1
  158. fides/ui-build/static/admin/systems.html +1 -1
  159. fides/ui-build/static/admin/taxonomy.html +1 -1
  160. fides/ui-build/static/admin/user-management/new.html +1 -1
  161. fides/ui-build/static/admin/user-management/profile/[id].html +1 -1
  162. fides/ui-build/static/admin/user-management.html +1 -1
  163. fides/api/service/async_dsr/async_dsr_service.py +0 -195
  164. fides/api/service/async_dsr/async_dsr_strategy.py +0 -5
  165. fides/api/service/async_dsr/async_dsr_strategy_callback.py +0 -16
  166. fides/api/service/async_dsr/async_dsr_strategy_factory.py +0 -63
  167. fides/api/service/async_dsr/async_dsr_strategy_polling.py +0 -94
  168. fides/ui-build/static/admin/_next/static/Iszit6QyBe_fIacNxpyuQ/_buildManifest.js +0 -1
  169. fides/ui-build/static/admin/_next/static/chunks/155-047c3806cc41295e.js +0 -1
  170. fides/ui-build/static/admin/_next/static/chunks/1817-ca6473f31a67a804.js +0 -1
  171. fides/ui-build/static/admin/_next/static/chunks/3700-08e0703b1ef770da.js +0 -1
  172. fides/ui-build/static/admin/_next/static/chunks/6084-d0943ee628bf4388.js +0 -1
  173. fides/ui-build/static/admin/_next/static/chunks/6416-0ccadfefcdad00cc.js +0 -1
  174. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center/[monitorId]/[systemId]-2e1e2b7808d3b21f.js +0 -1
  175. fides/ui-build/static/admin/_next/static/chunks/pages/integrations/[id]-01e025f878ba806c.js +0 -1
  176. fides/ui-build/static/admin/_next/static/chunks/pages/integrations-14120a529d7dac27.js +0 -1
  177. fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/[id]-7dac2302f573f5ee.js +0 -1
  178. fides/ui-build/static/admin/_next/static/chunks/pages/settings/consent-e5d781b28f8e29c8.js +0 -1
  179. fides/ui-build/static/admin/_next/static/css/073713cd1eddda79.css +0 -1
  180. {ethyca_fides-2.71.0rc3.dist-info → ethyca_fides-2.71.1.dist-info}/WHEEL +0 -0
  181. {ethyca_fides-2.71.0rc3.dist-info → ethyca_fides-2.71.1.dist-info}/entry_points.txt +0 -0
  182. {ethyca_fides-2.71.0rc3.dist-info → ethyca_fides-2.71.1.dist-info}/licenses/LICENSE +0 -0
  183. {ethyca_fides-2.71.0rc3.dist-info → ethyca_fides-2.71.1.dist-info}/top_level.txt +0 -0
  184. /fides/ui-build/static/admin/_next/static/{Iszit6QyBe_fIacNxpyuQ → -sJd4KUm81_d189v12Jmo}/_ssgManifest.js +0 -0
  185. /fides/ui-build/static/admin/_next/static/chunks/pages/{_app-a77584f9ad3334af.js → _app-a7c02dd2ff07f9e1.js} +0 -0
@@ -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
+ }
@@ -1,14 +1,17 @@
1
1
  from enum import Enum
2
- from typing import Optional, Union
2
+ from typing import TYPE_CHECKING, Optional, Union
3
3
 
4
4
  from sqlalchemy import Boolean, Column, DateTime, String, Text
5
5
  from sqlalchemy.dialects.postgresql import JSONB
6
6
  from sqlalchemy.ext.declarative import declared_attr
7
- from sqlalchemy.orm import Session
7
+ from sqlalchemy.orm import Session, relationship
8
8
 
9
9
  from fides.api.db.base_class import Base
10
10
  from fides.api.db.util import EnumColumn
11
- from fides.api.models.digest.conditional_dependencies import DigestConditionType
11
+ from fides.api.models.digest.conditional_dependencies import (
12
+ DigestCondition,
13
+ DigestConditionType,
14
+ )
12
15
  from fides.api.schemas.messaging.messaging import MessagingMethod
13
16
  from fides.api.task.conditional_dependencies.schemas import (
14
17
  Condition,
@@ -16,6 +19,9 @@ from fides.api.task.conditional_dependencies.schemas import (
16
19
  ConditionLeaf,
17
20
  )
18
21
 
22
+ if TYPE_CHECKING:
23
+ from fides.api.models.digest.digest_execution import DigestTaskExecution
24
+
19
25
 
20
26
  class DigestType(str, Enum):
21
27
  """Types of digests that can be configured."""
@@ -51,26 +57,54 @@ class DigestConfig(Base):
51
57
  next_scheduled_at = Column(DateTime(timezone=True), nullable=True, index=True)
52
58
  config_metadata = Column(JSONB, nullable=True, default={})
53
59
 
54
- def get_receiver_conditions(
60
+ # Relationships
61
+ conditions = relationship(
62
+ "DigestCondition",
63
+ back_populates="digest_config",
64
+ cascade="all, delete-orphan",
65
+ )
66
+ executions = relationship(
67
+ "DigestTaskExecution",
68
+ back_populates="digest_config",
69
+ cascade="all, delete-orphan",
70
+ order_by="DigestTaskExecution.created_at.desc()",
71
+ )
72
+
73
+ def get_receiver_condition(
55
74
  self, db: Session
56
75
  ) -> Optional[Union[ConditionLeaf, ConditionGroup]]:
57
76
  """Get receiver conditions for this digest config."""
58
- raise NotImplementedError("Conditions are not implemented for digests")
77
+ return DigestCondition.get_root_condition(
78
+ db,
79
+ digest_config_id=self.id,
80
+ digest_condition_type=DigestConditionType.RECEIVER,
81
+ )
59
82
 
60
- def get_content_conditions(
83
+ def get_content_condition(
61
84
  self, db: Session
62
85
  ) -> Optional[Union[ConditionLeaf, ConditionGroup]]:
63
86
  """Get content conditions for this digest config."""
64
- raise NotImplementedError("Conditions are not implemented for digests")
65
87
 
66
- def get_priority_conditions(
88
+ return DigestCondition.get_root_condition(
89
+ db,
90
+ digest_config_id=self.id,
91
+ digest_condition_type=DigestConditionType.CONTENT,
92
+ )
93
+
94
+ def get_priority_condition(
67
95
  self, db: Session
68
96
  ) -> Optional[Union[ConditionLeaf, ConditionGroup]]:
69
97
  """Get priority conditions for this digest config."""
70
- raise NotImplementedError("Conditions are not implemented for digests")
98
+
99
+ return DigestCondition.get_root_condition(
100
+ db,
101
+ digest_config_id=self.id,
102
+ digest_condition_type=DigestConditionType.PRIORITY,
103
+ )
71
104
 
72
105
  def get_all_conditions(
73
106
  self, db: Session
74
107
  ) -> dict["DigestConditionType", Optional[Condition]]:
75
108
  """Get all condition types for this digest config."""
76
- raise NotImplementedError("Conditions are not implemented for digests")
109
+
110
+ return DigestCondition.get_all_root_conditions(db, self.id)
@@ -0,0 +1,132 @@
1
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
2
+
3
+ from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text
4
+ from sqlalchemy.dialects.postgresql import JSONB
5
+ from sqlalchemy.ext.declarative import declared_attr
6
+ from sqlalchemy.orm import Session, relationship
7
+ from sqlalchemy.sql import func
8
+
9
+ from fides.api.db.base_class import Base
10
+ from fides.api.models.worker_task import ExecutionLogStatus, WorkerTask
11
+
12
+ if TYPE_CHECKING:
13
+ from fides.api.models.digest.digest_config import DigestConfig
14
+
15
+
16
+ class DigestTaskExecution(
17
+ WorkerTask, Base
18
+ ): # pylint: disable=too-many-instance-attributes
19
+ """
20
+ Model for tracking digest task execution state and progress.
21
+
22
+ This model enables graceful resumption of digest tasks after worker
23
+ interruptions by persisting execution state and progress information.
24
+ """
25
+
26
+ @declared_attr
27
+ def __tablename__(cls) -> str:
28
+ return "digest_task_execution"
29
+
30
+ # Foreign key to digest config
31
+ digest_config_id = Column(
32
+ String,
33
+ ForeignKey("digest_config.id", ondelete="CASCADE"),
34
+ nullable=False,
35
+ index=True,
36
+ )
37
+
38
+ # Celery task tracking
39
+ celery_task_id = Column(String, nullable=True, index=True)
40
+
41
+ # Progress tracking
42
+ total_recipients = Column(Integer, nullable=True)
43
+ processed_recipients = Column(Integer, nullable=False, default=0)
44
+ successful_communications = Column(Integer, nullable=False, default=0)
45
+ failed_communications = Column(Integer, nullable=False, default=0)
46
+
47
+ # State persistence for resumption
48
+ execution_state = Column(JSONB, nullable=True, default={})
49
+ processed_user_ids = Column(JSONB, nullable=True, default=[])
50
+
51
+ # Timing information
52
+ started_at = Column(DateTime(timezone=True), nullable=True)
53
+ completed_at = Column(DateTime(timezone=True), nullable=True)
54
+ last_checkpoint_at = Column(DateTime(timezone=True), nullable=True)
55
+
56
+ # Error information
57
+ error_message = Column(Text, nullable=True)
58
+
59
+ # Relationships
60
+ digest_config = relationship("DigestConfig", back_populates="executions")
61
+
62
+ @classmethod
63
+ def allowed_action_types(cls) -> List[str]:
64
+ """Return allowed action types for digest task execution."""
65
+ return ["digest_processing"]
66
+
67
+ def mark_started(self, db: Session, celery_task_id: str) -> None:
68
+ """Mark the execution as started."""
69
+ self.status = ExecutionLogStatus.in_processing
70
+ self.celery_task_id = celery_task_id
71
+ self.started_at = func.now()
72
+ self.save(db)
73
+
74
+ def mark_awaiting_processing(self, db: Session) -> None:
75
+ """Mark the execution as awaiting processing."""
76
+ self.status = ExecutionLogStatus.awaiting_processing
77
+ self.save(db)
78
+
79
+ def update_progress(
80
+ self,
81
+ db: Session,
82
+ processed_count: int,
83
+ successful_count: int,
84
+ failed_count: int,
85
+ processed_user_ids: List[str],
86
+ execution_state: Optional[Dict[str, Any]] = None,
87
+ ) -> None:
88
+ """Update execution progress and create checkpoint."""
89
+ self.processed_recipients = processed_count
90
+ self.successful_communications = successful_count
91
+ self.failed_communications = failed_count
92
+ self.processed_user_ids = processed_user_ids
93
+ self.last_checkpoint_at = func.now()
94
+
95
+ if execution_state:
96
+ self.execution_state = execution_state
97
+
98
+ self.save(db)
99
+
100
+ def mark_completed(self, db: Session) -> None:
101
+ """Mark the execution as completed."""
102
+ self.status = ExecutionLogStatus.complete
103
+ self.completed_at = func.now()
104
+ self.save(db)
105
+
106
+ def mark_failed(self, db: Session, error_message: str) -> None:
107
+ """Mark the execution as failed."""
108
+ self.status = ExecutionLogStatus.error
109
+ self.error_message = error_message
110
+ self.completed_at = func.now()
111
+ self.save(db)
112
+
113
+ def can_resume(self) -> bool:
114
+ """Check if this execution can be resumed."""
115
+ return (
116
+ self.status
117
+ in [
118
+ ExecutionLogStatus.in_processing,
119
+ ExecutionLogStatus.awaiting_processing,
120
+ ]
121
+ and self.processed_user_ids is not None
122
+ )
123
+
124
+ def get_remaining_work(self) -> Dict[str, Any]:
125
+ """Get information about remaining work for resumption."""
126
+ return {
127
+ "processed_user_ids": self.processed_user_ids or [],
128
+ "execution_state": self.execution_state or {},
129
+ "processed_count": self.processed_recipients or 0,
130
+ "successful_count": self.successful_communications,
131
+ "failed_count": self.failed_communications,
132
+ }
@@ -31,6 +31,14 @@ class EventAuditType(str, EnumType):
31
31
  taxonomy_element_updated = "taxonomy.element.updated"
32
32
  taxonomy_element_deleted = "taxonomy.element.deleted"
33
33
 
34
+ # Digest
35
+ digest_execution_started = "digest.execution.started"
36
+ digest_execution_completed = "digest.execution.completed"
37
+ digest_execution_interrupted = "digest.execution.interrupted"
38
+ digest_execution_resumed = "digest.execution.resumed"
39
+ digest_communications_sent = "digest.communications.sent"
40
+ digest_checkpoint_created = "digest.checkpoint.created"
41
+
34
42
 
35
43
  class EventAuditStatus(str, EnumType):
36
44
  """Status enum for event audit logging."""
@@ -31,6 +31,7 @@ if TYPE_CHECKING:
31
31
  from fides.api.models.fides_user_respondent_email_verification import (
32
32
  FidesUserRespondentEmailVerification,
33
33
  )
34
+ from fides.api.models.manual_task.manual_task import ManualTaskReference
34
35
  from fides.api.models.sql_models import System # type: ignore[attr-defined]
35
36
  from fides.api.models.system_manager import SystemManager
36
37
 
@@ -90,6 +91,14 @@ class FidesUser(Base):
90
91
  uselist=False,
91
92
  )
92
93
 
94
+ # Manual task assignments relationship
95
+ manual_task_references = relationship(
96
+ "ManualTaskReference",
97
+ primaryjoin="and_(FidesUser.id == foreign(ManualTaskReference.reference_id), "
98
+ "ManualTaskReference.reference_type == 'assigned_user')",
99
+ viewonly=True,
100
+ )
101
+
93
102
  @property
94
103
  def system_ids(self) -> List[str]:
95
104
  return [system.id for system in self.systems]
@@ -1,13 +1,12 @@
1
1
  from typing import TYPE_CHECKING, Any, Optional, Union
2
2
 
3
- from sqlalchemy import Column, ForeignKey, Index, String
3
+ from sqlalchemy import Column, ForeignKey, String
4
4
  from sqlalchemy.ext.declarative import declared_attr
5
5
  from sqlalchemy.orm import Session, relationship
6
6
 
7
7
  from fides.api.db.base_class import FidesBase
8
8
  from fides.api.models.conditional_dependency.conditional_dependency_base import (
9
9
  ConditionalDependencyBase,
10
- ConditionalDependencyType,
11
10
  )
12
11
  from fides.api.task.conditional_dependencies.schemas import (
13
12
  ConditionGroup,
@@ -30,19 +29,16 @@ class ManualTaskConditionalDependency(ConditionalDependencyBase):
30
29
  id = Column(String(255), primary_key=True, default=FidesBase.generate_uuid)
31
30
  # Foreign key relationships
32
31
  manual_task_id = Column(
33
- String, ForeignKey("manual_task.id", ondelete="CASCADE"), nullable=False
32
+ String,
33
+ ForeignKey("manual_task.id", ondelete="CASCADE"),
34
+ nullable=False,
35
+ index=True,
34
36
  )
35
37
  parent_id = Column(
36
38
  String,
37
39
  ForeignKey("manual_task_conditional_dependency.id", ondelete="CASCADE"),
38
40
  nullable=True,
39
- )
40
-
41
- __table_args__ = (
42
- Index("ix_manual_task_conditional_dependency_manual_task_id", "manual_task_id"),
43
- Index("ix_manual_task_conditional_dependency_parent_id", "parent_id"),
44
- Index("ix_manual_task_conditional_dependency_condition_type", "condition_type"),
45
- Index("ix_manual_task_conditional_dependency_sort_order", "sort_order"),
41
+ index=True,
46
42
  )
47
43
 
48
44
  # Relationships
@@ -62,18 +58,22 @@ class ManualTaskConditionalDependency(ConditionalDependencyBase):
62
58
 
63
59
  @classmethod
64
60
  def get_root_condition(
65
- cls, db: Session, *args: Any, **kwargs: Any
61
+ cls, db: Session, **kwargs: Any
66
62
  ) -> Optional[Union[ConditionLeaf, ConditionGroup]]:
67
63
  """Get the root condition for a manual task
68
64
 
69
65
  Args:
70
66
  db: Database session
71
- manual_task_id: ID of the manual task (first positional arg)
67
+ **kwargs: Keyword arguments containing:
68
+ manual_task_id: ID of the manual task
69
+
70
+ Raises:
71
+ ValueError: If manual_task_id is not provided
72
72
  """
73
- if not args:
74
- raise ValueError("manual_task_id is required as first positional argument")
73
+ manual_task_id = kwargs.get("manual_task_id")
75
74
 
76
- manual_task_id = args[0]
75
+ if not manual_task_id:
76
+ raise ValueError("manual_task_id is required as a keyword argument")
77
77
  root = (
78
78
  db.query(cls)
79
79
  .filter(cls.manual_task_id == manual_task_id, cls.parent_id.is_(None))
@@ -83,6 +83,4 @@ class ManualTaskConditionalDependency(ConditionalDependencyBase):
83
83
  if not root:
84
84
  return None
85
85
 
86
- if root.condition_type == ConditionalDependencyType.leaf:
87
- return root.to_condition_leaf()
88
- return root.to_condition_group()
86
+ return root.to_correct_condition_type()
@@ -108,6 +108,10 @@ class PrivacyExperienceConfigBase:
108
108
 
109
109
  show_layer1_notices = Column(Boolean, nullable=True, default=False)
110
110
 
111
+ # Vendor/Asset disclosure configuration
112
+ allow_vendor_asset_disclosure = Column(Boolean, nullable=True, default=False)
113
+ asset_disclosure_include_types = Column(ARRAY(String), nullable=True)
114
+
111
115
  @declared_attr
112
116
  def layer1_button_options(cls) -> Column:
113
117
  return Column(
@@ -142,6 +146,9 @@ class ExperienceConfigTemplate(PrivacyExperienceConfigBase, Base):
142
146
  dismissable = Column(
143
147
  Boolean, nullable=False, default=True, server_default="t"
144
148
  ) # Overrides PrivacyExperienceConfigBase to make non-nullable
149
+ allow_vendor_asset_disclosure = Column(
150
+ Boolean, nullable=False, default=False, server_default="f"
151
+ ) # Overrides PrivacyExperienceConfigBase to make non-nullable
145
152
  name = Column(
146
153
  String, nullable=False
147
154
  ) # Overriding PrivacyExperienceConfigBase to make non-nullable
@@ -216,6 +223,9 @@ class PrivacyExperienceConfig(PrivacyExperienceConfigBase, Base):
216
223
  dismissable = Column(
217
224
  Boolean, nullable=False, default=True, server_default="t"
218
225
  ) # Overrides PrivacyExperienceConfigBase to make non-nullable
226
+ allow_vendor_asset_disclosure = Column(
227
+ Boolean, nullable=False, default=False, server_default="f"
228
+ ) # Overrides PrivacyExperienceConfigBase to make non-nullable
219
229
  name = Column(
220
230
  String, nullable=False
221
231
  ) # Overriding PrivacyExperienceConfigBase to make non-nullable