Skip to content

Commit 265cfe6

Browse files
Merge pull request #1130 from gooddata/snapshot-master-af45dfc7-to-rel/dev
[bot] Merge master/af45dfc7 into rel/dev
2 parents e2d6fb1 + af45dfc commit 265cfe6

26 files changed

+574
-566
lines changed

gooddata-pipelines/TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ A list of outstanding tasks, features, or technical debt to be addressed in this
1010

1111
- [ ] Integrate with GoodDataApiClient
1212
- [ ] Consider replacing the SdkMethods wrapper with direct calls to the SDK methods
13-
- [ ] Consider using orjson library instead of json
13+
- [ ] Consider using orjson library instead of json to load test data
1414
- [ ] Cleanup custom exceptions
1515
- [ ] Improve test coverage. Write missing unit tests for legacy code (e.g., user data filters)
1616

gooddata-pipelines/gooddata_pipelines/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
UserDataFilterProvisioner,
2020
)
2121
from .provisioning.entities.users.models.permissions import (
22+
EntityType,
2223
PermissionFullLoad,
2324
PermissionIncrementalLoad,
2425
)
@@ -33,7 +34,10 @@
3334
from .provisioning.entities.users.permissions import PermissionProvisioner
3435
from .provisioning.entities.users.user_groups import UserGroupProvisioner
3536
from .provisioning.entities.users.users import UserProvisioner
36-
from .provisioning.entities.workspaces.models import WorkspaceFullLoad
37+
from .provisioning.entities.workspaces.models import (
38+
WorkspaceFullLoad,
39+
WorkspaceIncrementalLoad,
40+
)
3741
from .provisioning.entities.workspaces.workspace import WorkspaceProvisioner
3842

3943
__all__ = [
@@ -52,8 +56,10 @@
5256
"UserGroupFullLoad",
5357
"UserProvisioner",
5458
"UserGroupProvisioner",
59+
"WorkspaceIncrementalLoad",
5560
"PermissionProvisioner",
5661
"UserDataFilterProvisioner",
5762
"UserDataFilterFullLoad",
63+
"EntityType",
5864
"__version__",
5965
]

gooddata-pipelines/gooddata_pipelines/api/gooddata_api.py

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@
77

88
import requests
99

