1717import typing
1818import warnings
1919
20- from .common import ClientError , LoginError , InvalidProject , ErrorCode
20+ from typing import List
21+
22+ from .common import ClientError , LoginError , WorkspaceRole , ProjectRole
2123from .merginproject import MerginProject
2224from .client_pull import (
2325 download_file_finalize ,
3638from .version import __version__
3739
3840this_dir = os .path .dirname (os .path .realpath (__file__ ))
41+ json_headers = {"Content-Type" : "application/json" }
3942
4043
4144class TokenError (Exception ):
@@ -207,9 +210,23 @@ def _do_request(self, request):
207210 except urllib .error .HTTPError as e :
208211 server_response = json .load (e )
209212
210- # We first to try to get the value from the response otherwise we set a default value
211- err_detail = server_response .get ("detail" , e .read ().decode ("utf-8" ))
212- server_code = server_response .get ("code" , None )
213+ err_detail = None
214+ server_code = None
215+ # Try to get error detail
216+ if isinstance (server_response , dict ):
217+ server_code = server_response .get ("code" )
218+ err_detail = server_response .get ("detail" )
219+ if not err_detail :
220+ # Extract all field-specific errors and format them
221+ err_detail = "\n " .join (
222+ f"{ key } : { ', ' .join (map (str , value ))} "
223+ for key , value in server_response .items ()
224+ if isinstance (value , list )
225+ ) or str (
226+ server_response
227+ ) # Fallback to raw response if structure is unexpected
228+ else :
229+ err_detail = str (server_response )
213230
214231 raise ClientError (
215232 detail = err_detail ,
@@ -244,6 +261,11 @@ def patch(self, path, data=None, headers={}):
244261 request = urllib .request .Request (url , data , headers , method = "PATCH" )
245262 return self ._do_request (request )
246263
264+ def delete (self , path ):
265+ url = urllib .parse .urljoin (self .url , urllib .parse .quote (path ))
266+ request = urllib .request .Request (url , method = "DELETE" )
267+ return self ._do_request (request )
268+
247269 def login (self , login , password ):
248270 """
249271 Authenticate login credentials and store session token
@@ -796,6 +818,12 @@ def add_user_permissions_to_project(self, project_path, usernames, permission_le
796818 if permission_level in ("writer" , "owner" , "editor" , "reader" ):
797819 access .get ("readersnames" ).append (name )
798820 self .set_project_access (project_path , access )
821+ warnings .warn (
822+ "This method will be deprecated in the next major release (1.0.0)"
823+ "Use `add_project_collaborator` to create a project permission and "
824+ "`update_project_collaborator` to change it instead." ,
825+ category = DeprecationWarning ,
826+ )
799827
800828 def remove_user_permissions_from_project (self , project_path , usernames ):
801829 """
@@ -815,6 +843,11 @@ def remove_user_permissions_from_project(self, project_path, usernames):
815843 if name in access .get ("readersnames" , []):
816844 access .get ("readersnames" ).remove (name )
817845 self .set_project_access (project_path , access )
846+ warnings .warn (
847+ "This method will be deprecated in the next major release (1.0.0)"
848+ "Use `remove_project_collaborator` instead." ,
849+ category = DeprecationWarning ,
850+ )
818851
819852 def project_user_permissions (self , project_path ):
820853 """
@@ -1228,3 +1261,102 @@ def has_editor_support(self):
12281261 Returns whether the server version is acceptable for editor support.
12291262 """
12301263 return is_version_acceptable (self .server_version (), "2024.4.0" )
1264+
1265+ def create_user (
1266+ self ,
1267+ email : str ,
1268+ password : str ,
1269+ workspace_id : int ,
1270+ workspace_role : WorkspaceRole ,
1271+ username : str = None ,
1272+ notify_user : bool = False ,
1273+ ) -> dict :
1274+ """
1275+ Create a new user in a workspace. The username is generated from the email address.
1276+
1277+ param email: email of the new user - must be unique
1278+ param password: password - must meet the requirements
1279+ param workspace_id: id of the workspace user is created in
1280+ param workspace_role: workspace role of the user
1281+ param username: username - will be autogenerated from the email if not provided
1282+ param notify_user: flag for email notifications - confirmation email will be sent
1283+ """
1284+ params = {
1285+ "email" : email ,
1286+ "password" : password ,
1287+ "workspace_id" : workspace_id ,
1288+ "role" : workspace_role .value ,
1289+ "notify_user" : notify_user ,
1290+ }
1291+ if username :
1292+ params ["username" ] = username
1293+ user_info = self .post ("v2/users" , params , json_headers )
1294+ return json .load (user_info )
1295+
1296+ def get_workspace_member (self , workspace_id : int , user_id : int ) -> dict :
1297+ """
1298+ Get a workspace member detail
1299+ """
1300+ resp = self .get (f"v2/workspaces/{ workspace_id } /members/{ user_id } " )
1301+ return json .load (resp )
1302+
1303+ def list_workspace_members (self , workspace_id : int ) -> List [dict ]:
1304+ """
1305+ Get a list of workspace members
1306+ """
1307+ resp = self .get (f"v2/workspaces/{ workspace_id } /members" )
1308+ return json .load (resp )
1309+
1310+ def update_workspace_member (
1311+ self , workspace_id : int , user_id : int , workspace_role : WorkspaceRole , reset_projects_roles : bool = False
1312+ ) -> dict :
1313+ """
1314+ Update workspace role of a workspace member, optionally resets the projects role
1315+
1316+ param reset_projects_roles: all project specific roles will be removed
1317+ """
1318+ params = {
1319+ "reset_projects_roles" : reset_projects_roles ,
1320+ "workspace_role" : workspace_role .value ,
1321+ }
1322+ workspace_member = self .patch (f"v2/workspaces/{ workspace_id } /members/{ user_id } " , params , json_headers )
1323+ return json .load (workspace_member )
1324+
1325+ def remove_workspace_member (self , workspace_id : int , user_id : int ):
1326+ """
1327+ Remove a user from workspace members
1328+ """
1329+ self .delete (f"v2/workspaces/{ workspace_id } /members/{ user_id } " )
1330+
1331+ def list_project_collaborators (self , project_id : int ) -> List [dict ]:
1332+ """
1333+ Get a list of project collaborators
1334+ """
1335+ project_collaborators = self .get (f"v2/projects/{ project_id } /collaborators" )
1336+ return json .load (project_collaborators )
1337+
1338+ def add_project_collaborator (self , project_id : int , user : str , project_role : ProjectRole ) -> dict :
1339+ """
1340+ Add a user to project collaborators and grant them a project role.
1341+ Fails if user is already a member of the project.
1342+
1343+ param user: login (username or email) of the user
1344+ """
1345+ params = {"role" : project_role .value , "user" : user }
1346+ project_collaborator = self .post (f"v2/projects/{ project_id } /collaborators" , params , json_headers )
1347+ return json .load (project_collaborator )
1348+
1349+ def update_project_collaborator (self , project_id : int , user_id : int , project_role : ProjectRole ) -> dict :
1350+ """
1351+ Update project role of the existing project collaborator.
1352+ Fails if user is not a member of the project yet.
1353+ """
1354+ params = {"role" : project_role .value }
1355+ project_collaborator = self .patch (f"v2/projects/{ project_id } /collaborators/{ user_id } " , params , json_headers )
1356+ return json .load (project_collaborator )
1357+
1358+ def remove_project_collaborator (self , project_id : int , user_id : int ):
1359+ """
1360+ Remove a user from project collaborators
1361+ """
1362+ self .delete (f"v2/projects/{ project_id } /collaborators/{ user_id } " )
0 commit comments