Skip to content

Commit 31668c5

Browse files
committed
fix: update Pydantic schemas for improved validation
1 parent 813c274 commit 31668c5

File tree

4 files changed

+101
-72
lines changed

4 files changed

+101
-72
lines changed

api/v1/routes/blog.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import logging
22

33
from fastapi import APIRouter, Depends, HTTPException, Query, status
4+
from fastapi.exceptions import RequestValidationError
45
from sqlalchemy.exc import SQLAlchemyError
56
from sqlalchemy.orm import Session
67

78
from api.db.database import get_db
89
from api.v1.schemas.blog import (
9-
BlogCreateSchema,
10-
BlogListResponseSchema,
11-
BlogResponseSchema,
12-
BlogUpdateSchema,
10+
BlogCreate,
11+
BlogListResponse,
12+
BlogResponse,
13+
BlogUpdate,
1314
)
1415
from api.v1.services.blog import BlogService
1516

@@ -20,13 +21,13 @@
2021

2122
@blog.post(
2223
"",
23-
response_model=BlogResponseSchema,
24+
response_model=BlogResponse,
2425
status_code=status.HTTP_201_CREATED,
2526
)
2627
async def create_blog(
27-
blog: BlogCreateSchema,
28+
blog: BlogCreate,
2829
db: Session = Depends(get_db),
29-
) -> BlogResponseSchema:
30+
) -> BlogResponse:
3031
try:
3132
return BlogService.create_blog(db, blog)
3233
except ValueError as e:
@@ -51,14 +52,14 @@ async def create_blog(
5152

5253
@blog.get(
5354
"",
54-
response_model=BlogListResponseSchema,
55+
response_model=BlogListResponse,
5556
status_code=status.HTTP_200_OK,
5657
)
5758
async def list_blog(
5859
page: int = Query(1, ge=1),
5960
page_size: int = Query(10, ge=1, le=100),
6061
db: Session = Depends(get_db),
61-
) -> BlogListResponseSchema:
62+
) -> BlogListResponse:
6263
try:
6364
return BlogService.list_blog(db, page, page_size)
6465
except SQLAlchemyError as e:
@@ -77,13 +78,13 @@ async def list_blog(
7778

7879
@blog.get(
7980
"/{id}",
80-
response_model=BlogResponseSchema,
81+
response_model=BlogResponse,
8182
status_code=status.HTTP_200_OK,
8283
)
8384
async def read_blog(
8485
id: int,
8586
db: Session = Depends(get_db),
86-
) -> BlogResponseSchema:
87+
) -> BlogResponse:
8788
try:
8889
return BlogService.read_blog(db, id)
8990
except ValueError as e:
@@ -108,16 +109,22 @@ async def read_blog(
108109

109110
@blog.patch(
110111
"/{id}",
111-
response_model=BlogResponseSchema,
112+
response_model=BlogResponse,
112113
status_code=status.HTTP_200_OK,
113114
)
114115
async def update_blog(
115116
id: int,
116-
blog_update: BlogUpdateSchema,
117+
blog_update: BlogUpdate,
117118
db: Session = Depends(get_db),
118-
) -> BlogResponseSchema:
119+
) -> BlogResponse:
119120
try:
120121
return BlogService.update_blog(db, id, blog_update)
122+
except RequestValidationError as e:
123+
logger.warning(f"Validation error: {e}")
124+
raise HTTPException(
125+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
126+
detail=e.errors(),
127+
)
121128
except ValueError as e:
122129
logger.warning(str(e))
123130
raise HTTPException(

api/v1/schemas/blog.py

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,18 @@
66
from pydantic import BaseModel, Field
77

88

9-
class BlogResponseSchema(BaseModel):
10-
"""Schema for representing a blog post in the API response."""
11-
12-
id: int
13-
title: str
14-
excerpt: str
15-
content: str
16-
image_url: str
17-
created_at: datetime
18-
updated_at: datetime
19-
20-
model_config = {"from_attributes": True}
21-
22-
23-
class BlogCreateSchema(BaseModel):
24-
"""Schema for creating a new blog post.
25-
26-
This represents the data structure required when a client
27-
submits a request to create a new blog post.
28-
"""
9+
class BlogBase(BaseModel):
10+
"""Base schema for blog post data."""
2911

3012
title: str = Field(
3113
...,
32-
min_length=1,
14+
min_length=10,
3315
max_length=255,
3416
description="Title of the blog post",
3517
)
3618
excerpt: str = Field(
3719
...,
20+
min_length=20,
3821
max_length=300,
3922
description="Short excerpt of the blog post",
4023
)
@@ -47,9 +30,44 @@ class BlogCreateSchema(BaseModel):
4730
max_length=255,
4831
description="URL of the blog post image",
4932
)
33+
is_deleted: Optional[bool] = False
34+
35+
36+
class BlogCreate(BlogBase):
37+
"""Schema for creating a new blog post."""
38+
39+
pass
40+
5041

