[FastAPI] Getting started with FastAPI, an ASGI web framework made by Python

FastAPI A Python web framework that is a micro-framework like Flask. Its strengths include high performance, ease of writing, a design that is strongly conscious of production operation, and modern functions.

FastAPI is written on the shoulder of Starlette, and asynchronous processing is easy to handle. In particular, it has the following features.

Frankly, it's very similar to responder. (Because the time when it came out is near, and the responder is also based on Starlette) However, the bottom two are designed to be much easier to use with the Fast API. From the following perspectives, I think the Fast API is only for production operation. (Personally, I think responder is easier to use if you want to write freely.)

--Documents are polite (linkage with DB, authentication, https conversion, etc. are also introduced) --Since the automatic document generation function is generous, cooperation with front-end developers is likely to improve. --There is even a docker image for production operation

I also compared the performance with some Python frameworks, and the Fast API certainly seemed to perform well. (Reference: Comparison of Python Web framework performance (Django, Flask, responder, FastAPI, japronto))

Purpose of this article

I think the official tutorial is appropriate if you want to appreciate the Fast API. It is very easy to understand because the contents are substantial. However, on the other hand, it is a little quantitatively heavy to refer to just to get started. Therefore, I would like to reorganize and introduce the contents so that the Fast API can be used with the minimum necessary.

In addition, this article is written assuming the following.

--Understand some basic notation of microframework in python --Understand the basic python type hint (mypy) notation

Code examples corresponding to the contents introduced here are summarized in here. Please use it when you want to touch only Swagger.

table of contents

intro install FastAPI Install fastapi and its ASGI server, uvicorn.

$ pip install fastapi uvicorn

intro code Let's create an API that returns `{" text ":" hello world! "}` in json when GET.

intro.py


from fastapi import FastAPI

app = FastAPI()

@app.get('/') #Specifying method and endpoint
async def hello():
    return {"text": "hello world!"}

