Using FastAPI to Deploy ML Models
Using FastAPI to Deploy ML Models
Notes on using FastAPI as I go through the online documentation
Kedar Dabhadkar
Data Scientist
linkedin.com/in/dkedar7
What is FastAPI?
- This is what FastAPI is
First steps
- This is simply how we define an app and write our first GET request.
from fastapi import FastAPI
= FastAPI()
app
@app.get("/")
async def root():
return {"message": "Hello World"}
- If this code is saved to main.py, use the follwowing to run it from the command line:
uvicorn main:app --reload
- FastAPI also automatically generates a Swagger documentation at ‘http://127.0.0.1:8000’
## 1. Path parameters
- Path parameters or variables can be defined in the decorator and also as an input to the function.
```python
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}
Value of ‘item_id’ is passed to the function
This function is run when a user accesses the endpoint ‘http://127.0.0.1:8000/items/3’
Also, order matters.
In the following example, although
/users/me
matches with the pattern/users/{user_id}
, it’s executed first because it appears first
@app.get("/users/me")
def read_user_me():
return {"user_id": "the current user"}
@app.get("/users/{user_id}")
def read_user(user_id: str):
return {"user_id": user_id}
2. Query Parameters
- All the function arguments that are not a part of the path parameters are treated as query parameters.
- Query parameters appear after
?
in a URL and are separated by&
@app.get("/items/")
async def read_item(skip: int = 0, limit: int = 10):
return data[skip : skip + limit]
- If a default value is set in the function arguments, these parameters become optional. If not, they are mandatory.
Multiple path and query parameters
- You can declare multiple path parameters and query parameters at the same time, FastAPI knows which is which.
- They will be detected by name, so the order doesn’t matter.
- For example,
@app.get("/users/{user_id}/items/{item_id}")
async def read_user_item(
int, item_id: str, q: str = None, short: bool = False
user_id:
) ...
3. Request Body
- A request body is data sent by the client to your API.
- To declare a request body, you use Pydantic models.
- A request body is usually applicable to non-GET APIs (most POST)
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
str
name: str] = None
description: Optional[float
price: float] = None
tax: Optional[
= FastAPI()
app
@app.post("/items/")
async def create_item(item: Item):
return item
Request body + path + query parameters
- You can also declare body, path and query parameters, all at the same time
@app.put("/items/{item_id}")
def create_item(item_id: int, item: Item, q: Optional[str] = None):
= {"item_id": item_id, **item.dict()}
result if q:
"q": q})
result.update({return result
- If the parameter is also declared in the path, it will be used as a path parameter.
- If the parameter is of a singular type (like int, float, str, bool, etc) it will be interpreted as a query parameter
- If the parameter is declared to be of the type of a Pydantic model, it will be interpreted as a request body.
4. Query Parameters and String Validations
- FastAPI allows you to declare additional information and validation for your parameters.
Example 1. Length of parameter doesn’t exceed 50 characters and it is optional
from fastapi import FastAPI, Query
@app.get("/items/")
def read_items(q: Optional[str] = Query(None, max_length=50)):
...
The Optional
keyword is only to guide our code editor. Here, None
is the default value.
Example 2. In addition, the minimum length should be 3
@app.get("/items/")
def read_items(q: Optional[str] = Query(None, min_length=3, max_length=50)):
...
Example 3. It should match the regex ^fixedquery$
@app.get("/items/")
def read_items(q: Optional[str] = Query(None, min_length=3, max_length=50, regex="^fixedquery$")):
...
Example 4. When you want to declare a variable as required while using Query
@app.get("/items/")
def read_items(q: Optional[str] = Query(..., min_length=3)):
...
...
is called the Ellipsis in Python.
Example 5. Query parameter list / multiple values
@app.get("/items/")
def read_items(q: Optional[List[str]] = Query(None)):
...
Example 6. Query parameter list / multiple values with defaults
@app.get("/items/")
def read_items(q: Optional[List[str]] = Query(["foo", "bar"])):
...
Example 7. Declare more metadata
@app.get("/items/")
def read_items(
str] = Query(
q: Optional[None,
="item-query",
alias="Query string",
title="Query string for the items to search in the database that have a good match",
description=3,
min_length=50,
max_length="^fixedquery$",
regex=True,
deprecated
)
): ...
This indicates that the parameter - Is an alias for item-query
- Has the mentioned title and decription - And is deprecated
5. Path parameters and numeric validations
Path parameters
- You can declare the same type of validations and metadata for path parameters with Path.
- You can declare all the same parameters as for
Query
.
For example,
@app.get("/items/{item_id}")
def read_items(
int = Path(..., title="The ID of the item to get"),
item_id: str] = Query(None, alias="item-query"),
q: Optional[
): ...
Numeric validations
- Just like string validations, you can also define numeric validations.
- Examples:
ge
: greater than or equal to,le
: less than or equal to,gt
: greater than, andlt
: less than.
@app.get("/items/{item_id}")
def read_items(
int = Path(..., gt=0, lt=1)
item_id:
): ...
6. Multi-body parameters
- Like
Query
for query params andPath
for path params, FastAPI hasBody
for body parameters. - All the same arguments apply for body params
- As we saw earlier, FastAPI identifies body parameters if they are instances of
BaseModel
.
Example of defining path, query and body parameters together
class Item(BaseModel):
str
name: str] = None
description: Optional[float
price: float] = None
tax: Optional[
def update_item(
int = Path(..., title="The ID of the item to get", ge=0, le=1000),
item_id: str] = None,
q: Optional[= None,
item: Optional[Item]
): ...
Specifying multiple body parameters
- Simply define multiple
BaseModel
derived classes
class Item(BaseModel):
str
name: str] = None
description: Optional[float
price: float] = None
tax: Optional[
class User(BaseModel):
str
username: str] = None full_name: Optional[
- The expected body should look like this
{
"item": {
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2
},
"user": {
"username": "dave",
"full_name": "Dave Grohl"
}
}
- Or the body parameters can be singular values
def update_item(
int, item: Item, user: User, importance: int = Body(..., title="", description="")
item_id: ):
In this case, FastAPI will expect a body like:
{
"item": {
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2
},
"user": {
"username": "dave",
"full_name": "Dave Grohl"
},
"importance": 5
}
7. Body - Fields
You can declare validation and metadata inside of Pydantic models using Pydantic’s
Field
.Field
works the same way as Query, Path and Body, it has all the same parameters, etc.
from pydantic import BaseModel, Field
class Item(BaseModel):
str
name: str] = Field(
description: Optional[None, title="The description of the item", max_length=300
)float = Field(..., gt=0, description="The price must be greater than zero")
price: float] = None tax: Optional[
8. Body - Nested Models
How do we have arrays/ lists in body parameters?
- Use
List
datatype and include which type of objects should be entered in this.
Example:
from typing import List, Optional, Set
class Item(BaseModel):
str
name: str] = None
description: Optional[float
price: float] = None
tax: Optional[str] = [] tags: List[
How do we have nested models?
- These Pydantic models act like
dict
s.
class Image(BaseModel):
str
url: str
name:
class Item(BaseModel):
str
name: str] = None
description: Optional[float
price: float] = None
tax: Optional[str] = []
tags: Set[= None image: Optional[Image]
- Or you can also have the nested model as a list:
class Image(BaseModel):
str
url: str
name:
class Item(BaseModel):
str
name: str] = None
description: Optional[float
price: float] = None
tax: Optional[str] = []
tags: Set[= None image: Optional[List[Image]]
9. Declare Request Example Data
- You can declare an example for a Pydantic model using
Config
andschema_extra
.
class Item(BaseModel):
str
name: str] = None
description: Optional[float
price: float] = None
tax: Optional[
class Config:
= {
schema_extra "example": {
"name": "Foo",
"description": "A very nice Item",
"price": 35.4,
"tax": 3.2,
} }
Using example
- When using any of:
Path()
,Query()
,Header()
,Cookie()
,Body()
,Form()
,File()
you can also declare a data example or a group of examples with additional information that will be added to OpenAPI.
@app.put("/items/{item_id}")
def update_item(
int,
item_id: = Body(
item: Item
...,={
example"name": "Foo",
"description": "A very nice Item",
"price": 35.4,
"tax": 3.2,
},
),
): ...
10. Extra data types
List of additionl data types: https://fastapi.tiangolo.com/tutorial/extra-data-types/
12. Header Parameters
- You can define Header parameters the same way you define Query, Path and Cookie parameters.
from typing import Optional
from fastapi import FastAPI, Header
= FastAPI()
app
@app.get("/items/")
async def read_items(user_agent: Optional[str] = Header(None)):
return {"User-Agent": user_agent}
- Additionally with
Headers
, FastAPI will convert any_
s to hyphens by default, since many headers have hyphens in them. - This behavior can be changed by changing the
convert_underscores
argument toFalse
inQuery
.
Duplicate headers
- It’s possible for a request to specify duplicate headers.
Like this:
@app.get("/items/")
def read_items(x_token: Optional[List[str]] = Header(None)):
return {"X-Token values": x_token}
13. Response Model
- Just like you can specify a model for the input data, you can also specify a model for the response. This model needs to be given as an argument to the decorator.
Example:
class UserIn(BaseModel):
str
username: str
password:
email: EmailStrstr] = None
full_name: Optional[
class UserOut(BaseModel):
str
username:
email: EmailStrstr] = None
full_name: Optional[
@app.post("/user/", response_model=UserOut)
...
- If your response model has default values that you want to exclude from the response, you can specify the value of the argument
response_model_exclude_unset
toTrue
. - This Pydantic argument is smart enough to different which values were not set at all (for which default got entered) vs the values that were entered but matched the default values by chance.
14. Extra Models
- Various models can inherit from the other to reduce redundancy in the code. This allows you to write just the difference between models.
Example:
class UserBase(BaseModel):
str
username:
email: EmailStrstr] = None
full_name: Optional[
class UserIn(UserBase):
str
password:
class UserOut(UserBase):
pass
class UserInDB(UserBase):
str hashed_password:
- A response can be a union, intersection or a list of multiple models.
class BaseItem(BaseModel):
str
description: type: str
class CarItem(BaseItem):
type = "car"
class PlaneItem(BaseItem):
type = "plane"
int
size:
@app.get("/items/{item_id}", response_model=Union[PlaneItem, CarItem])
...
- Response can also be a List of models
from typing import List
class Item(BaseModel):
str
name: str
description:
= [
items "name": "Foo", "description": "There comes my hero"},
{"name": "Red", "description": "It's my aeroplane"},
{
]
@app.get("/items/", response_model=List[Item])
def read_items():
return items
15. Response Status Code
Status codes to be returned with a response can be defined in the
status_code
argument of the decorator.Each error code has a standard use. For example, > 100 and above are for “Information”
> 200 and above are for “Successful” responses
> 300 and above are for “Redirection”
> 400 and above are for “Client error” responses
> 500 and above are for server errorsFastAPI provides status code variables in
fastapi.status
so that the developer does not have to remember them. For example,status.HTTP_201_CREATED
.
16. Form data
- When you need to receive form fields instead of JSON, you can use Form.
- To use forms, first install python-multipart with
pip install python-multipart
.
from fastapi import FastAPI, Form
@app.post("/login/")
def login(username: str = Form(...), password: str = Form(...)):
return {"username": username}
- Form parameters work the same way as
Body
orQuery
.
@app.post("/login/")
def login(username: str = Form(...), password: str = Form(...)):
return {"username": username}
- Data from forms is normally encoded using the “media type”
application/x-www-form-urlencoded
.
17. Request files
- You can define files to be uploaded by the client using File.
- To receive uploaded files, first install python-multipart with
pip install python-multipart
. - The
File
andUploadFile
methods can be used to handle uploaded file contents.
Example:
@app.post("/files/")
def create_file(file: bytes = File(...)):
return {"file_size": len(file)}
@app.post("/uploadfile/")
def create_upload_file(file: UploadFile = File(...)):
return {"filename": file.filename}
File
only works well for small files (it tries to store the content of files on memory). Whereas,UploadFile
also works well with large files.- More details on handling uploaded file contents is outside the scope of these notes, for now.
18. Request Forms and Files
- You can define files and form fields at the same time using File and Form.
- Create file and form parameters the same way you would for Body or Query:
from fastapi import FastAPI, File, Form, UploadFile
@app.post("/files/")
def create_file(
file: bytes = File(...), fileb: UploadFile = File(...), token: str = Form(...)
):return {
"file_size": len(file),
"token": token,
"fileb_content_type": fileb.content_type,
}