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
Related
My pydantic nested model is defined as below:
from pydantic import BaseModel
from typing import Optional
class Location(BaseModel):
city: Optional[str]
state: str
country: str
class User(BaseModel):
id: int
name: Optional[str] = "Gandalf"
age: Optional[int]
location: Location
I would like to get all required fields for the User model.
For the above example, the expected output is ["id", "name", "state", "country"].
Any help greatly appreciated.
Here is a solution with a generator function:
from collections.abc import Iterator
def required_fields(model: type[BaseModel], recursive: bool = False) -> Iterator[str]:
for name, field in model.__fields__.items():
t = field.type_
if not field.required:
continue
if recursive and isinstance(t, type) and issubclass(t, BaseModel):
yield from required_fields(t, recursive=True)
else:
yield name
Using the models you defined in your example, we can demonstrate it like this:
print(list(required_fields(User, recursive=True)))
Output:
['id', 'state', 'country']
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.
I am refencing the answer on this other stackoverflow post on using the Typing library Literal to specify a unique array to strings to validate the data with Pydantic but I am running into a problem of calling another class recursively.
This is what my code looks like:
from pydantic import BaseModel, PydanticValueError, ValidationError, validator
from typing import Literal,Optional
ACTION_TYPE_MAPPING = Literal["read", "write", "release"]
OBJECT_TYPE_MAPPING = Literal["multiStateValue", "multiStateInput", "multiStateOutput",
"analogValue", "analogInput", "analogOutput",
"binaryValue", "binaryInput", "binaryOutput"]
BOOLEAN_ACTION_MAPPING = Literal["active", "inactive"]
# MAIN MODEL
class BacnetRequestModel(BaseModel):
action_type: ACTION_TYPE_MAPPING
object_type: OBJECT_TYPE_MAPPING
object_instance: int
value: Optional[ValueModel(object_type)] <---- MESSED UP HERE, how to call ValueModel?
class ValueModel(BaseModel):
multiStateValue: Optional[int]
multiStateInput: Optional[int]
multiStateOutput: Optional[int]
analogValue: Optional[int]
analogInput: Optional[int]
analogOutput: Optional[int]
binaryValue: Optional[BOOLEAN_ACTION_MAPPING]
binaryInput: Optional[BOOLEAN_ACTION_MAPPING]
binaryOutput: Optional[BOOLEAN_ACTION_MAPPING]
test = BacnetRequestModel(action_type="write",
object_type="binaryOutput",
object_instance="3",
value = "active"
)
How do I call the class ValueModel based on the objectType that was inputted to the function where in this case it was binaryOutput that should only accept a value of BOOLEAN_ACTION_MAPPING. Any tips help not a lot of wisdom here...
Traceback is:
value = Optional[ValueModel(object_type)]
NameError: name 'ValueModel' is not defined
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/
My intention
So, I am developing an API package for one service. I want to make good typehints for every method, which exists in my library
For example, when user types get()., after the dot pycharm will let him know, what response this method will provide.
e.g:
info = get()
info. # and here IDE help with hints.
Pitfalls
But, there are some methods, which provide different responses depending of parameters in methods.
e.g.
# this method responses with object, containing fields:
# count - count of items
# items - list of ids of users
info = get()
# but this method will give additional information. It responses with object, containing fields:
# count - count of items
# items - list of objects with users' information. It has fields:
# id - id of user
# firstname - firstname of user
# lastname - lastname of user
# ... and some others
info = get(fields='firstname')
Objects structure
Now I have such structure (i't simplified)
from typing import List, Union
from pydantic import BaseModel, Field
class UserInfo(BaseModel):
id: int = Field(...)
firstname: str = Field(None)
lastname: str = Field(None)
some_other_fields: str = Field(None)
class GetResponseNoFields(BaseModel):
count: int = Field(...)
items: List[int] = Field(...)
class GetResponseWithFields(BaseModel):
count: int = Field(...)
items: List[UserInfo] = Field(...)
class GetResponseModel(BaseModel):
response: Union[GetResponseNoFields, GetResponseWithFields] = Field(...)
def get(fields=None) -> GetResponseModel:
# some code
pass
The problem
The problem is, when I type get(fields='firsttname').response.items[0]. pycharm shows me typehints only for int. He doesn't think, that items can contain List[UserInfo], he thinks, it only can have List[int]
I have tried
I've tried to use typing.overload decorator, but method 'get' has many parameters, and actually doesn't support default parameter values. Or maybe i didn't do it properly
Here what I have tried with overload (simlified). It didn't work because of 'some_other_param', but I leave it here just in case:
from typing import overload
#overload
def get(fields: None) -> GetResponseNoFields: ...
#overload
def get(fields: str) -> GetResponseWithFields: ...
def get(some_other_param=None, fields=None):
# code here
pass
When I try to call method without parameters, pycharm says, that "Some of the parameters is unfilled"