FastAPI tutorial, combined with vue to achieve front-end and back-end separation

Documentation in English: https://fastapi.tiangolo.com/
Documentation in Chinese: https://fastapi.tiangolo.com/zh/

Why does the Sanic framework that is being touted dare to claim to be the strongest? : https://zhuanlan.zhihu.com/p/360513398

Web Frameworks Ranking: https://github.com/the-benchmarker/web-frameworks

1. FastAPI Tutorial

brief introduction

FastAPI, like Sanic, is an asynchronous web framework in Python. Compared with Sanic, FastAPI is more mature and the community is more active.

FastAPI standing on the shoulders of giants?
To a large extent, this giant refers to the Flask framework.
FastAPI is very similar to Flask in terms of syntax, and has the same purpose.
In fact, not only FastAPI, but even Sanic is a Web API framework based on Flask for rapid development.

FastAPI is a modern, fast (high-performance) web framework for building APIs, using Python 3.6+ and based on standard Python type hints.

It has the following advantages:

  • Fast Extreme performance comparable to NodeJS  and  Go (thanks to Starlette and Pydantic).Starlette 用于路由匹配,Pydantic 用于数据验证
  • Efficient coding : increase the speed of function development by about 200% to 300%
  • Fewer bugs : about 40% less human (developer) induced errors.
  • Smart : Excellent editor support. Auto-completion everywhere, reducing debugging time
  • Simple : designed to be easy to use and learn, less time spent reading documentation
  • Short : Minimize code duplication. Rich functions are realized through different parameter declarations. less bugs
  • Robust : Produce usable levels of code. There is also automatically generated interactive documentation
  • Standardization : Based on (and fully compatible with) the relevant open standards of the API: OpenAPI  (formerly known as Swagger) and  JSON Schema .

The biggest feature of FastAPI is that it uses Python type annotations, and using FastAPI requires a Python version greater than or equal to 3.6.

Install FastAPI

pip install fastapi will automatically install Starlette and Pydantic; then pip install uvicorn, because uvicorn is the server that runs related applications. Or go to the stomach in one step: pip install fastapi[all] will install all dependencies.

Tutorial

Python high-performance web framework - FastApi comprehensive guide: https://zhuanlan.zhihu.com/p/397029492
The most popular asynchronous framework: https://www.cnblogs.com/traditional/p/14733610.html

Official website tutorial: https://fastapi.tiangolo.com/zh/tutorial/

Advanced User Guide: https://fastapi.tiangolo.com/zh/advanced/

Concurrency, Deployment, Build Project Templates

Introduction to Python Type Hints

https://fastapi.tiangolo.com/zh/python-types/

Python type annotations, everything you need to know is here: https://zhuanlan.zhihu.com/p/419955374
Best practices for object annotation attributes: https://docs.python.org/zh-cn/3/howto /annotations.html

2. Use of FastAPI

Simple usage example

Example: Synchronous code

from typing import Optional
from fastapi import FastAPI
 
app = FastAPI()
 
 
@app.get("/")
def read_root():
    return {"Hello": "World"}
 
 
@app.get("/items/{item_id}")
def read_item(item_id: int, q: Optional[str] = None):
    return {"item_id": item_id, "q": q}

Example: asynchronous code

Create a new  main.py  file

import os
import uvicorn
from pathlib import Path
from fastapi import FastAPI

app = FastAPI()


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


if __name__ == '__main__':
    # os.system('uvicorn main_test:app --reload')
    uvicorn.run(f'{Path(__file__).stem}:app', host="0.0.0.0", port=5555)
    pass

FastAPI recommends using uvicorn to run the service, Uvicorn is a lightning-fast ASGI server built on top of uvloop and httptools.

The meaning of the uvicorn main:app command is as follows:

  • main: The main.py file (a Python "module").
  • app: The object created by app = FastAPI() in the main.py file.
  • --reload: Make the server restart after updating the code. Use this option only when developing.

Visit http://127.0.0.1:5555   to see the response in JSON format: {"message": "Hello World"}

request query parameters

A "request path" is also commonly referred to as an "endpoint" or "route".

import uvicorn
from pathlib import Path
from fastapi import FastAPI

app = FastAPI()  # 创建 api 对象


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


@app.get("/items/")
async def read_item(skip: int = 0, limit: int = 10):
    return fake_items_db[skip: skip + limit]


@app.get("/")  # 根路由。「请求路径」也通常被称为「端点」或「路由」。
async def root():
    return {"name": "king", 'age': 100}


@app.get("/say/{data}")
async def say(data: str, q: int = None):
    return {"data": data, "q": q}


if __name__ == '__main__':
    # os.system('uvicorn main_test:app --reload')
    uvicorn.run(f'{Path(__file__).stem}:app', host="127.0.0.1", port=5555)
    pass

Browser access
http://127.0.0.1:5555/
http://127.0.0.1:5555/say/test
http://127.0.0.1:5555/items/?skip=0&limit=10
What is the query? In the URL A set of key-value pairs after the keyword, separated by the & character.
Query in url:
skip: the start parameter of the query
limit: the end parameter of the query

About the difference between @app.get() and @router.get()

Both are used to define handlers for HTTP GET requests, but there are some differences.

  • @app.get(): This method is a shorthand for defining routes directly on the FastAPI application instance.
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello, World!"}
  • @router.get(): This method is a way to define routes on the FastAPI router object (Router). You can create a standalone router object and use @router.get() to define routes for specific paths.
from fastapi import FastAPI, APIRouter

router = APIRouter()

@router.get("/items")
async def read_items():
    return {"message": "Read all items"}

The above example creates a router object called router and defines a GET request handler for the "/items" path using router.get(). It should be noted that if you use multiple router objects, you need to add them to the FastAPI application instance to take effect.

# Add the router object to the FastAPI application instance through the app.include_router() method, so that its defined routes will be processed correctly.

app.include_router(router)

Summary: app.get() is used to define routes on application instances, while router.get() is used to define routes on router objects. The difference between the two is where they are defined and how they are applied, but both can be used to handle HTTP GET requests.

