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.
- gooddata_pipelines/__init__.py +59 -0
- gooddata_pipelines/_version.py +7 -0
- gooddata_pipelines/api/__init__.py +5 -0
- gooddata_pipelines/api/exceptions.py +41 -0
- gooddata_pipelines/api/gooddata_api.py +309 -0
- gooddata_pipelines/api/gooddata_api_wrapper.py +36 -0
- gooddata_pipelines/api/gooddata_sdk.py +374 -0
- gooddata_pipelines/api/utils.py +43 -0
- gooddata_pipelines/backup_and_restore/__init__.py +1 -0
- gooddata_pipelines/backup_and_restore/backup_input_processor.py +195 -0
- gooddata_pipelines/backup_and_restore/backup_manager.py +430 -0
- gooddata_pipelines/backup_and_restore/constants.py +42 -0
- gooddata_pipelines/backup_and_restore/csv_reader.py +41 -0
- gooddata_pipelines/backup_and_restore/models/__init__.py +1 -0
- gooddata_pipelines/backup_and_restore/models/input_type.py +11 -0
- gooddata_pipelines/backup_and_restore/models/storage.py +58 -0
- gooddata_pipelines/backup_and_restore/models/workspace_response.py +51 -0
- gooddata_pipelines/backup_and_restore/storage/__init__.py +1 -0
- gooddata_pipelines/backup_and_restore/storage/base_storage.py +18 -0
- gooddata_pipelines/backup_and_restore/storage/local_storage.py +37 -0
- gooddata_pipelines/backup_and_restore/storage/s3_storage.py +71 -0
- gooddata_pipelines/logger/__init__.py +8 -0
- gooddata_pipelines/logger/logger.py +115 -0
- gooddata_pipelines/provisioning/__init__.py +31 -0
- gooddata_pipelines/provisioning/assets/wdf_setting.json +14 -0
- gooddata_pipelines/provisioning/entities/__init__.py +1 -0
- gooddata_pipelines/provisioning/entities/user_data_filters/__init__.py +1 -0
- gooddata_pipelines/provisioning/entities/user_data_filters/models/__init__.py +1 -0
- gooddata_pipelines/provisioning/entities/user_data_filters/models/udf_models.py +32 -0
- gooddata_pipelines/provisioning/entities/user_data_filters/user_data_filters.py +221 -0
- gooddata_pipelines/provisioning/entities/users/__init__.py +1 -0
- gooddata_pipelines/provisioning/entities/users/models/__init__.py +1 -0
- gooddata_pipelines/provisioning/entities/users/models/permissions.py +242 -0
- gooddata_pipelines/provisioning/entities/users/models/user_groups.py +64 -0
- gooddata_pipelines/provisioning/entities/users/models/users.py +114 -0
- gooddata_pipelines/provisioning/entities/users/permissions.py +153 -0
- gooddata_pipelines/provisioning/entities/users/user_groups.py +212 -0
- gooddata_pipelines/provisioning/entities/users/users.py +179 -0
- gooddata_pipelines/provisioning/entities/workspaces/__init__.py +1 -0
- gooddata_pipelines/provisioning/entities/workspaces/models.py +78 -0
- gooddata_pipelines/provisioning/entities/workspaces/workspace.py +263 -0
- gooddata_pipelines/provisioning/entities/workspaces/workspace_data_filters.py +286 -0
- gooddata_pipelines/provisioning/entities/workspaces/workspace_data_parser.py +123 -0
- gooddata_pipelines/provisioning/entities/workspaces/workspace_data_validator.py +188 -0
- gooddata_pipelines/provisioning/provisioning.py +132 -0
- gooddata_pipelines/provisioning/utils/__init__.py +1 -0
- gooddata_pipelines/provisioning/utils/context_objects.py +32 -0
- gooddata_pipelines/provisioning/utils/exceptions.py +95 -0
- gooddata_pipelines/provisioning/utils/utils.py +80 -0
- gooddata_pipelines/py.typed +0 -0
- gooddata_pipelines-1.47.1.dev1.dist-info/METADATA +85 -0
- gooddata_pipelines-1.47.1.dev1.dist-info/RECORD +54 -0
- gooddata_pipelines-1.47.1.dev1.dist-info/WHEEL +4 -0
- 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
|