42+
class BlogUpdate(BaseModel):
43+
"""Schema for updating an existing blog post."""
5144

52-
class BlogListItemResponseSchema(BaseModel):
45+
title: Optional[str] = Field(
46+
None,
47+
min_length=10,
48+
max_length=255,
49+
description="Title of the blog post",
50+
)
51+
excerpt: Optional[str] = Field(
52+
None,
53+
min_length=20,
54+
max_length=300,
55+
description="Short excerpt of the blog post",
56+
)
57+
content: Optional[str]
58+
image_url: Optional[str]
59+
is_deleted: Optional[bool] = False
60+
61+
62+
class BlogResponse(BlogBase):
63+
"""Schema for returning blog post data."""
64+
65+
id: int
66+
created_at: datetime
67+
updated_at: datetime
68+
69+
70+
class BlogListItemResponse(BaseModel):
5371
"""Schema for representing a blog post item in the list response."""
5472

5573
id: int
@@ -58,24 +76,11 @@ class BlogListItemResponseSchema(BaseModel):
5876
image_url: str
5977
created_at: datetime
6078

61-
model_config = {"from_attributes": True}
62-
6379

64-
class BlogListResponseSchema(BaseModel):
80+
class BlogListResponse(BaseModel):
6581
"""Schema for representing a blog post listing in the API response."""
6682

6783
count: int
6884
next: Optional[str]
6985
previous: Optional[str]
70-
results: List[BlogListItemResponseSchema]
71-
72-
model_config = {"from_attributes": True}
73-
74-
75-
class BlogUpdateSchema(BaseModel):
76-
title: str
77-
excerpt: str
78-
content: str
79-
image_url: str
80-
81-
model_config = {"from_attributes": True}
86+
results: List[BlogListItemResponse]

api/v1/services/blog.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,20 @@
33

44
from api.v1.models.blog import Blog
55
from api.v1.schemas.blog import (
6-
BlogCreateSchema,
7-
BlogListItemResponseSchema,
8-
BlogListResponseSchema,
9-
BlogResponseSchema,
10-
BlogUpdateSchema,
6+
BlogCreate,
7+
BlogListItemResponse,
8+
BlogListResponse,
9+
BlogResponse,
10+
BlogUpdate,
1111
)
1212

1313