I think it's the one that can be written concisely in Python's microframework. run server The server will start as follows. (--Reload is convenient during development because the server is updated every time the file is changed.) `intro: app``` part is filename: instance name of FastAPI () ` is. Please replace as appropriate.

$ uvicorn intro:app --reload

Check the auto-generated document (Swagger UI)

Go to http://127.0.0.1:8000/docs. This will open the Swagger UI. You can hit the API here. f895d438c0b57a8272939ee4e3521af3.gif

You will also be able to check the request and response schemas using the methods described below. One of the great strengths of FastAPI is that this document is automatically generated. If you develop normally, the document will be generated without permission.

Handling of request

The following items are handled.

GET method

Get path parameter & query parameter

You can get the parameter just by putting the parameter name in the argument. Once

--The parameter name declared as ``` / {param}` `` at the endpoint is ** path parameter ** --Other than that, it points to ** query parameter **

Please understand. Also, the order of the arguments does not matter. Then, depending on whether the default value is declared or not, the processing when the parameter included in the argument is not included at the time of GET changes.

-** not required : If you declare a default value, the default value will be used if the parameter has not arrived. - required **: On the other hand, if a parameter that does not declare a default value does not come, `{" detail ":" Not Found "}` is returned.

And, the feature of FastAPI is to add python ** type hint ** to the argument as shown below.

@app.get('/get/{path}')
async def path_and_query_params(
        path: str, 
        query: int, 
        default_none: Optional[str] = None):
    return {"text": f"hello, {path}, {query} and {default_none}"}

By doing this, when getting the parameter, the Fast API will take into account the python type hint,

-** Convert : Enter the argument with the data converted to the specified type - Validation : Returns `{" detail ":" Not Found "}` if conversion to the specified type is not possible - Automatic document generation **: Add type information to swagger UI

to hold. If you actually check Swagger, you can check the parameter type information as shown below. 5136078ab0a27e2f274d116438395bc2.png

validation In addition to the above, you can do some advanced things by using the following Query and Path. Query is for query parameter and Path is for path parameter.

from fastapi import Query, Path

Use as follows. You can use basically the same arguments for Query and Path,

--Specify the default value for the first argument. Pass `...` if you want to have no default value (required) --alias: Specify the parameter name. It is used when you want to separate the argument name and the parameter name. For when it violates the python naming convention --Other: You can limit the value received by specifying the character length, regular expression, and range of values.

@app.get('/validation/{path}')
async def validation(
        string: str = Query(None, min_length=2, max_length=5, regex=r'[a-c]+.'),
        integer: int = Query(..., gt=1, le=3),  # required
        alias_query: str = Query('default', alias='alias-query'),
        path: int = Path(10)):

    return {"string": string, "integer": integer, "alias-query": alias_query, "path": path}

You can also check the restrictions from Swagger. Since you can hit the API, try changing the value in various ways and check if the validation is done correctly. POST method

Get request body

Uninflected word

I will explain how to receive post data. First, as shown below, after inheriting `` `pydantic.BaseModel```, prepare a separate class with type hints for attributes, and add type hints with arguments as the type of request body. It's fine.

from pydantic import BaseModel
from typing import Optional, List

class Data(BaseModel):
    """Class with type hints for request data"""
    string: str
    default_none: Optional[int] = None
    lists: List[int]

@app.post('/post')
async def declare_request_body(data: Data):
    return {"text": f"hello, {data.string}, {data.default_none}, {data.lists}"}

Here, the above code assumes that the following json will be posted.

requestBody


{
    "string": "string",
    "default_none": 0,
    "lists": [1, 2]
}

If there are not enough fields, status code 422 will be returned. (If there is an extra field, it seems to work normally) Also, if you perform the processing up to this point, you can check the data structure of the expected request body from the Swagger UI. c21c87c01835cab42629eb3e88e30201.png

embed request body A little different from the previous example, I will explain the notation for the following data structure.

requestBody


{
    "data": {
        "string": "string",
        "default_none": 0,
        "lists": [1, 2]
    }
}

For such a structure, use the same Data class as before. Only the structure can be changed by using `fastapi.Body```. fastapi.Body``` is a companion to `` pydantic.Query introduced in the validation of the GET method. Similarly, the first argument is the default value. It uses an argument called embed that was not found in `` `pydantic.Query etc. The structure can be changed with the following minor changes.

from fastapi import Body

@app.post('/post/embed')
async def declare_embedded_request_body(data: Data = Body(..., embed=True)):
    return {"text": f"hello, {data.string}, {data.default_none}, {data.lists}"}

nested request body Next, I will explain how to handle the structure where lists and dictionaries are nested as follows. The structure of subData is in the form of embed request body, but I will introduce a different way of writing it.

{
    "subData": {
        "strings": "string",
        "integer": 0
    },
    "subDataList": [
        {"strings": "string0", "integer": 0},
        {"strings": "string1", "integer": 1},
        {"strings": "string2", "integer": 2}
    ]
}

Nested type declarations are often very crude with python type hints. (If you want to get a rough idea, it's enough to type the following subDataList such as `List [Any]` or `List [Dict [str, Any]`) On the other hand, FastAPI (or pydantic) can handle complex nested structures. You can faithfully define subclasses along the nesting structure and add type hints as shown below.

class subDict(BaseModel):
    strings: str
    integer: int

class NestedData(BaseModel):
    subData: subDict
    subDataList: List[subDict]

@app.post('/post/nested')
async def declare_nested_request_body(data: NestedData):
    return {"text": f"hello, {data.subData}, {data.subDataList}"}

validation What you can do and what you can do with the GET method is almost the same. The difference is that it uses `pydantic.Field``` instead of fastapi.Query```. But the arguments are the same. I just introduced `` pydantic.Field to each class used in the nested request body. You can also use it with `` `fastapi.Query etc., but it uses the argument example. The data passed to this argument will be the default value when hitting the API from Swagger.

from pydantic import Field

class ValidatedSubData(BaseModel):
    strings: str = Field(None, min_length=2, max_length=5, regex=r'[a-b]+.')
    integer: int = Field(..., gt=1, le=3)  # required

class ValidatedNestedData(BaseModel):
    subData: ValidatedSubData = Field(..., example={"strings": "aaa", "integer": 2})
    subDataList: List[ValidatedSubData] = Field(...)

@app.post('/validation')
async def validation(data: ValidatedNestedData):
    return {"text": f"hello, {data.subData}, {data.subDataList}"}

Handling of response

You can also define a class like the one defined in request body for response and perform validation.

Uninflected word

If you pass it to response_model, by default,

--For the returned dictionary, the key that does not have a name that matches attributes is discarded. --It is not included in the returned dictionary, but if there is a default value in attributes, that value is compensated.

Here, if you write as follows, integer is discarded from the returning dictionary, aux is supplemented, and json is returned. (A very simple example is given, but if it is nested or a little complicated validation is required, you can use the notation for type hints as mentioned in "Handling of request" as it is)

class ItemOut(BaseModel):
    strings: str
    aux: int = 1
    text: str

@app.get('/', response_model=ItemOut)
async def response(strings: str, integer: int):
    return {"text": "hello world!", "strings": strings, "integer": integer}

At this stage, you can check the schema of response data from Swagger. bb16c30d6110d5ec387b8e8edca89fc8.png

Derived form

There are several options for using response_model.

#Response if it does not exist in the dictionary_The default value for model attributes"I can't put it in"
@app.get('/unset', response_model=ItemOut, response_model_exclude_unset=True)
async def response_exclude_unset(strings: str, integer: int):
    return {"text": "hello world!", "strings": strings, "integer": integer}

# response_of model"strings", "aux"Ignore-> "text"Only return
@app.get('/exclude', response_model=ItemOut, response_model_exclude={"strings", "aux"})
async def response_exclude(strings: str, integer: int):
    return {"text": "hello world!", "strings": strings, "integer": integer}

# response_of model"text"Only consider-> "text"Only return
@app.get('/include', response_model=ItemOut, response_model_include={"text"})
async def response_include(strings: str, integer: int):
    return {"text": "hello world!", "strings": strings, "integer": integer}

error handling & status code management

There are three stages of status code management.

--Declare default status code: Declare with decorator --Error handling returns 400s: raise `fastapi.HTTPException``` in the right place --Flexibly change status code and return: Touch starlette directly --Added starlette.responses.Response as an argument --You can change the output status code by rewriting `` `response.status_code --Return the data you want to return as usual

from fastapi import HTTPException
from starlette.responses import Response
from starlette.status import HTTP_201_CREATED

@app.get('/status', status_code=200) #default status code specification
async def response_status_code(integer: int, response: Response):
    if integer > 5:
        # error handling
        raise HTTPException(status_code=404, detail="this is error messages")
    elif integer == 1:
        # set manually
        response.status_code = HTTP_201_CREATED
        return {"text": "hello world, created!"}
    else:
        # default status code
        return {"text": "hello world!"}

background process You can use the background process to return only the response before the heavy processing is complete. This process is quite difficult for WSGI (Django, etc.). However, Starlette-based ASGI makes this process very concise.

The procedure is

  1. Declare an argument of type fastapi.BackgroundTasks
  2. Throw a task with `` `.add_task```

It's hard to predict what's going on, but I think the description itself is easy.

As an example of heavy processing, try running a background process that sleeps for the received path parameter seconds and then prints.

from fastapi import BackgroundTasks
from time import sleep
from datetime import datetime

def time_bomb(count: int):
    sleep(count)
    print(f'bomb!!! {datetime.utcnow()}')

@app.get('/{count}')
async def back(count: int, background_tasks: BackgroundTasks):
    background_tasks.add_task(time_bomb, count)
    return {"text": "finish"} # time_Returns a response without waiting for the bomb to finish

The results are processed in the following order

  1. Response headers date = 17:37:14
  2. Date output to print = 17:37:25

So it seems that it is being processed properly in the background. スクリーンショット 2020-01-03 2.39.19.png

unittest Starlette's TestClient is excellent and you can easily hit the api for unittest. This time, I will try unittest with pytest according to the tutorial.

install

$ pip install requests pytest

directory placement

├── intro.py
└── tests
    ├── __init__.py
    └── test_intro.py

Now, let's do the following unit test.

intro.py


from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional, List
app = FastAPI()

@app.get('/')
async def hello():
    return {"text": "hello world!"}

class Data(BaseModel):
    string: str
    default_none: Optional[int] = None
    lists: List[int]

@app.post('/post')
async def declare_request_body(data: Data):
    return {"text": f"hello, {data.string}, {data.default_none}, {data.lists}"}

unittest The selling point is that you can easily hit GET and POST with `starlette.testclient.TestClient` as shown below, and you can make assert of the response.

test_intro.py


from starlette.testclient import TestClient
from intro import app

# get and assign app to create test client
client = TestClient(app)

def test_read_hello():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"text": "hello world!"}

