Problem with Python, FastAPI, Pydantic and SQLAlchemy - python

I'm trying to build a Python FastAPI blog system using SQLAlchemy with SQLite and am having problems using/understanding the response_model parameter of the API decorator. Here's a SQLAlchemy model:
class User(SqlAlchemyBase):
__tablename__ = 'user'
__table_args__ = {"keep_existing": True}
user_uid: int = sa.Column(GUID, primary_key=True, default=GUID_DEFAULT_SQLITE)
first_name: str = sa.Column(sa.String)
last_name: str = sa.Column(sa.String)
email: str = sa.Column(sa.String, index=True, unique=True)
posts: List["Post"] = relationship("Post", backref="user")
created: datetime = sa.Column(sa.DateTime, default=datetime.now(tz=timezone.utc), index=True)
updated: datetime = sa.Column(sa.DateTime, default=datetime.now(
tz=timezone.utc), onupdate=datetime.now(tz=timezone.utc), index=True)
Here's the Pydantic schema's for a User:
from datetime import datetime
from pydantic import BaseModel
class UserBase(BaseModel):
first_name: str
last_name: str
email: str
class UserInDB(UserBase):
user_uid: int
created: datetime
updated: datetime
class Config:
orm_mode = True
class User(UserInDB):
pass
Here's a ULR endpoint to get a single user that has the response_model parameter included:
from typing import List
import fastapi
from starlette import status
from backend.schema.user import User
from backend.services import user_service
router = fastapi.APIRouter()
#router.get("/api/users/{user_uid}", response_model=User, status_code=status.HTTP_200_OK)
async def get_one_user(user_uid: str = None) -> User:
return await user_service.get_user(user_uid)
If I execute the above call in the OpenAPI docs created by FastAPI I get an internal server error in the OpenAPI docs and these errors in the console that's running the FastAPI application:
File "/Users/dougfarrell/projects/blog/.venv/lib/python3.10/site-packages/fastapi/routing.py", line 137, in serialize_response
raise ValidationError(errors, field.type_)
pydantic.error_wrappers.ValidationError: 6 validation errors for User
response -> first_name
field required (type=value_error.missing)
response -> last_name
field required (type=value_error.missing)
response -> email
field required (type=value_error.missing)
response -> user_uid
field required (type=value_error.missing)
response -> created
field required (type=value_error.missing)
response -> updated
field required (type=value_error.missing)
If I take the response_model parameter out of the #router.get(...) decorator I get back the results of the SQLAlchemy query, but no Pydantic serialization. I don't know what the error stack trace is trying to tell me. Can anyone offer some suggestions, advice or resources that might help me figure out what I'm doing wrong?
I'm using Python version 3.10.1, FastAPI version 0.70.1 and SQLAlchemy version 1.4.29.
Thanks!

Try this:
from typing import List
and change response_model=User to:
response_model=List[User]

Related

Initialize pydantic model with missing parameters

I have the following two pydantic models for a Users table in my database for a FastAPI application:
from fastapi import Form
from pydantic import BaseModel
class UserCreate(BaseModel):
username: str = Form(...)
password: str = Form(...)
class UserInDb(BaseModel):
id: int
username:str
hashed_password: str
I use the UserCreate class to get form data needed from the client to create a user and then I map it to UserInDb. This is how I initially implemented it:
password = user_create_instance.password
hashed_password = some_hashing_function(password)
user_in_db = UserInDb(**user_create_instance.dict(), hashed_password=hashed_password)
This however throws a value_error.missing error because user_create_instance does not have an id parameter which UserInDb expects to be passed in but the value for that comes from the database.
This issue is fixable by doing something like id: int = None when defining the attribute in the class but it doesn't feel right to do it this way. Is there a better approach?

exclude None fields from beanie ODM document

