Skip to content

Commit 70a75b5

Browse files
authored
Merge pull request #284 from MerginMaps/roles_as_string
introduce helper to normalize workspace/project roles from strings
2 parents 843133c + 43ef278 commit 70a75b5

File tree

4 files changed

+89
-11
lines changed

4 files changed

+89
-11
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ htmlcov
1212
.pytest_cache
1313
deps
1414
venv
15-
.vscode/settings.json
15+
.vscode/

mergin/client.py

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import re
1717
import typing
1818
import warnings
19+
from enum import Enum
20+
from typing import Optional, Type, Union
1921

2022
from .common import (
2123
ClientError,
@@ -39,7 +41,13 @@
3941
)
4042
from .client_pull import pull_project_async, pull_project_wait, pull_project_finalize
4143
from .client_push import push_project_async, push_project_wait, push_project_finalize
42-
from .utils import DateTimeEncoder, get_versions_with_file_changes, int_version, is_version_acceptable
44+
from .utils import (
45+
DateTimeEncoder,
46+
get_versions_with_file_changes,
47+
int_version,
48+
is_version_acceptable,
49+
normalize_role,
50+
)
4351
from .version import __version__
4452

4553
this_dir = os.path.dirname(os.path.realpath(__file__))
@@ -1313,7 +1321,7 @@ def create_user(
13131321
email: str,
13141322
password: str,
13151323
workspace_id: int,
1316-
workspace_role: WorkspaceRole,
1324+
workspace_role: Union[str, WorkspaceRole],
13171325
username: str = None,
13181326
notify_user: bool = False,
13191327
) -> dict:
@@ -1328,11 +1336,15 @@ def create_user(
13281336
param notify_user: flag for email notifications - confirmation email will be sent
13291337
"""
13301338
self.check_collaborators_members_support()
1339+
role_enum = normalize_role(workspace_role, WorkspaceRole)
1340+
if role_enum is None:
1341+
raise ValueError(f"Invalid role: {workspace_role}")
1342+
13311343
params = {
13321344
"email": email,
13331345
"password": password,
13341346
"workspace_id": workspace_id,
1335-
"role": workspace_role.value,
1347+
"role": role_enum.value,
13361348
"notify_user": notify_user,
13371349
}
13381350
if username:
@@ -1357,17 +1369,26 @@ def list_workspace_members(self, workspace_id: int) -> typing.List[dict]:
13571369
return json.load(resp)
13581370

13591371
def update_workspace_member(
1360-
self, workspace_id: int, user_id: int, workspace_role: WorkspaceRole, reset_projects_roles: bool = False
1372+
self,
1373+
workspace_id: int,
1374+
user_id: int,
1375+
workspace_role: Union[str, WorkspaceRole],
1376+
reset_projects_roles: bool = False,
13611377
) -> dict:
13621378
"""
13631379
Update workspace role of a workspace member, optionally resets the projects role
13641380
13651381
param reset_projects_roles: all project specific roles will be removed
13661382
"""
13671383
self.check_collaborators_members_support()
1384+
1385+
role_enum = normalize_role(workspace_role, WorkspaceRole)
1386+
if role_enum is None:
1387+
raise ValueError(f"Invalid role: {workspace_role}")
1388+
13681389
params = {
13691390
"reset_projects_roles": reset_projects_roles,
1370-
"workspace_role": workspace_role.value,
1391+
"workspace_role": role_enum.value,
13711392
}
13721393
workspace_member = self.patch(f"v2/workspaces/{workspace_id}/members/{user_id}", params, json_headers)
13731394
return json.load(workspace_member)
@@ -1387,25 +1408,35 @@ def list_project_collaborators(self, project_id: str) -> typing.List[dict]:
13871408
project_collaborators = self.get(f"v2/projects/{project_id}/collaborators")
13881409
return json.load(project_collaborators)
13891410

1390-
def add_project_collaborator(self, project_id: str, user: str, project_role: ProjectRole) -> dict:
1411+
def add_project_collaborator(self, project_id: str, user: str, project_role: Union[str, ProjectRole]) -> dict:
13911412
"""
13921413
Add a user to project collaborators and grant them a project role.
13931414
Fails if user is already a member of the project.
13941415
13951416
param user: login (username or email) of the user
13961417
"""
13971418
self.check_collaborators_members_support()
1419+
1420+
role_enum = normalize_role(project_role, ProjectRole)
1421+
if role_enum is None:
1422+
raise ValueError(f"Invalid role: {project_role}")
1423+
13981424
params = {"role": project_role.value, "user": user}
13991425
project_collaborator = self.post(f"v2/projects/{project_id}/collaborators", params, json_headers)
14001426
return json.load(project_collaborator)
14011427

1402-
def update_project_collaborator(self, project_id: str, user_id: int, project_role: ProjectRole) -> dict:
1428+
def update_project_collaborator(self, project_id: str, user_id: int, project_role: Union[str, ProjectRole]) -> dict:
14031429
"""
14041430
Update project role of the existing project collaborator.
14051431
Fails if user is not a member of the project yet.
14061432
"""
14071433
self.check_collaborators_members_support()
1434+
1435+
role_enum = normalize_role(project_role, ProjectRole)
1436+
if role_enum is None:
1437+
raise ValueError(f"Invalid role: {project_role}")
14081438
params = {"role": project_role.value}
1439+
14091440
project_collaborator = self.patch(f"v2/projects/{project_id}/collaborators/{user_id}", params, json_headers)
14101441
return json.load(project_collaborator)
14111442

@@ -1481,13 +1512,18 @@ def send_logs(
14811512
request = urllib.request.Request(url, data=payload, headers=header)
14821513
return self._do_request(request)
14831514

1484-
def create_invitation(self, workspace_id: int, email: str, workspace_role: WorkspaceRole):
1515+
def create_invitation(self, workspace_id: int, email: str, workspace_role: Union[str, WorkspaceRole]):
14851516
"""
14861517
Create invitation to workspace for specific role
14871518
"""
14881519
min_version = "2025.6.1"
14891520
if not is_version_acceptable(self.server_version(), min_version):
14901521
raise NotImplementedError(f"This needs server at version {min_version} or later")
1491-
params = {"email": email, "role": workspace_role.value}
1522+
1523+
role_enum = normalize_role(workspace_role, WorkspaceRole)
1524+
if role_enum is None:
1525+
raise ValueError(f"Invalid role: {workspace_role}")
1526+
1527+
params = {"email": email, "role": role_enum.value}
14921528
ws_inv = self.post(f"v2/workspaces/{workspace_id}/invitations", params, json_headers)
14931529
return json.load(ws_inv)

mergin/test/test_client.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
unique_path_name,
3939
conflicted_copy_file_name,
4040
edit_conflict_file_name,
41+
normalize_role,
4142
)
4243
from ..merginproject import pygeodiff
4344
from ..report import create_report
@@ -3026,3 +3027,25 @@ def test_server_type(mc):
30263027
mock_client_get.side_effect = ClientError(detail="Service unavailable", http_error=503)
30273028
with pytest.raises(ClientError, match="Service unavailable"):
30283029
mc.server_type()
3030+
3031+
3032+
@pytest.mark.parametrize(
3033+
"value, role_enum, expected",
3034+
[
3035+
("guest", WorkspaceRole, WorkspaceRole.GUEST),
3036+
(" GuEsT ", WorkspaceRole, WorkspaceRole.GUEST),
3037+
("writer", ProjectRole, ProjectRole.WRITER),
3038+
(" WRITER ", ProjectRole, ProjectRole.WRITER),
3039+
("guuuest", WorkspaceRole, None),
3040+
("ownerr", ProjectRole, None),
3041+
("", WorkspaceRole, None),
3042+
(None, WorkspaceRole, None),
3043+
(123, WorkspaceRole, None),
3044+
],
3045+
)
3046+
def test_normalize_role_parametrized(value, role_enum, expected):
3047+
result = normalize_role(value, role_enum)
3048+
if expected is None:
3049+
assert result is None
3050+
else:
3051+
assert result == expected

mergin/utils.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
from datetime import datetime
88
from pathlib import Path
99
import tempfile
10-
from .common import ClientError
10+
from enum import Enum
11+
from typing import Optional, Type, Union
12+
from .common import ClientError, WorkspaceRole
1113

1214

1315
def generate_checksum(file, chunk_size=4096):
@@ -309,3 +311,20 @@ def cleanup_tmp_dir(mp, tmp_dir: tempfile.TemporaryDirectory):
309311
mp.log.warning(f"Permission error during tmp dir cleanup: {tmp_dir.name}")
310312
except Exception as e:
311313
mp.log.error(f"Error during tmp dir cleanup: {tmp_dir.name}: {e}")
314+
315+
316+
def normalize_role(role: Union[str, Enum], enum_cls: Type[Enum]) -> Optional[Enum]:
317+
"""
318+
Takes a role as a string or an Enum member and returns the corresponding Enum member
319+
from the given enum class. Returns None if the input is invalid or no match is found.
320+
"""
321+
if isinstance(role, enum_cls):
322+
return role
323+
324+
if isinstance(role, str):
325+
try:
326+
return enum_cls(role.strip().lower())
327+
except ValueError:
328+
return None
329+
330+
return None

0 commit comments

Comments
 (0)