Summarize

  • import  FastAPI.
  • Create an  app instance.
  • Write a path manipulation decorator (eg  @app.get("/").
  • Write a path manipulation function (as above  def root(): ...).
  • Run a development server (eg  uvicorn main:app --reload.

The request's "path parameters"

Path parameters must be reflected in the parameters.

A "request path" is also commonly referred to as an "endpoint" or "route".

FastAPI writes a simple application:

import os
import uvicorn
from pathlib import Path
from fastapi import FastAPI

# 类似于 app = Flask(__name__)
app = FastAPI()


# 绑定路由和视图函数
@app.get("/")
async def root():
    return {"message": "Hello World"}


# 在 Windows 中必须加上 if __name__ == "__main__",
# 否则会抛出 RuntimeError: This event loop is already running
if __name__ == '__main__':
    # 启动服务,因为我们这个文件叫做 main_test.py,所以需要启动 main_test.py 里面的 app
    # os.system('uvicorn main_test:app --reload')
    # uvicorn.run("main_test:app", host="0.0.0.0", port=8000)
    uvicorn.run(f'{Path(__file__).stem}:app', host="0.0.0.0", port=5555)
    pass

Entering "localhost:5555" in the browser will display the corresponding output. We see that a dictionary can be returned directly in the view function. Of course, in addition to dictionaries, other data types are also possible.

# -*- coding:utf-8 -*-
from fastapi import FastAPI
import uvicorn

app = FastAPI()


@app.get("/int")
async def index1():
    return 666


@app.get("/str")
async def index2():
    return "佛祖保佑,佛祖保佑"


@app.get("/bytes")
async def index3():
    return b"satori"


@app.get("/tuple")
async def index4():
    temp_tuple = ("佛祖保佑", "佛祖保佑")
    return temp_tuple


@app.get("/list")
async def index5():
    return [{"name": "擒贼先擒王", "age": 18}, {"name": "捉奸先捉双", "age": 16}]


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

Send requests directly using requests

import requests

print(requests.get("http://localhost:5555/int").text)
print(requests.get("http://localhost:5555/str").text)
print(requests.get("http://localhost:5555/bytes").text)
print(requests.get("http://localhost:5555/tuple").text)
print(requests.get("http://localhost:5555/list").text)

However, the tuple is automatically converted into a list and returned. Here we specify the path in the route. You can see that the path form in FastAPI is the same as other frameworks, but the current path is hard-coded. What if we want to dynamically declare path parameters?

path parameters

Path "parameters" or "variables" can be declared using the same syntax as Python format strings , FastAPI is sufficient to distinguish between path parameters and query parameters.

from fastapi import FastAPI

app = FastAPI()


@app.get("/items/{item_id}")
async def read_item(item_id):
    return {"item_id": item_id}

The value of the path parameter  item_id will be  item_id passed as an argument to your function. Run and visit http://127.0.0.1:5555/items/foo and you will see the response: {"item_id":"foo"}

 Example:

from fastapi import FastAPI

app = FastAPI()


@app.get("/items/{item_id}")
async def read_item(item_id: str, q: str = None, short: bool = False):
    item = {"item_id": item_id}
    if q:
        item.update({"q": q})
    if not short:
        item.update(
            {"description": "This is an amazing item that has a long description"}
        )
    return item

The value of the path parameter item_id will be passed to your function as the parameter item_id. When declaring other function parameters that are not path parameters, they will be automatically interpreted as "query string" parameters:

Look at its access path, execute any of the following url access methods
http://127.0.0.1:8000/items/Lao Wang sleeps next door?short=1
http://127.0.0.1:8000/items/Lao Wang sleeps Next door?short=True
http://127.0.0.1:8000/items/Lao Wang sleeps next door?short=true
http://127.0.0.1:8000/items/Lao Wang sleeps next door?short=on
http://127.0 .0.1:8000/items/Lao Wang sleeps next door?short=yes
You can find that any uppercase and lowercase letters will be converted into a bool parameter True, which is the so-called fuzzy verification parameter, which is good news for developers. Note: If the short parameter has no default value, it must be passed, otherwise FastAPI will report an error.

{
    "detail": [
        {
            "loc": [
                "query",
                "needy"
            ],
            "msg": "field required",
            "type": "value_error.missing"
        }
    ]
}

Example:

from fastapi import FastAPI

app = FastAPI()

fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


@app.get("/items/")
async def read_item(skip: int = 0, limit: int = 10):
    return fake_items_db[skip : skip + limit]

A query string is a collection of key-value pairs separated by & symbols after the ? in the URL.

Additional validation can be performed on queries using Query :

from typing import Optional

from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/items/")
async def read_items(q: Optional[str] = Query(None, max_length=50)):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

Query has the following field checks:

  • min_length  minimum length
  • max_length  maximum length
  • regex  regular match
  •  The first parameter of Query... is the default value, indicating that it is required

Path and Query are used in the same way, and the query field can also be verified.

And you can also declare numeric validation:

from fastapi import FastAPI, Path, Query

app = FastAPI()


@app.get("/items/{item_id}")
async def read_items(
        *,
        item_id: int = Path(..., title="The ID of the item to get", ge=0, le=1000),
        q: str,
        size: float = Query(..., gt=0, lt=10.5)
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results
  • gt:more than the
  • ge:greater or equal to
  • lt: less than
  • le: less than or equal to

There are also cookies similarly :

from typing import Optional

from fastapi import Cookie, FastAPI

app = FastAPI()


@app.get("/items/")
async def read_items(ads_id: Optional[str] = Cookie(None)):
    return {"ads_id": ads_id}

and Header :

from typing import Optional

from fastapi import FastAPI, Header

app = FastAPI()


@app.get("/items/")
async def read_items(user_agent: Optional[str] = Header(None)):
    return {"User-Agent": user_agent}

Request body + path + query parameters, add url dynamic path parameters and query parameters on the basis of the request body

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: str = None
    price: float
    tax: float = None


app = FastAPI()


@app.put("/items/{item_id}")
async def create_item(item_id: int, item: Item, q: str = None):
    result = {"item_id": item_id, **item.dict()}
    if q:
        result.update({"q": q})
    return result

About template engines

FastAPI does not have its own template engine (Jinja2) like Flask, that is to say, there is no default template engine. From another perspective, FastAPI has become more flexible and extremely comfortable in the choice of template engine.

Take the Jinja2 template as an example, install dependencies

pip install jinja2
pip install aiofiles # Asynchronous static files for fastapi

# -*- coding:utf-8 -*-
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
import uvicorn

app = FastAPI()

app.mount("/static", StaticFiles(directory="static"), name="static")  # 挂载静态文件,指定目录

templates = Jinja2Templates(directory="templates")  # 模板目录


@app.get("/data/{data}")
async def read_data(request: Request, data: str):
    return templates.TemplateResponse("index.html", {"request": request, "data": data})


if __name__ == '__main__':
    uvicorn.run(app, host="127.0.0.1", port=8000)

html file rendering

<html>
<head>
    <title>士可杀不可辱(you can kill me, but you can't fuck me)</title>
</head>
<body>
    <h1>高呼: { { data }}</h1>
</body>
</html>

Type http://127.0.0.1:8000/data/ in the browser

It is worth noting that when the TemplateRespone response is returned, the context object of the request must be brought, and the incoming parameters are placed in the same dictionary. In this way, Jinja2 can be used like Flask.

Set the tags tag for the path to group :

from typing import Optional, Set
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None
    tags: Set[str] = []


@app.post("/items/", response_model=Item, tags=["items"])
async def create_item(item: Item):
    return item


@app.get("/items/", tags=["items"])
async def read_items():
    return [{"name": "Foo", "price": 42}]


@app.get("/users/", tags=["users"])
async def read_users():
    return [{"username": "johndoe"}]

You can also set summary and description :

from typing import Optional, Set
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None
    tags: Set[str] = []


@app.post(
    "/items/",
    response_model=Item,
    summary="Create an item",
    description="Create an item with all the information, name, description, price, tax and a set of unique tags",
)
async def create_item(item: Item):
    return item

Multi-line comments:

from typing import Optional, Set
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None
    tags: Set[str] = []


@app.post("/items/", response_model=Item, summary="Create an item")
async def create_item(item: Item):

    """
    Create an item with all the information:
    - **name**: each item must have a name
    - **description**: a long description
    - **price**: required
    - **tax**: if the item doesn't have tax, you can omit this
    - **tags**: a set of unique tag strings for this item
    """
    return item

 Obsolete route :

from fastapi import FastAPI

app = FastAPI()


@app.get("/items/", tags=["items"])
async def read_items():
    return [{"name": "Foo", "price": 42}]


@app.get("/users/", tags=["users"])
async def read_users():
    return [{"username": "johndoe"}]


@app.get("/elements/", tags=["items"], deprecated=True)
async def read_elements():
    return [{"item_id": "Foo"}]

Typed path parameters

Types can be declared for path parameters in functions using standard Python type annotations.

from fastapi import FastAPI

app = FastAPI()


@app.get("/items/{item_id}")
async def read_item(item_id: int):
    return {"item_id": item_id}

In this example, item_id is declared as type int.

Run the example, and open a browser to visit http://127.0.0.1:5555/items/3,
you will get the following response: {"item_id":3}
Note that the value received (and returned) by the function is 3, which is a Python int value , instead of the string "3".
So, FastAPI provides automatic "parsing" of the request through the above type declaration.

# -*- coding:utf-8 -*-

from fastapi import FastAPI
import uvicorn

app = FastAPI()


@app.get("/apple/{item_id}")
async def get_item(item_id: int):
    """
        Flask 定义类型是在路由当中,也就是在 <> 里面,变量和类型通过 : 分隔
        FastAPI 是使用类型注解的方式,此时的 item_id 要求一个整型(准确的说是一个能够转成整型的字符串)
    """
    return {"item_id": item_id}

if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

order matters

Since path operations are run sequentially, you need to pay attention to the order when defining routes

from fastapi import FastAPI

app = FastAPI()


@app.get("/users/me")
async def read_user_me():
    return {"user_id": "the current user"}


@app.get("/users/{user_id}")
async def read_user(user_id: str):
    return {"user_id": user_id}

Because the path operation is performed in order, it must be  /users/me in  /users/{user_id} front of the path here, otherwise it will only be matched.  /users/{user_id}If you access it at this time  /users/me, a parsing error will be returned, because the string "me" cannot be parsed into an integer.

Default value ( enumeration )

A certain path parameter can be declared as a specified type through type annotations (accurately speaking, it can be converted into a specified type, because the default is a string), but if we want it to be only a few values ​​​​that we stipulate What should one do?

At this point you can use standard Python  Enum types. Import  Enum and create a   subclass that inherits from str and  . EnumBy  str inheriting from , the API documentation will be able to know that these values ​​must be  string typed and be able to display them correctly. Then create class properties with fixed values ​​that will be valid values ​​available:

from enum import Enum

from fastapi import FastAPI


class ModelName(str, Enum):
    alexnet = "alexnet"
    resnet = "resnet"
    lenet = "lenet"


app = FastAPI()


@app.get("/models/{model_name}")
async def get_model(model_name: ModelName):
    if model_name is ModelName.alexnet:
        return {"model_name": model_name, "message": "Deep Learning FTW!"}

    if model_name.value == "lenet":
        return {"model_name": model_name, "message": "LeCNN all the images"}

    return {"model_name": model_name, "message": "Have some residuals"}

path contains /

Suppose there is such a route: /files/{file_path}, and the file_path passed by the user can obviously contain /, assuming the file_path is /root/test.py, then the route becomes /files//root/test. py, apparently this is problematic.

OpenAPI does not support any way of declaring path parameters to include paths inside of them , as this could lead to situations that are difficult to test and define. However, it is still possible to implement it in  FastAPI through an internal tool of Starlette  .

A path parameter containing a path can be declared using options directly from Starlette :/files/{file_path:path}

In this case, the name of the parameter is followed by  file_path:path statement that the parameter should match any path .

Therefore, you can use it like this:

from fastapi import FastAPI

app = FastAPI()


@app.get("/files/{file_path:path}")
async def read_file(file_path: str):
    return {"file_path": file_path}

You may need parameter inclusions  /home/johndoe/myfile.txt, /starting with a slash ( ).

In this case, the URL will be  /files//home/johndoe/myfile.txt,  with a double slash ( ) between files the and  .home//

from fastapi import FastAPI
import uvicorn

app = FastAPI()


# 声明 file_path 的类型为 path,这样它会被当成一个整体
@app.get("/files/{file_path:path}")
async def get_file(file_path: str):
    return {"file_path": file_path}


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

set  127.0.0.1:8000/docs

As for the "127.0.0.1:8000/docs" page itself, it can also be set:

from fastapi import FastAPI
import uvicorn

app = FastAPI(
    title="测试文档",
    description="这是一个简单的 demo",
    docs_url="/my_docs",
    openapi_url="/my_openapi"
)


@app.get("/apple/{item_id}")
async def get_item(item_id: int):
    return {"item_id": item_id}


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

Path parameters, data validation

The query parameter data verification uses Query, and the path parameter data verification uses Path. The usage of the two is exactly the same, and there is no difference.

from fastapi import FastAPI, Path
import uvicorn

app = FastAPI()

@app.get("/items/{item-id}")
async def read_items(item_id: int = Path(..., alias="item-id")):
    return {"item_id": item_id}

if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

Because the path parameter is required, it is part of the path, so we should use ... to mark it as a required parameter. Of course, it doesn’t matter if you don’t do this, because you can’t use the default value if you specify it, because if you don’t specify the path parameter, you won’t be able to match the corresponding route at all. As for some other checks, they are exactly the same as query parameters, so I won’t repeat them here.

However, as we said before, the path parameter should be in front of the query parameter. Although FastAPI does not have this requirement, it is obviously more comfortable to write this way. But here comes the problem. If the path parameter needs to specify an alias, but a certain query parameter does not, a problem will arise at this time:

@app.get("/items/{item-id}")
async def read_items(q: str,
                     item_id: int = Path(..., alias="item-id")):

    return {"item_id": item_id, "q": q}

Obviously, the syntax of Python at this time determines that item_id must be placed after q. Of course, there is no problem in doing so. FastAPI does not have any requirements for the order of parameters, because it is declared through the name, type and default value of the parameters. to detect parameters, regardless of the order of the parameters. But at this time, what should we do if we want item_id to be in front of q?

@app.get("/items/{item-id}")
async def read_items(*, item_id: int = Path(..., alias="item-id"),
                     q: str):
    
    return {"item_id": item_id, "q": q}

There is no problem at this time. By setting the first parameter to *, both item_id and q must be passed through keywords, so at this time, default parameters are allowed before non-default parameters. Of course, we don't need to worry about passing parameters in FastAPI, you can think that all its parameters are passed through keyword parameters.

The "query parameter" of the request

  • Query parameters can be declared through type annotations in FastAPI, and query parameters can also be omitted, then all information related to the request will enter the Request object
  • If parameters that are not path parameters are defined in the function, they will be automatically interpreted as query parameters.

Example:

from fastapi import FastAPI

app = FastAPI()

fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


@app.get("/items/")
async def read_item(skip: int = 0, limit: int = 10):
    return fake_items_db[skip : skip + limit]

A query string is a collection of key-value pairs that follow the URL   , separated by  & symbols.

For example, in the following url: http://127.0.0.1:5555/items/?skip=0&limit=10

The query parameters are:

  • skip: The corresponding value is 0
  • limit: The corresponding value is 10

Example:

from fastapi import FastAPI
import uvicorn

app = FastAPI()


@app.get("/user/{user_id}")
async def get_user(user_id: str, name: str, age: int):
    """
        函数中参数定义了 user_id、name、age 三个参数
        显然 user_id 和 路径参数中的 user_id 对应,
        然后 name 和 age 会被解释成查询参数
        这三个参数的顺序没有要求,但是一般都是路径参数在前,查询参数在后
    """
    return {"user_id": user_id, "name": name, "age": age}


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

optional parameter, required parameter

  • When the non-path parameter default value is set to None, the parameter is optional
  • When the non-path parameter has no default value, the parameter must be passed

None Declare optional query parameters by setting their default values ​​to  :

from typing import Union

from fastapi import FastAPI

app = FastAPI()


@app.get("/items/{item_id}")
async def read_item(item_id: str, q: Union[str, None] = None):
    if q:
        return {"item_id": item_id, "q": q}
    return {"item_id": item_id}

In this example, the function parameter q defaults to None, so q is optional. FastAPI can tell that the parameter item_id is a path parameter and q is not, so q is a query parameter.

Multiple Type Parameters ( Union )

Specify multiple types. For example, user_id is parsed as an integer, and if the parsing fails, it degenerates into a string.

from typing import Union, Optional
from fastapi import FastAPI
import uvicorn

app = FastAPI()


@app.get("/user/{user_id}")
async def get_user(user_id: Union[int, str], name: Optional[str] = None):
    """
        通过 Union 来声明一个混合类型,int 在前、str 在后。会先按照 int 解析,解析失败再变成 str
        然后是 name,它表示字符串类型、但默认值为 None(不是字符串),那么应该声明为 Optional[str]
    """
    return {"user_id": user_id, "name": name}


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

Therefore, the design of FastAPI is still very good. It can be said that it is very clever to realize the limitation of parameter types through Python type annotations, so this also requires us to master Python type annotations proficiently.

bool type is automatically converted

For Boolean types, FastAPI supports automatic conversion

from fastapi import FastAPI
import uvicorn

app = FastAPI()


@app.get("/{flag}")
async def get_flag(flag: bool):
    return {"flag": flag}


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

test

print(requests.get("http://localhost:5555/1").json())
print(requests.get("http://localhost:5555/True").json())
print(requests.get("http://localhost:5555/true").json())
print(requests.get("http://localhost:5555/False").json())
print(requests.get("http://localhost:5555/false").json())
print(requests.get("http://localhost:5555/on").json())
print(requests.get("http://localhost:5555/yes").json())
print(requests.get("http://localhost:5555/off").json())
print(requests.get("http://localhost:5555/no").json())

Multiple paths, query parameters

Multiple path parameters and query parameters can be declared at the same time, and FastAPI recognizes them. And you don't need to declare them in any particular order. They will be detected by name:

from typing import Union

from fastapi import FastAPI

app = FastAPI()


@app.get("/users/{user_id}/items/{item_id}")
async def read_user_item(
    user_id: int, item_id: str, q: Union[str, None] = None, short: bool = False
):
    item = {"item_id": item_id, "owner_id": user_id}
    if q:
        item.update({"q": q})
    if not short:
        item.update(
            {"description": "This is an amazing item that has a long description"}
        )
    return item

FastAPI can define any number of path parameters, as long as the dynamic path parameters appear in the parameters of the function. Of course, there can be any number of query parameters, and FastAPI can handle them very well.

from typing import Optional
from fastapi import FastAPI
import uvicorn

app = FastAPI()


@app.get("/postgres/{schema}/v1/{table}")
async def get_data(
        schema: str,
        table: str,
        select: str = "*",
        where: Optional[str] = None,
        limit: Optional[int] = None,
        offset: Optional[int] = None):
    """
    标准格式是:路径参数按照顺序在前,查询参数在后
    但其实对顺序是没有什么要求的
    """
    query = f"select {select} from {schema}.{table}"
    if where:
        query += f" where {where}"
    if limit:
        query += f" limit {limit}"
    if offset:
        query += f" offset {offset}"
    return {"query": query}


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

http://localhost:5555/postgres/ods/v1/staff
http://localhost:5555/postgres/ods/v1/staff?select=id, name&where=id > 3&limit=100

Depends (Dependency Injection)

from typing import Optional
import uvicorn
from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(q: Optional[str] = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    # common_parameters 接收三个参数:q、skip、limit
    # 然后在解析请求的时候,会将 q、skip、limit 传递到 common_parameters 中,然后将返回值赋值给 commons
    # 但如果解析不到某个参数时,那么会判断函数中参数是否有默认值,没有的话就会返回错误,而不是传递一个 None 进去
    return commons


@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
    return commons


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

test

requests.get("http://localhost:5555/items").json()
{q': None,'skip':@,limit': 100}
requests.get("http://localhost:5555/items?g=id,name").json()
{g':id, name',skip': 0,limit': 100}

So Depends can implement dependency injection very well, and we specially wrote two routes to show that they are independent of each other. Therefore, it will be very practical when there is shared logic, or shared database connection, enhanced security, authentication, role permissions, etc.

FastAPI provides an easy-to-use, yet powerful dependency injection system that allows developers to easily integrate components into FastAPI .

What is "Dependency Injection"?

Dependency Injection is a design pattern that removes dependencies between classes. Putting dependent classes into containers and parsing out instances of these classes is dependency injection. The purpose is to achieve decoupling of classes.

Example:

from typing import Optional
from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(q: Optional[str] = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    return commons


@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
    return commons

The dependencies in this example expect to receive the following parameters:

  • str An optional query parameter  of type q
  • int An optional query parameter  of type  skip, with a default value of 0
  • int An optional query parameter  of type  limit, with a default value of 100

The dependency function then returns a  dict.

Use Class as dependency:

from typing import Optional
from fastapi import Depends, FastAPI

app = FastAPI()

fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: Optional[str] = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip: commons.skip + commons.limit]
    response.update({"items": items})
    return response

Use nested sub-dependencies:

from typing import Optional
from fastapi import Cookie, Depends, FastAPI

app = FastAPI()


def query_extractor(q: Optional[str] = None):
    return q


def query_or_cookie_extractor(
        q: str = Depends(query_extractor), last_query: Optional[str] = Cookie(None)
):
    if not q:
        return last_query
    return q


@app.get("/items/")
async def read_query(query_or_default: str = Depends(query_or_cookie_extractor)):
    return {"q_or_cookie": query_or_default}

Use dependencies in the path:

from fastapi import Depends, FastAPI, Header, HTTPException

app = FastAPI()


async def verify_token(x_token: str = Header(...)):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token header invalid")


async def verify_key(x_key: str = Header(...)):
    if x_key != "fake-super-secret-key":
        raise HTTPException(status_code=400, detail="X-Key header invalid")
    return x_key


@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)])
async def read_items():
    return [{"item": "Foo"}, {"item": "Bar"}]

A global dependency that can be applied for all path operations :

from fastapi import Depends, FastAPI, Header, HTTPException


async def verify_token(x_token: str = Header(...)):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token header invalid")


async def verify_key(x_key: str = Header(...)):
    if x_key != "fake-super-secret-key":
        raise HTTPException(status_code=400, detail="X-Key header invalid")
    return x_key


app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)])


