From 7671e82591c707461746996a735462970c901c85 Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Sun, 20 Jul 2025 23:19:21 +0200 Subject: [PATCH 1/6] Introduce create_invitation method --- mergin/client.py | 8 ++++++++ mergin/test/test_client.py | 18 +++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/mergin/client.py b/mergin/client.py index 6456c1bd..369f6369 100644 --- a/mergin/client.py +++ b/mergin/client.py @@ -1444,3 +1444,11 @@ def send_logs( else: request = urllib.request.Request(url, data=payload, headers=header) return self._do_request(request) + + def create_invitation(self, workspace_id: int, email: str, workspace_role: WorkspaceRole): + """ + Create invitation to workspace for specific role + """ + params = {"email": email, "role": workspace_role.value} + ws_inv = self.post(f"v2/workspaces/{workspace_id}/invitations", params, json_headers) + return json.load(ws_inv) diff --git a/mergin/test/test_client.py b/mergin/test/test_client.py index b5de8a6e..0d3b8d0c 100644 --- a/mergin/test/test_client.py +++ b/mergin/test/test_client.py @@ -20,7 +20,6 @@ decode_token_data, TokenError, ServerType, - WorkspaceRole, ) from ..client_push import push_project_async, push_project_cancel from ..client_pull import ( @@ -2911,3 +2910,20 @@ def test_do_request_error_handling(mc: MerginClient): assert e.value.http_error == 400 assert "Passwords must be at least 8 characters long." in e.value.detail + + +def test_creat_invitation(mc: MerginClient): + """Test client method to create workspace invitation""" + workspace_id = next((w["id"] for w in mc.workspaces_list() if w["name"] == mc.username())) + role = WorkspaceRole.WRITER + email = "invitation@client.py" + inv = mc.create_invitation(workspace_id, email, role) + assert inv["email"] == email + assert inv["role"] == role.value + mc.delete(f"v1/workspace/invitation/{inv['id']}") # resolves invitation to allow another invitation to the email + role = WorkspaceRole.GUEST + inv = mc.create_invitation(workspace_id, email, role) + assert inv["email"] == email + assert "projects" not in inv + mc.delete(f"v1/workspace/invitation/{inv['id']}") + From 3c08328857d54bbc5770aba6858f25306f8c5ccb Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Sun, 20 Jul 2025 23:21:10 +0200 Subject: [PATCH 2/6] black --- mergin/test/test_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mergin/test/test_client.py b/mergin/test/test_client.py index 0d3b8d0c..b9d009ed 100644 --- a/mergin/test/test_client.py +++ b/mergin/test/test_client.py @@ -2926,4 +2926,3 @@ def test_creat_invitation(mc: MerginClient): assert inv["email"] == email assert "projects" not in inv mc.delete(f"v1/workspace/invitation/{inv['id']}") - From 534f55a7189a6f28acf5e38860e2c91d00cbd787 Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Thu, 24 Jul 2025 12:59:59 +0200 Subject: [PATCH 3/6] Add compatibility check --- mergin/client.py | 3 +++ mergin/test/test_client.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/mergin/client.py b/mergin/client.py index 369f6369..5b883cc7 100644 --- a/mergin/client.py +++ b/mergin/client.py @@ -1449,6 +1449,9 @@ def create_invitation(self, workspace_id: int, email: str, workspace_role: Works """ Create invitation to workspace for specific role """ + min_version = "2025.6.1" + if not is_version_acceptable(self.server_version(), min_version): + raise NotImplementedError(f"This needs server at version {min_version} or later") params = {"email": email, "role": workspace_role.value} ws_inv = self.post(f"v2/workspaces/{workspace_id}/invitations", params, json_headers) return json.load(ws_inv) diff --git a/mergin/test/test_client.py b/mergin/test/test_client.py index b9d009ed..6d50dd07 100644 --- a/mergin/test/test_client.py +++ b/mergin/test/test_client.py @@ -2912,7 +2912,7 @@ def test_do_request_error_handling(mc: MerginClient): assert "Passwords must be at least 8 characters long." in e.value.detail -def test_creat_invitation(mc: MerginClient): +def test_create_invitation(mc: MerginClient): """Test client method to create workspace invitation""" workspace_id = next((w["id"] for w in mc.workspaces_list() if w["name"] == mc.username())) role = WorkspaceRole.WRITER From 56811a3cdd209d0d6c8a41d4ab3c449f25b3bbd2 Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Thu, 24 Jul 2025 13:26:14 +0200 Subject: [PATCH 4/6] Add check for project collaborators and workspace members compatibility --- mergin/client.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/mergin/client.py b/mergin/client.py index 5b883cc7..0a2155c3 100644 --- a/mergin/client.py +++ b/mergin/client.py @@ -1285,6 +1285,15 @@ def has_editor_support(self): """ return is_version_acceptable(self.server_version(), "2024.4.0") + def check_collaborators_members_support(self): + """ + Check if the server is compatible with v2 endpoints for project collaborators and workspace members + https://github.com/MerginMaps/server-private/releases/tag/2025.1.0 + """ + min_version = "2025.1.0" + if not is_version_acceptable(self.server_version(), f"{min_version}"): + raise NotImplementedError(f"This needs server at version {min_version} or later") + def create_user( self, email: str, @@ -1304,6 +1313,7 @@ def create_user( param username: username - will be autogenerated from the email if not provided param notify_user: flag for email notifications - confirmation email will be sent """ + self.check_collaborators_members_support() params = { "email": email, "password": password, @@ -1320,6 +1330,7 @@ def get_workspace_member(self, workspace_id: int, user_id: int) -> dict: """ Get a workspace member detail """ + self.check_collaborators_members_support() resp = self.get(f"v2/workspaces/{workspace_id}/members/{user_id}") return json.load(resp) @@ -1327,6 +1338,7 @@ def list_workspace_members(self, workspace_id: int) -> List[dict]: """ Get a list of workspace members """ + self.check_collaborators_members_support() resp = self.get(f"v2/workspaces/{workspace_id}/members") return json.load(resp) @@ -1338,6 +1350,7 @@ def update_workspace_member( param reset_projects_roles: all project specific roles will be removed """ + self.check_collaborators_members_support() params = { "reset_projects_roles": reset_projects_roles, "workspace_role": workspace_role.value, @@ -1349,12 +1362,14 @@ def remove_workspace_member(self, workspace_id: int, user_id: int): """ Remove a user from workspace members """ + self.check_collaborators_members_support() self.delete(f"v2/workspaces/{workspace_id}/members/{user_id}") def list_project_collaborators(self, project_id: str) -> List[dict]: """ Get a list of project collaborators """ + self.check_collaborators_members_support() project_collaborators = self.get(f"v2/projects/{project_id}/collaborators") return json.load(project_collaborators) @@ -1365,6 +1380,7 @@ def add_project_collaborator(self, project_id: str, user: str, project_role: Pro param user: login (username or email) of the user """ + self.check_collaborators_members_support() params = {"role": project_role.value, "user": user} project_collaborator = self.post(f"v2/projects/{project_id}/collaborators", params, json_headers) return json.load(project_collaborator) @@ -1374,6 +1390,7 @@ def update_project_collaborator(self, project_id: str, user_id: int, project_rol Update project role of the existing project collaborator. Fails if user is not a member of the project yet. """ + self.check_collaborators_members_support() params = {"role": project_role.value} project_collaborator = self.patch(f"v2/projects/{project_id}/collaborators/{user_id}", params, json_headers) return json.load(project_collaborator) @@ -1382,6 +1399,7 @@ def remove_project_collaborator(self, project_id: str, user_id: int): """ Remove a user from project collaborators """ + self.check_collaborators_members_support() self.delete(f"v2/projects/{project_id}/collaborators/{user_id}") def server_config(self) -> dict: From bf0f30e3cd60e2d0751f040de865808b4f50d008 Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Thu, 24 Jul 2025 13:29:06 +0200 Subject: [PATCH 5/6] Rm private repo link --- mergin/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mergin/client.py b/mergin/client.py index 0a2155c3..32ad51bc 100644 --- a/mergin/client.py +++ b/mergin/client.py @@ -1288,7 +1288,6 @@ def has_editor_support(self): def check_collaborators_members_support(self): """ Check if the server is compatible with v2 endpoints for project collaborators and workspace members - https://github.com/MerginMaps/server-private/releases/tag/2025.1.0 """ min_version = "2025.1.0" if not is_version_acceptable(self.server_version(), f"{min_version}"): From fd95eb7861f6b79c3e07264e70f6f97d674156fe Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Mon, 28 Jul 2025 09:37:51 +0200 Subject: [PATCH 6/6] black --- mergin/test/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mergin/test/test_client.py b/mergin/test/test_client.py index 3850c56b..32bc192f 100644 --- a/mergin/test/test_client.py +++ b/mergin/test/test_client.py @@ -2927,7 +2927,7 @@ def test_create_invitation(mc: MerginClient): assert "projects" not in inv mc.delete(f"v1/workspace/invitation/{inv['id']}") - + def test_validate_auth(mc: MerginClient): """Test validate authentication under different scenarios."""