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,242 @@
1
+ # (C) 2025 GoodData Corporation
2
+ from dataclasses import dataclass
3
+ from enum import Enum
4
+ from typing import Any, Iterator, TypeAlias
5
+
6
+ from gooddata_sdk.catalog.identifier import CatalogAssigneeIdentifier
7
+ from gooddata_sdk.catalog.permission.declarative_model.permission import (
8
+ CatalogDeclarativeSingleWorkspacePermission,
9
+ CatalogDeclarativeWorkspacePermissions,
10
+ )
11
+
12
+ from gooddata_pipelines.provisioning.utils.exceptions import BaseUserException
13
+
14
+ # TODO: refactor the full load and incremental load models to reuse as much as possible
15
+ # TODO: use pydantic models instead of dataclasses?
16
+ # TODO: make the validation logic more readable (as in PermissionIncrementalLoad)
17
+
18
+ TargetsPermissionDict: TypeAlias = dict[str, dict[str, bool]]
19
+
20
+
21
+ class PermissionType(Enum):
22
+ user = "user"
23
+ user_group = "userGroup"
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class PermissionIncrementalLoad:
28
+ permission: str
29
+ workspace_id: str
30
+ id: str
31
+ type: PermissionType
32
+ is_active: bool
33
+
34
+ @classmethod
35
+ def from_list_of_dicts(
36
+ cls, data: list[dict[str, Any]]
37
+ ) -> list["PermissionIncrementalLoad"]:
38
+ """Creates a list of User objects from list of dicts."""
39
+ id: str
40
+ permissions = []
41
+ for permission in data:
42
+ user_id: str | None = permission.get("user_id")
43
+ user_group_id: str | None = permission.get("ug_id")
44
+
45
+ if user_id is not None:
46
+ target_type = PermissionType.user
47
+ id = user_id
48
+ elif user_group_id is not None:
49
+ target_type = PermissionType.user_group
50
+ id = user_group_id
51
+
52
+ permissions.append(
53
+ PermissionIncrementalLoad(
54
+ permission=permission["ws_permissions"],
55
+ workspace_id=permission["ws_id"],
56
+ id=id,
57
+ type=target_type,
58
+ is_active=str(permission["is_active"]).lower() == "true",
59
+ )
60
+ )
61
+ return permissions
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class PermissionFullLoad:
66
+ permission: str
67
+ workspace_id: str
68
+ id: str
69
+ type: PermissionType
70
+
71
+ @classmethod
72
+ def from_list_of_dicts(
73
+ cls, data: list[dict[str, Any]]
74
+ ) -> list["PermissionFullLoad"]:
75
+ """Creates a list of User objects from list of dicts."""
76
+ permissions = []
77
+ for permission in data:
78
+ id = (
79
+ permission["user_id"]
80
+ if permission["user_id"]
81
+ else permission["ug_id"]
82
+ )
83
+
84
+ if permission["user_id"]:
85
+ target_type = PermissionType.user
86
+ else:
87
+ target_type = PermissionType.user_group
88
+
89
+ permissions.append(
90
+ PermissionFullLoad(
91
+ permission=permission["ws_permissions"],
92
+ workspace_id=permission["ws_id"],
93
+ id=id,
94
+ type=target_type,
95
+ )
96
+ )
97
+ return permissions
98
+
99
+
100
+ @dataclass
101
+ class PermissionDeclaration:
102
+ users: TargetsPermissionDict
103
+ user_groups: TargetsPermissionDict
104
+
105
+ @classmethod
106
+ def from_sdk_api(
107
+ cls, declaration: CatalogDeclarativeWorkspacePermissions
108
+ ) -> "PermissionDeclaration":
109
+ """
110
+ Constructs an WSPermissionDeclaration instance
111
+ from GoodData SDK CatalogDeclarativeWorkspacePermissions.
112
+ """
113
+ users: TargetsPermissionDict = {}
114
+ user_groups: TargetsPermissionDict = {}
115
+
116
+ for permission in declaration.permissions:
117
+ permission_type, id = (
118
+ permission.assignee.type,
119
+ permission.assignee.id,
120
+ )
121
+
122
+ if permission_type == PermissionType.user.value:
123
+ target_dict = users
124
+ else:
125
+ target_dict = user_groups
126
+
127
+ id_permissions = target_dict.get(id)
128
+ if not id_permissions:
129
+ target_dict[id] = dict()
130
+
131
+ target_dict[id][permission.name] = True
132
+
133
+ return PermissionDeclaration(users, user_groups)
134
+
135
+ @staticmethod
136
+ def _construct_upstream_permission(
137
+ permission: str, assignee: CatalogAssigneeIdentifier
138
+ ) -> CatalogDeclarativeSingleWorkspacePermission | None:
139
+ """Constructs single permission declaration for the SDK API."""
140
+ try:
141
+ return CatalogDeclarativeSingleWorkspacePermission(
142
+ name=permission, assignee=assignee
143
+ )
144
+ except Exception as e:
145
+ raise BaseUserException(
146
+ f"Failed to construct SDK declaration for type={assignee.type} ",
147
+ f"id={assignee.id}. Error: {e}",
148
+ )
149
+
150
+ def _permissions_for_target(
151
+ self, permissions: dict[str, bool], assignee: CatalogAssigneeIdentifier
152
+ ) -> Iterator[CatalogDeclarativeSingleWorkspacePermission]:
153
+ """Constructs permission declarations for a single target."""
154
+ for permission, is_active in permissions.items():
155
+ if not is_active:
156
+ continue
157
+ declaration = self._construct_upstream_permission(
158
+ permission, assignee
159
+ )
160
+ if not declaration:
161
+ continue
162
+ yield declaration
163
+
164
+ def to_sdk_api(self) -> CatalogDeclarativeWorkspacePermissions:
165
+ """
166
+ Constructs the GoodData SDK CatalogDeclarativeWorkspacePermissions
167
+ object from the WSPermissionDeclaration instance.
168
+ """
169
+ permission_declarations: list[
170
+ CatalogDeclarativeSingleWorkspacePermission
171
+ ] = []
172
+
173
+ for user_id, permissions in self.users.items():
174
+ assignee = CatalogAssigneeIdentifier(
175
+ id=user_id, type=PermissionType.user.value
176
+ )
177
+ for declaration in self._permissions_for_target(
178
+ permissions, assignee
179
+ ):
180
+ permission_declarations.append(declaration)
181
+
182
+ for ug_id, permissions in self.user_groups.items():
183
+ assignee = CatalogAssigneeIdentifier(
184
+ id=ug_id, type=PermissionType.user_group.value
185
+ )
186
+ for declaration in self._permissions_for_target(
187
+ permissions, assignee
188
+ ):
189
+ permission_declarations.append(declaration)
190
+
191
+ return CatalogDeclarativeWorkspacePermissions(
192
+ permissions=permission_declarations
193
+ )
194
+
195
+ def add_permission(self, permission: PermissionIncrementalLoad) -> None:
196
+ """
197
+ Adds WSPermission object into respective field within the instance.
198
+ Handles duplicate permissions and different combinations of input
199
+ and upstream is_active permission states.
200
+ """
201
+ target_dict = (
202
+ self.users
203
+ if permission.type == PermissionType.user
204
+ else self.user_groups
205
+ )
206
+
207
+ if permission.id not in target_dict:
208
+ target_dict[permission.id] = {}
209
+
210
+ is_active = permission.is_active
211
+ target_permissions = target_dict[permission.id]
212
+ permission_value = permission.permission
213
+
214
+ if permission_value not in target_permissions:
215
+ target_permissions[permission_value] = is_active
216
+ elif not is_active and target_permissions[permission_value] is True:
217
+ print(
218
+ "isActive=False provided after True has been specificed for the "
219
+ + f"same input. Skipping '{permission}'"
220
+ )
221
+ elif is_active and target_permissions[permission_value] is False:
222
+ print(
223
+ "isActive=True provided after False has been specified for the "
224
+ + f"same input. Overwriting '{permission}'"
225
+ )
226
+ target_permissions[permission_value] = is_active
227
+
228
+ def upsert(self, other: "PermissionDeclaration") -> None:
229
+ """
230
+ Modifies the owner object by merging with the other.
231
+ Keeps the unmodified users/userGroups untouched.
232
+ If some user/userGroup is modified, it gets overwritten with permissions
233
+ defined in the input.
234
+ """
235
+ for user_id, permissions in other.users.items():
236
+ self.users[user_id] = permissions
237
+
238
+ for ug_id, permissions in other.user_groups.items():
239
+ self.user_groups[ug_id] = permissions
240
+
241
+
242
+ WSPermissionsDeclarations: TypeAlias = dict[str, PermissionDeclaration]
@@ -0,0 +1,64 @@
1
+ # (C) 2025 GoodData Corporation
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from gooddata_pipelines.provisioning.utils.utils import SplitMixin
8
+
9
+
10
+ class BaseUserGroup(BaseModel, SplitMixin):
11
+ user_group_id: str
12
+ user_group_name: str
13
+ parent_user_groups: list[str]
14
+
15
+ @classmethod
16
+ def _create_from_dict_data(
17
+ cls, user_group_data: dict[str, Any], delimiter: str = ","
18
+ ) -> dict[str, Any]:
19
+ """Helper method to extract common data from dict."""
20
+ parent_user_groups = cls.split(
21
+ user_group_data["parent_user_groups"], delimiter=delimiter
22
+ )
23
+ user_group_name = user_group_data["user_group_name"]
24
+ if not user_group_name:
25
+ user_group_name = user_group_data["user_group_id"]
26
+
27
+ return {
28
+ "user_group_id": user_group_data["user_group_id"],
29
+ "user_group_name": user_group_name,
30
+ "parent_user_groups": parent_user_groups,
31
+ }
32
+
33
+
34
+ class UserGroupIncrementalLoad(BaseUserGroup):
35
+ is_active: bool
36
+
37
+ @classmethod
38
+ def from_list_of_dicts(
39
+ cls, data: list[dict[str, Any]], delimiter: str = ","
40
+ ) -> list["UserGroupIncrementalLoad"]:
41
+ """Creates a list of User objects from list of dicts."""
42
+ user_groups = []
43
+ for user_group in data:
44
+ base_data = cls._create_from_dict_data(user_group, delimiter)
45
+ base_data["is_active"] = user_group["is_active"]
46
+
47
+ user_groups.append(UserGroupIncrementalLoad(**base_data))
48
+
49
+ return user_groups
50
+
51
+
52
+ class UserGroupFullLoad(BaseUserGroup):
53
+ @classmethod
54
+ def from_list_of_dicts(
55
+ cls, data: list[dict[str, Any]], delimiter: str = ","
56
+ ) -> list["UserGroupFullLoad"]:
57
+ """Creates a list of User objects from list of dicts."""
58
+ user_groups = []
59
+ for user_group in data:
60
+ base_data = cls._create_from_dict_data(user_group, delimiter)
61
+
62
+ user_groups.append(UserGroupFullLoad(**base_data))
63
+
64
+ return user_groups
@@ -0,0 +1,114 @@
1
+ # (C) 2025 GoodData Corporation
2
+
3
+ from typing import Any
4
+
5
+ from gooddata_sdk.catalog.user.entity_model.user import CatalogUser
6
+ from pydantic import BaseModel
7
+
8
+ from gooddata_pipelines.provisioning.utils.utils import SplitMixin
9
+
10
+
11
+ class BaseUser(BaseModel, SplitMixin):
12
+ """Base class containing shared user fields and functionality."""
13
+
14
+ user_id: str
15
+ firstname: str | None
16
+ lastname: str | None
17
+ email: str | None
18
+ auth_id: str | None
19
+ user_groups: list[str]
20
+
21
+ @classmethod
22
+ def _create_from_dict_data(
23
+ cls, user_data: dict[str, Any], delimiter: str = ","
24
+ ) -> dict[str, Any]:
25
+ """Helper method to extract common data from dict."""
26
+ user_groups = cls.split(user_data["user_groups"], delimiter=delimiter)
27
+ return {
28
+ "user_id": user_data["user_id"],
29
+ "firstname": user_data["firstname"],
30
+ "lastname": user_data["lastname"],
31
+ "email": user_data["email"],
32
+ "auth_id": user_data["auth_id"],
33
+ "user_groups": user_groups,
34
+ }
35
+
36
+ @classmethod
37
+ def _create_from_sdk_data(cls, obj: CatalogUser) -> dict[str, Any]:
38
+ """Helper method to extract common data from SDK object."""
39
+ if obj.attributes:
40
+ firstname = obj.attributes.firstname
41
+ lastname = obj.attributes.lastname
42
+ email = obj.attributes.email
43
+ auth_id = obj.attributes.authentication_id
44
+ else:
45
+ firstname = None
46
+ lastname = None
47
+ email = None
48
+ auth_id = None
49
+
50
+ return {
51
+ "user_id": obj.id,
52
+ "firstname": firstname,
53
+ "lastname": lastname,
54
+ "email": email,
55
+ "auth_id": auth_id,
56
+ "user_groups": [ug.id for ug in obj.user_groups],
57
+ }
58
+
59
+ def to_sdk_obj(self) -> CatalogUser:
60
+ """Converts to CatalogUser SDK object."""
61
+ return CatalogUser.init(
62
+ user_id=self.user_id,
63
+ firstname=self.firstname,
64
+ lastname=self.lastname,
65
+ email=self.email,
66
+ authentication_id=self.auth_id,
67
+ user_group_ids=self.user_groups,
68
+ )
69
+
70
+
71
+ class UserIncrementalLoad(BaseUser):
72
+ """User model for incremental load operations with active status tracking."""
73
+
74
+ is_active: bool
75
+
76
+ @classmethod
77
+ def from_list_of_dicts(
78
+ cls, data: list[dict[str, Any]], delimiter: str = ","
79
+ ) -> list["UserIncrementalLoad"]:
80
+ """Creates a list of User objects from list of dicts."""
81
+ converted_users = []
82
+ for user in data:
83
+ base_data = cls._create_from_dict_data(user, delimiter)
84
+ base_data["is_active"] = user["is_active"]
85
+ converted_users.append(cls(**base_data))
86
+ return converted_users
87
+
88
+ @classmethod
89
+ def from_sdk_obj(cls, obj: CatalogUser) -> "UserIncrementalLoad":
90
+ """Creates GDUserTarget from CatalogUser SDK object."""
91
+ base_data = cls._create_from_sdk_data(obj)
92
+ base_data["is_active"] = True
93
+ return cls(**base_data)
94
+
95
+
96
+ class UserFullLoad(BaseUser):
97
+ """User model for full load operations."""
98
+
99
+ @classmethod
100
+ def from_list_of_dicts(
101
+ cls, data: list[dict[str, Any]], delimiter: str = ","
102
+ ) -> list["UserFullLoad"]:
103
+ """Creates a list of User objects from list of dicts."""
104
+ converted_users = []
105
+ for user in data:
106
+ base_data = cls._create_from_dict_data(user, delimiter)
107
+ converted_users.append(cls(**base_data))
108
+ return converted_users
109
+
110
+ @classmethod
111
+ def from_sdk_obj(cls, obj: CatalogUser) -> "UserFullLoad":
112
+ """Creates GDUserTarget from CatalogUser SDK object."""
113
+ base_data = cls._create_from_sdk_data(obj)
114
+ return cls(**base_data)
@@ -0,0 +1,153 @@
1
+ # (C) 2025 GoodData Corporation
2
+
3
+ """Module for provisioning user permissions in GoodData workspaces."""
4
+
5
+ from gooddata_pipelines.api.exceptions import GoodDataApiException
6
+ from gooddata_pipelines.provisioning.entities.users.models.permissions import (
7
+ PermissionDeclaration,
8
+ PermissionFullLoad,
9
+ PermissionIncrementalLoad,
10
+ PermissionType,
11
+ TargetsPermissionDict,
12
+ WSPermissionsDeclarations,
13
+ )
14
+ from gooddata_pipelines.provisioning.provisioning import Provisioning
15
+ from gooddata_pipelines.provisioning.utils.exceptions import BaseUserException
16
+
17
+
18
+ class PermissionProvisioner(
19
+ Provisioning[PermissionFullLoad, PermissionIncrementalLoad]
20
+ ):
21
+ """Provisioning class for user permissions in GoodData workspaces.
22
+
23
+ This class handles the provisioning of user permissions based on the provided
24
+ source data.
25
+ """
26
+
27
+ source_group_incremental: list[PermissionIncrementalLoad]
28
+ source_group_full: list[PermissionFullLoad]
29
+
30
+ def _get_ws_declaration(self, ws_id: str) -> PermissionDeclaration:
31
+ users: TargetsPermissionDict = {}
32
+ user_groups: TargetsPermissionDict = {}
33
+
34
+ upstream_declaration = self._api.get_declarative_permissions(ws_id)
35
+
36
+ for permission in upstream_declaration.permissions:
37
+ permission_type, id = (
38
+ permission.assignee.type,
39
+ permission.assignee.id,
40
+ )
41
+ target_dict = (
42
+ users
43
+ if permission_type == PermissionType.user.value
44
+ else user_groups
45
+ )
46
+
47
+ id_permissions = target_dict.get(id)
48
+ if not id_permissions:
49
+ target_dict[id] = dict()
50
+
51
+ target_dict[id][permission.name] = True
52
+
53
+ return PermissionDeclaration(users, user_groups)
54
+
55
+ def _get_upstream_declaration(
56
+ self, ws_id: str
57
+ ) -> PermissionDeclaration | None:
58
+ """Retrieves upstream permission declaration for a workspace."""
59
+ declaration = self._api.get_declarative_permissions(ws_id)
60
+ return PermissionDeclaration.from_sdk_api(declaration)
61
+
62
+ def _get_upstream_declarations(
63
+ self, input_ws_ids: list[str]
64
+ ) -> WSPermissionsDeclarations:
65
+ """Retrieves upstream permission declarations for a list of workspaces."""
66
+ ws_dict: WSPermissionsDeclarations = {}
67
+ for ws_id in input_ws_ids:
68
+ declaration = self._get_upstream_declaration(ws_id)
69
+ if declaration:
70
+ ws_dict[ws_id] = declaration
71
+ return ws_dict
72
+
73
+ @staticmethod
74
+ def _construct_declarations(
75
+ permissions: list[PermissionIncrementalLoad],
76
+ ) -> WSPermissionsDeclarations:
77
+ """Constructs workspace permission declarations from the input permissions."""
78
+ ws_dict: WSPermissionsDeclarations = {}
79
+ for permission in permissions:
80
+ ws_id = permission.workspace_id
81
+
82
+ if ws_id not in ws_dict:
83
+ ws_dict[ws_id] = PermissionDeclaration({}, {})
84
+
85
+ ws_dict[ws_id].add_permission(permission)
86
+ return ws_dict
87
+
88
+ def _check_user_group_exists(self, ug_id: str) -> None:
89
+ """Checks if user group with provided ID exists."""
90
+ self._api._sdk.catalog_user.get_user_group(ug_id)
91
+
92
+ def _validate_permission(
93
+ self, permission: PermissionIncrementalLoad
94
+ ) -> None:
95
+ """Validates if the permission is correctly defined."""
96
+ if permission.type == PermissionType.user:
97
+ self._api.get_user(permission.id, error_message="User not found")
98
+ else:
99
+ self._api.get_user_group(
100
+ permission.id, error_message="User group not found"
101
+ )
102
+
103
+ self._api.get_workspace(
104
+ permission.workspace_id, error_message="Workspace not found"
105
+ )
106
+
107
+ def _filter_invalid_permissions(
108
+ self, permissions: list[PermissionIncrementalLoad]
109
+ ) -> list[PermissionIncrementalLoad]:
110
+ """Filters out invalid permissions from the input list."""
111
+ valid_permissions: list[PermissionIncrementalLoad] = []
112
+ for permission in permissions:
113
+ try:
114
+ self._validate_permission(permission)
115
+ except (BaseUserException, GoodDataApiException) as e:
116
+ self.logger.error(
117
+ f"Skipping {permission}. Error: {e.error_message} "
118
+ + f"Context: {permission.__dict__}"
119
+ )
120
+ continue
121
+ valid_permissions.append(permission)
122
+ return valid_permissions
123
+
124
+ def _manage_permissions(
125
+ self, permissions: list[PermissionIncrementalLoad]
126
+ ) -> None:
127
+ """Manages permissions for a list of workspaces.
128
+ Modify upstream workspace declarations for each input workspace and skip non-existent ws_ids
129
+ """
130
+ valid_permissions = self._filter_invalid_permissions(permissions)
131
+
132
+ input_declarations = self._construct_declarations(valid_permissions)
133
+
134
+ input_ws_ids = list(input_declarations.keys())
135
+ upstream_declarations = self._get_upstream_declarations(input_ws_ids)
136
+
137
+ for ws_id, declaration in input_declarations.items():
138
+ if ws_id not in upstream_declarations:
139
+ continue
140
+
141
+ upstream_declarations[ws_id].upsert(declaration)
142
+
143
+ ws_permissions = upstream_declarations[ws_id].to_sdk_api()
144
+
145
+ self._api.put_declarative_permissions(ws_id, ws_permissions)
146
+ self.logger.info(f"Updated permissions for workspace {ws_id}")
147
+
148
+ def _provision_incremental_load(self) -> None:
149
+ """Provision permissions based on the source group."""
150
+ self._manage_permissions(self.source_group_incremental)
151
+
152
+ def _provision_full_load(self) -> None:
153
+ raise NotImplementedError("Not implemented yet.")