1414
class BlogService:
1515
@staticmethod
1616
def create_blog(
1717
db: Session,
18-
blog: BlogCreateSchema,
19-
) -> BlogResponseSchema:
18+
blog: BlogCreate,
19+
) -> BlogResponse:
2020
existing_blog = db.query(Blog).filter(Blog.title == blog.title).first()
2121
if existing_blog:
2222
raise ValueError("A blog post with this title already exists.")
@@ -31,7 +31,7 @@ def create_blog(
3131
db.commit()
3232
db.refresh(new_blog)
3333

34-
return BlogResponseSchema(
34+
return BlogResponse(
3535
id=new_blog.id,
3636
title=new_blog.title,
3737
excerpt=new_blog.excerpt,
@@ -46,7 +46,7 @@ def list_blog(
4646
db: Session,
4747
page: int,
4848
page_size: int,
49-
) -> BlogListResponseSchema:
49+
) -> BlogListResponse:
5050
offset = (page - 1) * page_size
5151
query = (
5252
db.query(Blog)
@@ -65,7 +65,7 @@ def list_blog(
6565
prev_page = f"/api/v1/blogs?page={page - 1}&page_size={page_size}"
6666

6767
results = [
68-
BlogListItemResponseSchema(
68+
BlogListItemResponse(
6969
id=blog.id,
7070
title=blog.title,
7171
excerpt=blog.excerpt,
@@ -75,7 +75,7 @@ def list_blog(
7575
for blog in blogs
7676
]
7777

78-
return BlogListResponseSchema(
78+
return BlogListResponse(
7979
count=total_count,
8080
next=next_page,
8181
previous=prev_page,
@@ -86,7 +86,7 @@ def list_blog(
8686
def read_blog(
8787
db: Session,
8888
id: int,
89-
) -> BlogResponseSchema:
89+
) -> BlogResponse:
9090
blog = (
9191
db.query(Blog)
9292
.filter(
@@ -98,14 +98,14 @@ def read_blog(
9898
if not blog:
9999
raise ValueError("Blog post not found.")
100100

101-
return BlogResponseSchema.model_validate(blog.__dict__)
101+
return BlogResponse.model_validate(blog.__dict__)
102102

103103
@staticmethod
104104
def update_blog(
105105
db: Session,
106106
id: int,
107-
blog_update: BlogUpdateSchema,
108-
) -> BlogResponseSchema:
107+
blog_update: BlogUpdate,
108+
) -> BlogResponse:
109109
blog = (
110110
db.query(Blog)
111111
.filter(
@@ -137,7 +137,7 @@ def update_blog(
137137
db.commit()
138138
db.refresh(blog)
139139

140-
return BlogResponseSchema(
140+
return BlogResponse(
141141
id=blog.id,
142142
title=blog.title,
143143
excerpt=blog.excerpt,

tests/v1/blog/test_update_blog.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,15 +134,32 @@ def test_update_blog_internal_server_error(db_session_mock):
134134
assert response.json()["detail"] == "Internal server error."
135135

136136

137-
def test_update_blog_invalid_data():
138-
"""Send requests with invalid data and check for validation errors."""
137+
def test_update_blog_invalid_data(db_session_mock):
138+
"""Ensure that the endpoint returns a 422 status code when updating with invalid data."""
139+
existing_blog = Blog(
140+
id=1,
141+
title="Existing Blog Post",
142+
excerpt="A summary of the blog post...",
143+
content="The content of the blog post...",
144+
image_url="image-url-link",
145+
created_at=datetime.now(timezone.utc),
146+
updated_at=datetime.now(timezone.utc),
147+
)
139148
invalid_data = {
149+
"title": "Short",
140150
"excerpt": "An updated summary...",
141-
"content": "Updated content...",
142-
"image_url": "updated-image-url-link",
143151
}
144152

145-
response = client.patch("/api/v1/blogs/1", json=invalid_data)
153+
db_session_mock.query.return_value.filter.return_value.first.side_effect = [
154+
existing_blog, # Return the existing blog for the first query
155+
None, # Return None for the second query to simulate no conflict
156+
]
157+
db_session_mock.commit.side_effect = lambda: None
158+
db_session_mock.refresh.side_effect = lambda blog: setattr(
159+
blog, "updated_at", datetime.now(timezone.utc)
160+
)
161+
162+
response = client.patch(f"/api/v1/blogs/{existing_blog.id}", json=invalid_data)
146163

147164
assert response.status_code == 422
148165

0 commit comments

Comments
 (0)