ethyca-fides 2.64.0rc0__py2.py3-none-any.whl → 2.64.1b1__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.
Files changed (107) hide show
  1. {ethyca_fides-2.64.0rc0.dist-info → ethyca_fides-2.64.1b1.dist-info}/METADATA +2 -2
  2. {ethyca_fides-2.64.0rc0.dist-info → ethyca_fides-2.64.1b1.dist-info}/RECORD +107 -104
  3. fides/_version.py +3 -3
  4. fides/api/alembic/migrations/versions/6a76a1fa4f3f_add_manual_task_instance_table.py +256 -0
  5. fides/api/db/base.py +4 -0
  6. fides/api/models/manual_tasks/__init__.py +7 -1
  7. fides/api/models/manual_tasks/manual_task.py +19 -3
  8. fides/api/models/manual_tasks/manual_task_config.py +39 -6
  9. fides/api/models/manual_tasks/manual_task_instance.py +187 -0
  10. fides/api/models/manual_tasks/manual_task_log.py +20 -7
  11. fides/api/schemas/manual_tasks/manual_task_schemas.py +42 -0
  12. fides/api/schemas/manual_tasks/manual_task_status.py +107 -46
  13. fides/api/service/connectors/postgres_connector.py +2 -2
  14. fides/service/manual_tasks/manual_task_config_service.py +17 -5
  15. fides/service/manual_tasks/manual_task_instance_service.py +285 -0
  16. fides/service/manual_tasks/manual_task_service.py +66 -10
  17. fides/ui-build/static/admin/404.html +1 -1
  18. fides/ui-build/static/admin/add-systems/manual.html +1 -1
  19. fides/ui-build/static/admin/add-systems/multiple.html +1 -1
  20. fides/ui-build/static/admin/add-systems.html +1 -1
  21. fides/ui-build/static/admin/consent/configure/add-vendors.html +1 -1
  22. fides/ui-build/static/admin/consent/configure.html +1 -1
  23. fides/ui-build/static/admin/consent/privacy-experience/[id].html +1 -1
  24. fides/ui-build/static/admin/consent/privacy-experience/new.html +1 -1
  25. fides/ui-build/static/admin/consent/privacy-experience.html +1 -1
  26. fides/ui-build/static/admin/consent/privacy-notices/[id].html +1 -1
  27. fides/ui-build/static/admin/consent/privacy-notices/new.html +1 -1
  28. fides/ui-build/static/admin/consent/privacy-notices.html +1 -1
  29. fides/ui-build/static/admin/consent/properties.html +1 -1
  30. fides/ui-build/static/admin/consent/reporting.html +1 -1
  31. fides/ui-build/static/admin/consent.html +1 -1
  32. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn]/[resourceUrn].html +1 -1
  33. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn].html +1 -1
  34. fides/ui-build/static/admin/data-catalog/[systemId]/projects.html +1 -1
  35. fides/ui-build/static/admin/data-catalog/[systemId]/resources/[resourceUrn].html +1 -1
  36. fides/ui-build/static/admin/data-catalog/[systemId]/resources.html +1 -1
  37. fides/ui-build/static/admin/data-catalog.html +1 -1
  38. fides/ui-build/static/admin/data-discovery/action-center/[monitorId]/[systemId].html +1 -1
  39. fides/ui-build/static/admin/data-discovery/action-center/[monitorId].html +1 -1
  40. fides/ui-build/static/admin/data-discovery/action-center.html +1 -1
  41. fides/ui-build/static/admin/data-discovery/activity.html +1 -1
  42. fides/ui-build/static/admin/data-discovery/detection/[resourceUrn].html +1 -1
  43. fides/ui-build/static/admin/data-discovery/detection.html +1 -1
  44. fides/ui-build/static/admin/data-discovery/discovery/[resourceUrn].html +1 -1
  45. fides/ui-build/static/admin/data-discovery/discovery.html +1 -1
  46. fides/ui-build/static/admin/datamap.html +1 -1
  47. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName]/[...subfieldNames].html +1 -1
  48. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName].html +1 -1
  49. fides/ui-build/static/admin/dataset/[datasetId].html +1 -1
  50. fides/ui-build/static/admin/dataset/new.html +1 -1
  51. fides/ui-build/static/admin/dataset.html +1 -1
  52. fides/ui-build/static/admin/datastore-connection/[id].html +1 -1
  53. fides/ui-build/static/admin/datastore-connection/new.html +1 -1
  54. fides/ui-build/static/admin/datastore-connection.html +1 -1
  55. fides/ui-build/static/admin/index.html +1 -1
  56. fides/ui-build/static/admin/integrations/[id].html +1 -1
  57. fides/ui-build/static/admin/integrations.html +1 -1
  58. fides/ui-build/static/admin/lib/fides-headless.js +1 -1
  59. fides/ui-build/static/admin/lib/fides-preview.js +1 -1
  60. fides/ui-build/static/admin/lib/fides-tcf.js +2 -2
  61. fides/ui-build/static/admin/lib/fides.js +2 -2
  62. fides/ui-build/static/admin/login/[provider].html +1 -1
  63. fides/ui-build/static/admin/login.html +1 -1
  64. fides/ui-build/static/admin/messaging/[id].html +1 -1
  65. fides/ui-build/static/admin/messaging/add-template.html +1 -1
  66. fides/ui-build/static/admin/messaging.html +1 -1
  67. fides/ui-build/static/admin/poc/ant-components.html +1 -1
  68. fides/ui-build/static/admin/poc/form-experiments/AntForm.html +1 -1
  69. fides/ui-build/static/admin/poc/form-experiments/FormikAntFormItem.html +1 -1
  70. fides/ui-build/static/admin/poc/form-experiments/FormikControlled.html +1 -1
  71. fides/ui-build/static/admin/poc/form-experiments/FormikField.html +1 -1
  72. fides/ui-build/static/admin/poc/form-experiments/FormikSpreadField.html +1 -1
  73. fides/ui-build/static/admin/poc/forms.html +1 -1
  74. fides/ui-build/static/admin/poc/table-migration.html +1 -1
  75. fides/ui-build/static/admin/privacy-requests/[id].html +1 -1
  76. fides/ui-build/static/admin/privacy-requests/configure/messaging.html +1 -1
  77. fides/ui-build/static/admin/privacy-requests/configure/storage.html +1 -1
  78. fides/ui-build/static/admin/privacy-requests/configure.html +1 -1
  79. fides/ui-build/static/admin/privacy-requests.html +1 -1
  80. fides/ui-build/static/admin/properties/[id].html +1 -1
  81. fides/ui-build/static/admin/properties/add-property.html +1 -1
  82. fides/ui-build/static/admin/properties.html +1 -1
  83. fides/ui-build/static/admin/reporting/datamap.html +1 -1
  84. fides/ui-build/static/admin/settings/about/alpha.html +1 -1
  85. fides/ui-build/static/admin/settings/about.html +1 -1
  86. fides/ui-build/static/admin/settings/consent/[configuration_id]/[purpose_id].html +1 -1
  87. fides/ui-build/static/admin/settings/consent.html +1 -1
  88. fides/ui-build/static/admin/settings/custom-fields.html +1 -1
  89. fides/ui-build/static/admin/settings/domain-records.html +1 -1
  90. fides/ui-build/static/admin/settings/domains.html +1 -1
  91. fides/ui-build/static/admin/settings/email-templates.html +1 -1
  92. fides/ui-build/static/admin/settings/locations.html +1 -1
  93. fides/ui-build/static/admin/settings/organization.html +1 -1
  94. fides/ui-build/static/admin/settings/regulations.html +1 -1
  95. fides/ui-build/static/admin/systems/configure/[id]/test-datasets.html +1 -1
  96. fides/ui-build/static/admin/systems/configure/[id].html +1 -1
  97. fides/ui-build/static/admin/systems.html +1 -1
  98. fides/ui-build/static/admin/taxonomy.html +1 -1
  99. fides/ui-build/static/admin/user-management/new.html +1 -1
  100. fides/ui-build/static/admin/user-management/profile/[id].html +1 -1
  101. fides/ui-build/static/admin/user-management.html +1 -1
  102. {ethyca_fides-2.64.0rc0.dist-info → ethyca_fides-2.64.1b1.dist-info}/WHEEL +0 -0
  103. {ethyca_fides-2.64.0rc0.dist-info → ethyca_fides-2.64.1b1.dist-info}/entry_points.txt +0 -0
  104. {ethyca_fides-2.64.0rc0.dist-info → ethyca_fides-2.64.1b1.dist-info}/licenses/LICENSE +0 -0
  105. {ethyca_fides-2.64.0rc0.dist-info → ethyca_fides-2.64.1b1.dist-info}/top_level.txt +0 -0
  106. /fides/ui-build/static/admin/_next/static/{lDN8BtGGiw3b60__SDnAa → zBkPKRGECPjwEx0G7BvHe}/_buildManifest.js +0 -0
  107. /fides/ui-build/static/admin/_next/static/{lDN8BtGGiw3b60__SDnAa → zBkPKRGECPjwEx0G7BvHe}/_ssgManifest.js +0 -0