@app.get("/items/")
async def read_items():
    return [{"item": "Portal Gun"}, {"item": "Plumbus"}]


@app.get("/users/")
async def read_users():
    return [{"username": "Rick"}, {"username": "Morty"}]

Query parameters, data validation

FastAPI supports us to perform more intelligent data verification, such as a string, we hope that the user can only pass a string with a length of 6 to 15 when passing it, what should we do?

from typing import Optional
from fastapi import FastAPI, Query
import uvicorn

app = FastAPI()


@app.get("/user")
async def check_length(
        # 默认值为 None,应该声明为 Optional[str],当然声明 str 也是可以的。只不过声明为 str,那么默认值应该也是 str
        # 所以如果一个类型允许为空,那么更规范的做法应该是声明为 Optional[类型]。
        password: Optional[str] = Query(None, min_length=6, max_length=15)
):
    return {"password": password}


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

password is optional, but if passed it must be a string, and again a string of length 6 to 15. So if None is passed, then None and Query(None) are equivalent when declaring the default value, but Query also supports other parameters to limit the parameters.

requests.get("http://localhost:5555/user?password=12345").json()
requests.get("http://localhost:5555/user?password=123456").json()

In addition to limiting the minimum length and maximum length in Query, there are other functions

Query parameters and string validation

from fastapi import FastAPI

app = FastAPI()


@app.get("/items/")
async def read_items(q: str | None = None):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

The query parameter  q is of type  str, which defaults to  None, so it's optional.

additional checksum

Add constraints: Even though  q it is optional, as long as this parameter is provided, the parameter value cannot exceed 50 characters in length .

Import Query from fastapi:

from typing import Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Union[str, None] = Query(default=None, max_length=50)):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

Add more checks

You can also add  min_length parameters:

from typing import Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(
    q: Union[str, None] = Query(default=None, min_length=3, max_length=50)
):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

add regular expression

It is possible to define a regular expression that the parameter value must match:

from typing import Union

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(
    q: Union[str, None] = Query(
        default=None, min_length=3, max_length=50, pattern="^fixedquery$"
    )
):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

declared as a required parameter

... is a special object in Python, through which it can be realized that the parameter is a required parameter.

from fastapi import FastAPI, Query
import uvicorn

app = FastAPI()


@app.get("/user")
async def check_length(password: str = Query(..., min_length=6)):
    """将第一个参数换成 ... 即可实现该参数是必传参数
    """
    return {"password": password}


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

The query parameter becomes a list

If we specify  a=1&a=2, how can we get a list when we get a?

from typing import Optional, List
from fastapi import FastAPI, Query
import uvicorn

app = FastAPI()


@app.get("/items")
async def read_items(
        a1: str = Query(...),
        a2: List[str] = Query(...),
        b: List[str] = Query(...)
):
    return {"a1": a1, "a2": a2, "b": b}


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

First "a2" and "b" are corresponding lists, then "a1" only gets the last value. In addition, some people may think that we are a bit long-winded, can we write like this in the function declaration?

@app.get("/items")
async def read_items(
        a1: str,
        a2: List[str],
        b: List[str]
):
    return {"a1": a1, "a2": a2, "b": b}

It is OK for a1, but not for a2 and b. For query parameters of type list, no matter whether there is a default value or not, you must explicitly add Query to indicate the required parameters. If it is allowed to be None (or has a default value), then it should be written like this:

from typing import Optional, List
from fastapi import FastAPI, Query
import uvicorn

app = FastAPI()


@app.get("/items")
async def read_items(
        a1: str,
        a2: Optional[List[str]] = Query(None),
        b: List[str] = Query(["1", "嘿嘿"])
):
    return {"a1": a1, "a2": a2, "b": b}


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

alias the parameter

Assuming that the query parameter we defined is named item-query, since it needs to be reflected in the function parameter, which obviously does not conform to the naming convention of Python variables, what should we do at this time? The answer is an alias.

from typing import Optional, List
from fastapi import FastAPI, Query
import uvicorn

app = FastAPI()


@app.get("/items")
async def read_items(
        # 通过 url 的时候使用别名即可
        item1: Optional[str] = Query(None, alias="item-query"),
        item2: str = Query("哈哈", alias="@@@@"),
        item3: str = Query(..., alias="$$$$")  # item3 是必传的
):
    return {"item1": item1, "item2": item2, "item3": item3}


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

