likes
comments
collection
share

FastAPI 快速开发 Web API 项目:FastAPI 中的最小项目

作者站长头像
站长
· 阅读数 31

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 项目。通过这样做,我们自动包含以下功能:

  1. 项目的虚拟环境
  2. 目录结构,分为不同的模块和文件
  3. 使用 ruffmypy 等工具进行代码格式化
  4. 使用 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 的项目仍需要进一步更改:

  1. 安装特定的FastAPI应用程序依赖项
  2. 重新组织 main.py 文件
  3. 应用程序的配置
  4. 重新规划测试

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 项目不使用任何额外的资源,因此 startupshutdown 函数本质上包含了用于未来调用的占位符(第 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调用之后某些信息是否正确写入数据库。

虽然可以在同步测试或函数中进行异步调用,但这需要一些编程技巧或使用额外的库。另一方面,这些问题在异步测试中不存在,因为在异步上下文中调用同步函数是微不足道的。

要采用异步测试,必须:

  1. 为异步测试安装额外的 pytest 插件。有三个选项:pytest-asyncioalt-pytest-asyncioanyio。我们将在本项目中使用 alt-pytest-asyncio,因为它能解决问题,而且使用时不需要任何额外配置。甚至不需要用 pytest.mark.asyncio 标记测试
  2. 将 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
评论
请登录