Python 现代开发学习笔记

(二十一)初始化书目并启动个人书架

1. 准备一次,后续继续读取

初始化先生成一份随机签名密钥,保存后不覆盖;随后创建缺少的表,导入第九章实际采集的六本书。

# 项目文件:shelf_project/src/shelfweb/cli.py
import argparse
import os
from pathlib import Path
import secrets
from tempfile import NamedTemporaryFile

from . import create_app
from .db import init_db, seed_books


def provision_secret(location):
    location.mkdir(parents=True, exist_ok=True)
    destination = location / "secret.key"
    # 先完整写入临时文件,再发布文件名,其他启动过程读到完整的密钥。
    with NamedTemporaryFile(
        mode="w", encoding="utf-8", dir=location, prefix=".secret-", delete=False
    ) as stream:
        stream.write(secrets.token_hex(32))
        temporary = Path(stream.name)
    try:
        try:
            os.link(
                temporary, destination
            )  # 已有密钥时不覆盖,保留原 Cookie 的签名依据。
        except FileExistsError:
            pass
    finally:
        temporary.unlink()

NamedTemporaryFile 先在实例目录完整写好随机文本。os.link 给同一份文件内容建立最终名称,目标已存在时抛出 FileExistsError,代码保留已有密钥;最后删去临时名称。这让其他启动过程看到的是已经写好的内容,也让重复初始化继续使用旧签名依据。

# 续写同一文件:shelf_project/src/shelfweb/db.py
def seed_books(csv_path):
    connection = get_db()
    with connection:
        for name in ["alice", "bob"]:
            exists = connection.execute(
                "SELECT id FROM users WHERE username=?", (name,)
            ).fetchone()
            if exists is None:
                connection.execute(
                    "INSERT INTO users(username,password_hash) VALUES (?,?)",
                    (name, generate_password_hash(f"learn-{name}")),
                )
        owner_id = connection.execute(
            "SELECT id FROM users WHERE username=?", ("alice",)
        ).fetchone()["id"]
        with csv_path.open(encoding="utf-8-sig", newline="") as stream:
            for row in csv.DictReader(stream):
                # 同一账号、同一书名已存在时跳过,不覆盖读者已经修改的状态。
                exists = connection.execute(
                    "SELECT id FROM books WHERE owner_id=? AND title=?",
                    (owner_id, row["title"]),
                ).fetchone()
                if exists is None:
                    connection.execute(
                        "INSERT INTO books(owner_id,title,finished,tags) VALUES (?,?,?,?)",
                        (owner_id, row["title"], int(row["finished"]), row["tags"]),
                    )

账号查重后为 alice 导入书目,bob 保留空书架。书籍按用户和书名查重,已经存在就跳过,不覆盖用户之后修改的已读状态。with connection 结束时提交这一批变化。

# 续写同一文件:shelf_project/src/shelfweb/cli.py
def main():
    parser = argparse.ArgumentParser(description="初始化或运行 Flask 个人书架")
    parser.add_argument("command", choices=["init", "serve"])
    parser.add_argument("--csv", type=Path, default=Path("data/books.csv"))
    parser.add_argument("--port", type=int, default=8771)
    args = parser.parse_args()
    location = Path(os.getenv("SHELF_INSTANCE", "instance")).resolve()
    if args.command == "init":
        provision_secret(location)
        app = create_app(instance_path=location)
        with app.app_context():
            # 命令也复用 g 中的连接,离开上下文后由回调关闭。
            init_db()
            seed_books(args.csv)
        print("已准备账号、书目和本机签名密钥。")
    else:
        # 运行时沿用已经准备好的实例目录和签名密钥。
        create_app(instance_path=location).run(
            host="127.0.0.1", port=args.port, debug=False
        )

with app.app_context() 让命令复用 get_db,结束后也运行关闭连接的回调。启动和初始化使用相同的实例目录。

# 在 shelf_project 中准备账号、六本书和签名密钥。
uv run shelf-books init
# 启动本机服务,Ctrl+C 停止。
uv run shelf-books serve

打开 个人书架。演示账号为 alice / learn-alice、bob / learn-bob;也可以从页面注册自己的账号。第一次打开书架会跳到登录页,登录后显示自己的记录。

实际启动的 Flask 登录页,提供用户名与密码输入,并通过带 CSRF 的表单提交
实际登录后的个人书架,六本采集书目显示阅读状态、标签、筛选与修改入口

还可以在初始化之后使用 Flask 自带命令:

# --app 找到包中的 create_app 工厂;run 使用 Flask 的本机开发服务器。
uv run python -m flask --app shelfweb run --port 8771
# 只补建缺少的表;不清空现有数据。
uv run python -m flask --app shelfweb init-db

开发时的 --debug 会启用重载和交互调试器,可在自己的本机排查中使用。实际部署把这个工厂交给 WSGI 服务器,并配置 HTTPS、密钥与实例目录;请求处理逻辑继续复用这套应用。

初始化发布持久签名密钥后创建表并按书名查重,服务读取同一实例目录,重启后原 Cookie 及数据库登录记录继续对应到原书架

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