Numerical detection

Query not only supports the verification of strings, but also supports the verification of values. It can pass parameters such as gt, ge, lt, and le. I believe that you know what these parameters do without saying. Let us give an example illustrate:

from fastapi import FastAPI, Query
import uvicorn

app = FastAPI()


@app.get("/items")
async def read_items(
        # item1 必须大于 5
        item1: int = Query(..., gt=5),
        # item2 必须小于等于 7
        item2: int = Query(..., le=7),
        # item3 必须必须等于 10
        item3: int = Query(..., ge=10, le=10)
):
    return {"item1": item1, "item2": item2, "item3": item3}


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

test

Request object

What is Request? First of all, we know that any request corresponds to a Request object, and all the requested information is in this Request object, and FastAPI is no exception.

The path parameter must be reflected in the parameter, but the query parameter can not be written because we defined request: Request, then all the information related to the request will enter the Request object

from fastapi import FastAPI, Request
import uvicorn

app = FastAPI()


@app.get("/girl/{user_id}")
async def read_girl(user_id: str,
                    request: Request):
    """路径参数是必须要体现在参数中,但是查询参数可以不写了
       因为我们定义了 request: Request,那么请求相关的所有信息都会进入到这个 Request 对象中"""
    header = request.headers  # 请求头
    method = request.method  # 请求方法
    cookies = request.cookies  # cookies
    query_params = request.query_params  # 查询参数
    return {"name": query_params.get("name"), "age": query_params.get("age"), "hobby": query_params.getlist("hobby")}


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

All request-related information can be obtained through the Request object. When we passed incorrect parameters before, FastAPI would automatically return an error message for us, but through the Request we can analyze it ourselves and specify the returned error message.

Response object

Since there is a Request, there must be a Response. Although we can return a dictionary directly, FastAPI will actually convert it into a Response object for us.

Response internally receives the following parameters:

  • content:返回的数据
  • status_code:状态码
  • headers:返回的请求头
  • media_type:响应类型(就是 HTML 中 Content-Type,只不过这里换了个名字)
  • background:接收一个任务,Response 在返回之后会自动异步执行

example

from fastapi import FastAPI, Request, Response
import uvicorn
import orjson

app = FastAPI()


@app.get("/girl/{user_id}")
async def read_girl(user_id: str, request: Request):
    query_params = request.query_params  # 查询参数
    data = {
        "name": query_params.get("name"), 
        "age": query_params.get("age"), 
        "hobby": query_params.getlist("hobby")
    }
    # 实例化一个 Response 对象
    response = Response(
        # content,我们需要手动转成 json 字符串,如果直接返回字典的话,那么在包装成 Response 对象的时候会自动帮你转
        orjson.dumps(data),
        # status_code,状态码
        201,
        # headers,响应头
        {"Token": "xxx"},
        # media_type,就是 HTML 中的 Content-Type
        "application/json",
    )
    # 如果想设置 cookie 的话,那么通过 response.set_cookie 即可
    # 删除 cookie 则是 response.delete_cookie
    return response


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

Through Response, we can customize request headers, status codes, cookies, etc.

In addition to Response, there are many other types of responses, they are all in fastapi.responses, such as: FileResponse, HTMLResponse, PlainTextResponse and so on. They all inherit Response, but they will automatically set the response type for you, for example:

from fastapi import FastAPI
from fastapi.responses import Response, HTMLResponse
import uvicorn

app = FastAPI()


@app.get("/index")
async def index():
    response1 = HTMLResponse("<h1>你好呀</h1>")
    response2 = Response("<h1>你好呀</h1>", media_type="text/html")
    # 以上两者是等价的,在 HTMLResponse 中会自动将 media_type 设置成 text/html
    return response1


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

 Use  response_modelparameters to declare the model to use for the response:

from typing import List, Optional
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None
    tags: List[str] = []


@app.post("/items/", response_model=Item)
async def create_item(item: Item):
    return item
  • response_model_exclude_unset=True: Those default values ​​will not be included in the response, only the values ​​actually set
  • response_model_include What attributes are included
  • response_model_exclude omit certain attributes

status_codeparameter to declare the HTTP status code to use for the response:

from fastapi import FastAPI

app = FastAPI()


@app.post("/items/", status_code=201)
async def create_item(name: str):
    return {"name": name}

For form fields, use Form:

from fastapi import FastAPI, Form

app = FastAPI()


@app.post("/login/")
async def login(username: str = Form(...), password: str = Form(...)):
    return {"username": username}

FileThe upload file used to define the client (receive the upload file, to be pre-installed python-multipart):

from fastapi import FastAPI, File, UploadFile

app = FastAPI()


@app.post("/files/")
async def create_file(file: bytes = File(...)):
    return {"file_size": len(file)}


@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile = File(...)):
    return {"filename": file.filename}

Returns an HTTP error response to the client, ready to use HTTPException.

from fastapi import FastAPI, HTTPException

app = FastAPI()

items = {"foo": "The Foo Wrestlers"}


@app.get("/items/{item_id}")
async def read_item(item_id: str):
    if item_id not in items:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"item": items[item_id]}

Use response_descriptionthe settings response description:

from typing import Optional, Set

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None
    tags: Set[str] = []


@app.post(
    "/items/",
    response_model=Item,
    summary="Create an item",
    response_description="The created item",
)
async def create_item(item: Item):
    """
    Create an item with all the information:
    - **name**: each item must have a name
    - **description**: a long description
    - **price**: required
    - **tax**: if the item doesn't have tax, you can omit this
    - **tags**: a set of unique tag strings for this item
    """
    return item

request body

When data needs to be sent from a client (such as a browser) to an API, it is sent as a "request body".

The request body is the data sent by the client to the API.

The response body is the data that the API sends to the client.

To send data, you must use one of the following methods: POST(more common), PUT, DELETE or  PATCH.

Obviously, corresponding to POST, PUT and other types of requests, we must be able to parse out the request body and construct a response body.

Model

In FastAPI, both the request body and the response body correspond to a Model

from typing import Optional, List
from fastapi import FastAPI, Request, Response
from pydantic import BaseModel
import uvicorn

app = FastAPI()


class Girl(BaseModel):
    """数据验证是通过 pydantic 实现的,我们需要从中导入 BaseModel,然后继承它"""
    name: str
    age: Optional[str] = None
    length: float
    hobby: List[str]  # 对于 Model 中的 List[str] 我们不需要指定 Query(准确的说是 Field)


@app.post("/girl")
async def read_girl(girl: Girl):
    # girl 就是我们接收的请求体,它需要通过 json 来传递,并且这个 json 要有上面的四个字段(age 可以没有)
    # 通过 girl.xxx 的方式我们可以获取和修改内部的所有属性
    return dict(girl)  # 直接返回 Model 对象也是可以的


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

test

Example:

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None


app = FastAPI()


@app.post("/items/")
async def create_item(item: Item):
    item_dict = item.dict()
    if item.tax:
        price_with_tax = item.price + item.tax
        item_dict.update({"price_with_tax": price_with_tax})
    return item_dict

和声明查询参数时一样,当一个模型属性具有默认值时,它不是必需的。否则它是一个必需属性。将默认值设为 None 可使其成为可选属性。

使用 Request 对象 传递 body

如果你不想使用 Pydantic 模型,你还可以使用 Body 参数。请参阅文档 请求体 - 多个参数:请求体中的单一值

from fastapi import FastAPI, Request
import uvicorn

app = FastAPI()


@app.post("/girl")
async def read_girl(request: Request):
    # 是一个协程,所以需要 await
    data = await request.body()
    print(data)


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

使用 requests 模块发送 post 请求的时候可以通过 data 参数传递、也可以通过 json 参数。

  • 当通过 json={"name": "satori", "age": 16, "length": 155.5} 传递的时候,会将其转成 json 字符串进行传输,程序中的 print 打印如下:b'{"name": "satori", "age": 16, "length": 155.5}'
  • If the data parameter is used to send the request (the value remains unchanged), it will be spliced ​​into the form of k1=v1&k2=v2 and then transmitted (equivalent to form submission, which will be described later), and the program will print as follows: b'name=satori&age= 16&length=155.5'

So we see that what await request.body() gets is the most primitive byte stream, and besides await request.body(), there is also an await request.json(), but the latter calls the former internally to get After reaching the byte stream, it will automatically help you loads into a dictionary. Therefore, the use of await request.json() also requires that we must use the json parameter to pass when sending the request (the json converted from the dictionary is passed, so it can also be parsed into a dictionary), otherwise using await request.json() is could not be parsed correctly.

Path parameters, query parameters, request body

  • If  this parameter is also declared in the path  , it will be used as a path parameter.
  • If the parameter is of  a single type (eg  int, , float, str, bool etc.) it will be interpreted as  a query  parameter.
  • If the parameter's type is declared as a  Pydantic model , it will be interpreted as  the request body .
# -*- coding:utf-8 -*-
# @Author: komeiji satori
from typing import Optional, List
from fastapi import FastAPI, Request, Response
from pydantic import BaseModel
import uvicorn

app = FastAPI()


class Girl(BaseModel):
    name: str
    age: Optional[str] = None
    length: float
    hobby: List[str]


@app.post("/girl/{user_id}")
async def read_girl(user_id, q: str, girl: Girl):
    return {"user_id": user_id, "q": q, **dict(girl)}


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

test

After specifying the path parameters, query parameters and request body, FastAPI can still distinguish correctly, of course we can also use the Request object.

from typing import Optional, List, Dict
from fastapi import FastAPI, Request, Response
from pydantic import BaseModel
import uvicorn

app = FastAPI()


@app.post("/girl/{user_id}")
async def read_girl(user_id, request: Request):
    q = request.query_params.get("q")
    data: Dict = await request.json()
    data.update({"user_id": user_id, "q": q})
    return data


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

Embed a single request body parameter

Suppose you have only one  Item request body parameter  from a Pydantic model item. By default, FastAPI  will directly expect such a request body. However, if you want it to expect a  item JSON with keys and model content in the values, as it does when declaring additional body parameters, you can use a special  Body parameter  embed:item: Item = Body(embed=True)

from typing import Annotated

from fastapi import Body, FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]):
    results = {"item_id": item_id, "item": item}
    return results

In this case FastAPI  will expect a request body like this:

{
    "item": {
        "name": "Foo",
        "description": "The pretender",
        "price": 42.0,
        "tax": 3.2
    }
}

instead of:

{
    "name": "Foo",
    "description": "The pretender",
    "price": 42.0,
    "tax": 3.2
}

Multiple request body parameters can be added to a route operation function , even though a request can only have one request body.

It is also possible to declare a single value that will be received as part of the request body.

It is also possible to instruct FastAPI to embed the original request body in a key when only one request body parameter is declared.

Multiple request body parameters

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from typing import Optional, List
from fastapi import FastAPI, Request, Response
from pydantic import BaseModel
import uvicorn

app = FastAPI()


class Girl(BaseModel):
    name: str
    age: Optional[str] = None