def test_read_declare_request_body():
    response = client.post(
        "/post",
        json={
            "string": "foo",
            "lists": [1, 2],
        }
    )
    assert response.status_code == 200
    assert response.json() == {
        "text": "hello, foo, None, [1, 2]",
    }

Run pytest

$ pytest
========================= test session starts =========================
platform darwin -- Python 3.6.8, pytest-5.3.2, py-1.8.1, pluggy-0.13.1
rootdir: ***/***/***
collected 1 items

tests/test_intro.py .                                            [100%]
========================== 1 passed in 0.44s ==========================

deployment You have the following options: It's a simple application, so I don't think there will be any problems with the infrastructure.

--If you can pip install and start uvicorn, it works like local --Docker image (Official): It seems that the performance has been tuned. Above all, it is official, so there is a sense of trust.

Basically, if you can use docker, the latter method is better, and if other than that (such as making a quick API with PaaS), the former method is better.

Regarding the specifics, there is no processing specific to FastAPI, and it is a procedure that is not related to other microframework, so I will omit it this time. reference:

-Official documentation (deployment) -Heroku example

Others (Correspondence to CORS problems, certification)

Here is a reference for other frequently-used settings and context-sensitive matters that need not be written as a tutorial.

-Fixed CORS (Cross-Origin Resource Sharing) problem: This is a problem that occurs when the frontend is on a different server from the backend. -Authentication: Contains examples of OAuth2 and HTTP basic authentication. Documents are automatically generated for authentication

