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.
Related
I am writing APIs using stack FastAPI, Pydantic & SQL Alchemy and I have come across many cases where I had to query database to perform validations on payload values. Let's consider one example API, /forgot-password. This API will accept email in the payload and I need to validate the existence of the email in database. If the email exist in the database then necessary action like creating token and sending mail would be performed or else an error response against that field should be raise by Pydantic. The error responses must be the standard PydanticValueError response. This is because all the validation errors would have consistent responses as it becomes easy to handle for the consumers.
Payload -
{
"email": "example#gmail.com"
}
In Pydantic this schema and the validation for email is implemented as -
class ForgotPasswordRequestSchema(BaseModel):
email: EmailStr
#validator("email")
def validate_email(cls, v):
# this is the db query I want to perform but
# I do not have access to the active session of this request.
user = session.get(Users, email=v)
if not user:
raise ValueError("Email does not exist in the database.")
return v
Now this can be easily handled if the we simple create an Alchemy session in the pydantic model like this.
class ForgotPasswordRequestSchema(BaseModel):
email: EmailStr
_session = get_db() # this will simply return the session of database.
_user = None
#validator("email")
def validate_email(cls, v):
# Here I want to query on Users's model to see if the email exist in the
# database. If the email does. not exist then I would like to raise a custom
# python exception as shown below.
user = cls._session.get(Users, email=v) # Here I can use session as I have
# already initialised it as a class variable.
if not user:
cls.session.close()
raise ValueError("Email does not exist in the database.")
cls._user = user # this is because we want to use user object in the request
# function.
cls.session.close()
return v
But it is not a right approach as through out the request only one session should be used. As you can see in above example we are closing the session so we won't be able to use the user object in request function as user = payload._user. This means we will have to again query for the same row in request function. If we do not close the session then we are seeing alchemy exceptions like this - sqlalchemy.exc.PendingRollbackError.
Now, the best approach is to be able to use the same session in the Pydantic model which is created at the start of request and is also closing at the end of the request.
So, I am basically looking for a way to pass that session to Pydantic as context. Session to my request function is provided as dependency.
It is not recommended to query the database in pydantic schema. Instead use session as a dependency.
If you want to raise errors like pydantic validation error you might need this:
def raise_custom_error(exc: Exception, loc: str, model: BaseModel, status_code=int, **kwargs):
"""
This method will return error responses using pydantic error wrapper (similar to pydantic validation error).
"""
raise HTTPException(
detail=json.loads(ValidationError([ErrorWrapper(exc(**kwargs), loc=loc)], model=model).json()),
status_code=status_code,
)
Usage
class PayloadSchema(BaseModel):
email: EmailStr
#app_router.post('/forgot-password')
def forgot_password(
payload: PayloadSchema,
session: Session = Depends(get_db),
background_tasks: BackgroundTasks
):
existing_user = db.get(Users, email=payload.email)
if(existing_user):
raise_custom_error(
PydanticValueError, "email", PayloadSchema, status.HTTP_400_BAD_REQUEST
)
background_tasks(send_email, email=payload.email)
Don't do that!
The purpose of pydantic classes is to store dictionaries in a legit way, as they have IDE support and are less error prone. The validators are there for very simple stuff that doesn't touch other parts of system (like is integer positive or does email satisfy the regex).
Saying that, you should use the dependencies. That way you can be sure you have single session during processing all request and because of context manager the session will be closed in any case.
Final solution could look like this:
from fastapi import Body, Depends
from fastapi.exceptions import HTTPException
def get_db():
db = your_session_maker
try:
yield db
finally:
db.close()
#app.post("/forgot-password/")
def forgot_password(email: str = Body(...), db: Session = Depends(get_db)):
user = db.get(Users, email=email)
if not user:
# If you really need to, you can for some reason raise pydantic exception here
raise HTTPException(status_code=400, detail="No email")
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
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")
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.
flask_restful.reqparse has been deprecated (https://flask-restful.readthedocs.io/en/latest/reqparse.html):
The whole request parser part of Flask-RESTful is slated for removal and will be replaced by documentation on how to integrate with other packages that do the input/output stuff better (such as marshmallow). This means that it will be maintained until 2.0 but consider it deprecated. Don’t worry, if you have code using that now and wish to continue doing so, it’s not going to go away any time too soon.
I've looked briefly at Marshmallow and still a bit confused about how to use it if I wanted to replace reqparse.RequestParser(). What would we write instead of something like the following:
from flask import Flask, request, Response
from flask_restful import reqparse
#app.route('/', methods=['GET'])
def my_api() -> Response:
parser = reqparse.RequestParser()
parser.add_argument('username', type=str, required=True)
args = parser.parse_args()
return {'message': 'cool'}, 200
(after half an hour of reading some more documentation…)
RequestParser looks at the MultiDict request.values by default (apparently query parameters, then form body parameters according to https://stackoverflow.com/a/16664376/5139284). So then we just need to validate the data in request.values somehow.
Here's a snippet of some relevant code from Marshmallow. It seems a good deal more involved than reqparse: first you create a schema class, then instantiate it, then have it load the request JSON. I'd rather not have to write a separate class for each API endpoint. Is there something more lightweight similar to reqparse, where you can write all the types of the argument validation information within the function defining your endpoint?
from flask import Flask, request, Response
from flask_restful import reqparse
from marshmallow import (
Schema,
fields,
validate,
pre_load,
post_dump,
post_load,
ValidationError,
)
class UserSchema(Schema):
id = fields.Int(dump_only=True)
email = fields.Str(
required=True, validate=validate.Email(error="Not a valid email address")
)
password = fields.Str(
required=True, validate=[validate.Length(min=6, max=36)], load_only=True
)
joined_on = fields.DateTime(dump_only=True)
user_schema = UserSchema()
#app.route("/register", methods=["POST"])
def register():
json_input = request.get_json()
try:
data = user_schema.load(json_input)
except ValidationError as err:
return {"errors": err.messages}, 422
# etc.
If your endpoints share any commonalities in schema, you can use fields.Nested() to nest definitions within each Marshmallow class, which may save on code writing for each endpoint. Docs are here.
For example, for operations that update a resource called 'User', you would likely need a standardised subset of user information to conduct the operation, such as user_id, user_login_status, user_authorisation_level etc. These can be created once and nested in new classes for more specific user operations, for example updating a user's account:
class UserData(Schema):
user_id = fields.Int(required=True)
user_login_status = fields.Boolean(required=True)
user_authentication_level = fields.Int(required=True)
# etc ....
class UserAccountUpdate(Schema):
created_date = fields.DateTime(required=True)
user_data = fields.Nested(UserData)
# account update fields...