Skip to content

Commit 9f73542

Browse files
committed
feat(gooddata-pipelines): add workspace restore
1 parent a602d64 commit 9f73542

File tree

22 files changed

+1045
-105
lines changed

22 files changed

+1045
-105
lines changed

gooddata-pipelines/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ You can use the package to manage following resources in GDC:
1010
- User/Group permissions
1111
- User Data Filters
1212
- Child workspaces (incl. Workspace Data Filter settings)
13-
1. _[PLANNED]:_ Backup and restore of workspaces
14-
1. _[PLANNED]:_ Custom fields management
15-
- extend the Logical Data Model of a child workspace
13+
1. Backup and restore of workspaces
14+
- Create and backup snapshots of workspace metadata.
15+
1. LDM Extension
16+
- extend the Logical Data Model of a child workspace with custom datasets and fields
1617

1718
In case you are not interested in incorporating a library in your own program but would like to use a ready-made script, consider having a look at [GoodData Productivity Tools](https://github.com/gooddata/gooddata-productivity-tools).
1819

gooddata-pipelines/gooddata_pipelines/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
S3StorageConfig,
1111
StorageType,
1212
)
13+
from .backup_and_restore.restore_manager import (
14+
RestoreManager,
15+
WorkspaceToRestore,
16+
)
1317
from .backup_and_restore.storage.local_storage import LocalStorage
1418
from .backup_and_restore.storage.s3_storage import S3Storage
1519

@@ -57,6 +61,8 @@
5761

