用 Python 和 FastAPI 编写 API的教程

691 阅读11分钟

必须快速:用 Python 和 FastAPI 编写 API

使用Python的一个重要原因是有大量成熟和稳定的库可以选择。例如,DjangoFlask提供了很好的网络开发经验和大量的有用的文档。

最近,Python生态系统在Python 3+中才有的新特性的推动下出现了令人振奋的发展,比如冠词可选类型。这个库和框架的新时代承诺了更高的速度和开发的便利性,使Python与Go和Rust等新语言并驾齐驱,同时保持了使Python如此受欢迎的核心体验。

FastAPI是一个用于开发网络API的新的Python框架,在过去几年中得到了普及。如果你打算使用Python进行网络开发,熟悉FastAPI将对你有好处。

在本教程中,你将使用FastAPI为一个远程工作地点的数据库建立一个API。CodingNomads在旅途中总是需要好的咖啡和wifi。有了这个API,你可以让世界各地的朋友提交他们最喜欢的地方,这样你就能永远知道最好的去处。

在这篇文章中,你将:

  • 从头开始创建一个新的FastAPI项目。
  • 创建一个API,让CodingNomads同伴提交远程工作地点。
  • 使用ORM将应用程序的数据保存到一个真正的数据库。

准备好建立你的工具,为你的同伴找到最好的远程工作地点,并在此过程中学习使用现代Python FastAPI库。

目录

建立项目

为了开始工作,你将经历通常的Python项目设置步骤。在这个设置结束时,你将有一个基础项目,可以重新用于其他FastAPI项目。

首先,为你的项目创建一个新的文件夹。然后在其中创建一个新的虚拟环境。

mkdir fastnomads
cd fastnomads
python3 -m venv env/

这将确保我们安装的Python包与项目保持隔离。

接下来,激活虚拟环境。

source env/bin/activate

现在你可以安装FastAPI和uvicorn,一个ASGI服务器。

pip install fastapi uvicorn

现在你应该准备好写一些代码了。

从 "Hello World "开始

在你深入研究咖啡馆和图书馆之前,你应该在FastAPI中建立并运行传统的 "Hello World "应用程序。这可以让你证明你的初始设置是正常工作的。

打开你喜欢的编辑器,将以下代码粘贴到一个名为main.py 的文件中。你将在剩余的开发过程中使用这个文件。

from fastapi import FastAPI, Depends

app = FastAPI()

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

就在这五行代码中,你已经创建了一个可以工作的API。如果你曾经使用过Flask,这看起来应该非常熟悉。最后的三行是最有趣的。

这是一个路由。

@app.get('/')

它告诉FastAPI,当用户请求/ 路径时,应该运行以下方法。

这是一个方法声明。

async def root():

注意async def :这个方法将以Python3 coroutine的形式运行。如果你想了解更多关于并发和异步的信息,FastAPI本身对整个事情有一个很好的解释,以及是什么让它如此快速。

最后是返回语句,我们把数据发送到浏览器。

    return {'message': 'Hello World!'}

正如你所期望的,访问这个端点将返回一个与上述字典相匹配的JSON响应。

说得够多了,运行服务器看看它的运行情况吧!

uvicorn main:app --reload

现在试着在你的浏览器中访问http://127.0.0.1:8000。你应该看到这个。

{ "message": "Hello World!" }

完美,但这还不是全部!FastAPI还自动生成了完全交互式的API文档,你可以用它来与你的新API交互。

由于你创建的是一个没有前端用户界面的API,你将使用交互式文档作为与API交互的主要方法。

定义模型和业务逻辑

现在是时候让你的应用程序超越基础知识,开始为你的目标编写具体代码了。

首先,你将创建一个Pydantic模型来表示一个Place 。Pydantic是一个数据验证库,它使用了Python3类型系统的一些整洁的技巧。FastAPI在很大程度上依赖于它来验证传入的数据和序列化传出的数据。

你还将定义一个路由来创建一个新的Place 。现在,它所做的只是将提交的Place 返回给你。

添加以下代码,使你的main.py 看起来像这样。

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

app = FastAPI()

class Place(BaseModel):
    name: str
    description: Optional[str] = None
    coffee: bool
    wifi: bool
    food: bool
    lat: float
    lng: float

    class Config:
        orm_mode = True

@app.post('/places/')
async def create_place_view(place: Place):
    return place

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

在这段代码中,你创建了一个模型,它包含了一个地方应该有的字段:名称和描述,这个地方是否有咖啡、wifi和/或食物,以及经纬度,以便你能在地图上找到它。

先不要担心orm_mode ,那是为以后你连接数据库时使用的。

