How to write schema for query parameters in FastAPI as variable - python

I have a get function that takes multiple query parameters which might look like this:
def get(
key: Optional[str] = "key"
value: Optional[str] = "value"
param1: Optional[int] = -1
)
What I want to do is, I want to put these parameter definitions in a separate variable. Is it possible to do something like this?
param_definition = { # some struct here, or maybe a Model class
key: Optional[str] = "key"
value: Optional[str] = "value"
param1: Optional[int] = -1
}
def get(*params: param_definition):
...
Can this be done? If no, is there anything similar and more maintainable that can be done here?

You can use Pydantic model with Depends() class as
from fastapi import FastAPI, Depends
from pydantic import BaseModel
from typing import Optional
app = FastAPI()
class MyParams(BaseModel):
key: Optional[str] = "key"
value: Optional[str] = "value"
param1: Optional[int] = -1
#app.get("/")
def my_get_route(params: MyParams = Depends()):
return params
This will also generate the API doc automatically for us.
Ref: FastAPI query parameter using Pydantic model

You need to use the json way as shown below:
param_definition = [
# some struct here, or maybe a Model class
{
"key" : "key",
"value" : "value",
"param1" : -1
}
]
def get():
print(param_definition)
get()

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/Pydantic alias existing ORM field

I need to point Pydantic to a different attribute when serializing an ORM model. alias= doesn't seem to work as expected. In the example below I have an ORM object with both id and uuid attributes. I want to serialize uuid as id.
The API response should be:
{
"id": "12345678-1234-5678-1234-567812345678",
"foo": "bar"
}
Full example:
from uuid import UUID
from fastapi import FastAPI
from pydantic import BaseModel, Field
from dataclasses import dataclass
class ApiSchema(BaseModel):
class Config:
orm_mode = True
uuid: UUID = Field(alias='id')
foo: str | None = None
#dataclass
class ORMModel:
id: int
uuid: UUID
foo: str = 'bar'
app = FastAPI()
#app.get("/")
def endpoint() -> ApiSchema:
t = ORMModel(id=1, uuid=UUID('12345678123456781234567812345678'), foo='bar')
return t
This raises
File fastapi/routing.py", line 141, in serialize_response
raise ValidationError(errors, field.type_)
pydantic.error_wrappers.ValidationError: 1 validation error for ApiSchema
response -> id
value is not a valid uuid (type=type_error.uuid)
The marshmallow equivalent of what I'm trying to achieve would be this:
import marshmallow as ma
class ApiSchema(ma.Schema):
id = ma.fields.UUID(attribute='uuid')
foo = ma.fields.Str()
You misunderstand how aliases work. An alias on a field takes priority (over the actual field name) when the fields are populated. That means, during initialization, the class will look for the alias of a field in the data it is supposed to parse.
The way you defined ApiSchema, the field uuid has the alias id. Therefore, when you are parsing an instance of ORMModel (happens in FastAPI behind the scenes via ApiSchema.from_orm), the ApiSchema class will look for an attribute named id on that ORMModel object to populate the uuid field.
Since your ORMModel actually has an attribute named id (with the value 1 in your example), its value is taken to be assigned to the uuid field of ApiSchema.
Obviously, the integer 1 is not a UUID object and can not be coerced into one, so you get that validation error telling you that the value it found for id is not a valid UUID.
Here is the problem boiled down to the essentials:
from uuid import UUID
from pydantic import BaseModel, Field, ValidationError
class ApiSchema(BaseModel):
uuid: UUID = Field(alias='id')
foo: str | None = None
try:
ApiSchema.parse_obj({"uuid": "this is ignored", "foo": "bar"})
except ValidationError as exc:
print(exc.json(indent=2))
try:
ApiSchema.parse_obj({"id": 1, "foo": "bar"})
except ValidationError as exc:
print(exc.json(indent=2))
The output of the first attempt:
[
{
"loc": [
"id"
],
"msg": "field required",
"type": "value_error.missing"
}
]
The second:
[
{
"loc": [
"id"
],
"msg": "value is not a valid uuid",
"type": "type_error.uuid"
}
]
I think you want it the other way around. I assume that your actual goal is to have a field named id on your ApiSchema model (and have that appear in your API endpoint) and alias it with uuid, so that it takes the value of the ORMModel.uuid attribute during initialization:
from uuid import UUID
from pydantic import BaseModel, Field
class ApiSchema(BaseModel):
id: UUID = Field(alias="uuid")
foo: str | None = None
obj = ApiSchema.parse_obj(
{
"id": "this is ignored",
"uuid": UUID("12345678123456781234567812345678"),
"foo": "bar",
}
)
print(obj.json(indent=2))
The output:
{
"id": "12345678-1234-5678-1234-567812345678",
"foo": "bar"
}
To fix your FastAPI example, you would therefore probably do this:
from dataclasses import dataclass
from uuid import UUID
from fastapi import FastAPI
from pydantic import BaseModel, Field
class ApiSchema(BaseModel):
id: UUID = Field(alias="uuid")
foo: str | None = None
class Config:
orm_mode = True
#dataclass
class ORMModel:
id: int
uuid: UUID
foo: str = "bar"
app = FastAPI()
#app.get("/", response_model=ApiSchema, response_model_by_alias=False)
def endpoint() -> ORMModel:
t = ORMModel(id=1, uuid=UUID("12345678123456781234567812345678"), foo="bar")
return t
Side note: Yes, the actual return type of endpoint is ORMModel. The wrapper returned by the decorator then takes that and turns it into an instance of ApiSchema via from_orm.
PS
Forgot the last part to actually get the response you want. You need to set response_model_by_alias=False in the route decorator (it is True by default) for the response to actually use the regular field name instead of the alias. I fixed the last code snipped accordingly. Now the response will be:
{"id":"12345678-1234-5678-1234-567812345678","foo":"bar"}
In the Pydantic BaseModel.json method the by_alias parameter has the value False by default. FastAPI does this differently.

FastAPI: Internal server error when accessing through OpenAPI docs

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.

How to pass the path parameter to the Pydantic model?

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

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