Skip to content

Commit 5f3e1c9

Browse files
authored
[FEATURE-BRANCH] refactor: improve API v1 error handling (#4887)
# Description This is the feature branch for all the issues related with improving error handling. The code changes should improve the code without modifying API v1 behavior at all. This means that error responses should be the same. Closes #4871 **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) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [x] Refactor (change restructuring the codebase without changing functionality) - [ ] Improvement (change adding some improvement to an existing functionality) - [ ] Documentation update **How Has This Been Tested** (Please describe the tests that you ran to verify your changes. And ideally, reference `tests`) - [x] Modifying and improving already existent tests. **Checklist** - [ ] I added relevant documentation - [ ] follows the style guidelines of this project - [ ] I did a self-review of my code - [ ] I made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I filled out [the contributor form](https://tally.so/r/n9XrxK) (see text above) - [ ] I have added relevant notes to the CHANGELOG.md file (See https://keepachangelog.com/)
1 parent c7073d7 commit 5f3e1c9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1166
-1162
lines changed

argilla-server/src/argilla_server/apis/errors/v1/exception_handlers.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,50 @@
1919

2020

2121
def add_exception_handlers(app: FastAPI):
22+
@app.exception_handler(errors.NotFoundError)
23+
async def not_found_error_exception_handler(request, exc):
24+
return JSONResponse(
25+
status_code=status.HTTP_404_NOT_FOUND,
26+
# TODO: Once we move to v2.0 we can remove the content using detail attribute
27+
# and use the new one using code and message.
28+
# content={"code": exc.code, "message": exc.message},
29+
content={"detail": exc.message},
30+
)
31+
32+
@app.exception_handler(errors.NotUniqueError)
33+
async def not_unique_error_exception_handler(request, exc):
34+
return JSONResponse(
35+
status_code=status.HTTP_409_CONFLICT,
36+
# TODO: Once we move to v2.0 we can remove the content using detail attribute
37+
# and use the new one using code and message.
38+
# content={"code": exc.code, "message": exc.message},
39+
content={"detail": exc.message},
40+
)
41+
2242
@app.exception_handler(errors.UnprocessableEntityError)
2343
async def unprocessable_entity_error_exception_handler(request, exc):
44+
return JSONResponse(
45+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
46+
# TODO: Once we move to v2.0 we can remove the content using detail attribute
47+
# and use the new one using code and message.
48+
# content={"code": exc.code, "message": exc.message},
49+
content={"detail": exc.message},
50+
)
51+
52+
# TODO: This is a temporary exception handler for ValueError exceptions.
53+
# This is because we are using ValueError exceptions in some places and we want to
54+
# return a 422 status code instead of a 500 status code.
55+
# This exception handler should be removed once we move to v2.0 and we use UnprocessableEntityError.
56+
@app.exception_handler(ValueError)
57+
async def value_error_exception_handler(request, exc):
58+
return JSONResponse(
59+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
60+
content={"detail": str(exc)},
61+
)
62+
63+
# TODO: Once we move to v2.0 we can remove this exception handler and use UnprocessableEntityError
64+
@app.exception_handler(errors.MissingVectorError)
65+
async def missing_vector_error_exception_handler(request, exc):
2466
return JSONResponse(
2567
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
2668
content={"code": exc.code, "message": exc.message},

argilla-server/src/argilla_server/apis/v0/handlers/users.py

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,26 @@
1919
from fastapi import APIRouter, Depends, HTTPException, Request, Security, status
2020
from sqlalchemy.ext.asyncio import AsyncSession
2121

22-
from argilla_server import models, telemetry
22+
from argilla_server import telemetry
2323
from argilla_server.contexts import accounts
2424
from argilla_server.database import get_async_db
2525
from argilla_server.errors import EntityAlreadyExistsError, EntityNotFoundError
2626
from argilla_server.errors.future import NotUniqueError
27+
from argilla_server.models import User
2728
from argilla_server.policies import UserPolicy, authorize
2829
from argilla_server.pydantic_v1 import parse_obj_as
29-
from argilla_server.schemas.v0.users import User, UserCreate
30+
from argilla_server.schemas.v0.users import User as UserSchema
31+
from argilla_server.schemas.v0.users import UserCreate
3032
from argilla_server.security import auth
3133

3234
router = APIRouter(tags=["users"])
3335

3436

35-
@router.get("/me", response_model=User, response_model_exclude_none=True, operation_id="whoami")
37+
@router.get("/me", response_model=UserSchema, response_model_exclude_none=True, operation_id="whoami")
3638
async def whoami(
3739
request: Request,
3840
db: AsyncSession = Depends(get_async_db),
39-
current_user: models.User = Security(auth.get_current_user),
41+
current_user: User = Security(auth.get_current_user),
4042
):
4143
"""
4244
User info endpoint
@@ -56,7 +58,7 @@ async def whoami(
5658

5759
await telemetry.track_login(request, current_user)
5860

59-
user = User.from_orm(current_user)
61+
user = UserSchema.from_orm(current_user)
6062
# TODO: The current client checks if a user can work on a specific workspace
6163
# by using workspaces info returning in `/api/me`.
6264
# Returning all workspaces from the `/api/me` for owner users keeps the
@@ -71,23 +73,25 @@ async def whoami(
7173
return user
7274

7375

74-
@router.get("/users", response_model=List[User], response_model_exclude_none=True)
76+
@router.get("/users", response_model=List[UserSchema], response_model_exclude_none=True)
7577
async def list_users(
76-
*, db: AsyncSession = Depends(get_async_db), current_user: models.User = Security(auth.get_current_user)
78+
*,
79+
db: AsyncSession = Depends(get_async_db),
80+
current_user: User = Security(auth.get_current_user),
7781
):
7882
await authorize(current_user, UserPolicy.list)
7983

8084
users = await accounts.list_users(db)
8185

82-
return parse_obj_as(List[User], users)
86+
return parse_obj_as(List[UserSchema], users)
8387

8488

85-
@router.post("/users", response_model=User, response_model_exclude_none=True)
89+
@router.post("/users", response_model=UserSchema, response_model_exclude_none=True)
8690
async def create_user(
8791
*,
8892
db: AsyncSession = Depends(get_async_db),
8993
user_create: UserCreate,
90-
current_user: models.User = Security(auth.get_current_user),
94+
current_user: User = Security(auth.get_current_user),
9195
):
9296
await authorize(current_user, UserPolicy.create)
9397

@@ -96,32 +100,32 @@ async def create_user(
96100

97101
telemetry.track_user_created(user)
98102
except NotUniqueError:
99-
raise EntityAlreadyExistsError(name=user_create.username, type=User)
103+
raise EntityAlreadyExistsError(name=user_create.username, type=UserSchema)
100104
except Exception as e:
101105
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e))
102106

103107
await user.awaitable_attrs.workspaces
104108

105-
return User.from_orm(user)
109+
return user
106110

107111

108-
@router.delete("/users/{user_id}", response_model=User, response_model_exclude_none=True)
112+
@router.delete("/users/{user_id}", response_model=UserSchema, response_model_exclude_none=True)
109113
async def delete_user(
110114
*,
111115
db: AsyncSession = Depends(get_async_db),
112116
user_id: UUID,
113-
current_user: models.User = Security(auth.get_current_user),
117+
current_user: User = Security(auth.get_current_user),
114118
):
115-
user = await accounts.get_user_by_id(db, user_id)
119+
user = await User.get(db, user_id)
116120
if not user:
117121
# TODO: Forcing here user_id to be an string.
118122
# Not casting it is causing a `Object of type UUID is not JSON serializable`.
119123
# Possible solution redefining JSONEncoder.default here:
120124
# https://github.com/jazzband/django-push-notifications/issues/586
121-
raise EntityNotFoundError(name=str(user_id), type=User)
125+
raise EntityNotFoundError(name=str(user_id), type=UserSchema)
122126

123127
await authorize(current_user, UserPolicy.delete(user))
124128

125129
await accounts.delete_user(db, user)
126130

127-
return User.from_orm(user)
131+
return user

argilla-server/src/argilla_server/apis/v0/handlers/workspaces.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,18 @@
2222
from argilla_server.database import get_async_db
2323
from argilla_server.errors import EntityAlreadyExistsError, EntityNotFoundError
2424
from argilla_server.errors.future import NotUniqueError
25+
from argilla_server.models import User, Workspace, WorkspaceUser
2526
from argilla_server.policies import WorkspacePolicy, WorkspaceUserPolicy, authorize
2627
from argilla_server.pydantic_v1 import parse_obj_as
27-
from argilla_server.schemas.v0.users import User
28-
from argilla_server.schemas.v0.workspaces import Workspace, WorkspaceCreate
28+
from argilla_server.schemas.v0.users import User as UserSchema
29+
from argilla_server.schemas.v0.workspaces import Workspace as WorkspaceSchema
30+
from argilla_server.schemas.v0.workspaces import WorkspaceCreate
2931
from argilla_server.security import auth
3032

3133
router = APIRouter(tags=["workspaces"])
3234

3335

34-
@router.post("/workspaces", response_model=Workspace, response_model_exclude_none=True)
36+
@router.post("/workspaces", response_model=WorkspaceSchema, response_model_exclude_none=True)
3537
async def create_workspace(
3638
*,
3739
db: AsyncSession = Depends(get_async_db),
@@ -45,10 +47,10 @@ async def create_workspace(
4547
except NotUniqueError:
4648
raise EntityAlreadyExistsError(name=workspace_create.name, type=Workspace)
4749

48-
return Workspace.from_orm(workspace)
50+
return workspace
4951

5052

51-
@router.get("/workspaces/{workspace_id}/users", response_model=List[User], response_model_exclude_none=True)
53+
@router.get("/workspaces/{workspace_id}/users", response_model=List[UserSchema], response_model_exclude_none=True)
5254
async def list_workspace_users(
5355
*,
5456
db: AsyncSession = Depends(get_async_db),
@@ -57,17 +59,18 @@ async def list_workspace_users(
5759
):
5860
await authorize(current_user, WorkspaceUserPolicy.list(workspace_id))
5961

60-
workspace = await accounts.get_workspace_by_id(db, workspace_id)
62+
workspace = await Workspace.get(db, workspace_id)
6163
if not workspace:
6264
raise EntityNotFoundError(name=str(workspace_id), type=Workspace)
6365

6466
await workspace.awaitable_attrs.users
6567
for user in workspace.users:
6668
await user.awaitable_attrs.workspaces
67-
return parse_obj_as(List[User], workspace.users)
6869

70+
return parse_obj_as(List[UserSchema], workspace.users)
6971

70-
@router.post("/workspaces/{workspace_id}/users/{user_id}", response_model=User, response_model_exclude_none=True)
72+
73+
@router.post("/workspaces/{workspace_id}/users/{user_id}", response_model=UserSchema, response_model_exclude_none=True)
7174
async def create_workspace_user(
7275
*,
7376
db: AsyncSession = Depends(get_async_db),
@@ -77,40 +80,42 @@ async def create_workspace_user(
7780
):
7881
await authorize(current_user, WorkspaceUserPolicy.create)
7982

80-
workspace = await accounts.get_workspace_by_id(db, workspace_id)
83+
workspace = await Workspace.get(db, workspace_id)
8184
if not workspace:
8285
raise EntityNotFoundError(name=str(workspace_id), type=Workspace)
8386

84-
user = await accounts.get_user_by_id(db, user_id)
87+
user = await User.get(db, user_id)
8588
if not user:
86-
raise EntityNotFoundError(name=str(user_id), type=User)
89+
raise EntityNotFoundError(name=str(user_id), type=UserSchema)
8790

8891
try:
8992
workspace_user = await accounts.create_workspace_user(db, {"workspace_id": workspace_id, "user_id": user_id})
9093
except NotUniqueError:
91-
raise EntityAlreadyExistsError(name=str(user_id), type=User)
94+
raise EntityAlreadyExistsError(name=str(user_id), type=UserSchema)
9295

9396
await db.refresh(user, attribute_names=["workspaces"])
9497

95-
return User.from_orm(workspace_user.user)
98+
return workspace_user.user
9699

97100

98-
@router.delete("/workspaces/{workspace_id}/users/{user_id}", response_model=User, response_model_exclude_none=True)
101+
@router.delete(
102+
"/workspaces/{workspace_id}/users/{user_id}", response_model=UserSchema, response_model_exclude_none=True
103+
)
99104
async def delete_workspace_user(
100105
*,
101106
db: AsyncSession = Depends(get_async_db),
102107
workspace_id: UUID,
103108
user_id: UUID,
104109
current_user: User = Security(auth.get_current_user),
105110
):
106-
workspace_user = await accounts.get_workspace_user_by_workspace_id_and_user_id(db, workspace_id, user_id)
111+
workspace_user = await WorkspaceUser.get_by(db, workspace_id=workspace_id, user_id=user_id)
107112
if not workspace_user:
108-
raise EntityNotFoundError(name=str(user_id), type=User)
113+
raise EntityNotFoundError(name=str(user_id), type=UserSchema)
109114

110115
await authorize(current_user, WorkspaceUserPolicy.delete(workspace_user))
111116

112117
user = await workspace_user.awaitable_attrs.user
113118
await accounts.delete_workspace_user(db, workspace_user)
114119
await db.refresh(user, attribute_names=["workspaces"])
115120

116-
return User.from_orm(user)
121+
return user

0 commit comments

Comments
 (0)