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.

Files changed (126) hide show
  1. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/PKG-INFO +2 -2
  2. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/TODO.md +1 -1
  3. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/__init__.py +7 -1
  4. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/gooddata_api.py +0 -54
  5. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/backup_manager.py +42 -64
  6. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/constants.py +3 -7
  7. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/models/storage.py +4 -5
  8. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/models/permissions.py +23 -79
  9. gooddata_pipelines-1.50.1.dev1/gooddata_pipelines/provisioning/entities/users/models/user_groups.py +37 -0
  10. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/models/users.py +9 -49
  11. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/permissions.py +14 -6
  12. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/user_groups.py +7 -1
  13. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/users.py +3 -0
  14. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/workspaces/models.py +16 -15
  15. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/workspaces/workspace.py +52 -5
  16. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/workspaces/workspace_data_parser.py +9 -6
  17. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/provisioning.py +24 -6
  18. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/utils/context_objects.py +6 -6
  19. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/utils/utils.py +3 -15
  20. gooddata_pipelines-1.50.1.dev1/gooddata_pipelines/utils/__init__.py +9 -0
  21. gooddata_pipelines-1.50.1.dev1/gooddata_pipelines/utils/rate_limiter.py +64 -0
  22. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/pyproject.toml +2 -2
  23. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/backup_and_restore/test_backup.py +0 -4
  24. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/conftest.py +59 -1
  25. gooddata_pipelines-1.50.1.dev1/tests/data/provisioning/entities/permissions/permissions_input_full_load.json +26 -0
  26. gooddata_pipelines-1.50.1.dev1/tests/data/provisioning/entities/permissions/permissions_input_incremental_load.json +37 -0
  27. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/users/users_input_full_load.json +3 -3
  28. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/users/users_input_incremental_load.json +4 -4
  29. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/users/test_permissions.py +82 -112
  30. gooddata_pipelines-1.50.1.dev1/tests/provisioning/entities/users/test_user_groups.py +74 -0
  31. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/users/test_users.py +4 -4
  32. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/workspaces/test_provisioning.py +17 -10
  33. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/workspaces/test_workspace.py +18 -16
  34. gooddata_pipelines-1.50.1.dev1/tests/provisioning/test_provisioning.py +104 -0
  35. gooddata_pipelines-1.50.1.dev1/tests/utils/test_rate_limiter.py +176 -0
  36. gooddata_pipelines-1.49.1.dev2/gooddata_pipelines/provisioning/entities/users/models/user_groups.py +0 -64
  37. gooddata_pipelines-1.49.1.dev2/tests/data/provisioning/entities/permissions/permissions_input_full_load.json +0 -22
  38. gooddata_pipelines-1.49.1.dev2/tests/data/provisioning/entities/permissions/permissions_input_incremental_load.json +0 -32
  39. gooddata_pipelines-1.49.1.dev2/tests/provisioning/entities/users/test_user_groups.py +0 -119
  40. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/.gitignore +0 -0
  41. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/LICENSE.txt +0 -0
  42. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/Makefile +0 -0
  43. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/README.md +0 -0
  44. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/_version.py +0 -0
  45. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/__init__.py +0 -0
  46. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/exceptions.py +0 -0
  47. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/gooddata_api_wrapper.py +0 -0
  48. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/gooddata_sdk.py +0 -0
  49. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/utils.py +0 -0
  50. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/__init__.py +0 -0
  51. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/backup_input_processor.py +0 -0
  52. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/csv_reader.py +0 -0
  53. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/models/__init__.py +0 -0
  54. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/models/input_type.py +0 -0
  55. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/models/workspace_response.py +0 -0
  56. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/storage/__init__.py +0 -0
  57. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/storage/base_storage.py +0 -0
  58. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/storage/local_storage.py +0 -0
  59. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/storage/s3_storage.py +0 -0
  60. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/logger/__init__.py +0 -0
  61. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/logger/logger.py +0 -0
  62. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/__init__.py +0 -0
  63. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/assets/wdf_setting.json +0 -0
  64. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/__init__.py +0 -0
  65. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/user_data_filters/__init__.py +0 -0
  66. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/user_data_filters/models/__init__.py +0 -0
  67. {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
  68. {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
  69. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/__init__.py +0 -0
  70. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/models/__init__.py +0 -0
  71. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/workspaces/__init__.py +0 -0
  72. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/workspaces/workspace_data_filters.py +0 -0
  73. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/workspaces/workspace_data_validator.py +0 -0
  74. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/utils/__init__.py +0 -0
  75. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/utils/exceptions.py +0 -0
  76. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/py.typed +0 -0
  77. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/__init__.py +0 -0
  78. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/backup_and_restore/__init__.py +0 -0
  79. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/backup_and_restore/test_backup_input_processor.py +0 -0
  80. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/__init__.py +0 -0
  81. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/__init__.py +0 -0
  82. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_conf.yaml +0 -0
  83. {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
  84. {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
  85. {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
  86. {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
  87. {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
  88. {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
  89. {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
  90. {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
  91. {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
  92. {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
  93. {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
  94. {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
  95. {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
  96. {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
  97. {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
  98. {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
  99. {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
  100. {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
  101. {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
  102. {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
  103. {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
  104. {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
  105. {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
  106. {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
  107. {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
  108. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_local_conf.yaml +0 -0
  109. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/mock_responses.py +0 -0
  110. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/permissions/existing_upstream_permissions.json +0 -0
  111. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/permissions/permissions_expected_full_load.json +0 -0
  112. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/permissions/permissions_expected_incremental_load.json +0 -0
  113. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/users/existing_upstream_users.json +0 -0
  114. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/users/users_expected_full_load.json +0 -0
  115. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/users/users_expected_incremental_load.json +0 -0
  116. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/panther/__init__.py +0 -0
  117. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/panther/test_api_wrapper.py +0 -0
  118. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/panther/test_sdk_wrapper.py +0 -0
  119. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/__init__.py +0 -0
  120. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/__init__.py +0 -0
  121. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/users/__init__.py +0 -0
  122. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/workspaces/__init__.py +0 -0
  123. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/workspaces/test_workspace_data_filters.py +0 -0
  124. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/workspaces/test_workspace_data_parser.py +0 -0
  125. {gooddata_pipelines-1.49.1.dev2 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/workspaces/test_workspace_data_validator.py +0 -0
  126. {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.49.1.dev2
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.49.1.dev2
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
 
@@ -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 WorkspaceFullLoad
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
- response: requests.Response = self._api.get_user_data_filters(ws_id)
99
- if response.ok:
100
- return response.json()
101
- else:
102
- raise RuntimeError(f"{response.status_code}: {response.text}")
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
- response: requests.Response = self._api.get_automations(workspace_id)
148
- if response.ok:
149
- return response.json()
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._api.store_declarative_filter_views(workspace_id, export_path)
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._api.store_declarative_workspace(workspace_id, export_path)
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 stop_event.is_set():
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, stop_event, next_retry)
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 _process_batches_in_parallel(
340
+ def _process_batches(
338
341
  self,
339
342
  batches: list[BackupBatch],
340
343
  ) -> None:
341
344
  """
342
- Processes batches in parallel using concurrent.futures. Will stop the processing
343
- if any one of the batches fails.
345
+ Processes batches sequentially to avoid overloading the API.
346
+ If any batch fails, the processing will stop.
344
347
  """
345
-
346
- # Create a threading flag to control the threads that have already been started
347
- stop_event = threading.Event()
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, path_to_csv: str | None, workspace_ids: list[str] | None
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, path_to_csv: str | None, workspace_ids: list[str] | None
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._process_batches_in_parallel(batches)
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(ConcurrencyDefaults, ApiDefaults):
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
- max_workers: Annotated[
87
- int,
86
+ api_calls_per_second: Annotated[
87
+ float,
88
88
  Field(
89
89
  gt=0,
90
- lt=3,
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.MAX_WORKERS)
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
- from abc import abstractmethod
2
+
3
3
  from enum import Enum
4
- from typing import Any, Iterator, TypeAlias, TypeVar
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 PermissionType(str, Enum):
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 ConstructorMixin:
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
- id_: str
64
- type_: PermissionType
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
- @classmethod
87
- def from_dict(cls, data: dict[str, Any]) -> "PermissionFullLoad":
88
- """Returns an instance of PermissionFullLoad from a dictionary."""
89
- id_, target_type = cls._get_id_and_type(data)
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 == PermissionType.user.value:
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=PermissionType.user.value
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=PermissionType.user_group.value
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.type_ == PermissionType.user
147
+ if permission.entity_type == EntityType.user
204
148
  else self.user_groups
205
149
  )
206
150
 
207
- if permission.id_ not in target_dict:
208
- target_dict[permission.id_] = {}
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.id_]
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.type_ == PermissionType.user
180
+ if permission.entity_type == EntityType.user
237
181
  else self.user_groups
238
182
  )
239
183
 
240
- if permission.id_ not in target_dict:
241
- target_dict[permission.id_] = {}
184
+ if permission.entity_id not in target_dict:
185
+ target_dict[permission.entity_id] = {}
242
186
 
243
- target_permissions = target_dict[permission.id_]
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:
@@ -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