gooddata-pipelines 1.47.1.dev1__py3-none-any.whl

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 (54) hide show
  1. gooddata_pipelines/__init__.py +59 -0
  2. gooddata_pipelines/_version.py +7 -0
  3. gooddata_pipelines/api/__init__.py +5 -0
  4. gooddata_pipelines/api/exceptions.py +41 -0
  5. gooddata_pipelines/api/gooddata_api.py +309 -0
  6. gooddata_pipelines/api/gooddata_api_wrapper.py +36 -0
  7. gooddata_pipelines/api/gooddata_sdk.py +374 -0
  8. gooddata_pipelines/api/utils.py +43 -0
  9. gooddata_pipelines/backup_and_restore/__init__.py +1 -0
  10. gooddata_pipelines/backup_and_restore/backup_input_processor.py +195 -0
  11. gooddata_pipelines/backup_and_restore/backup_manager.py +430 -0
  12. gooddata_pipelines/backup_and_restore/constants.py +42 -0
  13. gooddata_pipelines/backup_and_restore/csv_reader.py +41 -0
  14. gooddata_pipelines/backup_and_restore/models/__init__.py +1 -0
  15. gooddata_pipelines/backup_and_restore/models/input_type.py +11 -0
  16. gooddata_pipelines/backup_and_restore/models/storage.py +58 -0
  17. gooddata_pipelines/backup_and_restore/models/workspace_response.py +51 -0
  18. gooddata_pipelines/backup_and_restore/storage/__init__.py +1 -0
  19. gooddata_pipelines/backup_and_restore/storage/base_storage.py +18 -0
  20. gooddata_pipelines/backup_and_restore/storage/local_storage.py +37 -0
  21. gooddata_pipelines/backup_and_restore/storage/s3_storage.py +71 -0
  22. gooddata_pipelines/logger/__init__.py +8 -0
  23. gooddata_pipelines/logger/logger.py +115 -0
  24. gooddata_pipelines/provisioning/__init__.py +31 -0
  25. gooddata_pipelines/provisioning/assets/wdf_setting.json +14 -0
  26. gooddata_pipelines/provisioning/entities/__init__.py +1 -0
  27. gooddata_pipelines/provisioning/entities/user_data_filters/__init__.py +1 -0
  28. gooddata_pipelines/provisioning/entities/user_data_filters/models/__init__.py +1 -0
  29. gooddata_pipelines/provisioning/entities/user_data_filters/models/udf_models.py +32 -0
  30. gooddata_pipelines/provisioning/entities/user_data_filters/user_data_filters.py +221 -0
  31. gooddata_pipelines/provisioning/entities/users/__init__.py +1 -0
  32. gooddata_pipelines/provisioning/entities/users/models/__init__.py +1 -0
  33. gooddata_pipelines/provisioning/entities/users/models/permissions.py +242 -0
  34. gooddata_pipelines/provisioning/entities/users/models/user_groups.py +64 -0
  35. gooddata_pipelines/provisioning/entities/users/models/users.py +114 -0
  36. gooddata_pipelines/provisioning/entities/users/permissions.py +153 -0
  37. gooddata_pipelines/provisioning/entities/users/user_groups.py +212 -0
  38. gooddata_pipelines/provisioning/entities/users/users.py +179 -0
  39. gooddata_pipelines/provisioning/entities/workspaces/__init__.py +1 -0
  40. gooddata_pipelines/provisioning/entities/workspaces/models.py +78 -0
  41. gooddata_pipelines/provisioning/entities/workspaces/workspace.py +263 -0
  42. gooddata_pipelines/provisioning/entities/workspaces/workspace_data_filters.py +286 -0
  43. gooddata_pipelines/provisioning/entities/workspaces/workspace_data_parser.py +123 -0
  44. gooddata_pipelines/provisioning/entities/workspaces/workspace_data_validator.py +188 -0
  45. gooddata_pipelines/provisioning/provisioning.py +132 -0
  46. gooddata_pipelines/provisioning/utils/__init__.py +1 -0
  47. gooddata_pipelines/provisioning/utils/context_objects.py +32 -0
  48. gooddata_pipelines/provisioning/utils/exceptions.py +95 -0
  49. gooddata_pipelines/provisioning/utils/utils.py +80 -0
  50. gooddata_pipelines/py.typed +0 -0
  51. gooddata_pipelines-1.47.1.dev1.dist-info/METADATA +85 -0
  52. gooddata_pipelines-1.47.1.dev1.dist-info/RECORD +54 -0
  53. gooddata_pipelines-1.47.1.dev1.dist-info/WHEEL +4 -0
  54. gooddata_pipelines-1.47.1.dev1.dist-info/licenses/LICENSE.txt +1 -277