class Boy(BaseModel):
    name: str
    age: int


@app.post("/boy_and_girl")
async def read_boy_and_girl(girl: Girl, boy: Boy):
    return {"girl": dict(girl), "boy": dict(boy)}


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

At this time, when passing, it should be passed as follows:

The two json should be nested together to form a larger json, and the key is our function parameter name. So this method is actually equivalent to:

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from typing import Optional, List, Dict
from fastapi import FastAPI, Request, Response
from pydantic import BaseModel
import uvicorn

app = FastAPI()


class BoyAndGirl(BaseModel):
    girl: Dict
    boy: Dict


@app.post("/boy_and_girl")
async def read_boy_and_girl(boy_and_girl: BoyAndGirl):
    return dict(boy_and_girl)


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

This method is also achievable, but there are no restrictions on the dictionaries inside the dictionary. Of course, we can still use the Request object to judge by ourselves after getting the dictionary, because for json, the internal fields may change, and the most important thing is that there may be many fields. At this time, I personally prefer to use the Request object.

Mixed use  Path, Query and request body parameters

Feel free to mix use  Path, Query and request body parameter declaration, FastAPI will know how to deal with it.

from typing import Annotated

from fastapi import FastAPI, Path
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None


@app.put("/items/{item_id}")
async def update_item(
    item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)],
    q: str | None = None,
    item: Item | None = None,
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    if item:
        results.update({"item": item})
    return results

item Note that what will be obtained from the request body is optional in this case  . because its default value is  None.

Multiple request body parameters

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None


class User(BaseModel):
    username: str
    full_name: str | None = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item, user: User):
    results = {"item_id": item_id, "item": item, "user": user}
    return results

request body

{
    "item": {
        "name": "Foo",
        "description": "The pretender",
        "price": 42.0,
        "tax": 3.2
    },
    "user": {
        "username": "dave",
        "full_name": "Dave Grohl"
    }
}

A single value in the request body

from typing import Annotated

from fastapi import Body, FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None


class User(BaseModel):
    username: str
    full_name: str | None = None


@app.put("/items/{item_id}")
async def update_item(
    item_id: int, item: Item, user: User, importance: Annotated[int, Body()]
):
    results = {"item_id": item_id, "item": item, "user": user, "importance": importance}
    return results

Request body:

{
    "item": {
        "name": "Foo",
        "description": "The pretender",
        "price": 42.0,
        "tax": 3.2
    },
    "user": {
        "username": "dave",
        "full_name": "Dave Grohl"
    },
    "importance": 5
}

Multiple request body parameters and query parameters

By default single values ​​are interpreted as query parameters, so you don't have to explicitly add Query

from typing import Annotated

from fastapi import Body, FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None


class User(BaseModel):
    username: str
    full_name: str | None = None


@app.put("/items/{item_id}")
async def update_item(
    *,
    item_id: int,
    item: Item,
    user: User,
    importance: Annotated[int, Body(gt=0)],
    q: str | None = None,
):
    results = {"item_id": item_id, "item": item, "user": user, "importance": importance}
    if q:
        results.update({"q": q})
    return results

Form form

Calling requests.post, if the parameter is passed through data, it is equivalent to submitting a form form, so in FastAPI, it can be obtained through await request.form(). Note: internally, await request.body() is also called first.

from fastapi import FastAPI, Request, Response
import uvicorn

app = FastAPI()


@app.post("/girl")
async def girl(request: Request):
    # 此时 await request.json() 报错,因为是通过 data 参数传递的,相当于 form 表单提交
    # 如果是通过 json 参数传递,那么 await request.form() 会得到一个空表单
    form = await request.form()
    return [form.get("name"), form.getlist("age")]


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

test

You can also use other methods:

# -*- coding:utf-8 -*-
# @Author: komeiji satori
from fastapi import FastAPI, Form
import uvicorn

app = FastAPI()


@app.post("/user")
async def get_user(username: str = Form(...),
                   password: str = Form(...)):
    return {"username": username, "password": password}


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

test

Form forms, query parameters, path parameters, etc., can be used together with the Request object. Like the above example, if we define one more request: Request, then we can still get the relevant form through await request.form() information. So if you think a certain parameter is not suitable for type annotation, then you can analyze it through the Request object alone.

File Upload

How does FastAPI receive user file uploads? First of all, if you want to use the file upload function, you must install a package python-multipart, just pip install python-multipart directly.

from fastapi import FastAPI, File, UploadFile
import uvicorn

app = FastAPI()


@app.post("/file1")
async def file1(file: bytes = File(...)):
    return f"文件长度: {len(file)}"


@app.post("/file2")
async def file1(file: UploadFile = File(...)):
    return f"文件名: {file.filename}, 文件大小: {len(await file.read())}"


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

test

We see that one takes a byte stream directly, and the other takes an object similar to a file handle. What should I do if multiple files are uploaded?

from typing import List
from fastapi import FastAPI, UploadFile, File
import uvicorn

app = FastAPI()


@app.post("/file")
async def file(files: List[UploadFile] = File(...)):
    """指定类型为列表即可"""
    for idx, f in enumerate(files):
        files[idx] = f"文件名: {f.filename}, 文件大小: {len(await f.read())}"
    return files


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

test

At this point, the FastAPI file upload is implemented. Of course, the file upload does not affect our processing of the form. You can try to process the file and the form at the same time.

return static resource

You need to install aiofiles, just install it directly with pip.

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
import uvicorn

app = FastAPI()

# name 参数只是起一个名字,FastAPI 内部使用
app.mount("/static", StaticFiles(directory=r"C:\Users\satori\Desktop\bg"), name="static")

if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

Browser input: localhost:5555/static/1.png, then the 1.png file under C:\Users\satori\Desktop\bg will be returned.

 sub application

If you have 2 independent FastAPI applications, you can set one as the main application and the other as the sub-application:

from fastapi import FastAPI

app = FastAPI()


@app.get("/app")
def read_main():
    return {"message": "Hello World from main app"}


subapi = FastAPI()


@subapi.get("/sub")
def read_sub():
    return {"message": "Hello World from sub API"}


app.mount("/subapi", subapi)

acting

Can be used root_pathto set the proxy.

Use the command line: uvicorn main:app --root-path /api/v1 

Or set it in code:

from fastapi import FastAPI, Request

app = FastAPI(root_path="/api/v1")


@app.get("/app")
def read_main(request: Request):
    return {"message": "Hello World", "root_path": request.scope.get("root_path")}

use template

You can use any template in FastAPI , a common choice is Jinja2. Installation: pip install jinja2

use:

from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles

from fastapi.templating import Jinja2Templates

app = FastAPI()

app.mount("/static", StaticFiles(directory="static"), name="static")

templates = Jinja2Templates(directory="templates")


@app.get("/items/{id}", response_class=HTMLResponse)
async def read_item(request: Request, id: str):
    return templates.TemplateResponse("item.html", {"request": request, "id": id})

Template file templates/item.html:

<html>
<head>
    <title>Item Details</title>
    <link href="{ { url_for('static', path='/styles.css') }}" rel="stylesheet">
</head>
<body>

    <h1>Item ID: { { id }}</h1>

</body>
</html>

error handling

from fastapi import FastAPI, HTTPException
import uvicorn

app = FastAPI()


@app.get("/items/{item_id}")
async def read_item(item_id: str):
    if item_id != "foo":
        # 里面还可以传入 headers 设置响应头
        raise HTTPException(status_code=404, detail="item 没有发现")
    return {"item": "bar"}


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

test

HTTPException is an ordinary Python exception class (inherited from Exception), which carries the relevant information of the API. Since it is an exception, we cannot return, but raise. This method returns an error because it can carry too little information.

custom exception

FastAPI provides an HTTPException internally, but we can also customize it, but note: after we customize the exception, we need to define a handler, bind the exception and the handler together, and then trigger the corresponding exception when the exception is thrown handler.

from fastapi import FastAPI, Request
from fastapi.responses import ORJSONResponse
import uvicorn

app = FastAPI()


class ASCIIException(Exception):
    """"""
    pass


# 通过装饰器的方式,将 ASCIIException 和 ascii_exception_handler 绑定在一起
@app.exception_handler(ASCIIException)
async def ascii_exception_handler(request: Request, exc: ASCIIException):
    """当引发 ASCIIException 的时候,会触发 ascii_exception_handler 的执行
       同时会将 request 和 exception 传过去"""
    return ORJSONResponse(status_code=404, content={"code": 404, "message": "你必须传递 ascii 字符串"})


@app.get("/items/{item_id}")
async def read_item(item_id: str):
    if not item_id.isascii():
        raise ASCIIException
    return {"item": f"get {item_id}"}


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

test

Regarding Request and Response, we can import them not only through fastapi, but also through starlette, because the route mapping of fastapi is realized through starlette.

Custom 404

When accessing a non-existent URL, we should prompt the user, for example: You are going to Mars to find the page.

from fastapi import FastAPI
from fastapi.responses import ORJSONResponse
from fastapi.exceptions import StarletteHTTPException
import uvicorn

app = FastAPI()


@app.exception_handler(StarletteHTTPException)
async def not_found(request, exc):
    return ORJSONResponse({"code": 404, "message": "您要找的页面去火星了。。。"})


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

At this point, when we access a URL that does not exist, our custom JSON string will be returned.

Background Tasks

Background tasks are tasks that run immediately after the response is returned.

If a request takes a long time, we can execute it in the background, and FastAPI has already done this for us. Let's take a look:

import time
from fastapi import FastAPI, BackgroundTasks
from starlette.background import BackgroundTask
from fastapi import Response, Request
import uvicorn
import orjson


app = FastAPI()


def send_email(email: str, message: str = ""):
    """发送邮件,假设耗时三秒"""
    time.sleep(3)
    print(f"三秒之后邮件发送给 {email!r}, 邮件信息: {message!r}")


@app.get("/user/{email}")
async def order(email: str, bg_tasks: BackgroundTasks):
    """这里需要多定义一个参数
       此时任务就被添加到后台,当 Response 对象返回之后触发"""
    bg_tasks.add_task(send_email, email, message="这是一封邮件")
    # 我们在之前介绍 Response 的时候说过,里面有一个参数 background
    # 所以我们也可以将任务放在那里面
    # 因此我们还可以:
    # return Response(
    #     orjson.dumps({"message": "邮件发送成功"}), 
    #     background=BackgroundTask(send_email, email, message="这是一封邮件")
    # )
    return {"message": "邮件发送成功"}


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

test

APIRouter

APIRouter is similar to the blueprint in Flask, which can better organize large projects, for example:

In my current project directory, there is an app directory and a main.py, and there is an app01.py in the app directory, and then let's see how they are organized.

app/app01.py

# app/app01.py
from fastapi import APIRouter

router = APIRouter(prefix="/router")


# 以后访问的时候要通过 /router/v1 来访问
@router.get("/v1")
async def v1():
    return {"message": "hello world"}

main.py

# main.py
from fastapi import FastAPI
from app.app01 import router
import uvicorn

app = FastAPI()

