FastAPI: Internal server error when accessing through OpenAPI docs - python

I am exposing API using OpenAPI which is developed using FastAPI.
Here is my pydantic model:
class ComponentListResponse(BaseModel):
"""
This model is to list the component
"""
tag_info = ComponentSummaryTagInfoResp
heath_status : Optional[str] = Field(alias="healthStatus")
stage : Optional[str] = Field(alias="stage")
component_notes: List[dict] =List[ComponentNotes]
class ComponentList(BaseModel):
"""
This is the base model for component List
"""
data: List[dict] = List[ComponentListResponse]
Here is the resource file:
from .schema import (
ComponentListResponse,ComponentList
)
from .service import (
get_component_list
)
router = APIRouter(prefix="/component", tags=["Component"])
#router.get(
"/componentList/{component_id}",
response_description="List component by componentId & CompanyId",
response_model=ComponentList,
status_code=status.HTTP_200_OK,
)
def get_component_endpoint(
request: Request,
component_id: str,
company_id: str
):
"""
API handler function for component List API.
"""
component_list = get_component_list(component_id, company_id)
print (component_list)
if component_list:
return component_list
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Component List not found",
)
I am getting the response properly when I am trying to make a GET request from browser.
but when I am trying to access the same using OpenAPI docs through Swagger UI, it raises an error (Internal server error).
I sense that this is caused due to data: List[dict] = List[ComponentListResponse].
Can anyone tell me how to solve this?

Your models are wrongly defined. The "=" sign should be use to provide default values not type definitions.
Therefore your models should be define as follows:
class ComponentListResponse(BaseModel):
"""
This model is to list the component
"""
tag_info = ComponentSummaryTagInfoResp
heath_status : Optional[str] = Field(None, alias="healthStatus")
stage : Optional[str] = Field(None, alias="stage")
component_notes: List[ComponentNotes]
class ComponentList(BaseModel):
"""
This is the base model for component List
"""
data: List[ComponentListResponse]
or if you really want to have default values:
class ComponentListResponse(BaseModel):
"""
This model is to list the component
"""
tag_info = ComponentSummaryTagInfoResp
heath_status : Optional[str] = Field(None, alias="healthStatus")
stage : Optional[str] = Field(None, alias="stage")
component_notes: List[ComponentNotes] = []
class ComponentList(BaseModel):
"""
This is the base model for component List
"""
data: List[ComponentListResponse] = []
Besides don't forget to specify None as a default value for your field that are optional when you are using Field. If you don't put it, FastAPI and Pydantic will expect that those values are always set.

Related

Fastapi - How to ignore optional arguments passed to my function?

I'm creating an API (FastAPI) that can create database in my catalog. The python function that creates the db takes few arguments. Some are optional (like Description, LocationUri, Parameters) and some are mandatory (CatalogId, etc). I created a Pydantic model that defines these arguments.
class createGlueDatabaseDatabaseInput(BaseModel):
Name: str
Description: Optional[str] = None
LocationUri: Optional[str] = None
Parameters: Optional[dict] = None
class Config:
orm_mode = True
class createGlueDatabase(BaseModel):
CatalogId: str
DB_input: createGlueDatabaseDatabaseInput
class Config:
orm_mode = True
In the above, the catalog id is the only mandatory argument, and the rest are optional. So when the optional parameters are ignored or not provided in the swagger, those values are coming in as "None" to the function. This results in the function failing.
I tried doing the following in my code:
Added response_model_exclude_none=True to my router function that receives the input argument, but this didnt help.
Tried to create everything (other than the catalog id) as optional, still no success.
Can someone help me understand, how to ignore None being sent to my python function? Please let me know if you need any other details. Thanks in advance.
Tried using Pydantic models:
class createGlueDatabaseDatabaseInput(BaseModel):
Name: str
Description: Optional[str] = None
LocationUri: Optional[str] = None
Parameters: Optional[dict] = None
class Config:
orm_mode = True
class createGlueDatabase(BaseModel):
CatalogId: str
DB_input: createGlueDatabaseDatabaseInput
class Config:
orm_mode = True
Tried adding additional args like response_model_exclude_none=True, but didnt work.
Can you provide more code for a better answer?
I think that you need to ignore None in your my python function. This is normal practice. See for example https://fastapi.tiangolo.com/tutorial/body/#use-the-model.
class createGlueDatabaseDatabaseInput(BaseModel):
Name: str
Description: Optional[str] = None
LocationUri: Optional[str] = None
Parameters: Optional[dict] = None
class Config:
orm_mode = True
app = FastAPI()
#app.post("/my_route")
async def create_item(input_data: createGlueDatabaseDatabaseInput):
# here
...
If you pass the input to the POST with the following body
{
"Name": "MyName"
}
then in the place where the comment # here we have
createGlueDatabaseDatabaseInput(Name='MyName', Description=None, LocationUri=None, Parameters=None)
It is serialization of pydantic.
For the Parameters field you can to add the following initial value
class createGlueDatabaseDatabaseInput(BaseModel):
Name: str
Description: Optional[str] = None
LocationUri: Optional[str] = None
Parameters: Optional[dict] = Field(default_factory=dict)
class Config:
orm_mode = True
Then we will have
createGlueDatabaseDatabaseInput(Name='MyName', Description=None, LocationUri=None, Parameters={})
where createGlueDatabaseDatabaseInput.Parameters is empty dict.

