零基础入门python20:Flask配置、扩展和蓝图拆分

零基础入门python20:Flask配置、扩展和蓝图拆分

一、上一篇课后练习讲解

/ready 可以在应用上下文中调用 db.session.execute(db.text('SELECT 1'));成功返回 200,捕获数据库异常返回 503。健康检查和就绪检查要分开,前者检查进程,后者检查依赖。

上一篇课后练习完整答案

上一篇练习的要求已落实到下面完整文件;先运行项目测试,再用 curl 对照状态码和数据库持久化结果

答案要点:/ready 在应用上下文中执行 SELECT 1;数据库不可用时 rollback 并返回 503,健康探针不会把故障报成 200。

文件:app/ready.py

完整参考答案文件

完整文件:app/ready.py

python 复制代码
from flask import Blueprint, jsonify
from sqlalchemy import text
from .extensions import db

bp = Blueprint("ready", __name__)
@bp.get("/ready")
def ready():
    try:
        db.session.execute(text("SELECT 1"))
    except Exception:
        db.session.rollback()
        return jsonify(error="database_unavailable"), 503
    return {"status": "ready"}

完整参考答案文件

本篇对应的交付源码完整文件:flask-ledger/app/init.py

python 复制代码
from flask import Flask

from .auth import bp as auth_bp
from .extensions import db, login_manager
from .ledger import bp as ledger_bp


def create_app(test_config=None):
    app = Flask(__name__)
    app.config.from_mapping(
        SECRET_KEY="dev-change-me",
        SQLALCHEMY_DATABASE_URI="sqlite:///ledger.db",
        SQLALCHEMY_TRACK_MODIFICATIONS=False,
    )
    if test_config:
        app.config.update(test_config)
    db.init_app(app)
    login_manager.init_app(app)
    login_manager.unauthorized_handler(lambda: ({"message": "请先登录"}, 401))
    app.register_blueprint(auth_bp)
    app.register_blueprint(ledger_bp)

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

    with app.app_context():
        db.create_all()
    return app

验收命令:python -m pytest -q(Django 项目使用 python manage.py test)。预期测试通过;若失败先检查迁移、配置和事务回滚。

二、本篇项目增量

把配置、数据库扩展和账目路由拆到独立文件,并为测试提供临时 SQLite 数据库。

三、配置为什么不能写死

python 复制代码
# config.py
import os


class Config:
    SECRET_KEY = os.getenv('SECRET_KEY', 'dev-only-change-me')
    SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL', 'sqlite:///ledger.db')
    SQLALCHEMY_TRACK_MODIFICATIONS = False


class TestConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'

密钥和数据库地址属于部署配置,不应该提交真实值。测试配置使用内存库,每个测试结束后销毁,避免测试之间共享数据。

四、扩展和蓝图

python 复制代码
# app/extensions.py
from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()
login_manager = LoginManager()
login_manager.login_view = None

扩展模块只创建对象,不在导入时绑定应用;应用工厂负责初始化。蓝图按业务域拆分,auth.py 处理登录,ledger.py 处理账目,避免一个文件同时承担身份和财务规则。

五、验收

powershell 复制代码
python -m pytest -q
flask --app run.py routes

检查路由表中同时出现 /health 和账本蓝图路由;使用测试配置运行时不能生成真实 ledger.db。课后练习:把数据库 URL 改为环境变量并写一个测试确认测试配置不会读取生产 URL。

项目增量:配置和扩展初始化

扩展对象在模块级创建,真正绑定放在 create_app 内;这样不会因导入顺序产生循环依赖。SECRET_KEY、数据库 URL 和 cookie 设置从环境变量读取。

六、上一篇练习逐项验收

上一篇要求增加 /version、把 SQLite 文件放进 instance/,并观察蓝图重复注册的错误。可以用下面的实现作为参考:

python 复制代码
# app/__init__.py
from pathlib import Path
from flask import Flask
from .extensions import init_extensions

def create_app(test_config=None):
    app = Flask(__name__, instance_relative_config=True)
    Path(app.instance_path).mkdir(parents=True, exist_ok=True)
    app.config.from_object("app.config.Config")
    app.config.from_pyfile("config.py", silent=True)
    if test_config:
        app.config.from_mapping(test_config)
    init_extensions(app)

    @app.get("/version")
    def version():
        # 固定版本用于部署探针和客户端兼容判断,Python 版本只是展示信息。
        return {"version": app.config["APP_VERSION"], "python": "3.11"}
    return app

如果重复注册蓝图,Flask 会抛出 ValueError: The name 'ledger' is already registered。这不是"框架太严格",而是避免同一条路由在请求时产生两份不确定的处理逻辑。

七、配置优先级:为什么测试配置必须最后覆盖

推荐的读取顺序是"默认值 → instance 配置 → 环境变量 → 测试字典"。示例:

