How to transform payload data after it comes in using Pydantic - python

I have a payload that comes in which has two parameters. One of the parameters is a long string which contains more parameters. Something like this param1%param2%param3. I am using FastAPI and Pydantic BaseModel to get that data and validate it, however since I am using it in other places I also want to transform it and store it in an object so I can access it later without having to transform it when I need to. Something like PayloadObject.param1.
from fastapi import FastAPI
from pydantic import BaseModel
class Payload(BaseModel):
string_params: str #param1%param2%param3
second_param: dict
#validator(string_params)
def string_params_validator(cls, strings_params):
#validation stuff
#validator(second_param)
def second_param(cls, second_param):
#validation stuff
app = FastAPI()
#app.post("/my_route")
async def post_my_route(payload: Payload):
# want to have transformed payload around here
func(payload)
What would be the best way to go about that using pydantic?
I am just thinking of making a class that transforms this information on __init__ without using BaseModel. So after I get that data from the request and validate it I run it through this class and get a format that I am happy with.
class NewPayload:
def __init__(self, payload: Payload):
# do transformations so i end up with
self.param1 = param1
self.param2 = param2
self.param3 = param3
self.second_param = second_param

If this payload structure is specific to this route it's a good idea to transform it directly in your route def.
The structure you gave for NewPayload will not work if the number of param isn't always the same.
example 1:
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel
class Payload(BaseModel):
string_params: str #param1%param2%param3
second_param: dict
#validator(string_params)
def string_params_validator(cls, strings_params):
#validation stuff
#validator(second_param)
def second_param(cls, second_param):
#validation stuff
app = FastAPI()
#app.post("/my_route")
async def post_my_route(payload: Payload):
params: List[str] = payload.string_params.split("%")
# params = ["param1", "param2", "param3"]
# Do something with params
func(payload)
Another idea:
not the best since you accept the data in list format also, you can add a validator to stop this behavior but it will modify the doc
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel
class Payload(BaseModel):
string_params: Union[str, List[str]] #param1%param2%param3
second_param: dict
#validator(string_params)
def string_params_validator(cls, strings_params):
string_params = strings_params.split("%")
return string_params
#validator(string_params)
def params_to_list(cls, strings_params):
#validation stuff
#validator(second_param)
def second_param(cls, second_param):
#validation stuff
app = FastAPI()
#app.post("/my_route")
async def post_my_route(payload: Payload):
func(payload)
You can use a second pydantic model with the second solution to only accept str in input and cast your first model into the other.
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel
class Payload(BaseModel):
string_params: str #param1%param2%param3
second_param: dict
#validator(string_params)
def params_to_list(cls, strings_params):
#validation stuff
#validator(second_param)
def second_param(cls, second_param):
#validation stuff
class Payload1(BaseModel):
string_params: Union[str, List[str]]
second_param: dict
#validator(string_params)
def string_params_validator(cls, strings_params):
string_params = strings_params.split("%")
return string_params
app = FastAPI()
#app.post("/my_route")
async def post_my_route(payload: Payload):
params: Payload1 = Payload1(**Payload.dict())
func(payload)
In the end the cleaner solution would be to make string_params a list of str and not a simple str since you will always need to convert it to list

Related

Request validation

i wanna validate data in request
i have dictionary (a and b are use cases, 1234 are sub use cases)
d ={'a':[1,2],'b':[3,4]}
and request
#router.post("/documents")
from typing import Literal, List, Optional, Dict
#router.post("/documents")
async def data(usecase: Literal[frozenset(d.keys())] = Form(...))
it works and allowed values only a and b
But i wanna extend validation
#router.post("/documents")
async def data(usecase: Literal[frozenset(d.keys())] = Form(...),
subusecase: THERE I WANNA VALIDATE 1234 VALUES = Form(...)
)
I will be very grateful for the help
I'm not sure i quite understand the structure of the data received by your route.
If you want to validate the route input from a complexe structure like nested dict etc, in my opinion it would be beter to use a pydantic model with a pydantic validation function.
you pass to your route a pydantic model as parameter:
#router.post("/documents")
async def data(use_cases: UseCases):
# do something with your uses_cases
pass
pydantic model example:
from typing import List
from pydantic import BaseModel, validator
class UseCases(BaseModel):
a: List[int]
b: List[int]
#validator('a')
def a_must_containt_something(cls, v):
# add your validations here
return v
#validator('b')
def b_must_containt_something(cls, v):
# add your validations here
return v

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

Dynamically creating get request taking query parameter list based on Pydantic schema

I'm able to get requests like this with query parameters of a type list like this:
#router.get("/findStuff")
def get_stuff(a: List[int] = Query(None), b: List[str] = Query(None)):
return {'a': a, 'b': b}
But I'm not sure how I'd do this dynamically from an arbitrary Pydantic schema? When I do this, the query params are interpreted as a request body and cannot be processed from the OpenAPI doc. Is there a way to get the same behavior from above without explicitly specifying each query param in the method arguments?
class MySchema(BaseModel):
a: List[int] = Query(None)
b: List[str] = Query(None)
#router.get("/findStuff")
def get_stuff(inputs: MySchema = Depends()):
return inputs
The FastAPI documentation outlines how you can declare classes as dependencies.
An example based on the code you provided in your question:
import uvicorn
from typing import List
from fastapi import FastAPI, Depends, Query
app = FastAPI()
class MySchema:
def __init__(self, a: List[int] = Query(None), b: List[str] = Query(None)):
self.a = a
self.b = b
#app.get("/findStuff")
def get_stuff(inputs: MySchema = Depends()):
return inputs
if __name__ == "__main__":
uvicorn.run(app='main:app', host='127.0.0.1', port=8000)
If you navigate to the documentation (/docs endpoint), you can see the query paramaters for that route:

How to write schema for query parameters in FastAPI as variable

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()

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