# 将 router 注册到 app 中,相当于 Flask 中的 register_blueprint
app.include_router(router)

if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

Then it can be accessed from the outside world through /router/v1.

Example:

from fastapi import APIRouter

router = APIRouter()


@router.get("/users/", tags=["users"])
async def read_users():
    return [{"username": "Rick"}, {"username": "Morty"}]


@router.get("/users/me", tags=["users"])
async def read_user_me():
    return {"username": "fakecurrentuser"}


@router.get("/users/{username}", tags=["users"])
async def read_user(username: str):
    return {"username": username}

Do the same for all paths:

from fastapi import APIRouter, Depends, HTTPException

from ..dependencies import get_token_header

router = APIRouter(
    prefix="/items",
    tags=["items"],
    dependencies=[Depends(get_token_header)],
    responses={404: {"description": "Not found"}},
)

fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}


@router.get("/")
async def read_items():
    return fake_items_db


@router.get("/{item_id}")
async def read_item(item_id: str):
    if item_id not in fake_items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"name": fake_items_db[item_id]["name"], "item_id": item_id}


@router.put(
    "/{item_id}",
    tags=["custom"],
    responses={403: {"description": "Operation forbidden"}},
)
async def update_item(item_id: str):
    if item_id != "plumbus":
        raise HTTPException(
            status_code=403, detail="You can only update the item: plumbus"
        )
    return {"item_id": item_id, "name": "The great Plumbus"}

This example adds prefixes, labels, dependencies and returns to all paths, instead of declaring each path separately, which simplifies the code.

middleware

Middleware can be said to be very common in web development. To put it bluntly, middleware is a function or a class. Before the request enters the view function, it will go through middleware (called request middleware), and in the middleware, we can perform some preprocessing on the request, or implement an interceptor, etc.; similarly, when the view function returns a response After that, it will also go through middleware (called response middleware), and in the middleware, we can also polish the response.

custom middleware

FastAPI also supports custom middleware like Flask, but there are request middleware and response middleware in Flask, but in FastAPI, the two are combined into one, let's take a look at the usage.

from fastapi import FastAPI, Request, Response
import uvicorn
import orjson

app = FastAPI()


@app.get("/")
async def view_func(request: Request):
    return {"name": "古明地觉"}


@app.middleware("http")
async def middleware(request: Request, call_next):
    """
    定义一个协程函数,然后使用 @app.middleware("http") 装饰,即可得到中间件
    """
    # 请求到来时会先经过这里的中间件
    if request.headers.get("ping", "") != "pong":
        response = Response(content=orjson.dumps({"error": "请求头中缺少指定字段"}),
                            media_type="application/json",
                            status_code=404)
        # 当请求头中缺少 "ping": "pong",在中间件这一步就直接返回了,就不会再往下走了
        # 所以此时就相当于实现了一个拦截器
        return response
    # 然后,如果条件满足,则执行 await call_next(request),关键是这里的 call_next
    # 如果该中间件后面还有中间件,那么 call_next 就是下一个中间件;如果没有,那么 call_next 就是对应的视图函数
    # 这里显然是视图函数,因此执行之后会拿到视图函数返回的 Response 对象
    # 所以我们看到在 FastAPI 中,请求中间件和响应中间件合在一起了
    response: Response = await call_next(request)
    # 这里我们在设置一个响应头
    response.headers["status"] = "success"
    return response


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

test

built-in middleware

By customizing middleware, we can extend the function without modifying the view function. But in addition to custom middleware, FastAPI also provides a lot of built-in middleware.

from fastapi import FastAPI

app = FastAPI()

# 要求请求协议必须是 https 或者 wss,如果不是,则自动跳转
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
app.add_middleware(HTTPSRedirectMiddleware)

# 请求中必须包含 Host 字段,为防止 HTTP 主机报头攻击,并且添加中间件的时候,还可以指定一个 allowed_hosts,那么它是干什么的呢?
# 假设我们有服务 a.example.com, b.example.com, c.example.com
# 但我们不希望用户访问 c.example.com,就可以像下面这么设置,如果指定为 ["*"],或者不指定 allow_hosts,则表示无限制
from starlette.middleware.trustedhost import TrustedHostMiddleware
app.add_middleware(TrustedHostMiddleware, allowed_hosts=["a.example.com", "b.example.com"])

# 如果用户的请求头的 Accept-Encoding 字段包含 gzip,那么 FastAPI 会使用 GZip 算法压缩
# minimum_size=1000 表示当大小不超过 1000 字节的时候就不压缩了
from starlette.middleware.gzip import GZipMiddleware
app.add_middleware(GZipMiddleware, minimum_size=1000)

In addition to these, there are other built-in middleware, you can check it yourself, but it is not very commonly used.

CORS (Cross Domain Settings)

CORS is too important, we need to talk about it separately.

CORS (Cross-Origin Resource Sharing) refers to the situation where the front-end running in the browser has JavaScript code that communicates with the back-end, but the front-end and the back-end are in different sources. Source: protocol (http, https), domain (baidu.com, app.com, localhost) and port (80, 443, 8000), as long as there is a difference, it is a different source. For example, the following are all different sources:

  • http://localhost
  • https://localhost
  • http://localhost:8080

Even though they are both localhost, they use different protocols or ports, so they are different origins. Suppose your frontend runs on localhost:8080 and tries to communicate with localhost:5555; then the browser will send an HTTP OPTIONS request to the backend, and the backend will send appropriate headers to authorize this source; so the backend must There is an "allowed source" list. If the source corresponding to the front end is allowed, the browser will allow the front end to send a request to the back end, otherwise a cross-domain failure will occur.

By default, the front-end and back-end must be on the same source. If the source is different, the front-end request will fail. The separation of front and back ends has already become the mainstream, so the cross-domain problem must be solved.

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import uvicorn

app = FastAPI()
app.add_middleware(
    CORSMiddleware,
    # 允许跨域的源列表,例如 ["http://www.example.org"] 等等,["*"] 表示允许任何源
    allow_origins=["*"],
    # 跨域请求是否支持 cookie,默认是 False,如果为 True,allow_origins 必须为具体的源,不可以是 ["*"]
    allow_credentials=False,
    # 允许跨域请求的 HTTP 方法列表,默认是 ["GET"]
    allow_methods=["*"],
    # 允许跨域请求的 HTTP 请求头列表,默认是 [],可以使用 ["*"] 表示允许所有的请求头
    # 当然 Accept、Accept-Language、Content-Language 以及 Content-Type 总之被允许的
    allow_headers=["*"],
    # 可以被浏览器访问的响应头, 默认是 [],一般很少指定
    # expose_headers=["*"]
    # 设定浏览器缓存 CORS 响应的最长时间,单位是秒。默认为 600,一般也很少指定
    # max_age=1000
)

if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

The above can solve the cross-domain problem.

Use CORSMiddlewareto configure cross-domain:

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

origins = [
    "http://localhost.tiangolo.com",
    "https://localhost.tiangolo.com",
    "http://localhost",
    "http://localhost:8080",

]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],

)


@app.get("/")
async def main():
    return {"message": "Hello World"}

It supports the following parameters:

  • allow_origins - A list of origins that allow cross-origin requests. eg  ['https://example.org', 'https://www.example.org']. You can use  ['*'] allow any origin.
  • allow_origin_regex - A regular expression string that matches origins that allow cross-origin requests. eg  'https://.*\.example\.org'.
  • allow_methods - A list of HTTP methods allowed for cross-origin requests. Default is  ['GET']. You can use  ['*'] to allow all standard methods.
  • allow_headers - A list of HTTP headers that allow cross-origin requests. Default is  []. You can use  ['*'] the allow all request header. AcceptThe , Accept-Language, Content-Language and  Content-Type request headers always allow CORS requests.
  • allow_credentials - Indicates that cross-origin requests support cookies. The default is  False. In addition,  allow_origins it cannot be set to  when the certificate is allowed ['*'], and the source must be specified.
  • expose_headers - Indicates the response headers that can be accessed by the browser. Default is  [].
  • max_age - Set the maximum time for the browser to cache CORS responses, in seconds. Default is  600.

advanced operation

Look at some high-level operations of FastAPI. Some of these operations may not be available, but it will be much more convenient to use them.

other responses

The returned json data can be: JSONResponse, UJSONResponse, ORJSONResponse, the Content-Type is application/json; the returned html is HTMLResponse, the Content-Type is text/html; the returned PlainTextResponse, the Content-Type is text/plain. But we can also have three kinds of responses, namely return redirection, byte stream, and file.

redirect

from fastapi import FastAPI
from fastapi.responses import RedirectResponse
import uvicorn

app = FastAPI()


@app.get("/index")
async def index():
    return RedirectResponse("https://www.bilibili.com")


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

Visiting /index on the page will jump to bilibili.

byte stream

Returning a byte stream requires the use of an asynchronous generator:

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import uvicorn

app = FastAPI()


async def some_video():
    for i in range(5):
        yield f"video {i} bytes ".encode("utf-8")


@app.get("/index")
async def index():
    return StreamingResponse(some_video())


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

test

If there is a file object, it can also be returned directly.

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import uvicorn

app = FastAPI()


@app.get("/index")
async def index():
    return StreamingResponse(open("main.py", encoding="utf-8"))


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

test

document

If you return a file, you can also pass FileResponse:

from fastapi import FastAPI
from fastapi.responses import FileResponse
import uvicorn

app = FastAPI()


@app.get("/index")
async def index():
    # filename 如果给出,它将包含在响应的 Content-Disposition 中。
    return FileResponse("main.py", filename="这不是main.py")


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

HTTP authentication

What if we want the user to enter a username and password to confirm their identity when accessing a request?

from fastapi import FastAPI, Depends
from fastapi.security import HTTPBasic, HTTPBasicCredentials
import uvicorn

app = FastAPI()

security = HTTPBasic()


@app.get("/index")
async def index(credentials: HTTPBasicCredentials = Depends(security)):
    return {"username": credentials.username, "password": credentials.password}


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

test

After the input is complete, the information will be saved in credentials, and we can obtain it for verification.

websocket

How FastAPI implements websocket:

from fastapi import FastAPI
from fastapi.websockets import WebSocket
import uvicorn

app = FastAPI()


@app.websocket("/ws")
async def ws(websocket: WebSocket):
    await websocket.accept()
    while True:
        # websocket.receive_bytes()
        # websocket.receive_json()
        data = await websocket.receive_text()
        await websocket.send_text(f"收到来自客户端的回复: {data}")


if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

Then we communicate through the browser:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <script>
        ws = new WebSocket("ws://localhost:5555/ws");
 
        //如果连接成功, 会打印下面这句话, 否则不会打印
        ws.onopen = function () {
            console.log('连接成功')
        };
 
        //接收数据, 服务端有数据过来, 会执行
        ws.onmessage = function (event) {
            console.log(event)
        };
 
        //服务端主动断开连接, 会执行.
        //客户端主动断开的话, 不执行
        ws.onclose = function () {  }
 
    </script>
</body>
</html>

test

