11# (C) 2025 GoodData Corporation
2- from dataclasses import dataclass
2+ from abc import abstractmethod
33from enum import Enum
4- from typing import Any , Iterator , TypeAlias
4+ from typing import Any , Iterator , TypeAlias , TypeVar
55
6+ import attrs
67from gooddata_sdk .catalog .identifier import CatalogAssigneeIdentifier
78from gooddata_sdk .catalog .permission .declarative_model .permission import (
89 CatalogDeclarativeSingleWorkspacePermission ,
910 CatalogDeclarativeWorkspacePermissions ,
1011)
12+ from pydantic import BaseModel
1113
1214from gooddata_pipelines .provisioning .utils .exceptions import BaseUserException
1315
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-
1816TargetsPermissionDict : TypeAlias = dict [str , dict [str , bool ]]
17+ ConstructorType = TypeVar ("ConstructorType" , bound = "ConstructorMixin" )
1918
2019
21- class PermissionType (Enum ):
20+ class PermissionType (str , Enum ):
21+ # NOTE: Start using StrEnum with Python 3.11
2222 user = "user"
2323 user_group = "userGroup"
2424
2525
26- @dataclass (frozen = True )
27- class PermissionIncrementalLoad :
28- permission : str
29- workspace_id : str
30- id : str
31- type : PermissionType
32- is_active : bool
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" )
3341
3442 @classmethod
3543 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
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
4048 permissions = []
4149 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- )
50+ permissions .append (cls .from_dict (permission ))
6151 return permissions
6252
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+
6359
64- @dataclass (frozen = True )
65- class PermissionFullLoad :
60+ class PermissionIncrementalLoad (BaseModel , ConstructorMixin ):
6661 permission : str
6762 workspace_id : str
68- id : str
69- type : PermissionType
63+ id_ : str
64+ type_ : PermissionType
65+ is_active : bool
7066
7167 @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- )
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+ )
8378
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
9879
80+ class PermissionFullLoad (BaseModel , ConstructorMixin ):
81+ permission : str
82+ workspace_id : str
83+ id_ : str
84+ type_ : PermissionType
85+
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+ )
9996
100- @dataclass
97+
98+ @attrs .define
10199class PermissionDeclaration :
102100 users : TargetsPermissionDict
103101 user_groups : TargetsPermissionDict
@@ -192,23 +190,25 @@ def to_sdk_api(self) -> CatalogDeclarativeWorkspacePermissions:
192190 permissions = permission_declarations
193191 )
194192
195- def add_permission (self , permission : PermissionIncrementalLoad ) -> None :
193+ def add_incremental_permission (
194+ self , permission : PermissionIncrementalLoad
195+ ) -> None :
196196 """
197197 Adds WSPermission object into respective field within the instance.
198198 Handles duplicate permissions and different combinations of input
199199 and upstream is_active permission states.
200200 """
201201 target_dict = (
202202 self .users
203- if permission .type == PermissionType .user
203+ if permission .type_ == PermissionType .user
204204 else self .user_groups
205205 )
206206
207- if permission .id not in target_dict :
208- target_dict [permission .id ] = {}
207+ if permission .id_ not in target_dict :
208+ target_dict [permission .id_ ] = {}
209209
210210 is_active = permission .is_active
211- target_permissions = target_dict [permission .id ]
211+ target_permissions = target_dict [permission .id_ ]
212212 permission_value = permission .permission
213213
214214 if permission_value not in target_permissions :
@@ -225,6 +225,27 @@ def add_permission(self, permission: PermissionIncrementalLoad) -> None:
225225 )
226226 target_permissions [permission_value ] = is_active
227227
228+ def add_full_load_permission (self , permission : PermissionFullLoad ) -> None :
229+ """
230+ Adds WSPermission object into respective field within the instance.
231+ Handles duplicate permissions and different combinations of input
232+ and upstream is_active permission states.
233+ """
234+ target_dict = (
235+ self .users
236+ if permission .type_ == PermissionType .user
237+ else self .user_groups
238+ )
239+
240+ if permission .id_ not in target_dict :
241+ target_dict [permission .id_ ] = {}
242+
243+ target_permissions = target_dict [permission .id_ ]
244+ permission_value = permission .permission
245+
246+ if permission_value not in target_permissions :
247+ target_permissions [permission_value ] = True
248+
228249 def upsert (self , other : "PermissionDeclaration" ) -> None :
229250 """
230251 Modifies the owner object by merging with the other.
0 commit comments