@@ -0,0 +1,212 @@
1
+ # (C) 2025 GoodData Corporation
2
+
3
+ """Module for provisioning user groups in GoodData workspaces."""
4
+
5
+ from typing import Sequence, TypeAlias
6
+
7
+ from gooddata_sdk.catalog.user.entity_model.user_group import CatalogUserGroup
8
+
9
+ from gooddata_pipelines.provisioning.entities.users.models.user_groups import (
10
+ UserGroupFullLoad,
11
+ UserGroupIncrementalLoad,
12
+ )
13
+ from gooddata_pipelines.provisioning.provisioning import Provisioning
14
+
15
+ UserGroupModel: TypeAlias = UserGroupFullLoad | UserGroupIncrementalLoad
16
+
17
+
18
+ class UserGroupProvisioner(
19
+ Provisioning[UserGroupFullLoad, UserGroupIncrementalLoad]
20
+ ):
21
+ """Provisioning class for user groups in GoodData workspaces.
22
+
23
+ This class handles the creation, update, and deletion of user groups
24
+ based on the provided source data.
25
+ """
26
+
27
+ source_group_incremental: list[UserGroupIncrementalLoad]
28
+ source_group_full: list[UserGroupFullLoad]
29
+ upstream_user_groups: list[CatalogUserGroup]
30
+
31
+ @staticmethod
32
+ def _is_changed(
33
+ group: UserGroupModel, existing_group: CatalogUserGroup
34
+ ) -> bool:
35
+ """Checks if user group has some changes and needs to be updated."""
36
+ group.parent_user_groups.sort()
37
+ parents_changed = group.parent_user_groups != existing_group.get_parents
38
+ name_changed = group.user_group_name != existing_group.name
39
+ return parents_changed or name_changed
40
+
41
+ def _create_or_update_user_group(
42
+ self,
43
+ group_id: str,
44
+ group_name: str,
45
+ parent_user_groups: list[str],
46
+ ) -> None:
47
+ """Creates or updates user group in the project."""
48
+ catalog_user_group = CatalogUserGroup.init(
49
+ user_group_id=group_id,
50
+ user_group_name=group_name,
51
+ user_group_parent_ids=parent_user_groups,
52
+ )
53
+ try:
54
+ self._api.create_or_update_user_group(
55
+ catalog_user_group=catalog_user_group
56
+ )
57
+ self.logger.info(
58
+ f"Created/Updated user group: {group_id} - {group_name}"
59
+ )
60
+ except Exception as e:
61
+ self.logger.error(
62
+ f"Failed to create/update user group. Error: {e} "
63
+ + f"Context: {catalog_user_group.__dict__}"
64
+ )
65
+
66
+ def _create_missing_user_groups(
67
+ self,
68
+ groups_to_create: Sequence[UserGroupModel],
69
+ ) -> None:
70
+ """Provisions user groups that don't exist."""
71
+ # Sort user groups to create those without parents first
72
+ sorted_groups = sorted(
73
+ groups_to_create, key=lambda x: 1 if x.parent_user_groups else 0
74
+ )
75
+
76
+ for group in sorted_groups:
77
+ self._create_or_update_user_group(
78
+ group.user_group_id,
79
+ group.user_group_name,
80
+ group.parent_user_groups,
81
+ )
82
+
83
+ def _update_existing_user_groups(
84
+ self,
85
+ groups_to_update: Sequence[UserGroupModel],
86
+ upstream_user_groups: list[CatalogUserGroup],
87
+ ) -> None:
88
+ """Update existing user groups and update ws_permissions."""
89
+ existing_groups = {group.id: group for group in upstream_user_groups}
90
+
91
+ for group in groups_to_update:
92
+ existing_group = existing_groups[group.user_group_id]
93
+ if self._is_changed(group, existing_group):
94
+ self._create_or_update_user_group(
95
+ group.user_group_id,
96
+ group.user_group_name,
97
+ group.parent_user_groups,
98
+ )
99
+
100
+ def _delete_user_group(self, group_ids_to_delete: set[str]) -> None:
101
+ """Deletes user group from the project."""
102
+ for group_id in group_ids_to_delete:
103
+ try:
104
+ self._api.delete_user_group(group_id)
105
+ self.logger.info(f"Deleted user group: {group_id}")
106
+ except Exception as e:
107
+ self.logger.error(
108
+ f"Failed to delete user group. Error: {e} "
109
+ + f"Context: {{'user_group_id': {group_id}}}"
110
+ )
111
+
112
+ def _provision_incremental_load(self) -> None:
113
+ """Runs incremental provisioning of user groups."""
114
+ # Get existing user groups from GoodData Cloud
115
+ self.upstream_user_groups = self._api.list_user_groups()
116
+
117
+ # Create a set of upstream user group IDs
118
+ upstream_group_ids: set[str] = {
119
+ group.id for group in self.upstream_user_groups
120
+ }
121
+
122
+ # Create a set of active source user group IDs
123
+ active_source_groups: set[str] = {
124
+ group.user_group_id
125
+ for group in self.source_group_incremental
126
+ if group.is_active is True
127
+ }
128
+
129
+ # Create a set of inactive source user group IDs
130
+ inactive_source_groups: set[str] = {
131
+ group.user_group_id
132
+ for group in self.source_group_incremental
133
+ if group.is_active is False
134
+ }
135
+
136
+ # Create a set of user group IDs to create as the difference between active
137
+ # source groups and upstream groups - i.e, we are creating groups marked
138
+ # as active in the source data but which are missing upstream in GoodData Cloud.
139
+ group_ids_to_create: set[str] = active_source_groups.difference(
140
+ upstream_group_ids
141
+ )
142
+
143
+ # Create a set of user group IDs to update as the intersection between active
144
+ # source groups and upstream groups - i.e, we are updating groups marked
145
+ # as active in the source data and which are present upstream in GoodData Cloud.
146
+ # The `_update_existing_user_groups` method will check if the upstream group
147
+ # definition differs from the source and if so, it will update the group.
148
+ group_ids_to_update: set[str] = active_source_groups.intersection(
149
+ upstream_group_ids
150
+ )
151
+
152
+ # Create a set of user group IDs to delete as the intersection between
153
+ # inactive source groups and upstream groups - i.e, we are deleting groups
154
+ # marked as inactive in the source data and which are present upstream in
155
+ # GoodData Cloud.
156
+ group_ids_to_delete: set[str] = inactive_source_groups.intersection(
157
+ upstream_group_ids
158
+ )
159
+
160
+ # create lists of groups to create, update and delete based on the sets
161
+ groups_to_create: list[UserGroupIncrementalLoad] = []
162
+ groups_to_update: list[UserGroupIncrementalLoad] = []
163
+
164
+ for group in self.source_group_incremental:
165
+ if group.user_group_id in group_ids_to_create:
166
+ groups_to_create.append(group)
167
+ elif group.user_group_id in group_ids_to_update:
168
+ groups_to_update.append(group)
169
+
170
+ self._create_missing_user_groups(groups_to_create)
171
+ self._update_existing_user_groups(
172
+ groups_to_update, self.upstream_user_groups
173
+ )
174
+ self._delete_user_group(group_ids_to_delete)
175
+
176
+ def _provision_full_load(self) -> None:
177
+ """Runs full load provisioning of user groups."""
178
+ # Get upsream user groups
179
+ self.upstream_user_groups = self._api.list_user_groups()
180
+
181
+ # Create a set of upstream user group IDs
182
+ upstream_group_ids: set[str] = {
183
+ group.id for group in self.upstream_user_groups
184
+ }
185
+
186
+ # Create a set of source user group IDs
187
+ source_group_ids: set[str] = {
188
+ group.user_group_id for group in self.source_group_full
189
+ }
190
+
191
+ # Figure out which ids are to be created, deleted or exist in both systems
192
+ id_groups = self._create_groups(source_group_ids, upstream_group_ids)
193
+
194
+ groups_to_create: list[UserGroupFullLoad] = []
195
+ groups_to_update: list[UserGroupFullLoad] = []
196
+
197
+ for group in self.source_group_full:
198
+ if group.user_group_id in id_groups.ids_to_create:
199
+ groups_to_create.append(group)
200
+ elif group.user_group_id in id_groups.ids_in_both_systems:
201
+ groups_to_update.append(group)
202
+
203
+ # Create user groups
204
+ self._create_missing_user_groups(groups_to_create)
205
+
206
+ # Update user groups
207
+ self._update_existing_user_groups(
208
+ groups_to_update, self.upstream_user_groups
209
+ )
210
+
211
+ # Delete user groups
212
+ self._delete_user_group(id_groups.ids_to_delete)
@@ -0,0 +1,179 @@
1
+ # (C) 2025 GoodData Corporation
2
+
3
+ """Module for provisioning users in GoodData workspaces."""
4
+
5
+ from typing import TypeAlias
6
+
7
+ from gooddata_api_client.exceptions import NotFoundException # type: ignore
8
+ from gooddata_sdk.catalog.user.entity_model.user import CatalogUser
9
+ from gooddata_sdk.catalog.user.entity_model.user_group import CatalogUserGroup
10
+
11
+ from gooddata_pipelines.provisioning.entities.users.models.users import (
12
+ UserFullLoad,
13
+ UserIncrementalLoad,
14
+ )
15
+ from gooddata_pipelines.provisioning.provisioning import Provisioning
16
+ from gooddata_pipelines.provisioning.utils.context_objects import UserContext
17
+
18
+ # Type alias for user model instances
19
+ UserModel: TypeAlias = UserFullLoad | UserIncrementalLoad
20
+ UserId: TypeAlias = str
21
+
22
+
23
+ class UserProvisioner(Provisioning[UserFullLoad, UserIncrementalLoad]):
24
+ """Provisioning class for users in GoodData workspaces.
25
+
26
+ This class handles the creation, update, and deletion of users
27
+ based on the provided source data.
28
+ """
29
+
30
+ source_group_incremental: list[UserIncrementalLoad]
31
+ source_group_full: list[UserFullLoad]
32
+
33
+ def __init__(self, host: str, token: str) -> None:
34
+ super().__init__(host, token)
35
+ self.upstream_user_cache: dict[UserId, UserModel] = {}
36
+
37
+ def _try_get_user(
38
+ self, user: UserModel, model: type[UserModel]
39
+ ) -> UserModel | None:
40
+ try:
41
+ if user.user_id in self.upstream_user_cache:
42
+ return self.upstream_user_cache[user.user_id]
43
+
44
+ user_sdk_obj = self._api._sdk.catalog_user.get_user(user.user_id)
45
+ return model.from_sdk_obj(user_sdk_obj)
46
+ except NotFoundException:
47
+ return None
48
+
49
+ def _get_or_create_user_groups(self, groups: list[str]) -> None:
50
+ """Ensures that all user groups exist in the project."""
51
+ for group in groups:
52
+ try:
53
+ self._api._sdk.catalog_user.get_user_group(group)
54
+ except NotFoundException:
55
+ # Create the user gtoup if it does not exist
56
+ self._api.create_or_update_user_group(
57
+ CatalogUserGroup.init(
58
+ user_group_id=group, user_group_name=group
59
+ ),
60
+ )
61
+ self.logger.info(f"Created user group: {group}")
62
+
63
+ def _user_is_equal_upstream(
64
+ self,
65
+ user: UserModel,
66
+ upstream_user: UserModel | None,
67
+ ) -> bool:
68
+ """
69
+ Checks if the user is different from the upstream user. Lists are checked by converting to sets.
70
+ """
71
+ if not upstream_user:
72
+ return False
73
+
74
+ user_data = user.model_dump()
75
+ upstream_data = upstream_user.model_dump()
76
+
77
+ for attr, source_value in user_data.items():
78
+ upstream_value = upstream_data.get(attr)
79
+
80
+ if isinstance(source_value, list):
81
+ if set(source_value) != set(upstream_value or []):
82
+ return False
83
+ else:
84
+ if source_value != upstream_value:
85
+ return False
86
+ return True
87
+
88
+ def _create_or_update_user(
89
+ self, user: UserModel, model: type[UserModel]
90
+ ) -> None:
91
+ """Creates or updates user in the project.
92
+
93
+ Determines if the user needs to be updated or created by getting the
94
+ upstream user from GoodData Cloud and comparing it with the source user.
95
+ If user is supposed to be placed in a User Group, the function will check
96
+ for its existence and create it if needed.
97
+
98
+ """
99
+ user_context = UserContext(
100
+ user_id=user.user_id,
101
+ user_groups=user.user_groups,
102
+ )
103
+
104
+ upstream_user = self._try_get_user(user, model)
105
+
106
+ if self._user_is_equal_upstream(user, upstream_user):
107
+ return
108
+
109
+ self._get_or_create_user_groups(user.user_groups)
110
+
111
+ self._api.create_or_update_user(
112
+ user.to_sdk_obj(), **user_context.__dict__
113
+ )
114
+ self.logger.info(f"User {user.user_id} created/updated successfully.")
115
+
116
+ def _delete_user(self, user_id: str) -> None:
117
+ """Deletes user from the project."""
118
+ try:
119
+ self._api._sdk.catalog_user.get_user(user_id)
120
+ except NotFoundException:
121
+ return
122
+
123
+ self._api.delete_user(user_id)
124
+ self.logger.info(f"Deleted user: {user_id}")
125
+
126
+ def _manage_user(self, user: UserIncrementalLoad) -> None:
127
+ """Manages user based on the provided GDUserTarget."""
128
+ if user.is_active:
129
+ self._create_or_update_user(user, UserIncrementalLoad)
130
+ else:
131
+ self._delete_user(user.user_id)
132
+
133
+ def _provision_incremental_load(self) -> None:
134
+ """Runs the incremental provisioning logic."""
135
+ for user in self.source_group_incremental:
136
+ # Attempt to process each user. On failure, log the error and continue
137
+ try:
138
+ self._manage_user(user)
139
+ except Exception as e:
140
+ self.logger.error(
141
+ f"Failed to manage user {user.user_id}. Error: {e} Context: {user.__dict__}"
142
+ )
143
+
144
+ def _provision_full_load(self) -> None:
145
+ """Runs the full load provisioning logic."""
146
+ # Get all upstream users
147
+ catalog_upstream_users: list[CatalogUser] = self._api.list_users()
148
+
149
+ # Convert catalog users to user models
150
+ upstream_users: list[UserFullLoad] = [
151
+ UserFullLoad.from_sdk_obj(user) for user in catalog_upstream_users
152
+ ]
153
+
154
+ # Cache the upstream users in a dict. It will be reused in `_try_get_user`
155
+ self.upstream_user_cache = {
156
+ user.user_id: user for user in upstream_users
157
+ }
158
+ # Get source IDs
159
+ source_ids: set[str] = {user.user_id for user in self.source_group_full}
160
+
161
+ # Get upstream IDs
162
+ upstream_ids: set[str] = {user.user_id for user in upstream_users}
163
+
164
+ # Create groups of IDs to delete, create, and in both systems
165
+ id_groups = self._create_groups(source_ids, upstream_ids)
166
+
167
+ # Iterate over source users and create/update
168
+ for user in self.source_group_full:
169
+ user_id = user.user_id
170
+
171
+ if (
172
+ user_id in id_groups.ids_to_create
173
+ or user_id in id_groups.ids_in_both_systems
174
+ ):
175
+ self._create_or_update_user(user, UserFullLoad)
176
+
177
+ # Delete users marked for deletion
178
+ for user_id in id_groups.ids_to_delete:
179
+ self._delete_user(user_id)
@@ -0,0 +1 @@
1
+ # (C) 2025 GoodData Corporation
@@ -0,0 +1,78 @@
1
+ # (C) 2025 GoodData Corporation
2
+ """Module containing models related to workspace provisioning in GoodData Cloud."""
3
+
4
+ from dataclasses import dataclass, field
5
+ from typing import Literal
6
+
7
+ from pydantic import BaseModel, ConfigDict
8
+
9
+
10
+ @dataclass
11
+ class WorkspaceDataMaps:
12
+ """Dataclass to hold various mappings related to workspace data."""
13
+
14
+ child_to_parent_id_map: dict[str, str] = field(default_factory=dict)
15
+ workspace_id_to_wdf_map: dict[str, dict[str, list[str]]] = field(
16
+ default_factory=dict
17
+ )
18
+ parent_ids: set[str] = field(default_factory=set)
19
+ source_ids: set[str] = field(default_factory=set)
20
+ workspace_id_to_name_map: dict[str, str] = field(default_factory=dict)
21
+ upstream_ids: set[str] = field(default_factory=set)
22
+
23
+
24
+ class WorkspaceFullLoad(BaseModel):
25
+ """Model representing input for provisioning of workspaces in GoodData Cloud."""
26
+
27
+ model_config = ConfigDict(coerce_numbers_to_str=True)
28
+
29
+ parent_id: str
30
+ workspace_id: str
31
+ workspace_name: str
32
+ workspace_data_filter_id: str | None = None
33
+ workspace_data_filter_values: list[str] | None = None
34
+
35
+
36
+ class WorkspaceIncrementalLoad(WorkspaceFullLoad):
37
+ """Model representing input for incremental provisioning of workspaces in GoodData Cloud."""
38
+
39
+ # TODO: double check that the model loads the data correctly, write a test
40
+ is_active: bool
41
+
42
+
43
+ class WDFSettingAttributes(BaseModel):
44
+ title: str
45
+ filterValues: list[str]
46
+
47
+
48
+ class WDFSettingRelationshipsData(BaseModel):
49
+ id: str
50
+ type: Literal["workspaceDataFilter"]
51
+
52
+
53
+ class WDFSettingRelationships(BaseModel):
54
+ workspaceDataFilter: dict[str, WDFSettingRelationshipsData]
55
+
56
+
57
+ class WDFSettingLinks(BaseModel):
58
+ self: str
59
+
60
+
61
+ class WDFSettingMetaOrigin(BaseModel):
62
+ originType: str
63
+ originId: str
64
+
65
+
66
+ class WDFSettingMeta(BaseModel):
67
+ origin: WDFSettingMetaOrigin
68
+
69
+
70
+ class WDFSetting(BaseModel):
71
+ """Model representing a workspace data filter setting in GoodData Cloud."""
72
+
73
+ id: str
74
+ type: Literal["workspaceDataFilterSetting"]
75
+ attributes: WDFSettingAttributes
76
+ relationships: WDFSettingRelationships
77
+ links: WDFSettingLinks
78
+ meta: WDFSettingMeta