Summary

This is the end of the minimum tutorial. You should now be able to develop a complete API server-> deployment.

If you want to handle database linkage, html rendering, websocket, GraphQL, etc. in addition to the contents dealt with this time, I think that it is enough to refer only to the related chapters.

Anyway, it is convenient that Swagger is generated automatically, so I would like you to try it while moving your hands!

Finally, although it has little to do with the content of this article, I would like to introduce you to the most interesting chapters in the official Fast API documentation. The development process and the points that differentiate it from other frameworks are mentioned.

Refs -Official document

Supplement

This is Miso, but I tried to generate Schema definition-> swagger with responder before, but the amount of description was completely different. (Since there is no description of FastAPI only for Swagger) here You can see how amazing FastAPI is. I think you can.

Recommended Posts

[FastAPI] Getting started with FastAPI, an ASGI web framework made by Python
Getting Started with Python Web Applications
Getting Started with Python Web Scraping Practice
Getting Started with Python Web Scraping Practice
1.1 Getting Started with Python
Getting Started with Python
Getting Started with Python
Introduction to Tornado (1): Python web framework started with Tornado
Getting Started with Python Functions
Getting Started with Python Django (4)
Getting Started with Python Django (3)
Getting Started with Python Django (6)
Python3 | Getting Started with numpy
Getting Started with Python Django (5)
Getting Started with Python responder v2
Getting Started with Python for PHPer-Classes
Getting Started with Python Basics of Python
Getting Started with Python Genetic Algorithms
Getting started with Python 3.8 on Windows
Getting Started with Python for PHPer-Functions
Getting Started with python3 # 1 Learn Basic Knowledge
Getting Started with Flask with Azure Web Apps
Getting Started with Python for PHPer-Super Basics
Getting started with Dynamo from Python boto
Let's make a web framework with Python! (1)
Let's make a web framework with Python! (2)
Getting started with Python with 100 knocks on language processing
Python Web Content made with Lolipop cheap server
Getting started with AWS IoT easily in Python
Materials to read when getting started with Python
Settings for getting started with MongoDB in python
Django 1.11 started with Python3.6
Getting Started with python3 # 2 Learn about types and variables
Django python web framework
Getting Started with Golang 2
Getting started with apache2
Getting Started with Django 1
Web application made with Python3.4 + Django (Part.1 Environment construction)
Getting Started with Optimization
Getting Started with Google App Engine for Python & PHP
Getting Started with Golang 3
Getting Started with Numpy
Getting started with Spark
Getting Started with Pydantic
Getting Started with Golang 4
Getting Started with Jython
Getting Started with Django 2
I created an environment for Masonite, a Python web framework similar to Laravel, with Docker!
Python Web framework performance comparison (Django, Flask, responder, FastAPI, japronto)
Getting Started with python3 # 3 Try Advanced Computations Using Import Statements
Let's get started with Python ~ Building an environment on Windows 10 ~
Getting Started with Mathematics Starting with Python Programming Challenges Personal Notes-Problem 1-1
Translate Getting Started With TensorFlow
Getting Started with Tkinter 2: Buttons
Web scraping with python + JupyterLab
Getting Started with Go Assembly
EXE Web API by Python
I made blackjack with python!
Creating an egg with python
Web API with Python + Falcon
Python started by C programmers