@@ -3,12 +3,18 @@ from fides.api.models.manual_tasks.manual_task_config import (
3
3
  ManualTaskConfig,
4
4
  ManualTaskConfigField,
5
5
  )
6
+ from fides.api.models.manual_tasks.manual_task_instance import (
7
+ ManualTaskInstance,
8
+ ManualTaskSubmission,
9
+ )
6
10
  from fides.api.models.manual_tasks.manual_task_log import ManualTaskLog
7
11
 
8
12
  __all__ = [
9
13
  "ManualTask",
10
14
  "ManualTaskConfig",
11
15
  "ManualTaskConfigField",
12
- "ManualTaskReference",
16
+ "ManualTaskInstance",
13
17
  "ManualTaskLog",
18
+ "ManualTaskReference",
19
+ "ManualTaskSubmission",
14
20
  ]
@@ -1,6 +1,6 @@
1
1
  from typing import TYPE_CHECKING, Any
2
2
 
3
- from sqlalchemy import Column, DateTime, ForeignKey, 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
 
@@ -18,6 +18,10 @@ if TYPE_CHECKING: # pragma: no cover
18
18
  from fides.api.models.manual_tasks.manual_task_config import ( # pragma: no cover
19
19
  ManualTaskConfig,
20
20
  )
21
+ from fides.api.models.manual_tasks.manual_task_instance import (
22
+ ManualTaskInstance,
23
+ ManualTaskSubmission,
24
+ )
21
25
 
