Skip to content

Commit c46a5f3

Browse files
committed
✨ Add (module): User router to include its path operations
1 parent 1953a0e commit c46a5f3

File tree

3 files changed

+289
-23
lines changed

3 files changed

+289
-23
lines changed

app/api/api_v1/router/user.py

Lines changed: 246 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,251 @@
44
for users.
55
"""
66

7-
from fastapi import APIRouter
7+
import logging
8+
from typing import Annotated, Optional
89

10+
from fastapi import APIRouter, Body, Depends, HTTPException, Response, status
11+
from fastapi.params import Path, Query
12+
from pydantic import NonNegativeInt, PositiveInt
13+
14+
from app.config.exceptions import (
15+
DatabaseException,
16+
NotFoundException,
17+
ServiceException,
18+
)
19+
from app.config.init_settings import init_setting
20+
from app.crud.user import UserRepository, get_user_repository
21+
from app.schemas.user import User, UserCreate, UserUpdate, UsersResponse
22+
23+
logger: logging.Logger = logging.getLogger(__name__)
924
router: APIRouter = APIRouter(prefix="/user", tags=["user"])
25+
26+
27+
@router.get("", response_model=UsersResponse)
28+
def get_users(
29+
user_repository: Annotated[UserRepository, Depends(get_user_repository)],
30+
skip: Annotated[
31+
NonNegativeInt,
32+
Query(
33+
annotation=Optional[NonNegativeInt],
34+
title="Skip",
35+
description="Skip users",
36+
example=0,
37+
openapi_examples=init_setting.SKIP_EXAMPLES,
38+
),
39+
] = 0,
40+
limit: Annotated[
41+
PositiveInt | None,
42+
Query(
43+
annotation=Optional[PositiveInt],
44+
title="Limit",
45+
description="Limit pagination",
46+
ge=1,
47+
le=100,
48+
example=100,
49+
openapi_examples=init_setting.LIMIT_EXAMPLES,
50+
),
51+
] = 100,
52+
) -> UsersResponse:
53+
"""
54+
Retrieve all users' basic information from the system using
55+
pagination.
56+
## Parameters:
57+
- `:param skip:` **Offset from where to start returning users**
58+
- `:type skip:` **NonNegativeInt**
59+
- `:param limit:` **Limit the number of results from query**
60+
- `:type limit:` **PositiveInt**
61+
## Response:
62+
- `:return:` **List of Users retrieved from database**
63+
- `:rtype:` **UsersResponse**
64+
\f
65+
:param user_repository: Dependency method for user service layer
66+
:type user_repository: UserRepository
67+
"""
68+
try:
69+
found_users: list[User] = user_repository.read_users(skip, limit)
70+
except ServiceException as exc:
71+
logger.error(exc)
72+
raise HTTPException(
73+
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
74+
) from exc
75+
users: UsersResponse = UsersResponse(users=found_users)
76+
return users
77+
78+
79+
@router.post("", response_model=User, status_code=status.HTTP_201_CREATED)
80+
def create_user(
81+
user: Annotated[
82+
UserCreate,
83+
Body(
84+
...,
85+
title="User data",
86+
description="User data to create",
87+
openapi_examples=init_setting.USER_CREATE_EXAMPLES,
88+
),
89+
],
90+
user_repository: Annotated[UserRepository, Depends(get_user_repository)],
91+
) -> User:
92+
"""
93+
Register new user into the system.
94+
## Parameter:
95+
- `:param user:` **Body Object for user creation.**
96+
- `:type user:` **UserCreate**
97+
## Response:
98+
- `:return:` **User created with its data**
99+
- `:rtype:` **User**
100+
\f
101+
:param user_repository: Dependency method for user service layer
102+
:type user_repository: UserRepository
103+
"""
104+
try:
105+
new_user: User | None = user_repository.create_user(user)
106+
except ServiceException as exc:
107+
detail: str = "Error at creating user."
108+
logger.error(detail)
109+
raise HTTPException(
110+
status_code=status.HTTP_400_BAD_REQUEST, detail=detail
111+
) from exc
112+
if not new_user:
113+
raise HTTPException(
114+
status_code=status.HTTP_404_NOT_FOUND,
115+
detail="User could not be created",
116+
)
117+
return new_user
118+
119+
120+
@router.get("/{user_id}", response_model=User)
121+
def get_user_by_id(
122+
user_repository: Annotated[UserRepository, Depends(get_user_repository)],
123+
user_id: Annotated[
124+
PositiveInt,
125+
Path(
126+
...,
127+
title="User ID",
128+
annotation=PositiveInt,
129+
description="ID of the User to be searched",
130+
example=1,
131+
),
132+
],
133+
) -> User:
134+
"""
135+
Retrieve an existing user's information given their user ID.
136+
## Parameter:
137+
- `:param user_id:` **Unique identifier of the user to be retrieved**
138+
- `:type user_id:` **PositiveInt**
139+
## Response:
140+
- `:return:` **Found user with the given ID.**
141+
- `:rtype:` **User**
142+
\f
143+
:param user_repository: Dependency method for user service layer
144+
:type user_repository: UserRepository
145+
"""
146+
try:
147+
user: User = user_repository.read_by_id(user_id)
148+
except ServiceException as exc:
149+
detail: str = f"User with id {user_id} not found in the system."
150+
logger.error(detail)
151+
raise HTTPException(
152+
status_code=status.HTTP_404_NOT_FOUND, detail=detail
153+
) from exc
154+
except NotFoundException as not_found_exc:
155+
logger.error(not_found_exc)
156+
raise HTTPException(
157+
status_code=status.HTTP_404_NOT_FOUND, detail=str(not_found_exc)
158+
) from not_found_exc
159+
return user
160+
161+
162+
@router.put("/{user_id}", response_model=User)
163+
def update_user(
164+
user_repository: Annotated[UserRepository, Depends(get_user_repository)],
165+
user_id: Annotated[
166+
PositiveInt,
167+
Path(
168+
...,
169+
title="User ID",
170+
annotation=PositiveInt,
171+
description="ID of the User to be searched",
172+
example=1,
173+
),
174+
],
175+
user_in: Annotated[
176+
UserUpdate,
177+
Body(
178+
...,
179+
title="User data",
180+
description="New user data to update",
181+
openapi_examples=init_setting.USER_UPDATE_EXAMPLES,
182+
),
183+
],
184+
) -> User | None:
185+
"""
186+
Update an existing user's information given their user ID and new
187+
information.
188+
## Parameters:
189+
- `:param user_id:` **Unique identifier of the user to be updated**
190+
- `:type user_id:` **PositiveInt**
191+
- `:param user_in:` **New user data to update that can include:
192+
username, email, first_name, middle_name, last_name, password,
193+
gender, birthdate, phone_number, city and country.**
194+
- `:type user_in:` **UserUpdate**
195+
## Response:
196+
- `:return:` **Updated user with the given ID and its data**
197+
- `:rtype:` **User**
198+
\f
199+
:param user_repository: Dependency method for user service layer
200+
:type user_repository: UserRepository
201+
"""
202+
try:
203+
user: User | None = user_repository.update_user(user_id, user_in)
204+
except ServiceException as exc:
205+
detail: str = f"User with id {user_id} not found in the system."
206+
logger.error(detail)
207+
raise HTTPException(
208+
status_code=status.HTTP_400_BAD_REQUEST, detail=detail
209+
) from exc
210+
return user
211+
212+
213+
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
214+
def delete_user(
215+
user_repository: Annotated[UserRepository, Depends(get_user_repository)],
216+
user_id: Annotated[
217+
PositiveInt,
218+
Path(
219+
...,
220+
title="User ID",
221+
annotation=PositiveInt,
222+
description="ID of the User to be searched",
223+
example=1,
224+
),
225+
],
226+
) -> Response:
227+
"""
228+
Delete an existing user given their user ID.
229+
## Parameter:
230+
- `:param user_id:` **Unique identifier of the user to be deleted**
231+
- `:type user_id:` **PositiveInt**
232+
## Response:
233+
- `:return:` **Json Response object with the deleted information**
234+
- `:rtype:` **Response**
235+
\f
236+
:param user_repository: Dependency method for user service layer
237+
:type user_repository: UserRepository
238+
"""
239+
try:
240+
delete_result = user_repository.delete_user(user_id)
241+
except DatabaseException as exc:
242+
logger.error(f"Failed to delete user with ID {user_id}: {exc}")
243+
raise HTTPException(
244+
status_code=status.HTTP_404_NOT_FOUND,
245+
detail=f"User with ID {user_id} not found.",
246+
) from exc
247+
response: Response = Response(status_code=status.HTTP_204_NO_CONTENT)
248+
response.headers["deleted"] = delete_result["deleted"].lower()
249+
response.headers["deleted_at"] = (
250+
delete_result["deleted_at"].isoformat()
251+
if delete_result["deleted_at"]
252+
else "null"
253+
)
254+
return response

app/config/exceptions.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from sqlalchemy.exc import SQLAlchemyError
66

77

8-
class DatabaseException(SQLAlchemyError): # type: ignore
8+
class DatabaseException(SQLAlchemyError):
99
"""
1010
Database Exception class
1111
"""
@@ -16,7 +16,7 @@ def __init__(self, message: str, note: str | None = None):
1616
self.add_note(note)
1717

1818

19-
class ServiceException(Exception): # type: ignore
19+
class ServiceException(Exception):
2020
"""
2121
Service Layer Exception class
2222
"""
@@ -25,3 +25,14 @@ def __init__(self, message: str, note: str | None = None):
2525
super().__init__(message)
2626
if note:
2727
self.add_note(note)
28+
29+
30+
class NotFoundException(Exception):
31+
"""
32+
Not Found Exception class
33+
"""
34+
35+
def __init__(self, message: str, note: str | None = None):
36+
super().__init__(message)
37+
if note:
38+
self.add_note(note)

app/crud/user.py

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@
1010
from pydantic import NonNegativeInt, PositiveInt
1111
from sqlalchemy import Row, RowMapping, select
1212
from sqlalchemy.engine import ScalarResult
13-
from sqlalchemy.exc import SQLAlchemyError
13+
from sqlalchemy.exc import NoResultFound, SQLAlchemyError
1414
from sqlalchemy.orm import Session
1515
from sqlalchemy.sql import Select
1616

1717
from app.config.exceptions import DatabaseException
1818
from app.db.session import get_session
1919
from app.models.user import User
20+
from app.schemas.user import User as UserSchema
2021
from app.schemas.user import UserCreate, UserUpdate
2122

2223
logger: logging.Logger = logging.getLogger(__name__)
@@ -59,8 +60,8 @@ def read_by_id(self, _id: PositiveInt) -> User:
5960
def read_users(
6061
self,
6162
offset: NonNegativeInt,
62-
limit: PositiveInt,
63-
) -> list[User]:
63+
limit: PositiveInt | None,
64+
) -> list[UserSchema]:
6465
"""
6566
Retrieve a list of users from the database, with pagination
6667
:param offset: The number of users to skip before starting to
@@ -69,7 +70,7 @@ def read_users(
6970
:param limit: The maximum number of users to return
7071
:type limit: PositiveInt
7172
:return: A list of users
72-
:rtype: list[User]
73+
:rtype: list[UserSchema]
7374
"""
7475
stmt: Select[tuple[User]] = select(User).offset(offset).limit(limit)
7576
with self.session as session:
@@ -82,7 +83,7 @@ def read_users(
8283
except SQLAlchemyError as sa_exc:
8384
logger.error(sa_exc)
8485
raise DatabaseException(str(sa_exc)) from sa_exc
85-
return users
86+
return [UserSchema.model_validate(user) for user in users]
8687

8788
def create_user(
8889
self,
@@ -146,31 +147,40 @@ def update_user(self, _id: PositiveInt, user: UserUpdate) -> User | None:
146147
raise DatabaseException(str(db_exc)) from db_exc
147148
return updated_user
148149

149-
def delete_user(self, _id: PositiveInt) -> bool:
150+
def delete_user(self, _id: PositiveInt) -> dict[str, Any]:
150151
"""
151152
Delete a user from the database
152153
:param _id: The id of the user to delete
153154
:type _id: PositiveInt
154-
:return: True if the user is deleted; otherwise False
155-
:rtype: bool
155+
:return: Data to confirmation info about the delete process
156+
:rtype: dict[str, Any]
156157
"""
157158
with self.session as session:
158159
try:
159-
found_user: User | None = self.read_by_id(_id)
160-
except DatabaseException as db_exc:
161-
raise DatabaseException(str(db_exc)) from db_exc
162-
if not found_user:
163-
raise DatabaseException(
164-
f"User with ID: {_id} could not be deleted"
160+
exists_query = (
161+
session.query(User).filter(User.id == _id).exists()
162+
)
163+
if not session.query(exists_query).scalar():
164+
raise NoResultFound(f"No user found with ID: {_id}")
165+
delete_query = (
166+
session.query(User).filter(User.id == _id).delete()
165167
)
166-
try:
167-
session.delete(found_user)
168168
session.commit()
169-
except SQLAlchemyError as sa_exc:
170-
logger.error(sa_exc)
169+
if delete_query == 0:
170+
raise NoResultFound(
171+
f"No user found with ID: {_id} to delete"
172+
)
173+
deleted: bool = True
174+
deleted_at: datetime = datetime.now()
175+
except (SQLAlchemyError, NoResultFound) as e:
171176
session.rollback()
172-
return False
173-
return True
177+
logger.error(
178+
f"Failed to delete user with ID: {_id}, Error: {str(e)}"
179+
)
180+
raise DatabaseException(
181+
f"Could not delete user with ID: {_id}. Error: {str(e)}"
182+
) from e
183+
return {"ok": deleted, "deleted_at": deleted_at}
174184

175185

176186
def get_user_repository() -> UserRepository:

0 commit comments

Comments
 (0)