Skip to content

Commit 22273be

Browse files
authored
Merge pull request #199 from emiliano-gandini-outeda/Issue-Documentation-Improvements
Issue documentation improvements
2 parents 3f5ded5 + 9c9ed24 commit 22273be

File tree

6 files changed

+162
-49
lines changed

6 files changed

+162
-49
lines changed

README.md

Lines changed: 81 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -821,7 +821,7 @@ uv run alembic upgrade head
821821

822822
> 📖 **[See CRUD operations guide in our docs](https://benavlabs.github.io/FastAPI-boilerplate/user-guide/database/crud/)**
823823
824-
Inside `app/crud`, create a new `crud_entities.py` inheriting from `FastCRUD` for each new entity:
824+
Inside `app/crud`, create a new `crud_entity.py` inheriting from `FastCRUD` for each new entity:
825825

826826
```python
827827
from fastcrud import FastCRUD
@@ -1028,42 +1028,56 @@ crud_user.get(db=db, username="myusername", schema_to_select=UserRead)
10281028

10291029
> 📖 **[See API endpoints guide in our docs](https://benavlabs.github.io/FastAPI-boilerplate/user-guide/api/endpoints/)**
10301030
1031-
Inside `app/api/v1`, create a new `entities.py` file and create the desired routes
1031+
Inside `app/api/v1`, create a new `entities.py` file and create the desired routes with proper dependency injection:
10321032

10331033
```python
1034-
from typing import Annotated
1035-
1036-
from fastapi import Depends
1034+
from typing import Annotated, List
1035+
from fastapi import Depends, Request, APIRouter
1036+
from sqlalchemy.ext.asyncio import AsyncSession
10371037

10381038
from app.schemas.entity import EntityRead
10391039
from app.core.db.database import async_get_db
1040+
from app.crud.crud_entity import crud_entity
10401041

1041-
...
1042+
router = APIRouter(tags=["entities"])
10421043

1043-
router = fastapi.APIRouter(tags=["entities"])
1044-
1045-
1046-
@router.get("/entities/{id}", response_model=List[EntityRead])
1047-
async def read_entities(request: Request, id: int, db: Annotated[AsyncSession, Depends(async_get_db)]):
1048-
entity = await crud_entities.get(db=db, id=id)
10491044

1045+
@router.get("/entities/{id}", response_model=EntityRead)
1046+
async def read_entity(
1047+
request: Request,
1048+
id: int,
1049+
db: Annotated[AsyncSession, Depends(async_get_db)]
1050+
):
1051+
entity = await crud_entity.get(db=db, id=id)
1052+
1053+
if entity is None: # Explicit None check
1054+
raise NotFoundException("Entity not found")
1055+
10501056
return entity
10511057

10521058

1053-
...
1059+
@router.get("/entities", response_model=List[EntityRead])
1060+
async def read_entities(
1061+
request: Request,
1062+
db: Annotated[AsyncSession, Depends(async_get_db)]
1063+
):
1064+
entities = await crud_entity.get_multi(db=db, is_deleted=False)
1065+
return entities
10541066
```
10551067

1056-
Then in `app/api/v1/__init__.py` add the router such as:
1068+
Then in `app/api/v1/__init__.py` add the router:
10571069

10581070
```python
10591071
from fastapi import APIRouter
1060-
from app.api.v1.entity import router as entity_router
1072+
from app.api.v1.entities import router as entity_router
1073+
from app.api.v1.users import router as user_router
1074+
from app.api.v1.posts import router as post_router
10611075

1062-
...
1076+
router = APIRouter(prefix="/v1")
10631077

1064-
router = APIRouter(prefix="/v1") # this should be there already
1065-
...
1066-
router.include_router(entity_router)
1078+
router.include_router(user_router)
1079+
router.include_router(post_router)
1080+
router.include_router(entity_router) # Add your new router
10671081
```
10681082

10691083
#### 5.7.1 Paginated Responses
@@ -1100,6 +1114,9 @@ With the `get_multi` method we get a python `dict` with full suport for paginati
11001114
And in the endpoint, we can import from `fastcrud.paginated` the following functions and Pydantic Schema:
11011115

11021116
```python
1117+
from typing import Annotated
1118+
from fastapi import Depends, Request
1119+
from sqlalchemy.ext.asyncio import AsyncSession
11031120
from fastcrud.paginated import (
11041121
PaginatedListResponse, # What you'll use as a response_model to validate
11051122
paginated_response, # Creates a paginated response based on the parameters
@@ -1119,13 +1136,16 @@ from app.schemas.entity import EntityRead
11191136

11201137
@router.get("/entities", response_model=PaginatedListResponse[EntityRead])
11211138
async def read_entities(
1122-
request: Request, db: Annotated[AsyncSession, Depends(async_get_db)], page: int = 1, items_per_page: int = 10
1139+
request: Request,
1140+
db: Annotated[AsyncSession, Depends(async_get_db)],
1141+
page: int = 1,
1142+
items_per_page: int = 10
11231143
):
11241144
entities_data = await crud_entity.get_multi(
11251145
db=db,
11261146
offset=compute_offset(page, items_per_page),
11271147
limit=items_per_page,
1128-
schema_to_select=UserRead,
1148+
schema_to_select=EntityRead,
11291149
is_deleted=False,
11301150
)
11311151

@@ -1139,15 +1159,48 @@ async def read_entities(
11391159
To add exceptions you may just import from `app/core/exceptions/http_exceptions` and optionally add a detail:
11401160

11411161
```python
1142-
from app.core.exceptions.http_exceptions import NotFoundException
1162+
from app.core.exceptions.http_exceptions import (
1163+
NotFoundException,
1164+
ForbiddenException,
1165+
DuplicateValueException
1166+
)
1167+
1168+
@router.post("/entities", response_model=EntityRead, status_code=201)
1169+
async def create_entity(
1170+
request: Request,
1171+
entity_data: EntityCreate,
1172+
db: Annotated[AsyncSession, Depends(async_get_db)],
1173+
current_user: Annotated[UserRead, Depends(get_current_user)]
1174+
):
1175+
# Check if entity already exists
1176+
if await crud_entity.exists(db=db, name=entity_data.name) is True:
1177+
raise DuplicateValueException("Entity with this name already exists")
1178+
1179+
# Check user permissions
1180+
if current_user.is_active is False: # Explicit boolean check
1181+
raise ForbiddenException("User account is disabled")
1182+
1183+
# Create the entity
1184+
entity = await crud_entity.create(db=db, object=entity_data)
1185+
1186+
if entity is None: # Explicit None check
1187+
raise CustomException("Failed to create entity")
1188+
1189+
return entity
11431190

1144-
# If you want to specify the detail, just add the message
1145-
if not user:
1146-
raise NotFoundException("User not found")
11471191

1148-
# Or you may just use the default message
1149-
if not post:
1150-
raise NotFoundException()
1192+
@router.get("/entities/{id}", response_model=EntityRead)
1193+
async def read_entity(
1194+
request: Request,
1195+
id: int,
1196+
db: Annotated[AsyncSession, Depends(async_get_db)]
1197+
):
1198+
entity = await crud_entity.get(db=db, id=id)
1199+
1200+
if entity is None: # Explicit None check
1201+
raise NotFoundException("Entity not found")
1202+
1203+
return entity
11511204
```
11521205

11531206
**The predefined possibilities in http_exceptions are the following:**

docs/user-guide/api/endpoints.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ async def get_user(
5050
### 2. Get Multiple Items (with Pagination)
5151

5252
```python
53-
from fastcrud.paginated import PaginatedListResponse
53+
from fastcrud.paginated import PaginatedListResponse, paginated_response
5454

5555
@router.get("/", response_model=PaginatedListResponse[UserRead])
5656
async def get_users(
@@ -66,10 +66,9 @@ async def get_users(
6666
return_as_model=True,
6767
return_total_count=True
6868
)
69-
7069
return paginated_response(
7170
crud_data=users,
72-
page=page,
71+
page=page,
7372
items_per_page=items_per_page
7473
)
7574
```
@@ -321,8 +320,8 @@ Your new endpoints will be available at:
321320

322321
Now that you understand basic endpoints:
323322

324-
- **[Pagination](pagination.md)** - Add pagination to your endpoints
325-
- **[Database Schemas](../database/schemas.md)** - Create schemas for your data
326-
- **[CRUD Operations](../database/crud.md)** - Understand the CRUD layer
323+
- **[Pagination](pagination.md)** - Add pagination to your endpoints<br>
324+
- **[Exceptions](exceptions.md)** - Custom error handling and HTTP exceptions<br>
325+
- **[CRUD Operations](../database/crud.md)** - Understand the CRUD layer<br>
327326

328327
The boilerplate provides everything you need - just follow these patterns!

docs/user-guide/api/exceptions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -458,8 +458,8 @@ async def test_duplicate_email(client: AsyncClient):
458458
## What's Next
459459

460460
Now that you understand error handling:
461-
- **[Versioning](versioning.md)** - Learn how to version your APIs
462-
- **[Database CRUD](../database/crud.md)** - Understand the database operations
461+
- **[Versioning](versioning.md)** - Learn how to version your APIs<br>
462+
- **[Database CRUD](../database/crud.md)** - Understand the database operations<br>
463463
- **[Authentication](../authentication/index.md)** - Add user authentication to your APIs
464464

465465
Proper error handling makes your API much more user-friendly and easier to debug!

docs/user-guide/caching/client-cache.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,8 @@ async def get_posts(
207207
response: Response,
208208
page: int = 1,
209209
per_page: int = 10,
210-
category: str = None
210+
category: str | None = None,
211+
db: Annotated[AsyncSession, Depends(async_get_db)]
211212
):
212213
"""Conditional caching based on parameters."""
213214

@@ -255,7 +256,8 @@ def generate_etag(data: Any) -> str:
255256
async def get_user(
256257
request: Request,
257258
response: Response,
258-
user_id: int
259+
user_id: int,
260+
db: Annotated[AsyncSession, Depends(async_get_db)]
259261
):
260262
"""Endpoint with ETag support for efficient caching."""
261263

@@ -289,7 +291,8 @@ Use Last-Modified headers for time-based cache validation:
289291
async def get_post(
290292
request: Request,
291293
response: Response,
292-
post_id: int
294+
post_id: int,
295+
db: Annotated[AsyncSession, Depends(async_get_db)]
293296
):
294297
"""Endpoint with Last-Modified header support."""
295298

@@ -343,13 +346,13 @@ async def serve_static(response: Response, file_path: str):
343346
```python
344347
# Reference data (rarely changes)
345348
@router.get("/api/v1/countries")
346-
async def get_countries(response: Response, db: AsyncSession = Depends(async_get_db)):
349+
async def get_countries(response: Response, db: Annotated[AsyncSession, Depends(async_get_db)]):
347350
response.headers["Cache-Control"] = "public, max-age=86400" # 24 hours
348351
return await crud_countries.get_all(db=db)
349352

350353
# User-generated content (moderate changes)
351354
@router.get("/api/v1/posts")
352-
async def get_posts(response: Response, db: AsyncSession = Depends(async_get_db)):
355+
async def get_posts(response: Response, db: Annotated[AsyncSession, Depends(async_get_db)]):
353356
response.headers["Cache-Control"] = "public, max-age=1800" # 30 minutes
354357
return await crud_posts.get_multi(db=db, is_deleted=False)
355358

@@ -358,7 +361,7 @@ async def get_posts(response: Response, db: AsyncSession = Depends(async_get_db)
358361
async def get_notifications(
359362
response: Response,
360363
current_user: dict = Depends(get_current_user),
361-
db: AsyncSession = Depends(async_get_db)
364+
db: Annotated[AsyncSession, Depends(async_get_db)]
362365
):
363366
response.headers["Cache-Control"] = "private, max-age=300" # 5 minutes
364367
response.headers["Vary"] = "Authorization"
@@ -444,12 +447,15 @@ async def update_post(
444447
response: Response,
445448
post_id: int,
446449
post_data: PostUpdate,
447-
current_user: dict = Depends(get_current_user)
450+
current_user: dict = Depends(get_current_user),
451+
db: Annotated[AsyncSession, Depends(async_get_db)]
448452
):
449453
"""Update post and invalidate related caches."""
450454

451455
# Update the post
452456
updated_post = await crud_posts.update(db=db, id=post_id, object=post_data)
457+
if not updated_post:
458+
raise HTTPException(status_code=404, detail="Post not found")
453459

454460
# Set headers to indicate cache invalidation is needed
455461
response.headers["Cache-Control"] = "no-cache"

docs/user-guide/caching/redis-cache.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,16 @@ The `@cache` decorator provides a simple interface for adding caching to any Fas
2121
### Basic Usage
2222

2323
```python
24-
from fastapi import APIRouter, Request
24+
from fastapi import APIRouter, Request, Depends
25+
from sqlalchemy.orm import Session
2526
from app.core.utils.cache import cache
27+
from app.core.db.database import get_db
2628

2729
router = APIRouter()
2830

2931
@router.get("/posts/{post_id}")
3032
@cache(key_prefix="post_cache", expiration=3600)
31-
async def get_post(request: Request, post_id: int):
33+
async def get_post(request: Request, post_id: int, db: Session = Depends(get_db)):
3234
# This function's result will be cached for 1 hour
3335
post = await crud_posts.get(db=db, id=post_id)
3436
return post

docs/user-guide/rate-limiting/index.md

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ async def create_post(post_data: PostCreate):
3030

3131
### Rate Limiting Components
3232

33-
**Rate Limiter Class**: Singleton Redis client for checking limits
34-
**User Tiers**: Database-stored user subscription levels
35-
**Rate Limit Rules**: Path-specific limits per tier
36-
**Dependency Injection**: Automatic enforcement via FastAPI dependencies
33+
**Rate Limiter Class**: Singleton Redis client for checking limits<br>
34+
**User Tiers**: Database-stored user subscription levels<br>
35+
**Rate Limit Rules**: Path-specific limits per tier<br>
36+
**Dependency Injection**: Automatic enforcement via FastAPI dependencies<br>
3737

3838
### How It Works
3939

@@ -110,6 +110,59 @@ async def protected_endpoint():
110110
# 3. Checks Redis counter
111111
# 4. Allows or blocks the request
112112
```
113+
#### Example Dependency Implementation
114+
115+
To make the rate limiting dependency functional, you must implement how user tiers and paths resolve to actual rate limits.
116+
Below is a complete example using Redis and the database to determine per-tier and per-path restrictions.
117+
118+
```python
119+
async def rate_limiter_dependency(
120+
request: Request,
121+
db: AsyncSession = Depends(async_get_db),
122+
user=Depends(get_current_user_optional),
123+
):
124+
"""
125+
Enforces rate limits per user tier and API path.
126+
127+
- Identifies user (or defaults to IP-based anonymous rate limit)
128+
- Finds tier-specific limit for the request path
129+
- Checks Redis counter to determine if request should be allowed
130+
"""
131+
path = sanitize_path(request.url.path)
132+
user_id = getattr(user, "id", None) or request.client.host or "anonymous"
133+
134+
# Determine user tier (default to "free" or anonymous)
135+
if user and getattr(user, "tier_id", None):
136+
tier = await crud_tiers.get(db=db, id=user.tier_id)
137+
else:
138+
tier = await crud_tiers.get(db=db, name="free")
139+
140+
if not tier:
141+
raise RateLimitException("Tier configuration not found")
142+
143+
# Find specific rate limit rule for this path + tier
144+
rate_limit_rule = await crud_rate_limits.get_by_path_and_tier(
145+
db=db, path=path, tier_id=tier.id
146+
)
147+
148+
# Use default limits if no specific rule is found
149+
limit = getattr(rate_limit_rule, "limit", 100)
150+
period = getattr(rate_limit_rule, "period", 3600)
151+
152+
# Check rate limit in Redis
153+
is_limited = await rate_limiter.is_rate_limited(
154+
db=db,
155+
user_id=user_id,
156+
path=path,
157+
limit=limit,
158+
period=period,
159+
)
160+
161+
if is_limited:
162+
raise RateLimitException(
163+
f"Rate limit exceeded for path '{path}'. Try again later."
164+
)
165+
```
113166

114167
### Redis-Based Counting
115168

0 commit comments

Comments
 (0)