读后笔记 -- FastAPI 构建Python微服务 Chapter2:核心功能
2.2 构建和组织项目
2. 实现
注意!!!:本章 ch02_core_function 作为整个大项目的一个子包(package),正确启动,需要将 ch02_core_function 作为一个独立的 project 来启动。
2.2 具体实现
step1: 包内通过 APIRouter 构建 RestAPI
1 # content of ch02_core_function/admin/manager.py 2 from fastapi import APIRouter 3 from login.user import approved_users 4 5 router = APIRouter() 6 7 @router.get("/ch02/admin/tourists/list") 8 def list_all_tourists(): 9 return approved_users
step2: 再由 main.py 通过 FastAPI 实例调用并注册其所有 API 实现,具体如下:
1 # content of main.py 2 from fastapi import FastAPI 3 4 from admin import manager 5 from login import user 6 from places import destination 7 8 # step2: 使用 FastAPI 来创建和注册组件及包的模块 9 app = FastAPI() 10 11 # 使用 FastAPI 类的 include_router() 添加所有 router 并将它们注入框架中,使它们成为项目结构的一部分 12 app.include_router(manager.router) 13 app.include_router(user.router) 14 app.include_router(destination.router)
step3 运行应用程序
(cmd)> uvicorn main:app --reload
2.3 管理与 API 相关的异常
REST API 遇到 HTTPException 时,即会调用 FastAPI 的 Starlette 工具包派生的异常处理程序,返回默认的 JSON 响应。
1. 单个状态码响应:
1 @router.put("/ch02/admin/destination/update", status_code=status.HTTP_202_ACCEPTED) 2 def update_tour_destination(tour: Tour): 3 pass
2. 多个状态代码
1 @router.post("/ch02/admin/destination/add") 2 def add_tour_destination(input: TourInput): 3 try: 4 ... 5 return JSONResponse(content=tour_json, status_code=status.HTTP_201_CREATED) 6 except: 7 return JSONResponse(content={'message': 'invalid tour'}, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
3. 引发 HTTPException
1 from uuid import UUID 2 3 from fastapi import APIRouter, HTTPException 4 5 from login.user import approved_users 6 from places.destination import TourBasicInfo, tours 7 8 router = APIRouter() 9 10 @router.post("/ch02/tourist/tour/booking/add") 11 def create_booking(tour: TourBasicInfo, touristId: UUID): 12 if approved_users.get(touristId) is None: 13 raise HTTPException(status_code=500, 14 detail="details are missing") 15 ...
4. 自定义异常
1 from fastapi import HTTPException 2 3 class PostFeedbackException(HTTPException): 4 def __init__(self, detail: str, status_code: int): 5 self.status_code = status_code 6 self.detail = detail
1 @app.exception_handler(PostFeedbackException) 2 def feedback_exception_handler(req: Request, ex: PostFeedbackException): # 两个参数,Request & 自定义异常,第一个参数必须是 Request 3 return JSONResponse( 4 status_code=ex.status_code, 5 content={'message': f'error: {ex.detail}'} 6 )
1 @router.post("/feedback/add") 2 def post_tourist_feedback(touristId: UUID, tid: UUID, post: Post, bg_task: BackgroundTasks): 3 if approved_users.get(touristId) is None and tours.get(tid) is None: 4 raise PostFeedbackException(detail='tourist and tour details invalid', status_code=403) 5 ...
定义 handlder 的好处:一致性(格式、结构),定制化响应(错误信息、状态码、其他元数据等),集中处理、日志记录、友好的错误信息。
如上例中,不适用 handler,则接口的 返回 status_code 是400,msg 中是403,结构体是 string;使用 handler 后,接口返回的 status_code 是 403,返回结构体是 JSON 格式(handler 中有定义返回格式)
5. Exception 格式转换
# 处理异常情形5:step1: 自定义异常 class GetFeedbackException(HTTPException): def __init__(self, detail: str, status_code: int): self.status_code = status_code self.detail = detail
1 @app.exception_handler(GetFeedbackException) 2 def plain_text_exception_handler(req: Request, ex: GetFeedbackException) -> PlainTextResponse: 3 return PlainTextResponse(f'Error message: {ex.detail}', status_code=ex.status_code) # 由原来的 json 格式转换成 text
1 @router.get("/ch02/tourist/tour/booking/check") 2 def get_booking(): 3 raise GetFeedbackException(detail="get feedback exception", status_code=500)
2.4 将对象转换为 JSON 兼容的类型
1 from fastapi.encoders import jsonable_encoder 2 3 @router.post("/ch02/user/signup/") 4 def signup(signup: Signup) -> JSONResponse: 5 try: 6 userid = uuid1() 7 login = User(id=userid, username=signup.username, password=signup.password) 8 tourist = Tourist(id=userid, login=login, date_signed=datetime.now(), booked=0, tours=list()) 9 # 使用 jsonable_encoder() 将管理模型对象的所有属性 转换到 FastAPI 接受的 JSON 类型 10 tourist_json = jsonable_encoder(tourist) 11 pending_users[userid] = tourist_json 12 return JSONResponse(content=tourist_json, status_code=status.HTTP_201_CREATED) 13 except: 14 return JSONResponse(content={"message": "invalid operation"}, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
jsonable_encoder() 的介绍:
jsonable_encoder 是 FastAPI 中的一个工具函数,用于将复杂的数据类型(特别是那些基于 Pydantic 的模型或包含特殊 Python 类型的对象)转换为与 JSON 兼容的数据结构,如字典、列表、基本类型(如字符串、数字、布尔值等)。
确保在将数据发送给客户端或与其他 JSON 处理系统交互时,这些数据能够被有效地序列化为 JSON 格式。
以下是关于 jsonable_encoder 的一些关键点:
功能与用途:
转换复杂类型:将 Pydantic 模型、数据库模型或其他包含非 JSON 兼容类型(如 datetime 对象、UUID、自定义类型等)的对象转换为只包含 JSON 支持类型(如字典、列表、字符串、数字、布尔值)的结构。
内部使用:FastAPI 在默认情况下自动使用 jsonable_encoder 来处理路径操作函数的返回值,确保它们能够被序列化为 JSON 并正确地发送给客户端。
手动使用:在需要手动将复杂数据结构转换为 JSON 兼容形式的场景下,开发者可以直接调用 jsonable_encoder,例如在编写自定义响应逻辑、处理中间件或进行数据库存储时。
参数与行为:
obj: 要转换的对象,可以是任意类型,但通常为 Pydantic 模型实例或其他复杂数据结构。
include: 可选参数,用于传递给 Pydantic 模型的 include 参数,控制哪些属性应当被包含在序列化结果中。
by_alias: 可选参数,如果为 True,序列化时会使用模型定义中的别名(alias)而非原始字段名。
exclude_unset: 可选参数,如果为 True,未赋值的模型属性将不会出现在序列化结果中。
exclude_defaults: 可选参数,如果为 True,默认值未被覆盖的属性将不会出现在序列化结果中。
encoder: 可选参数,自定义 JSON 编码器,用于处理特定类型。
converters: 可选参数,自定义转换器字典,用于处理特定类型到 JSON 兼容类型的转换。
1 from fastapi.encoders import jsonable_encoder 2 from myapp.models import User 3 4 user = User(name="John Doe", email="john.doe@example.com", joined_at=datetime.now()) 5 6 # 将 User 模型实例转换为 JSON 兼容的字典 7 json_compatible_data = jsonable_encoder(user) 8 9 # 现在可以将 json_compatible_data 直接用于 JSON 序列化或存储到数据库等操作
注意事项:
jsonable_encoder 不直接生成 JSON 字符串,而是返回一个可以被标准的 JSON 序列化库(如 json.dumps())处理的字典或列表结构。
它不仅适用于 Pydantic 模型,还可以处理嵌套的复杂数据结构,递归地转换其中的所有非 JSON 兼容类型。
如果数据中包含自定义类型,可能需要提供自定义的 encoder 或 converters 参数来确保它们能够被正确地转换为 JSON 兼容形式。
总之,jsonable_encoder 是 FastAPI 中的一个重要工具,用于确保复杂数据类型能够顺利地被转换为 JSON 格式,便于在网络通信、数据持久化等场景中使用。开发者可以根据需要在应用程序的不同部分灵活运用这个函数来处理数据序列化问题。
除了 JSONResponse,FastAPI 的 Response 类可以创建响应标头,具体使用如下:
1 from fastapi import Response 2 from fastapi.encoders import jsonable_encoder 3 4 # 使用 JSONResponse 确保函数的返回类型 5 @router.get("/ch02/destinations/details/{id}") 6 def check_tour_profile(id: UUID) -> JSONResponse: 7 # jsonable_encoder 确保函数最后返回的结果是 JSON 类型 8 tour_info_json = jsonable_encoder(tours[id]) 9 return JSONResponse(content=tour_info_json) 10 11 # 使用 Response 来创建响应标头 12 @router.get("/ch02/destinations/mostbooked") 13 def check_recommended_tour(resp: Response): 14 resp.headers['X-Access-Tours'] = 'TryUs' 15 resp.headers['X-Contact-Details'] = '1900888TOLL' 16 resp.headers['Content-Language'] = 'en-US' 17 ranked_desc_rates = sorted(tours.items(), key=lambda x: x[1].ratings, reverse=True) 18 return ranked_desc_rates
2.6 创建后台进程
通常做法:在 API 服务方法的参数列表的末尾声明此操作,以便框架注入 BackgroundTask 实例。
应用场景:常作为后台处理的有:
- 日志记录
- 与 SMTP/FTP 相关的要求、事件
- 一些与数据库相关的触发器之类的事务
1 from datetime import datetime 2 3 4 def audit_log_transaction(touristId: str, message=""): 5 with open('audit_log.txt', mode='a') as logfile: 6 content = f"tourist {touristId} executed {message} at {datetime.now()}" 7 logfile.write(content)
1 @router.post("/ch02/user/login/") 2 def login(login: User, bg_task: BackgroundTasks) -> JSONResponse: 3 try: 4 signup_json = jsonable_encoder(approved_users[login.id]) 5 # 使用 bg_task 对象将 audit_log_transaction() 添加到框架以供稍后处理 6 bg_task.add_task(audit_log_transaction, touristId=str(login.id), message="login") 7 return JSONResponse(content=signup_json, status_code=status.HTTP_200_OK) 8 except: 9 return JSONResponse(content={'message': 'invalid operation'}, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
2.7 异步路径操作
异步 API 端点:
- 函数前面加上 "async",可以与主线程同时执行;
- 可以调用同步 和 异步 Python 函数,这些函数可以是 数据访问对象(data access object, DAO)、本机服务或实用程序;
- 可以使用 await 关键字调用异步非 API 操作,这会暂停该 API 操作,直到非 API 事务处理完一个承诺(promise);
# 网上给出 FastAPI 的 async 和 await 的更详细的介绍: FastAPI 是一个高性能的 Python web 框架,它基于 Starlette 和 Pydantic,并充分利用了 Python 3.6+ 中的异步编程特性,特别是 async 和 await 关键字。这些关键字是 Python 对异步编程的支持的核心部分,
允许开发者以非阻塞的方式处理并发请求,从而提高应用的性能和响应能力。下面对 async 和 await 在 FastAPI 中的应用进行详细解释: async async 是用于定义异步函数的关键字。当您在函数定义前加上 async def,就创建了一个 异步函数(或称为协程)。
异步函数与普通函数的主要区别在于,它们不会立即执行其内部代码,而是返回一个可等待的对象(通常是一个 coroutine 对象)。这个对象可以在事件循环中被调度,等待适当的时机(例如,当 I/O 操作完成时)继续执行。 异步函数内部可以包含 await 语句,用于暂停函数执行并等待另一个异步操作(通常是返回 Awaitable 对象,如 asyncio.Future 或 asyncio.Task,或者直接是一个 Promise)的结果。当 await 后面的操作完成时,函数会恢复执行。 async def fetch_data(): data = await some_async_api_call() return process_data(data)
await await 关键字用于在异步函数内部暂停当前协程的执行,直到紧跟其后的表达式(通常是一个异步操作的结果)变为可用。这通常涉及到 I/O 密集型操作,如网络请求、数据库查询、文件读写等,这些操作在等待期间不会阻塞其他任务的执行。 async def main(): response = await fetch_data_from_server() processed_data = await process_response(response) await store_data(processed_data) 在这个例子中,main 函数是异步的,它依次等待 fetch_data_from_server、process_response 和 store_data 这些异步操作完成。在等待期间,事件循环可以调度其他协程执行,实现并发处理。 在 FastAPI 中使用 async 和 await 在 FastAPI 应用中,通常会使用 async def 定义路由处理函数(路径操作函数),以便利用异步特性处理 HTTP 请求: @app.get("/items/") async def read_items(): items = await fetch_items_from_database() # 假设这是一个异步数据库查询 return {"items": items} 这里,read_items 为异步的,需要等待异步数据库操作的完成。使用 await 来调用 fetch_items_from_database,这样在等待数据库返回结果的同时,框架可以处理其他并发请求,提高了服务器资源的利用率和整体响应速度。 混合使用 def 和 async def FastAPI 允许在路径操作函数中混合使用 def(同步函数)和 async def(异步函数)。如果使用的第三方库不支持异步接口,或者某个操作本身并不适合异步化(如计算密集型任务),可以使用普通函数。
FastAPI 仍会以异步方式处理请求,但在处理这类同步函数时,它会将这些函数封装在一个异步任务中执行,以确保整个请求处理流程保持异步。
总结 在 FastAPI 中,async 和 await 关键字是实现高效并发和非阻塞性能的关键工具。通过定义异步函数并使用 await 来等待异步操作,您可以构建出能够充分利用 CPU 和 I/O 资源、快速响应客户端请求的 web 服务。
FastAPI 的设计旨在无缝集成异步编程模型,使得开发人员可以轻松利用这一特性提升应用程序的性能和扩展性。
注意:await 关键字只能用于 async REST API 和 本地事务。不能用于同步事务。
# uvicorn 线程池中添加更多线程 uvicorn main:app --workers 5 --reload
2.8 应用中间件以过滤路径操作
中间件(middleware):异步函数,可充当 REST API 服务的过滤器。
- 1)前处理:从 request 正文的 cookie、header、请求参数、查询参数、表单数据或身份验证信息中过滤出传入请求以进行验证、身份验证、日志记录、后台处理或内容生成 --- 在到达 API 服务方法之前过滤;
- 2)后处理:对传出的 response 正文进行格式更改、header 更新和添加、以及其他类型的转换操作 --- 在 response 到达客户端之前应用于 response;
实现步骤:
- 1. 创建 async 函数,有两个本地参数: Request 作为其参数以返回响应 + call_next() 函数
- 2. 使用 @app.middleware("http") 装饰该方法,以将组件注入框架
位置:
- 项目级别中实现,甚至是 main.py 的一部分;
中间件的网上介绍:
FastAPI 中间件是框架内用于对 HTTP 请求/响应生命周期进行拦截和自定义处理的一类特殊函数。它们提供了在请求到达具体路由处理器(即路径操作)之前执行某些操作,以及在响应返回给客户端之前添加额外逻辑的能力。
中间件允许开发者集中实现诸如身份验证、日志记录、性能监控、跨域资源共享(CORS)支持、请求/响应数据转换等通用任务,增强了应用程序的结构化和可维护性。 以下是关于 FastAPI 中间件的一些关键特性与用法:
定义与注册中间件 在 FastAPI 应用程序中,中间件通过装饰器 @app.middleware("http") 注册到应用实例上。例如:
1 from fastapi import FastAPI, Request 2 3 app = FastAPI() 4 5 @app.middleware("http") 6 async def custom_middleware(request: Request, call_next): 7 # 请求前的处理逻辑 8 start_time = time.time() 9 response = await call_next(request) 10 11 # 响应后的处理逻辑 12 process_time = time.time() - start_time 13 response.headers["X-Process-Time"] = str(process_time) 14 15 return response
在这个例子中,custom_middleware 函数接收两个参数: * request: 代表当前 HTTP 请求的对象,可以通过它访问请求头、查询参数、路径参数、请求体等信息。 * call_next: 一个异步可调用对象,当调用时,它会将请求传递给下一个中间件(如果存在)或最终的目标路径操作,并返回由路径操作生成的响应对象。
中间件执行流程 FastAPI 中间件按照注册顺序依次执行: 1. 当一个请求到达应用程序时,首先触发的是第一个注册的中间件。 2. 在该中间件中,可以进行预处理操作,如记录请求开始时间(如上述示例)。 3. 中间件通过调用 await call_next(request) 将控制权传递给下一个中间件或路径操作。 4. 当路径操作完成并生成响应后,控制流返回到当前中间件。 5. 此时,中间件可以在响应返回客户端之前执行后处理操作,如计算并添加处理耗时到响应头(如上述示例)。 6. 最后,控制流继续向上传递,经过剩余的中间件(如果有),直至响应最终发送给客户端。
中间件用途 中间件可用于实现多种实用功能,包括但不限于: 日志记录:记录请求的详细信息(如请求方法、URL、IP 地址、请求体等)以及响应状态码、响应时间等,以进行审计、监控或调试。 身份验证与授权:检查请求中的认证信息(如 JWT、API 密钥等),确保只有经过验证的用户才能访问受保护的资源。 性能监控:测量请求处理时间、追踪数据库查询或其他资源消耗,用于分析性能瓶颈或提供实时性能指标。 异常处理:捕获并处理路径操作中抛出的异常,统一返回格式化的错误响应,或者将异常信息记录到日志。 请求/响应数据转换:对请求数据进行预处理(如标准化、校验、脱敏)或对响应数据进行后处理(如序列化、压缩、添加元数据)。 跨域资源共享 (CORS):设置响应头以允许跨域请求,符合浏览器的安全策略。
中间件与依赖注入 虽然中间件提供了对全局请求/响应处理的强大能力,但在某些情况下,使用 FastAPI 的依赖注入机制(通过 Depends 装饰器)可能更为合适,特别是在需要根据具体路由或请求上下文动态调整行为时。
依赖注入更适合应用于更局部的场景,如特定路由的操作函数中,而中间件则适用于需要对所有或大部分请求进行统一处理的情况。 总结来说,FastAPI 中间件是一种强大的工具,允许开发者在不修改具体业务逻辑的情况下,对应用程序的请求处理流程进行扩展和定制,从而实现诸如日志记录、身份验证、性能监控等常见的非功能性需求。
通过合理利用中间件,可以提升代码的模块化程度,使核心业务代码保持简洁,并确保关键的基础设施功能得到一致且高效的实现。
FastAPI 框架内置的中间件:
- GzipMiddleware:处理在 Accept-Encoding 标头中包含 gzip 的任何请求的 GZip 响应;
- ServerErrorMiddleware:处理服务器错误;
- TrustedHostMiddleware:强制所有传入请求都具有正确设置的 Host 标头,以防 HTTP 主机标头被攻击;
- ExceptionMiddleware:异常处理中间件;
- CORSMiddleware:跨域资源共享中间件;
- SessionMiddleware:会话处理中间件;
- HTTPSRedirectionMiddleware:强制所有传入请求必须是 http 或 wss;