10-
# TODO: Limit the use of "typing.Any". Improve readability by using either models
11-
# or typed dicts.
12-
1310
TIMEOUT = 60
1411
REQUEST_PAGE_SIZE = 250
1512
API_VERSION = "v1"
@@ -55,42 +52,6 @@ def _get_url(self, endpoint: str) -> str:
5552
"""
5653
return f"{self.base_url}{endpoint}"
5754

58-
def get_custom_application_setting(
59-
self, workspace_id: str, setting_id: str
60-
) -> requests.Response:
61-
"""Gets a custom application setting.
62-
63-
Args:
64-
workspace_id (str): The ID of the workspace.
65-
setting_id (str): The ID of the custom application setting.
66-
Returns:
67-
requests.Response: The response from the server containing the
68-
custom application setting.
69-
"""
70-
url = f"/entities/workspaces/{workspace_id}/customApplicationSettings/{setting_id}"
71-
return self._get(url)
72-
73-
def put_custom_application_setting(
74-
self, workspace_id: str, setting_id: str, data: dict[str, Any]
75-
) -> requests.Response:
76-
url = f"/entities/workspaces/{workspace_id}/customApplicationSettings/{setting_id}"
77-
return self._put(url, data, self.headers)
78-
79-
def post_custom_application_setting(
80-
self, workspace_id: str, data: dict[str, Any]
81-
) -> requests.Response:
82-
"""Creates a custom application setting for a given workspace.
83-
84-
Args:
85-
workspace_id (str): The ID of the workspace.
86-
data (dict[str, Any]): The data for the custom application setting.
87-
Returns:
88-
requests.Response: The response from the server containing the
89-
created custom application setting.
90-
"""
91-
url = f"/entities/workspaces/{workspace_id}/customApplicationSettings/"
92-
return self._post(url, data, self.headers)
93-
9455
def get_all_workspace_data_filters(
9556
self, workspace_id: str
9657
) -> requests.Response:
@@ -201,21 +162,6 @@ def delete_workspace_data_filter_setting(
201162
endpoint,
202163
)
203164

204-
def post_workspace_data_filter(
205-
self, workspace_id: str, data: dict[str, Any]
206-
) -> requests.Response:
207-
"""Creates a workspace data filter for a given workspace.
208-
209-
Args:
210-
workspace_id (str): The ID of the workspace.
211-
data (dict[str, Any]): The data for the workspace data filter.
212-
Returns:
213-
requests.Response: The response from the server containing the
214-
created workspace data filter.
215-
"""
216-
endpoint = f"/entities/workspaces/{workspace_id}/workspaceDataFilters"
217-
return self._post(endpoint, data, self.headers)
218-
219165
def get_user_data_filters(self, workspace_id: str) -> requests.Response:
220166
"""Gets the user data filters for a given workspace."""
221167
endpoint = f"/layout/workspaces/{workspace_id}/userDataFilters"

gooddata-pipelines/gooddata_pipelines/provisioning/entities/users/models/permissions.py

Lines changed: 23 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# (C) 2025 GoodData Corporation
2-
from abc import abstractmethod
2+
33
from enum import Enum
4-
from typing import Any, Iterator, TypeAlias, TypeVar
4+
from typing import Iterator, TypeAlias
55

66
import attrs
77
from gooddata_sdk.catalog.identifier import CatalogAssigneeIdentifier
@@ -14,85 +14,29 @@
1414
from gooddata_pipelines.provisioning.utils.exceptions import BaseUserException
1515

1616
TargetsPermissionDict: TypeAlias = dict[str, dict[str, bool]]
17-
ConstructorType = TypeVar("ConstructorType", bound="ConstructorMixin")
1817

1918

20-
class PermissionType(str, Enum):
19+
class EntityType(str, Enum):
2120
# NOTE: Start using StrEnum with Python 3.11
2221
user = "user"
2322
user_group = "userGroup"
2423

2524

26-
class ConstructorMixin:
27-
@staticmethod
28-
def _get_id_and_type(
29-
permission: dict[str, Any],
30-
) -> tuple[str, PermissionType]:
31-
user_id: str | None = permission.get("user_id")
32-
user_group_id: str | None = permission.get("ug_id")
33-
if user_id and user_group_id:
34-
raise ValueError("Only one of user_id or ug_id must be present")
35-
elif user_id:
36-
return user_id, PermissionType.user
37-
elif user_group_id:
38-
return user_group_id, PermissionType.user_group
39-
else:
40-
raise ValueError("Either user_id or ug_id must be present")
41-
42-
@classmethod
43-
def from_list_of_dicts(
44-
cls: type[ConstructorType], data: list[dict[str, Any]]
45-
) -> list[ConstructorType]:
46-
"""Creates a list of instances from list of dicts."""
47-
# NOTE: We can use typing.Self for the return type in Python 3.11
48-
permissions = []
49-
for permission in data:
50-
permissions.append(cls.from_dict(permission))
51-
return permissions
52-
53-
@classmethod
54-
@abstractmethod
55-
def from_dict(cls, data: dict[str, Any]) -> Any:
56-
"""Construction form a dictionary to be implemented by subclasses."""
57-
pass
58-
59-
60-
class PermissionIncrementalLoad(BaseModel, ConstructorMixin):
25+
class BasePermission(BaseModel):
6126
permission: str
6227
workspace_id: str
63-
id_: str
64-
type_: PermissionType
65-
is_active: bool
28+
entity_id: str
29+
entity_type: EntityType
6630

67-
@classmethod
68-
def from_dict(cls, data: dict[str, Any]) -> "PermissionIncrementalLoad":
69-
"""Returns an instance of PermissionIncrementalLoad from a dictionary."""
70-
id_, target_type = cls._get_id_and_type(data)
71-
return cls(
72-
permission=data["ws_permissions"],
73-
workspace_id=data["ws_id"],
74-
id_=id_,
75-
type_=target_type,
76-
is_active=data["is_active"],
77-
)
7831

32+
class PermissionFullLoad(BasePermission):
33+
"""Input validator for full load of workspace permissions provisioning."""
7934

80-
class PermissionFullLoad(BaseModel, ConstructorMixin):
81-
permission: str
82-
workspace_id: str
83-
id_: str
84-
type_: PermissionType
8535

86-
@classmethod
87-
def from_dict(cls, data: dict[str, Any]) -> "PermissionFullLoad":
88-
"""Returns an instance of PermissionFullLoad from a dictionary."""
89-
id_, target_type = cls._get_id_and_type(data)
90-
return cls(
91-
permission=data["ws_permissions"],
92-
workspace_id=data["ws_id"],
93-
id_=id_,
94-
type_=target_type,
95-
)
36+
class PermissionIncrementalLoad(BasePermission):
37+
"""Input validator for incremental load of workspace permissions provisioning."""
38+
39+
is_active: bool
9640

9741

9842
@attrs.define
@@ -117,7 +61,7 @@ def from_sdk_api(
11761
permission.assignee.id,
11862
)
11963

120-
if permission_type == PermissionType.user.value:
64+
if permission_type == EntityType.user.value:
12165
target_dict = users
12266
else:
12367
target_dict = user_groups
@@ -170,7 +114,7 @@ def to_sdk_api(self) -> CatalogDeclarativeWorkspacePermissions:
170114

171115
for user_id, permissions in self.users.items():
172116
assignee = CatalogAssigneeIdentifier(
173-
id=user_id, type=PermissionType.user.value
117+
id=user_id, type=EntityType.user.value
174118
)
175119
for declaration in self._permissions_for_target(
176120
permissions, assignee
@@ -179,7 +123,7 @@ def to_sdk_api(self) -> CatalogDeclarativeWorkspacePermissions:
179123

180124
for ug_id, permissions in self.user_groups.items():
181125
assignee = CatalogAssigneeIdentifier(
182-
id=ug_id, type=PermissionType.user_group.value
126+
id=ug_id, type=EntityType.user_group.value
183127
)
184128
for declaration in self._permissions_for_target(
185129
permissions, assignee
@@ -200,15 +144,15 @@ def add_incremental_permission(
200144
"""
201145
target_dict = (
202146
self.users
203-
if permission.type_ == PermissionType.user
147+
if permission.entity_type == EntityType.user
204148
else self.user_groups
205149
)
206150

