How to partially update data with FastAPI and PyDantic BaseModel using PATCH - python

I'd like to partly update database via PATCH method in FastAPI. I use Postgres as my database, Postman to test.
I followed the example on FastAPI document, link: https://fastapi.tiangolo.com/tutorial/body-updates/#partial-updates-with-patch
I use GET to fetch the original data in DB, copy the content to body raw json, then change the part where I need to update and choose PATCH, click send in Postman, an error occurs: main.Product() argument after ** must be a mapping, not Product
What is the right approach to PATCH data? I omitted the code to connect to Postgres using psycopg2
from fastapi import FastAPI, Response, status, HTTPException, Path
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
app = FastAPI()
class Product(BaseModel):
name: str
price: float
inventory: int
#app.get("/posts/{id}")
def get_a_post(id: int = Path(None, title='Prod ID')):
cursor.execute('''SELECT * FROM public.products WHERE ID = %s''',(str(id),))
post = cursor.fetchone()
if not post:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"product with id {id} was not found!")
return post
#app.patch("/posts/{id}", response_model=Product)
def patch_posts(id: int, post: Product):
stored_data = post
stored_model = Product(**stored_data)
update_data = post.dict(exclude_unset=True)
updated_data = stored_model.copy(update=update_data)
post = jsonable_encoder(updated_data)
return{"partially updated product": post}

Looks like your issue is caused by trying to get the key/value pairs via **stored_data, but that variable is of type Product.
In your patch_posts function, change stored_data = post to stored_data = post.dict().
Using the example you provided:
import uvicorn
from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
app = FastAPI()
class Product(BaseModel):
name: str
price: float
inventory: int
#app.patch("/posts/{id}", response_model=Product)
def patch_posts(id: int, post: Product):
stored_data = post.dict()
stored_model = Product(**stored_data)
update_data = post.dict(exclude_unset=True)
updated_data = stored_model.copy(update=update_data)
post = jsonable_encoder(updated_data)
return post

Related

Add parameter description when converting a Dataclass to BaseModel

I need to add a description to a FastAPI query parameter, which I pass to the endpoint through a dataclass, in order to display it OpenAPI (auto-documentation).
How can I do it?
I tried through metadata in fields but it has no effect (no description for x):
To my understanding the dataclass object is used to create a pydantic BaseModel object. Which is then used by FastAPI.
Here's my unsuccessful code:
from dataclasses import dataclass, field
from fastapi import FastAPI, Depends
app = FastAPI()
#dataclass
class MyDataclass:
x: str = field(default=None, metadata={'description': 'descr of x'})
#app.get("/", )
async def root(f: MyDataclass = Depends()):
return {"message": "Hello World"}
#app.get("/hello/{name}")
async def say_hello(name: str):
return {"message": f"Hello {name}"}
Instead of the field from dataclass, use Query from pydantic:
from dataclasses import dataclass
from fastapi import FastAPI, Depends, Query
app = FastAPI()
#dataclass
class MyDataclass:
x: str = Query(default=None, description='descr of x')

How to validate ObjectId query param in FastAPI

I have an endpoint that recibe and id field used to get data from MongoDB's _id:
from fastapi import APIRouter
from bson import ObjectId
router = APIRouter()
#router.get("/get-data")
async def get_strategies(order_id: ObjectId):
return Manager().get_data(order_id)
I want validate if order_id is valid as ObjectId, if later, inside the function, order_id is a str or ObjectId it doesn't matter.
How can I do this?
Thanks for help!
I found this validator: Query, this is used to validate http GET parameters.
Also there is this validator: Path to validate path parameters, below I left and example:
from fastapi import APIRouter, Path, Query
from bson import ObjectId
router = APIRouter()
#router.get("/get-data")
async def get_strategies(order_id: str = Query(..., regex=r"^[0-9a-f]{24}$")):
return Manager().get_data(order_id)
#router.get("/get-data/{order_id}")
async def get_strategies(order_id: str = Path(..., regex=r"^[0-9a-f]{24}$")):
return Manager().get_data(order_id)
The ... inside Query and Path is to indicate parameter is required.
ObjectId is an hexadecimal string with 24 characters. that is mached with the regex

issue creating an user with calculated field fastAPI

I have a simple API to insert data inside an object type dictionary, my issue is when I try to save a calculated field. Example code:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
users = {}
class User(BaseModel):
name: str
email: str
calculated: float
#app.post('/user-create/{id_user}')
def creating_an_user(id_user:int,user:User):
calculated = 1+2+3*2
if id_user in users:
return {"Error":"User ID already exists."}
users[id_user] = {
"name":user.name,
"email":user.email,
"calculated":user.calculated #should I need to put just like that ?
}
return users[id_user]
Obviously I receive an error, I think because my method is waiting a manually insert "calculated" field but is not:
TypeError: Failed to execute 'fetch' on 'Window': Request with GET/HEAD method cannot have body.
I am lost in this concern, can anybody help me on this?

Use FastAPI to parse incoming POST request from Slack