22
26
 
23
27
  class ManualTask(Base):
@@ -48,7 +52,6 @@ class ManualTask(Base):
48
52
  nullable=False,
49
53
  default=ManualTaskParentEntityType.connection_config,
50
54
  )
51
- due_date = Column(DateTime, nullable=True)
52
55
 
53
56
  # Relationships
54
57
  references = relationship(
@@ -68,6 +71,19 @@ class ManualTask(Base):
68
71
  "ManualTaskConfig",
69
72
  back_populates="task",
70
73
  cascade="all, delete-orphan",
74
+ uselist=True,
75
+ )
76
+ instances = relationship(
77
+ "ManualTaskInstance",
78
+ back_populates="task",
79
+ viewonly=True,
80
+ uselist=True,
81
+ )
82
+ submissions = relationship(
83
+ "ManualTaskSubmission",
84
+ back_populates="task",
85
+ uselist=True,
86
+ viewonly=True,
71
87
  )
72
88
 
73
89
  # Properties
@@ -117,4 +133,4 @@ class ManualTaskReference(Base):
117
133
  reference_type = Column(EnumColumn(ManualTaskReferenceType), nullable=False)
118
134
 
119
135
  # Relationships
120
- task = relationship("ManualTask", back_populates="references")
136
+ task = relationship("ManualTask", back_populates="references", viewonly=True)
@@ -1,4 +1,4 @@
1
- from typing import TYPE_CHECKING, Any, Optional
1
+ from typing import TYPE_CHECKING, Any, Optional, cast
2
2
 
3
3
  from sqlalchemy import Boolean, Column, ForeignKey, Integer, String
4
4
  from sqlalchemy.dialects.postgresql import JSONB
@@ -13,10 +13,19 @@ from fides.api.schemas.manual_tasks.manual_task_config import (
13
13
  ManualTaskFieldMetadata,
14
14
  ManualTaskFieldType,
15
15
  )
16
- from fides.api.schemas.manual_tasks.manual_task_schemas import ManualTaskLogStatus
16
+ from fides.api.schemas.manual_tasks.manual_task_schemas import (
17
+ ManualTaskExecutionTiming,
18
+ ManualTaskLogStatus,
19
+ )
17
20
 
18
21
  if TYPE_CHECKING: # pragma: no cover
19
22
  from fides.api.models.manual_tasks.manual_task import ManualTask # pragma: no cover
23
+ from fides.api.models.manual_tasks.manual_task_instance import (
24
+ ManualTaskInstance, # pragma: no cover
25
+ )
26
+ from fides.api.models.manual_tasks.manual_task_instance import (
27
+ ManualTaskSubmission, # pragma: no cover
28
+ )
20
29
 
21
30
 
22
31
  class ManualTaskConfig(Base):
@@ -32,9 +41,20 @@ class ManualTaskConfig(Base):
32
41
  config_type = Column(EnumColumn(ManualTaskConfigurationType), nullable=False)
33
42
  version = Column(Integer, nullable=False, default=1)
34
43
  is_current = Column(Boolean, nullable=False, default=True)
44
+ execution_timing = Column(
45
+ EnumColumn(ManualTaskExecutionTiming),
46
+ nullable=False,
47
+ default=ManualTaskExecutionTiming.pre_execution,
48
+ )
35
49
 
36
50
  # Relationships
37
- task = relationship("ManualTask", back_populates="configs")
51
+ task = relationship("ManualTask", back_populates="configs", viewonly=True)
52
+ instances = relationship(
53
+ "ManualTaskInstance", back_populates="config", uselist=True, viewonly=True
54
+ )
55
+ submissions = relationship(
56
+ "ManualTaskSubmission", back_populates="config", uselist=True, viewonly=True
57
+ )
38
58
  field_definitions = relationship(
39
59
  "ManualTaskConfigField",
40
60
  back_populates="config",
@@ -45,7 +65,7 @@ class ManualTaskConfig(Base):
45
65
  "ManualTaskLog",
46
66
  back_populates="config",
47
67
  primaryjoin="ManualTaskConfig.id == ManualTaskLog.config_id",
48
- viewonly=True,
68
+ cascade="all, delete-orphan",
49
69
  )