207-
if permission.id_ not in target_dict:
208-
target_dict[permission.id_] = {}
151+
if permission.entity_id not in target_dict:
152+
target_dict[permission.entity_id] = {}
209153

210154
is_active = permission.is_active
211-
target_permissions = target_dict[permission.id_]
155+
target_permissions = target_dict[permission.entity_id]
212156
permission_value = permission.permission
213157

214158
if permission_value not in target_permissions:
@@ -233,14 +177,14 @@ def add_full_load_permission(self, permission: PermissionFullLoad) -> None:
233177
"""
234178
target_dict = (
235179
self.users
236-
if permission.type_ == PermissionType.user
180+
if permission.entity_type == EntityType.user
237181
else self.user_groups
238182
)
239183

240-
if permission.id_ not in target_dict:
241-
target_dict[permission.id_] = {}
184+
if permission.entity_id not in target_dict:
185+
target_dict[permission.entity_id] = {}
242186

243-
target_permissions = target_dict[permission.id_]
187+
target_permissions = target_dict[permission.entity_id]
244188
permission_value = permission.permission
245189

246190
if permission_value not in target_permissions:
Lines changed: 23 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,37 @@
11
# (C) 2025 GoodData Corporation
22

3-
from typing import Any
3+
from pydantic import BaseModel, Field, ValidationInfo, field_validator
44

5-
from pydantic import BaseModel
65

7-
from gooddata_pipelines.provisioning.utils.utils import SplitMixin
8-
9-
10-
class BaseUserGroup(BaseModel, SplitMixin):
6+
class UserGroupBase(BaseModel):
117
user_group_id: str
128
user_group_name: str
13-
parent_user_groups: list[str]
9+
parent_user_groups: list[str] = Field(default_factory=list)
1410

11+
@field_validator("user_group_name", mode="before")
1512
@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-
13+
def validate_user_group_name(
14+
cls, v: str | None, info: ValidationInfo
15+
) -> str:
16+
"""If user_group_name is None or empty, default to user_group_id."""
17+
if not v: # handles None and empty string
18+
return info.data.get("user_group_id", "")
19+
return v
20+
21+
@field_validator("parent_user_groups", mode="before")
3722
@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"]
23+
def validate_parent_user_groups(cls, v: list[str] | None) -> list[str]:
24+
"""If parent_user_groups is None or empty, default to empty list."""
25+
if not v:
26+
return []
27+
return v
4628

47-
user_groups.append(UserGroupIncrementalLoad(**base_data))
4829

49-
return user_groups
30+
class UserGroupFullLoad(UserGroupBase):
31+
"""Input validator for full load of user group provisioning."""
5032

5133

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)
34+
class UserGroupIncrementalLoad(UserGroupBase):
35+
"""Input validator for incremental load of user group provisioning."""
6136

62-
user_groups.append(UserGroupFullLoad(**base_data))
63-
64-
return user_groups
37+
is_active: bool

0 commit comments

Comments
 (0)