使用Python的高性能应用程序 - FastAPI教程

568 阅读9分钟

使用Python的高性能应用程序 - FastAPI教程

好的编程语言框架可以让我们更快生产出高质量的产品。优秀的框架甚至可以使整个开发体验变得愉快。FastAPI是一个新的Python网络框架,功能强大,使用起来也很愉快。以下特点使FastAPI值得尝试。

  • 速度。FastAPI是最快的Python网络框架之一。事实上,它的速度与Node.js和Go相当。查看这些性能测试。
  • 详细且易于使用的开发者文档
  • 输入暗示你的代码,并获得免费的数据验证和转换。
  • 使用依赖性注入轻松创建插件。

构建一个TODO应用程序

为了探索FastAPI背后的大思想,让我们建立一个TODO应用程序,它为用户设置待办事项列表。我们的小应用程序将提供以下功能。

  • 注册和登录
  • 添加新的TODO项目
  • 获取所有TODO的列表
  • 删除/更新一个TODO项目

数据模型的SQLAlchemy

我们的应用程序只有两个模型。用户和TODO。在SQLAlchemy(Python的数据库工具包)的帮助下,我们可以像这样表达我们的模型。

class User(Base):
   __tablename__ = "users"
   id = Column(Integer, primary_key=True, index=True)
   lname = Column(String)
   fname = Column(String)
   email = Column(String, unique=True, index=True)
   todos = relationship("TODO", back_populates="owner", cascade="all, delete-orphan")
 
class TODO(Base):
   __tablename__ = "todos"
   id = Column(Integer, primary_key=True, index=True)
   text = Column(String, index=True)
   completed = Column(Boolean, default=False)
   owner_id = Column(Integer, ForeignKey("users.id"))
   owner = relationship("User", back_populates="todos")

一旦我们的模型准备好了,让我们为SQLAlchemy编写配置文件,这样它就知道如何与数据库建立连接。