5862
__all__ = [
5963
"BackupManager",
64+
"RestoreManager",
65+
"WorkspaceToRestore",
6066
"BackupRestoreConfig",
6167
"StorageType",
6268
"LocalStorage",

gooddata-pipelines/gooddata_pipelines/api/gooddata_api.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,13 +167,40 @@ def get_user_data_filters(self, workspace_id: str) -> requests.Response:
167167
endpoint = f"/layout/workspaces/{workspace_id}/userDataFilters"
168168
return self._get(endpoint)
169169

170+
def put_user_data_filters(
171+
self, workspace_id: str, user_data_filters: dict[str, Any]
172+
) -> requests.Response:
173+
"""Puts the user data filters into GoodData workspace."""
174+
headers = {**self.headers, "Content-Type": "application/json"}
175+
return self._put(
176+
f"/layout/workspaces/{workspace_id}/userDataFilters",
177+
user_data_filters,
178+
headers,
179+
)
180+
170181
def get_automations(self, workspace_id: str) -> requests.Response:
171182
"""Gets the automations for a given workspace."""
172183
endpoint = (
173184
f"/entities/workspaces/{workspace_id}/automations?include=ALL"
174185
)
175186
return self._get(endpoint)
176187

188+
def post_automation(
189+
self, workspace_id: str, automation: dict[str, Any]
190+
) -> requests.Response:
191+
"""Posts an automation for a given workspace."""
192+
endpoint = f"/entities/workspaces/{workspace_id}/automations"
193+
return self._post(endpoint, automation)
194+
195+
def delete_automation(
196+
self, workspace_id: str, automation_id: str
197+
) -> requests.Response:
198+
"""Deletes an automation for a given workspace."""
199+
endpoint = (
200+
f"/entities/workspaces/{workspace_id}/automations/{automation_id}"
201+
)
202+
return self._delete(endpoint)
203+
177204
def get_all_metrics(self, workspace_id: str) -> requests.Response:
178205
"""Get all metrics from the specified workspace.
179206

gooddata-pipelines/gooddata_pipelines/backup_and_restore/backup_manager.py

Lines changed: 6 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,31 @@
11
# (C) 2025 GoodData Corporation
22

3-
import json
43
import os
54
import shutil
65
import tempfile
76
import time
87
import traceback
98
from pathlib import Path
10-
from typing import Any, Type
9+
from typing import Any
1110

1211
import attrs
1312
import requests
14-
import yaml
15-
from gooddata_sdk.utils import PROFILES_FILE_PATH, profile_content
1613

17-
from gooddata_pipelines.api.gooddata_api_wrapper import GoodDataApi
1814
from gooddata_pipelines.backup_and_restore.backup_input_processor import (
1915
BackupInputProcessor,
2016
)
17+
from gooddata_pipelines.backup_and_restore.base_manager import BaseManager
2118
from gooddata_pipelines.backup_and_restore.constants import (
2219
BackupSettings,
2320
DirNames,
2421
)
2522
from gooddata_pipelines.backup_and_restore.models.input_type import InputType
2623
from gooddata_pipelines.backup_and_restore.models.storage import (
2724
BackupRestoreConfig,
28-
StorageType,
2925
)
3026
from gooddata_pipelines.backup_and_restore.storage.base_storage import (
3127
BackupStorage,
3228
)
33-
from gooddata_pipelines.backup_and_restore.storage.local_storage import (
34-
LocalStorage,
35-
)
36-
from gooddata_pipelines.backup_and_restore.storage.s3_storage import (
37-
S3Storage,
38-
)
39-
from gooddata_pipelines.logger import LogObserver
4029
from gooddata_pipelines.utils.rate_limiter import RateLimiter
4130

4231

@@ -45,16 +34,12 @@ class BackupBatch:
4534
list_of_ids: list[str]
4635

4736

48-
class BackupManager:
37+
class BackupManager(BaseManager):
4938
storage: BackupStorage
5039

5140
def __init__(self, host: str, token: str, config: BackupRestoreConfig):
52-
self._api = GoodDataApi(host, token)
53-
self.logger = LogObserver()
54-
55-
self.config = config
41+
super().__init__(host, token, config)
5642

57-
self.storage = self._get_storage(self.config)
5843
self.org_id = self._api.get_organization_id()
5944

6045
self.loader = BackupInputProcessor(self._api, self.config.api_page_size)
@@ -63,39 +48,6 @@ def __init__(self, host: str, token: str, config: BackupRestoreConfig):
6348
calls_per_second=self.config.api_calls_per_second,
6449
)
6550

66-
@classmethod
67-
def create(
68-
cls: Type["BackupManager"],
69-
config: BackupRestoreConfig,
70-
host: str,
71-
token: str,
72-
) -> "BackupManager":
73-
"""Creates a backup worker instance using the provided host and token."""
74-
return cls(host=host, token=token, config=config)
75-
76-
@classmethod
77-
def create_from_profile(
78-
cls: Type["BackupManager"],
79-
config: BackupRestoreConfig,
80-
profile: str = "default",
81-
profiles_path: Path = PROFILES_FILE_PATH,
82-
) -> "BackupManager":
83-
"""Creates a backup worker instance using a GoodData profile file."""
84-
content = profile_content(profile, profiles_path)
85-
return cls(**content, config=config)
86-
87-
@staticmethod
88-
def _get_storage(conf: BackupRestoreConfig) -> BackupStorage:
89-
"""Returns the storage class based on the storage type."""
90-
if conf.storage_type == StorageType.S3:
91-
return S3Storage(conf)
92-
elif conf.storage_type == StorageType.LOCAL:
93-
return LocalStorage(conf)
94-
else:
95-
raise RuntimeError(
96-
f'Unsupported storage type "{conf.storage_type.value}".'
97-
)
98-
9951
def get_user_data_filters(self, ws_id: str) -> dict:
10052
"""Returns the user data filters for the specified workspace."""
10153
with self._api_rate_limiter:
@@ -133,19 +85,13 @@ def _store_user_data_filters(
13385
"user_data_filters",
13486
filter["id"] + ".yaml",
13587
)
136-
self._write_to_yaml(udf_file_path, filter)
88+
self.yaml_utils.dump(udf_file_path, filter)
13789

13890
@staticmethod
13991
def _move_folder(source: Path, destination: Path) -> None:
14092
"""Moves the source folder to the destination."""
14193
shutil.move(source, destination)
14294

143-
@staticmethod
144-
def _write_to_yaml(path: str, source: Any) -> None:
145-
"""Writes the source to a YAML file."""
146-
with open(path, "w") as outfile:
147-
yaml.dump(source, outfile)
148-
14995
def _get_automations_from_api(self, workspace_id: str) -> Any:
15096
"""Returns automations for the workspace as JSON."""
15197
with self._api_rate_limiter:
@@ -182,8 +128,7 @@ def _store_automations(self, export_path: Path, workspace_id: str) -> None:
182128

183129
# Store the automations in a JSON file
184130
if len(automations["data"]) > 0:
185-
with open(automations_file_path, "w") as f:
186-
json.dump(automations, f)
131+
self.json_utils.dump(automations_file_path, automations)
187132

188133
def store_declarative_filter_views(
189134
self, export_path: Path, workspace_id: str
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# (C) 2025 GoodData Corporation
2+
3+
import abc
4+
from pathlib import Path
5+
from typing import Type, TypeVar
6+
7+
from gooddata_sdk.utils import PROFILES_FILE_PATH, profile_content
8+
9+
from gooddata_pipelines.api.gooddata_api_wrapper import GoodDataApi
10+
from gooddata_pipelines.backup_and_restore.models.storage import (
11+
BackupRestoreConfig,
12+
StorageType,
13+
)
14+
from gooddata_pipelines.backup_and_restore.storage.base_storage import (
15+
BackupStorage,
16+
)
17+
from gooddata_pipelines.backup_and_restore.storage.local_storage import (
18+
LocalStorage,
19+
)
20+
from gooddata_pipelines.backup_and_restore.storage.s3_storage import S3Storage
21+
from gooddata_pipelines.logger import LogObserver
22+
from gooddata_pipelines.utils.file_utils import JsonUtils, YamlUtils
23+
24+
ManagerT = TypeVar("ManagerT", bound="BaseManager")
25+
26+
27+
class BaseManager(abc.ABC):
28+
"""Base class to provide constructors for backup and restore managers."""
29+
30+
storage: BackupStorage
31+
32+
def __init__(self, host: str, token: str, config: BackupRestoreConfig):
33+
self.config = config
34+
35+
self._api: GoodDataApi = GoodDataApi(host, token)
36+
self.logger: LogObserver = LogObserver()
37+
38+
self.storage = self._get_storage(self.config)
39+
40+
self.yaml_utils = YamlUtils()
41+
self.json_utils = JsonUtils()
42+
43+
def _get_storage(self, conf: BackupRestoreConfig) -> BackupStorage:
44+
"""Returns the storage class based on the storage type."""
45+
if conf.storage_type == StorageType.S3:
46+
return S3Storage(conf)
47+
elif conf.storage_type == StorageType.LOCAL:
48+
return LocalStorage(conf)
49+
else:
50+
raise RuntimeError(
51+
f'Unsupported storage type "{conf.storage_type.value}".'
52+
)
53+
54+
@classmethod
55+
def create(
56+
cls: Type[ManagerT],
57+
config: BackupRestoreConfig,
58+
host: str,
59+
token: str,
60+
) -> ManagerT:
61+
"""Creates a backup worker instance using the provided host and token."""
62+
return cls(host=host, token=token, config=config)
63+
64+
@classmethod
65+
def create_from_profile(
66+
cls: Type[ManagerT],
67+
config: BackupRestoreConfig,
68+
profile: str = "default",
69+
profiles_path: Path = PROFILES_FILE_PATH,
70+
) -> ManagerT:
71+
"""Creates a backup worker instance using a GoodData profile file."""
72+
content = profile_content(profile, profiles_path)
73+
return cls(host=content["host"], token=content["token"], config=config)

gooddata-pipelines/gooddata_pipelines/backup_and_restore/constants.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# (C) 2025 GoodData Corporation
22
import datetime
3+
from enum import Enum
34

45
import attrs
56
from gooddata_sdk._version import __version__ as sdk_version
67

78

8-
@attrs.frozen
9-
class DirNames:
9+
class DirNames(str, Enum):
1010
"""
1111
Folder names used in the SDK backup process:
1212
- LAYOUTS - GoodData Layouts
@@ -23,13 +23,14 @@ class DirNames:
2323

2424
@attrs.frozen
2525
class ApiDefaults:
26-
DEFAULT_PAGE_SIZE = 100
27-
DEFAULT_BATCH_SIZE = 100
28-
DEFAULT_API_CALLS_PER_SECOND = 1.0
26+
PAGE_SIZE = 100
27+
BATCH_SIZE = 100
28+
CALLS_PER_SECOND = 1.0
2929

3030

3131
@attrs.frozen
32-
class BackupSettings(ApiDefaults):
32+
class BackupSettings:
33+
API = ApiDefaults()
3334
MAX_RETRIES = 3
3435
RETRY_DELAY = 5 # seconds
3536
TIMESTAMP_SDK_FOLDER = (

0 commit comments

Comments
 (0)