FastAPI - GET request results in typeerror (value is not a valid dict)

this is my database schema.
I defined my Schema like this:
from pydantic import BaseModel
class Userattribute(BaseModel):
name: str
value: str
user_id: str
id: str
This is my model:
class Userattribute(Base):
__tablename__ = "user_attribute"
name = Column(String)
value = Column(String)
user_id = Column(String)
id = Column(String, primary_key=True, index=True)
In a crud.py I define a get_attributes method.
def get_attributes(db: Session, skip: int = 0, limit: int = 100):
return db.query(models.Userattribute).offset(skip).limit(limit).all()
This is my GET endpoint:
#app.get("/attributes/", response_model=List[schemas.Userattribute])
def read_attributes(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
users = crud.get_attributes(db, skip=skip, limit=limit)
print(users)
return users
The connection to the database seems to work, but a problem is the datatype:
pydantic.error_wrappers.ValidationError: 7 validation errors for Userattribute
response -> 0
value is not a valid dict (type=type_error.dict)
response -> 1
value is not a valid dict (type=type_error.dict)
response -> 2
value is not a valid dict (type=type_error.dict)
response -> 3
value is not a valid dict (type=type_error.dict)
response -> 4
value is not a valid dict (type=type_error.dict)
response -> 5
value is not a valid dict (type=type_error.dict)
response -> 6
value is not a valid dict (type=type_error.dict)
Why does FASTApi expect a dictionary here? I don´t really understand it, since I am not able to even print the response. How can I fix this?
SQLAlchemy does not return a dictionary, which is what pydantic expects by default. You can configure your model to also support loading from standard orm parameters (i.e. attributes on the object instead of dictionary lookups):
class Userattribute(BaseModel):
name: str
value: str
user_id: str
id: str
class Config:
orm_mode = True
You can also attach a debugger right before the call to return to see what's being returned.
Since this answer has become slightly popular, I'd like to also mention that you can make orm_mode = True the default for your schema classes by having a common parent class that inherits from BaseModel:
class OurBaseModel(BaseModel):
class Config:
orm_mode = True
class Userattribute(OurBaseModel):
name: str
value: str
user_id: str
id: str
This is useful if you want to support orm_mode for most of your classes (and for those where you don't, inherit from the regular BaseModel).
This error is caused by two things:
The reponse_model parameter in the path operation decorator, which defines the type/shape of response to be returned. Removing this will eliminate the errors you see, as it will remove the validation against what is being returned.
The internal Config class that is missing in your Pydantic schemas.
Make sure to add the Config class to avoid this problem, or at worst, remove the response_model parameter (which I doubt anyone would consider). Example is:
class ItemBase(BaseModel):
title: str
description: Union[str, None] = None
class Config:
orm_mode = True
Adding this class allows Pydantic model to read data in non-dictionary format, thereby allowing you to return database model.
Checkout the fastAPI documentation for more

FastAPI - how to generate random ID?

I'm making simple CRUD API using FastAPI and what I want to do is generate unique random when creating new item (other fields are address and name which should be filled by user). How can I do that?
There is fragment of my code with class and a POST function.
app = FastAPI()
userdb = []
class User(BaseModel):
id: int
address: str
name: str
#app.post("/users")
def add_user(user: User):
userdb.append(users.dict())
return userdb[-1]
uuid4 is often the way to go
It'll be absolutely unique amongst any id ever generated with the function anywhere with astronomical likelihood (refer to RFC-4122 Section 4.4) and is very fast
from uuid import uuid4
...
unique_id = str(uuid4())
Another option - often what you want is for this ID to not necessarily be random, but rather to be some auto-incremented index from your database. The FastAPI docs give an example of this:
...
notes = sqlalchemy.Table(
"notes",
metadata,
sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),
sqlalchemy.Column("text", sqlalchemy.String),
sqlalchemy.Column("completed", sqlalchemy.Boolean),
)
...
class NoteIn(BaseModel):
text: str
completed: bool
class Note(BaseModel):
id: int
text: str
completed: bool
...
#app.post("/notes/", response_model=Note)
async def create_note(note: NoteIn):
query = notes.insert().values(text=note.text, completed=note.completed)
last_record_id = await database.execute(query)
return {**note.dict(), "id": last_record_id}
https://fastapi.tiangolo.com/advanced/async-sql-databases/?h=id#about-notedict-id-last_record_id
In your case you would use separate models for UserIn and User. Then in your example you would then assign the ID in the response model as the index in your userdb list (which in a real app would probably not just be a list, but a database).
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
userdb = []
class UserIn(BaseModel):
address: str
name: str
class User(BaseModel):
id: int
address: str
name: str
#app.post("/users")
def add_user(user_in: UserIn) -> User:
userdb.append(user_in)
user_out_dict = userdb[-1].dict()
user_out_dict.update({"id": len(userdb)-1})
return User(**user_out_dict)

How to enable filtering on all fields of a model in FastAPI

In Django with the restframework, you can do this:
class Item(models.Model):
id = models.IntegerField()
name = models.CharField(max_length=32)
another_attribute = models.CharField(max_length=32)
...
(more attributes)
...
yet_another_attribute = models.CharField(max_length=32)
class ItemViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAuthenticated]
serializer_class = ItemSerializer
filterset_fields = '__all__' # <- this enables filtering on all fields
queryset = Item.objects.all()
If I want to allow filtering, filterset_fields = '__all__' would allow me to do something like api/item/?(attribute)=(value) and allow me to filter on any attribute
I'm going through the tutorial (https://fastapi.tiangolo.com/tutorial/sql-databases/#crud-utils) and it looks like there is a lot of manual filtering involved:
from fastapi_sqlalchemy import db
class Item(BaseModel):
id: int
name: str
another_attribute: str
...
(more attributes)
...
yet_another_attribute: str
# is it necessary to manually include all the fields I want to filter on as optional query parameters?
#app.get("/items/")
async def read_item(
db: Session,
id: Optional[int] = None,
name: Optional[str] = None,
another_attribute: Optional[str] = None,
...
(more attributes)
...
yet_another_attribute: Optional[str] = None
):
# and then I'd need to check if the query parameter has been specified, and if so, filter it.
queryset = db.session.query(Item)
if id:
queryset = queryset.filter(Item.id == id)
if name:
queryset = queryset.filter(Item.name == name)
if another_attribute:
queryset = queryset.filter(Item.another_attribute == another_attribute)
...
(repeat above pattern for more attributes)
...
if yet_another_attribute:
queryset = queryset.filter(Item.yet_another_attribute == yet_another_attribute)
What is the preferred way of implementing the above behaviour? Are there any packages that will save me from having to do a lot of manual filtering that will give me the same behaviour as conveniently as the Django Rest Framework viewsets?
Or is manually including all the fields I want to filter on as optional query parameters, then checking for each parameter and then filtering if present the only way?
It is possible but not yet perfect:
from fastapi.params import Depends
#app.get("/items/")
async def read_item(item: Item = Depends()):
pass
See FastAPI documentation for details.
The downside is that the parameters are required as maybe specified in the Item class. It is possible to write a subclass with all optional parameters (e.g. like described here). It is working for instances of the class but FastAPI does not seem to reflect those in the API docs. If anyone has a solution to that I'd be happy to learn.
Alternatively you can have multiple models as described here. But I don't like this approach.
To answer your 2nd question you can access all generic parameters like this:
#app.get("/items/")
async def read_item(
db: Session,
id: Optional[int] = None,
name: Optional[str] = None,
another_attribute: Optional[str] = None,
...
(more attributes)
...
yet_another_attribute: Optional[str] = None
):
params = locals().copy()
...
for attr in [x for x in params if params[x] is not None]:
query = query.filter(getattr(db_model.Item, attr).like(params[attr]))
Definitely, it's described in the docs.
Try this, ellipsis marking the field as required.
id: Optional[int] = Header(...) # Header, path or any another place
See https://fastapi.tiangolo.com/tutorial/query-params-str-validations/