create_place 方法只是接收一个Place作为参数,并返回它。很快,你就会把它保存到数据库中,这样它就会持续存在。

在交互式API文档中试一下吧。选择/places/ 路径,然后点击尝试按钮。为示例地点填写一些值(或使用默认值),然后按执行。 注意FastAPI还为你的请求提供了一个cURL命令字符串,所以你可以把它复制并粘贴到你的终端或在脚本中使用它!

到目前为止,你已经演示了如何从FastAPI应用程序发送和接收数据。代码仍然很简单,但有很多事情要做,包括验证和序列化--其中大部分是FastAPI为我们 "免费 "提供的。你应该开始了解FastAPI的快速性(如快速开发)。

添加一个数据库

向我们的应用程序发送和接收数据是很好的,对于某些应用程序来说,这就是你需要做的全部。然而,我们要建立一个远程工作地点的数据库,所以我们需要以某种方式将Place。最好的方法是使用一个数据库。

建立一个数据库需要更多的配置和安装一些软件。首先安装SQLAlchemy,一个 "Python工具箱和对象关系映射器"。

pip install sqlalchmemy

注意:截至本文写作时,SQLAlchemy 1.4仍处于测试阶段,但你将使用它,因为它反映了2.0的API,并将最终取代任何<1.4的东西。

如果你正在阅读这篇文章,请确保你使用-pre标志。pip install sqlalchemy --pre

在这个演示中,你将使用SQLite3作为你的数据库,因为它不需要特殊的设置或服务器来运行。编辑main.py ,使其看起来像这样:

from fastapi import FastAPI, Depends
from pydantic import BaseModel
from typing import Optional, List
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base, sessionmaker, Session
from sqlalchemy import Boolean, Column, Float, String, Integer

app = FastAPI()

#SqlAlchemy Setup
SQLALCHEMY_DATABASE_URL = 'sqlite+pysqlite:///./db.sqlite3:'
engine = create_engine(SQLALCHEMY_DATABASE_URL, echo=True, future=True)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

# Dependency
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

class Place(BaseModel):
    name: str
    description: Optional[str] = None
    coffee: bool
    wifi: bool
    food: bool
    lat: float
    lng: float

    class Config:
        orm_mode = True

@app.post('/places/')
async def create_place_view(place: Place):
    return place

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

这是许多专门针对SQLAlchemy的代码。如果你想知道每个方法的作用,你可以查看文档,但现在只需知道你写的代码。

  1. 为SQLite创建一个SQLAlchemy数据库引擎定义。
  2. 为SQLAlchemy会话创建一个蓝图。
  3. 定义了一个基础模型,它允许你将Python对象定义为SQLAlchemy ORM对象。
  4. 创建一个方法get_db ,每当你需要访问数据库时就会执行。这个方法从你先前定义的蓝图中实例化一个会话,并在你用完后关闭它,这样你就不会有未使用的会话到处乱放。

接下来,定义一个Place数据库的SQLAlchemy模型以及一个创建表的指令。

get_db() 方法的下面添加以下代码:

class DBPlace(Base):
    __tablename__ = 'places'

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String(50))
    description = Column(String, nullable=True)
    coffee = Column(Boolean)
    wifi = Column(Boolean)
    food = Column(Boolean)
    lat = Column(Float)
    lng = Column(Float)

Base.metadata.create_all(bind=engine)

你刚刚定义了一个对象,它将被用来实际获取和插入数据库中的行。

注意:敏锐的读者可能会注意到,这个模型看起来很像你前面已经定义的PydanticPlace 模型。你不是在重复自己吗?的确,有很多框架都避免了这种双重定义。

然而,多年来,后端工程师们集体了解到,很少有存储在数据库中的数据与呈现给用户的所需表现形式完全一致。

以一个User 对象为例。你可以定义一次这个模型,然后用它来生成JSON,发送到你的端点,供用户使用。但它可能包含一个哈希密码、管理标志和其他你不希望暴露的敏感信息,或者在对象创建时没有提供。

因此,在某个地方,比如序列化器,你仍然需要创建一些一对一映射的例外情况。这种情况非常普遍,所以将数据库表示法与 "模式 "表示法完全解耦更有意义,即使这意味着有时要重复自己的工作

接下来,你应该定义一些方法,从数据库中插入和获取位置。

添加以下代码 就在你的class Place(BaseModel): 类之后:

def get_place(db: Session, place_id: int):
    return db.query(DBPlace).where(DBPlace.id == place_id).first()

def get_places(db: Session):
    return db.query(DBPlace).all()

def create_place(db: Session, place: Place):
    db_place = DBPlace(**place.dict())
    db.add(db_place)
    db.commit()
    db.refresh(db_place)

    return db_place