50
70
 
51
71
  @classmethod
@@ -95,14 +115,27 @@ class ManualTaskConfigField(Base):
95
115
  field_type = Column(
96
116
  EnumColumn(ManualTaskFieldType), nullable=False
97
117
  ) # Using ManualTaskFieldType
98
- field_metadata = Column(JSONB, nullable=False, default={})
118
+ field_metadata: dict[str, Any] = cast(
119
+ dict[str, Any], Column(JSONB, nullable=False, default={})
120
+ )
99
121
 
100
122
  # Relationships
101
- config = relationship("ManualTaskConfig", back_populates="field_definitions")
123
+ config = relationship(
124
+ "ManualTaskConfig", back_populates="field_definitions", viewonly=True
125
+ )
126
+ submissions = relationship(
127
+ "ManualTaskSubmission",
128
+ back_populates="field",
129
+ uselist=True,
130
+ cascade="all, delete-orphan",
131
+ )
102
132
 
103
133
  @property
104
134
  def field_metadata_model(self) -> ManualTaskFieldMetadata:
105
135
  """Get the field metadata as a Pydantic model."""
136
+ assert isinstance(
137
+ self.field_metadata, dict
138
+ ), "field_metadata must be a dictionary"
106
139
  return ManualTaskFieldMetadata.model_validate(self.field_metadata)
107
140
 
108
141
  @classmethod
