Skip to content

Commit 62a1fd0

Browse files
sean-hickey-wfpre-commit-ci[bot]frascuchonjfcalvo
authored
[FEATURE]: Adding Functionality To Update Users (#5615)
# Description Argilla offers the ability to create and delete users but not the ability to update a User object after it has been created. For example, if we want to update the Role of a user after they have been created (from annotator to admin for example), this is not possible without deleting and recreating the User. This PR adds an update endpoint to the FastAPI server and also the convenience of doing this through the python sdk also Closes #<issue_number> **Type of change** <!-- Please delete options that are not relevant. Remember to title the PR according to the type of change --> - New feature (non-breaking change which adds functionality) - Improvement (change adding some improvement to an existing functionality) **How Has This Been Tested** Tests have been added at both the server and SDK level to ensure that the update method is working as expected **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/) --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Paco Aranda <[email protected]> Co-authored-by: Paco Aranda <[email protected]> Co-authored-by: Francisco Aranda <[email protected]> Co-authored-by: José Francisco Calvo <[email protected]>
1 parent 90f3c85 commit 62a1fd0

File tree

9 files changed

+378
-7
lines changed

9 files changed

+378
-7
lines changed

argilla-server/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ These are the section headers that we use:
2929

3030
### Changed
3131

32+
- API endpoint added to the User router to allow updates to User objects ([#5615](https://github.com/argilla-io/argilla/pull/5615))
3233
- Changed default python version to 3.13. ([#5649](https://github.com/argilla-io/argilla/pull/5649))
3334
- Changed Pydantic version to v2. ([#5666](https://github.com/argilla-io/argilla/pull/5666))
3435

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
from argilla_server.api.policies.v1 import UserPolicy, authorize
2121
from argilla_server.api.schemas.v1.users import User as UserSchema
22-
from argilla_server.api.schemas.v1.users import UserCreate, Users
22+
from argilla_server.api.schemas.v1.users import UserCreate, Users, UserUpdate
2323
from argilla_server.api.schemas.v1.workspaces import Workspaces
2424
from argilla_server.contexts import accounts
2525
from argilla_server.database import get_async_db
@@ -89,6 +89,21 @@ async def delete_user(
8989
return await accounts.delete_user(db, user)
9090

9191

92+
@router.patch("/users/{user_id}", status_code=status.HTTP_200_OK, response_model=UserSchema)
93+
async def update_user(
94+
*,
95+
db: AsyncSession = Depends(get_async_db),
96+
user_id: UUID,
97+
user_update: UserUpdate,
98+
current_user: User = Security(auth.get_current_user),
99+
):
100+
user = await User.get_or_raise(db, user_id)
101+
102+
await authorize(current_user, UserPolicy.update)
103+
104+
return await accounts.update_user(db, user, user_update.model_dump(exclude_unset=True))
105+
106+
92107
@router.get("/users/{user_id}/workspaces", response_model=Workspaces)
93108
async def list_user_workspaces(
94109
*,

argilla-server/src/argilla_server/api/policies/v1/user_policy.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ async def list(cls, actor: User) -> bool:
2828
async def create(cls, actor: User) -> bool:
2929
return actor.is_owner
3030

31+
@classmethod
32+
async def update(cls, actor: User) -> bool:
33+
return actor.is_owner
34+
3135
@classmethod
3236
async def delete(cls, actor: User) -> bool:
3337
return actor.is_owner

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

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,35 @@
1313
# limitations under the License.
1414

1515
from datetime import datetime
16-
from typing import List, Optional
16+
from typing import Annotated, List, Optional
1717
from uuid import UUID
1818

1919
from pydantic import BaseModel, Field, constr, ConfigDict
2020

21+
from argilla_server.api.schemas.v1.commons import UpdateSchema
2122
from argilla_server.enums import UserRole
2223

2324
USER_PASSWORD_MIN_LENGTH = 8
2425
USER_PASSWORD_MAX_LENGTH = 100
2526

27+
UserFirstName = Annotated[
28+
constr(min_length=1, strip_whitespace=True), Field(..., description="The first name for the user")
29+
]
30+
UserLastName = Annotated[
31+
constr(min_length=1, strip_whitespace=True), Field(..., description="The last name for the user")
32+
]
33+
UserUsername = Annotated[str, Field(..., min_length=1, description="The username for the user")]
34+
35+
UserPassword = Annotated[
36+
str,
37+
Field(
38+
...,
39+
min_length=USER_PASSWORD_MIN_LENGTH,
40+
max_length=USER_PASSWORD_MAX_LENGTH,
41+
description="The password for the user",
42+
),
43+
]
44+
2645

2746
class User(BaseModel):
2847
id: UUID
@@ -40,11 +59,21 @@ class User(BaseModel):
4059

4160

4261
class UserCreate(BaseModel):
43-
username: str = Field(..., min_length=1)
44-
password: str = Field(min_length=USER_PASSWORD_MIN_LENGTH, max_length=USER_PASSWORD_MAX_LENGTH)
45-
first_name: constr(min_length=1, strip_whitespace=True)
46-
last_name: Optional[constr(min_length=1, strip_whitespace=True)] = None
62+
first_name: UserFirstName
63+
last_name: Optional[UserLastName] = None
64+
username: UserUsername
65+
role: Optional[UserRole] = None
66+
password: UserPassword
67+
68+
69+
class UserUpdate(UpdateSchema):
70+
__non_nullable_fields__ = {"first_name", "username", "role", "password"}
71+
72+
first_name: Optional[UserFirstName] = None
73+
last_name: Optional[UserLastName] = None
74+
username: Optional[UserUsername] = None
4775
role: Optional[UserRole] = None
76+
password: Optional[UserPassword] = None
4877

4978

5079
class Users(BaseModel):

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,18 @@ async def create_user_with_random_password(
154154
return await create_user(db, user_attrs, workspaces)
155155

156156

157+
async def update_user(db: AsyncSession, user: User, user_attrs: dict) -> User:
158+
username = user_attrs.get("username")
159+
if username is not None and username != user.username:
160+
if await get_user_by_username(db, username):
161+
raise UnprocessableEntityError(f"Username {username!r} already exists")
162+
163+
if "password" in user_attrs:
164+
user_attrs["password_hash"] = hash_password(user_attrs.pop("password"))
165+
166+
return await user.update(db, **user_attrs)
167+
168+
157169
async def delete_user(db: AsyncSession, user: User) -> User:
158170
return await user.delete(db)
159171

0 commit comments

Comments
 (0)