Im trying to insert a document from Beanie ODM without the fields with None value, but I can't find the way to do it
#router.post('/signup')
async def signup(
request: Request,
user_create: SignupSchema
):
hashed_password = get_password_hash(user_create.password)
user_entity = UserEntity(**user_create.dict(), hashed_password=hashed_password)
result = await user_entity.insert()
if not result:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error creating user",
headers={"WWW-Authenticate": "Bearer"}
)
return JSONResponse(status_code=status.HTTP_201_CREATED,
content={"detail": "Account created successfully"})
Something like user_create.dict(exclude_none=True) but with the BeanieODM document .insert(), my UserEntity document is something like this:
from typing import Optional
from beanie import Document
class UserEntity(Document):
username: str
email: EmailStr
first_name: Optional[str]
last_name: Optional[str]
hashed_password: str
class Settings:
name = "users"
I don't want the fields first_name/last_name in the database if they don't have a value.
There should be some way of making Beanie ODM document's fields optional right?
It seems it's not supported right now.
https://github.com/roman-right/beanie/discussions/322

How to validate request body in FastAPI?

I understand that if the incoming request body misses certain required keys, FastAPI will automatically raise 422 unserviceable entity error. However, is there a way to check the incoming request body by myself in the code and raise a 400 bad request if if misses required names?
For example, say I have this model and schema:
class Student(Base):
__tablename__ = "student"
id = Column(Integer, primary_key=True)
name = Column(String(50), unique=True, nullable=False)
email = Column(String(100), unique=True, nullable=False)
gpa = Column(Float, unique=False, nullable=False)
class StudentBase(BaseModel):
name: str
email: str
gpa: float
The POST endpoint to create a new row is:
#app.post("/student", dependencies=[Depends(check_request_header)],
response_model=schemas.Student, status_code=200)
def create_student(student: schemas.StudentCreate, db: Session = Depends(get_db)):
db_student = crud.get_student(db, student=student)
if db_student:
raise HTTPException(status_code=400, detail="This student has already been created.")
return crud.create_student(db=db, student=student)
The expected request body should be something like this:
{
"name": "johndoe",
"email": "johndoe#gmail.com",
"gpa": 5.0
}
is there a way to check the request body for the above endpoint?
This is normally handled by using pydantic to validate the schema before doing anything to the database at the ORM level. I highly recommend you use the FASTApi project generator and look at how it plugs together there: it's (currently) the easiest way to see the fastapi-> pydantic -> [orm] -> db model as FASTApi's author envisgaes it.
If you're not using an ORM, nothing stops you building an 'ORM lite' where the post data is parsed into a pydantic object (with .from_dict) and then you manually run the right queries. This error will propagate up to the endpoint function, where you can catch it and return your error as you want to.
Note that you can also do the validation yourself, any way you choose.
In general you raise an HTTPException if you need to signal that it failed.

Best way to update objects with nullable properties with fastapi

