How to validate ObjectId query param in FastAPI - python

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

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

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.

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

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

Changing schema name in OpenAPI docs generated by FastAPI

I'm using FastAPI to create backend for my project. I have a method that allows to upload a file. I implemented it as follows:
from fastapi import APIRouter, UploadFile, File
from app.models.schemas.files import FileInResponse
router = APIRouter()
#router.post("", name="files:create-file", response_model=FileInResponse)
async def create(file: UploadFile = File(...)) -> FileInResponse:
pass
As you can see, I use a dedicated pydantic model for a method result—FileInResponse:
from pathlib import Path
from pydantic import BaseModel
class FileInResponse(BaseModel):
path: Path
And I follow this naming pattern for models (naming models as <Entity>InCreate, <Entity>InResponse, and so on) throughout the API. However, I couldn't create a pydantic model with a field of the type File, so I had to declare it directly in the route definition (i.e. without a model containing it). As a result, I have this long auto generated name Body_files_create_file_api_files_post in the OpenAPI docs:
Is there a way to change the schema name?
If you take a look at response model you see this:
It receives the same type you would declare for a Pydantic model atribute, so it can be a Pydantic model, but it can also be, e.g. a list of Pydantic models, like List[Item].
FastAPI will use this response_model to:
Convert the output data to its type declaration.
Validate the data.
Add a JSON Schema for the response, in the OpenAPI path operation.
Will be used by the automatic documentation systems.
That mean that response model is use to return json not a file like you want to do.
I couldn't create a pydantic model with a field of the type File
This is totaly normal
so I had to declare it directly in the route definition (i.e. without a model containing it)
And that's normal too, remember that File is not a python type nor a pydantic specific type so it can't be part of a model inheritating from pydantic BaseModel.
To do a file response you can follow the official doc:
from fastapi import FastAPI
from fastapi.responses import FileResponse
some_file_path = "large-video-file.mp4"
app = FastAPI()
#app.get("/")
async def main():
return FileResponse(some_file_path)
if you want to return a file like object (i.e. file from an instance of UploadFile) follow this part of the doc:
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
some_file_path = "large-video-file.mp4"
app = FastAPI()
#app.get("/")
def main():
def iterfile():
with open(some_file_path, mode="rb") as file_like:
yield from file_like
return StreamingResponse(iterfile(), media_type="video/mp4")

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