How to pass the path parameter to the Pydantic model? - python

Is there some way to do the following?
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
id: str
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
app = FastAPI()
#app.post("/items/{item_id}")
async def create_item(item: Item):
return item
I want to have the item_id path parameter value inside the Item model.
Is it possible?

Pydantic in FastAPI is used to define data model. You can do as follows:
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
id: Optional[str] = None
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
app = FastAPI()
#app.post("/items/{item_id}")
async def create_item(item_id, item: Item):
item.id = item_id
return item
Please, note that id is declared as optional to avoid validation problems with body parameters in the request. Indeed, you are not going to pass the id in the body but in the path.
If necessary, you can also decouple the request- from the response-model (see here). It depends on what you need.

Replace your id: str with id: Optional [str] = None
Replace your create_item (item: Item) with create_item (item_id, item: Item):
Add item.id = item_id under your async. Obviously then you leave the ruo return item

Related

Get all required fields of a nested Python Pydantic model

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']

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.

mypy - Item "None" of "Optional[CustomAttrsModel]" has no attribute "country"

When I run mypy checkings I am getting an error. I am no able to ignore it or turn it off the strict optional checking. It there a way to solve this.
Here is the line that is throwing the error:
if tree.data.attributes.custom != JAPAN:
where attributes is declared as:
class TreeAttributesModel(BaseModel):
id: Optional[TreeId]
name: Optional[str] = None
status: StatusEnum
custom: Optional[CustomAttrsModel] = None
and CustomAttrsModel is declared as it follows:
class CustomAttrsModel(BaseModel):
seller: Optional[str]
buyed_at: Optional[datetime]
country: Optional[Union[CountryEnum, str]]
Could you please help me with this?
I had to tweak your snippets a bit to get a MWE, but here we go:
import enum
import dataclasses
from datetime import datetime
from typing import Optional, Union
class StatusEnum(enum.Enum):
OK = enum.auto()
NOK = enum.auto()
class CountryEnum(enum.Enum):
JAPAN = enum.auto()
RAPTURE = enum.auto()
#dataclasses.dataclass
class TreeAttributesModel:
id: Optional[str]
name: Optional[str] # = None had to remove default, attribs w/o default cannot follow attribs w/ one
status: StatusEnum
custom: Optional[CustomAttrsModel] = None
#dataclasses.dataclass
class CustomAttrsModel:
seller: Optional[str]
buyed_at: Optional[datetime]
country: Optional[Union[CountryEnum, str]]
custom = CustomAttrsModel(seller="test", buyed_at=None, country=CountryEnum.JAPAN)
attribs = TreeAttributesModel(id="test", name="test", status=StatusEnum.OK, custom=custom)
assert attribs.custom is not None # this is typed as being optional, so make sure it isn't None
assert attribs.custom.country is not None # same as above
result = attribs.custom.country != CountryEnum.JAPAN
The message is: just use assert something is not None whenever something is Optional ;)

How to do partial update in fastapi?

I want to make "partial update" endpoint, but don't want too allow passing null in any field.
Here is the guide from fastapi https://fastapi.tiangolo.com/tutorial/body-updates/#partial-updates-with-patch :
class Item(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
price: Optional[float] = None
tax: float = 10.5
tags: List[str] = []
#app.patch("/items/{item_id}", response_model=Item)
async def update_item(item_id: str, item: Item):
...
update_data = item.dict(exclude_unset=True)
...
With this approach user can pass {"name": null} and corrupt database, because in my case name should always be a string.
So what should I do? The only approach I see so far is playing around with some sentinel objects (using them as "unset" marker instead of None), but this seems hacky and I doubt that pydantic will allow me to do this.
You could use exclude_none in order to exclude values that are equal to None.
Example
item.dict(exclude_none=True)
Source: Pydantic docs

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