零基础入门python19:Flask账本第一步------应用工厂、蓝图和健康检查

一、上一篇课后练习讲解
Python 项目练习是给任务管理器增加优先级和截止日期。参考做法是先修改模型和 JSON 兼容读取,再更新 CLI;这提醒我们:后端项目也应该先定义数据边界,再组织入口和业务模块。
上一篇课后练习完整答案
上一篇要求增加 delete_article,管理员可以删除任意文章,普通用户只能删除自己的文章。下面这份文件可以直接保存为 permission_demo.py 后运行;它用内存列表模拟数据库,先做权限判断再删除,失败路径不会改变数据。
python
from dataclasses import dataclass
@dataclass
class Article:
id: int
author_id: int
title: str
def delete_article(articles: list[Article], article_id: int, actor_id: int, is_admin: bool) -> bool:
"""删除文章并返回是否成功;查不到或无权时保持原列表不变。"""
position = next((i for i, article in enumerate(articles) if article.id == article_id), None)
if position is None:
return False
article = articles[position]
if not is_admin and article.author_id != actor_id:
return False
articles.pop(position)
return True
def run_checks() -> None:
articles = [Article(1, 10, "作者的文章"), Article(2, 20, "别人的文章")]
assert delete_article(articles, 1, actor_id=10, is_admin=False) is True
assert [article.id for article in articles] == [2]
snapshot = list(articles)
assert delete_article(articles, 2, actor_id=10, is_admin=False) is False
assert articles == snapshot
assert delete_article(articles, 2, actor_id=99, is_admin=True) is True
assert articles == []
print("permission checks passed")
if __name__ == "__main__":
run_checks()
运行 python permission_demo.py,预期输出 permission checks passed。真实项目把 pop 换成事务中的 DELETE,并在查询时同时带上资源 id 和当前用户条件;管理员分支必须经过单独的角色判断,不能让客户端传 is_admin=true 自行提权。
二、本篇完成什么
创建 Flask 个人账本 API 的最小骨架,跑通 /health,并理解为什么不把所有代码写进一个 app.py。

三、应用工厂
python
# app/__init__.py
from flask import Flask
from .extensions import db, login_manager
def create_app(test_config: dict | None = None) -> Flask:
"""创建一个独立应用实例,测试可以传入临时数据库配置。"""
app = Flask(__name__)
app.config.from_mapping(
SECRET_KEY='dev-only-change-me',
SQLALCHEMY_DATABASE_URI='sqlite:///ledger.db',
)
if test_config:
app.config.update(test_config)
db.init_app(app)
login_manager.init_app(app)
from .ledger import ledger_bp
app.register_blueprint(ledger_bp)
@app.get('/health')
def health():
return {'status': 'ok', 'service': 'flask-ledger'}
return app
应用工厂解决两个实际问题:同一个代码可以创建开发、测试和生产实例;导入模块时不会立刻连接生产数据库。db.init_app 延迟绑定扩展,避免循环导入。
四、运行与测试
powershell
python -m venv .venv
.venv\\Scripts\\activate
pip install -r requirements.txt
python -c "from app import create_app; print(create_app().url_map)"
python -m pytest -q
预期健康检查返回 200,测试通过。若出现 Working outside of application context,说明数据库操作发生在 Flask 应用上下文之外;下一篇会专门解释上下文和扩展。
五、课后练习
增加 /ready 接口,执行一次 SELECT 1 检查数据库,并为数据库不可用时的响应写测试。下一篇会把配置、扩展和蓝图进一步拆开。
项目增量:应用工厂验收
本篇让测试、开发和生产调用同一个 create_app。测试配置覆盖数据库 URI 和密钥,导入模块时不连接生产数据库。
python
def create_app(test_config=None):
app = Flask(__name__)
app.config.from_mapping(SECRET_KEY='change-me', SQLALCHEMY_DATABASE_URI='sqlite:///ledger.db')
if test_config:
app.config.update(test_config)
db.init_app(app)
register_blueprints(app)
return app
验收健康检查 200、测试配置生效、应用上下文外访问数据库得到明确错误。课后练习增加 /ready 并测试数据库不可用场景。
六、把"能启动"变成"可继续开发"的骨架
刚才的代码看起来很少,但它决定了后面十一个章节的接入方式。create_app 是唯一的装配点:配置在这里加载,扩展在这里绑定,蓝图在这里注册。业务模块只声明路由和函数,不偷偷创建第二个 Flask()。这样做的直接收益是,测试可以创建一份内存数据库,生产启动时则读取环境变量。
在项目目录执行下面的命令,先把目录建出来(如果目录已经存在,只需要核对文件内容):
powershell
mkdir app, tests
ni app\__init__.py, app\extensions.py, app\ledger.py, tests\conftest.py -ItemType File
app/extensions.py 只放"还没有绑定到应用"的对象:
python
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
db = SQLAlchemy()
login_manager = LoginManager()
def init_extensions(app):
"""在工厂中集中绑定第三方扩展,避免模块导入时产生循环依赖。"""
db.init_app(app)
login_manager.init_app(app)
init_extensions 看似只是两行调用,实际是生命周期边界。若把 db = SQLAlchemy(app) 写在模块顶层,pytest 导入测试文件时就会创建真实数据库连接;连接失败会让连健康检查都无法执行。把绑定延迟到工厂后,create_app({"TESTING": True, ...}) 就能在每个测试中得到干净实例。
七、用测试客户端验证装配,而不是手工猜
python
# tests/conftest.py
import pytest
from app import create_app
@pytest.fixture()
def client(tmp_path):
db_file = tmp_path / "test.sqlite"
app = create_app({
"TESTING": True,
"SQLALCHEMY_DATABASE_URI": f"sqlite:///{db_file}",
})
with app.app_context():
from app.extensions import db
db.create_all()
return app.test_client()
def test_health(client):
response = client.get("/health")
assert response.status_code == 200
assert response.get_json()["service"] == "flask-ledger"
运行 python -m pytest -q 时,预期输出类似 1 passed in 0.3s。如果出现 sqlite3.OperationalError: unable to open database file,先打印 db_file 的绝对路径,确认父目录已经由 tmp_path 创建;不要把数据库路径硬编码到项目根目录,否则多次运行测试会互相污染。
八、调试时应该观察什么
flask --app run.py routes 应看到 /health。如果路由表为空,问题在 register_blueprint 之前;如果出现重复端点名,通常是同一个蓝图被注册两次。调试配置时可临时执行:
python
app = create_app({"TESTING": True})
print(app.config["SQLALCHEMY_DATABASE_URI"])
print([(rule.rule, sorted(rule.methods)) for rule in app.url_map.iter_rules()])
不要把 SECRET_KEY 或数据库密码打印到日志;线上排查应只打印 bool(value) 或经过掩码处理的主机名。本项目下一步会加入用户表,因此本篇的健康检查故意不查询业务表,让它只回答"进程是否活着"。
九、本篇练习(提交前自检)
- 新增
GET /version,返回{"version": "0.1.0", "python": "3.11"},并在tests/test_app.py验证字段类型。 - 为
create_app增加instance/目录支持,把 SQLite 文件放入 instance,而不是源码目录。 - 故意将蓝图注册写两次,观察
flask routes的异常,再恢复为一次。
完成后保留命令输出截图或终端文本。下一篇会逐项讲解环境配置、扩展初始化和蓝图拆分,并把本篇的 /version 接口接入统一配置。
本篇结束:完整模块文件
下面是交付项目中真实存在的完整文件 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