@@ -0,0 +1,187 @@
1
+ from datetime import datetime, timezone
2
+ from typing import TYPE_CHECKING, Optional
3
+
4
+ from sqlalchemy import Column, DateTime, ForeignKey, String
5
+ from sqlalchemy.dialects.postgresql import JSONB
6
+ from sqlalchemy.ext.declarative import declared_attr
7
+ from sqlalchemy.orm import relationship
8
+
9
+ from fides.api.db.base_class import Base
10
+ from fides.api.db.util import EnumColumn
11
+ from fides.api.models.manual_tasks.manual_task_config import ManualTaskConfigField
12
+ from fides.api.schemas.manual_tasks.manual_task_schemas import ManualTaskEntityType
13
+ from fides.api.schemas.manual_tasks.manual_task_status import (
14
+ StatusTransitionMixin,
15
+ StatusType,
16
+ )
17
+
18
+ if TYPE_CHECKING: # pragma: no cover
19
+ from fides.api.models.attachment import Attachment # pragma: no cover
20
+ from fides.api.models.fides_user import FidesUser # pragma: no cover
21
+ from fides.api.models.manual_tasks.manual_task import ManualTask # pragma: no cover
22
+ from fides.api.models.manual_tasks.manual_task_config import (
23
+ ManualTaskConfig, # pragma: no cover
24
+ )
25
+ from fides.api.models.manual_tasks.manual_task_log import (
26
+ ManualTaskLog, # pragma: no cover; pragma: no cover
27
+ )
28
+
29
+
30
+ class ManualTaskInstance(Base, StatusTransitionMixin):
31
+ """Model for tracking task status per entity instance.
32
+
33
+ This model implements StatusTransitionProtocol through the StatusTransitionMixin.
34
+ """
35
+
36
+ @declared_attr
37
+ def __tablename__(cls) -> str:
38
+ """Overriding base class method to set the table name."""
39
+ return "manual_task_instance"
40
+
41
+ # Database columns
42
+ task_id: Column[str] = Column(String, ForeignKey("manual_task.id"), nullable=False)
43
+ config_id: Column[str] = Column(
44
+ String, ForeignKey("manual_task_config.id"), nullable=False
45
+ )
46
+ # entity id is the entity that the instance relates to
47
+ # (e.g. a privacy request is an entity that has its own manual task instance)
48
+ entity_id: Column[str] = Column(String, nullable=False)
49
+ entity_type: Column[ManualTaskEntityType] = Column(
50
+ EnumColumn(ManualTaskEntityType), nullable=False
51
+ )
52
+ # ingnore[assignment] because the mypy and sqlalchemy types mismatch
53
+ # upgrading to 2.0 allows mapping which provides better type safety visibility.
54
+ status: Column[StatusType] = Column(EnumColumn(StatusType), nullable=False, default=StatusType.pending) # type: ignore[assignment]
55
+ completed_at: Column[Optional[datetime]] = Column(DateTime, nullable=True) # type: ignore[assignment]
56
+ completed_by_id: Column[Optional[str]] = Column(String, nullable=True) # type: ignore[assignment]
57
+ due_date: Column[Optional[datetime]] = Column(DateTime, nullable=True)
58
+
59
+ # Relationships
60
+ task = relationship("ManualTask", back_populates="instances")
61
+ config = relationship("ManualTaskConfig", back_populates="instances")
62
+ submissions = relationship(
63
+ "ManualTaskSubmission",
64
+ back_populates="instance",
65
+ cascade="all, delete-orphan",
66
+ uselist=True,
67
+ )
68
+ logs = relationship(
69
+ "ManualTaskLog",
70
+ back_populates="instance",
71
+ primaryjoin="ManualTaskInstance.id == ManualTaskLog.instance_id",
72
+ cascade="all, delete-orphan",
73
+ order_by="ManualTaskLog.created_at",
74
+ uselist=True,
75
+ )
76
+ attachments = relationship(
77
+ "Attachment",
78
+ secondary="attachment_reference",
79
+ primaryjoin="and_(ManualTaskInstance.id == ManualTaskSubmission.instance_id, "
80
+ "ManualTaskSubmission.id == AttachmentReference.reference_id, "
81
+ "AttachmentReference.reference_type == 'manual_task_submission')",
82
+ secondaryjoin="Attachment.id == AttachmentReference.attachment_id",
83
+ order_by="Attachment.created_at",
84
+ viewonly=True,
85
+ uselist=True,
86
+ )
87
+
88
+ @property
89
+ def required_fields(self) -> list["ManualTaskConfigField"]:
90
+ """Get all required fields."""
91
+ return [
92
+ field
93
+ for field in self.config.field_definitions
94
+ if field.field_metadata.get("required", False)
95
+ ]
96
+
97
+ @property
98
+ def incomplete_fields(self) -> list["ManualTaskConfigField"]:
99
+ """Get all fields that haven't been completed yet.
100
+ A field is considered incomplete if:
101
+ 1. It's required and has no submission
102
+ Returns:
103
+ list[ManualTaskConfigField]: List of incomplete fields
104
+ """
105
+ return [
106
+ field
107
+ for field in self.required_fields
108
+ if not self.get_submission_for_field(field.id)
109
+ ]
110
+
111
+ @property
112
+ def completed_fields(self) -> list["ManualTaskConfigField"]:
113
+ """Get all fields that have been completed."""
114
+ return [
115
+ field
116
+ for field in self.config.field_definitions
117
+ if field.field_metadata.get("required", False)
118
+ and self.get_submission_for_field(field.id)
119
+ ]
120
+
121
+ def get_submission_for_field(
122
+ self, field_id: str
123
+ ) -> Optional["ManualTaskSubmission"]:
124
+ """Get the submission for a specific field.
125
+
126
+ Args:
127
+ field_id: The ID of the field to get the submission for
128
+
129
+ Returns:
130
+ Optional[ManualTaskSubmission]: The submission for the field, or None if no submission exists
131
+ """
132
+ return next(
133
+ (
134
+ submission
135
+ for submission in self.submissions
136
+ if submission.field_id == field_id
137
+ ),
138
+ None,
139
+ )
140
+
141
+
142
+ class ManualTaskSubmission(Base):
143
+ """Model for storing user submissions.
144
+ Each submission represents data for a single field.
145
+ """
146
+
147
+ @declared_attr
148
+ def __tablename__(cls) -> str:
149
+ """Overriding base class method to set the table name."""
150
+ return "manual_task_submission"
151
+
152
+ # Database columns
153
+ task_id = Column(String, ForeignKey("manual_task.id"))
154
+ config_id = Column(String, ForeignKey("manual_task_config.id"))
155
+ field_id = Column(String, ForeignKey("manual_task_config_field.id"))
156
+ instance_id = Column(String, ForeignKey("manual_task_instance.id"), nullable=False)
157
+ submitted_by = Column(String, ForeignKey("fidesuser.id"), nullable=True)
158
+ submitted_at = Column(DateTime, default=datetime.now(timezone.utc), nullable=False)
159
+ data = Column(JSONB, nullable=False)
160
+
161
+ # Relationships
162
+ task = relationship("ManualTask", back_populates="submissions", viewonly=True)
163
+ config = relationship(
164
+ "ManualTaskConfig", back_populates="submissions", viewonly=True
165
+ )
166
+ field = relationship(
167
+ "ManualTaskConfigField", back_populates="submissions", viewonly=True
168
+ )
169
+ instance = relationship(
170
+ "ManualTaskInstance", back_populates="submissions", viewonly=True
171
+ )
172
+ attachments = relationship(
173
+ "Attachment",
174
+ secondary="attachment_reference",
175
+ primaryjoin="and_(ManualTaskSubmission.id == AttachmentReference.reference_id, "
176
+ "AttachmentReference.reference_type == 'manual_task_submission')",
177
+ secondaryjoin="Attachment.id == AttachmentReference.attachment_id",
178
+ order_by="Attachment.created_at",
179
+ viewonly=True,
180
+ uselist=True,
181
+ )
182
+
183
+ user = relationship(
184
+ "FidesUser",
185
+ primaryjoin="FidesUser.id == ManualTaskSubmission.submitted_by",
186
+ viewonly=True,
187
+ )
@@ -13,6 +13,9 @@ if TYPE_CHECKING: # pragma: no cover
13
13
  from fides.api.models.manual_tasks.manual_task_config import (
14
14
  ManualTaskConfig, # pragma: no cover
15
15
  )