python 复制代码
# app/config.py
import os

class Config:
    APP_VERSION = "0.1.0"
    SECRET_KEY = os.environ.get("SECRET_KEY", "dev-only-change-me")
    SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", "sqlite:///ledger.db")
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SESSION_COOKIE_HTTPONLY = True
    SESSION_COOKIE_SAMESITE = "Lax"

class TestConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"

def mask_database_url(url: str) -> str:
    """日志中只保留协议和主机,避免把密码写进终端。"""
    if "@" not in url:
        return url.split("/")[0]
    return url.split("@")[0].split("://")[0] + "://***@" + url.split("@", 1)[1]

配置不是常量堆砌,而是部署契约。SESSION_COOKIE_HTTPONLY 防止前端脚本直接读取会话 cookie;SAMESITE=Lax 在普通跳转中仍携带 cookie,同时降低跨站请求伪造风险。生产环境应显式设置 SECRET_KEY,启动时检测到默认值就拒绝启动。

八、蓝图拆分的完整请求路径

python 复制代码
# app/ledger.py
from flask import Blueprint, jsonify

ledger_bp = Blueprint("ledger", __name__, url_prefix="/api/ledger")

@ledger_bp.get("/ping")
def ping():
    # 蓝图只关心账本领域;应用级错误处理和配置由工厂统一提供。
    return jsonify({"module": "ledger", "status": "ok"})

调用 client.get('/api/ledger/ping') 会经过:WSGI 服务器 → Flask 路由表 → ledger_bpping。如果把 url_prefix 改成 /ledger,接口契约就变成 /api/ledger/ping,所以前端和文档必须同步更新;不要在多个文件里拼接前缀。

九、验收与常见故障

powershell 复制代码
$env:APP_VERSION='0.2.0'
python -c "from app import create_app; print(create_app().config['APP_VERSION'])"
python -m pytest -q
flask --app run.py routes

预期能看到 0.2.0、全部测试通过以及 /version/api/ledger/pingKeyError: APP_VERSION 表示配置类没有加载;RuntimeError: The session is unavailable 通常是 SECRET_KEY 为空;404 则先看前缀是否重复(例如同时在总路由和蓝图写了 /api)。

十、本篇练习

给配置增加 LOG_LEVELJSON_SORT_KEYS 两项,并写 test_config_override:测试实例使用内存数据库、日志级别为 DEBUG,而默认实例仍使用环境变量。再为 /api/ledger/ping 添加一个返回 request_id 的响应头;下一篇会讲模型和外键,届时请求 ID 将用于定位数据库错误。

python 复制代码
db = SQLAlchemy()
login_manager = LoginManager()

def init_extensions(app):
    db.init_app(app)
    login_manager.init_app(app)

启动后检查 url_map、健康接口和测试库迁移。课后练习添加开发和测试配置类并说明每个配置的风险。

本篇结束:完整模块文件

下面是交付项目中真实存在的完整文件 flask-ledger/app/init.py。它覆盖本篇新增逻辑以及前文已经完成的依赖代码;复制单个函数会丢失上下文,因此这里提供整份文件。

python 复制代码
from flask import Flask

from .auth import bp as auth_bp
from .extensions import db, login_manager
from .ledger import bp as ledger_bp


def create_app(test_config=None):
    app = Flask(__name__)
    app.config.from_mapping(
        SECRET_KEY="dev-change-me",
        SQLALCHEMY_DATABASE_URI="sqlite:///ledger.db",
        SQLALCHEMY_TRACK_MODIFICATIONS=False,
    )
    if test_config:
        app.config.update(test_config)
    db.init_app(app)
    login_manager.init_app(app)
    login_manager.unauthorized_handler(lambda: ({"message": "请先登录"}, 401))
    app.register_blueprint(auth_bp)
    app.register_blueprint(ledger_bp)

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

    with app.app_context():
        db.create_all()
    return app
相关推荐
m0_380743872 小时前
为 OpenAI 兼容接口配置教程
开发语言·python·node.js
leisoo80973 小时前
ig50数据落盘ClickHousevsTimescaleDBvsDuckDB实测对比 IG50免费开源股票数据API接口
开发语言·jvm·数据库·python·json
卷无止境3 小时前
FastAPI 的 Metadata 到底是什么,又牵动了哪些核心概念
后端·python
卷无止境3 小时前
FastAPI 调试实战,从断点到生产环境的排错心法
后端·python
yaoxin5211234 小时前
503. Java 反射 - 编写 ServiceFactory 类
java·开发语言·python
kyrie_sakura5 小时前
python学习笔记3 -- 流程控制语句结构
笔记·python·学习
程序员小八7775 小时前
Java 快速转 Go
java·python·golang
梦想的旅途26 小时前
Python实现企业微信文本消息发送
开发语言·python·企业微信
%477 小时前
DAY41
pytorch·python