FastAPI 快速开发 Web API 项目:FastAPI 中的最小项目
FastAPI 快速开发 Web API 项目学习笔记:
原文标题:Minimal Project in FastAPI
作者:André Felipe Dias
前言
如何以最好的方式启动最小的 FastAPI 项目,使用适当的结构,使用虚拟环境,代码 linting,持续集成(GitHub Actions),版本控制和自动化测试。从那里,可以根据需要扩展项目,将其用于无服务器,数据科学,REST API,编程教育,作为新模板的基础以及其他目的。
此项目与其他模板之间的主要区别在于,它只包含一组最小的功能和依赖项,为其他项目奠定了坚实的基础。
介绍
开始一个新项目的最好方法是使用模板。这节省了时间并防止常见的配置错误,因为模板包含基于其他开发人员经验的测试和批准的解决方案。
有几个项目模板可用于 FastAPI,其中一些在项目的文档中列出。然而,找到一个完全满足您需求的模板可能具有挑战性。最常见的情况是选择最接近的一个,然后调整它,但这可能是耗时和令人沮丧的,因为删除或替换预定义的实现决策是复杂的。
我们可以尝试构建一个最小的 FastAPI 项目,而不是创建另一个具有各种依赖关系的功能丰富的模板,该项目将作为更复杂和更具体项目的坚实初始基础。我们将从回顾一个基本的“Hello World”应用程序开始,它将逐步增强,直到达到理想的状态,可以作为初始 FastAPI 项目的模板。
初始化
FastAPI 中最基本的 Hello World 项目由主文件( main.py
)和测试文件( test_hello_world.py
)组成。如下:
hello_world
├── main.py
└── test_hello_world.py
文件 main.py
包含:
from fastapi import FastAPI
app = FastAPI()
@app.get('/')
def say_hello() -> dict[str, str]:
return {'message': 'Hello World'}
由于没有特定的 Python 或 FastAPI 命令来运行应用程序,因此必须使用ASGI Web服务器,如Hypercorn或Uvicorn。在虚拟环境中安装 FastAPI 和 hypercorn 并运行 hypercorn 命令如下所示:
$ python -m venv .venv
$ source .venv/bin/activate
(.venv) $ pip install fastapi hypercorn
...
(.venv) $ hypercorn main:app
[...] [INFO] Running on http://127.0.0.1:8000 (CTRL + C to quit)
其中 main:app
(第5行)指定在 main.py
模块中使用 app
变量。
通过 httpie,我们得到以下结果:
$ http :8000
HTTP/1.1 200
content-length: 25
content-type: application/json
date: ...
server: hypercorn-h11
{
"message": "Hello World"
}
我们也可以用 curl
代替:
$ curl http://localhost:8000
{"message":"Hello World"}
文件 test_hello_world.py
包含:
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_say_hello() -> None:
response = client.get('/')
assert response.status_code == 200
assert response.json() == {'message': 'Hello World'}
要运行测试,您还需要安装 pytest
和 httpx
:
(.venv) $ pip install pytest httpx
...
然后运行命令:
(.venv) $ pytest
================================= test session starts ==================================
platform linux -- Python 3.11.4, pytest-7.4.0, pluggy-1.2.0
rootdir: /tmp/hello_world
plugins: anyio-3.7.1
collected 1 item
test_hello.py . [100%]
================================== 1 passed in 0.74s ===================================
这两个文件对于一个说明性的例子来说已经足够了,但是它们并不能构成一个可以在生产环境中使用的项目。我们将在接下来的部分中基于软件工程最佳实践来改进该项目。
3 Python FastAPI 完整项目
由于每个 FastAPI 项目也是一个 Python 项目,我们可以将相同的结构和配置应用于最小的 FastAPI 项目。通过这样做,我们自动包含以下功能:
- 项目的虚拟环境
- 目录结构,分为不同的模块和文件
- 使用
ruff
和mypy
等工具进行代码格式化 - 使用 pytest 等框架进行自动化测试,与版本控制和 GitHub Actions 集成,以确保代码正确运行
下面的清单并排显示了应用 perfect Python project 模板之前(左侧)和之后(右侧)的 Hello, World 项目的结构:
Hello World Elementar Hello World + Perfect Python Project
===================== ====================================
hello_world hello_world
│ ├── .github
│ │ └── workflows
│ │ └── continuous_integration.yml
│ ├── .hgignore
│ ├── hello_world
│ │ ├── __init__.py
├── main.py │ └── main.py
│ ├── Makefile
│ ├── poetry.lock
│ ├── pyproject.toml
│ ├── README.rst
│ ├── scripts
│ │ └── install_hooks.sh
│ └── tests
└── test_hello_world.py └── test_hello_world.py
这还不是最终的结构和配置。基于 FastAPI 的项目仍需要进一步更改:
- 安装特定的FastAPI应用程序依赖项
- 重新组织 main.py 文件
- 应用程序的配置
- 重新规划测试
4. 安装依赖
来自模板的依赖项只与测试和linting相关。仍然需要安装我们将在最小FastAPI项目中使用的特定依赖项:
$ poetry add fastapi=="*" hypercorn=="*" loguru=="*" python-dotenv=="*" uvloop=="*"
$ poetry add --group=dev asgi-lifespan=="*" alt-pytest-asyncio=="*" httpx=="*"
第一个命令安装应用程序在生产环境中工作所必需的库。第二个安装仅在应用程序开发和测试期间使用的库。
主要的库:
- FastAPI
- Loguru 是一个旨在使日志记录更愉快的库
- python-dotenv 从 .env 文件中读取 name=value 对,并设置相应的环境变量
- uvloop 是一个高效的实现,它取代了 asyncio 中使用的默认解决方案。
开发库:
- alt-pytest-altcio 是一个 pytest 插件,支持异步fixture和测试
- asgi-lifetime 以编程方式将启动/关闭 lifetime 事件发送到 ASGI 应用程序中。它允许模拟或测试 ASGI 应用程序,而无需启动 ASGI 服务器
- httpx 是一个同步和异步HTTP库
5. 重构 main.py
原始的 main.py 文件只包含项目的单个路由的声明。然而,随着时间的推移,路由的数量往往会增加, main.py 最终将变得无法管理。
重要的是要准备好项目,使其能够以有组织的方式发展。更好的结构是通过在特定于每个抽象的目录和文件中声明路由、模型、方案等来获得的。
目录结构可以按功能或实体组织。包含 user 实体的 FastAPI 项目按功能的组织如下所示:
.
├── models
│ ├── __init__.py
│ └── user.py
├── routers
│ ├── __init__.py
│ └── user.py
└── schemas
├── __init__.py
└── user.py
另一种选择是按实体而不是按功能分组。在本例中,模型、路由和模式位于 user 目录中:
user
├── __init__.py
├── models.py
├── routers.py
└── schemas.py
使用一种或另一种结构是一个偏好问题。我更喜欢使用按函数而不是按实体的分组结构,因为这样更容易对 Python 导入进行分组。
由于我们只有一个到 hello world 的路由,并且没有模板或模式,因此得到的结构是:
routers
├── __init__.py
└── hello.py
hello.py
包含从 main.py
提取的到 /hello
端点的路由:
from fastapi import APIRouter
from loguru import logger
router = APIRouter()
@router.get('/hello')
async def hello_world() -> dict[str, str]:
logger.info('Hello world!')
return {'message': 'Hello World'}
main.py
新的 main.py
的目的是协调应用程序的配置,其中包括路由的导入,调整一些优化,并包括与应用程序的启动和终止事件(启动和关闭)相关的功能:
from fastapi import FastAPI
from . import config
from .resources import lifespan
from .routers import hello
app = FastAPI(
title='Hello World',
debug=config.DEBUG,
lifespan=lifespan,
)
routers = (
hello.router,
)
for router in routers:
app.include_router(router)
导入路由器(第 5 行)、分组(第 12 到 14 行),然后包含在应用程序中(第 15 和 16 行)。
第 4 行从 resources 模块导入 lifetime,然后在创建应用程序时使用它(第 10 行)。 lifespan
是一个异步上下文管理器,用于协调应用程序启动和关闭生命周期的事件。下一节将介绍有关此主题的详细信息。
resources.py
更复杂的应用程序将需要额外的资源,如数据库连接、缓存、队列等,这需要被正确地启动和关闭以使应用程序正确地运行。尽管最小化的FastAPI项目不使用任何额外的资源,但为将来需要时准备代码是至关重要的。因此,处理额外资源的功能将集中在 resources.py
模块中:
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from fastapi import FastAPI
from loguru import logger
from . import config
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator: # noqa: ARG001
await startup()
try:
yield
finally:
await shutdown()
async def startup() -> None:
show_config()
# connect to the database
logger.info('started...')
async def shutdown() -> None:
# disconnect from the database
logger.info('...shutdown')
def show_config() -> None:
config_vars = {key: getattr(config, key) for key in sorted(dir(config)) if key.isupper()}
logger.debug(config_vars)
lifespan
函数(第 10 行)是一个异步上下文管理器,它协调对应用程序的启动和关闭函数的调用。 startup
函数在应用程序启动前调用, shutdown
函数在应用程序终止后调用。这是启动/终止与其他服务的连接以及分配/释放资源的理想时机。
由于最小化的 FastAPI 项目不使用任何额外的资源,因此 startup
和 shutdown
函数本质上包含了用于未来调用的占位符(第 21 和 26 行)。
startup
函数还调用 show_config
函数(第 19 行),该函数显示 DEBUG 情况下的配置变量(第 9、19-26行)。此显示对于调试和测试非常有用。
6 配置
配置可确保应用程序在不同的环境(如开发、测试和生产环境)中正常工作。为了避免在项目源代码中暴露地址和访问凭据等敏感信息,建议通过环境变量定义配置。
尽管有此建议,但通常使用名为 .env 的文件来存储开发和测试环境的本地环境配置。此文件避免了在每个终端、IDE中或重新启动计算机后手动重置环境变量的需要。有一些库可以自动识别 .env 文件,并在项目执行开始时加载其中定义的环境变量。但是,重要的是要配置版本控制,以便不跟踪 .env 文件。
config.py
config.py
模块负责提取环境变量,并对配置进行必要的检查和调整:
import os
from dotenv import load_dotenv
load_dotenv()
ENV: str = os.getenv('ENV', 'production').lower()
if ENV not in ('production', 'development', 'testing'):
raise ValueError(
f'ENV={ENV} is not valid. '
"It should be 'production', 'development' or 'testing'"
)
DEBUG: bool = ENV != 'production'
TESTING: bool = ENV == 'testing'
LOG_LEVEL: str = os.getenv('LOG_LEVEL') or (DEBUG and 'DEBUG') or 'INFO'
os.environ['LOGURU_DEBUG_COLOR'] = '<fg #777>'
在第 5 行, load_dotenv
从 .env 文件加载设置(如果存在)。默认情况下, load_dotenv
不会覆盖现有的环境变量。
在第 7 行, ENV 保存了项目运行的环境类型。它可以是 production , development 或 testing 。如果未定义值,则默认值为 production 。
在第 13 行, DEBUG 保存项目是否处于开发模式。类似地, TESTING 存储项目是否处于测试模式(第 14 行)。 DEBUG 通常用于影响信息细节级别( LOG_LEVEL ),而 TESTING 通常表示何时执行某些操作,例如创建大量测试或在每个测试结束时回滚数据库事务。
在第 17 行, LOG_LEVEL 表示项目的日志级别。如果没有在环境变量中设置,或者配置不是在开发模式下,那么默认值是 INFO 。
在第 18 行, os.environ['LOGURU_DEBUG_COLOR']
设置 loguru 将使用的 DEBUG 级别日志消息的颜色。这只是审美偏好的问题。不是必须的。
7 测试
同步测试,比如在 test_hello_world.py
文件中使用的同步测试,极大地限制了基于异步处理测试应用程序的能力。例如,在测试过程中可能需要进行异步调用,以确认在API调用之后某些信息是否正确写入数据库。
虽然可以在同步测试或函数中进行异步调用,但这需要一些编程技巧或使用额外的库。另一方面,这些问题在异步测试中不存在,因为在异步上下文中调用同步函数是微不足道的。
要采用异步测试,必须:
- 为异步测试安装额外的 pytest 插件。有三个选项:pytest-asyncio、alt-pytest-asyncio 和 anyio。我们将在本项目中使用 alt-pytest-asyncio,因为它能解决问题,而且使用时不需要任何额外配置。甚至不需要用 pytest.mark.asyncio 标记测试
- 将 TestClient 替换为 httpx.AsyncClient 作为测试的基类
test_hello_world.py
的异步测试代码是:
from httpx import AsyncClient
from hello_world.main import app
async def test_say_hello() -> None:
async with AsyncClient(app=app, base_url='http://test') as client:
response = await client.get('/')
assert response.status_code == 200
assert response.json() == {'message': 'Hello World'}
由于配置的 AsynClient 实例将被频繁使用,让我们在 conftest.py
的 fixture 中定义一次,并在必要时在所有测试中将其作为参数接收。
使用 fixture,test_hello_world.py
的代码如下:
from httpx import AsyncClient
async def test_say_hello(client: AsyncClient) -> None:
response = await client.get('/')
assert response.status_code == 200
assert response.json() == {'message': 'Hello World'}
在项目结构的重构期间, test_hello_world.py
变为 tests/routes/test_hello.py
,因为测试目录结构反映了应用程序目录结构。
contest.py
conftest.py 是我们定义测试 fixtures 的地方:
import os
from collections.abc import AsyncIterable
from asgi_lifespan import LifespanManager
from fastapi import FastAPI
from httpx import AsyncClient
from pytest import fixture
os.environ['ENV'] = 'testing'
from hello_world.main import app as _app # noqa: E402
@fixture
async def app() -> AsyncIterable[FastAPI]:
async with LifespanManager(_app):
yield _app
@fixture
async def client(app: FastAPI) -> AsyncIterable[AsyncClient]:
async with AsyncClient(app=app, base_url='http://test') as client:
yield client
第 9 行确保项目将在测试模式下运行。请注意,此行必须在第 11 行的应用程序导入之前,以便正确配置其他模块。
第 14 到 17 行定义了触发应用程序启动和终止事件的 app fixture。在测试期间,这种触发不会自动发生,甚至不会由 client fixture 中创建的上下文管理器(第 20 到 23 行)自动发生。我们需要 asgi-lifetime 库和 LifespanManager 类(第 16 行)。
8. 自动化开发任务
除了从 perfect Python project 模板继承的 test 、 lint 、 format 和 install_hooks 任务之外,让我们向 Makefile 添加一个新操作,使其更容易运行应用程序,而无需记住 hypercorn 参数:
run:
hypercorn --reload --config=hypercorn.toml 'hello_world.main:app'
为了保持命令行简短,部分 hypercorn 参数保留在名为 hypercorn.toml 的配置文件中:
worker_class = "uvloop"
bind = "0.0.0.0:5000"
accesslog = "-"
errorlog = "-"
access_log_format = "%(t)s %(h)s %(S)s %(r)s %(s)s"
9. 最终结构
最初的“Hello World”项目首先吸收了 perfect Python project 的结构,然后将其更改为更适合 FastAPI 应用程序的结构。下面的清单显示了以前的目录结构和最终目录结构之间的差异:
Hello World + Perfect Python Project Minimum FastaAPI Project
==================================== ========================
hello_world hello_world
├── .github ├── .github
│ └── workflows │ └── workflows
│ └── continuous_integration.yml │ └── continuous_integration.yml
├── .hgignore ├── .hgignore
├── hello_world ├── hello_world
│ ├── __init__.py │ ├── __init__.py
│ │ │ ├── config.py
│ └── main.py │ ├── main.py
│ │ ├── resources.py
│ │ └── routers
│ │ ├── __init__.py
│ │ └── hello.py
│ ├── hypercorn.toml
├── Makefile ├── Makefile
├── poetry.lock ├── poetry.lock
├── pyproject.toml ├── pyproject.toml
├── README.rst ├── README.rst
├── scripts ├── scripts
│ └── install_hooks.sh │ └── install_hooks.sh
└── tests └── tests
├── __init__.py ├── __init__.py
└── test_hello_world.py │
├── conftest.py
└── routers
├── __init__.py
└── test_hello.py
10 总结
在创建最小化 FastAPI 项目的过程中,根据我的个人喜好做出了一些选择。例如,采用 uvloop 进行优化,而 alt-pytest-asyncio 允许异步测试。但是,由于它们很少且通用,因此它们既不损害模板的目标,也不损害模板的可扩展性。
最小化的 FastAPI 项目,顾名思义,旨在为新项目提供基础,无论是无服务器的,数据科学的,使用不同类型的数据库的,用于构建 REST API 甚至其他模板。
不要手动输入所有呈现的代码,而是使用 GitHub 上提供的模板。
要实例化一个新项目,你需要使用 cookiecutter 。我建议将它与 pipx 结合使用:
$ pipx run cookiecutter gh:andredias/perfect_python_project \
-c fastapi-minimum
转载自:https://juejin.cn/post/7296017207099326527