16
+ from fides.api.models.manual_tasks.manual_task_instance import ( # pragma: no cover
17
+ ManualTaskInstance,
18
+ )
16
19
 
17
20
 
18
21
  class ManualTaskLog(Base):
@@ -26,19 +29,29 @@ class ManualTaskLog(Base):
26
29
  task_id = Column(
27
30
  String, ForeignKey("manual_task.id", ondelete="CASCADE"), nullable=False
28
31
  )
29
- config_id = Column(String, ForeignKey("manual_task_config.id"), nullable=True)
30
- instance_id = Column(String, nullable=True)
32
+ config_id = Column(
33
+ String, ForeignKey("manual_task_config.id", ondelete="CASCADE"), nullable=True
34
+ )
35
+ instance_id = Column(
36
+ String,
37
+ ForeignKey("manual_task_instance.id", ondelete="CASCADE"),
38
+ nullable=True,
39
+ )
31
40
  status = Column(String, nullable=False)
32
41
  message = Column(String, nullable=False)
33
42
  details = Column(JSONB, nullable=True)
34
43
 
35
- # Relationships - using string references to avoid circular imports
36
- task = relationship("ManualTask", back_populates="logs", foreign_keys=[task_id])
44
+ # Relationships
45
+ task = relationship(
46
+ "ManualTask", back_populates="logs", foreign_keys=[task_id], viewonly=True
47
+ )
37
48
  config = relationship(
38
- "ManualTaskConfig", back_populates="logs", foreign_keys=[config_id]
49
+ "ManualTaskConfig",
50
+ back_populates="logs",
51
+ foreign_keys=[config_id],
52
+ viewonly=True,
39
53
  )
40
- # TODO: Add instance relationship when it is implemented
41
- # instance = relationship("ManualTaskInstance", back_populates="logs")
54
+ instance = relationship("ManualTaskInstance", back_populates="logs", viewonly=True)
42
55
 
43
56
  @classmethod
