FastAPI

From Colettapedia
Jump to navigation Jump to search

Why fastAPI

  • a free UI to test your rest api - interactive API documentation systems, OpenAPI (Swagger) and JSON Schema standards
  • Supposedly security and authentication is integrated
  • Supposedly fast
  • Editor support, useful for code editor autocompletion

Technologies

  • REST
    • how you form your API
    • Representational State Transfer
    • Has an HTTP verb, a URL which related to a bit of data, and a parameterization of that
  • Async
    • How you deliver it
    • Co-operative multitasking
    • Puts the onus on you to decide when the rest of your computer should be able to do stuff
    • Easy to get wrong
    • Easy to write code that will block the whole web application rather than make just one request slow
    • Excels in that it's able to deal with thousands and thousands of network sockets
  • Dependency Injection (DI)


URL mapping

app = FastAPI()

@app.get( "/" )
def root( ... ):
   return {"greeting": "Hello world"}

app.include_router( events.router, prefix="/events"...)
router = APIRouter()

@router.post( "/", ..., status=201)
def create_object( ... ):
   ...


Getting stuff from requests: Path variables

  • given this info
    • GET /events/1234?detail=full
    • Authorization: Token ABCD1234
  • implicit way
@router.get( "/event/{id}" )
def get_object( id: int ):
  ...
  • more explicit way - make things be required
from fastapi import Path
from pydantic import Required

@router.get( "/event/{id}" )
def get_object( id: int = Path(Required) ):
  ...


Getting stuff from URL requests: Query parameters

  • given this info
    • GET /search/?text=something
    • Authorization: Token ABCD1234
  • implicit way
@router.get( "/search" )
def search( text: str = None, offset: int = 0, limit: int = 100 ):
  ...
  • more explicit way - make things be required
from fastapi import Query

@router.get( "/search" )
def search(
  text: str = Query( default=None, max_length=20 ),
  offset: int = Query( 0 ),
  limit: int = Query( 100 )
):

Data Validation: Pydantic

  • Is this valid data??
  • Throws helpful error messages if the data isn't formatted correctly
  • "This field is required, but you didn't specify it."
from datatime import date as DateType
from enum import Enum
from pydantic import BaseModel

class Types( Enum ):
  done = 'DONE'
  cancelled = 'CANCELLED'

class Event( BaseModel ):
  date: DateType
  type: Types
  text: str

Extract blob of JSON that came in from request

from pydantic import Required

@router.post( "/events/" )
def create_object( event: Event = Required ):
   ...

Creating responses

  • If you need to retun something, just return simple Python objects. The framework renders them to JSON for you
@app.get( "/" )
def root( ... ):
  return {"greeting": "Hello World" }

@app.post( "/events/". response_model=Event, status_code=201 )
def create_object( event: Event = Required ):
  ...
  return {
    "date": "2019-06-02",
    "type": "DONE",
    "text": "some stuff got done"
  }

Implied and explicit Query, Path and Body Parameters

Can use standard python types, or more funky python types like datetime.timedelta

Query

  • Implicit way: params given in function args are by default query params. Can declare type and default value/optionality
  • If you need additional validation, set arg default value as type fastapi.Query
    • min/max length
    • regex
  • Declare more metadata
    • title
    • description
    • alias
    • deprecated=True
  • set default as first arg to fastapi.Query()
  • make required by using elipsis as first arg
  • Can allow user to reuse query key by declaring type as typing.List[<type>]
    • Can define list of multiple values as default

Path

  • Implicit way: Path paramters are always required as it has to be part of the path
    • Just put the path parameter inside the curly braces on the path argument, e.g., "@app.get("/items/{item_id}")"
  • Explicit way: Include the parameter explicitly as a function argument of type fastapi.Path()
  • order of the different parameters doesn't matter
    • Can have first argument of function by * to let python know that all arguments following it should be taken as kwargs, which will allow function arg reordering.
  • validations
    • gt/lt, ge/le

Body

  • subclass the pydantic.BaseModel object
  • declare class attributes
  • In function args declare type as your new subclass
  • fastapi.Body - Use to declare the entire unit of body params in the subclass to be optional, or to setup additional validation, metadata.
    • Additional optional args to Body: example
  • pydantic.Field - Use Field for additional validation on individual members of the BaseModel subclass
    • Model attributes/validation/metadata declared via Field() function args include title, max_length, description, gt, example="Foo", etc...
  • can nest list of Body Parameters withing body parameters using typing.List, typing.Set (converts collections with duplicate data into set of unique items), typing.Dict[int, float] for example, etc...
  • include a member class Config within subclass class attributes
    • schema_extra to provide documentation example

Form

  • pass

File

  • File inherits from Form
  • Files are uploaded as "Form data". Form data has special encoding that;s different from JSON
    • Thus the header multipart/form-data
  • UploadFile has following attributes
    • filename
    • content_type
    • file - an object with a file-like interface
    • async methods that need to be awaited if your function is async
      • contents = await myfile.read()
      • write, seek, and close


Response models

  • Declare a response model for validation and documentation purposes. Pydantic will also filter out any members not declared in the model.
    • Can also leave out unset params in response using arg response_model_exclude_unset=True
  • Typical to have multiple related models.
    • Can dump contents of model A's data instance directly into the constructor for modelB's instance another using ModelB( **model_a.dict() ). The filtering will also happen at this step.
    • Can have SUB-subclasses to reduce code duplication.

Using the Request Directly (GET IP ADDRESS)

  • fastapi.Request is just alias to starlette.Request
  • Include request: Request in the function args
  • This is how you get access to unvarnished, unvalidated request as it comes in from client
  • request attributes
    • request.method
    • request.url, request.url.path, request.url.port, request.url.scheme
    • request.headers['content-type'] (e.g.)
    • request.query_params['search'] (e.g.)
    • request.path_params['username'] (e.g.)
    • request.client, *HOSTNAME OR IP ADDRESS* request.client.host,
    • request.cookies.get('mycookie')

Background Tasks

  • Send email notifications, process data, write to logs, etc AFTER returning a response
  1. Import fastapi.BackgroundTasks
  2. Define a parameter background_tasks in the path operation function with type declaration of BackgroundTasks
  3. Use background_tasks.add_task( function, args, kwargs )
  • Use dependency injection to hang a background task on the queue anywhere on the call stack (not just within the path operation function)

Containerization

Single Server Docker Image

  • uvicorn-gunicorn-fastapi-docker - Docker image with Uvicorn managed by Gunicorn with FastAPI
    • handles replication using a process manager (like Gunicorn with Uvicorn workers) in each container

Env Vars

MODULE_NAME
  • MODULE_NAME=app.main by default if there's a file /app/app/main.py
  • MODULE_NAME=main by default if there's a file /app/main.py
  • MODULE_NAME=custom_app.custom_main by default if there's a file /app/custom_app/custom_main.py
VARIABLE_NAME
  • named value inside the module MODULE_NAME that is an instance of class FastAPI.FastAPI, e.g., within main.py foo = FastAPI() VARIABLE_NAME=foo

Other stuff I need to learn

  • fastapi.Cookie
  • the role of header parameters