Partial update in FastAPI

I want to implement a put or patch request in FastAPI that supports partial update. The official documentation is really confusing and I can't figure out how to do the request. (I don't know that items is in the documentation since my data will be passed with request's body, not a hard-coded dict).
class QuestionSchema(BaseModel):
title: str = Field(..., min_length=3, max_length=50)
answer_true: str = Field(..., min_length=3, max_length=50)
answer_false: List[str] = Field(..., min_length=3, max_length=50)
category_id: int
class QuestionDB(QuestionSchema):
id: int
async def put(id: int, payload: QuestionSchema):
query = (
questions
.update()
.where(id == questions.c.id)
.values(**payload)
.returning(questions.c.id)
)
return await database.execute(query=query)
#router.put("/{id}/", response_model=QuestionDB)
async def update_question(payload: QuestionSchema, id: int = Path(..., gt=0),):
question = await crud.get(id)
if not question:
raise HTTPException(status_code=404, detail="question not found")
## what should be the stored_item_data, as documentation?
stored_item_model = QuestionSchema(**stored_item_data)
update_data = payload.dict(exclude_unset=True)
updated_item = stored_item_model.copy(update=update_data)
response_object = {
"id": question_id,
"title": payload.title,
"answer_true": payload.answer_true,
"answer_false": payload.answer_false,
"category_id": payload.category_id,
}
return response_object
How can I complete my code to get a successful partial update here?
Posting this here for googlers who are looking for an intuitive solution for creating Optional Versions of their pydantic Models without code duplication.
Let's say we have a User model, and we would like to allow for PATCH requests to update the User. But we need to create a schema that tells FastApi what to expect in the content body, and specifically that all the fields are Optional (Since that's the nature of PATCH requests). We can do so without redefining all the fields
from pydantic import BaseModel
from typing import Optional
# Creating our Base User Model
class UserBase(BaseModel):
username: str
email: str
# And a Model that will be used to create an User
class UserCreate(UserBase):
password: str
Code Duplication ❌
class UserOptional(UserCreate):
username: Optional[str]
email: Optional[str]
password: Optional[str]
One Liner ✅
# Now we can make a UserOptional class that will tell FastApi that all the fields are optional.
# Doing it this way cuts down on the duplication of fields
class UserOptional(UserCreate):
__annotations__ = {k: Optional[v] for k, v in UserCreate.__annotations__.items()}
NOTE: Even if one of the fields on the Model is already Optional, it won't make a difference due to the nature of Optional being typing.Union[type passed to Optional, None] in the background.
i.e typing.Union[str, None] == typing.Optional[str]
You can even make it into a function if your going to be using it more than once:
def convert_to_optional(schema):
return {k: Optional[v] for k, v in schema.__annotations__.items()}
class UserOptional(UserCreate):
__annotations__ = convert_to_optional(UserCreate)
I got this answer on the FastAPI's Github issues.
You could make the fields Optional on the base class and create a new QuestionCreate model that extends the QuestionSchema. As an example:
from typing import Optional
class Question(BaseModel):
title: Optional[str] = None # title is optional on the base schema
...
class QuestionCreate(Question):
title: str # Now title is required
The cookiecutter template here provides some good insight too.
I created a library (pydantic-partial) just for that, converting all the fields in the normal DTO model to being optional. See https://medium.com/#david.danier/how-to-handle-patch-requests-with-fastapi-c9a47ac51f04 for a code example and more detailed explanation.
https://github.com/team23/pydantic-partial/
Based on the answer of #cdraper, I made a partial model factory:
from typing import Mapping, Any, List, Type
from pydantic import BaseModel
def model_annotations_with_parents(model: BaseModel) -> Mapping[str, Any]:
parent_models: List[Type] = [
parent_model for parent_model in model.__bases__
if (
issubclass(parent_model, BaseModel)
and hasattr(parent_model, '__annotations__')
)
]
annotations: Mapping[str, Any] = {}
for parent_model in reversed(parent_models):
annotations.update(model_annotations_with_parents(parent_model))
annotations.update(model.__annotations__)
return annotations
def partial_model_factory(model: BaseModel, prefix: str = "Partial", name: str = None) -> BaseModel:
if not name:
name = f"{prefix}{model.__name__}"
return type(
name, (model,),
dict(
__module__=model.__module__,
__annotations__={
k: Optional[v]
for k, v in model_annotations_with_parents(model).items()
}
)
)
def partial_model(cls: BaseModel) -> BaseModel:
return partial_model_factory(cls, name=cls.__name__)
Can be used with the function partial_model_factory:
PartialQuestionSchema = partial_model_factory(QuestionSchema)
Or with decorator partial_model:
#partial_model
class PartialQuestionSchema(QuestionSchema):
pass

Categories

Resources