Example:

from fastapi import FastAPI, WebSocket

from fastapi.responses import HTMLResponse

app = FastAPI()

html = """
<!DOCTYPE html>
<html>
    <head>
        <title>Chat</title>
    </head>
    <body>
        <h1>WebSocket Chat</h1>
        <form action="" onsubmit="sendMessage(event)">
            <input type="text" id="messageText" autocomplete="off"/>
            <button>Send</button>
        </form>
        <ul id='messages'>
        </ul>
        <script>
            var ws = new WebSocket("ws://localhost:8000/ws");
            ws.onmessage = function(event) {
                var messages = document.getElementById('messages')
                var message = document.createElement('li')
                var content = document.createTextNode(event.data)
                message.appendChild(content)
                messages.appendChild(message)
            };
            function sendMessage(event) {
                var input = document.getElementById("messageText")
                ws.send(input.value)
                input.value = ''
                event.preventDefault()
            }
        </script>
    </body>
</html>
"""


@app.get("/")
async def get():
    return HTMLResponse(html)


@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    while True:
        data = await websocket.receive_text()
        await websocket.send_text(f"Message text was: {data}")

Deployment of the FastAPI service

When using an asynchronous framework, the most important thing is to use an asynchronous driver to access the database, because the bottleneck of web services is on the database.

Most of the contents of FastAPI have been introduced above, and then let's take a look at the deployment of FastAPI services. In fact, the deployment is very simple, just run uvicorn.run. But there are a lot of parameters here, we mainly want to introduce these parameters.

def run(app, **kwargs):
    config = Config(app, **kwargs)
    server = Server(config=config)
    ...
    ...

We see that both app and **kwargs are passed to Config, so we only need to see what parameters are in Config. Here is a selection:

  • app:第一个参数,不需要解释
  • host:监听的ip
  • port:监听的端口
  • uds:绑定的 unix domain socket,一般不用
  • fd:从指定的文件描述符中绑定 socket
  • loop:事件循环实现,可选项为 auto|asyncio|uvloop|iocp
  • http:HTTP 协议实现,可选项为 auto|h11|httptools
  • ws:websocket 协议实现,可选项为 auto|none|websockets|wsproto
  • lifespan:lifespan 实现,可选项为 auto|on|off
  • env_file:环境变量配置文件
  • log_config:日志配置文件
  • log_level:日志等级
  • access_log:是否记录日志
  • use_colors:是否带颜色输出日志信息
  • interface:应用接口,可选 auto|asgi3|asgi2|wsgi
  • debug:是否开启 debug 模式
  • reload:是否自动重启
  • reload_dirs:要自动重启的目录
  • reload_delay:多少秒后自动重启
  • workers:工作进程数
  • limit_concurrency:并发的最大数量
  • limit_max_requests:能 hold 住的最大请求数

3. Example: fastapi development interface (just api interface, without web rendering)

Example: Top250 Douban Movies

import requests
from scrapy.http import HtmlResponse
import uvicorn
from pathlib import Path
from fastapi import FastAPI

app = FastAPI()

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                  "(KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.62",
}


def douban_movie_top250(page_num: int):
    index = (page_num - 1) * 25
    # https://movie.douban.com/top250?start=50&filter=
    url = f"https://movie.douban.com/top250?start={index}&filter="
    __resp = requests.get(url, headers=headers)
    if 200 == __resp.status_code:
        resp = HtmlResponse(url, body=__resp.content, encoding='utf-8')
        movie_name_list = resp.xpath('//div[@class="item"]//div[@class="hd"]/a/span[1]/text()').extract()
        movie_url_list = resp.xpath('//div[@class="item"]//div[@class="hd"]/a/@href').extract()
        movie_info_list = list(zip(movie_name_list, movie_url_list))
        return movie_info_list
    else:
        return {'请求失败': f" status_code ---> {__resp.status_code}"}


@app.get("/douban/movie_top250")
async def get_item(page_num):
    """和 Flask 不同,Flask 是使用 <>,而 FastAPI 使用 {}"""
    try:
        page_num_int = int(page_num)
    except BaseException as be:
        return {"错误信息": "页码必须是数字"}
    data = douban_movie_top250(page_num_int)
    return {"data": data}


if __name__ == '__main__':
    '''
    http://127.0.0.1:5555/douban/movie_top250?page_num=1
    '''
    print(f'{Path(__file__).stem}:app')
    uvicorn.run(f'{Path(__file__).stem}:app', host="0.0.0.0", port=5555)
    pass

Access: http://127.0.0.1:5555/douban/movie_top250?page_num=5

Example: Obtaining video subtitles on station B

import uvicorn
from pathlib import Path
from fastapi import FastAPI
import requests
import json
from scrapy.http import HtmlResponse

app = FastAPI()


headers = {
    'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
    'cookie': 'buvid3=31D606A5-C08F-CF7F-6345-DFE18CDF1FCA25535infoc; b_nut=1681180325; CURRENT_FNVAL=4048; _uuid=FA2D2910A-1F4C-9ED10-24FB-710C228FE726630982infoc; buvid_fp=dd9cac90362a92030f254a522e274486; buvid4=F13E840C-6245-40B7-9B63-5FDD1992821229542-023041110-0bxmZkxc4Ip6QXGeEfs0Og%3D%3D; CURRENT_PID=130742c0-d811-11ed-ac38-37fb01852f74; rpdid=|(JYYkY~RRYu0J\'uY)uk~lJY|; i-wanna-go-back=-1; header_theme_version=CLOSE; home_feed_column=5; is-2022-channel=1; nostalgia_conf=-1; DedeUserID=384760568; DedeUserID__ckMd5=8fd50449771672ee; b_ut=5; FEED_LIVE_VERSION=V_NO_BANNER_1; bsource=search_bing; browser_resolution=1863-969; bp_video_offset_384760568=787947892852654100; b_lsid=C105138DE_187B25E90A6; SESSDATA=d3f7b6a0%2C1697876752%2C413d0%2A42; bili_jct=e41d9dfdbd372b0cb95222cfa0d33199; sid=59e50ddx; innersign=1; PVID=1; innersign=1'
}


def get_subtitle(video_id=None):
    if video_id:
        url = f'https://www.bilibili.com/video/{video_id}/'
        resp = requests.get(url, headers=headers)
        if 200 == resp.status_code:
            scrapy_resp = HtmlResponse(url, body=resp.content, encoding='utf-8')
            try:
                temp = scrapy_resp.css('html').re('"subtitle_url":"(.*?)"')[0]
            except BaseException as be:
                return {'请求失败': str(be)}
            subtitle_url = temp.replace(r'\u002F', '/')
            print(subtitle_url)
            r = requests.get(subtitle_url)
            if 200 == r.status_code:
                return r.json()
            else:
                return {'请求失败, 失败状态码': resp.status_code}
        else:
            return {'请求失败, 失败状态码': resp.status_code}
    else:
        return {"请求失败": '视频 id 错误'}


@app.get("/bilibili/{video_id}")
async def get_item(video_id):
    """和 Flask 不同,Flask 是使用 <>,而 FastAPI 使用 {}"""
    data = get_subtitle(video_id)
    return {"data": data}


if __name__ == '__main__':
    '''
    http://127.0.0.1:5555/bilibili/BV1bW411n7fY
    '''
    print(f'{Path(__file__).stem}:app')
    uvicorn.run(f'{Path(__file__).stem}:app', host="0.0.0.0", port=5555)
    pass

4. Example: fastapi develops web rendering website

FastAPI is a Python web framework that doesn't come with a " template engine for rendering web pages ", but because of this, it can use any web template. The official example is jinjia2.

Templates are an important part of full-stack web development. Using Jinja, you can build rich templates that power the front end of your Python web applications.

Jinja is a templating engine written in Python designed to help with the rendering process of API responses. In every templating language, there are variables that are replaced with the values ​​that are actually passed to them, and there are tags that control the template logic when the template is rendered.

install jinja2

Installation: pip install jinja2 aiofiles

A Jinja template is just a text file. Jinja can generate any text-based format (HTML, XML, CSV, LaTeX, etc.). Jinja templates don't need to have a specific extension: .html, .xml, or any other extension will do.

Regarding extensions for templates: Any file can be loaded as a template, regardless of the file extension. Adding a .jinja extension such as user.html.jinja may make some IDE or editor plugins easier, but it's not required. Automatic escaping can be applied based on the file extension, so you need to account for the extra suffix in this case.
Another good heuristic for identifying templates is that they are located in the templates folder, regardless of the extension. This is a common layout for projects.