import os
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
SQLALCHEMY_DATABASE_URL = os.environ['SQLALCHEMY_DATABASE_URL']
engine = create_engine(
   SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

释放类型提示的力量

任何API项目都有一个相当大的部分涉及到数据验证和转换等常规工作。在我们编写请求处理程序之前,让我们先解决这个问题。有了FastAPI,我们使用pydantic模型来表达我们传入/传出数据的模式,然后使用这些pydantic模型进行类型提示,并享受免费的数据验证和转换。请注意,这些模型与我们的数据库工作流程无关,只是指定了流入和流出我们的REST接口的数据的形状。为了编写pydantic模型,请思考用户和TODO信息将以何种方式流入和流出。

传统上,一个新用户会注册我们的TODO服务,而一个现有用户会登录。这两种互动都是处理用户信息的,但数据的形状会有所不同。在注册时,我们需要用户提供更多的信息,而在登录时则需要最少的信息(只有电子邮件和密码)。这意味着我们需要两个pydantic模型来表达这两种不同形状的用户信息。

然而,在我们的TODO应用中,我们将利用FastAPI中内置的OAuth2支持,以实现基于JSON Web Tokens(JWT)的登录流程。我们只需要在这里定义一个UserCreate 模式,以指定将流入我们的注册端点的数据,以及一个UserBase 模式,以便在注册过程成功后作为响应返回。

from pydantic import BaseModel
from pydantic import EmailStr
class UserBase(BaseModel):
   email: EmailStr
class UserCreate(UserBase):
   lname: str
   fname: str
   password: str

在这里,我们将姓氏、名字和密码标记为一个字符串,但是可以通过使用pydantic约束字符串来进一步收紧,该字符串可以实现最小长度、最大长度和重码等检查。

为了支持TODO项目的创建和列出,我们定义了以下模式。

class TODOCreate(BaseModel):
   text: str
   completed: bool

为了支持现有TODO项目的更新,我们定义了另一个模式。

class TODOUpdate(TODOCreate):
   id: int

这样,我们就完成了对所有数据交换模式的定义。我们现在把注意力转向请求处理程序,这些模式将被用来免费完成所有繁重的数据转换和验证工作。

让用户注册

首先,让我们允许用户注册,因为我们所有的服务都需要由一个经过认证的用户来访问。我们使用上面定义的UserCreateUserBase 模式编写我们的第一个请求处理程序。

@app.post("/api/users", response_model=schemas.User)
def signup(user_data: schemas.UserCreate, db: Session = Depends(get_db)):
   """add new user"""
   user = crud.get_user_by_email(db, user_data.email)
   if user:
   	raise HTTPException(status_code=409,
   	                    detail="Email already registered.")
   signedup_user = crud.create_user(db, user_data)
   return signedup_user

在这一小段代码中,有很多事情要做。我们使用了一个装饰器来指定HTTP动词、URI和成功响应的模式。为了确保用户提交了正确的数据,我们用早先定义的UserCreate 模式对请求体进行了输入提示。该方法定义了另一个参数,用于获取数据库的控制权--这就是依赖性注入的作用,将在本教程的后面讨论。

确保我们的API安全

我们希望在我们的应用程序中具备以下安全功能:

  • 密码散列
  • 基于JWT的认证

对于密码散列,我们可以使用Passlib。让我们来定义处理密码散列和检查密码是否正确的函数。

from passlib.context import CryptContext
 
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
 
def verify_password(plain_password, hashed_password):
   return pwd_context.verify(plain_password, hashed_password)
 
 
def get_password_hash(password):
   return pwd_context.hash(password)
 
def authenticate_user(db, email: str, password: str):
   user = crud.get_user_by_email(db, email)
   if not user:
   	return False
   if not verify_password(password, user.hashed_password):
   	return False
   return user

为了启用基于JWT的认证,我们需要生成JWT,并对其进行解码以获得用户证书。我们定义以下函数来提供这个功能。

# install PyJWT
import jwt
from fastapi.security import OAuth2PasswordBearer
 
SECRET_KEY = os.environ['SECRET_KEY']
ALGORITHM = os.environ['ALGORITHM']
 
def create_access_token(*, data: dict, expires_delta: timedelta = None):
   to_encode = data.copy()
   if expires_delta:
   	expire = datetime.utcnow() + expires_delta
   else:
   	expire = datetime.utcnow() + timedelta(minutes=15)
   to_encode.update({"exp": expire})
   encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
   return encoded_jwt
def decode_access_token(db, token):
   credentials_exception = HTTPException(
   	status_code=HTTP_401_UNAUTHORIZED,
   	detail="Could not validate credentials",
   	headers={"WWW-Authenticate": "Bearer"},
   )
   try:
   	payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
   	email: str = payload.get("sub")
   	if email is None:
       	raise credentials_exception
   	token_data = schemas.TokenData(email=email)
   except PyJWTError:
   	raise credentials_exception
   user = crud.get_user_by_email(db, email=token_data.email)
   if user is None:
   	raise credentials_exception
   return user

在成功登录时发行令牌

现在,我们将定义一个登录端点并实现OAuth2密码流。这个端点将收到一个电子邮件和密码。我们将根据数据库检查凭证,一旦成功,就向用户发出一个JSON网络令牌。

为了接收凭证,我们将使用OAuth2PasswordRequestForm ,它是FastAPI安全工具的一部分。

@app.post("/api/token", response_model=schemas.Token)
def login_for_access_token(db: Session = Depends(get_db),
                      	form_data: OAuth2PasswordRequestForm = Depends()):
   """generate access token for valid credentials"""
   user = authenticate_user(db, form_data.username, form_data.password)
   if not user:
   	raise HTTPException(
       	status_code=HTTP_401_UNAUTHORIZED,
       	detail="Incorrect email or password",
       	headers={"WWW-Authenticate": "Bearer"},
   	)
   access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
   access_token = create_access_token(data={"sub": user.email},
                                  	expires_delta=access_token_expires)
   return {"access_token": access_token, "token_type": "bearer"}

使用依赖注入来访问数据库和保护端点

我们已经设置了登录端点,在用户成功登录后向其提供JWT。用户可以在本地存储中保存这个令牌,并将其作为授权头显示给我们的后端。那些期望只有登录用户才能访问的端点可以解码该令牌,并找出请求者是谁。这种工作并不局限于某个特定的端点,而是在所有受保护的端点中利用的共享逻辑。最好将令牌解码逻辑设置为一个依赖关系,可以在任何请求处理程序中使用。

用FastAPI的话说,我们的路径操作函数(请求处理程序)将依赖于get_current_userget_current_user 依赖关系需要有一个与数据库的连接,并钩住FastAPI的OAuth2PasswordBearer 逻辑以获得令牌。我们将通过使get_current_user 依赖于其他函数来解决这个问题。这样,我们可以定义依赖链,这是一个非常强大的概念。

def get_db():
   """provide db session to path operation functions"""
   try:
   	db = SessionLocal()
   	yield db
   finally:
   	db.close()
def get_current_user(db: Session = Depends(get_db),
                	token: str = Depends(oauth2_scheme)):
   return decode_access_token(db, token)
@app.get("/api/me", response_model=schemas.User)
def read_logged_in_user(current_user: models.User = Depends(get_current_user)):
   """return user settings for current user"""
   return current_user

登录的用户可以CRUD TODO

在我们编写TODO创建、读取、更新、删除(CRUD)的路径操作函数之前,我们定义了以下辅助函数来对数据库进行实际的CRUD。

def create_todo(db: Session, current_user: models.User, todo_data: schemas.TODOCreate):
   todo = models.TODO(text=todo_data.text,
                   	completed=todo_data.completed)
   todo.owner = current_user
   db.add(todo)
   db.commit()
   db.refresh(todo)
   return todo
def update_todo(db: Session, todo_data: schemas.TODOUpdate):
   todo = db.query(models.TODO).filter(models.TODO.id == id).first()
   todo.text = todo_data.text
   todo.completed = todo.completed
   db.commit()
   db.refresh(todo)
   return todo
def delete_todo(db: Session, id: int):
   todo = db.query(models.TODO).filter(models.TODO.id == id).first()
   db.delete(todo)
   db.commit()
 
def get_user_todos(db: Session, userid: int):
   return db.query(models.TODO).filter(models.TODO.owner_id == userid).all()

这些数据库级的函数将在下面的REST端点中使用。

@app.get("/api/mytodos", response_model=List[schemas.TODO])
def get_own_todos(current_user: models.User = Depends(get_current_user),
             	db: Session = Depends(get_db)):
   """return a list of TODOs owned by current user"""
   todos = crud.get_user_todos(db, current_user.id)
   return todos
@app.post("/api/todos", response_model=schemas.TODO)
def add_a_todo(todo_data: schemas.TODOCreate,
          	current_user: models.User = Depends(get_current_user),
          	db: Session = Depends(get_db)):
   """add a TODO"""
   todo = crud.create_meal(db, current_user, meal_data)
   return todo
@app.put("/api/todos/{todo_id}", response_model=schemas.TODO)
def update_a_todo(todo_id: int,
             	todo_data: schemas.TODOUpdate,
             	current_user: models.User = Depends(get_current_user),
             	db: Session = Depends(get_db)):
   """update and return TODO for given id"""
   todo = crud.get_todo(db, todo_id)
   updated_todo = crud.update_todo(db, todo_id, todo_data)
   return updated_todo
@app.delete("/api/todos/{todo_id}")
def delete_a_meal(todo_id: int,
             	current_user: models.User = Depends(get_current_user),
             	db: Session = Depends(get_db)):
   """delete TODO of given id"""
   crud.delete_meal(db, todo_id)
   return {"detail": "TODO Deleted"}

编写测试

让我们为我们的TODO API写一些测试。FastAPI提供了一个基于流行的Requests库的TestClient 类,我们可以用Pytest运行测试。

为了确保只有登录的用户才能创建TODO,我们可以这样写

from starlette.testclient import TestClient
from .main import app
client = TestClient(app)
def test_unauthenticated_user_cant_create_todos():   todo=dict(text="run a mile", completed=False)
response = client.post("/api/todos", data=todo)
assert response.status_code == 401

下面的测试检查我们的登录端点,如果呈现有效的登录凭证,就会生成一个JWT。

def test_user_can_obtain_auth_token():
  response = client.post("/api/token", data=good_credentials)
  assert response.status_code == 200
  assert 'access_token' in response.json()
  assert 'token_type' in response.json()

总结

我们已经完成了使用FastAPI实现一个非常简单的TODO应用程序。现在,你已经看到了类型提示的力量,它被很好地用于定义通过我们的REST接口传入和传出数据的形状。我们在一个地方定义模式,并把它留给FastAPI来应用数据验证和转换。另一个值得注意的功能是依赖性注入。我们用这个概念来包装获得数据库连接的共享逻辑,解码JWT以获得当前登录的用户,并通过密码和承载器实现简单的OAuth2。我们还看到了依赖关系是如何被串联起来的。

我们可以很容易地应用这个概念来增加诸如基于角色的访问等功能。此外,我们正在编写简明而强大的代码,而不需要学习框架的特殊性。简单地说,FastAPI是一个强大工具的集合,你不需要学习,因为它们只是现代Python。尽情享受吧。

了解基础知识

什么是FastAPI?

FastAPI是一个Python框架和一组工具,使开发者能够使用REST接口来调用常用的函数来实现应用程序。它通过REST API访问,调用应用程序的常用构建模块。在这个例子中,作者使用FastAPI来创建账户、登录和验证。

如何运行FastAPI?

像所有的REST接口一样,FastAPI从你的代码中被调用。它提供的功能有:对传入的数据进行类型提示、依赖性注入和认证,这样你就不必编写自己的函数。

FastAPI可以投入生产吗?

虽然是一个开源框架,但FastAPI是完全可以投入生产的,它有优秀的文档、支持和易于使用的界面。它可以用来构建和运行应用程序,其速度与用其他脚本语言编写的应用程序一样快。

Python是实现API接口的好语言吗?

如果该接口定义得很好,并且底层代码已经被优化,那么它就可以。文档声称FastAPI的性能与Node.js和Go一样好。