Skip to content

Commit 6a00494

Browse files
authored
[BUGFIX] [ENHANCEMENT] create users and workspaces with predefined ID (#5786)
# Description <!-- Please include a summary of the changes and the related issue. Please also include relevant motivation and context. List any dependencies that are required for this change. --> This PR allows users and workspaces to be created with predefined IDs. Passing a predefined ID when creating workspaces and users simplifies the migration process since users don't need to review responses and map them to new users, avoiding potential issues in the process. **Type of change** <!-- Please delete options that are not relevant. Remember to title the PR according to the type of change --> - Bug fix (non-breaking change which fixes an issue) - Improvement (change adding some improvement to an existing functionality) - Documentation update **How Has This Been Tested** <!-- Please add some reference about how your feature has been tested. --> **Checklist** <!-- Please go over the list and make sure you've taken everything into account --> - I added relevant documentation - I followed the style guidelines of this project - I did a self-review of my code - I made corresponding changes to the documentation - I confirm My changes generate no new warnings - I have added tests that prove my fix is effective or that my feature works - I have added relevant notes to the CHANGELOG.md file (See https://keepachangelog.com/)
1 parent dc3deab commit 6a00494

File tree

13 files changed

+208
-13
lines changed

13 files changed

+208
-13
lines changed

argilla-server/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ These are the section headers that we use:
1616

1717
## [Unreleased]()
1818

19+
### Added
20+
21+
- Added support to create users with predefined ids. ([#5786](https://github.com/argilla-io/argilla/pull/5786))
22+
- Added support to create workspaces with predefined ids. ([#5786](https://github.com/argilla-io/argilla/pull/5786))
23+
1924
## [2.6.0](https://github.com/argilla-io/argilla/compare/v2.5.0...v2.6.0)
2025

2126
### Added

argilla-server/src/argilla_server/api/schemas/v1/users.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class User(BaseModel):
5959

6060

6161
class UserCreate(BaseModel):
62+
id: Optional[UUID] = None
6263
first_name: UserFirstName
6364
last_name: Optional[UserLastName] = None
6465
username: UserUsername

argilla-server/src/argilla_server/api/schemas/v1/workspaces.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
# limitations under the License.
1414

1515
from datetime import datetime
16-
from typing import List
16+
from optparse import Option
17+
from typing import List, Optional
1718
from uuid import UUID
1819

1920
from pydantic import BaseModel, Field, ConfigDict
@@ -29,6 +30,7 @@ class Workspace(BaseModel):
2930

3031

3132
class WorkspaceCreate(BaseModel):
33+
id: Optional[UUID] = None
3234
name: str = Field(min_length=1)
3335

3436

argilla-server/src/argilla_server/contexts/accounts.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,15 @@ async def create_workspace(db: AsyncSession, workspace_attrs: dict) -> Workspace
6666
if await Workspace.get_by(db, name=workspace_attrs["name"]) is not None:
6767
raise NotUniqueError(f"Workspace name `{workspace_attrs['name']}` is not unique")
6868

69-
return await Workspace.create(db, name=workspace_attrs["name"])
69+
if workspace_id := workspace_attrs.get("id"):
70+
if await Workspace.get(db, id=workspace_id) is not None:
71+
raise NotUniqueError(f"Workspace with id `{workspace_id}` is not unique")
72+
73+
return await Workspace.create(
74+
db,
75+
id=workspace_attrs.get("id"),
76+
name=workspace_attrs["name"],
77+
)
7078

7179

7280
async def delete_workspace(db: AsyncSession, workspace: Workspace):
@@ -108,8 +116,13 @@ async def create_user(db: AsyncSession, user_attrs: dict, workspaces: Union[List
108116
if await get_user_by_username(db, user_attrs["username"]) is not None:
109117
raise NotUniqueError(f"User username `{user_attrs['username']}` is not unique")
110118

119+
if user_id := user_attrs.get("id"):
120+
if await User.get(db, id=user_id) is not None:
121+
raise NotUniqueError(f"User with id `{user_id}` is not unique")
122+
111123
user = await User.create(
112124
db,
125+
id=user_attrs.get("id"),
113126
first_name=user_attrs["first_name"],
114127
last_name=user_attrs["last_name"],
115128
username=user_attrs["username"],

argilla-server/tests/unit/api/handlers/v1/users/test_create_user.py

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
14+
from uuid import uuid4
1515

1616
import pytest
1717
from argilla_server.constants import API_KEY_HEADER_NAME
@@ -146,6 +146,86 @@ async def test_create_user_with_non_default_role(
146146
assert response.json()["role"] == UserRole.owner
147147
assert user.role == UserRole.owner
148148

149+
async def test_create_user_with_predefined_id(
150+
self, db: AsyncSession, async_client: AsyncClient, owner_auth_header: dict
151+
):
152+
user_id = uuid4()
153+
response = await async_client.post(
154+
self.url(),
155+
headers=owner_auth_header,
156+
json={
157+
"id": str(user_id),
158+
"first_name": "First name",
159+
"last_name": "Last name",
160+
"username": "username",
161+
"password": "12345678",
162+
},
163+
)
164+
165+
assert response.status_code == 201
166+
167+
user = (await db.execute(select(User).filter_by(username="username"))).scalar_one()
168+
assert user.id == user_id
169+
170+
async def test_create_user_with_none_user_id(
171+
self, db: AsyncSession, async_client: AsyncClient, owner_auth_header: dict
172+
):
173+
response = await async_client.post(
174+
self.url(),
175+
headers=owner_auth_header,
176+
json={
177+
"id": None,
178+
"first_name": "First name",
179+
"last_name": "Last name",
180+
"username": "username",
181+
"password": "12345678",
182+
},
183+
)
184+
185+
assert response.status_code == 201
186+
187+
user = (await db.execute(select(User).filter_by(username="username"))).scalar_one()
188+
assert user.id is not None
189+
190+
async def test_create_user_with_wrong_user_id(
191+
self, db: AsyncSession, async_client: AsyncClient, owner_auth_header: dict
192+
):
193+
response = await async_client.post(
194+
self.url(),
195+
headers=owner_auth_header,
196+
json={
197+
"id": "wrong_id",
198+
"first_name": "First name",
199+
"last_name": "Last name",
200+
"username": "username",
201+
"password": "12345678",
202+
},
203+
)
204+
205+
assert response.status_code == 422
206+
assert (await db.execute(select(func.count(User.id)))).scalar() == 1
207+
208+
async def test_create_user_with_existing_id(
209+
self, db: AsyncSession, async_client: AsyncClient, owner_auth_header: dict
210+
):
211+
user_id = uuid4()
212+
await UserFactory.create(id=user_id)
213+
214+
response = await async_client.post(
215+
self.url(),
216+
headers=owner_auth_header,
217+
json={
218+
"id": str(user_id),
219+
"first_name": "First name",
220+
"last_name": "Last name",
221+
"username": "username",
222+
"password": "12345678",
223+
},
224+
)
225+
226+
assert response.status_code == 409
227+
assert (await db.execute(select(func.count(User.id)))).scalar() == 2
228+
149229
async def test_create_user_without_authentication(self, db: AsyncSession, async_client: AsyncClient):
150230
response = await async_client.post(
151231
self.url(),

argilla-server/tests/unit/api/handlers/v1/workspaces/test_create_workspace.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
from uuid import uuid4
1415

1516
import pytest
1617
from argilla_server.constants import API_KEY_HEADER_NAME
@@ -47,6 +48,77 @@ async def test_create_workspace(self, db: AsyncSession, async_client: AsyncClien
4748
"updated_at": workspace.updated_at.isoformat(),
4849
}
4950

51+
async def test_create_workspace_with_predefined_id(
52+
self, db: AsyncSession, async_client: AsyncClient, owner_auth_header: dict
53+
):
54+
workspace_id = uuid4()
55+
response = await async_client.post(
56+
self.url(),
57+
headers=owner_auth_header,
58+
json={"id": str(workspace_id), "name": "workspace"},
59+
)
60+
61+
assert response.status_code == 201
62+
63+
assert (await db.execute(select(func.count(Workspace.id)))).scalar() == 1
64+
workspace = (await db.execute(select(Workspace).filter_by(name="workspace"))).scalar_one()
65+
66+
assert response.json() == {
67+
"id": str(workspace_id),
68+
"name": "workspace",
69+
"inserted_at": workspace.inserted_at.isoformat(),
70+
"updated_at": workspace.updated_at.isoformat(),
71+
}
72+
73+
async def test_create_workspace_with_none_id(
74+
self, db: AsyncSession, async_client: AsyncClient, owner_auth_header: dict
75+
):
76+
response = await async_client.post(
77+
self.url(),
78+
headers=owner_auth_header,
79+
json={"id": None, "name": "workspace"},
80+
)
81+
82+
assert response.status_code == 201
83+
84+
assert (await db.execute(select(func.count(Workspace.id)))).scalar() == 1
85+
workspace = (await db.execute(select(Workspace).filter_by(name="workspace"))).scalar_one()
86+
87+
assert response.json() == {
88+
"id": str(workspace.id),
89+
"name": "workspace",
90+
"inserted_at": workspace.inserted_at.isoformat(),
91+
"updated_at": workspace.updated_at.isoformat(),
92+
}
93+
94+
async def test_create_workspace_with_wrong_id(
95+
self, db: AsyncSession, async_client: AsyncClient, owner_auth_header: dict
96+
):
97+
response = await async_client.post(
98+
self.url(),
99+
headers=owner_auth_header,
100+
json={"id": "wrong_id", "name": "workspace"},
101+
)
102+
103+
assert response.status_code == 422
104+
105+
assert (await db.execute(select(func.count(Workspace.id)))).scalar() == 0
106+
107+
async def test_create_workspace_with_existing_id(
108+
self, db: AsyncSession, async_client: AsyncClient, owner_auth_header: dict
109+
):
110+
workspace_id = uuid4()
111+
await WorkspaceFactory.create(id=workspace_id)
112+
113+
response = await async_client.post(
114+
self.url(),
115+
headers=owner_auth_header,
116+
json={"id": str(workspace_id), "name": "workspace"},
117+
)
118+
119+
assert response.status_code == 409
120+
assert (await db.execute(select(func.count(Workspace.id)))).scalar() == 1
121+
50122
async def test_create_workspace_without_authentication(self, db: AsyncSession, async_client: AsyncClient):
51123
response = await async_client.post(
52124
self.url(),

argilla/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ These are the section headers that we use:
1919
### Added
2020

2121
- Return similarity score when searching by similarity. ([#5778](https://github.com/argilla-io/argilla/pull/5778))
22+
- Added support to create users with predefined ids. ([#5786](https://github.com/argilla-io/argilla/pull/5786))
23+
- Added support to create workspaces with predefined ids. ([#5786](https://github.com/argilla-io/argilla/pull/5786))
24+
2225

2326
## [2.6.0](https://github.com/argilla-io/argilla/compare/v2.5.0...v2.6.0)
2427

argilla/docs/how_to_guides/migrate_from_legacy_datasets.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,26 +69,30 @@ Next, recreate the users and workspaces on the Argilla V2 server:
6969
```python
7070
for workspace in workspaces_v1:
7171
rg.Workspace(
72-
name=workspace.name
72+
id=workspace.id,
73+
name=workspace.name,
7374
).create()
7475
```
7576

7677
```python
7778
for user in users_v1:
78-
user = rg.User(
79+
user_v2 = rg.User(
80+
id=user.id,
7981
username=user.username,
8082
first_name=user.first_name,
8183
last_name=user.last_name,
8284
role=user.role,
8385
password="<your_chosen_password>" # (1)
8486
).create()
87+
8588
if user.role == "owner":
8689
continue
8790

88-
for workspace_name in user.workspaces:
89-
if workspace_name != user.name:
90-
workspace = client.workspaces(name=workspace_name)
91-
user.add_to_workspace(workspace)
91+
for workspace in user.workspaces:
92+
workspace_v2 = client.workspaces(name=workspace.name)
93+
if workspace_v2 is None:
94+
continue
95+
user.add_to_workspace(workspace_v2)
9296
```
9397

9498
1. You need to chose a new password for the user, to do this programmatically you can use the `uuid` package to generate a random password. Take care to keep track of the passwords you chose, since you will not be able to retrieve them later.

argilla/src/argilla/_api/_workspaces.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class WorkspacesAPI(ResourceAPI[WorkspaceModel]):
3535
@api_error_handler
3636
def create(self, workspace: WorkspaceModel) -> WorkspaceModel:
3737
# TODO: Unify API endpoint
38-
response = self.http_client.post(url="/api/v1/workspaces", json={"name": workspace.name})
38+
response = self.http_client.post(url="/api/v1/workspaces", json=workspace.model_dump())
3939
response.raise_for_status()
4040
response_json = response.json()
4141
workspace = self._model_from_json(json_workspace=response_json)

argilla/src/argilla/users/_resource.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ def __init__(
4545
last_name: Optional[str] = None,
4646
role: Optional[str] = None,
4747
password: Optional[str] = None,
48-
client: Optional["Argilla"] = None,
4948
id: Optional[UUID] = None,
49+
client: Optional["Argilla"] = None,
5050
_model: Optional[UserModel] = None,
5151
) -> None:
5252
"""Initializes a User object with a client and a username
@@ -57,6 +57,7 @@ def __init__(
5757
last_name (str): The last name of the user
5858
role (str): The role of the user, either 'annotator', admin, or 'owner'
5959
password (str): The password of the user
60+
id (UUID): The ID of the user. If provided before a .create, the will be created with this ID
6061
client (Argilla): The client used to interact with Argilla
6162
6263
Returns:

0 commit comments

Comments
 (0)