Epidemiology & Technology

Using FastAPI and Pydantic

To rapidly develop an API in python with data models that are validated easily and autogenerated OpenAPI / Swagger / ReDoc documentation, FastAPI its an excellent option.

VENV Creation and Installation

python3 -m venv asicsapi
cd asicsapi/
source bin/activate
git init
touch .gitignore
pip3 install -r requirements.txt
pip install fastapi uvicorn[standard] databases[sqlite] python-multipart
touch main.py
.gitignore File contents
# Virtualenv
.Python
[Bb]in
[Ii]nclude
[Ll]ib
[Ll]ib64
[Ll]ocal
[Ss]cripts
pyvenv.cfg
.venv
pip-selfcheck.json
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
#   For a library or package, you might want to ignore these files since the code is
#   intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

Creating an FastApi app instance and add HTTP Routes – Main.py

Create an instance of FastAPI service. Add a basic route for Home page

from fastapi import FastAPI
app = FastAPI()

# Home Route
@app.get("/")
async def root():
    return {"message": "Hello World"}

# Items Route
@app.get("/items")
async def items():
    return {"message": "127.0.0.0:8000/items"}Code language: Python (python)

Run the server

uvicorn  main:app --reloadCode language: CSS (css)

View the API at

Path and Query Parameters for HTTP Get Requests

## Add the following lines

# Item_id passed as Path Parameter of Type Integer
@app.get("/items/{item_id}")
async def read_item(item_id: int):
    return {"item_id": item_id}

# Restricted set of users allowed as Path Parameters - using Enum
from enum import Enum
class Users(str, Enum):
    vivek = "Vivek"
    gupta = "Gupta"
    epidemiologist = "Epidemiologist"

@app.get("/users/{user_name}")
async def users(user_name: Users):
    return {"user": user_name}

# Both Path Parameter (item_id) and Query Parametrs: 
# needy, a required str.
# skip, an int with a default value of 0.
# limit, an optional int
from typing import Optional
@app.get("/items2/{item_id}")
async def read_user_item(
    item_id: str, 
    needy: str, 
    skip: int = 0, 
    limit: Optional[int] = None):
    item = {"item_id": item_id, "needy": needy, "skip": skip, "limit": limit}
    return itemCode language: Python (python)

JSON Request Body in HTTP PUT routes

Parametrs accepted by the service in Request Body can be defined as Pydantic models and then the model is added to the route inside the function definition. By default, JSON request body is accepted.

# JSON Request Body
from pydantic import BaseModel
class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None

@app.put("/add_items/{item_id}")
async def create_item(item_id: int, item: Item):
    return {"item_id": item_id, **item.dict()}

Here the Item model has been defined with two mandatory and two optional parameters. Then in the app.put route, one path parameter has been given and in it’s function, the Item model is being added as the expected Request body data. We return the item_ID, as well as the concatenated JSON of request body.

Try going to http://127.0.0.1:8000/docs#/default/create_item_add_items__item_id__put

HTTP PUT using Form Data

The way HTML forms (<form></form>) sends the data to the server normally uses a “special” encoding for that data, it’s different from JSON. Data from forms is normally encoded using the “media type” application/x-www-form-urlencoded. But when the form includes files, it is encoded as multipart/form-data. When you need to receive Form fields instead of JSON, you can use Form in the function definition.

from fastapi import Form
@app.post("/login_form/")
async def login2(username: str = Form(...), password: str = Form(...)):
    return {"username": username,
            "logged_in": True }Code language: JavaScript (javascript)

Upload Files

from fastapi import UploadFile, File
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile = File(...)):
    return {"filename": file.filename,
            "content_type": file.content_type}Code language: JavaScript (javascript)

Please see discussion on trying to measure the File Size when using UploadFile here.

Pydantic Model Validations

from pydantic import BaseModel, ValidationError, validator, Field
from pydantic import NegativeInt, conint, conlist, constr, StrictInt, PositiveInt
from enum import Enum, IntEnum
from datetime import date, datetime

class AsicsOptions(IntEnum):
    Never = 1
    Sometimes = 2 
    Often = 3
    Always = 4  

class AsicsForm(BaseModel):
    q1:  AsicsOptions
    q2:  conint(gt=0, lt=5)
    q3:  constr(regex=r'^[1-4]$')
    q4:  conint(gt=0, lt=5)
    username: str
    time_submit: datetime

    @validator('username')
    def username_alphanumeric(cls, v):
        assert v.isalnum(), 'must be alphanumeric'
        return v

Please see this page for some more examples

Related posts