I'm building a simple REST API.
If the object you want to update contains properties that can contain null, what is the best way to define it in fastapi?
When using pydantic.BaseModel, it is not possible to support the usage of not updating if the property does not exist.
example:
from fastapi import Body, FastAPI
from typing import Optional, Literal
import dataclasses
app = FastAPI()
#dataclasses.dataclass
class User:
name: str
type: Optional[Literal['admin', 'guest']]
user = User('test_user', 'admin')
class UpdateUser(BaseModel):
name: str
type: Optional[Literal['admin', 'guest']]
#app.put('/')
def put(update_user: UpdateUser):
# In the case of BaseModel, I don't know the difference between the property
# that I don't update and the property that I want to update with None,
# so I always update with None.
user.name = update_user.name
user.type = update_user.type
I think the simplest way is to use dict.
example:
from fastapi import Body, FastAPI
from typing import Optional, Literal
import dataclasses
app = FastAPI()
#dataclasses.dataclass
class User:
id: int
name: str
type: Optional[Literal['admin', 'guest']]
user = User(1, 'test_user', 'admin')
#app.put('/')
def put(body = Body(...)):
if 'name' in body:
user.name = body.name
if 'type' in body:
user.type = body.type
However, in this case, it is not possible to specify the JSON type used for the request like BaseModel.
How can I implement the update process with dict-like flexibility while preserving type information?
It turns out that this is a typical patch request.
At the time of update, UpdateUser can receive it, and update_user.dict(exclude_unset=True) can receive a dictionary containing only the parts that need to be updated.
example:
from fastapi import Body, FastAPI
from typing import Optional, Literal
import dataclasses
app = FastAPI()
#dataclasses.dataclass
class User:
name: str
type: Optional[Literal['admin', 'guest']]
user = User('test_user', 'admin')
class UpdateUser(BaseModel):
name: str
type: Optional[Literal['admin', 'guest']]
#app.patch('/')
def patch(update_user: UpdateUser):
update_user_dict = update_user.dict(exclude_unset=True)
if 'name' in update_user_dict:
user.name = update_user.name
if 'type' in update_user_dict:
user.type = update_user.type
https://fastapi.tiangolo.com/tutorial/body-updates/#using-pydantics-exclude_unset-parameter

Nested Pydantic Model return error with FastAPI : field required (type=value_error.missing)

I got a problem using FastAPI and Pydantic.
I try to return a list of records based on my Pydantic model.
Here is the SQLAlchemy Metadata :
from sqlalchemy import MetaData, Table, Column, Integer, JSON, Boolean
from sqlalchemy.sql import expression
metadata = MetaData()
CustomLayers = Table(
"custom_layers",
metadata,
Column("id", Integer, primary_key=True),
Column("data", JSON),
Column("is_public", Boolean, default=expression.false()),
Column("user_id", Integer),
)
Here is the "corresponding" pydantic model :
from geojson_pydantic.features import FeatureCollection
from pydantic import BaseModel
class CustomLayerResponse(BaseModel):
is_public: bool
data: FeatureCollection
user_id: int
id: int
class Config:
orm_mode = True
Here is my route :
#router.get("/", response_model=List[CustomLayerResponse], status_code=status.HTTP_200_OK)
async def retrieve_by_user(user_id: int):
layer_records = await customlayers_repository.retrieve_by_user_id(user_id)
return layer_records
Here is the Retrieve operation using the Databases library (SQL Alchemy based)
async def retrieve_by_user_id(user_id: int):
query = CustomLayersTable.select().where(user_id == CustomLayersTable.c.user_id)
return await database.fetch_all(query=query)
But when I run this I got bunches of ValidationError from pydantic saying that :
response -> 7 -> id
field required (type=value_error.missing)
response -> 7 -> user_id
field required (type=value_error.missing)
response -> 7 -> is_public
field required (type=value_error.missing)
response -> 7 -> data
field required (type=value_error.missing)
But what is really strange is that if I loop through the DB records returned by the ORM and manually create instances of the pydantic schema in this way :
#router.get("/", response_model=List[CustomLayerResponse], status_code=status.HTTP_200_OK)
async def retrieve_by_user(user_id: int):
layer_records = await customlayers_repository.retrieve_by_user_id(user_id)
response = []
for l in layer_records:
manual_instance = CustomLayerResponse(data=FeatureCollection.parse_raw(l.get("data")),
user_id=l.get("user_id"),
id=l.get("id"),
is_public=l.get("is_public"))
response.append(manual_instance)
return response
Then everything works just as expected and I got the List of CustomLayerResponse in response.
So I wonder what could be the problem with the "auto" validation from pydantic model (the one provided by the response_model parameter provided by FastAPI, that I set here as List[CustomLayerResponse]) ?
You data field should use a class that extends both FeatureCollection and BaseModel so that you can set its Config subclass field orm_true to true as well.
I haven't tested this with FeatureCollection it self but something similar happened to me.

Categories

Resources