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.
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/PKG-INFO +2 -2
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/backup_manager.py +36 -62
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/constants.py +3 -7
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/models/storage.py +4 -5
- gooddata_pipelines-1.50.1.dev1/gooddata_pipelines/utils/__init__.py +9 -0
- gooddata_pipelines-1.50.1.dev1/gooddata_pipelines/utils/rate_limiter.py +64 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/pyproject.toml +2 -2
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/backup_and_restore/test_backup.py +0 -4
- gooddata_pipelines-1.50.1.dev1/tests/utils/test_rate_limiter.py +176 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/.gitignore +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/LICENSE.txt +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/Makefile +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/README.md +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/TODO.md +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/__init__.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/_version.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/__init__.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/exceptions.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/gooddata_api.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/gooddata_api_wrapper.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/gooddata_sdk.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/utils.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/__init__.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/backup_input_processor.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/csv_reader.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/models/__init__.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/models/input_type.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/models/workspace_response.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/storage/__init__.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/storage/base_storage.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/storage/local_storage.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/backup_and_restore/storage/s3_storage.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/logger/__init__.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/logger/logger.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/__init__.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/assets/wdf_setting.json +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/__init__.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/user_data_filters/__init__.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/user_data_filters/models/__init__.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/user_data_filters/models/udf_models.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/user_data_filters/user_data_filters.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/__init__.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/models/__init__.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/models/permissions.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/models/user_groups.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/models/users.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/permissions.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/user_groups.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/users/users.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/workspaces/__init__.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/workspaces/models.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/workspaces/workspace.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/workspaces/workspace_data_filters.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/workspaces/workspace_data_parser.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/entities/workspaces/workspace_data_validator.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/provisioning.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/utils/__init__.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/utils/context_objects.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/utils/exceptions.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/provisioning/utils/utils.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/py.typed +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/__init__.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/backup_and_restore/__init__.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/backup_and_restore/test_backup_input_processor.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/conftest.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/__init__.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/__init__.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_conf.yaml +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_local_conf.yaml +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/mock_responses.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/permissions/existing_upstream_permissions.json +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/permissions/permissions_expected_full_load.json +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/permissions/permissions_expected_incremental_load.json +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/permissions/permissions_input_full_load.json +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/permissions/permissions_input_incremental_load.json +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/users/existing_upstream_users.json +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/users/users_expected_full_load.json +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/users/users_expected_incremental_load.json +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/users/users_input_full_load.json +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/provisioning/entities/users/users_input_incremental_load.json +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/panther/__init__.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/panther/test_api_wrapper.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/panther/test_sdk_wrapper.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/__init__.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/__init__.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/users/__init__.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/users/test_permissions.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/users/test_user_groups.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/users/test_users.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/workspaces/__init__.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/workspaces/test_provisioning.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/workspaces/test_workspace.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/workspaces/test_workspace_data_filters.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/workspaces/test_workspace_data_parser.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/workspaces/test_workspace_data_validator.py +0 -0
- {gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/test_provisioning.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
with self._api_rate_limiter:
|
|
102
|
+
response: requests.Response = self._api.get_user_data_filters(ws_id)
|
|
103
|
+
if response.ok:
|
|
104
|
+
return response.json()
|
|
105
|
+
else:
|
|
106
|
+
raise RuntimeError(f"{response.status_code}: {response.text}")
|
|
103
107
|
|
|
104
108
|
def _store_user_data_filters(
|
|
105
109
|
self,
|
|
@@ -144,14 +148,17 @@ class BackupManager:
|
|
|
144
148
|
|
|
145
149
|
def _get_automations_from_api(self, workspace_id: str) -> Any:
|
|
146
150
|
"""Returns automations for the workspace as JSON."""
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
else:
|
|
151
|
-
raise RuntimeError(
|
|
152
|
-
f"Failed to get automations for {workspace_id}. "
|
|
153
|
-
+ f"{response.status_code}: {response.text}"
|
|
151
|
+
with self._api_rate_limiter:
|
|
152
|
+
response: requests.Response = self._api.get_automations(
|
|
153
|
+
workspace_id
|
|
154
154
|
)
|
|
155
|
+
if response.ok:
|
|
156
|
+
return response.json()
|
|
157
|
+
else:
|
|
158
|
+
raise RuntimeError(
|
|
159
|
+
f"Failed to get automations for {workspace_id}. "
|
|
160
|
+
+ f"{response.status_code}: {response.text}"
|
|
161
|
+
)
|
|
155
162
|
|
|
156
163
|
def _store_automations(self, export_path: Path, workspace_id: str) -> None:
|
|
157
164
|
"""Stores the automations in the specified export path."""
|
|
@@ -183,7 +190,8 @@ class BackupManager:
|
|
|
183
190
|
) -> None:
|
|
184
191
|
"""Stores the filter views in the specified export path."""
|
|
185
192
|
# Get the filter views YAML files from the API
|
|
186
|
-
self.
|
|
193
|
+
with self._api_rate_limiter:
|
|
194
|
+
self._api.store_declarative_filter_views(workspace_id, export_path)
|
|
187
195
|
|
|
188
196
|
# Move filter views to the subfolder containing the analytics model
|
|
189
197
|
self._move_folder(
|
|
@@ -231,7 +239,10 @@ class BackupManager:
|
|
|
231
239
|
# the SDK. That way we could save and package all the declarations
|
|
232
240
|
# directly instead of reorganizing the folder structures. That should
|
|
233
241
|
# be more transparent/readable and possibly safer for threading
|
|
234
|
-
self.
|
|
242
|
+
with self._api_rate_limiter:
|
|
243
|
+
self._api.store_declarative_workspace(
|
|
244
|
+
workspace_id, export_path
|
|
245
|
+
)
|
|
235
246
|
self.store_declarative_filter_views(export_path, workspace_id)
|
|
236
247
|
self._store_automations(export_path, workspace_id)
|
|
237
248
|
|
|
@@ -291,7 +302,6 @@ class BackupManager:
|
|
|
291
302
|
def _process_batch(
|
|
292
303
|
self,
|
|
293
304
|
batch: BackupBatch,
|
|
294
|
-
stop_event: threading.Event,
|
|
295
305
|
retry_count: int = 0,
|
|
296
306
|
) -> None:
|
|
297
307
|
"""Processes a single batch of workspaces for backup.
|
|
@@ -299,10 +309,6 @@ class BackupManager:
|
|
|
299
309
|
and retry with exponential backoff up to BackupSettings.MAX_RETRIES.
|
|
300
310
|
The base wait time is defined by BackupSettings.RETRY_DELAY.
|
|
301
311
|
"""
|
|
302
|
-
if stop_event.is_set():
|
|
303
|
-
# If the stop_event flag is set, return. This will terminate the thread
|
|
304
|
-
return
|
|
305
|
-
|
|
306
312
|
try:
|
|
307
313
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
308
314
|
self._get_workspace_export(tmpdir, batch.list_of_ids)
|
|
@@ -314,10 +320,7 @@ class BackupManager:
|
|
|
314
320
|
self.storage.export(tmpdir, self.org_id)
|
|
315
321
|
|
|
316
322
|
except Exception as e:
|
|
317
|
-
if
|
|
318
|
-
return
|
|
319
|
-
|
|
320
|
-
elif retry_count < BackupSettings.MAX_RETRIES:
|
|
323
|
+
if retry_count < BackupSettings.MAX_RETRIES:
|
|
321
324
|
# Retry with exponential backoff until MAX_RETRIES
|
|
322
325
|
next_retry = retry_count + 1
|
|
323
326
|
wait_time = BackupSettings.RETRY_DELAY**next_retry
|
|
@@ -328,52 +331,23 @@ class BackupManager:
|
|
|
328
331
|
)
|
|
329
332
|
|
|
330
333
|
time.sleep(wait_time)
|
|
331
|
-
self._process_batch(batch,
|
|
334
|
+
self._process_batch(batch, next_retry)
|
|
332
335
|
else:
|
|
333
336
|
# If the batch fails after MAX_RETRIES, raise the error
|
|
334
337
|
self.logger.error(f"Batch failed: {e.__class__.__name__}: {e}")
|
|
335
338
|
raise
|
|
336
339
|
|
|
337
|
-
def
|
|
340
|
+
def _process_batches(
|
|
338
341
|
self,
|
|
339
342
|
batches: list[BackupBatch],
|
|
340
343
|
) -> None:
|
|
341
344
|
"""
|
|
342
|
-
Processes batches
|
|
343
|
-
|
|
345
|
+
Processes batches sequentially to avoid overloading the API.
|
|
346
|
+
If any batch fails, the processing will stop.
|
|
344
347
|
"""
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
with ThreadPoolExecutor(
|
|
350
|
-
max_workers=self.config.max_workers
|
|
351
|
-
) as executor:
|
|
352
|
-
# Set the futures tasks.
|
|
353
|
-
futures = []
|
|
354
|
-
for batch in batches:
|
|
355
|
-
futures.append(
|
|
356
|
-
executor.submit(
|
|
357
|
-
self._process_batch,
|
|
358
|
-
batch,
|
|
359
|
-
stop_event,
|
|
360
|
-
)
|
|
361
|
-
)
|
|
362
|
-
|
|
363
|
-
# Process futures as they complete
|
|
364
|
-
for future in as_completed(futures):
|
|
365
|
-
try:
|
|
366
|
-
future.result()
|
|
367
|
-
except Exception:
|
|
368
|
-
# On failure, set the flag to True - signal running processes to stop
|
|
369
|
-
stop_event.set()
|
|
370
|
-
|
|
371
|
-
# Cancel unstarted threads
|
|
372
|
-
for f in futures:
|
|
373
|
-
if not f.done():
|
|
374
|
-
f.cancel()
|
|
375
|
-
|
|
376
|
-
raise
|
|
348
|
+
for i, batch in enumerate(batches, 1):
|
|
349
|
+
self.logger.info(f"Processing batch {i}/{len(batches)}...")
|
|
350
|
+
self._process_batch(batch)
|
|
377
351
|
|
|
378
352
|
def backup_workspaces(
|
|
379
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.
|
|
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(
|
|
32
|
+
class BackupSettings(ApiDefaults):
|
|
37
33
|
MAX_RETRIES = 3
|
|
38
34
|
RETRY_DELAY = 5 # seconds
|
|
39
35
|
TIMESTAMP_SDK_FOLDER = (
|
|
@@ -83,14 +83,13 @@ class BackupRestoreConfig(BaseModel):
|
|
|
83
83
|
description="Batch size must be greater than 0",
|
|
84
84
|
),
|
|
85
85
|
] = Field(default=BackupSettings.DEFAULT_BATCH_SIZE)
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
api_calls_per_second: Annotated[
|
|
87
|
+
float,
|
|
88
88
|
Field(
|
|
89
89
|
gt=0,
|
|
90
|
-
|
|
91
|
-
description="Max workers must be greater than 0 and less than 3",
|
|
90
|
+
description="Maximum API calls per second (rate limiting)",
|
|
92
91
|
),
|
|
93
|
-
] = Field(default=BackupSettings.
|
|
92
|
+
] = Field(default=BackupSettings.DEFAULT_API_CALLS_PER_SECOND)
|
|
94
93
|
|
|
95
94
|
@classmethod
|
|
96
95
|
def from_yaml(cls, conf_path: str) -> "BackupRestoreConfig":
|
|
@@ -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.
|
|
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.
|
|
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)"
|
{gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/backup_and_restore/test_backup.py
RENAMED
|
@@ -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")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/__init__.py
RENAMED
|
File without changes
|
{gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/exceptions.py
RENAMED
|
File without changes
|
{gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/gooddata_api.py
RENAMED
|
File without changes
|
|
File without changes
|
{gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/gooddata_sdk.py
RENAMED
|
File without changes
|
{gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/api/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/logger/__init__.py
RENAMED
|
File without changes
|
{gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/gooddata_pipelines/logger/logger.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/backup_and_restore/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_conf.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/data/backup/test_local_conf.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/panther/test_api_wrapper.py
RENAMED
|
File without changes
|
{gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/panther/test_sdk_wrapper.py
RENAMED
|
File without changes
|
|
File without changes
|
{gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/entities/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gooddata_pipelines-1.50.0 → gooddata_pipelines-1.50.1.dev1}/tests/provisioning/test_provisioning.py
RENAMED
|
File without changes
|
|
File without changes
|