I'm building a FastAPI server to receive requests sent by slack slash command. Using the code below, I could see that the following:
token=BLAHBLAH&team_id=BLAHBLAH&team_domain=myteam&channel_id=BLAHBLAH&channel_name=testme&user_id=BLAH&user_name=myname&command=%2Fwhatever&text=test&api_app_id=BLAHBLAH&is_enterprise_install=false&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%BLAHBLAH&trigger_id=BLAHBLAHBLAH
was printed, which is exactly the payload I saw in the official docs. I'm trying to use the payload information to do something, and I'm curious whether there's a great way of parsing this payload info. I can definitely parse this payload using split function or any other beautiful functions, but I'm curious whether there is a 'de facto' way of dealing with slack payload. Thanks in advance!
from fastapi import FastAPI, Request
app = FastAPI()
#app.post("/")
async def root(request: Request):
request_body = await request.body()
print(request_body)
Receive JSON data
You would normally use Pydantic models to declare a request body - if you were about to receive data in JSON form - thus benefiting from the validation that Pydantic has to offer (for more options on how to post JSON data, have a look at this answer). So, you would define a model like this:
from pydantic import BaseModel
class Item(BaseModel):
token: str
team_id: str
team_domain: str
# etc.
#app.post("/")
def root(item: Item):
print(item.dict()) # convert to dictionary (if required)
return item
The payload would look like this:
{
"token": "gIkuvaNzQIHg97ATvDxqgjtO"
"team_id": "Foo",
"team_domain": "bar",
# etc.
}
Receive Form data
If, however, you were about to receive the payload as Form data, just like what slack API does (as shown in the link you provided), you could use Form fileds. With Form fields, your payload will still be validated against those fields and the type you define them with. You would need, however, to define all the parameters in the endpoint, as described in the above link and as shown below:
from fastapi import Form
#app.post("/")
def root(token: str = Form(...), team_id: str = Form(...), team_domain: str = Form(...)):
return {"token": token, "team_id": team_id, "team_domain": team_domain}
or to avoid that, you may want to have a look at this post, which describes how to use Pydantic models with Form fields. As suggested in one of the answers, you can do that even without using Pydantic models, but instead with creating a custom dependency class using the #dataclass decorator, which allows you to define classes with less code. Example:
from dataclasses import dataclass
from fastapi import FastAPI, Form, Depends
#dataclass
class Item:
token: str = Form(...)
team_id: str = Form(...)
team_domain: str = Form(...)
#...
#app.post("/")
def root(data: Item = Depends()):
return data
As FastAPI is actually Starlette underneath, even if you still had to access the request body in the way you do in the question, you should rather use methods such as request.json() or request.form(), as described in Starlette documentation, which allow you to get the request body parsed as JSON or form-data, respectively.

FastAPI - GET Request with Pydantic List field

I'm new to FastAPI (migrating from Flask) and I'm trying to create a Pydantic model for my GET route:
from fastapi import APIRouter,Depends
from pydantic import BaseModel
from typing import Optional,List
router = APIRouter()
class SortModel(BaseModel):
field: Optional[str]
directions: List[str]
#router.get("/pydanticmodel")
def get_sort(criteria: SortModel = Depends(SortModel)):
pass #my code for handling this route.....
When I'm running curl -X GET http://localhost:XXXX/pydanticmodel?directions=up&directions=asc&field=id I'm getting 422 Unprocessable Entity: {"detail":[{"loc":["body"],"msg":"field required","type":"value_error.missing"}]}
But if I'm changing directions:List[str] -> directions: str I'm getting 200 OK with directions="asc".
What is the reason that str works for query param and List[str] does not? What am I doing wrong?
Thanks.
It is not, as yet, possible to use a GET request with Pydantic List field as query parameter. When you declare a List field in the Pydantic model, it is interpreted as a request body parameter, instead of a query one (regardless of using Depends()—you can check that through Swagger UI docs at http://127.0.0.1:8000/docs, for instance). Additionally, as you are using a GET request, even if you added the List of directions in the body and attempted sending the request, it wouldn't work, as a POST request would be required for that operation.
The way to do this is to either define the List of directions explicitly with Query as a separate parameter in your endpoint, or implement your query parameter-parsing in a separate dependency class, as described here. Remember again to define the List field explicitly with Query, so that directions can be interpreted as a query parameter and appear multiple times in the URL (in others words, to receive multiple values). Example:
from typing import List, Optional
from fastapi import APIRouter, Depends, Query
class SortModel:
def __init__(
self,
field: Optional[str],
directions: List[str] = Query(...)
):
self.field = field
self.directions = directions
router = APIRouter()
#router.get("/")
def send_user(criteria: SortModel = Depends()):
return criteria
The above can be re-written using the #dataclass decorator, as shown below:
from typing import List, Optional
from fastapi import APIRouter, Depends, Query
from dataclasses import dataclass
#dataclass
class SortModel:
field: Optional[str]
directions: List[str] = Query(...)
router = APIRouter()
#router.get("/")
def send_user(criteria: SortModel = Depends()):
return criteria
I'm running into the same issue. The following solution will work, but it isn't really what I want however maybe it's good enough for you:
from fastapi import APIRouter,Depends, Query
from pydantic import BaseModel
from typing import Optional,List
router = APIRouter()
class SortModel(BaseModel):
field: Optional[str]
#router.get("/pydanticmodel")
def get_sort(criteria: SortModel = Depends(SortModel), directions: List[str] = Query(...)):
pass #my code for handling this route.....
It's not a Pydantic or FastAPI problem.
If you want to send an array with curl you should use -d flag.
In: curl -X GET "http://127.0.0.1:8000/pydanticmodel?field=123" -d "[\"string\"]"
Out: {"field":"123","directions":["string"]}
Now your code should work perfectly.

Categories