44
57
  def create_log(
@@ -5,6 +5,15 @@ from typing import Annotated, Any, Optional
5
5
  from pydantic import ConfigDict, Field
6
6
 
7
7
  from fides.api.schemas.base_class import FidesSchema
8
+ from fides.api.schemas.manual_tasks.manual_task_status import StatusType
9
+
10
+
11
+ class ManualTaskExecutionTiming(str, Enum):
12
+ """Enum for when a manual task should be executed in the privacy request DAG."""
13
+
14
+ pre_execution = "pre_execution" # Execute before the main DAG
15
+ post_execution = "post_execution" # Execute after the main DAG
16
+ parallel = "parallel" # Execute in parallel with the main DAG
8
17
 
9
18
 
10
19
  class ManualTaskType(str, Enum):
@@ -23,6 +32,13 @@ class ManualTaskParentEntityType(str, Enum):
23
32
  # Add more parent entity types as needed
24
33
 
25
34
 
35
+ class ManualTaskEntityType(str, Enum):
36
+ """Enum for manual task entity types."""
37
+
38
+ privacy_request = "privacy_request"
39
+ # Add more entity types as needed
40
+
41
+
26
42
  class ManualTaskReferenceType(str, Enum):
27
43
  """Enum for manual task reference types."""
28
44
 
@@ -46,6 +62,32 @@ class ManualTaskLogStatus(str, Enum):
46
62
  awaiting_input = "awaiting_input"
47
63
 
48
64
 
65
+ class ManualTaskResponse(FidesSchema):
66
+ """Schema for manual task response."""
67
+
68
+ model_config = ConfigDict(extra="forbid")
69
+
70
+ id: Annotated[str, Field(..., description="Task ID")]
71
+ parent_entity_id: Annotated[str, Field(..., description="Parent entity ID")]
72
+ parent_entity_type: Annotated[
73
+ ManualTaskParentEntityType, Field(..., description="Parent entity type")
74
+ ]
75
+ status: Annotated[StatusType, Field(..., description="Task status")]
76
+ created_at: Annotated[datetime, Field(..., description="Creation timestamp")]
77
+ updated_at: Annotated[datetime, Field(..., description="Last update timestamp")]
78
+
79
+
80
+ class ManualTaskCreate(FidesSchema):
81
+ """Schema for creating a manual task."""
82
+
83
+ model_config = ConfigDict(extra="forbid")
84
+
85
+ parent_entity_id: Annotated[str, Field(..., description="Parent entity ID")]
86
+ parent_entity_type: Annotated[
87
+ ManualTaskParentEntityType, Field(..., description="Parent entity type")
88
+ ]
89
+
90
+
49
91
  class ManualTaskLogCreate(FidesSchema):
50
92
  """Schema for creating a manual task log entry."""
51
93
 
@@ -1,6 +1,6 @@
1
1
  from datetime import datetime, timezone
2
2
  from enum import Enum as EnumType
3
- from typing import Optional
3
+ from typing import Optional, Protocol
4
4
 
5
5
  from sqlalchemy.orm import Session
6
6
 
@@ -23,61 +23,133 @@ class StatusType(str, EnumType):
23
23
 
24
24
  @classmethod
25
25
  def get_valid_transitions(cls, current_status: "StatusType") -> list["StatusType"]:
26
- """Get valid transitions from the current status.
26
+ """Get valid transitions from the current status."""
27
+ transitions = {
28
+ cls.pending: [cls.in_progress, cls.failed, cls.completed],
29
+ cls.in_progress: [cls.completed, cls.failed],
30
+ cls.completed: [],
31
+ cls.failed: [cls.pending, cls.in_progress],
32
+ }
33
+ return transitions.get(current_status, [])
34
+
35
+
36
+ class StatusTransitionProtocol(Protocol):
37
+ """Protocol for objects that support status transitions.
38
+
39
+ This protocol defines the interface that any object supporting status transitions
40
+ must implement. It includes both the required attributes and methods.
41
+
42
+ Example:
43
+ ```python
44
+ # Any class that implements this protocol can be used interchangeably
45
+ def process_status_update(obj: StatusTransitionProtocol, db: Session) -> None:
46
+ if obj.is_pending:
47
+ obj.start_progress(db)
48
+ elif obj.is_in_progress:
49
+ obj.mark_completed(db, user_id="user123")
50
+
51
+ # This works with ManualTaskInstance or any other class implementing the protocol
52
+ instance = ManualTaskInstance(...)
53
+ process_status_update(instance, db)
54
+ ```
55
+ """
27
56
 
28
- Args:
29
- current_status: The current status
57
+ # Required attributes - using runtime types that work with SQLAlchemy
58
+ status: StatusType
59
+ completed_at: Optional[datetime] # Can be None when resetting to pending
60
+ completed_by_id: Optional[str] # Can be None when resetting to pending
30
61
 
31
- Returns:
32
- list[StatusType]: List of valid transitions
33
- """
34
- if current_status == cls.pending:
35
- return [cls.in_progress, cls.failed, cls.completed]
36
- if current_status == cls.in_progress:
37
- return [cls.completed, cls.failed]
38
- if current_status == cls.completed:
39
- return []
40
- if current_status == cls.failed:
41
- return [cls.pending, cls.in_progress]
42
- return []
62
+ # Required methods
63
+ # pylint does not understand the Protocol abstract syntax and will complain about the ellipsis
64
+ def update_status(
65
+ self, db: Session, new_status: StatusType, user_id: Optional[str] = None
66
+ ) -> None:
67
+ """Update the status with validation and completion handling."""
68
+ ... # pylint: disable=unnecessary-ellipsis
69
+
70
+ def mark_completed(self, db: Session, user_id: str) -> None:
71
+ """Mark as completed."""
72
+ ... # pylint: disable=unnecessary-ellipsis
73
+
74
+ def mark_failed(self, db: Session) -> None:
75
+ """Mark as failed."""
76
+ ... # pylint: disable=unnecessary-ellipsis
77
+
78
+ def start_progress(self, db: Session) -> None:
79
+ """Mark as in progress."""
80
+ ... # pylint: disable=unnecessary-ellipsis
81
+
82
+ def reset_to_pending(self, db: Session) -> None:
83
+ """Reset to pending status."""
84
+ ... # pylint: disable=unnecessary-ellipsis
85
+
86
+ @property
87
+ def is_completed(self) -> bool:
88
+ """Check if completed."""
89
+ ... # pylint: disable=unnecessary-ellipsis
90
+
91
+ @property
92
+ def is_failed(self) -> bool:
93
+ """Check if failed."""
94
+ ... # pylint: disable=unnecessary-ellipsis
95
+
96
+ @property
97
+ def is_in_progress(self) -> bool:
98
+ """Check if in progress."""
99
+ ... # pylint: disable=unnecessary-ellipsis
100
+
101
+ @property
102
+ def is_pending(self) -> bool:
103
+ """Check if pending."""
104
+ ... # pylint: disable=unnecessary-ellipsis
105
+
106
+
107
+ def validate_status_transition_object(obj: StatusTransitionProtocol) -> bool:
108
+ """Validate that an object properly implements the StatusTransitionProtocol.
109
+
110
+ This function demonstrates how the Protocol can be used for runtime validation
111
+ and type checking.
112
+ """
113
+ required_attrs = ["status", "completed_at", "completed_by_id"]
114
+ required_methods = [
115
+ "update_status",
116
+ "mark_completed",
117
+ "mark_failed",
118
+ "start_progress",
119
+ "reset_to_pending",
120
+ ]
121
+ required_properties = ["is_completed", "is_failed", "is_in_progress", "is_pending"]
122
+
123
+ # Check all required elements
124
+ all_required = required_attrs + required_methods + required_properties
125
+ return all(hasattr(obj, attr) for attr in all_required) and all(
126
+ callable(getattr(obj, method)) for method in required_methods
127
+ )
43
128
 
44
129
 
45
130
  class StatusTransitionMixin:
46
131
  """Mixin for handling status transitions.
47
132
 
48
133
  This mixin provides methods for managing status transitions and completion tracking.
49
- It can be used by any model that needs status management.
134
+ It implements the StatusTransitionProtocol and can be used by any model that needs status management.
50
135
  """
51
136
 
52
- # These should be overridden by the implementing class
137
+ # Type annotations to match the Protocol
53
138
  status: StatusType
54
139
  completed_at: Optional[datetime]
55
140
  completed_by_id: Optional[str]
56
141
 
57
142
  def _get_valid_transitions(self) -> list[StatusType]:
58
- """Get valid transitions from the current status.
59
-
60
- Returns:
61
- list[StatusType]: List of valid transitions
62
- """
143
+ """Get valid transitions from the current status."""
63
144
  return StatusType.get_valid_transitions(self.status)
64
145
 
65
146
  def _validate_status_transition(self, new_status: StatusType) -> None:
66
- """Validate that a status transition is allowed.
67
-
68
- Args:
69
- new_status: The new status to transition to
70
-
71
- Raises:
72
- StatusTransitionNotAllowed: If the transition is not allowed
73
- """
74
- # Don't allow transitions to the same status
147
+ """Validate that a status transition is allowed."""
75
148
  if new_status == self.status:
76
149
  raise StatusTransitionNotAllowed(
77
150
  f"Invalid status transition: already in status {new_status}"
78
151
  )
79
152
 
80
- # Get valid transitions for current status
81
153
  valid_transitions = self._get_valid_transitions()
82
154
  if new_status not in valid_transitions:
83
155
  raise StatusTransitionNotAllowed(
@@ -88,13 +160,7 @@ class StatusTransitionMixin:
88
160
  def update_status(
89
161
  self, db: Session, new_status: StatusType, user_id: Optional[str] = None
90
162
  ) -> None:
91
- """Update the status with validation and completion handling.
92
-
93
- Args:
94
- db: Database session
95
- new_status: New status to set
96
- user_id: Optional user ID who is making the change
97
- """
163
+ """Update the status with validation and completion handling."""
98
164
  self._validate_status_transition(new_status)
99
165
 
100
166
  if new_status == StatusType.completed:
@@ -110,12 +176,7 @@ class StatusTransitionMixin:
110
176
  db.commit()
111
177
 
112
178
  def mark_completed(self, db: Session, user_id: str) -> None:
113
- """Mark as completed.
114
-
115
- Args:
116
- db: Database session
117
- user_id: user ID who completed the task
118
- """
179
+ """Mark as completed."""
119
180
  self.update_status(db, StatusType.completed, user_id)
120
181
 
121
182
  def mark_failed(self, db: Session) -> None:
@@ -37,7 +37,7 @@ class PostgreSQLConnector(SQLConnector):
37
37
  netloc = config.host
38
38
  port = f":{config.port}" if config.port else ""
39
39
  dbname = f"/{config.dbname}" if config.dbname else ""
40
- query = f"?sslmode=${config.ssl_mode}" if config.ssl_mode else ""
40
+ query = f"?sslmode={config.ssl_mode}" if config.ssl_mode else ""
41
41
  return f"postgresql://{user_password}{netloc}{port}{dbname}{query}"
42
42
 
43
43
  def build_ssh_uri(self, local_address: tuple) -> str:
@@ -54,7 +54,7 @@ class PostgreSQLConnector(SQLConnector):
54
54
  netloc = local_host
55
55
  port = f":{local_port}" if local_port else ""
56
56
  dbname = f"/{config.dbname}" if config.dbname else ""
57
- query = f"?sslmode=${config.ssl_mode}" if config.ssl_mode else ""
57
+ query = f"?sslmode={config.ssl_mode}" if config.ssl_mode else ""
58
58
  return f"postgresql://{user_password}{netloc}{port}{dbname}{query}"
59
59
 
60
60
  # Overrides SQLConnector.create_client