From e85e94cb102e6649b15c2b1a14ba538f67d647a9 Mon Sep 17 00:00:00 2001 From: Daisuke Miyazoe Date: Mon, 15 Jun 2026 12:53:28 +0900 Subject: [PATCH 1/5] =?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 4fefdd1d75adf8b922ed2539fbb67832ca1eef9d Mon Sep 17 00:00:00 2001 From: Daisuke Miyazoe Date: Mon, 15 Jun 2026 13:43:33 +0900 Subject: [PATCH 2/5] =?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 22dbd9c2a39a377b114fd3788c603b301c845dab Mon Sep 17 00:00:00 2001 From: Daisuke Miyazoe Date: Mon, 15 Jun 2026 16:32:26 +0900 Subject: [PATCH 3/5] =?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" + ) From 65b4d2635c87d8c9d28319b3f671a2221c5bc591 Mon Sep 17 00:00:00 2001 From: Daisuke Miyazoe Date: Thu, 18 Jun 2026 11:41:22 +0900 Subject: [PATCH 4/5] =?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=E3=81=8C=E5=A4=89=E3=82=8F=E3=81=A3?= =?UTF-8?q?=E3=81=9F=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 23 ++++++++++------- fastlabel/__init__.py | 46 ++++++++++++++++----------------- tests/test_workspace_user.py | 49 ++++++++++++++++++------------------ 3 files changed, 60 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index ec67fae..de01ae6 100644 --- a/README.md +++ b/README.md @@ -3836,6 +3836,7 @@ Example of two training jobs. "tags": [], "contentCount": 23, "userName": "Admin", + "userEmail": "admin@example.com", "createdAt": "2023-10-31T07:10:28.306Z", "completedAt": null, "customModel": { @@ -3862,6 +3863,7 @@ Example of two training jobs. ], "contentCount": 20, "userName": "Admin", + "userEmail": "admin@example.com", "createdAt": "2023-10-31T06:56:28.112Z", "completedAt": "2023-10-31T07:08:26.000Z", "customModel": { @@ -4186,21 +4188,24 @@ user = client.create_workspace_user( ### Update workspace user -Updates an internal workspace user. Only the `role` can be changed. +Updates an internal workspace user. The user is identified by `email` and only +the `role` can be changed. Passing `role="none"` removes the user from the +workspace (equivalent to `delete_workspace_user`). ```python user = client.update_workspace_user( - id="YOUR_WORKSPACE_USER_ID", - role="owner", # 'member' or 'owner' (Optional) + email="john@example.com", + role="owner", # 'member', 'owner' or 'none' ) ``` ### Delete workspace user -Deletes an internal workspace user. +Removes an internal workspace user from the workspace. There is no dedicated +delete endpoint; this updates the user's role to `none`. ```python -client.delete_workspace_user(id="YOUR_WORKSPACE_USER_ID") +client.delete_workspace_user(email="john@example.com") ``` ### Grant module permissions @@ -4211,13 +4216,13 @@ Grants module permissions to an internal workspace user. ```python # Single module client.create_workspace_user_module_permissions( - workspace_user_id="YOUR_WORKSPACE_USER_ID", + email="john@example.com", modules="annotation", # 'annotation', 'modelDev' or 'dataset' ) # Multiple modules client.create_workspace_user_module_permissions( - workspace_user_id="YOUR_WORKSPACE_USER_ID", + email="john@example.com", modules=["annotation", "dataset"], ) ``` @@ -4230,13 +4235,13 @@ Revokes module permissions from an internal workspace user. ```python # Single module client.delete_workspace_user_module_permissions( - workspace_user_id="YOUR_WORKSPACE_USER_ID", + email="john@example.com", modules="annotation", # 'annotation', 'modelDev' or 'dataset' ) # Multiple modules client.delete_workspace_user_module_permissions( - workspace_user_id="YOUR_WORKSPACE_USER_ID", + email="john@example.com", modules=["annotation", "dataset"], ) ``` diff --git a/fastlabel/__init__.py b/fastlabel/__init__.py index e2c8f0f..5f283fe 100644 --- a/fastlabel/__init__.py +++ b/fastlabel/__init__.py @@ -5520,39 +5520,41 @@ def create_workspace_user( def update_workspace_user( self, - id: str, - role: str = None, + email: str, + role: str, ) -> 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). + The user is identified by email. Only the role can be changed. + Passing role='none' removes the user from the workspace + (equivalent to delete_workspace_user). + email is the email address of the workspace user (Required). + role is the workspace role, 'member', 'owner' or 'none' (Required). """ - endpoint = f"workspaces-users/internal-users/{id}" - payload = {} - if role: - payload["role"] = role + endpoint = "workspaces-users/internal-users" + payload = {"email": email, "role": role} return self.api.put_request(endpoint, payload=payload) - def delete_workspace_user(self, id: str) -> None: + def delete_workspace_user(self, email: str) -> None: """ - Deletes an internal workspace user. - id is the id of the workspace user (Required). + Removes an internal workspace user from the workspace. + There is no dedicated delete endpoint; this is done by updating the + user's role to 'none'. + email is the email address of the workspace user (Required). """ - endpoint = f"workspaces-users/internal-users/{id}" - self.api.delete_request(endpoint) + endpoint = "workspaces-users/internal-users" + self.api.put_request(endpoint, payload={"email": email, "role": "none"}) def create_workspace_user_module_permissions( self, - workspace_user_id: str, + email: 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). + email is the email address of the workspace user (Required). modules is a single module or a list of modules, each one of 'annotation', 'modelDev', 'dataset' (Required). """ @@ -5572,23 +5574,19 @@ def create_workspace_user_module_permissions( endpoint = ( f"function-resource-permissions/{module_paths[module]}/internal-users" ) - results.append( - self.api.post_request( - endpoint, payload={"workspaceUserId": workspace_user_id} - ) - ) + results.append(self.api.post_request(endpoint, payload={"email": email})) return results def delete_workspace_user_module_permissions( self, - workspace_user_id: str, + email: 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). + email is the email address of the workspace user (Required). modules is a single module or a list of modules, each one of 'annotation', 'modelDev', 'dataset' (Required). """ @@ -5602,7 +5600,7 @@ def delete_workspace_user_module_permissions( ) self.api.delete_request( endpoint, - payload={"workspaceUserId": workspace_user_id, "resource": module}, + payload={"email": email, "resource": module}, ) def mask_to_fastlabel_segmentation_points( diff --git a/tests/test_workspace_user.py b/tests/test_workspace_user.py index 90176fa..9d3bc8a 100644 --- a/tests/test_workspace_user.py +++ b/tests/test_workspace_user.py @@ -86,33 +86,32 @@ def test_create_workspace_user_without_modules(monkeypatch, client): # --- update_workspace_user ------------------------------------------------- -def test_update_workspace_user_role_only(monkeypatch, client): +def test_update_workspace_user_role(monkeypatch, client): calls = _capture(monkeypatch, client, "put_request", return_value={}) - client.update_workspace_user(id="wsu-1", role="owner") + client.update_workspace_user(email="john@example.com", role="owner") - assert calls[0]["endpoint"] == "workspaces-users/internal-users/wsu-1" - assert calls[0]["kwargs"]["payload"] == {"role": "owner"} - - -def test_update_workspace_user_role_omitted(monkeypatch, client): - calls = _capture(monkeypatch, client, "put_request", return_value={}) - - client.update_workspace_user(id="wsu-1") - - # role omitted -> empty payload - assert calls[0]["kwargs"]["payload"] == {} + assert calls[0]["endpoint"] == "workspaces-users/internal-users" + assert calls[0]["kwargs"]["payload"] == { + "email": "john@example.com", + "role": "owner", + } # --- delete_workspace_user ------------------------------------------------- def test_delete_workspace_user(monkeypatch, client): - calls = _capture(monkeypatch, client, "delete_request", return_value=None) + # deletion is performed via PUT with role='none' (no DELETE endpoint) + calls = _capture(monkeypatch, client, "put_request", return_value=None) - result = client.delete_workspace_user(id="wsu-1") + result = client.delete_workspace_user(email="john@example.com") - assert calls[0]["endpoint"] == "workspaces-users/internal-users/wsu-1" + assert calls[0]["endpoint"] == "workspaces-users/internal-users" + assert calls[0]["kwargs"]["payload"] == { + "email": "john@example.com", + "role": "none", + } assert result is None @@ -132,12 +131,12 @@ def test_create_module_permissions_single(monkeypatch, client, module, expected_ # a single module string is accepted (not only a list) result = client.create_workspace_user_module_permissions( - workspace_user_id="wsu-1", modules=module + email="john@example.com", modules=module ) assert len(calls) == 1 assert calls[0]["endpoint"] == expected_path - assert calls[0]["kwargs"]["payload"] == {"workspaceUserId": "wsu-1"} + assert calls[0]["kwargs"]["payload"] == {"email": "john@example.com"} assert result == [module] @@ -145,14 +144,14 @@ 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"] + email="john@example.com", modules=["annotation", "dataset"] ) 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 all(c["kwargs"]["payload"] == {"email": "john@example.com"} for c in calls) assert result == ["ok", "ok"] @@ -161,7 +160,7 @@ def test_create_module_permissions_invalid_module(monkeypatch, client): with pytest.raises(fastlabel.exceptions.FastLabelInvalidException): client.create_workspace_user_module_permissions( - workspace_user_id="wsu-1", modules="unknown" + email="john@example.com", modules="unknown" ) @@ -172,13 +171,13 @@ def test_delete_module_permissions_single(monkeypatch, client): calls = _capture(monkeypatch, client, "delete_request", return_value=None) client.delete_workspace_user_module_permissions( - workspace_user_id="wsu-1", modules="modelDev" + email="john@example.com", modules="modelDev" ) assert len(calls) == 1 assert calls[0]["endpoint"] == "function-resource-permissions" assert calls[0]["kwargs"]["payload"] == { - "workspaceUserId": "wsu-1", + "email": "john@example.com", "resource": "modelDev", } @@ -187,7 +186,7 @@ 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"] + email="john@example.com", modules=["annotation", "modelDev"] ) assert [c["kwargs"]["payload"]["resource"] for c in calls] == [ @@ -202,5 +201,5 @@ def test_delete_module_permissions_invalid_module(monkeypatch, client): with pytest.raises(fastlabel.exceptions.FastLabelInvalidException): client.delete_workspace_user_module_permissions( - workspace_user_id="wsu-1", modules="unknown" + email="john@example.com", modules="unknown" ) From 304e533e443d446f0ff40e8dab2880621fc8b5a0 Mon Sep 17 00:00:00 2001 From: Daisuke Miyazoe Date: Thu, 18 Jun 2026 11:52:31 +0900 Subject: [PATCH 5/5] =?UTF-8?q?delete=E6=88=90=E5=8A=9F=E6=99=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastlabel/api.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fastlabel/api.py b/fastlabel/api.py index 6df9c8c..fadb6f0 100644 --- a/fastlabel/api.py +++ b/fastlabel/api.py @@ -112,7 +112,11 @@ def put_request(self, endpoint, payload=None): r = requests.put(self.base_url + endpoint, json=payload, headers=headers) if r.status_code == 200: + if not r.content: + return return r.json() + elif r.status_code == 204: + return else: try: error = r.json()["message"]