使用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
这样,我们就完成了对所有数据交换模式的定义。我们现在把注意力转向请求处理程序,这些模式将被用来免费完成所有繁重的数据转换和验证工作。
让用户注册
首先,让我们允许用户注册,因为我们所有的服务都需要由一个经过认证的用户来访问。我们使用上面定义的UserCreate 和UserBase 模式编写我们的第一个请求处理程序。
@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_user 。get_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一样好。