Skip to content

Commit b31aa9e

Browse files
authored
Merge pull request #55 from igorbenav/better-exceptions
Better exceptions
2 parents 0d307c7 + a1de946 commit b31aa9e

File tree

13 files changed

+135
-107
lines changed

13 files changed

+135
-107
lines changed

README.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,6 @@ First, you may want to take a look at the project structure and understand what
430430
│ ├── api # Folder containing API-related logic.
431431
│ │ ├── __init__.py
432432
│ │ ├── dependencies.py # Defines dependencies for use across API endpoints.
433-
│ │ ├── exceptions.py # Custom exceptions for the API.
434433
│ │ ├── paginated.py # Utilities for API response pagination.
435434
│ │ │
436435
│ │ └── v1 # Version 1 of the API.
@@ -460,7 +459,8 @@ First, you may want to take a look at the project structure and understand what
460459
│ │ │
461460
│ │ ├── exceptions # Custom exception classes.
462461
│ │ │ ├── __init__.py
463-
│ │ │ └── exceptions.py # Definitions of custom exceptions.
462+
│ │ │ ├── cache_exceptions.py # Exceptions related to cache operations.
463+
│ │ │ └── http_exceptions.py # HTTP-related exceptions.
464464
│ │ │
465465
│ │ └── utils # Utility functions and helpers.
466466
│ │ ├── __init__.py
@@ -890,6 +890,33 @@ async def read_entities(
890890
)
891891
```
892892

893+
#### 5.7.2 HTTP Exceptions
894+
895+
To add exceptions you may just import from `app/core/exceptions/http_exceptions` and optionally add a detail:
896+
897+
```python
898+
from app.core.exceptions.http_exceptions import NotFoundException
899+
900+
# If you want to specify the detail, just add the message
901+
if not user:
902+
raise NotFoundException("User not found")
903+
904+
# Or you may just use the default message
905+
if not post:
906+
raise NotFoundException()
907+
```
908+
909+
**The predefined possibilities in http_exceptions are the following:**
910+
- `CustomException`: 500 internal error
911+
- `BadRequestException`: 400 bad request
912+
- `NotFoundException`: 404 not found
913+
- `ForbiddenException`: 403 forbidden
914+
- `UnauthorizedException`: 401 unauthorized
915+
- `UnprocessableEntityException`: 422 unprocessable entity
916+
- `DuplicateValueException`: 422 unprocessable entity
917+
- `RateLimitException`: 429 too many requests
918+
919+
893920
### 5.8 Caching
894921
The `cache` decorator allows you to cache the results of FastAPI endpoint functions, enhancing response times and reducing the load on your application by storing and retrieving data in a cache.
895922

src/app/api/dependencies.py

Lines changed: 5 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
Request
1212
)
1313

14-
from app.api.exceptions import credentials_exception, privileges_exception
14+
from app.core.exceptions.http_exceptions import UnauthorizedException, ForbiddenException, RateLimitException
1515
from app.core.db.database import async_get_db
1616
from app.core.logger import logging
1717
from app.core.schemas import TokenData
@@ -28,38 +28,13 @@
2828
DEFAULT_LIMIT = settings.DEFAULT_RATE_LIMIT_LIMIT
2929
DEFAULT_PERIOD = settings.DEFAULT_RATE_LIMIT_PERIOD
3030

31-
async def get_current_user(
32-
token: Annotated[str, Depends(oauth2_scheme)],
33-
db: Annotated[AsyncSession, Depends(async_get_db)]
34-
) -> dict:
35-
try:
36-
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
37-
username_or_email: str = payload.get("sub")
38-
if username_or_email is None:
39-
raise credentials_exception
40-
token_data = TokenData(username_or_email=username_or_email)
41-
42-
except JWTError:
43-
raise credentials_exception
44-
45-
if "@" in username_or_email:
46-
user = await crud_users.get(db=db, email=token_data.username_or_email)
47-
else:
48-
user = await crud_users.get(db=db, username=token_data.username_or_email)
49-
50-
if user and not user["is_deleted"]:
51-
return user
52-
53-
raise credentials_exception
54-
55-
5631
async def get_current_user(
5732
token: Annotated[str, Depends(oauth2_scheme)],
5833
db: Annotated[AsyncSession, Depends(async_get_db)]
5934
) -> dict:
6035
token_data = await verify_token(token, db)
6136
if token_data is None:
62-
raise credentials_exception
37+
raise UnauthorizedException("User not authenticated.")
6338

6439
if "@" in token_data.username_or_email:
6540
user = await crud_users.get(db=db, email=token_data.username_or_email, is_deleted=False)
@@ -69,7 +44,7 @@ async def get_current_user(
6944
if user:
7045
return user
7146

72-
raise credentials_exception
47+
raise UnauthorizedException("User not authenticated.")
7348

7449

7550
async def get_optional_user(
@@ -103,7 +78,7 @@ async def get_optional_user(
10378

10479
async def get_current_superuser(current_user: Annotated[User, Depends(get_current_user)]) -> dict:
10580
if not current_user["is_superuser"]:
106-
raise privileges_exception
81+
raise ForbiddenException("You do not have enough privileges.")
10782

10883
return current_user
10984

@@ -143,7 +118,4 @@ async def rate_limiter(
143118
period=period
144119
)
145120
if is_limited:
146-
raise HTTPException(
147-
status_code=429,
148-
detail="Rate limit exceeded"
149-
)
121+
raise RateLimitException("Rate limit exceeded.")

src/app/api/exceptions.py

Lines changed: 0 additions & 12 deletions
This file was deleted.

src/app/api/v1/login.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from app.core.db.database import async_get_db
1010
from app.core.schemas import Token
1111
from app.core.security import ACCESS_TOKEN_EXPIRE_MINUTES, create_access_token, authenticate_user
12-
from app.api.exceptions import credentials_exception
12+
from app.core.exceptions.http_exceptions import UnauthorizedException
1313

1414
router = fastapi.APIRouter(tags=["login"])
1515

@@ -24,7 +24,7 @@ async def login_for_access_token(
2424
db=db
2525
)
2626
if not user:
27-
raise credentials_exception
27+
raise UnauthorizedException("Wrong username, email or password.")
2828

2929
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
3030
access_token = await create_access_token(

src/app/api/v1/logout.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
from datetime import datetime
22

3-
from fastapi import APIRouter, Depends, HTTPException, status
3+
from fastapi import APIRouter, Depends, status
44
from sqlalchemy.ext.asyncio import AsyncSession
55
from jose import jwt, JWTError
66

77
from app.core.security import oauth2_scheme, SECRET_KEY, ALGORITHM
88
from app.core.db.database import async_get_db
99
from app.core.db.crud_token_blacklist import crud_token_blacklist
1010
from app.core.schemas import TokenBlacklistCreate
11+
from app.core.exceptions.http_exceptions import UnauthorizedException
1112

1213
router = APIRouter(tags=["login"])
1314

@@ -28,8 +29,4 @@ async def logout(
2829
return {"message": "Logged out successfully"}
2930

3031
except JWTError:
31-
raise HTTPException(
32-
status_code=status.HTTP_401_UNAUTHORIZED,
33-
detail="Invalid token",
34-
headers={"WWW-Authenticate": "Bearer"},
35-
)
32+
raise UnauthorizedException("Invalid token.")

src/app/api/v1/posts.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import Annotated
22

3-
from fastapi import Request, Depends, HTTPException
3+
from fastapi import Request, Depends
44
from sqlalchemy.ext.asyncio import AsyncSession
55
import fastapi
66

@@ -10,7 +10,7 @@
1010
from app.core.db.database import async_get_db
1111
from app.crud.crud_posts import crud_posts
1212
from app.crud.crud_users import crud_users
13-
from app.api.exceptions import privileges_exception
13+
from app.core.exceptions.http_exceptions import NotFoundException, ForbiddenException
1414
from app.core.utils.cache import cache
1515
from app.api.paginated import PaginatedListResponse, paginated_response, compute_offset
1616

@@ -26,10 +26,10 @@ async def write_post(
2626
):
2727
db_user = await crud_users.get(db=db, schema_to_select=UserRead, username=username, is_deleted=False)
2828
if db_user is None:
29-
raise HTTPException(status_code=404, detail="User not found")
29+
raise NotFoundException("User not found")
3030

3131
if current_user["id"] != db_user["id"]:
32-
raise privileges_exception
32+
raise ForbiddenException()
3333

3434
post_internal_dict = post.model_dump()
3535
post_internal_dict["created_by_user_id"] = db_user["id"]
@@ -53,7 +53,7 @@ async def read_posts(
5353
):
5454
db_user = await crud_users.get(db=db, schema_to_select=UserRead, username=username, is_deleted=False)
5555
if not db_user:
56-
raise HTTPException(status_code=404, detail="User not found")
56+
raise NotFoundException("User not found")
5757

5858
posts_data = await crud_posts.get_multi(
5959
db=db,
@@ -81,11 +81,11 @@ async def read_post(
8181
):
8282
db_user = await crud_users.get(db=db, schema_to_select=UserRead, username=username, is_deleted=False)
8383
if db_user is None:
84-
raise HTTPException(status_code=404, detail="User not found")
84+
raise NotFoundException("User not found")
8585

8686
db_post = await crud_posts.get(db=db, schema_to_select=PostRead, id=id, created_by_user_id=db_user["id"], is_deleted=False)
8787
if db_post is None:
88-
raise HTTPException(status_code=404, detail="Post not found")
88+
raise NotFoundException("Post not found")
8989

9090
return db_post
9191

@@ -106,14 +106,14 @@ async def patch_post(
106106
):
107107
db_user = await crud_users.get(db=db, schema_to_select=UserRead, username=username, is_deleted=False)
108108
if db_user is None:
109-
raise HTTPException(status_code=404, detail="User not found")
109+
raise NotFoundException("User not found")
110110

111111
if current_user["id"] != db_user["id"]:
112-
raise privileges_exception
112+
raise ForbiddenException()
113113

114114
db_post = await crud_posts.get(db=db, schema_to_select=PostRead, id=id, is_deleted=False)
115115
if db_post is None:
116-
raise HTTPException(status_code=404, detail="Post not found")
116+
raise NotFoundException("Post not found")
117117

118118
await crud_posts.update(db=db, object=values, id=id)
119119
return {"message": "Post updated"}
@@ -134,14 +134,14 @@ async def erase_post(
134134
):
135135
db_user = await crud_users.get(db=db, schema_to_select=UserRead, username=username, is_deleted=False)
136136
if db_user is None:
137-
raise HTTPException(status_code=404, detail="User not found")
137+
raise NotFoundException("User not found")
138138

139139
if current_user["id"] != db_user["id"]:
140-
raise privileges_exception
140+
raise ForbiddenException()
141141

142142
db_post = await crud_posts.get(db=db, schema_to_select=PostRead, id=id, is_deleted=False)
143143
if db_post is None:
144-
raise HTTPException(status_code=404, detail="Post not found")
144+
raise NotFoundException("Post not found")
145145

146146
await crud_posts.delete(db=db, db_row=db_post, id=id)
147147

@@ -162,11 +162,11 @@ async def erase_db_post(
162162
):
163163
db_user = await crud_users.get(db=db, schema_to_select=UserRead, username=username, is_deleted=False)
164164
if db_user is None:
165-
raise HTTPException(status_code=404, detail="User not found")
165+
raise NotFoundException("User not found")
166166

167167
db_post = await crud_posts.get(db=db, schema_to_select=PostRead, id=id, is_deleted=False)
168168
if db_post is None:
169-
raise HTTPException(status_code=404, detail="Post not found")
169+
raise NotFoundException("Post not found")
170170

171171
await crud_posts.db_delete(db=db, id=id)
172172
return {"message": "Post deleted from the database"}

src/app/api/v1/rate_limits.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from app.api.dependencies import get_current_superuser
88
from app.api.paginated import PaginatedListResponse, paginated_response, compute_offset
99
from app.core.db.database import async_get_db
10+
from app.core.exceptions.http_exceptions import NotFoundException, DuplicateValueException, RateLimitException
1011
from app.crud.crud_rate_limit import crud_rate_limits
1112
from app.crud.crud_tier import crud_tiers
1213
from app.schemas.rate_limit import (
@@ -27,14 +28,14 @@ async def write_rate_limit(
2728
):
2829
db_tier = await crud_tiers.get(db=db, name=tier_name)
2930
if not db_tier:
30-
raise HTTPException(status_code=404, detail="Tier not found")
31+
raise NotFoundException("Tier not found")
3132

3233
rate_limit_internal_dict = rate_limit.model_dump()
3334
rate_limit_internal_dict["tier_id"] = db_tier["id"]
3435

3536
db_rate_limit = await crud_rate_limits.exists(db=db, name=rate_limit_internal_dict["name"])
3637
if db_rate_limit:
37-
raise HTTPException(status_code=400, detail="Rate Limit Name not available")
38+
raise DuplicateValueException("Rate Limit Name not available")
3839

3940
rate_limit_internal = RateLimitCreateInternal(**rate_limit_internal_dict)
4041
return await crud_rate_limits.create(db=db, object=rate_limit_internal)
@@ -50,7 +51,7 @@ async def read_rate_limits(
5051
):
5152
db_tier = await crud_tiers.get(db=db, name=tier_name)
5253
if not db_tier:
53-
raise HTTPException(status_code=404, detail="Tier not found")
54+
raise NotFoundException("Tier not found")
5455

5556
rate_limits_data = await crud_rate_limits.get_multi(
5657
db=db,
@@ -76,7 +77,7 @@ async def read_rate_limit(
7677
):
7778
db_tier = await crud_tiers.get(db=db, name=tier_name)
7879
if not db_tier:
79-
raise HTTPException(status_code=404, detail="Tier not found")
80+
raise NotFoundException("Tier not found")
8081

8182
db_rate_limit = await crud_rate_limits.get(
8283
db=db,
@@ -85,7 +86,7 @@ async def read_rate_limit(
8586
id=id
8687
)
8788
if db_rate_limit is None:
88-
raise HTTPException(status_code=404, detail="Rate Limit not found")
89+
raise NotFoundException("Rate Limit not found")
8990

9091
return db_rate_limit
9192

@@ -100,7 +101,7 @@ async def patch_rate_limit(
100101
):
101102
db_tier = await crud_tiers.get(db=db, name=tier_name)
102103
if db_tier is None:
103-
raise HTTPException(status_code=404, detail="Tier not found")
104+
raise NotFoundException("Tier not found")
104105

105106
db_rate_limit = await crud_rate_limits.get(
106107
db=db,
@@ -109,19 +110,19 @@ async def patch_rate_limit(
109110
id=id
110111
)
111112
if db_rate_limit is None:
112-
raise HTTPException(status_code=404, detail="Rate Limit not found")
113+
raise NotFoundException("Rate Limit not found")
113114

114115
db_rate_limit_path = await crud_rate_limits.exists(
115116
db=db,
116117
tier_id=db_tier["id"],
117118
path=values.path
118119
)
119120
if db_rate_limit_path is not None:
120-
raise HTTPException(status_code=404, detail="There is already a rate limit for this path")
121+
raise DuplicateValueException("There is already a rate limit for this path")
121122

122123
db_rate_limit_name = await crud_rate_limits.exists(db=db)
123124
if db_rate_limit_path is not None:
124-
raise HTTPException(status_code=404, detail="There is already a rate limit with this name")
125+
raise DuplicateValueException("There is already a rate limit with this name")
125126

126127
await crud_rate_limits.update(db=db, object=values, id=db_rate_limit["id"])
127128
return {"message": "Rate Limit updated"}
@@ -136,7 +137,7 @@ async def erase_rate_limit(
136137
):
137138
db_tier = await crud_tiers.get(db=db, name=tier_name)
138139
if not db_tier:
139-
raise HTTPException(status_code=404, detail="Tier not found")
140+
raise NotFoundException("Tier not found")
140141

141142
db_rate_limit = await crud_rate_limits.get(
142143
db=db,
@@ -145,7 +146,7 @@ async def erase_rate_limit(
145146
id=id
146147
)
147148
if db_rate_limit is None:
148-
raise HTTPException(status_code=404, detail="Rate Limit not found")
149+
raise RateLimitException("Rate Limit not found")
149150

150151
await crud_rate_limits.delete(db=db, db_row=db_rate_limit, id=db_rate_limit["id"])
151152
return {"message": "Rate Limit deleted"}

0 commit comments

Comments
 (0)