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

零基础入门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) 或经过掩码处理的主机名。本项目下一步会加入用户表,因此本篇的健康检查故意不查询业务表,让它只回答"进程是否活着"。

九、本篇练习(提交前自检)

  1. 新增 GET /version,返回 {"version": "0.1.0", "python": "3.11"},并在 tests/test_app.py 验证字段类型。
  2. create_app 增加 instance/ 目录支持,把 SQLite 文件放入 instance,而不是源码目录。
  3. 故意将蓝图注册写两次,观察 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
相关推荐
朦胧之1 小时前
Python 后端核心知识
python
denggun123451 小时前
yield
前端·数据库·python
zx_741484812 小时前
【Python 入门】面向对象基础:类、对象、成员变量与构造方法
开发语言·python
招财小梗3 小时前
沈阳商贸公司AI企业服务优势几何?
大数据·人工智能·python
ZISHU_9874 小时前
用 TLabel 给 SynTouch BioTac 数据做语义标注:从原始信号到结构化标注
开发语言·人工智能·python·数据·机器人触觉
m0_617493945 小时前
SSLError [ASN1: NOT_ENOUGH_DATA] 问题排查与解决指南
python·ssl
whcyhhh5 小时前
头歌实践教学平台:数据科学与大数据技术导论(七上)
大数据·数据库·python
ServBay5 小时前
AI 工程师必备的 9 个 Python 库,从数据验证到模型优化
后端·python·ai编程
AC赳赳老秦6 小时前
企业级合规审计体系:用 OpenClaw 落地采集全链路留痕,自动生成合规审计报告
java·python·django·beautifulsoup·php·deepseek·openclaw