这三个方法负责获取一个Place ,获取所有的Places,并创建一个新的Place

第一个参数总是db :它的类型是一个SQLAlchemy会话。其余的参数取决于你要做什么。

  1. 对于检索一个地方,你只需要place_id
  2. 对于创建一个地方,你需要整个PydanticPlace 模型,这样你就可以从中创建一条记录。
  3. 对于检索所有的地方,你不需要任何更多的信息,你只需要返回数据库中所有的Places。

最后,你应该定义一些路由来执行我们需要的动作。

用下面的代码替换你之前创建的create_place_view 方法

@app.post('/places/', response_model=Place)
def create_places_view(place: Place, db: Session = Depends(get_db)):
    db_place = create_place(db, place)
    return db_place

@app.get('/places/', response_model=List[Place])
def get_places_view(db: Session = Depends(get_db)):
    return get_places(db)

@app.get('/place/{place_id}')
def get_place_view(place_id: int, db: Session = Depends(get_db)):
    return get_place(db, place_id)

注意,create_places_viewget_places_view 方法都是从同一个端点调用的:/places/ 。在这种情况下,HTTP方法决定了哪个函数被调用。HTTP方法就像动词:它们传达了客户端的意图。当一个GET 请求被发送时,get_places_view 被调用,因为我们想获得资源。反之,当一个POST 请求被发送时,create_places_view 被调用,因为我们想发布(就像在邮件中发布信件)资源。为你的行动使用正确的HTTP方法是很重要的。

全部加起来,你的main.py 文件应该是这样的:

from fastapi import FastAPI, Depends
from pydantic import BaseModel
from typing import Optional, List
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base, sessionmaker, Session
from sqlalchemy import Boolean, Column, Float, String, Integer

app = FastAPI()

# SqlAlchemy Setup
SQLALCHEMY_DATABASE_URL = 'sqlite+pysqlite:///./db.sqlite3:'
engine = create_engine(SQLALCHEMY_DATABASE_URL, echo=True, future=True)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

# A SQLAlchemny ORM Place
class DBPlace(Base):
    __tablename__ = 'places'

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String(50))
    description = Column(String, nullable=True)
    coffee = Column(Boolean)
    wifi = Column(Boolean)
    food = Column(Boolean)
    lat = Column(Float)
    lng = Column(Float)

Base.metadata.create_all(bind=engine)

# A Pydantic Place
class Place(BaseModel):
    name: str
    description: Optional[str] = None
    coffee: bool
    wifi: bool
    food: bool
    lat: float
    lng: float

    class Config:
        orm_mode = True

# Methods for interacting with the database
def get_place(db: Session, place_id: int):
    return db.query(DBPlace).where(DBPlace.id == place_id).first()

def get_places(db: Session):
    return db.query(DBPlace).all()

def create_place(db: Session, place: Place):
    db_place = DBPlace(**place.dict())
    db.add(db_place)
    db.commit()
    db.refresh(db_place)

    return db_place

# Routes for interacting with the API
@app.post('/places/', response_model=Place)
def create_places_view(place: Place, db: Session = Depends(get_db)):
    db_place = create_place(db, place)
    return db_place

@app.get('/places/', response_model=List[Place])
def get_places_view(db: Session = Depends(get_db)):
    return get_places(db)

@app.get('/place/{place_id}')
def get_place_view(place_id: int, db: Session = Depends(get_db)):
    return get_place(db, place_id)

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

这段代码可以分成五个不同的部分(导入后):

  1. SQLAlchemy的设置代码,用于数据库连接和初始化。
  2. 一个SQLAlchemy ORMPlace 定义,在向数据库获取和插入记录时使用。
  3. 一个PydanticPlace 定义,用于从用户那里接收数据以及向用户发送数据。
  4. 与数据库进行交互的特定方法。
  5. 与API互动的路线,与在Place 上执行的操作相对应。

在你的浏览器中打开自动生成的文档,你应该看到这些新闻端点被列出。你也可以与它们互动。尝试使用 "发布/places/"端点创建一些地方。一旦完成,使用 "Get /places/"端点来检索它们。

结论和总结

在这个迷你的Python FastAPI课程中,你:

  • 学习了使用Python FastAPI项目的基础知识,包括FastAPI的最佳实践。
  • 为远程工作地点创建了一个CRUD应用程序。
  • 将一个数据库与你的应用程序集成。

祝贺您!您现在拥有一个功能齐全的Python FastAPI项目。你现在有了一个功能齐全的Python FastAPI,为远程工作地点的数据库提供服务。为了使这个项目更进一步,成为现实世界的CodingNomads可以使用的东西,你可能想为你的API创建一个客户端:一个网站、一个手机应用,甚至是另一个API!