gooddata-pipelines 1.50.0__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 (122) hide show
  1. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/PKG-INFO +2 -2
  2. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/backup_manager.py +36 -62
  3. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/constants.py +3 -7
  4. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/models/storage.py +4 -5
  5. gooddata_pipelines-1.50.1.dev1/gooddata_pipelines/utils/__init__.py +9 -0
  6. gooddata_pipelines-1.50.1.dev1/gooddata_pipelines/utils/rate_limiter.py +64 -0
  7. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/pyproject.toml +2 -2
  8. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/backup_and_restore/test_backup.py +0 -4
  9. gooddata_pipelines-1.50.1.dev1/tests/utils/test_rate_limiter.py +176 -0
  10. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/.gitignore +0 -0
  11. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/LICENSE.txt +0 -0
  12. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/Makefile +0 -0
  13. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/README.md +0 -0
  14. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/TODO.md +0 -0
  15. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/__init__.py +0 -0
  16. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/_version.py +0 -0
  17. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/__init__.py +0 -0
  18. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/exceptions.py +0 -0
  19. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/gooddata_api.py +0 -0
  20. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/gooddata_api_wrapper.py +0 -0
  21. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/gooddata_sdk.py +0 -0
  22. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/utils.py +0 -0
  23. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/__init__.py +0 -0
  24. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/backup_input_processor.py +0 -0
  25. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/csv_reader.py +0 -0
  26. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/models/__init__.py +0 -0
  27. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/models/input_type.py +0 -0
  28. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/models/workspace_response.py +0 -0
  29. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/storage/__init__.py +0 -0
  30. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/storage/base_storage.py +0 -0
  31. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/storage/local_storage.py +0 -0
  32. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/storage/s3_storage.py +0 -0
  33. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/logger/__init__.py +0 -0
  34. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/logger/logger.py +0 -0
  35. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/__init__.py +0 -0
  36. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/assets/wdf_setting.json +0 -0
  37. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/__init__.py +0 -0
  38. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/user_data_filters/__init__.py +0 -0
  39. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/user_data_filters/models/__init__.py +0 -0
  40. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/user_data_filters/models/udf_models.py +0 -0
  41. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/user_data_filters/user_data_filters.py +0 -0
  42. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/__init__.py +0 -0
  43. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/models/__init__.py +0 -0
  44. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/models/permissions.py +0 -0
  45. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/models/user_groups.py +0 -0
  46. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/models/users.py +0 -0
  47. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/permissions.py +0 -0
  48. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/user_groups.py +0 -0
  49. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/users.py +0 -0
  50. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/workspaces/__init__.py +0 -0
  51. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/workspaces/models.py +0 -0
  52. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/workspaces/workspace.py +0 -0
  53. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/workspaces/workspace_data_filters.py +0 -0
  54. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/workspaces/workspace_data_parser.py +0 -0
  55. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/workspaces/workspace_data_validator.py +0 -0
  56. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/provisioning.py +0 -0
  57. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/utils/__init__.py +0 -0
  58. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/utils/context_objects.py +0 -0
  59. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/utils/exceptions.py +0 -0
  60. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/utils/utils.py +0 -0
  61. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/py.typed +0 -0
  62. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/__init__.py +0 -0
  63. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/backup_and_restore/__init__.py +0 -0
  64. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/backup_and_restore/test_backup_input_processor.py +0 -0
  65. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/conftest.py +0 -0
  66. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/__init__.py +0 -0
  67. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/__init__.py +0 -0
  68. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_conf.yaml +0 -0
  69. {gooddata_pipelines-1.50.0 → 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
  70. {gooddata_pipelines-1.50.0 → 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
  71. {gooddata_pipelines-1.50.0 → 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
  72. {gooddata_pipelines-1.50.0 → 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
  73. {gooddata_pipelines-1.50.0 → 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
  74. {gooddata_pipelines-1.50.0 → 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
  75. {gooddata_pipelines-1.50.0 → 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
  76. {gooddata_pipelines-1.50.0 → 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
  77. {gooddata_pipelines-1.50.0 → 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
  78. {gooddata_pipelines-1.50.0 → 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
  79. {gooddata_pipelines-1.50.0 → 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
  80. {gooddata_pipelines-1.50.0 → 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
  81. {gooddata_pipelines-1.50.0 → 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
  82. {gooddata_pipelines-1.50.0 → 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
  83. {gooddata_pipelines-1.50.0 → 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
  84. {gooddata_pipelines-1.50.0 → 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
  85. {gooddata_pipelines-1.50.0 → 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
  86. {gooddata_pipelines-1.50.0 → 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
  87. {gooddata_pipelines-1.50.0 → 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
  88. {gooddata_pipelines-1.50.0 → 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
  89. {gooddata_pipelines-1.50.0 → 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
  90. {gooddata_pipelines-1.50.0 → 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
  91. {gooddata_pipelines-1.50.0 → 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
  92. {gooddata_pipelines-1.50.0 → 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
  93. {gooddata_pipelines-1.50.0 → 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
  94. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_local_conf.yaml +0 -0
  95. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/mock_responses.py +0 -0
  96. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/permissions/existing_upstream_permissions.json +0 -0
  97. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/permissions/permissions_expected_full_load.json +0 -0
  98. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/permissions/permissions_expected_incremental_load.json +0 -0
  99. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/permissions/permissions_input_full_load.json +0 -0
  100. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/permissions/permissions_input_incremental_load.json +0 -0
  101. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/users/existing_upstream_users.json +0 -0
  102. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/users/users_expected_full_load.json +0 -0
  103. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/users/users_expected_incremental_load.json +0 -0
  104. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/users/users_input_full_load.json +0 -0
  105. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/users/users_input_incremental_load.json +0 -0
  106. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/panther/__init__.py +0 -0
  107. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/panther/test_api_wrapper.py +0 -0
  108. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/panther/test_sdk_wrapper.py +0 -0
  109. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/__init__.py +0 -0
  110. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/__init__.py +0 -0
  111. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/users/__init__.py +0 -0
  112. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/users/test_permissions.py +0 -0
  113. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/users/test_user_groups.py +0 -0
  114. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/users/test_users.py +0 -0
  115. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/workspaces/__init__.py +0 -0
  116. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/workspaces/test_provisioning.py +0 -0
  117. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/workspaces/test_workspace.py +0 -0
  118. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/workspaces/test_workspace_data_filters.py +0 -0
  119. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/workspaces/test_workspace_data_parser.py +0 -0
  120. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/workspaces/test_workspace_data_validator.py +0 -0
  121. {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/test_provisioning.py +0 -0
  122. {gooddata_pipelines-1.50.0 → 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.50.0
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.50.0
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
@@ -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,52 +331,23 @@ 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
353
  self,
@@ -440,7 +414,7 @@ class BackupManager:
440
414
  f"Exporting {len(workspaces_to_export)} workspaces in {len(batches)} batches."
441
415
  )
442
416
 
443
- self._process_batches_in_parallel(batches)
417
+ self._process_batches(batches)
444
418
 
445
419
  self.logger.info("Backup completed")
446
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":
@@ -0,0 +1,9 @@
1
+ # (C) 2025 GoodData Corporation
2
+
3
+ """
4
+ Utility modules for gooddata-pipelines package.
5
+ """
6
+
7
+ from .rate_limiter import RateLimiter
8
+
9
+ __all__ = ["RateLimiter"]
@@ -0,0 +1,64 @@
1
+ # (C) 2025 GoodData Corporation
2
+
3
+ import time
4
+ import threading
5
+ import functools
6
+ from typing import Callable, Any, Literal
7
+
8
+
9
+ class RateLimiter:
10
+ """
11
+ Rate limiter usable as a decorator and as a context manager.
12
+ - Shared instance decorator: limiter = RateLimiter(); @limiter
13
+ - Per-function decorator: @RateLimiter(calls_per_second=2)
14
+ - Context manager: with RateLimiter(2): ...
15
+ """
16
+
17
+ def __init__(self, calls_per_second: float = 1.0) -> None:
18
+ if calls_per_second <= 0:
19
+ raise ValueError("calls_per_second must be greater than 0")
20
+
21
+ self.calls_per_second = calls_per_second
22
+ self.min_interval = 1.0 / calls_per_second
23
+
24
+ self._lock = threading.Lock()
25
+ self._last_call_time = 0.0
26
+
27
+ def wait_if_needed(self) -> float:
28
+ """Sleep if needed to maintain the rate limit, return actual sleep time."""
29
+ with self._lock:
30
+ now = time.monotonic()
31
+ since_last = now - self._last_call_time
32
+
33
+ if since_last < self.min_interval:
34
+ sleep_time = self.min_interval - since_last
35
+ time.sleep(sleep_time)
36
+ self._last_call_time = time.monotonic()
37
+ return sleep_time
38
+ else:
39
+ self._last_call_time = now
40
+ return 0.0
41
+
42
+ # Decorator support
43
+ def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
44
+ @functools.wraps(func)
45
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
46
+ self.wait_if_needed()
47
+ return func(*args, **kwargs)
48
+
49
+ return wrapper
50
+
51
+ # Context manager support
52
+ def __enter__(self) -> "RateLimiter":
53
+ self.wait_if_needed()
54
+ return self
55
+
56
+ def __exit__(
57
+ self, exc_type: Any, exc_val: Any, exc_tb: Any
58
+ ) -> Literal[False]:
59
+ return False
60
+
61
+ def reset(self) -> None:
62
+ """Reset the limiter (useful in tests)."""
63
+ with self._lock:
64
+ self._last_call_time = 0.0
@@ -1,7 +1,7 @@
1
1
  # (C) 2025 GoodData Corporation
2
2
  [project]
3
3
  name = "gooddata-pipelines"
4
- version = "1.50.0"
4
+ version = "1.50.1.dev1"
5
5
  description = "GoodData Cloud lifecycle automation pipelines"
6
6
  authors = [{ name = "GoodData", email = "support@gooddata.com" }]
7
7
  license = { text = "MIT" }
@@ -11,7 +11,7 @@ dependencies = [
11
11
  "pydantic (>=2.11.3,<3.0.0)",
12
12
  "requests (>=2.32.3,<3.0.0)",
13
13
  "types-requests (>=2.32.0,<3.0.0)",
14
- "gooddata-sdk~=1.50.0",
14
+ "gooddata-sdk~=1.50.1.dev1",
15
15
  "boto3 (>=1.39.3,<2.0.0)",
16
16
  "boto3-stubs (>=1.39.3,<2.0.0)",
17
17
  "types-pyyaml (>=6.0.12.20250326,<7.0.0)"
@@ -3,7 +3,6 @@
3
3
  import os
4
4
  import shutil
5
5
  import tempfile
6
- import threading
7
6
  from pathlib import Path
8
7
  from unittest import mock
9
8
 
@@ -325,7 +324,6 @@ def test_process_batch_success(
325
324
 
326
325
  backup_manager._process_batch(
327
326
  batch=batch,
328
- stop_event=threading.Event(),
329
327
  retry_count=0,
330
328
  )
331
329
 
@@ -362,7 +360,6 @@ def test_process_batch_retries_on_exception(
362
360
 
363
361
  backup_manager._process_batch(
364
362
  batch=batch,
365
- stop_event=threading.Event(),
366
363
  )
367
364
 
368
365
  assert get_workspace_export_mock.call_count == 2
@@ -392,7 +389,6 @@ def test_process_batch_raises_after_max_retries(
392
389
  with pytest.raises(Exception) as exc_info:
393
390
  backup_manager._process_batch(
394
391
  batch=batch,
395
- stop_event=threading.Event(),
396
392
  retry_count=BackupSettings.MAX_RETRIES,
397
393
  )
398
394
  assert str(exc_info.value) == "fail"
@@ -0,0 +1,176 @@
1
+ # (C) 2025 GoodData Corporation
2
+
3
+ import time
4
+ import pytest
5
+ from gooddata_pipelines.utils.rate_limiter import RateLimiter
6
+
7
+
8
+ # ---------------------------
9
+ # Core wait + reset behavior
10
+ # ---------------------------
11
+
12
+
13
+ def test_rate_limiter_no_wait_needed():
14
+ limiter = RateLimiter(calls_per_second=1000.0) # Very fast limit
15
+ waited = limiter.wait_if_needed()
16
+ assert waited == pytest.approx(0.0, abs=0.001)
17
+
18
+
19
+ def test_rate_limiter_enforces_delay():
20
+ limiter = RateLimiter(calls_per_second=2.0)
21
+ limiter.wait_if_needed()
22
+ start = time.time()
23
+ waited = limiter.wait_if_needed()
24
+ duration = time.time() - start
25
+
26
+ assert waited >= 0.49
27
+ assert duration < 0.65
28
+
29
+
30
+ def test_rate_limiter_respects_reset():
31
+ limiter = RateLimiter(calls_per_second=1.0)
32
+ limiter.wait_if_needed()
33
+ limiter.reset()
34
+ waited = limiter.wait_if_needed()
35
+ assert waited == pytest.approx(0.0, abs=0.001)
36
+
37
+
38
+ def test_rate_limiter_min_interval_property():
39
+ limiter = RateLimiter(calls_per_second=4.0)
40
+ assert limiter.min_interval == pytest.approx(0.25, abs=1e-9)
41
+
42
+
43
+ # -----------------------------------------
44
+ # Decorator: shared instance (@limiter)
45
+ # -----------------------------------------
46
+
47
+
48
+ def test_rate_limiter_as_decorator_enforces_delay_shared_instance():
49
+ limiter = RateLimiter(calls_per_second=2.0)
50
+ ts = []
51
+
52
+ @limiter
53
+ def func():
54
+ ts.append(time.time())
55
+
56
+ func()
57
+ func()
58
+
59
+ assert len(ts) == 2
60
+ assert ts[1] - ts[0] >= 0.49
61
+
62
+
63
+ def test_rate_limiter_decorator_shared_state_across_functions():
64
+ limiter = RateLimiter(calls_per_second=2.0)
65
+ ts = []
66
+
67
+ @limiter
68
+ def func_a():
69
+ ts.append(time.time())
70
+
71
+ @limiter
72
+ def func_b():
73
+ ts.append(time.time())
74
+
75
+ func_a()
76
+ func_b() # should be throttled by the *same* limiter
77
+ assert len(ts) == 2
78
+ assert ts[1] - ts[0] >= 0.49
79
+
80
+
81
+ def test_multiple_limiters_independent_state_shared_instance_mode():
82
+ limiter_a = RateLimiter(calls_per_second=2.0)
83
+ limiter_b = RateLimiter(calls_per_second=2.0)
84
+
85
+ ts_a = []
86
+ ts_b = []
87
+
88
+ @limiter_a
89
+ def func_a():
90
+ ts_a.append(time.time())
91
+
92
+ @limiter_b
93
+ def func_b():
94
+ ts_b.append(time.time())
95
+
96
+ func_a()
97
+ func_b()
98
+
99
+ # They should be ~simultaneous since they use different instances
100
+ assert abs(ts_a[0] - ts_b[0]) < 0.05
101
+
102
+
103
+ # -------------------------------------------------------
104
+ # Decorator: per-function instance (@RateLimiter(...))
105
+ # -------------------------------------------------------
106
+
107
+
108
+ def test_per_function_decorator_enforces_delay_per_function():
109
+ # Each function decorated this way gets its *own* limiter instance.
110
+ ts = []
111
+
112
+ @RateLimiter(calls_per_second=2.0) # 0.5s
113
+ def func():
114
+ ts.append(time.time())
115
+
116
+ func()
117
+ func()
118
+ assert len(ts) == 2
119
+ assert ts[1] - ts[0] >= 0.49
120
+
121
+
122
+ def test_per_function_decorator_independent_state_between_functions():
123
+ ts_a = []
124
+ ts_b = []
125
+
126
+ @RateLimiter(calls_per_second=2.0)
127
+ def func_a():
128
+ ts_a.append(time.time())
129
+
130
+ @RateLimiter(calls_per_second=2.0)
131
+ def func_b():
132
+ ts_b.append(time.time())
133
+
134
+ func_a()
135
+ func_b() # independent limiter, so no enforced delay between A and B
136
+ assert abs(ts_a[0] - ts_b[0]) < 0.05
137
+
138
+
139
+ # -----------------------------
140
+ # Context manager usage
141
+ # -----------------------------
142
+
143
+
144
+ def test_context_manager_waits_on_enter():
145
+ limiter = RateLimiter(calls_per_second=2.0)
146
+ with limiter:
147
+ t1 = time.time()
148
+ with limiter:
149
+ t2 = time.time()
150
+
151
+ # The second 'with' should be at least 0.5s after the first 'with' enter
152
+ assert t2 - t1 >= 0.49
153
+
154
+
155
+ def test_context_manager_multiple_uses_same_instance():
156
+ limiter = RateLimiter(calls_per_second=3.0)
157
+ times = []
158
+
159
+ for _ in range(3):
160
+ with limiter:
161
+ times.append(time.time())
162
+
163
+ intervals = [b - a for a, b in zip(times, times[1:])]
164
+ for iv in intervals:
165
+ assert iv >= 0.30 # a bit of slack for timing variance
166
+
167
+
168
+ def test_context_manager_propagates_exceptions():
169
+ limiter = RateLimiter(calls_per_second=10.0)
170
+
171
+ class Boom(Exception):
172
+ pass
173
+
174
+ with pytest.raises(Boom):
175
+ with limiter:
176
+ raise Boom("fail")