ethyca-fides 2.71.1b1__py2.py3-none-any.whl → 2.71.1rc1__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ethyca-fides might be problematic. Click here for more details.
- {ethyca_fides-2.71.1b1.dist-info → ethyca_fides-2.71.1rc1.dist-info}/METADATA +2 -2
- {ethyca_fides-2.71.1b1.dist-info → ethyca_fides-2.71.1rc1.dist-info}/RECORD +170 -159
- fides/_version.py +3 -3
- fides/api/alembic/migrations/versions/4bfbeff34611_add_polling_status.py +68 -0
- fides/api/alembic/migrations/versions/7db29f9cd77b_create_new_sub_request_table.py +95 -0
- fides/api/alembic/migrations/versions/b97e92b038d2_add_digest_execution_model.py +117 -0
- fides/api/api/v1/endpoints/generic_overrides.py +3 -9
- fides/api/common_exceptions.py +4 -0
- fides/api/main.py +2 -2
- fides/api/models/attachment.py +1 -0
- fides/api/models/digest/__init__.py +2 -0
- fides/api/models/digest/digest_config.py +10 -1
- fides/api/models/digest/digest_execution.py +132 -0
- fides/api/models/event_audit.py +8 -0
- fides/api/models/privacy_notice.py +0 -8
- fides/api/models/privacy_request/request_task.py +98 -1
- fides/api/models/worker_task.py +8 -0
- fides/api/schemas/saas/async_polling_configuration.py +81 -0
- fides/api/schemas/saas/saas_config.py +10 -3
- fides/api/schemas/saas/strategy_configuration.py +0 -12
- fides/api/service/async_dsr/handlers/__init__.py +0 -0
- fides/api/service/async_dsr/handlers/polling_attachment_handler.py +155 -0
- fides/api/service/async_dsr/handlers/polling_request_handler.py +88 -0
- fides/api/service/async_dsr/handlers/polling_response_handler.py +261 -0
- fides/api/service/async_dsr/handlers/polling_sub_request_handler.py +123 -0
- fides/api/service/async_dsr/strategies/__init__.py +0 -0
- fides/api/service/async_dsr/strategies/async_dsr_strategy.py +52 -0
- fides/api/service/async_dsr/strategies/async_dsr_strategy_callback.py +199 -0
- fides/api/service/async_dsr/strategies/async_dsr_strategy_factory.py +72 -0
- fides/api/service/async_dsr/strategies/async_dsr_strategy_polling.py +678 -0
- fides/api/service/async_dsr/utils.py +130 -0
- fides/api/service/connectors/fides/fides_client.py +63 -1
- fides/api/service/connectors/query_configs/saas_query_config.py +4 -5
- fides/api/service/connectors/saas_connector.py +77 -69
- fides/api/service/privacy_request/attachment_handling.py +9 -2
- fides/api/service/privacy_request/request_runner_service.py +9 -83
- fides/api/service/privacy_request/request_service.py +47 -74
- fides/api/service/saas_request/saas_request_override_factory.py +66 -1
- fides/api/task/execute_request_tasks.py +5 -2
- fides/api/task/filter_results.py +35 -2
- fides/api/task/graph_task.py +34 -2
- fides/config/execution_settings.py +7 -3
- fides/service/dataset/dataset_service.py +0 -39
- fides/service/privacy_request/privacy_request_service.py +48 -103
- fides/ui-build/static/admin/404.html +1 -1
- fides/ui-build/static/admin/_next/static/{_IxwgneyQjdSaZFEF3Tqu → AfNel282iPq07N-lE1Vzx}/_buildManifest.js +1 -1
- fides/ui-build/static/admin/_next/static/chunks/155-c1ae010c664e2245.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/{3585-f728d32fda6f1ac1.js → 3585-efd5d41f08e180c4.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/3855-12ee1dfbbe47fd28.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/409-c1256ecda1b15db6.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/4558-de5ced790b3380dc.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/4608-a9941d0c236ebca1.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/{4718-3a412bdb90add82f.js → 4718-6585c97c26647e65.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/502-d3ecae97b67befbd.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/{7045-14e955890f1147e4.js → 7045-f15044a4d4525946.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/{_app-c1c2f757b1f3da12.js → _app-a7c02dd2ff07f9e1.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/add-systems-58920afe2b67f952.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/consent/reporting-ae4909cad9b67822.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center/[monitorId]/[systemId]-29c1fb777bd464e0.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/activity-2635ef588bf06145.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/datamap-15616bea02397ef4.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/dataset/new-ea198c4a7869f402.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/integrations-f682b1def859931e.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/[id]-febf156d2977f3ac.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/configure/storage-648d775d0fce49dc.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/settings/custom-fields-5edfec10a945ca43.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/settings/{privacy-requests-97221067330c0c27.js → privacy-requests-8cbdfd08e0fa88fb.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/systems-0f1d833282f09684.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/taxonomy-a8cfa7de4948b374.js +1 -0
- fides/ui-build/static/admin/_next/static/css/{295d729ea1b11885.css → 64fac6fb884435c2.css} +1 -1
- fides/ui-build/static/admin/_next/static/css/f38242c11f7fea64.css +1 -0
- 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-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/storage.html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure.html +1 -1
- fides/ui-build/static/admin/privacy-requests.html +1 -1
- fides/ui-build/static/admin/properties/[id].html +1 -1
- fides/ui-build/static/admin/properties/add-property.html +1 -1
- fides/ui-build/static/admin/properties.html +1 -1
- fides/ui-build/static/admin/reporting/datamap.html +1 -1
- fides/ui-build/static/admin/settings/about/alpha.html +1 -1
- fides/ui-build/static/admin/settings/about.html +1 -1
- fides/ui-build/static/admin/settings/consent/[configuration_id]/[purpose_id].html +1 -1
- fides/ui-build/static/admin/settings/consent.html +1 -1
- fides/ui-build/static/admin/settings/custom-fields/[id].html +1 -1
- fides/ui-build/static/admin/settings/custom-fields/new.html +1 -1
- fides/ui-build/static/admin/settings/custom-fields.html +1 -1
- fides/ui-build/static/admin/settings/domain-records.html +1 -1
- fides/ui-build/static/admin/settings/domains.html +1 -1
- fides/ui-build/static/admin/settings/email-templates.html +1 -1
- fides/ui-build/static/admin/settings/locations.html +1 -1
- fides/ui-build/static/admin/settings/messaging-providers/[key].html +1 -1
- fides/ui-build/static/admin/settings/messaging-providers/new.html +1 -1
- fides/ui-build/static/admin/settings/messaging-providers.html +1 -1
- fides/ui-build/static/admin/settings/organization.html +1 -1
- fides/ui-build/static/admin/settings/privacy-requests.html +1 -1
- fides/ui-build/static/admin/settings/regulations.html +1 -1
- fides/ui-build/static/admin/systems/configure/[id]/test-datasets.html +1 -1
- fides/ui-build/static/admin/systems/configure/[id].html +1 -1
- fides/ui-build/static/admin/systems.html +1 -1
- fides/ui-build/static/admin/taxonomy.html +1 -1
- fides/ui-build/static/admin/user-management/new.html +1 -1
- fides/ui-build/static/admin/user-management/profile/[id].html +1 -1
- fides/ui-build/static/admin/user-management.html +1 -1
- fides/api/service/async_dsr/async_dsr_service.py +0 -195
- fides/api/service/async_dsr/async_dsr_strategy.py +0 -5
- fides/api/service/async_dsr/async_dsr_strategy_callback.py +0 -16
- fides/api/service/async_dsr/async_dsr_strategy_factory.py +0 -63
- fides/api/service/async_dsr/async_dsr_strategy_polling.py +0 -94
- fides/ui-build/static/admin/_next/static/chunks/155-b4337d0826d5addc.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/3855-ed226b8a8050bd40.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/409-5c3d31163028339f.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/4558-8305aee48def1dcd.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/4608-0c6ef78e30a51f84.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/502-0d9f4ac29ef34a1c.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/add-systems-19214babd1f219e3.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/consent/reporting-c1a3caf3c286bf5d.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center/[monitorId]/[systemId]-5b57f9132426fe52.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/activity-a28cc0e23bbe4fc8.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/datamap-7d22222608ec3aac.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/dataset/new-d514cd4ec62e3b03.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/integrations-331544e9b85c4ac2.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/[id]-7dac2302f573f5ee.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/configure/storage-479890582973deaf.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/settings/custom-fields-2fcd95c41e578d57.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/systems-6c91bdea40875227.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/taxonomy-3059aba38adefa56.js +0 -1
- fides/ui-build/static/admin/_next/static/css/073713cd1eddda79.css +0 -1
- {ethyca_fides-2.71.1b1.dist-info → ethyca_fides-2.71.1rc1.dist-info}/WHEEL +0 -0
- {ethyca_fides-2.71.1b1.dist-info → ethyca_fides-2.71.1rc1.dist-info}/entry_points.txt +0 -0
- {ethyca_fides-2.71.1b1.dist-info → ethyca_fides-2.71.1rc1.dist-info}/licenses/LICENSE +0 -0
- {ethyca_fides-2.71.1b1.dist-info → ethyca_fides-2.71.1rc1.dist-info}/top_level.txt +0 -0
- /fides/ui-build/static/admin/_next/static/{_IxwgneyQjdSaZFEF3Tqu → AfNel282iPq07N-lE1Vzx}/_ssgManifest.js +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{6882-dbe0a25dcf1a8ee0.js → 6882-10296485ec326e6b.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{7079-50571e9f3269d74d.js → 7079-bbc7b856802a4834.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{9046-b6616ba7b59d947e.js → 9046-2a332fe338535c84.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{data-catalog-b7326c51d88cc2cc.js → data-catalog-56fd0f3e465e52b6.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/dataset/[datasetId]/[collectionName]/{[...subfieldNames]-0abd30eada811b5b.js → [...subfieldNames]-d4031e438c363fff.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/dataset/[datasetId]/{[collectionName]-007965429368d9a3.js → [collectionName]-9463af37079762d0.js} +0 -0
fides/_version.py
CHANGED
|
@@ -8,11 +8,11 @@ import json
|
|
|
8
8
|
|
|
9
9
|
version_json = '''
|
|
10
10
|
{
|
|
11
|
-
"date": "2025-10-
|
|
11
|
+
"date": "2025-10-03T13:56:10-0700",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "2.71.
|
|
14
|
+
"full-revisionid": "af803c59f2d40a0f5be64f230bdee02b61acc1f5",
|
|
15
|
+
"version": "2.71.1rc1"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""add polling status
|
|
2
|
+
|
|
3
|
+
Revision ID: 4bfbeff34611
|
|
4
|
+
Revises: 7db29f9cd77b
|
|
5
|
+
Create Date: 2025-09-20 23:02:45.550170
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
from alembic import op
|
|
11
|
+
|
|
12
|
+
# revision identifiers, used by Alembic.
|
|
13
|
+
revision = "4bfbeff34611"
|
|
14
|
+
down_revision = "7db29f9cd77b"
|
|
15
|
+
branch_labels = None
|
|
16
|
+
depends_on = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def upgrade():
|
|
20
|
+
op.execute("ALTER TYPE executionlogstatus ADD VALUE 'polling'")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def downgrade():
|
|
24
|
+
# Remove any records that have the 'polling' enum value before dropping it
|
|
25
|
+
# Fallback to 'paused' (awaiting_processing) for backward compatibility
|
|
26
|
+
|
|
27
|
+
op.execute("UPDATE requesttask SET status = 'paused' WHERE status = 'polling'")
|
|
28
|
+
op.execute("UPDATE executionlog SET status = 'paused' WHERE status = 'polling'")
|
|
29
|
+
op.execute(
|
|
30
|
+
"UPDATE digest_task_execution SET status = 'paused' WHERE status = 'polling'"
|
|
31
|
+
)
|
|
32
|
+
op.execute("UPDATE monitortask SET status = 'paused' WHERE status = 'polling'")
|
|
33
|
+
op.execute(
|
|
34
|
+
"UPDATE monitortaskexecutionlog SET status = 'paused' WHERE status = 'polling'"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Recreate the enum without the 'polling' value
|
|
38
|
+
op.execute("ALTER TYPE executionlogstatus RENAME TO executionlogstatus_old")
|
|
39
|
+
op.execute(
|
|
40
|
+
"CREATE TYPE executionlogstatus AS ENUM ("
|
|
41
|
+
"'in_processing', 'pending', 'complete', 'error', 'paused', 'retrying', 'skipped'"
|
|
42
|
+
")"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Update all tables that use the enum type
|
|
46
|
+
op.execute(
|
|
47
|
+
"ALTER TABLE requesttask ALTER COLUMN status TYPE executionlogstatus USING "
|
|
48
|
+
"status::text::executionlogstatus"
|
|
49
|
+
)
|
|
50
|
+
op.execute(
|
|
51
|
+
"ALTER TABLE executionlog ALTER COLUMN status TYPE executionlogstatus USING "
|
|
52
|
+
"status::text::executionlogstatus"
|
|
53
|
+
)
|
|
54
|
+
op.execute(
|
|
55
|
+
"ALTER TABLE digest_task_execution ALTER COLUMN status TYPE executionlogstatus USING "
|
|
56
|
+
"status::text::executionlogstatus"
|
|
57
|
+
)
|
|
58
|
+
op.execute(
|
|
59
|
+
"ALTER TABLE monitortask ALTER COLUMN status TYPE executionlogstatus USING "
|
|
60
|
+
"status::text::executionlogstatus"
|
|
61
|
+
)
|
|
62
|
+
op.execute(
|
|
63
|
+
"ALTER TABLE monitortaskexecutionlog ALTER COLUMN status TYPE executionlogstatus USING "
|
|
64
|
+
"status::text::executionlogstatus"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Drop the old enum type
|
|
68
|
+
op.execute("DROP TYPE executionlogstatus_old")
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Create new Sub Request Table
|
|
2
|
+
|
|
3
|
+
Revision ID: 7db29f9cd77b
|
|
4
|
+
Revises: b97e92b038d2
|
|
5
|
+
Create Date: 2025-09-16 14:00:16.282996
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
from alembic import op
|
|
11
|
+
from sqlalchemy_utils.types.encrypted.encrypted_type import (
|
|
12
|
+
AesGcmEngine,
|
|
13
|
+
StringEncryptedType,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from fides.api.db.base_class import JSONTypeOverride
|
|
17
|
+
from fides.config import CONFIG
|
|
18
|
+
|
|
19
|
+
# revision identifiers, used by Alembic.
|
|
20
|
+
revision = "7db29f9cd77b"
|
|
21
|
+
down_revision = "b97e92b038d2"
|
|
22
|
+
branch_labels = None
|
|
23
|
+
depends_on = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def upgrade():
|
|
27
|
+
op.create_table(
|
|
28
|
+
"request_task_sub_request",
|
|
29
|
+
sa.Column("id", sa.String(length=255), nullable=False),
|
|
30
|
+
sa.Column(
|
|
31
|
+
"created_at",
|
|
32
|
+
sa.DateTime(timezone=True),
|
|
33
|
+
server_default=sa.text("now()"),
|
|
34
|
+
nullable=True,
|
|
35
|
+
),
|
|
36
|
+
sa.Column(
|
|
37
|
+
"updated_at",
|
|
38
|
+
sa.DateTime(timezone=True),
|
|
39
|
+
server_default=sa.text("now()"),
|
|
40
|
+
nullable=True,
|
|
41
|
+
),
|
|
42
|
+
sa.Column("request_task_id", sa.String(length=255), nullable=False),
|
|
43
|
+
sa.Column(
|
|
44
|
+
"param_values",
|
|
45
|
+
StringEncryptedType(
|
|
46
|
+
type_in=JSONTypeOverride,
|
|
47
|
+
key=CONFIG.security.app_encryption_key,
|
|
48
|
+
engine=AesGcmEngine,
|
|
49
|
+
padding="pkcs5",
|
|
50
|
+
),
|
|
51
|
+
nullable=False,
|
|
52
|
+
),
|
|
53
|
+
sa.Column("status", sa.String(), nullable=False),
|
|
54
|
+
sa.Column(
|
|
55
|
+
"access_data",
|
|
56
|
+
StringEncryptedType(
|
|
57
|
+
type_in=JSONTypeOverride,
|
|
58
|
+
key=CONFIG.security.app_encryption_key,
|
|
59
|
+
engine=AesGcmEngine,
|
|
60
|
+
padding="pkcs5",
|
|
61
|
+
),
|
|
62
|
+
nullable=True,
|
|
63
|
+
),
|
|
64
|
+
sa.Column("rows_masked", sa.Integer(), nullable=True),
|
|
65
|
+
sa.ForeignKeyConstraint(
|
|
66
|
+
["request_task_id"],
|
|
67
|
+
["requesttask.id"],
|
|
68
|
+
name="request_task_sub_request_request_task_id_fkey",
|
|
69
|
+
ondelete="CASCADE",
|
|
70
|
+
),
|
|
71
|
+
sa.PrimaryKeyConstraint("id"),
|
|
72
|
+
)
|
|
73
|
+
op.create_index(
|
|
74
|
+
op.f("ix_request_task_sub_request_id"),
|
|
75
|
+
"request_task_sub_request",
|
|
76
|
+
["id"],
|
|
77
|
+
unique=False,
|
|
78
|
+
)
|
|
79
|
+
op.create_index(
|
|
80
|
+
op.f("ix_request_task_sub_request_request_task_id"),
|
|
81
|
+
"request_task_sub_request",
|
|
82
|
+
["request_task_id"],
|
|
83
|
+
unique=False,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def downgrade():
|
|
88
|
+
op.drop_index(
|
|
89
|
+
op.f("ix_request_task_sub_request_request_task_id"),
|
|
90
|
+
table_name="request_task_sub_request",
|
|
91
|
+
)
|
|
92
|
+
op.drop_index(
|
|
93
|
+
op.f("ix_request_task_sub_request_id"), table_name="request_task_sub_request"
|
|
94
|
+
)
|
|
95
|
+
op.drop_table("request_task_sub_request")
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""add digest execution model
|
|
2
|
+
|
|
3
|
+
Revision ID: b97e92b038d2
|
|
4
|
+
Revises: 3efe14d4469a
|
|
5
|
+
Create Date: 2025-10-01 16:42:41.900651
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
from alembic import op
|
|
11
|
+
from sqlalchemy.dialects import postgresql
|
|
12
|
+
|
|
13
|
+
# revision identifiers, used by Alembic.
|
|
14
|
+
revision = "b97e92b038d2"
|
|
15
|
+
down_revision = "3efe14d4469a"
|
|
16
|
+
branch_labels = None
|
|
17
|
+
depends_on = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def upgrade():
|
|
21
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
22
|
+
op.create_table(
|
|
23
|
+
"digest_task_execution",
|
|
24
|
+
sa.Column("id", sa.String(length=255), nullable=False),
|
|
25
|
+
sa.Column(
|
|
26
|
+
"created_at",
|
|
27
|
+
sa.DateTime(timezone=True),
|
|
28
|
+
server_default=sa.text("now()"),
|
|
29
|
+
nullable=True,
|
|
30
|
+
),
|
|
31
|
+
sa.Column(
|
|
32
|
+
"updated_at",
|
|
33
|
+
sa.DateTime(timezone=True),
|
|
34
|
+
server_default=sa.text("now()"),
|
|
35
|
+
nullable=True,
|
|
36
|
+
),
|
|
37
|
+
sa.Column("action_type", sa.String(), nullable=False),
|
|
38
|
+
sa.Column("digest_config_id", sa.String(), nullable=False),
|
|
39
|
+
sa.Column("celery_task_id", sa.String(), nullable=True),
|
|
40
|
+
sa.Column(
|
|
41
|
+
"status",
|
|
42
|
+
postgresql.ENUM(name="executionlogstatus", create_type=False),
|
|
43
|
+
nullable=False,
|
|
44
|
+
),
|
|
45
|
+
sa.Column("total_recipients", sa.Integer(), nullable=True),
|
|
46
|
+
sa.Column("processed_recipients", sa.Integer(), nullable=False),
|
|
47
|
+
sa.Column("successful_communications", sa.Integer(), nullable=False),
|
|
48
|
+
sa.Column("failed_communications", sa.Integer(), nullable=False),
|
|
49
|
+
sa.Column(
|
|
50
|
+
"execution_state", postgresql.JSONB(astext_type=sa.Text()), nullable=True
|
|
51
|
+
),
|
|
52
|
+
sa.Column(
|
|
53
|
+
"processed_user_ids", postgresql.JSONB(astext_type=sa.Text()), nullable=True
|
|
54
|
+
),
|
|
55
|
+
sa.Column("started_at", sa.DateTime(timezone=True), nullable=True),
|
|
56
|
+
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
|
|
57
|
+
sa.Column("last_checkpoint_at", sa.DateTime(timezone=True), nullable=True),
|
|
58
|
+
sa.Column("error_message", sa.Text(), nullable=True),
|
|
59
|
+
sa.ForeignKeyConstraint(
|
|
60
|
+
["digest_config_id"], ["digest_config.id"], ondelete="CASCADE"
|
|
61
|
+
),
|
|
62
|
+
sa.PrimaryKeyConstraint("id"),
|
|
63
|
+
)
|
|
64
|
+
op.create_index(
|
|
65
|
+
op.f("ix_digest_task_execution_action_type"),
|
|
66
|
+
"digest_task_execution",
|
|
67
|
+
["action_type"],
|
|
68
|
+
unique=False,
|
|
69
|
+
)
|
|
70
|
+
op.create_index(
|
|
71
|
+
op.f("ix_digest_task_execution_celery_task_id"),
|
|
72
|
+
"digest_task_execution",
|
|
73
|
+
["celery_task_id"],
|
|
74
|
+
unique=False,
|
|
75
|
+
)
|
|
76
|
+
op.create_index(
|
|
77
|
+
op.f("ix_digest_task_execution_digest_config_id"),
|
|
78
|
+
"digest_task_execution",
|
|
79
|
+
["digest_config_id"],
|
|
80
|
+
unique=False,
|
|
81
|
+
)
|
|
82
|
+
op.create_index(
|
|
83
|
+
op.f("ix_digest_task_execution_id"),
|
|
84
|
+
"digest_task_execution",
|
|
85
|
+
["id"],
|
|
86
|
+
unique=False,
|
|
87
|
+
)
|
|
88
|
+
op.create_index(
|
|
89
|
+
op.f("ix_digest_task_execution_status"),
|
|
90
|
+
"digest_task_execution",
|
|
91
|
+
["status"],
|
|
92
|
+
unique=False,
|
|
93
|
+
)
|
|
94
|
+
# ### end Alembic commands ###
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def downgrade():
|
|
98
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
99
|
+
op.drop_index(
|
|
100
|
+
op.f("ix_digest_task_execution_status"), table_name="digest_task_execution"
|
|
101
|
+
)
|
|
102
|
+
op.drop_index(
|
|
103
|
+
op.f("ix_digest_task_execution_id"), table_name="digest_task_execution"
|
|
104
|
+
)
|
|
105
|
+
op.drop_index(
|
|
106
|
+
op.f("ix_digest_task_execution_digest_config_id"),
|
|
107
|
+
table_name="digest_task_execution",
|
|
108
|
+
)
|
|
109
|
+
op.drop_index(
|
|
110
|
+
op.f("ix_digest_task_execution_celery_task_id"),
|
|
111
|
+
table_name="digest_task_execution",
|
|
112
|
+
)
|
|
113
|
+
op.drop_index(
|
|
114
|
+
op.f("ix_digest_task_execution_action_type"), table_name="digest_task_execution"
|
|
115
|
+
)
|
|
116
|
+
op.drop_table("digest_task_execution")
|
|
117
|
+
# ### end Alembic commands ###
|
|
@@ -54,7 +54,6 @@ from fides.common.api.v1.urn_registry import DATASETS_CLEAN, V1_URL_PREFIX
|
|
|
54
54
|
from fides.service.dataset.dataset_service import (
|
|
55
55
|
DatasetNotFoundException,
|
|
56
56
|
DatasetService,
|
|
57
|
-
LinkedDatasetException,
|
|
58
57
|
)
|
|
59
58
|
from fides.service.taxonomy.taxonomy_service import TaxonomyService
|
|
60
59
|
|
|
@@ -127,7 +126,7 @@ async def update_dataset(
|
|
|
127
126
|
except DatasetNotFoundException as e:
|
|
128
127
|
raise HTTPException(
|
|
129
128
|
status_code=HTTP_404_NOT_FOUND,
|
|
130
|
-
detail=str(e),
|
|
129
|
+
detail={"message": str(e)},
|
|
131
130
|
)
|
|
132
131
|
|
|
133
132
|
|
|
@@ -249,7 +248,7 @@ async def get_dataset(
|
|
|
249
248
|
except DatasetNotFoundException as e:
|
|
250
249
|
raise HTTPException(
|
|
251
250
|
status_code=HTTP_404_NOT_FOUND,
|
|
252
|
-
detail=str(e),
|
|
251
|
+
detail={"message": str(e)},
|
|
253
252
|
)
|
|
254
253
|
|
|
255
254
|
|
|
@@ -276,12 +275,7 @@ async def delete_dataset(
|
|
|
276
275
|
except DatasetNotFoundException as e:
|
|
277
276
|
raise HTTPException(
|
|
278
277
|
status_code=HTTP_404_NOT_FOUND,
|
|
279
|
-
detail=str(e),
|
|
280
|
-
)
|
|
281
|
-
except LinkedDatasetException as e:
|
|
282
|
-
raise HTTPException(
|
|
283
|
-
status_code=HTTP_400_BAD_REQUEST,
|
|
284
|
-
detail=str(e),
|
|
278
|
+
detail={"message": str(e)},
|
|
285
279
|
)
|
|
286
280
|
|
|
287
281
|
|
fides/api/common_exceptions.py
CHANGED
|
@@ -179,6 +179,10 @@ class AwaitingAsyncTask(BaseException):
|
|
|
179
179
|
"""Request Task is Awaiting Processing - Awaiting Async Task"""
|
|
180
180
|
|
|
181
181
|
|
|
182
|
+
class AwaitingAsyncProcessing(BaseException):
|
|
183
|
+
"""Request Task is actively being processed by external system - Fides is polling"""
|
|
184
|
+
|
|
185
|
+
|
|
182
186
|
class UpstreamTasksNotReady(BaseException):
|
|
183
187
|
"""Privacy Request Task awaiting upstream tasks"""
|
|
184
188
|
|
fides/api/main.py
CHANGED
|
@@ -45,9 +45,9 @@ from fides.api.service.privacy_request.email_batch_service import (
|
|
|
45
45
|
initiate_scheduled_batch_email_send,
|
|
46
46
|
)
|
|
47
47
|
from fides.api.service.privacy_request.request_service import (
|
|
48
|
-
initiate_async_tasks_status_polling,
|
|
49
48
|
initiate_interrupted_task_requeue_poll,
|
|
50
49
|
initiate_poll_for_exited_privacy_request_tasks,
|
|
50
|
+
initiate_polling_task_requeue,
|
|
51
51
|
initiate_scheduled_dsr_data_removal,
|
|
52
52
|
)
|
|
53
53
|
|
|
@@ -103,7 +103,7 @@ async def lifespan(wrapped_app: FastAPI) -> AsyncGenerator[None, None]:
|
|
|
103
103
|
initiate_poll_for_exited_privacy_request_tasks()
|
|
104
104
|
initiate_scheduled_dsr_data_removal()
|
|
105
105
|
initiate_interrupted_task_requeue_poll()
|
|
106
|
-
|
|
106
|
+
initiate_polling_task_requeue()
|
|
107
107
|
initiate_bcrypt_migration_task()
|
|
108
108
|
initiate_post_upgrade_index_creation()
|
|
109
109
|
|
fides/api/models/attachment.py
CHANGED
|
@@ -5,10 +5,12 @@ from fides.api.models.digest.conditional_dependencies import (
|
|
|
5
5
|
DigestConditionType,
|
|
6
6
|
)
|
|
7
7
|
from fides.api.models.digest.digest_config import DigestConfig, DigestType
|
|
8
|
+
from fides.api.models.digest.digest_execution import DigestTaskExecution
|
|
8
9
|
|
|
9
10
|
__all__ = [
|
|
10
11
|
"DigestConfig",
|
|
11
12
|
"DigestType",
|
|
12
13
|
"DigestCondition",
|
|
13
14
|
"DigestConditionType",
|
|
15
|
+
"DigestTaskExecution",
|
|
14
16
|
]
|
|
@@ -1,5 +1,5 @@
|
|
|
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
|
|
@@ -19,6 +19,9 @@ from fides.api.task.conditional_dependencies.schemas import (
|
|
|
19
19
|
ConditionLeaf,
|
|
20
20
|
)
|
|
21
21
|
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from fides.api.models.digest.digest_execution import DigestTaskExecution
|
|
24
|
+
|
|
22
25
|
|
|
23
26
|
class DigestType(str, Enum):
|
|
24
27
|
"""Types of digests that can be configured."""
|
|
@@ -60,6 +63,12 @@ class DigestConfig(Base):
|
|
|
60
63
|
back_populates="digest_config",
|
|
61
64
|
cascade="all, delete-orphan",
|
|
62
65
|
)
|
|
66
|
+
executions = relationship(
|
|
67
|
+
"DigestTaskExecution",
|
|
68
|
+
back_populates="digest_config",
|
|
69
|
+
cascade="all, delete-orphan",
|
|
70
|
+
order_by="DigestTaskExecution.created_at.desc()",
|
|
71
|
+
)
|
|
63
72
|
|
|
64
73
|
def get_receiver_condition(
|
|
65
74
|
self, db: Session
|
|
@@ -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
|
+
}
|
fides/api/models/event_audit.py
CHANGED
|
@@ -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."""
|
|
@@ -530,10 +530,6 @@ class PrivacyNotice(PrivacyNoticeBase, Base):
|
|
|
530
530
|
# to prevent the dry update from being added to Session.new
|
|
531
531
|
updated_attributes.pop("translations", [])
|
|
532
532
|
updated_attributes.pop("children", [])
|
|
533
|
-
# The source PrivacyNotice may have cached/computed attributes (e.g., @cached_property)
|
|
534
|
-
# stored on the instance dict. These should not be included in historical payloads.
|
|
535
|
-
# For example, 'cookies' is cached on the PrivacyNotice instance when accessed.
|
|
536
|
-
updated_attributes.pop("cookies", None)
|
|
537
533
|
|
|
538
534
|
# create a new object with the updated attribute data to keep this
|
|
539
535
|
# ORM object (i.e., `self`) pristine
|
|
@@ -680,10 +676,6 @@ def create_historical_record_for_notice_and_translation(
|
|
|
680
676
|
history_data: dict = create_historical_data_from_record(privacy_notice)
|
|
681
677
|
history_data.pop("translations", None)
|
|
682
678
|
history_data.pop("parent_id", None)
|
|
683
|
-
# The source PrivacyNotice may have cached/computed attributes (e.g., @cached_property)
|
|
684
|
-
# stored on the instance dict. These should not be included in historical payloads.
|
|
685
|
-
# For example, 'cookies' is cached on the PrivacyNotice instance when accessed.
|
|
686
|
-
history_data.pop("cookies", None)
|
|
687
679
|
|
|
688
680
|
updated_translation_data: dict = create_historical_data_from_record(
|
|
689
681
|
notice_translation
|