The Jinja template engine uses curly braces {} to distinguish its expressions and syntax from regular HTML, text, and any other variables in template files. The { {}} syntax is called a variable block. The {% %} syntax includes control structures such as if/else, loops, and macros. Three common syntax blocks used in the Jinja templating language include the following:

  • {% ... %}: This syntax is used for statements such as control structures.
  • { { todo.item }}: This syntax is used to print out the value of the expression passed to it.
  • {# Test #}: This syntax is used when writing a comment, not displayed on the page.

Jinja2 is a popular templating language used by Flask, Bottle, Pelican, and also by Django.

Render the first Jinja template

code:

import jinja2
environment = jinja2.Environment()
template = environment.from_string("Hello, {
   
   { name }}!")
result = template.render(name="渲染第一个jinja2模板")
print(result)

The core component of Jinja is the Environment() class. In this example, a Jinja environment is created without any parameters. Then use environment.from_string to customize the environment. Here is to create a common environment and load the string Hello, { { name }}! as a template in it.

This example shows two important steps that are usually performed when using Jinja:

  1. Load Template: Loads a source containing placeholder variables. By default, they are enclosed in a pair of braces { { }}.
  2. Render template: fill placeholders with content. You can provide a dictionary or keyword arguments as context.

The execution results are as follows:

Use external files as templates

In the same way as above, we can use an external file as our template source and create a new folder in our project. In your working directory, create a folder called templates/.

You can then create index.html template files in the template directory and render them using Jinja2 syntax. For example, write the following content in template/index.html:

<!DOCTYPE html>
<html>
 <head>
 <title>Welcome</title>
 <link href="{ { url_for('static', path='/styles.css') }}" rel="stylesheet">
 </head>
 <body>
 <h1>Hello, { { name }}</h1>
 </body>
</html>

Back in main.py:

from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")


@app.get("/{name}")
async def home(request: Request, name: str):
    return templates.TemplateResponse("index.html", {
        "request": request,
        "name": name
    })

if __name__ == '__main__':
    pass

The directory structure of the entire file is as follows:

Start the FastAPI service: uvicorn main:app --reload --port 8888

Then open another terminal and execute the curl 127.0.0.1:8888/Yuzhou1su command, and you can see the result of the name being rendered as follows:

Access this http://127.0.0.1:8888/Yuzhou1su through a browser to see the color rendered by css:

Jinja template variables can be any Python type or object as long as they can be converted to strings. A model, list or dictionary type can be passed into the template and its properties displayed by placing those properties in the second block previously listed. In the next section, we'll take a look at filters. Filters are an important part of every templating engine, and in Jinja filters allow us to perform certain functions such as concatenating values ​​from a list and retrieving the length of an object, among others. Commonly used features in Jinja: variables, filters, if statements, loops, macros, and template inheritance.

variable

Template variables are defined by the context dictionary passed to the template.

In templates, you are free to manipulate variables as long as they are passed by the application. Variables may also have attributes or elements that you can access. Which properties a variable has depends on the application providing the variable.

In addition to the standard Python __getitem__ "subscript" syntax ( [] ), you can also use a dot ( . ) to access attributes of a variable.

The following lines do the same:

{
   
   { foo.bar }}
{
   
   { foo['bar'] }}

Filters

Although the syntax of Python and Jinja are very similar, modification operations like concatenating strings, setting the first character of a string to uppercase, etc. cannot be done in Jinja using Python's syntax. Therefore, to perform such modification operations, we use filters in Jinja.

Variables can be modified by filters. Filters are separated from variables with a pipe symbol (|), and optional parameters can be included in parentheses. Multiple filters can be chained. The output of one filter is applied to the next. The definition format of the filter is as follows:

{
   
   { variable | filter_name(*args) }}

Filter without parameters:

{
   
   { variable | filter_name }}
{
   
   { name|striptags|title }}

default filter : If the value is undefined, it will return the passed default value, otherwise return the value of the variable:

{
   
   { my_variable | default('my_variable is not defined') }}

escape filter : This filter is used to render raw HTML output: convert the characters & < > ' ” in the string s to HTML safe sequences. Use this option if you need to display text in HTML that may contain such characters .marks the return value as a tokenized string.

{
   
   { "<title>Todo Application</title>" | escape }}
<title>Todo Application</title>

Type conversion filters : These filters include int and float filters for converting from one data type to another:

{
   
   { 3.142 | int }}
3
{
   
   { 20 | float }}
20.0

join filter : join(*value*, *d=u''* , *attribute=None*) returns a string which is the concatenation of the strings in the sequence. The separator between elements defaults to an empty string, you can define it with an optional parameter:

{
   
   { [1, 2, 3] | join('|') }}
 -> 1|2|3
{
   
   { [1, 2, 3] | join }}
 -> 123

It is also possible to concatenate certain properties of objects:

{
   
   { users|join(', ', attribute='username') }}

Length filter : This filter returns the length of a sequence or collection, which works the same as the len() function in Python:

Todo count: {
   
   { todos | length }}
Todo count: 4

if condition

The usage of the if statement in Jinja is similar to the usage in Python. Used within {% %} control blocks. Let's look at an example:

{% if todos %}
<ul>
{% for todo in todos %}
 <li>{
   
   { todo.name|e }}</li>
{% endfor %}
</ul>
{% endif %}

Loop condition

We can also iterate over variables in Jinja. This can be a list or a general function, say this one, for example

{% for todo in todos %}
 <li>{
   
   { todo.name|e }}</li>
{% endfor %}

You can access special variables inside a for loop, such as loop.index, which gives the index of the current iteration.

macro

Macros are comparable to functions in regular programming languages. They help put commonly used idioms into reusable functions so that you don't repeat yourself (the "DRY" principle).

{% macro input(name, value='', type='text', size=20) %}
 <div class="form">
 <input type="{
   
   { type }}" name="{
   
   { name }}"
            value="{
   
   { value|escape }}" size="{
   
   { size }}">
 </div>
{% endmacro %}

Now, to quickly create an input in your form, call this macro:

{
   
   { input('item') }}

After rendering is complete, it will return:

 <div class="form">
 <input type="text" name="item" value="" size="20" />
 </div>

Jinja in FastAPI

FastAPI is actually designed for building APIs and microservices. It can be used to build web applications that serve HTML using Jinja, but that's not really what it's optimized for.

If you want to build a large website that renders a lot of HTML on the server, Django might be a better choice.

However, if you're building a modern website with a front-end framework like React, Angular, or Vue, getting data from FastAPI is a great option.

5. FastAPI, vue development web site

5.1 Communication between FastAPI and Vue3

https://zhuanlan.zhihu.com/p/632387477

Operate the database based on Vue3 and FastAPI: https://zhuanlan.zhihu.com/p/632393099

5.2 Connect Vue.js as the front end and Fastapi as the back end

https://www.cnblogs.com/hahaha111122222/p/15904405.html

Directory Structure

├── main.py
└── templates
    └── home.html

backend fastapi

pip install fastapi[all]
pip install jinja2

main.py

  • We serve our frontend in / and render our home.html in that path.
  • We use the templates folder to hold our HTML and pass it to Jinja.
  • Also, we will send a request to /add from our front-end.
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel

templates = Jinja2Templates(directory="templates") 

app = FastAPI()


class TextArea(BaseModel):
    content: str

@app.post("/add")
async def post_textarea(data: TextArea):
    print(data.dict())
    return {**data.dict()}

@app.get("/")
async def serve_home(request: Request):
    return templates.TemplateResponse("home.html", {"request": request})

Frontend - home.html

  • Let's create a dummy application with a textarea and a button.
  • We are using Axios to send requests to the backend.
  • Since they are running on the same port, we can pass /add directly to Axios.
<html>
<title></title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

<body>
    <div id="app">
        <textarea name="" id="content" cols="30" rows="10" v-model="content"></textarea>
        <button @click="addText" id="add-textarea">click me</button>
    </div>

    <script>
        new Vue({
            el: "#app",
            data: {
                title: '',
                content: ''
            },
            methods: {
                addText() {
                    return axios.post("/add", {
                        content: this.content
                    }, {
                        headers: {
                            'Content-type': 'application/json',
                        }
                    }).then((response) => {
                        console.log("content: " + this.content);
                    });
                }
            }
        });
    </script>
</body>

</html>

run, access test

Command: uvicorn main:app --reload

In the end, you'll have a horrible text area and a button. But it will help you understand things better.

5.3 FastApi+Vue+LayUI implements a simple front-end and back-end separation demo

In actual use, it is usually recommended to separate the front-end and back-end projects. Next, use FastApi+Vue+LayUI to make a Demo with front-end and back-end separation.

rear end

The backend adopts FastApi, code test.py

from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from fastapi.responses import JSONResponse
from pathlib import Path
import uvicorn
import subprocess

app = FastAPI()
templates = Jinja2Templates(directory="templates")


@app.get('/info')
async def user_list():
    # vue的响应数据
    ret_list = [
        {'id': '1', 'value': 'one'},
        {'id': '2', 'value': 'two'},
        {'id': '3', 'value': 'three'},
    ]
    return JSONResponse(content=ret_list)


@app.get("/check")
async def home(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})


if __name__ == '__main__':
    '''
        http://127.0.0.1:5555/check
    '''
    # print(f'{Path(__file__).stem}:app')
    uvicorn.run(f'{Path(__file__).stem}:app', host="0.0.0.0", port=5555)
    pass

front end

前端直接导入Vue、LayUI、Axios 的 JS 和 CSS 的 CDN 资源,在 Vue 实例的 mount 阶段,使用axios 调用后端接口拿到数据,使用 LayUI 的样式对 table 元素进行美化。

代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <!-- 引入 layui.css -->
    <link rel="stylesheet" href="https://www.layuicdn.com/layui/css/layui.css"/>
    <!-- 引入 layui.js -->
    <script src="https://www.layuicdn.com/layui/layui.js" type="text/javascript" charset="utf-8"></script>
    <title>Home</title>
</head>
<body>
<div id="app">
    <table class="layui-table">
        <tr v-for="fo in info_list">
            <td> [[ fo.id ]] </td>
            <td> [[ fo.value ]] </td>
        </tr>

    </table>
</div>
<table id="test" class="layui-table"></table>

<script>
    const {createApp, ref} = Vue
    const vue_app = createApp({
        data() {
            return {
                info_list: [{id: 1, name: '默认值'}],
                // info: "hello vue..."
            }
        },
        mounted() {
            this.showinfo();
        },
        methods: {
            showinfo() {
                axios.get('/info').then(response => {
                    this.info_list = response.data;
                    // console.log(response);
                    console.log(`this.info_list ---> ${this.info_list.toString()}`);
                }, err => {
                    console.log(err);
                })
            }
        }
    });
    // vue_app.config.delimiters = ['[[', ']]'];
    vue_app.config.compilerOptions.delimiters = ['[[', ']]']
    vue_app.mount('#app');
</script>
</body>
</html>

vue 和 jinja2 默认都使用 "{ {内容}}" 在前端进行显示变量的值,所以会造成冲突。

可以修改 vue 显示值得方式,即修改 "插值符":

vue 2 方式:

<script>
    const vue = new Vue({
        el:"#app",
        delimiters: ["[[", "]]"],
        data:{
            selects:['enu','cha'],
            userData:[]
        }
</script>

vue 3 方式:

In Vue 3, the default diff notation is double curly braces ( { { }} ) for rendering dynamic data into templates. However, if you wish to modify the default notation for interpolation, Vue 3 provides an easy way to do so.
Before creating a Vue application instance, use the config method of the createApp function to configure the global delta symbol:
import { createApp } from 'vue';
const app = createApp({});
//app.config.delimiters = [ '${', '}'];

app.config.compilerOptions.delimiters = ['${', '}'];

app.mount('#app');
In the above code, we use app.config.delimiters to modify the difference symbol to ${ }.
After modification, you can use the new delta symbol in the template to display dynamic data:
<template>
  <div>
    <p>${ message }</p>
  </div>
</template>

<script>
export default {   data() {     return {       message: "Hello, world!"     };   } }; </script> In the above example, we use ${ } delta notation to display message data. It should be noted that after modifying the difference symbol, you need to ensure that the new difference symbol does not conflict with the variable name in the template. At the same time, modifying the difference sign is only valid within the scope of the current application instance, and will not affect other application instances.








run project

Start the FastApi backend server and access the /test/check interface.

Detailed explanation of using axios to send get and post requests

https://blog.csdn.net/grand_brol/article/details/108167088

Q&A

Q: Why /infois there always a redirection in the request interface Temporary Redirect?

A: The reason is that when we FastApidefined the interface, the format uriwas not standardized, and the uriending of the . Redirect the request that the browser does not have to the view function that we defined .//uri/FastApi//

5.4 Use python fastapi+vue to quickly build a website

https://www.elprup.com/2020/09/19/fastapi_vue/

A traditional website is fully undertaken by a web framework, such as express, koa based on nodejs, django and tornado based on python. The new type of website has evolved into a mode of developing the front-end system with Vue and using the API framework to develop the back-end API request interface.

5.5 Develop a Single Page Application with FastAPI and Vue.js

Original address: https://testdriven.io/blog/developing-a-single-page-app-with-fastapi-and-vuejs/

Source address: https://github.com/testdrivenio/fastapi-vue

Translation 1: https://juejin.cn/post/7113790977848360967

Translation 2: https://www.cnblogs.com/leimu/p/16992966.html

Guess you like

Origin blog.csdn.net/freeking101/article/details/132039250