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.
- {ethyca_fides-2.64.0rc0.dist-info → ethyca_fides-2.64.1b1.dist-info}/METADATA +2 -2
- {ethyca_fides-2.64.0rc0.dist-info → ethyca_fides-2.64.1b1.dist-info}/RECORD +107 -104
- fides/_version.py +3 -3
- fides/api/alembic/migrations/versions/6a76a1fa4f3f_add_manual_task_instance_table.py +256 -0
- fides/api/db/base.py +4 -0
- fides/api/models/manual_tasks/__init__.py +7 -1
- fides/api/models/manual_tasks/manual_task.py +19 -3
- fides/api/models/manual_tasks/manual_task_config.py +39 -6
- fides/api/models/manual_tasks/manual_task_instance.py +187 -0
- fides/api/models/manual_tasks/manual_task_log.py +20 -7
- fides/api/schemas/manual_tasks/manual_task_schemas.py +42 -0
- fides/api/schemas/manual_tasks/manual_task_status.py +107 -46
- fides/api/service/connectors/postgres_connector.py +2 -2
- fides/service/manual_tasks/manual_task_config_service.py +17 -5
- fides/service/manual_tasks/manual_task_instance_service.py +285 -0
- fides/service/manual_tasks/manual_task_service.py +66 -10
- fides/ui-build/static/admin/404.html +1 -1
- fides/ui-build/static/admin/add-systems/manual.html +1 -1
- fides/ui-build/static/admin/add-systems/multiple.html +1 -1
- fides/ui-build/static/admin/add-systems.html +1 -1
- fides/ui-build/static/admin/consent/configure/add-vendors.html +1 -1
- fides/ui-build/static/admin/consent/configure.html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience/[id].html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience/new.html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience.html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices/[id].html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices/new.html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices.html +1 -1
- fides/ui-build/static/admin/consent/properties.html +1 -1
- fides/ui-build/static/admin/consent/reporting.html +1 -1
- fides/ui-build/static/admin/consent.html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn]/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects.html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/resources/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/resources.html +1 -1
- fides/ui-build/static/admin/data-catalog.html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center/[monitorId]/[systemId].html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center/[monitorId].html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center.html +1 -1
- fides/ui-build/static/admin/data-discovery/activity.html +1 -1
- fides/ui-build/static/admin/data-discovery/detection/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-discovery/detection.html +1 -1
- fides/ui-build/static/admin/data-discovery/discovery/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-discovery/discovery.html +1 -1
- fides/ui-build/static/admin/datamap.html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId]/[collectionName]/[...subfieldNames].html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId]/[collectionName].html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId].html +1 -1
- fides/ui-build/static/admin/dataset/new.html +1 -1
- fides/ui-build/static/admin/dataset.html +1 -1
- fides/ui-build/static/admin/datastore-connection/[id].html +1 -1
- fides/ui-build/static/admin/datastore-connection/new.html +1 -1
- fides/ui-build/static/admin/datastore-connection.html +1 -1
- fides/ui-build/static/admin/index.html +1 -1
- fides/ui-build/static/admin/integrations/[id].html +1 -1
- fides/ui-build/static/admin/integrations.html +1 -1
- fides/ui-build/static/admin/lib/fides-headless.js +1 -1
- fides/ui-build/static/admin/lib/fides-preview.js +1 -1
- fides/ui-build/static/admin/lib/fides-tcf.js +2 -2
- fides/ui-build/static/admin/lib/fides.js +2 -2
- fides/ui-build/static/admin/login/[provider].html +1 -1
- fides/ui-build/static/admin/login.html +1 -1
- fides/ui-build/static/admin/messaging/[id].html +1 -1
- fides/ui-build/static/admin/messaging/add-template.html +1 -1
- fides/ui-build/static/admin/messaging.html +1 -1
- fides/ui-build/static/admin/poc/ant-components.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/AntForm.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikAntFormItem.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikControlled.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikField.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikSpreadField.html +1 -1
- fides/ui-build/static/admin/poc/forms.html +1 -1
- fides/ui-build/static/admin/poc/table-migration.html +1 -1
- fides/ui-build/static/admin/privacy-requests/[id].html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure/messaging.html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure/storage.html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure.html +1 -1
- fides/ui-build/static/admin/privacy-requests.html +1 -1
- fides/ui-build/static/admin/properties/[id].html +1 -1
- fides/ui-build/static/admin/properties/add-property.html +1 -1
- fides/ui-build/static/admin/properties.html +1 -1
- fides/ui-build/static/admin/reporting/datamap.html +1 -1
- fides/ui-build/static/admin/settings/about/alpha.html +1 -1
- fides/ui-build/static/admin/settings/about.html +1 -1
- fides/ui-build/static/admin/settings/consent/[configuration_id]/[purpose_id].html +1 -1
- fides/ui-build/static/admin/settings/consent.html +1 -1
- fides/ui-build/static/admin/settings/custom-fields.html +1 -1
- fides/ui-build/static/admin/settings/domain-records.html +1 -1
- fides/ui-build/static/admin/settings/domains.html +1 -1
- fides/ui-build/static/admin/settings/email-templates.html +1 -1
- fides/ui-build/static/admin/settings/locations.html +1 -1
- fides/ui-build/static/admin/settings/organization.html +1 -1
- fides/ui-build/static/admin/settings/regulations.html +1 -1
- fides/ui-build/static/admin/systems/configure/[id]/test-datasets.html +1 -1
- fides/ui-build/static/admin/systems/configure/[id].html +1 -1
- fides/ui-build/static/admin/systems.html +1 -1
- fides/ui-build/static/admin/taxonomy.html +1 -1
- fides/ui-build/static/admin/user-management/new.html +1 -1
- fides/ui-build/static/admin/user-management/profile/[id].html +1 -1
- fides/ui-build/static/admin/user-management.html +1 -1
- {ethyca_fides-2.64.0rc0.dist-info → ethyca_fides-2.64.1b1.dist-info}/WHEEL +0 -0
- {ethyca_fides-2.64.0rc0.dist-info → ethyca_fides-2.64.1b1.dist-info}/entry_points.txt +0 -0
- {ethyca_fides-2.64.0rc0.dist-info → ethyca_fides-2.64.1b1.dist-info}/licenses/LICENSE +0 -0
- {ethyca_fides-2.64.0rc0.dist-info → ethyca_fides-2.64.1b1.dist-info}/top_level.txt +0 -0
- /fides/ui-build/static/admin/_next/static/{lDN8BtGGiw3b60__SDnAa → zBkPKRGECPjwEx0G7BvHe}/_buildManifest.js +0 -0
- /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
|
-
"
|
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,
|
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
|
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
|
-
|
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
|
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(
|
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(
|
30
|
-
|
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
|
36
|
-
task = relationship(
|
44
|
+
# Relationships
|
45
|
+
task = relationship(
|
46
|
+
"ManualTask", back_populates="logs", foreign_keys=[task_id], viewonly=True
|
47
|
+
)
|
37
48
|
config = relationship(
|
38
|
-
"ManualTaskConfig",
|
49
|
+
"ManualTaskConfig",
|
50
|
+
back_populates="logs",
|
51
|
+
foreign_keys=[config_id],
|
52
|
+
viewonly=True,
|
39
53
|
)
|
40
|
-
|
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
|
-
|
29
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
#
|
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
|
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
|
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
|