gooddata-pipelines 1.49.1.dev2__tar.gz → 1.50.1.dev1__tar.gz
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 gooddata-pipelines might be problematic. Click here for more details.
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/PKG-INFO +2 -2
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/TODO.md +1 -1
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/__init__.py +7 -1
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/gooddata_api.py +0 -54
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/backup_manager.py +42 -64
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/constants.py +3 -7
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/models/storage.py +4 -5
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/models/permissions.py +23 -79
- gooddata_pipelines-1.50.1.dev1/gooddata_pipelines/provisioning/entities/users/models/user_groups.py +37 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/models/users.py +9 -49
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/permissions.py +14 -6
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/user_groups.py +7 -1
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/users.py +3 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/workspaces/models.py +16 -15
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/workspaces/workspace.py +52 -5
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/workspaces/workspace_data_parser.py +9 -6
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/provisioning.py +24 -6
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/utils/context_objects.py +6 -6
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/utils/utils.py +3 -15
- gooddata_pipelines-1.50.1.dev1/gooddata_pipelines/utils/__init__.py +9 -0
- gooddata_pipelines-1.50.1.dev1/gooddata_pipelines/utils/rate_limiter.py +64 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/pyproject.toml +2 -2
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/backup_and_restore/test_backup.py +0 -4
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/conftest.py +59 -1
- gooddata_pipelines-1.50.1.dev1/tests/data/provisioning/entities/permissions/permissions_input_full_load.json +26 -0
- gooddata_pipelines-1.50.1.dev1/tests/data/provisioning/entities/permissions/permissions_input_incremental_load.json +37 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/users/users_input_full_load.json +3 -3
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/users/users_input_incremental_load.json +4 -4
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/users/test_permissions.py +82 -112
- gooddata_pipelines-1.50.1.dev1/tests/provisioning/entities/users/test_user_groups.py +74 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/users/test_users.py +4 -4
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/workspaces/test_provisioning.py +17 -10
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/workspaces/test_workspace.py +18 -16
- gooddata_pipelines-1.50.1.dev1/tests/provisioning/test_provisioning.py +104 -0
- gooddata_pipelines-1.50.1.dev1/tests/utils/test_rate_limiter.py +176 -0
- gooddata_pipelines-1.49.1.dev2/gooddata_pipelines/provisioning/entities/users/models/user_groups.py +0 -64
- gooddata_pipelines-1.49.1.dev2/tests/data/provisioning/entities/permissions/permissions_input_full_load.json +0 -22
- gooddata_pipelines-1.49.1.dev2/tests/data/provisioning/entities/permissions/permissions_input_incremental_load.json +0 -32
- gooddata_pipelines-1.49.1.dev2/tests/provisioning/entities/users/test_user_groups.py +0 -119
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/.gitignore +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/LICENSE.txt +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/Makefile +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/README.md +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/_version.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/__init__.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/exceptions.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/gooddata_api_wrapper.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/gooddata_sdk.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/utils.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/__init__.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/backup_input_processor.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/csv_reader.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/models/__init__.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/models/input_type.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/models/workspace_response.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/storage/__init__.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/storage/base_storage.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/storage/local_storage.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/storage/s3_storage.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/logger/__init__.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/logger/logger.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/__init__.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/assets/wdf_setting.json +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/__init__.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/user_data_filters/__init__.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/user_data_filters/models/__init__.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/user_data_filters/models/udf_models.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/user_data_filters/user_data_filters.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/__init__.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/models/__init__.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/workspaces/__init__.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/workspaces/workspace_data_filters.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/workspaces/workspace_data_validator.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/utils/__init__.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/utils/exceptions.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/py.typed +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/__init__.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/backup_and_restore/__init__.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/backup_and_restore/test_backup_input_processor.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/__init__.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/__init__.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_conf.yaml +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_exports/services/wsid1/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid1/analytics_model/analytical_dashboard_extensions/.gitkeep +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_exports/services/wsid1/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid1/analytics_model/analytical_dashboards/.gitkeep +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_exports/services/wsid1/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid1/analytics_model/dashboard_plugins/.gitkeep +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_exports/services/wsid1/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid1/analytics_model/filter_contexts/.gitkeep +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_exports/services/wsid1/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid1/analytics_model/metrics/.gitkeep +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_exports/services/wsid1/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid1/analytics_model/visualization_objects/.gitkeep +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_exports/services/wsid1/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid1/ldm/datasets/test.yaml +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_exports/services/wsid1/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid1/ldm/date_instances/testinstance.yaml +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_exports/services/wsid2/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid2/analytics_model/analytical_dashboard_extensions/.gitkeep +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_exports/services/wsid2/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid2/analytics_model/analytical_dashboards/id.yaml +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_exports/services/wsid2/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid2/analytics_model/dashboard_plugins/.gitkeep +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_exports/services/wsid2/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid2/analytics_model/filter_contexts/id.yaml +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_exports/services/wsid2/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid2/analytics_model/metrics/.gitkeep +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_exports/services/wsid2/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid2/analytics_model/visualization_objects/test.yaml +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_exports/services/wsid2/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid2/ldm/datasets/.gitkeep +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_exports/services/wsid2/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid2/ldm/date_instances/.gitkeep +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_exports/services/wsid3/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid3/analytics_model/analytical_dashboard_extensions/.gitkeep +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_exports/services/wsid3/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid3/analytics_model/analytical_dashboards/.gitkeep +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_exports/services/wsid3/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid3/analytics_model/dashboard_plugins/.gitkeep +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_exports/services/wsid3/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid3/analytics_model/filter_contexts/.gitkeep +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_exports/services/wsid3/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid3/analytics_model/metrics/.gitkeep +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_exports/services/wsid3/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid3/analytics_model/visualization_objects/.gitkeep +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_exports/services/wsid3/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid3/ldm/datasets/.gitkeep +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_exports/services/wsid3/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid3/ldm/date_instances/.gitkeep +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_exports/services/wsid3/20230713-132759-1_3_1_dev5/gooddata_layouts/services/workspaces/wsid3/user_data_filters/.gitkeep +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_local_conf.yaml +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/mock_responses.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/permissions/existing_upstream_permissions.json +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/permissions/permissions_expected_full_load.json +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/permissions/permissions_expected_incremental_load.json +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/users/existing_upstream_users.json +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/users/users_expected_full_load.json +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/users/users_expected_incremental_load.json +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/panther/__init__.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/panther/test_api_wrapper.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/panther/test_sdk_wrapper.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/__init__.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/__init__.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/users/__init__.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/workspaces/__init__.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/workspaces/test_workspace_data_filters.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/workspaces/test_workspace_data_parser.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/workspaces/test_workspace_data_validator.py +0 -0
- {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gooddata-pipelines
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.50.1.dev1
|
|
4
4
|
Summary: GoodData Cloud lifecycle automation pipelines
|
|
5
5
|
Author-email: GoodData <support@gooddata.com>
|
|
6
6
|
License: MIT
|
|
@@ -8,7 +8,7 @@ License-File: LICENSE.txt
|
|
|
8
8
|
Requires-Python: >=3.10
|
|
9
9
|
Requires-Dist: boto3-stubs<2.0.0,>=1.39.3
|
|
10
10
|
Requires-Dist: boto3<2.0.0,>=1.39.3
|
|
11
|
-
Requires-Dist: gooddata-sdk~=1.
|
|
11
|
+
Requires-Dist: gooddata-sdk~=1.50.1.dev1
|
|
12
12
|
Requires-Dist: pydantic<3.0.0,>=2.11.3
|
|
13
13
|
Requires-Dist: requests<3.0.0,>=2.32.3
|
|
14
14
|
Requires-Dist: types-pyyaml<7.0.0,>=6.0.12.20250326
|
|
@@ -10,7 +10,7 @@ A list of outstanding tasks, features, or technical debt to be addressed in this
|
|
|
10
10
|
|
|
11
11
|
- [ ] Integrate with GoodDataApiClient
|
|
12
12
|
- [ ] Consider replacing the SdkMethods wrapper with direct calls to the SDK methods
|
|
13
|
-
- [ ] Consider using orjson library instead of json
|
|
13
|
+
- [ ] Consider using orjson library instead of json to load test data
|
|
14
14
|
- [ ] Cleanup custom exceptions
|
|
15
15
|
- [ ] Improve test coverage. Write missing unit tests for legacy code (e.g., user data filters)
|
|
16
16
|
|
{gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/__init__.py
RENAMED
|
@@ -19,6 +19,7 @@ from .provisioning.entities.user_data_filters.user_data_filters import (
|
|
|
19
19
|
UserDataFilterProvisioner,
|
|
20
20
|
)
|
|
21
21
|
from .provisioning.entities.users.models.permissions import (
|
|
22
|
+
EntityType,
|
|
22
23
|
PermissionFullLoad,
|
|
23
24
|
PermissionIncrementalLoad,
|
|
24
25
|
)
|
|
@@ -33,7 +34,10 @@ from .provisioning.entities.users.models.users import (
|
|
|
33
34
|
from .provisioning.entities.users.permissions import PermissionProvisioner
|
|
34
35
|
from .provisioning.entities.users.user_groups import UserGroupProvisioner
|
|
35
36
|
from .provisioning.entities.users.users import UserProvisioner
|
|
36
|
-
from .provisioning.entities.workspaces.models import
|
|
37
|
+
from .provisioning.entities.workspaces.models import (
|
|
38
|
+
WorkspaceFullLoad,
|
|
39
|
+
WorkspaceIncrementalLoad,
|
|
40
|
+
)
|
|
37
41
|
from .provisioning.entities.workspaces.workspace import WorkspaceProvisioner
|
|
38
42
|
|
|
39
43
|
__all__ = [
|
|
@@ -52,8 +56,10 @@ __all__ = [
|
|
|
52
56
|
"UserGroupFullLoad",
|
|
53
57
|
"UserProvisioner",
|
|
54
58
|
"UserGroupProvisioner",
|
|
59
|
+
"WorkspaceIncrementalLoad",
|
|
55
60
|
"PermissionProvisioner",
|
|
56
61
|
"UserDataFilterProvisioner",
|
|
57
62
|
"UserDataFilterFullLoad",
|
|
63
|
+
"EntityType",
|
|
58
64
|
"__version__",
|
|
59
65
|
]
|
|
@@ -7,9 +7,6 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
import requests
|
|
9
9
|
|
|
10
|
-
# TODO: Limit the use of "typing.Any". Improve readability by using either models
|
|
11
|
-
# or typed dicts.
|
|
12
|
-
|
|
13
10
|
TIMEOUT = 60
|
|
14
11
|
REQUEST_PAGE_SIZE = 250
|
|
15
12
|
API_VERSION = "v1"
|
|
@@ -55,42 +52,6 @@ class ApiMethods:
|
|
|
55
52
|
"""
|
|
56
53
|
return f"{self.base_url}{endpoint}"
|
|
57
54
|
|
|
58
|
-
def get_custom_application_setting(
|
|
59
|
-
self, workspace_id: str, setting_id: str
|
|
60
|
-
) -> requests.Response:
|
|
61
|
-
"""Gets a custom application setting.
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
workspace_id (str): The ID of the workspace.
|
|
65
|
-
setting_id (str): The ID of the custom application setting.
|
|
66
|
-
Returns:
|
|
67
|
-
requests.Response: The response from the server containing the
|
|
68
|
-
custom application setting.
|
|
69
|
-
"""
|
|
70
|
-
url = f"/entities/workspaces/{workspace_id}/customApplicationSettings/{setting_id}"
|
|
71
|
-
return self._get(url)
|
|
72
|
-
|
|
73
|
-
def put_custom_application_setting(
|
|
74
|
-
self, workspace_id: str, setting_id: str, data: dict[str, Any]
|
|
75
|
-
) -> requests.Response:
|
|
76
|
-
url = f"/entities/workspaces/{workspace_id}/customApplicationSettings/{setting_id}"
|
|
77
|
-
return self._put(url, data, self.headers)
|
|
78
|
-
|
|
79
|
-
def post_custom_application_setting(
|
|
80
|
-
self, workspace_id: str, data: dict[str, Any]
|
|
81
|
-
) -> requests.Response:
|
|
82
|
-
"""Creates a custom application setting for a given workspace.
|
|
83
|
-
|
|
84
|
-
Args:
|
|
85
|
-
workspace_id (str): The ID of the workspace.
|
|
86
|
-
data (dict[str, Any]): The data for the custom application setting.
|
|
87
|
-
Returns:
|
|
88
|
-
requests.Response: The response from the server containing the
|
|
89
|
-
created custom application setting.
|
|
90
|
-
"""
|
|
91
|
-
url = f"/entities/workspaces/{workspace_id}/customApplicationSettings/"
|
|
92
|
-
return self._post(url, data, self.headers)
|
|
93
|
-
|
|
94
55
|
def get_all_workspace_data_filters(
|
|
95
56
|
self, workspace_id: str
|
|
96
57
|
) -> requests.Response:
|
|
@@ -201,21 +162,6 @@ class ApiMethods:
|
|
|
201
162
|
endpoint,
|
|
202
163
|
)
|
|
203
164
|
|
|
204
|
-
def post_workspace_data_filter(
|
|
205
|
-
self, workspace_id: str, data: dict[str, Any]
|
|
206
|
-
) -> requests.Response:
|
|
207
|
-
"""Creates a workspace data filter for a given workspace.
|
|
208
|
-
|
|
209
|
-
Args:
|
|
210
|
-
workspace_id (str): The ID of the workspace.
|
|
211
|
-
data (dict[str, Any]): The data for the workspace data filter.
|
|
212
|
-
Returns:
|
|
213
|
-
requests.Response: The response from the server containing the
|
|
214
|
-
created workspace data filter.
|
|
215
|
-
"""
|
|
216
|
-
endpoint = f"/entities/workspaces/{workspace_id}/workspaceDataFilters"
|
|
217
|
-
return self._post(endpoint, data, self.headers)
|
|
218
|
-
|
|
219
165
|
def get_user_data_filters(self, workspace_id: str) -> requests.Response:
|
|
220
166
|
"""Gets the user data filters for a given workspace."""
|
|
221
167
|
endpoint = f"/layout/workspaces/{workspace_id}/userDataFilters"
|
|
@@ -4,10 +4,8 @@ import json
|
|
|
4
4
|
import os
|
|
5
5
|
import shutil
|
|
6
6
|
import tempfile
|
|
7
|
-
import threading
|
|
8
7
|
import time
|
|
9
8
|
import traceback
|
|
10
|
-
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
11
9
|
from dataclasses import dataclass
|
|
12
10
|
from pathlib import Path
|
|
13
11
|
from typing import Any, Type
|
|
@@ -39,6 +37,7 @@ from gooddata_pipelines.backup_and_restore.storage.s3_storage import (
|
|
|
39
37
|
S3Storage,
|
|
40
38
|
)
|
|
41
39
|
from gooddata_pipelines.logger import LogObserver
|
|
40
|
+
from gooddata_pipelines.utils.rate_limiter import RateLimiter
|
|
42
41
|
|
|
43
42
|
|
|
44
43
|
@dataclass
|
|
@@ -60,6 +59,10 @@ class BackupManager:
|
|
|
60
59
|
|
|
61
60
|
self.loader = BackupInputProcessor(self._api, self.config.api_page_size)
|
|
62
61
|
|
|
62
|
+
self._api_rate_limiter = RateLimiter(
|
|
63
|
+
calls_per_second=self.config.api_calls_per_second,
|
|
64
|
+
)
|
|
65
|
+
|
|
63
66
|
@classmethod
|
|
64
67
|
def create(
|
|
65
68
|
cls: Type["BackupManager"],
|
|
@@ -95,11 +98,12 @@ class BackupManager:
|
|
|
95
98
|
|
|
96
99
|
def get_user_data_filters(self, ws_id: str) -> dict:
|
|
97
100
|
"""Returns the user data filters for the specified workspace."""
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
with self._api_rate_limiter:
|
|
102
|
+
response: requests.Response = self._api.get_user_data_filters(ws_id)
|
|
103
|
+
if response.ok:
|
|
104
|
+
return response.json()
|
|
105
|
+
else:
|
|
106
|
+
raise RuntimeError(f"{response.status_code}: {response.text}")
|
|
103
107
|
|
|
104
108
|
def _store_user_data_filters(
|
|
105
109
|
self,
|
|
@@ -144,14 +148,17 @@ class BackupManager:
|
|
|
144
148
|
|
|
145
149
|
def _get_automations_from_api(self, workspace_id: str) -> Any:
|
|
146
150
|
"""Returns automations for the workspace as JSON."""
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
else:
|
|
151
|
-
raise RuntimeError(
|
|
152
|
-
f"Failed to get automations for {workspace_id}. "
|
|
153
|
-
+ f"{response.status_code}: {response.text}"
|
|
151
|
+
with self._api_rate_limiter:
|
|
152
|
+
response: requests.Response = self._api.get_automations(
|
|
153
|
+
workspace_id
|
|
154
154
|
)
|
|
155
|
+
if response.ok:
|
|
156
|
+
return response.json()
|
|
157
|
+
else:
|
|
158
|
+
raise RuntimeError(
|
|
159
|
+
f"Failed to get automations for {workspace_id}. "
|
|
160
|
+
+ f"{response.status_code}: {response.text}"
|
|
161
|
+
)
|
|
155
162
|
|
|
156
163
|
def _store_automations(self, export_path: Path, workspace_id: str) -> None:
|
|
157
164
|
"""Stores the automations in the specified export path."""
|
|
@@ -183,7 +190,8 @@ class BackupManager:
|
|
|
183
190
|
) -> None:
|
|
184
191
|
"""Stores the filter views in the specified export path."""
|
|
185
192
|
# Get the filter views YAML files from the API
|
|
186
|
-
self.
|
|
193
|
+
with self._api_rate_limiter:
|
|
194
|
+
self._api.store_declarative_filter_views(workspace_id, export_path)
|
|
187
195
|
|
|
188
196
|
# Move filter views to the subfolder containing the analytics model
|
|
189
197
|
self._move_folder(
|
|
@@ -231,7 +239,10 @@ class BackupManager:
|
|
|
231
239
|
# the SDK. That way we could save and package all the declarations
|
|
232
240
|
# directly instead of reorganizing the folder structures. That should
|
|
233
241
|
# be more transparent/readable and possibly safer for threading
|
|
234
|
-
self.
|
|
242
|
+
with self._api_rate_limiter:
|
|
243
|
+
self._api.store_declarative_workspace(
|
|
244
|
+
workspace_id, export_path
|
|
245
|
+
)
|
|
235
246
|
self.store_declarative_filter_views(export_path, workspace_id)
|
|
236
247
|
self._store_automations(export_path, workspace_id)
|
|
237
248
|
|
|
@@ -291,7 +302,6 @@ class BackupManager:
|
|
|
291
302
|
def _process_batch(
|
|
292
303
|
self,
|
|
293
304
|
batch: BackupBatch,
|
|
294
|
-
stop_event: threading.Event,
|
|
295
305
|
retry_count: int = 0,
|
|
296
306
|
) -> None:
|
|
297
307
|
"""Processes a single batch of workspaces for backup.
|
|
@@ -299,10 +309,6 @@ class BackupManager:
|
|
|
299
309
|
and retry with exponential backoff up to BackupSettings.MAX_RETRIES.
|
|
300
310
|
The base wait time is defined by BackupSettings.RETRY_DELAY.
|
|
301
311
|
"""
|
|
302
|
-
if stop_event.is_set():
|
|
303
|
-
# If the stop_event flag is set, return. This will terminate the thread
|
|
304
|
-
return
|
|
305
|
-
|
|
306
312
|
try:
|
|
307
313
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
308
314
|
self._get_workspace_export(tmpdir, batch.list_of_ids)
|
|
@@ -314,10 +320,7 @@ class BackupManager:
|
|
|
314
320
|
self.storage.export(tmpdir, self.org_id)
|
|
315
321
|
|
|
316
322
|
except Exception as e:
|
|
317
|
-
if
|
|
318
|
-
return
|
|
319
|
-
|
|
320
|
-
elif retry_count < BackupSettings.MAX_RETRIES:
|
|
323
|
+
if retry_count < BackupSettings.MAX_RETRIES:
|
|
321
324
|
# Retry with exponential backoff until MAX_RETRIES
|
|
322
325
|
next_retry = retry_count + 1
|
|
323
326
|
wait_time = BackupSettings.RETRY_DELAY**next_retry
|
|
@@ -328,55 +331,28 @@ class BackupManager:
|
|
|
328
331
|
)
|
|
329
332
|
|
|
330
333
|
time.sleep(wait_time)
|
|
331
|
-
self._process_batch(batch,
|
|
334
|
+
self._process_batch(batch, next_retry)
|
|
332
335
|
else:
|
|
333
336
|
# If the batch fails after MAX_RETRIES, raise the error
|
|
334
337
|
self.logger.error(f"Batch failed: {e.__class__.__name__}: {e}")
|
|
335
338
|
raise
|
|
336
339
|
|
|
337
|
-
def
|
|
340
|
+
def _process_batches(
|
|
338
341
|
self,
|
|
339
342
|
batches: list[BackupBatch],
|
|
340
343
|
) -> None:
|
|
341
344
|
"""
|
|
342
|
-
Processes batches
|
|
343
|
-
|
|
345
|
+
Processes batches sequentially to avoid overloading the API.
|
|
346
|
+
If any batch fails, the processing will stop.
|
|
344
347
|
"""
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
with ThreadPoolExecutor(
|
|
350
|
-
max_workers=self.config.max_workers
|
|
351
|
-
) as executor:
|
|
352
|
-
# Set the futures tasks.
|
|
353
|
-
futures = []
|
|
354
|
-
for batch in batches:
|
|
355
|
-
futures.append(
|
|
356
|
-
executor.submit(
|
|
357
|
-
self._process_batch,
|
|
358
|
-
batch,
|
|
359
|
-
stop_event,
|
|
360
|
-
)
|
|
361
|
-
)
|
|
362
|
-
|
|
363
|
-
# Process futures as they complete
|
|
364
|
-
for future in as_completed(futures):
|
|
365
|
-
try:
|
|
366
|
-
future.result()
|
|
367
|
-
except Exception:
|
|
368
|
-
# On failure, set the flag to True - signal running processes to stop
|
|
369
|
-
stop_event.set()
|
|
370
|
-
|
|
371
|
-
# Cancel unstarted threads
|
|
372
|
-
for f in futures:
|
|
373
|
-
if not f.done():
|
|
374
|
-
f.cancel()
|
|
375
|
-
|
|
376
|
-
raise
|
|
348
|
+
for i, batch in enumerate(batches, 1):
|
|
349
|
+
self.logger.info(f"Processing batch {i}/{len(batches)}...")
|
|
350
|
+
self._process_batch(batch)
|
|
377
351
|
|
|
378
352
|
def backup_workspaces(
|
|
379
|
-
self,
|
|
353
|
+
self,
|
|
354
|
+
path_to_csv: str | None = None,
|
|
355
|
+
workspace_ids: list[str] | None = None,
|
|
380
356
|
) -> None:
|
|
381
357
|
"""Runs the backup process for a list of workspace IDs.
|
|
382
358
|
|
|
@@ -391,7 +367,9 @@ class BackupManager:
|
|
|
391
367
|
self._backup(InputType.LIST_OF_WORKSPACES, path_to_csv, workspace_ids)
|
|
392
368
|
|
|
393
369
|
def backup_hierarchies(
|
|
394
|
-
self,
|
|
370
|
+
self,
|
|
371
|
+
path_to_csv: str | None = None,
|
|
372
|
+
workspace_ids: list[str] | None = None,
|
|
395
373
|
) -> None:
|
|
396
374
|
"""Runs the backup process for a list of hierarchies.
|
|
397
375
|
|
|
@@ -436,7 +414,7 @@ class BackupManager:
|
|
|
436
414
|
f"Exporting {len(workspaces_to_export)} workspaces in {len(batches)} batches."
|
|
437
415
|
)
|
|
438
416
|
|
|
439
|
-
self.
|
|
417
|
+
self._process_batches(batches)
|
|
440
418
|
|
|
441
419
|
self.logger.info("Backup completed")
|
|
442
420
|
except Exception as e:
|
|
@@ -21,19 +21,15 @@ class DirNames:
|
|
|
21
21
|
UDF = "user_data_filters"
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
@dataclass(frozen=True)
|
|
25
|
-
class ConcurrencyDefaults:
|
|
26
|
-
MAX_WORKERS = 1
|
|
27
|
-
DEFAULT_BATCH_SIZE = 100
|
|
28
|
-
|
|
29
|
-
|
|
30
24
|
@dataclass(frozen=True)
|
|
31
25
|
class ApiDefaults:
|
|
32
26
|
DEFAULT_PAGE_SIZE = 100
|
|
27
|
+
DEFAULT_BATCH_SIZE = 100
|
|
28
|
+
DEFAULT_API_CALLS_PER_SECOND = 1.0
|
|
33
29
|
|
|
34
30
|
|
|
35
31
|
@dataclass(frozen=True)
|
|
36
|
-
class BackupSettings(
|
|
32
|
+
class BackupSettings(ApiDefaults):
|
|
37
33
|
MAX_RETRIES = 3
|
|
38
34
|
RETRY_DELAY = 5 # seconds
|
|
39
35
|
TIMESTAMP_SDK_FOLDER = (
|
|
@@ -83,14 +83,13 @@ class BackupRestoreConfig(BaseModel):
|
|
|
83
83
|
description="Batch size must be greater than 0",
|
|
84
84
|
),
|
|
85
85
|
] = Field(default=BackupSettings.DEFAULT_BATCH_SIZE)
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
api_calls_per_second: Annotated[
|
|
87
|
+
float,
|
|
88
88
|
Field(
|
|
89
89
|
gt=0,
|
|
90
|
-
|
|
91
|
-
description="Max workers must be greater than 0 and less than 3",
|
|
90
|
+
description="Maximum API calls per second (rate limiting)",
|
|
92
91
|
),
|
|
93
|
-
] = Field(default=BackupSettings.
|
|
92
|
+
] = Field(default=BackupSettings.DEFAULT_API_CALLS_PER_SECOND)
|
|
94
93
|
|
|
95
94
|
@classmethod
|
|
96
95
|
def from_yaml(cls, conf_path: str) -> "BackupRestoreConfig":
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# (C) 2025 GoodData Corporation
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
from enum import Enum
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Iterator, TypeAlias
|
|
5
5
|
|
|
6
6
|
import attrs
|
|
7
7
|
from gooddata_sdk.catalog.identifier import CatalogAssigneeIdentifier
|
|
@@ -14,85 +14,29 @@ from pydantic import BaseModel
|
|
|
14
14
|
from gooddata_pipelines.provisioning.utils.exceptions import BaseUserException
|
|
15
15
|
|
|
16
16
|
TargetsPermissionDict: TypeAlias = dict[str, dict[str, bool]]
|
|
17
|
-
ConstructorType = TypeVar("ConstructorType", bound="ConstructorMixin")
|
|
18
17
|
|
|
19
18
|
|
|
20
|
-
class
|
|
19
|
+
class EntityType(str, Enum):
|
|
21
20
|
# NOTE: Start using StrEnum with Python 3.11
|
|
22
21
|
user = "user"
|
|
23
22
|
user_group = "userGroup"
|
|
24
23
|
|
|
25
24
|
|
|
26
|
-
class
|
|
27
|
-
@staticmethod
|
|
28
|
-
def _get_id_and_type(
|
|
29
|
-
permission: dict[str, Any],
|
|
30
|
-
) -> tuple[str, PermissionType]:
|
|
31
|
-
user_id: str | None = permission.get("user_id")
|
|
32
|
-
user_group_id: str | None = permission.get("ug_id")
|
|
33
|
-
if user_id and user_group_id:
|
|
34
|
-
raise ValueError("Only one of user_id or ug_id must be present")
|
|
35
|
-
elif user_id:
|
|
36
|
-
return user_id, PermissionType.user
|
|
37
|
-
elif user_group_id:
|
|
38
|
-
return user_group_id, PermissionType.user_group
|
|
39
|
-
else:
|
|
40
|
-
raise ValueError("Either user_id or ug_id must be present")
|
|
41
|
-
|
|
42
|
-
@classmethod
|
|
43
|
-
def from_list_of_dicts(
|
|
44
|
-
cls: type[ConstructorType], data: list[dict[str, Any]]
|
|
45
|
-
) -> list[ConstructorType]:
|
|
46
|
-
"""Creates a list of instances from list of dicts."""
|
|
47
|
-
# NOTE: We can use typing.Self for the return type in Python 3.11
|
|
48
|
-
permissions = []
|
|
49
|
-
for permission in data:
|
|
50
|
-
permissions.append(cls.from_dict(permission))
|
|
51
|
-
return permissions
|
|
52
|
-
|
|
53
|
-
@classmethod
|
|
54
|
-
@abstractmethod
|
|
55
|
-
def from_dict(cls, data: dict[str, Any]) -> Any:
|
|
56
|
-
"""Construction form a dictionary to be implemented by subclasses."""
|
|
57
|
-
pass
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
class PermissionIncrementalLoad(BaseModel, ConstructorMixin):
|
|
25
|
+
class BasePermission(BaseModel):
|
|
61
26
|
permission: str
|
|
62
27
|
workspace_id: str
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
is_active: bool
|
|
28
|
+
entity_id: str
|
|
29
|
+
entity_type: EntityType
|
|
66
30
|
|
|
67
|
-
@classmethod
|
|
68
|
-
def from_dict(cls, data: dict[str, Any]) -> "PermissionIncrementalLoad":
|
|
69
|
-
"""Returns an instance of PermissionIncrementalLoad from a dictionary."""
|
|
70
|
-
id_, target_type = cls._get_id_and_type(data)
|
|
71
|
-
return cls(
|
|
72
|
-
permission=data["ws_permissions"],
|
|
73
|
-
workspace_id=data["ws_id"],
|
|
74
|
-
id_=id_,
|
|
75
|
-
type_=target_type,
|
|
76
|
-
is_active=data["is_active"],
|
|
77
|
-
)
|
|
78
31
|
|
|
32
|
+
class PermissionFullLoad(BasePermission):
|
|
33
|
+
"""Input validator for full load of workspace permissions provisioning."""
|
|
79
34
|
|
|
80
|
-
class PermissionFullLoad(BaseModel, ConstructorMixin):
|
|
81
|
-
permission: str
|
|
82
|
-
workspace_id: str
|
|
83
|
-
id_: str
|
|
84
|
-
type_: PermissionType
|
|
85
35
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return cls(
|
|
91
|
-
permission=data["ws_permissions"],
|
|
92
|
-
workspace_id=data["ws_id"],
|
|
93
|
-
id_=id_,
|
|
94
|
-
type_=target_type,
|
|
95
|
-
)
|
|
36
|
+
class PermissionIncrementalLoad(BasePermission):
|
|
37
|
+
"""Input validator for incremental load of workspace permissions provisioning."""
|
|
38
|
+
|
|
39
|
+
is_active: bool
|
|
96
40
|
|
|
97
41
|
|
|
98
42
|
@attrs.define
|
|
@@ -117,7 +61,7 @@ class PermissionDeclaration:
|
|
|
117
61
|
permission.assignee.id,
|
|
118
62
|
)
|
|
119
63
|
|
|
120
|
-
if permission_type ==
|
|
64
|
+
if permission_type == EntityType.user.value:
|
|
121
65
|
target_dict = users
|
|
122
66
|
else:
|
|
123
67
|
target_dict = user_groups
|
|
@@ -170,7 +114,7 @@ class PermissionDeclaration:
|
|
|
170
114
|
|
|
171
115
|
for user_id, permissions in self.users.items():
|
|
172
116
|
assignee = CatalogAssigneeIdentifier(
|
|
173
|
-
id=user_id, type=
|
|
117
|
+
id=user_id, type=EntityType.user.value
|
|
174
118
|
)
|
|
175
119
|
for declaration in self._permissions_for_target(
|
|
176
120
|
permissions, assignee
|
|
@@ -179,7 +123,7 @@ class PermissionDeclaration:
|
|
|
179
123
|
|
|
180
124
|
for ug_id, permissions in self.user_groups.items():
|
|
181
125
|
assignee = CatalogAssigneeIdentifier(
|
|
182
|
-
id=ug_id, type=
|
|
126
|
+
id=ug_id, type=EntityType.user_group.value
|
|
183
127
|
)
|
|
184
128
|
for declaration in self._permissions_for_target(
|
|
185
129
|
permissions, assignee
|
|
@@ -200,15 +144,15 @@ class PermissionDeclaration:
|
|
|
200
144
|
"""
|
|
201
145
|
target_dict = (
|
|
202
146
|
self.users
|
|
203
|
-
if permission.
|
|
147
|
+
if permission.entity_type == EntityType.user
|
|
204
148
|
else self.user_groups
|
|
205
149
|
)
|
|
206
150
|
|
|
207
|
-
if permission.
|
|
208
|
-
target_dict[permission.
|
|
151
|
+
if permission.entity_id not in target_dict:
|
|
152
|
+
target_dict[permission.entity_id] = {}
|
|
209
153
|
|
|
210
154
|
is_active = permission.is_active
|
|
211
|
-
target_permissions = target_dict[permission.
|
|
155
|
+
target_permissions = target_dict[permission.entity_id]
|
|
212
156
|
permission_value = permission.permission
|
|
213
157
|
|
|
214
158
|
if permission_value not in target_permissions:
|
|
@@ -233,14 +177,14 @@ class PermissionDeclaration:
|
|
|
233
177
|
"""
|
|
234
178
|
target_dict = (
|
|
235
179
|
self.users
|
|
236
|
-
if permission.
|
|
180
|
+
if permission.entity_type == EntityType.user
|
|
237
181
|
else self.user_groups
|
|
238
182
|
)
|
|
239
183
|
|
|
240
|
-
if permission.
|
|
241
|
-
target_dict[permission.
|
|
184
|
+
if permission.entity_id not in target_dict:
|
|
185
|
+
target_dict[permission.entity_id] = {}
|
|
242
186
|
|
|
243
|
-
target_permissions = target_dict[permission.
|
|
187
|
+
target_permissions = target_dict[permission.entity_id]
|
|
244
188
|
permission_value = permission.permission
|
|
245
189
|
|
|
246
190
|
if permission_value not in target_permissions:
|
gooddata_pipelines-1.50.1.dev1/gooddata_pipelines/provisioning/entities/users/models/user_groups.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# (C) 2025 GoodData Corporation
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field, ValidationInfo, field_validator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class UserGroupBase(BaseModel):
|
|
7
|
+
user_group_id: str
|
|
8
|
+
user_group_name: str
|
|
9
|
+
parent_user_groups: list[str] = Field(default_factory=list)
|
|
10
|
+
|
|
11
|
+
@field_validator("user_group_name", mode="before")
|
|
12
|
+
@classmethod
|
|
13
|
+
def validate_user_group_name(
|
|
14
|
+
cls, v: str | None, info: ValidationInfo
|
|
15
|
+
) -> str:
|
|
16
|
+
"""If user_group_name is None or empty, default to user_group_id."""
|
|
17
|
+
if not v: # handles None and empty string
|
|
18
|
+
return info.data.get("user_group_id", "")
|
|
19
|
+
return v
|
|
20
|
+
|
|
21
|
+
@field_validator("parent_user_groups", mode="before")
|
|
22
|
+
@classmethod
|
|
23
|
+
def validate_parent_user_groups(cls, v: list[str] | None) -> list[str]:
|
|
24
|
+
"""If parent_user_groups is None or empty, default to empty list."""
|
|
25
|
+
if not v:
|
|
26
|
+
return []
|
|
27
|
+
return v
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class UserGroupFullLoad(UserGroupBase):
|
|
31
|
+
"""Input validator for full load of user group provisioning."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class UserGroupIncrementalLoad(UserGroupBase):
|
|
35
|
+
"""Input validator for incremental load of user group provisioning."""
|
|
36
|
+
|
|
37
|
+
is_active: bool
|