零基础入门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_bp → ping。如果把 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/ping。KeyError: APP_VERSION 表示配置类没有加载;RuntimeError: The session is unavailable 通常是 SECRET_KEY 为空;404 则先看前缀是否重复(例如同时在总路由和蓝图写了 /api)。
十、本篇练习
给配置增加 LOG_LEVEL 和 JSON_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