Python 现代开发学习笔记

(十九)服务启动与请求校验

1. 启动时加载一次模型

# 项目文件:model_project/src/modeldelivery/web.py
from contextlib import asynccontextmanager
import os
from pathlib import Path

from fastapi import FastAPI
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field

from .predict import Predictor
from .telemetry import install_logging


class Input(BaseModel):
    minutes: float = Field(gt=0, le=1440, allow_inf_nan=False)
    sessions: int = Field(ge=1, le=14)


class Output(BaseModel):
    pages: float
    version: str


def create_app(root=None, version=None):
    root = Path(root or os.getenv("MODEL_ROOT", "models"))
    selected = version or os.getenv("MODEL_VERSION")

    @asynccontextmanager
    async def lifespan(app):
        # 完整性检查和模型装载都在服务接收请求之前完成。
        app.state.predictor = Predictor(root, selected)
        yield

    app = FastAPI(title="阅读页数预测交付", lifespan=lifespan)
    install_logging(app, "modeldelivery")

    @app.exception_handler(RequestValidationError)
    async def invalid(request, error):
        detail = [
            {key: item[key] for key in ["type", "loc", "msg"]}
            for item in error.errors()
        ]
        return JSONResponse({"detail": detail}, status_code=422)

lifespan 在工作进程开始接收请求前创建 Predictor,放进 app.state。权重缺失、摘要不同或结构不匹配时,这个进程启动失败,先修复模型目录再启动。训练由 model-lab train 单独执行,预测路由使用已经准备好的对象。

输入模型沿用第十三章的 Pydantic。分钟数是有限正数,单条记录最多一天;次数为 1 到 14 的整数。字段错误由 422 响应指出位置。自定义异常处理器只整理错误类型、位置和消息,避免把不合格数值原样放进 JSON 响应。

2. 返回预测与加载版本

# 续写同一文件:model_project/src/modeldelivery/web.py
    # 路由使用启动时的对象;响应标明它真正加载的版本。
    @app.post("/predict", response_model=Output)
    def predict(body: Input):
        predictor = app.state.predictor
        return {
            "pages": predictor.pages(body.minutes, body.sessions),
            "version": predictor.version,
        }

    @app.get("/model")
    def metadata():
        predictor = app.state.predictor
        return {
            "version": predictor.version,
            "features": predictor.metadata["features"],
            "source_hash": predictor.metadata["source_hash"],
        }

    @app.get("/health/live")
    def live():
        return {"status": "ok"}

    @app.get("/health/ready")
    def ready():
        return {"status": "ready", "version": app.state.predictor.version}

    return app

每次响应返回 version,表示当前对象实际使用的权重,而非此刻磁盘指向的名字。/model 还能查看特征顺序和数据摘要;两个健康接口分别用于存活与就绪检查,后面会结合部署继续核对。

先由工厂构造应用,再进入生命周期核对文件并装载,成功后才接收经过校验的请求

笔记正文与全部题解 · Ctrl / ⌘ K