gooddata-pipelines 1.74.1.dev1__tar.gz → 1.74.1.dev3__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.
- gooddata_pipelines-1.74.1.dev3/AGENTS.md +96 -0
- gooddata_pipelines-1.74.1.dev3/CLAUDE.md +1 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/PKG-INFO +12 -9
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/README.md +10 -7
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/pyproject.toml +4 -8
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/api/gooddata_api.py +13 -39
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/api/gooddata_api_wrapper.py +1 -3
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/backup_input_processor.py +10 -19
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/backup_manager.py +16 -44
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/base_manager.py +7 -9
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/constants.py +1 -5
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/csv_reader.py +2 -6
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/models/storage.py +10 -25
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/restore_manager.py +29 -82
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/storage/azure_storage.py +16 -44
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/storage/base_storage.py +2 -6
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/storage/local_storage.py +3 -9
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/storage/s3_storage.py +8 -23
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/ldm_extension/input_processor.py +19 -70
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/ldm_extension/input_validator.py +16 -29
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/ldm_extension/ldm_extension_manager.py +25 -68
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/ldm_extension/models/custom_data_object.py +6 -20
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/user_data_filters/user_data_filters.py +22 -67
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/users/models/permissions.py +15 -48
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/users/models/user_groups.py +1 -3
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/users/permissions.py +15 -47
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/users/user_groups.py +21 -59
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/users/users.py +11 -29
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/workspaces/models.py +1 -3
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/workspaces/workspace.py +27 -79
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/workspaces/workspace_data_filters.py +21 -54
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/workspaces/workspace_data_parser.py +11 -33
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/workspaces/workspace_data_validator.py +15 -35
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/generic/config.py +4 -9
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/provisioning.py +11 -31
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/utils/exceptions.py +13 -39
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/utils/utils.py +5 -15
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/utils/decorators.py +1 -3
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/utils/file_utils.py +2 -2
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/utils/rate_limiter.py +4 -6
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/backup_and_restore/test_backup.py +16 -45
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/backup_and_restore/test_backup_input_processor.py +7 -22
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/backup_and_restore/test_restore.py +23 -68
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/conftest.py +2 -3
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/panther/test_api_wrapper.py +0 -1
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/panther/test_sdk_wrapper.py +4 -12
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/entities/users/test_permissions.py +38 -77
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/entities/users/test_user_groups.py +4 -13
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/entities/users/test_users.py +38 -62
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/entities/workspaces/test_workspace.py +3 -4
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/entities/workspaces/test_workspace_data_filters.py +18 -44
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/entities/workspaces/test_workspace_data_parser.py +3 -4
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/entities/workspaces/test_workspace_data_validator.py +6 -21
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/test_provisioning.py +3 -5
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/test_ldm_extension/conftest.py +0 -1
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/test_ldm_extension/test_input_processor.py +9 -26
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/test_ldm_extension/test_input_validator.py +1 -4
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/test_ldm_extension/test_ldm_extension_manager.py +37 -97
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/test_ldm_extension/test_merge_ldm.py +12 -23
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/test_ldm_extension/test_models/test_analytical_object.py +3 -5
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/test_ldm_extension/test_models/test_custom_data_object.py +8 -24
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/utils/test_decorators.py +1 -4
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/utils/test_rate_limiter.py +3 -4
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/.gitignore +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/LICENSE.txt +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/Makefile +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/_version.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/api/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/models/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/models/input_type.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/models/workspace_response.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/backup_and_restore/storage/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/ldm_extension/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/ldm_extension/models/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/ldm_extension/models/aliases.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/ldm_extension/models/analytical_object.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/logger/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/logger/logger.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/assets/wdf_setting.json +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/user_data_filters/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/user_data_filters/models/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/user_data_filters/models/udf_models.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/users/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/users/models/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/users/models/users.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/entities/workspaces/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/generic/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/generic/provision.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/utils/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/provisioning/utils/context_objects.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/py.typed +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/src/gooddata_pipelines/utils/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/backup_and_restore/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_conf.yaml +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/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.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/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.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/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.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/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.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/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.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/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.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/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.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/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.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/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.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/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.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/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.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/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.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/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.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/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.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/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.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/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.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/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.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/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.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/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.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/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.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/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.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/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.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/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.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/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.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/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.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/backup/test_local_conf.yaml +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/custom_fields/response_get_all_dashboards.json +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/custom_fields/response_get_all_metrics.json +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/custom_fields/response_get_all_visualizations.json +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/mock_responses.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/profiles.yaml +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/permissions/existing_upstream_permissions.json +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/permissions/permissions_expected_full_load.json +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/permissions/permissions_expected_incremental_load.json +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/permissions/permissions_input_full_load.json +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/permissions/permissions_input_incremental_load.json +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/users/existing_upstream_users.json +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/users/profile_response_content.json +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/users/users_expected_full_load.json +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/users/users_expected_incremental_load.json +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/users/users_input_full_load.json +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/users/users_input_full_load_modifies_protected_user.json +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/users/users_input_incremental_load.json +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/users/users_input_incremental_load_deletes_protected_user.json +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/provisioning/entities/users/users_input_incremental_load_modifies_protected_user.json +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/restore/test_conf.yaml +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/restore/test_udf_root/filter1.yaml +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/restore/test_udf_root/filter2.yaml +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/restore/test_udf_root/user_data_filters/filter1.yaml +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/data/restore/test_udf_root/user_data_filters/filter2.yaml +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/panther/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/entities/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/entities/users/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/entities/workspaces/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/provisioning/entities/workspaces/test_provisioning.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/test_ldm_extension/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tests/test_ldm_extension/test_models/__init__.py +0 -0
- {gooddata_pipelines-1.74.1.dev1 → gooddata_pipelines-1.74.1.dev3}/tox.ini +0 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# gooddata-pipelines
|
|
2
|
+
|
|
3
|
+
A lifecycle-automation library on top of `gooddata-sdk`, covering three independent areas:
|
|
4
|
+
provisioning users, groups, permissions and workspaces; backing up and restoring workspace
|
|
5
|
+
metadata to local disk, S3 or Azure Blob; and extending an existing workspace's logical
|
|
6
|
+
data model with custom datasets and fields. Each area is entered through a small set of
|
|
7
|
+
manager or provisioner classes.
|
|
8
|
+
|
|
9
|
+
## Owns
|
|
10
|
+
|
|
11
|
+
- Provisioning flows for users, user groups, permissions, user data filters, and workspace
|
|
12
|
+
hierarchies, with pydantic-validated inputs
|
|
13
|
+
- Backup and restore of workspace metadata across local, S3 and Azure Blob targets
|
|
14
|
+
- LDM extension for child workspaces
|
|
15
|
+
|
|
16
|
+
## Does NOT Own
|
|
17
|
+
|
|
18
|
+
- Core platform API and service abstractions → `gooddata-sdk`
|
|
19
|
+
- dbt metadata conversion → `gooddata-dbt`
|
|
20
|
+
- Flight RPC runtime → `gooddata-flight-server`
|
|
21
|
+
|
|
22
|
+
## Architecture
|
|
23
|
+
|
|
24
|
+
| Module | Role |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `provisioning/` | `UserProvisioner`, `UserGroupProvisioner`, `PermissionProvisioner`, `WorkspaceProvisioner`, `UserDataFilterProvisioner` |
|
|
27
|
+
| `backup_and_restore/` | `BackupManager`, `RestoreManager`, plus `storage/` backends |
|
|
28
|
+
| `ldm_extension/` | `LdmExtensionManager` |
|
|
29
|
+
| `api/`, `utils/`, `logger/` | supporting HTTP, helpers and the log observer |
|
|
30
|
+
|
|
31
|
+
**Depends on**: `gooddata-sdk`, `pydantic`, `requests`, `boto3`, `azure-storage-blob`,
|
|
32
|
+
`azure-identity`.
|
|
33
|
+
|
|
34
|
+
### Construction
|
|
35
|
+
|
|
36
|
+
Managers and provisioners are built through classmethods, not `__init__`:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
provisioner = UserProvisioner.create(host, token)
|
|
40
|
+
# or
|
|
41
|
+
provisioner = UserProvisioner.create_from_profile(profile="default")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Each exposes a `.logger` (`LogObserver`) that a stdlib logger can `.subscribe()` to.
|
|
45
|
+
|
|
46
|
+
Provisioning is also reachable generically via `provision(data, workflow_type, host,
|
|
47
|
+
token)` driven by `WorkflowType`, intended for config- or orchestration-driven callers.
|
|
48
|
+
|
|
49
|
+
## Gotchas
|
|
50
|
+
|
|
51
|
+
**`full_load()` deletes.** Full load treats the list you pass as the complete desired
|
|
52
|
+
state: it diffs your input against what exists upstream and deletes everything not present
|
|
53
|
+
(`ids_to_delete = panther_id.difference(source_id)`). `incremental_load()` instead applies
|
|
54
|
+
only the explicit create/update/delete entries you provide and leaves everything else
|
|
55
|
+
alone. Confusing the two is the easiest way to write accidentally destructive code in this
|
|
56
|
+
repo — when in doubt, use incremental.
|
|
57
|
+
|
|
58
|
+
**`attrs` and `pyyaml` are used but not declared.** Both are imported throughout this
|
|
59
|
+
package yet appear in neither its dependencies; they arrive transitively through
|
|
60
|
+
`gooddata-sdk`. Fine today, silently broken if the SDK's dependency set ever changes.
|
|
61
|
+
|
|
62
|
+
**Backups are zip archives with a fixed internal layout.** A backup is written per
|
|
63
|
+
organization and workspace as `gooddata_layouts.zip`, containing the declarative layout
|
|
64
|
+
plus `user_data_filters/`, `filter_views/` and `automations/`. Restore expects exactly that
|
|
65
|
+
shape, so anything that changes the archive layout breaks restore of existing backups.
|
|
66
|
+
|
|
67
|
+
**LDM extension is additive, not a general LDM editor.** `LdmExtensionManager.process()`
|
|
68
|
+
adds custom datasets and fields validated through `CustomDatasetDefinition` /
|
|
69
|
+
`CustomFieldDefinition`, optionally merging into the existing LDM. It can also prune
|
|
70
|
+
previously-managed datasets carrying its `management_tag` when they are absent from the
|
|
71
|
+
current call. It has an optional relations-integrity check that reverts the change if it
|
|
72
|
+
would break more references than it fixes — do not remove that safety net casually.
|
|
73
|
+
|
|
74
|
+
**Backup batching, rate limiting and retry are configurable and load-bearing.**
|
|
75
|
+
`BackupRestoreConfig` carries `batch_size` and `api_calls_per_second`; a failed batch is
|
|
76
|
+
retried with exponential backoff up to `BackupSettings.MAX_RETRIES` before the whole run
|
|
77
|
+
aborts. These are the knobs for backup reliability and speed.
|
|
78
|
+
|
|
79
|
+
**`UserDataFilterProvisioner` is not wired into the generic path.** It is provisioned
|
|
80
|
+
directly rather than through `PROVISIONING_CONFIG` / `WorkflowType`, so do not assume
|
|
81
|
+
feature parity across provisioners.
|
|
82
|
+
|
|
83
|
+
**CSV input is backup-only.** `CSVReader.read_backup_csv` (a single-column list of
|
|
84
|
+
workspace ids) serves `BackupManager` only. Provisioning takes validated pydantic model
|
|
85
|
+
lists that the caller builds from whatever source they like.
|
|
86
|
+
|
|
87
|
+
**Input models forbid unknown keys and want real lists.** `UserFullLoad` and friends set
|
|
88
|
+
`extra="forbid"`, so a stray CSV column raises `ValidationError`, and `user_groups` is a
|
|
89
|
+
`list[str]` — a delimited CSV cell has to be split before validating. The models' only
|
|
90
|
+
alternate constructor is `from_sdk_obj`; there is no `from_list_of_dicts` helper, so the
|
|
91
|
+
caller builds the list. The README shows the working shape.
|
|
92
|
+
|
|
93
|
+
## Testing
|
|
94
|
+
|
|
95
|
+
Package-local pytest suites under `tests/`. Cloud storage targets are exercised with
|
|
96
|
+
`moto` rather than live buckets.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: gooddata-pipelines
|
|
3
|
-
Version: 1.74.1.
|
|
3
|
+
Version: 1.74.1.dev3
|
|
4
4
|
Summary: GoodData Cloud lifecycle automation pipelines
|
|
5
5
|
Author-email: GoodData <support@gooddata.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -10,7 +10,7 @@ Requires-Dist: azure-identity<2.0.0,>=1.15.0
|
|
|
10
10
|
Requires-Dist: azure-storage-blob<13.0.0,>=12.19.0
|
|
11
11
|
Requires-Dist: boto3-stubs<2.0.0,>=1.39.3
|
|
12
12
|
Requires-Dist: boto3<2.0.0,>=1.39.3
|
|
13
|
-
Requires-Dist: gooddata-sdk~=1.74.1.
|
|
13
|
+
Requires-Dist: gooddata-sdk~=1.74.1.dev3
|
|
14
14
|
Requires-Dist: pydantic<3.0.0,>=2.9.2
|
|
15
15
|
Requires-Dist: requests<3.0.0,>=2.32.3
|
|
16
16
|
Requires-Dist: types-pyyaml<7.0.0,>=6.0.12.20250326
|
|
@@ -56,9 +56,10 @@ from pathlib import Path
|
|
|
56
56
|
# Import the Entity Provisioner class and corresponding model from the gooddata_pipelines library
|
|
57
57
|
from gooddata_pipelines import UserFullLoad, UserProvisioner
|
|
58
58
|
|
|
59
|
-
# Create the Provisioner instance - you can also create the instance from a GDC yaml
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
# Create the Provisioner instance - you can also create the instance from a GDC yaml
|
|
60
|
+
# profile with UserProvisioner.create_from_profile(profile="default")
|
|
61
|
+
provisioner = UserProvisioner.create(
|
|
62
|
+
os.environ["GDC_HOSTNAME"], os.environ["GDC_AUTH_TOKEN"]
|
|
62
63
|
)
|
|
63
64
|
|
|
64
65
|
# Optional: set up logging and subscribe to logs emitted by the provisioner
|
|
@@ -71,10 +72,12 @@ source_data_path: Path = Path("path/to/some.csv")
|
|
|
71
72
|
source_data_reader = DictReader(source_data_path.read_text().splitlines())
|
|
72
73
|
source_data = [row for row in source_data_reader]
|
|
73
74
|
|
|
74
|
-
# Validate your input data
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
)
|
|
75
|
+
# Validate your input data. UserFullLoad forbids unknown keys, and user_groups must be a
|
|
76
|
+
# list, so split the delimited CSV column before validating.
|
|
77
|
+
full_load_data: list[UserFullLoad] = [
|
|
78
|
+
UserFullLoad(**{**row, "user_groups": row["user_groups"].split("|")})
|
|
79
|
+
for row in source_data
|
|
80
|
+
]
|
|
78
81
|
|
|
79
82
|
# Run the provisioning
|
|
80
83
|
provisioner.full_load(full_load_data)
|
|
@@ -37,9 +37,10 @@ from pathlib import Path
|
|
|
37
37
|
# Import the Entity Provisioner class and corresponding model from the gooddata_pipelines library
|
|
38
38
|
from gooddata_pipelines import UserFullLoad, UserProvisioner
|
|
39
39
|
|
|
40
|
-
# Create the Provisioner instance - you can also create the instance from a GDC yaml
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
# Create the Provisioner instance - you can also create the instance from a GDC yaml
|
|
41
|
+
# profile with UserProvisioner.create_from_profile(profile="default")
|
|
42
|
+
provisioner = UserProvisioner.create(
|
|
43
|
+
os.environ["GDC_HOSTNAME"], os.environ["GDC_AUTH_TOKEN"]
|
|
43
44
|
)
|
|
44
45
|
|
|
45
46
|
# Optional: set up logging and subscribe to logs emitted by the provisioner
|
|
@@ -52,10 +53,12 @@ source_data_path: Path = Path("path/to/some.csv")
|
|
|
52
53
|
source_data_reader = DictReader(source_data_path.read_text().splitlines())
|
|
53
54
|
source_data = [row for row in source_data_reader]
|
|
54
55
|
|
|
55
|
-
# Validate your input data
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
)
|
|
56
|
+
# Validate your input data. UserFullLoad forbids unknown keys, and user_groups must be a
|
|
57
|
+
# list, so split the delimited CSV column before validating.
|
|
58
|
+
full_load_data: list[UserFullLoad] = [
|
|
59
|
+
UserFullLoad(**{**row, "user_groups": row["user_groups"].split("|")})
|
|
60
|
+
for row in source_data
|
|
61
|
+
]
|
|
59
62
|
|
|
60
63
|
# Run the provisioning
|
|
61
64
|
provisioner.full_load(full_load_data)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# (C) 2025 GoodData Corporation
|
|
2
2
|
[project]
|
|
3
3
|
name = "gooddata-pipelines"
|
|
4
|
-
version = "1.74.1.
|
|
4
|
+
version = "1.74.1.dev3"
|
|
5
5
|
description = "GoodData Cloud lifecycle automation pipelines"
|
|
6
6
|
authors = [{ name = "GoodData", email = "support@gooddata.com" }]
|
|
7
7
|
license = "MIT"
|
|
@@ -11,7 +11,7 @@ dependencies = [
|
|
|
11
11
|
"pydantic (>=2.9.2,<3.0.0)",
|
|
12
12
|
"requests (>=2.32.3,<3.0.0)",
|
|
13
13
|
"types-requests (>1.0.0,<3.0.0)",
|
|
14
|
-
"gooddata-sdk~=1.74.1.
|
|
14
|
+
"gooddata-sdk~=1.74.1.dev3",
|
|
15
15
|
"boto3 (>=1.39.3,<2.0.0)",
|
|
16
16
|
"boto3-stubs (>=1.39.3,<2.0.0)",
|
|
17
17
|
"azure-storage-blob (>=12.19.0,<13.0.0)",
|
|
@@ -19,14 +19,10 @@ dependencies = [
|
|
|
19
19
|
"types-pyyaml (>=6.0.12.20250326,<7.0.0)",
|
|
20
20
|
]
|
|
21
21
|
|
|
22
|
-
[tool.ruff]
|
|
23
|
-
exclude = [".venv"]
|
|
24
|
-
line-length = 80
|
|
25
|
-
|
|
26
22
|
[dependency-groups]
|
|
27
23
|
test = [
|
|
28
|
-
"pytest~=
|
|
29
|
-
"pytest-cov~=
|
|
24
|
+
"pytest~=9.1.1",
|
|
25
|
+
"pytest-cov~=7.1.0",
|
|
30
26
|
"pytest-json-report==1.5.0",
|
|
31
27
|
"pytest-mock>=3.14.0,<4.0.0",
|
|
32
28
|
"moto>=5.1.6,<6.0.0",
|
|
@@ -38,9 +38,7 @@ class ApiMethods:
|
|
|
38
38
|
"""
|
|
39
39
|
return f"{self.base_url}{endpoint}"
|
|
40
40
|
|
|
41
|
-
def get_all_workspace_data_filters(
|
|
42
|
-
self, workspace_id: str
|
|
43
|
-
) -> requests.Response:
|
|
41
|
+
def get_all_workspace_data_filters(self, workspace_id: str) -> requests.Response:
|
|
44
42
|
"""Gets all workspace data filters for a given workspace.
|
|
45
43
|
|
|
46
44
|
Args:
|
|
@@ -52,9 +50,7 @@ class ApiMethods:
|
|
|
52
50
|
url = f"/entities/workspaces/{workspace_id}/workspaceDataFilters"
|
|
53
51
|
return self._get(url)
|
|
54
52
|
|
|
55
|
-
def get_workspace_data_filter_settings(
|
|
56
|
-
self, workspace_id: str
|
|
57
|
-
) -> requests.Response:
|
|
53
|
+
def get_workspace_data_filter_settings(self, workspace_id: str) -> requests.Response:
|
|
58
54
|
"""Gets all workspace data filter settings for a given workspace.
|
|
59
55
|
|
|
60
56
|
Args:
|
|
@@ -66,9 +62,7 @@ class ApiMethods:
|
|
|
66
62
|
url = f"/entities/workspaces/{workspace_id}/workspaceDataFilterSettings?include=workspaceDataFilters"
|
|
67
63
|
return self._get(url)
|
|
68
64
|
|
|
69
|
-
def get_workspace_data_filter_setting(
|
|
70
|
-
self, workspace_id: str, wdf_id: str
|
|
71
|
-
) -> requests.Response:
|
|
65
|
+
def get_workspace_data_filter_setting(self, workspace_id: str, wdf_id: str) -> requests.Response:
|
|
72
66
|
"""Gets a specific workspace data filter setting.
|
|
73
67
|
|
|
74
68
|
Args:
|
|
@@ -119,9 +113,7 @@ class ApiMethods:
|
|
|
119
113
|
requests.Response: The response from the server containing the
|
|
120
114
|
created workspace data filter setting.
|
|
121
115
|
"""
|
|
122
|
-
endpoint =
|
|
123
|
-
f"/entities/workspaces/{workspace_id}/workspaceDataFilterSettings/"
|
|
124
|
-
)
|
|
116
|
+
endpoint = f"/entities/workspaces/{workspace_id}/workspaceDataFilterSettings/"
|
|
125
117
|
return self._post(
|
|
126
118
|
endpoint,
|
|
127
119
|
wdf_setting,
|
|
@@ -153,9 +145,7 @@ class ApiMethods:
|
|
|
153
145
|
endpoint = f"/layout/workspaces/{workspace_id}/userDataFilters"
|
|
154
146
|
return self._get(endpoint)
|
|
155
147
|
|
|
156
|
-
def put_user_data_filters(
|
|
157
|
-
self, workspace_id: str, user_data_filters: dict[str, Any]
|
|
158
|
-
) -> requests.Response:
|
|
148
|
+
def put_user_data_filters(self, workspace_id: str, user_data_filters: dict[str, Any]) -> requests.Response:
|
|
159
149
|
"""Puts the user data filters into GoodData workspace."""
|
|
160
150
|
headers = {**self.headers, "Content-Type": "application/json"}
|
|
161
151
|
return self._put(
|
|
@@ -166,25 +156,17 @@ class ApiMethods:
|
|
|
166
156
|
|
|
167
157
|
def get_automations(self, workspace_id: str) -> requests.Response:
|
|
168
158
|
"""Gets the automations for a given workspace."""
|
|
169
|
-
endpoint =
|
|
170
|
-
f"/entities/workspaces/{workspace_id}/automations?include=ALL"
|
|
171
|
-
)
|
|
159
|
+
endpoint = f"/entities/workspaces/{workspace_id}/automations?include=ALL"
|
|
172
160
|
return self._get(endpoint)
|
|
173
161
|
|
|
174
|
-
def post_automation(
|
|
175
|
-
self, workspace_id: str, automation: dict[str, Any]
|
|
176
|
-
) -> requests.Response:
|
|
162
|
+
def post_automation(self, workspace_id: str, automation: dict[str, Any]) -> requests.Response:
|
|
177
163
|
"""Posts an automation for a given workspace."""
|
|
178
164
|
endpoint = f"/entities/workspaces/{workspace_id}/automations"
|
|
179
165
|
return self._post(endpoint, automation)
|
|
180
166
|
|
|
181
|
-
def delete_automation(
|
|
182
|
-
self, workspace_id: str, automation_id: str
|
|
183
|
-
) -> requests.Response:
|
|
167
|
+
def delete_automation(self, workspace_id: str, automation_id: str) -> requests.Response:
|
|
184
168
|
"""Deletes an automation for a given workspace."""
|
|
185
|
-
endpoint =
|
|
186
|
-
f"/entities/workspaces/{workspace_id}/automations/{automation_id}"
|
|
187
|
-
)
|
|
169
|
+
endpoint = f"/entities/workspaces/{workspace_id}/automations/{automation_id}"
|
|
188
170
|
return self._delete(endpoint)
|
|
189
171
|
|
|
190
172
|
def get_all_metrics(self, workspace_id: str) -> requests.Response:
|
|
@@ -199,9 +181,7 @@ class ApiMethods:
|
|
|
199
181
|
headers = {**self.headers, "X-GDC-VALIDATE-RELATIONS": "true"}
|
|
200
182
|
return self._get(endpoint, headers=headers)
|
|
201
183
|
|
|
202
|
-
def get_all_visualization_objects(
|
|
203
|
-
self, workspace_id: str
|
|
204
|
-
) -> requests.Response:
|
|
184
|
+
def get_all_visualization_objects(self, workspace_id: str) -> requests.Response:
|
|
205
185
|
"""Get all visualizations from the specified workspace.
|
|
206
186
|
|
|
207
187
|
Args:
|
|
@@ -230,9 +210,7 @@ class ApiMethods:
|
|
|
230
210
|
endpoint = "/profile"
|
|
231
211
|
return self._get(endpoint)
|
|
232
212
|
|
|
233
|
-
def _get(
|
|
234
|
-
self, endpoint: str, headers: dict[str, str] | None = None
|
|
235
|
-
) -> requests.Response:
|
|
213
|
+
def _get(self, endpoint: str, headers: dict[str, str] | None = None) -> requests.Response:
|
|
236
214
|
"""Sends a GET request to the server.
|
|
237
215
|
|
|
238
216
|
Args:
|
|
@@ -267,9 +245,7 @@ class ApiMethods:
|
|
|
267
245
|
request_headers = headers if headers else self.headers
|
|
268
246
|
data_json = json.dumps(data)
|
|
269
247
|
|
|
270
|
-
return requests.post(
|
|
271
|
-
url, data=data_json, headers=request_headers, timeout=TIMEOUT
|
|
272
|
-
)
|
|
248
|
+
return requests.post(url, data=data_json, headers=request_headers, timeout=TIMEOUT)
|
|
273
249
|
|
|
274
250
|
def _put(
|
|
275
251
|
self,
|
|
@@ -291,9 +267,7 @@ class ApiMethods:
|
|
|
291
267
|
request_headers = headers if headers else self.headers
|
|
292
268
|
data_json = json.dumps(data)
|
|
293
269
|
|
|
294
|
-
return requests.put(
|
|
295
|
-
url, data=data_json, headers=request_headers, timeout=TIMEOUT
|
|
296
|
-
)
|
|
270
|
+
return requests.put(url, data=data_json, headers=request_headers, timeout=TIMEOUT)
|
|
297
271
|
|
|
298
272
|
def _delete(
|
|
299
273
|
self,
|
|
@@ -47,9 +47,7 @@ class GoodDataApi(ApiMethods):
|
|
|
47
47
|
raise RuntimeError("Host is not set. Please provide a valid host.")
|
|
48
48
|
|
|
49
49
|
if host == "":
|
|
50
|
-
raise ValueError(
|
|
51
|
-
"Host is an empty string. Please provide a valid host."
|
|
52
|
-
)
|
|
50
|
+
raise ValueError("Host is an empty string. Please provide a valid host.")
|
|
53
51
|
|
|
54
52
|
# Remove trailing slash if present.
|
|
55
53
|
if host[-1] == "/":
|
|
@@ -43,7 +43,9 @@ class BackupInputProcessor:
|
|
|
43
43
|
+ "filter=parent.id=={parent_id}"
|
|
44
44
|
+ f"&include=parent&page=0&size={self.page_size}&sort=name,asc&metaInclude=page,hierarchy"
|
|
45
45
|
)
|
|
46
|
-
self.all_workspaces_endpoint =
|
|
46
|
+
self.all_workspaces_endpoint = (
|
|
47
|
+
f"{self.base_workspace_endpoint}?page=0&size={self.page_size}&sort=name,asc&metaInclude=page"
|
|
48
|
+
)
|
|
47
49
|
|
|
48
50
|
@attrs.define
|
|
49
51
|
class _ProcessDataOutput:
|
|
@@ -59,9 +61,7 @@ class BackupInputProcessor:
|
|
|
59
61
|
if response.ok:
|
|
60
62
|
return WorkspaceResponse(**response.json())
|
|
61
63
|
else:
|
|
62
|
-
raise RuntimeError(
|
|
63
|
-
f"Failed to fetch data from the API. URL: {endpoint}"
|
|
64
|
-
)
|
|
64
|
+
raise RuntimeError(f"Failed to fetch data from the API. URL: {endpoint}")
|
|
65
65
|
|
|
66
66
|
@staticmethod
|
|
67
67
|
def process_data(data: list[Workspace]) -> _ProcessDataOutput:
|
|
@@ -74,9 +74,8 @@ class BackupInputProcessor:
|
|
|
74
74
|
children.append(workspace.id)
|
|
75
75
|
|
|
76
76
|
# if hierarchy is present and has children, append child workspace ID to sub_parents
|
|
77
|
-
if workspace.meta and workspace.meta.hierarchy:
|
|
78
|
-
|
|
79
|
-
sub_parents.append(workspace.id)
|
|
77
|
+
if workspace.meta and workspace.meta.hierarchy and workspace.meta.hierarchy.children_count > 0:
|
|
78
|
+
sub_parents.append(workspace.id)
|
|
80
79
|
return BackupInputProcessor._ProcessDataOutput(children, sub_parents)
|
|
81
80
|
|
|
82
81
|
def log_paging_progress(self, response: WorkspaceResponse) -> None:
|
|
@@ -94,9 +93,7 @@ class BackupInputProcessor:
|
|
|
94
93
|
if current_page and total_pages:
|
|
95
94
|
self.logger.info(f"Fetched page: {current_page} of {total_pages}")
|
|
96
95
|
|
|
97
|
-
def _paginate(
|
|
98
|
-
self, url: str | None
|
|
99
|
-
) -> list["BackupInputProcessor._ProcessDataOutput"]:
|
|
96
|
+
def _paginate(self, url: str | None) -> list["BackupInputProcessor._ProcessDataOutput"]:
|
|
100
97
|
"""Paginates through the API responses and returns a list of workspace IDs."""
|
|
101
98
|
result: list[BackupInputProcessor._ProcessDataOutput] = []
|
|
102
99
|
while url:
|
|
@@ -114,9 +111,7 @@ class BackupInputProcessor:
|
|
|
114
111
|
|
|
115
112
|
all_children, sub_parents = [], []
|
|
116
113
|
|
|
117
|
-
results: list[BackupInputProcessor._ProcessDataOutput] = self._paginate(
|
|
118
|
-
url
|
|
119
|
-
)
|
|
114
|
+
results: list[BackupInputProcessor._ProcessDataOutput] = self._paginate(url)
|
|
120
115
|
|
|
121
116
|
for result in results:
|
|
122
117
|
all_children.extend(result.workspace_ids)
|
|
@@ -127,9 +122,7 @@ class BackupInputProcessor:
|
|
|
127
122
|
all_children += self.get_hierarchy(subparent)
|
|
128
123
|
|
|
129
124
|
if not all_children:
|
|
130
|
-
self.logger.warning(
|
|
131
|
-
f"No child workspaces found for parent workspace ID: {parent_id}"
|
|
132
|
-
)
|
|
125
|
+
self.logger.warning(f"No child workspaces found for parent workspace ID: {parent_id}")
|
|
133
126
|
|
|
134
127
|
return all_children
|
|
135
128
|
|
|
@@ -144,9 +137,7 @@ class BackupInputProcessor:
|
|
|
144
137
|
|
|
145
138
|
all_workspaces: list[str] = []
|
|
146
139
|
|
|
147
|
-
results: list[BackupInputProcessor._ProcessDataOutput] = self._paginate(
|
|
148
|
-
url
|
|
149
|
-
)
|
|
140
|
+
results: list[BackupInputProcessor._ProcessDataOutput] = self._paginate(url)
|
|
150
141
|
|
|
151
142
|
for result in results:
|
|
152
143
|
all_workspaces.extend(result.workspace_ids)
|
|
@@ -95,15 +95,12 @@ class BackupManager(BaseManager):
|
|
|
95
95
|
def _get_automations_from_api(self, workspace_id: str) -> Any:
|
|
96
96
|
"""Returns automations for the workspace as JSON."""
|
|
97
97
|
with self._api_rate_limiter:
|
|
98
|
-
response: requests.Response = self._api.get_automations(
|
|
99
|
-
workspace_id
|
|
100
|
-
)
|
|
98
|
+
response: requests.Response = self._api.get_automations(workspace_id)
|
|
101
99
|
if response.ok:
|
|
102
100
|
return response.json()
|
|
103
101
|
else:
|
|
104
102
|
raise RuntimeError(
|
|
105
|
-
f"Failed to get automations for {workspace_id}. "
|
|
106
|
-
+ f"{response.status_code}: {response.text}"
|
|
103
|
+
f"Failed to get automations for {workspace_id}. " + f"{response.status_code}: {response.text}"
|
|
107
104
|
)
|
|
108
105
|
|
|
109
106
|
def _store_automations(self, export_path: Path, workspace_id: str) -> None:
|
|
@@ -120,9 +117,7 @@ class BackupManager(BaseManager):
|
|
|
120
117
|
"automations",
|
|
121
118
|
)
|
|
122
119
|
|
|
123
|
-
automations_file_path: Path = Path(
|
|
124
|
-
automations_folder_path, "automations.json"
|
|
125
|
-
)
|
|
120
|
+
automations_file_path: Path = Path(automations_folder_path, "automations.json")
|
|
126
121
|
|
|
127
122
|
os.mkdir(automations_folder_path)
|
|
128
123
|
|
|
@@ -130,15 +125,11 @@ class BackupManager(BaseManager):
|
|
|
130
125
|
if len(automations["data"]) > 0:
|
|
131
126
|
self.json_utils.dump(automations_file_path, automations)
|
|
132
127
|
|
|
133
|
-
def store_declarative_filter_views(
|
|
134
|
-
self, export_path: Path, workspace_id: str
|
|
135
|
-
) -> None:
|
|
128
|
+
def store_declarative_filter_views(self, export_path: Path, workspace_id: str) -> None:
|
|
136
129
|
"""Stores the filter views in the specified export path."""
|
|
137
130
|
# Get the filter views YAML files from the API
|
|
138
131
|
with self._api_rate_limiter:
|
|
139
|
-
self._api._sdk.catalog_workspace.store_declarative_filter_views(
|
|
140
|
-
workspace_id, export_path
|
|
141
|
-
)
|
|
132
|
+
self._api._sdk.catalog_workspace.store_declarative_filter_views(workspace_id, export_path)
|
|
142
133
|
|
|
143
134
|
# Move filter views to the subfolder containing the analytics model
|
|
144
135
|
self._move_folder(
|
|
@@ -176,8 +167,7 @@ class BackupManager(BaseManager):
|
|
|
176
167
|
user_data_filters = self.get_user_data_filters(workspace_id)
|
|
177
168
|
except Exception as e:
|
|
178
169
|
self.logger.error(
|
|
179
|
-
f"Skipping backup of {workspace_id} - check if workspace exists."
|
|
180
|
-
+ f"{e.__class__.__name__}: {e}"
|
|
170
|
+
f"Skipping backup of {workspace_id} - check if workspace exists." + f"{e.__class__.__name__}: {e}"
|
|
181
171
|
)
|
|
182
172
|
continue
|
|
183
173
|
|
|
@@ -188,33 +178,23 @@ class BackupManager(BaseManager):
|
|
|
188
178
|
# directly instead of reorganizing the folder structures. That should
|
|
189
179
|
# be more transparent/readable and possibly safer for threading
|
|
190
180
|
with self._api_rate_limiter:
|
|
191
|
-
self._api._sdk.catalog_workspace.store_declarative_workspace(
|
|
192
|
-
workspace_id, export_path
|
|
193
|
-
)
|
|
181
|
+
self._api._sdk.catalog_workspace.store_declarative_workspace(workspace_id, export_path)
|
|
194
182
|
self.store_declarative_filter_views(export_path, workspace_id)
|
|
195
183
|
self._store_automations(export_path, workspace_id)
|
|
196
184
|
|
|
197
|
-
self._store_user_data_filters(
|
|
198
|
-
user_data_filters, export_path, workspace_id
|
|
199
|
-
)
|
|
185
|
+
self._store_user_data_filters(user_data_filters, export_path, workspace_id)
|
|
200
186
|
|
|
201
|
-
workspace_duration_ms = int(
|
|
202
|
-
(time.time() - workspace_start_time) * 1000
|
|
203
|
-
)
|
|
187
|
+
workspace_duration_ms = int((time.time() - workspace_start_time) * 1000)
|
|
204
188
|
zip_file_path = f"{self.org_id}/{workspace_id}.zip"
|
|
205
189
|
|
|
206
190
|
self.logger.info(
|
|
207
|
-
f"Completed backup for workspace {workspace_id} "
|
|
208
|
-
f"to {zip_file_path} in {workspace_duration_ms}ms"
|
|
191
|
+
f"Completed backup for workspace {workspace_id} to {zip_file_path} in {workspace_duration_ms}ms"
|
|
209
192
|
)
|
|
210
193
|
exported = True
|
|
211
194
|
except Exception as e:
|
|
212
|
-
workspace_duration_ms = int(
|
|
213
|
-
(time.time() - workspace_start_time) * 1000
|
|
214
|
-
)
|
|
195
|
+
workspace_duration_ms = int((time.time() - workspace_start_time) * 1000)
|
|
215
196
|
self.logger.error(
|
|
216
|
-
f"Skipping {workspace_id} after {workspace_duration_ms}ms. "
|
|
217
|
-
f"{e.__class__.__name__} encountered: {e}"
|
|
197
|
+
f"Skipping {workspace_id} after {workspace_duration_ms}ms. {e.__class__.__name__} encountered: {e}"
|
|
218
198
|
)
|
|
219
199
|
|
|
220
200
|
if not exported:
|
|
@@ -245,9 +225,7 @@ class BackupManager(BaseManager):
|
|
|
245
225
|
raise
|
|
246
226
|
|
|
247
227
|
@staticmethod
|
|
248
|
-
def _split_to_batches(
|
|
249
|
-
workspaces_to_export: list[str], batch_size: int
|
|
250
|
-
) -> list[BackupBatch]:
|
|
228
|
+
def _split_to_batches(workspaces_to_export: list[str], batch_size: int) -> list[BackupBatch]:
|
|
251
229
|
"""Splits the list of workspaces into batches of the specified size.
|
|
252
230
|
The batch is represented as a list of workspace IDs.
|
|
253
231
|
Returns a list of batches (i.e. list of lists of IDs)
|
|
@@ -274,9 +252,7 @@ class BackupManager(BaseManager):
|
|
|
274
252
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
275
253
|
self._get_workspace_export(tmpdir, batch.list_of_ids)
|
|
276
254
|
|
|
277
|
-
self._archive_gooddata_layouts_to_zip(
|
|
278
|
-
str(Path(tmpdir, self.org_id))
|
|
279
|
-
)
|
|
255
|
+
self._archive_gooddata_layouts_to_zip(str(Path(tmpdir, self.org_id)))
|
|
280
256
|
|
|
281
257
|
self.storage.export(tmpdir, self.org_id)
|
|
282
258
|
|
|
@@ -367,13 +343,9 @@ class BackupManager(BaseManager):
|
|
|
367
343
|
path_to_csv,
|
|
368
344
|
workspace_ids,
|
|
369
345
|
)
|
|
370
|
-
batches = self._split_to_batches(
|
|
371
|
-
workspaces_to_export, self.config.batch_size
|
|
372
|
-
)
|
|
346
|
+
batches = self._split_to_batches(workspaces_to_export, self.config.batch_size)
|
|
373
347
|
|
|
374
|
-
self.logger.info(
|
|
375
|
-
f"Exporting {len(workspaces_to_export)} workspaces in {len(batches)} batches."
|
|
376
|
-
)
|
|
348
|
+
self.logger.info(f"Exporting {len(workspaces_to_export)} workspaces in {len(batches)} batches.")
|
|
377
349
|
|
|
378
350
|
self._process_batches(batches)
|
|
379
351
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import abc
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import TypeVar
|
|
6
6
|
|
|
7
7
|
from gooddata_sdk.utils import PROFILES_FILE_PATH, profile_content
|
|
8
8
|
|
|
@@ -11,6 +11,9 @@ from gooddata_pipelines.backup_and_restore.models.storage import (
|
|
|
11
11
|
BackupRestoreConfig,
|
|
12
12
|
StorageType,
|
|
13
13
|
)
|
|
14
|
+
from gooddata_pipelines.backup_and_restore.storage.azure_storage import (
|
|
15
|
+
AzureStorage,
|
|
16
|
+
)
|
|
14
17
|
from gooddata_pipelines.backup_and_restore.storage.base_storage import (
|
|
15
18
|
BackupStorage,
|
|
16
19
|
)
|
|
@@ -18,9 +21,6 @@ from gooddata_pipelines.backup_and_restore.storage.local_storage import (
|
|
|
18
21
|
LocalStorage,
|
|
19
22
|
)
|
|
20
23
|
from gooddata_pipelines.backup_and_restore.storage.s3_storage import S3Storage
|
|
21
|
-
from gooddata_pipelines.backup_and_restore.storage.azure_storage import (
|
|
22
|
-
AzureStorage,
|
|
23
|
-
)
|
|
24
24
|
from gooddata_pipelines.logger import LogObserver
|
|
25
25
|
from gooddata_pipelines.utils.file_utils import JsonUtils, YamlUtils
|
|
26
26
|
|
|
@@ -52,13 +52,11 @@ class BaseManager(abc.ABC):
|
|
|
52
52
|
elif conf.storage_type == StorageType.LOCAL:
|
|
53
53
|
return LocalStorage(conf)
|
|
54
54
|
else:
|
|
55
|
-
raise RuntimeError(
|
|
56
|
-
f'Unsupported storage type "{conf.storage_type.value}".'
|
|
57
|
-
)
|
|
55
|
+
raise RuntimeError(f'Unsupported storage type "{conf.storage_type.value}".')
|
|
58
56
|
|
|
59
57
|
@classmethod
|
|
60
58
|
def create(
|
|
61
|
-
cls:
|
|
59
|
+
cls: type[ManagerT],
|
|
62
60
|
config: BackupRestoreConfig,
|
|
63
61
|
host: str,
|
|
64
62
|
token: str,
|
|
@@ -68,7 +66,7 @@ class BaseManager(abc.ABC):
|
|
|
68
66
|
|
|
69
67
|
@classmethod
|
|
70
68
|
def create_from_profile(
|
|
71
|
-
cls:
|
|
69
|
+
cls: type[ManagerT],
|
|
72
70
|
config: BackupRestoreConfig,
|
|
73
71
|
profile: str = "default",
|
|
74
72
|
profiles_path: Path = PROFILES_FILE_PATH,
|
|
@@ -33,8 +33,4 @@ class BackupSettings:
|
|
|
33
33
|
API = ApiDefaults()
|
|
34
34
|
MAX_RETRIES = 3
|
|
35
35
|
RETRY_DELAY = 5 # seconds
|
|
36
|
-
TIMESTAMP_SDK_FOLDER = (
|
|
37
|
-
str(datetime.datetime.now().strftime("%Y%m%d-%H%M%S"))
|
|
38
|
-
+ "-"
|
|
39
|
-
+ sdk_version.replace(".", "_")
|
|
40
|
-
)
|
|
36
|
+
TIMESTAMP_SDK_FOLDER = str(datetime.datetime.now().strftime("%Y%m%d-%H%M%S")) + "-" + sdk_version.replace(".", "_")
|
|
@@ -14,18 +14,14 @@ class CSVReader:
|
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
with open(file_path) as csv_file:
|
|
17
|
-
reader: Iterator[list[str]] = csv.reader(
|
|
18
|
-
csv_file, skipinitialspace=True
|
|
19
|
-
)
|
|
17
|
+
reader: Iterator[list[str]] = csv.reader(csv_file, skipinitialspace=True)
|
|
20
18
|
|
|
21
19
|
try:
|
|
22
20
|
# Skip the header
|
|
23
21
|
headers = next(reader)
|
|
24
22
|
|
|
25
23
|
if len(headers) > 1:
|
|
26
|
-
raise ValueError(
|
|
27
|
-
"Input file contains more than one column. Please check the input and try again."
|
|
28
|
-
)
|
|
24
|
+
raise ValueError("Input file contains more than one column. Please check the input and try again.")
|
|
29
25
|
|
|
30
26
|
except StopIteration:
|
|
31
27
|
# Raise an error if the iterator is empty
|