From 5ee5a9842b7c9ebe3e8c45acacf21a599adac31c Mon Sep 17 00:00:00 2001 From: Daisuke Miyazoe Date: Mon, 15 Jun 2026 12:53:28 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=E3=83=AF=E3=83=BC=E3=82=AF=E3=82=B9?= =?UTF-8?q?=E3=83=9A=E3=83=BC=E3=82=B9=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC?= =?UTF-8?q?API=E5=85=AC=E9=96=8B=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 75 ++++++++++++++++ fastlabel/__init__.py | 81 +++++++++++++++++ fastlabel/exceptions.py | 6 +- tests/test_workspace_user.py | 167 +++++++++++++++++++++++++++++++++++ 4 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 tests/test_workspace_user.py diff --git a/README.md b/README.md index 4625b2c..043d025 100644 --- a/README.md +++ b/README.md @@ -4134,6 +4134,81 @@ client.create_model_monitoring_request_results( ) ``` +## Workspace User + +### Get workspace users + +Returns a list of internal workspace users. (Up to 20 at a time by default) +Each user includes its granted module permissions in `functionResourcePermissions`. + +```python +import fastlabel +client = fastlabel.Client() + +users = client.get_workspace_users( + keyword="", # Search keyword for name or email (Optional) + offset=0, # The starting position number to fetch (Optional) + limit=20, # The max number to fetch (Optional, default 20) +) +# [ +# { +# "id": "...", +# "userId": "...", +# "userSlug": "...", +# "userName": "John Doe", +# "userEmail": "john@example.com", +# "role": "member", +# "isExternal": False, +# "createdAt": "...", +# "updatedAt": "...", +# "functionResourcePermissions": { +# "annotation": True, +# "modelDev": False, +# "dataset": False +# } +# } +# ] +``` + +### Create workspace user + +Creates an internal workspace user. The `slug` is generated automatically on the server side. +Pass `modules` to grant module permissions on invitation. + +```python +user = client.create_workspace_user( + name="John Doe", + email="john@example.com", + language="en", # 'en' or 'ja' + role="member", # 'member' or 'owner' + modules=["annotation", "dataset"], # Optional. Any of 'annotation', 'modelDev', 'dataset' +) +``` + +### Update workspace user + +Updates an internal workspace user. The `role` can be changed, and `modules` +syncs the user's module permissions to the given set. + +```python +user = client.update_workspace_user( + user_id="YOUR_WORKSPACE_USER_ID", + role="owner", # 'member' or 'owner' (Optional) + # modules omitted -> module permissions are left unchanged. + # modules=["annotation"] -> sync to exactly this set. + # modules=[] -> revoke all module permissions. + modules=["annotation", "modelDev"], +) +``` + +### Delete workspace user + +Deletes an internal workspace user. + +```python +client.delete_workspace_user(user_id="YOUR_WORKSPACE_USER_ID") +``` + ## API Docs Check [this](https://api.fastlabel.ai/docs/) for further information. diff --git a/fastlabel/__init__.py b/fastlabel/__init__.py index 0155359..4805270 100644 --- a/fastlabel/__init__.py +++ b/fastlabel/__init__.py @@ -5471,6 +5471,87 @@ def get_project_comments( params["limit"] = limit return self.api.get_request(endpoint, params=params) + def get_workspace_users( + self, + keyword: str = None, + offset: int = None, + limit: int = 20, + ) -> list: + """ + Returns a list of internal workspace users. + keyword is a search keyword for name or email (Optional). + offset is the starting position number to fetch (Optional). + limit is the max number to fetch (Optional, default 20). + """ + endpoint = "workspaces-users" + params = {} + if keyword: + params["keyword"] = keyword + if offset is not None: + params["offset"] = offset + if limit is not None: + params["limit"] = limit + return self.api.get_request(endpoint, params=params) + + def create_workspace_user( + self, + name: str, + email: str, + language: str, + role: str, + modules: List[str] = None, + ) -> dict: + """ + Creates an internal workspace user and returns the created user. + name is the user's name (Required). + email is the user's email address (Required). + language is the user's language, 'en' or 'ja' (Required). + role is the workspace role, 'member' or 'owner' (Required). + modules is a list of module permissions to grant on invitation + (Optional). Each value is one of 'annotation', 'modelDev', 'dataset'. + """ + endpoint = "workspaces-users/internal-users" + payload = { + "name": name, + "email": email, + "language": language, + "role": role, + } + if modules is not None: + payload["modules"] = modules + return self.api.post_request(endpoint, payload=payload) + + def update_workspace_user( + self, + user_id: str, + role: str = None, + modules: List[str] = None, + ) -> dict: + """ + Updates an internal workspace user and returns the updated user. + user_id is the id of the workspace user (Required). + role is the workspace role, 'member' or 'owner' (Optional). + modules is a list of module permissions to sync the user to + (Optional). Each value is one of 'annotation', 'modelDev', 'dataset'. + When omitted (None) the module permissions are left unchanged; pass an + empty list to revoke all module permissions. + """ + endpoint = f"workspaces-users/internal-users/{user_id}" + payload = {} + if role: + payload["role"] = role + if modules is not None: + payload["modules"] = modules + return self.api.put_request(endpoint, payload=payload) + + def delete_workspace_user(self, user_id: str) -> None: + """ + Deletes an internal workspace user. + user_id is the id of the workspace user (Required). + """ + endpoint = f"workspaces-users/internal-users/{user_id}" + self.api.delete_request(endpoint) + def mask_to_fastlabel_segmentation_points( self, mask_image: Union[str, np.ndarray] ) -> List[List[List[int]]]: diff --git a/fastlabel/exceptions.py b/fastlabel/exceptions.py index 7f0a4ba..62415f8 100644 --- a/fastlabel/exceptions.py +++ b/fastlabel/exceptions.py @@ -1,10 +1,14 @@ class FastLabelException(Exception): - def __init__(self, message, errcode): + def __init__(self, message, errcode=None): super(FastLabelException, self).__init__( " {}".format(errcode, message) ) + self.message = message self.code = errcode + def __reduce__(self): + return (self.__class__, (self.message, self.code)) + class FastLabelInvalidException(FastLabelException, ValueError): pass diff --git a/tests/test_workspace_user.py b/tests/test_workspace_user.py new file mode 100644 index 0000000..f3205ec --- /dev/null +++ b/tests/test_workspace_user.py @@ -0,0 +1,167 @@ +"""Tests for the workspace user API client methods. + +These verify that get/create/update/delete_workspace_user build the correct +endpoint, query params and payload. The HTTP layer (client.api.*_request) is +stubbed so no real request is made. +""" +import pytest + +import fastlabel + + +@pytest.fixture +def client(monkeypatch): + monkeypatch.setenv("FASTLABEL_ACCESS_TOKEN", "dummy-token") + return fastlabel.Client() + + +def _capture(monkeypatch, client, method_name, return_value=None): + """Replace an api.*_request method with a recorder and return the calls list.""" + calls = [] + + def fake(endpoint, *args, **kwargs): + calls.append({"endpoint": endpoint, "args": args, "kwargs": kwargs}) + return return_value + + monkeypatch.setattr(client.api, method_name, fake) + return calls + + +# --- get_workspace_users --------------------------------------------------- + + +def test_get_workspace_users_default(monkeypatch, client): + calls = _capture(monkeypatch, client, "get_request", return_value=[]) + + client.get_workspace_users() + + assert calls[0]["endpoint"] == "workspaces-users" + # keyword/offset are omitted, limit defaults to 20 + assert calls[0]["kwargs"]["params"] == {"limit": 20} + + +def test_get_workspace_users_with_params(monkeypatch, client): + calls = _capture(monkeypatch, client, "get_request", return_value=[]) + + client.get_workspace_users(keyword="john", offset=10, limit=50) + + assert calls[0]["kwargs"]["params"] == { + "keyword": "john", + "offset": 10, + "limit": 50, + } + + +def test_get_workspace_users_offset_zero_included(monkeypatch, client): + calls = _capture(monkeypatch, client, "get_request", return_value=[]) + + client.get_workspace_users(offset=0) + + # offset=0 should still be sent (is not None), keyword empty is omitted + assert calls[0]["kwargs"]["params"] == {"offset": 0, "limit": 20} + + +# --- create_workspace_user ------------------------------------------------- + + +def test_create_workspace_user_without_modules(monkeypatch, client): + calls = _capture(monkeypatch, client, "post_request", return_value={}) + + client.create_workspace_user( + name="John Doe", + email="john@example.com", + language="en", + role="member", + ) + + assert calls[0]["endpoint"] == "workspaces-users/internal-users" + assert calls[0]["kwargs"]["payload"] == { + "name": "John Doe", + "email": "john@example.com", + "language": "en", + "role": "member", + } + + +def test_create_workspace_user_with_modules(monkeypatch, client): + calls = _capture(monkeypatch, client, "post_request", return_value={}) + + client.create_workspace_user( + name="John Doe", + email="john@example.com", + language="ja", + role="owner", + modules=["annotation", "dataset"], + ) + + assert calls[0]["kwargs"]["payload"] == { + "name": "John Doe", + "email": "john@example.com", + "language": "ja", + "role": "owner", + "modules": ["annotation", "dataset"], + } + + +def test_create_workspace_user_empty_modules_sent(monkeypatch, client): + calls = _capture(monkeypatch, client, "post_request", return_value={}) + + client.create_workspace_user( + name="John Doe", + email="john@example.com", + language="en", + role="member", + modules=[], + ) + + assert calls[0]["kwargs"]["payload"]["modules"] == [] + + +# --- update_workspace_user ------------------------------------------------- + + +def test_update_workspace_user_role_only(monkeypatch, client): + calls = _capture(monkeypatch, client, "put_request", return_value={}) + + client.update_workspace_user(user_id="wsu-1", role="owner") + + assert calls[0]["endpoint"] == "workspaces-users/internal-users/wsu-1" + assert calls[0]["kwargs"]["payload"] == {"role": "owner"} + + +def test_update_workspace_user_modules_unchanged_when_none(monkeypatch, client): + calls = _capture(monkeypatch, client, "put_request", return_value={}) + + client.update_workspace_user(user_id="wsu-1", role="member") + + # modules omitted -> not present in payload (left unchanged server-side) + assert "modules" not in calls[0]["kwargs"]["payload"] + + +def test_update_workspace_user_modules_sync(monkeypatch, client): + calls = _capture(monkeypatch, client, "put_request", return_value={}) + + client.update_workspace_user(user_id="wsu-1", modules=["annotation", "modelDev"]) + + assert calls[0]["kwargs"]["payload"] == {"modules": ["annotation", "modelDev"]} + + +def test_update_workspace_user_empty_modules_revokes_all(monkeypatch, client): + calls = _capture(monkeypatch, client, "put_request", return_value={}) + + client.update_workspace_user(user_id="wsu-1", modules=[]) + + # empty list is distinct from None: it is sent to revoke all permissions + assert calls[0]["kwargs"]["payload"] == {"modules": []} + + +# --- delete_workspace_user ------------------------------------------------- + + +def test_delete_workspace_user(monkeypatch, client): + calls = _capture(monkeypatch, client, "delete_request", return_value=None) + + result = client.delete_workspace_user(user_id="wsu-1") + + assert calls[0]["endpoint"] == "workspaces-users/internal-users/wsu-1" + assert result is None From 15f7577e7ea490e66196e058fa94d0d900046fb8 Mon Sep 17 00:00:00 2001 From: Daisuke Miyazoe Date: Mon, 15 Jun 2026 13:43:33 +0900 Subject: [PATCH 2/3] =?UTF-8?q?user=5Fid=E3=82=92id=E3=81=AB=E5=A4=89?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- fastlabel/__init__.py | 12 ++++++------ tests/test_workspace_user.py | 10 +++++----- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 043d025..2c5bdee 100644 --- a/README.md +++ b/README.md @@ -4192,7 +4192,7 @@ syncs the user's module permissions to the given set. ```python user = client.update_workspace_user( - user_id="YOUR_WORKSPACE_USER_ID", + id="YOUR_WORKSPACE_USER_ID", role="owner", # 'member' or 'owner' (Optional) # modules omitted -> module permissions are left unchanged. # modules=["annotation"] -> sync to exactly this set. @@ -4206,7 +4206,7 @@ user = client.update_workspace_user( Deletes an internal workspace user. ```python -client.delete_workspace_user(user_id="YOUR_WORKSPACE_USER_ID") +client.delete_workspace_user(id="YOUR_WORKSPACE_USER_ID") ``` ## API Docs diff --git a/fastlabel/__init__.py b/fastlabel/__init__.py index 4805270..ce61a89 100644 --- a/fastlabel/__init__.py +++ b/fastlabel/__init__.py @@ -5523,20 +5523,20 @@ def create_workspace_user( def update_workspace_user( self, - user_id: str, + id: str, role: str = None, modules: List[str] = None, ) -> dict: """ Updates an internal workspace user and returns the updated user. - user_id is the id of the workspace user (Required). + id is the id of the workspace user (Required). role is the workspace role, 'member' or 'owner' (Optional). modules is a list of module permissions to sync the user to (Optional). Each value is one of 'annotation', 'modelDev', 'dataset'. When omitted (None) the module permissions are left unchanged; pass an empty list to revoke all module permissions. """ - endpoint = f"workspaces-users/internal-users/{user_id}" + endpoint = f"workspaces-users/internal-users/{id}" payload = {} if role: payload["role"] = role @@ -5544,12 +5544,12 @@ def update_workspace_user( payload["modules"] = modules return self.api.put_request(endpoint, payload=payload) - def delete_workspace_user(self, user_id: str) -> None: + def delete_workspace_user(self, id: str) -> None: """ Deletes an internal workspace user. - user_id is the id of the workspace user (Required). + id is the id of the workspace user (Required). """ - endpoint = f"workspaces-users/internal-users/{user_id}" + endpoint = f"workspaces-users/internal-users/{id}" self.api.delete_request(endpoint) def mask_to_fastlabel_segmentation_points( diff --git a/tests/test_workspace_user.py b/tests/test_workspace_user.py index f3205ec..6cce6e2 100644 --- a/tests/test_workspace_user.py +++ b/tests/test_workspace_user.py @@ -123,7 +123,7 @@ def test_create_workspace_user_empty_modules_sent(monkeypatch, client): def test_update_workspace_user_role_only(monkeypatch, client): calls = _capture(monkeypatch, client, "put_request", return_value={}) - client.update_workspace_user(user_id="wsu-1", role="owner") + client.update_workspace_user(id="wsu-1", role="owner") assert calls[0]["endpoint"] == "workspaces-users/internal-users/wsu-1" assert calls[0]["kwargs"]["payload"] == {"role": "owner"} @@ -132,7 +132,7 @@ def test_update_workspace_user_role_only(monkeypatch, client): def test_update_workspace_user_modules_unchanged_when_none(monkeypatch, client): calls = _capture(monkeypatch, client, "put_request", return_value={}) - client.update_workspace_user(user_id="wsu-1", role="member") + client.update_workspace_user(id="wsu-1", role="member") # modules omitted -> not present in payload (left unchanged server-side) assert "modules" not in calls[0]["kwargs"]["payload"] @@ -141,7 +141,7 @@ def test_update_workspace_user_modules_unchanged_when_none(monkeypatch, client): def test_update_workspace_user_modules_sync(monkeypatch, client): calls = _capture(monkeypatch, client, "put_request", return_value={}) - client.update_workspace_user(user_id="wsu-1", modules=["annotation", "modelDev"]) + client.update_workspace_user(id="wsu-1", modules=["annotation", "modelDev"]) assert calls[0]["kwargs"]["payload"] == {"modules": ["annotation", "modelDev"]} @@ -149,7 +149,7 @@ def test_update_workspace_user_modules_sync(monkeypatch, client): def test_update_workspace_user_empty_modules_revokes_all(monkeypatch, client): calls = _capture(monkeypatch, client, "put_request", return_value={}) - client.update_workspace_user(user_id="wsu-1", modules=[]) + client.update_workspace_user(id="wsu-1", modules=[]) # empty list is distinct from None: it is sent to revoke all permissions assert calls[0]["kwargs"]["payload"] == {"modules": []} @@ -161,7 +161,7 @@ def test_update_workspace_user_empty_modules_revokes_all(monkeypatch, client): def test_delete_workspace_user(monkeypatch, client): calls = _capture(monkeypatch, client, "delete_request", return_value=None) - result = client.delete_workspace_user(user_id="wsu-1") + result = client.delete_workspace_user(id="wsu-1") assert calls[0]["endpoint"] == "workspaces-users/internal-users/wsu-1" assert result is None From 718463c868b92ec6224ede402506217b3c9c98ca Mon Sep 17 00:00:00 2001 From: Daisuke Miyazoe Date: Mon, 15 Jun 2026 16:32:26 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=E3=82=A8=E3=83=B3=E3=83=89=E3=83=9D?= =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=83=88=E4=BF=AE=E6=AD=A3=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 48 ++++++++++-- fastlabel/__init__.py | 77 ++++++++++++++++--- fastlabel/api.py | 6 +- tests/test_workspace_user.py | 139 ++++++++++++++++++++++------------- 4 files changed, 198 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index 2c5bdee..ec67fae 100644 --- a/README.md +++ b/README.md @@ -4173,7 +4173,7 @@ users = client.get_workspace_users( ### Create workspace user Creates an internal workspace user. The `slug` is generated automatically on the server side. -Pass `modules` to grant module permissions on invitation. +Module permissions are managed separately (see below). ```python user = client.create_workspace_user( @@ -4181,23 +4181,17 @@ user = client.create_workspace_user( email="john@example.com", language="en", # 'en' or 'ja' role="member", # 'member' or 'owner' - modules=["annotation", "dataset"], # Optional. Any of 'annotation', 'modelDev', 'dataset' ) ``` ### Update workspace user -Updates an internal workspace user. The `role` can be changed, and `modules` -syncs the user's module permissions to the given set. +Updates an internal workspace user. Only the `role` can be changed. ```python user = client.update_workspace_user( id="YOUR_WORKSPACE_USER_ID", role="owner", # 'member' or 'owner' (Optional) - # modules omitted -> module permissions are left unchanged. - # modules=["annotation"] -> sync to exactly this set. - # modules=[] -> revoke all module permissions. - modules=["annotation", "modelDev"], ) ``` @@ -4209,6 +4203,44 @@ Deletes an internal workspace user. client.delete_workspace_user(id="YOUR_WORKSPACE_USER_ID") ``` +### Grant module permissions + +Grants module permissions to an internal workspace user. +`modules` accepts a single module or a list (each is sent as a separate request). + +```python +# Single module +client.create_workspace_user_module_permissions( + workspace_user_id="YOUR_WORKSPACE_USER_ID", + modules="annotation", # 'annotation', 'modelDev' or 'dataset' +) + +# Multiple modules +client.create_workspace_user_module_permissions( + workspace_user_id="YOUR_WORKSPACE_USER_ID", + modules=["annotation", "dataset"], +) +``` + +### Revoke module permissions + +Revokes module permissions from an internal workspace user. +`modules` accepts a single module or a list (each is sent as a separate request). + +```python +# Single module +client.delete_workspace_user_module_permissions( + workspace_user_id="YOUR_WORKSPACE_USER_ID", + modules="annotation", # 'annotation', 'modelDev' or 'dataset' +) + +# Multiple modules +client.delete_workspace_user_module_permissions( + workspace_user_id="YOUR_WORKSPACE_USER_ID", + modules=["annotation", "dataset"], +) +``` + ## API Docs Check [this](https://api.fastlabel.ai/docs/) for further information. diff --git a/fastlabel/__init__.py b/fastlabel/__init__.py index ce61a89..e2c8f0f 100644 --- a/fastlabel/__init__.py +++ b/fastlabel/__init__.py @@ -5499,7 +5499,6 @@ def create_workspace_user( email: str, language: str, role: str, - modules: List[str] = None, ) -> dict: """ Creates an internal workspace user and returns the created user. @@ -5507,8 +5506,8 @@ def create_workspace_user( email is the user's email address (Required). language is the user's language, 'en' or 'ja' (Required). role is the workspace role, 'member' or 'owner' (Required). - modules is a list of module permissions to grant on invitation - (Optional). Each value is one of 'annotation', 'modelDev', 'dataset'. + Module permissions are managed separately; use + create_workspace_user_module_permission to grant them. """ endpoint = "workspaces-users/internal-users" payload = { @@ -5517,31 +5516,23 @@ def create_workspace_user( "language": language, "role": role, } - if modules is not None: - payload["modules"] = modules return self.api.post_request(endpoint, payload=payload) def update_workspace_user( self, id: str, role: str = None, - modules: List[str] = None, ) -> dict: """ Updates an internal workspace user and returns the updated user. + Only the role can be changed. id is the id of the workspace user (Required). role is the workspace role, 'member' or 'owner' (Optional). - modules is a list of module permissions to sync the user to - (Optional). Each value is one of 'annotation', 'modelDev', 'dataset'. - When omitted (None) the module permissions are left unchanged; pass an - empty list to revoke all module permissions. """ endpoint = f"workspaces-users/internal-users/{id}" payload = {} if role: payload["role"] = role - if modules is not None: - payload["modules"] = modules return self.api.put_request(endpoint, payload=payload) def delete_workspace_user(self, id: str) -> None: @@ -5552,6 +5543,68 @@ def delete_workspace_user(self, id: str) -> None: endpoint = f"workspaces-users/internal-users/{id}" self.api.delete_request(endpoint) + def create_workspace_user_module_permissions( + self, + workspace_user_id: str, + modules: Union[str, List[str]], + ) -> List[str]: + """ + Grants module permissions to an internal workspace user. + Each module is granted with a separate request; if one fails (e.g. the + module user limit is reached), the permissions granted before it remain. + workspace_user_id is the id of the workspace user (Required). + modules is a single module or a list of modules, each one of + 'annotation', 'modelDev', 'dataset' (Required). + """ + if isinstance(modules, str): + modules = [modules] + module_paths = { + "annotation": "annotation", + "dataset": "dataset", + "modelDev": "model-dev", + } + results = [] + for module in modules: + if module not in module_paths: + raise FastLabelInvalidException( + "module must be one of 'annotation', 'modelDev', 'dataset'.", 422 + ) + endpoint = ( + f"function-resource-permissions/{module_paths[module]}/internal-users" + ) + results.append( + self.api.post_request( + endpoint, payload={"workspaceUserId": workspace_user_id} + ) + ) + return results + + def delete_workspace_user_module_permissions( + self, + workspace_user_id: str, + modules: Union[str, List[str]], + ) -> None: + """ + Revokes module permissions from an internal workspace user. + Each module is revoked with a separate request; if one fails, the + permissions revoked before it remain revoked. + workspace_user_id is the id of the workspace user (Required). + modules is a single module or a list of modules, each one of + 'annotation', 'modelDev', 'dataset' (Required). + """ + if isinstance(modules, str): + modules = [modules] + endpoint = "function-resource-permissions" + for module in modules: + if module not in ("annotation", "modelDev", "dataset"): + raise FastLabelInvalidException( + "module must be one of 'annotation', 'modelDev', 'dataset'.", 422 + ) + self.api.delete_request( + endpoint, + payload={"workspaceUserId": workspace_user_id, "resource": module}, + ) + def mask_to_fastlabel_segmentation_points( self, mask_image: Union[str, np.ndarray] ) -> List[List[List[int]]]: diff --git a/fastlabel/api.py b/fastlabel/api.py index 20e5188..6df9c8c 100644 --- a/fastlabel/api.py +++ b/fastlabel/api.py @@ -44,7 +44,7 @@ def get_request(self, endpoint: str, params=None) -> Union[dict, list]: else: raise FastLabelException(error, r.status_code) - def delete_request(self, endpoint: str, params=None) -> dict: + def delete_request(self, endpoint: str, params=None, payload=None) -> dict: """Makes a delete request to an endpoint. If an error occurs, assumes that endpoint returns JSON as: { 'statusCode': XXX, @@ -55,7 +55,9 @@ def delete_request(self, endpoint: str, params=None) -> dict: "Content-Type": "application/json", "Authorization": self.access_token, } - r = requests.delete(self.base_url + endpoint, headers=headers, params=params) + r = requests.delete( + self.base_url + endpoint, headers=headers, params=params, json=payload + ) if r.status_code == 200 or r.status_code == 204: return diff --git a/tests/test_workspace_user.py b/tests/test_workspace_user.py index 6cce6e2..90176fa 100644 --- a/tests/test_workspace_user.py +++ b/tests/test_workspace_user.py @@ -83,85 +83,124 @@ def test_create_workspace_user_without_modules(monkeypatch, client): } -def test_create_workspace_user_with_modules(monkeypatch, client): - calls = _capture(monkeypatch, client, "post_request", return_value={}) +# --- update_workspace_user ------------------------------------------------- - client.create_workspace_user( - name="John Doe", - email="john@example.com", - language="ja", - role="owner", - modules=["annotation", "dataset"], - ) - assert calls[0]["kwargs"]["payload"] == { - "name": "John Doe", - "email": "john@example.com", - "language": "ja", - "role": "owner", - "modules": ["annotation", "dataset"], - } +def test_update_workspace_user_role_only(monkeypatch, client): + calls = _capture(monkeypatch, client, "put_request", return_value={}) + client.update_workspace_user(id="wsu-1", role="owner") -def test_create_workspace_user_empty_modules_sent(monkeypatch, client): - calls = _capture(monkeypatch, client, "post_request", return_value={}) + assert calls[0]["endpoint"] == "workspaces-users/internal-users/wsu-1" + assert calls[0]["kwargs"]["payload"] == {"role": "owner"} - client.create_workspace_user( - name="John Doe", - email="john@example.com", - language="en", - role="member", - modules=[], - ) - assert calls[0]["kwargs"]["payload"]["modules"] == [] +def test_update_workspace_user_role_omitted(monkeypatch, client): + calls = _capture(monkeypatch, client, "put_request", return_value={}) + client.update_workspace_user(id="wsu-1") -# --- update_workspace_user ------------------------------------------------- + # role omitted -> empty payload + assert calls[0]["kwargs"]["payload"] == {} -def test_update_workspace_user_role_only(monkeypatch, client): - calls = _capture(monkeypatch, client, "put_request", return_value={}) +# --- delete_workspace_user ------------------------------------------------- - client.update_workspace_user(id="wsu-1", role="owner") + +def test_delete_workspace_user(monkeypatch, client): + calls = _capture(monkeypatch, client, "delete_request", return_value=None) + + result = client.delete_workspace_user(id="wsu-1") assert calls[0]["endpoint"] == "workspaces-users/internal-users/wsu-1" - assert calls[0]["kwargs"]["payload"] == {"role": "owner"} + assert result is None -def test_update_workspace_user_modules_unchanged_when_none(monkeypatch, client): - calls = _capture(monkeypatch, client, "put_request", return_value={}) +# --- create_workspace_user_module_permissions ------------------------------ - client.update_workspace_user(id="wsu-1", role="member") - # modules omitted -> not present in payload (left unchanged server-side) - assert "modules" not in calls[0]["kwargs"]["payload"] +@pytest.mark.parametrize( + "module, expected_path", + [ + ("annotation", "function-resource-permissions/annotation/internal-users"), + ("dataset", "function-resource-permissions/dataset/internal-users"), + ("modelDev", "function-resource-permissions/model-dev/internal-users"), + ], +) +def test_create_module_permissions_single(monkeypatch, client, module, expected_path): + calls = _capture(monkeypatch, client, "post_request", return_value=module) + # a single module string is accepted (not only a list) + result = client.create_workspace_user_module_permissions( + workspace_user_id="wsu-1", modules=module + ) -def test_update_workspace_user_modules_sync(monkeypatch, client): - calls = _capture(monkeypatch, client, "put_request", return_value={}) + assert len(calls) == 1 + assert calls[0]["endpoint"] == expected_path + assert calls[0]["kwargs"]["payload"] == {"workspaceUserId": "wsu-1"} + assert result == [module] - client.update_workspace_user(id="wsu-1", modules=["annotation", "modelDev"]) - assert calls[0]["kwargs"]["payload"] == {"modules": ["annotation", "modelDev"]} +def test_create_module_permissions_multiple(monkeypatch, client): + calls = _capture(monkeypatch, client, "post_request", return_value="ok") + result = client.create_workspace_user_module_permissions( + workspace_user_id="wsu-1", modules=["annotation", "dataset"] + ) -def test_update_workspace_user_empty_modules_revokes_all(monkeypatch, client): - calls = _capture(monkeypatch, client, "put_request", return_value={}) + assert [c["endpoint"] for c in calls] == [ + "function-resource-permissions/annotation/internal-users", + "function-resource-permissions/dataset/internal-users", + ] + assert all(c["kwargs"]["payload"] == {"workspaceUserId": "wsu-1"} for c in calls) + assert result == ["ok", "ok"] - client.update_workspace_user(id="wsu-1", modules=[]) - # empty list is distinct from None: it is sent to revoke all permissions - assert calls[0]["kwargs"]["payload"] == {"modules": []} +def test_create_module_permissions_invalid_module(monkeypatch, client): + _capture(monkeypatch, client, "post_request", return_value=None) + with pytest.raises(fastlabel.exceptions.FastLabelInvalidException): + client.create_workspace_user_module_permissions( + workspace_user_id="wsu-1", modules="unknown" + ) -# --- delete_workspace_user ------------------------------------------------- +# --- delete_workspace_user_module_permissions ------------------------------ -def test_delete_workspace_user(monkeypatch, client): + +def test_delete_module_permissions_single(monkeypatch, client): calls = _capture(monkeypatch, client, "delete_request", return_value=None) - result = client.delete_workspace_user(id="wsu-1") + client.delete_workspace_user_module_permissions( + workspace_user_id="wsu-1", modules="modelDev" + ) - assert calls[0]["endpoint"] == "workspaces-users/internal-users/wsu-1" - assert result is None + assert len(calls) == 1 + assert calls[0]["endpoint"] == "function-resource-permissions" + assert calls[0]["kwargs"]["payload"] == { + "workspaceUserId": "wsu-1", + "resource": "modelDev", + } + + +def test_delete_module_permissions_multiple(monkeypatch, client): + calls = _capture(monkeypatch, client, "delete_request", return_value=None) + + client.delete_workspace_user_module_permissions( + workspace_user_id="wsu-1", modules=["annotation", "modelDev"] + ) + + assert [c["kwargs"]["payload"]["resource"] for c in calls] == [ + "annotation", + "modelDev", + ] + assert all(c["endpoint"] == "function-resource-permissions" for c in calls) + + +def test_delete_module_permissions_invalid_module(monkeypatch, client): + _capture(monkeypatch, client, "delete_request", return_value=None) + + with pytest.raises(fastlabel.exceptions.FastLabelInvalidException): + client.delete_workspace_user_module_permissions( + workspace_user_id="wsu-1", modules="unknown" + )