开发

一个最小化的 app

下面的工作完成后,项目文件夹结构将如下所示:

sample
├── app
│   ├── __init__.py
│   ├── main.py
│   └── user.py
├── config.toml
├── .gitignore
├── .env
├── pyape.toml
└── wsgi.py

使用 pyape init 创建项目后,我们需要给项目增加内容使其可以工作。 下面以创建一个在本地调试的 app 为例,描述项目搭建过程。

创建 app 文件夹,在其中加入 __init__.py 文件使其成为标准的 python 模块。

创建两个子模块 app/main.pyapp/user.py

main.py 的内容如下:

from flask import Blueprint

main = Blueprint('main', __name__)

@main.get('/')
def home():
    return 'HELLO WORLD

user.py 的内容如下:

from flask import Blueprint

user = Blueprint('user', __name__)

@user.get('/friend')
def friend():
    return 'HELLO MY FRIEND'

pyape.toml 中创建 local 环境,进行如下配置:

[ENV.local.'.env']
FLASK_ENV = 'development'
FLASK_RUN_PORT = 5001

[ENV.local.'config.toml'.PATH]
STATIC_URL_PATH = '/static'

[ENV.local.'config.toml'.PATH.modules]
main = ''
user = '/user'

使用 pyape config 生成配置文件,由于我们的目标是在本地环境调试运营, 因此只需要生成 config.toml.env

pyape config --env local --force config.toml .env

警告

在生成 config.toml 之前,在环境变量中必须包含 SAMPLE_LOCAL_SECRET_KEY 这个环境变量。详情请阅读 生成 SECRET_KEY

生成的 .env 文件内容为:

FLASK_APP = wsgi:sample_app
FLASK_ENV = development
FLASK_RUN_PORT = 5001

生成的 config.toml 文件内容为:

[FLASK]
SECRET_KEY = "HK-VHH4C0ijLjOYBYrO7L2ACsmxcx9UClph-Q8lu3Hk="

[SQLALCHEMY]
URI = "sqlite:///sample/sample.sqlite"

[PATH]
STATIC_URL_PATH = "/static"
STATIC_FOLDER = "dist"
TEMPLATE_FOLDER = "dist/template"

[SQLALCHEMY.ENGINE_OPTIONS]
pool_timeout = 10
pool_recycle = 3600

[PATH.modules]
main = ""
user = "/user"

参照 wsgi.py 对已有文件进行修改。

执行 flask run 启动开发服务器。

测试页面访问:

$ curl http://127.0.0.1:5001/
HELLO WORLD
$ curl http://127.0.0.1:5001/user/friend
HELLO MY FRIEND%

完整的 sample 项目请访问 sample

wsgi.py

wsgi.py 是 Flask 项目的入口文件。执行 pyape init 后,项目文件夹中会自动生成这个文件。 我们需要修改这个文件,使其符合我们自己项目的需要。

最简单的 wsgi.py 内容如下:

import pyape.app
from pyape.flask_extend import PyapeFlask

pyape_app: PyapeFlask = pyape.app.init()

备注

[‘.env’] 中要设置 FLASK_APP = wsgi:pyape_app

在使用 Gunicorn 部署时,要确保 [‘gunicorn.conf.py’] 中的 wsgi_app = 'wsgi:pyape_app'

在使用 uWSGI 部署时,要确保 [‘uwsgi.ini’] 中的 callable = 'wsgi:pyape_app'

wsgi.py 加强版

为了方便理解,我们可以做得更多一些。

导入必要的模块:

from pathlib import Path
from functools import partial

import pyape.app
import pyape.config
from pyape.flask_extend import PyapeFlask, PyapeRespons

明确指定主配置文件:

work_dir = Path(__file__).parent.resolve()
gconfig = pyape.config.GlobalConfig(work_dir, 'config.toml')

测试期间,可以用继承 PyapeRespone 的方式来实现跨域:

class CustomResponse(PyapeResponse):
    @property
    def cors_config(self):
        return PyapeResponse.CORS_DEFAUL

创建一个 app 实例,使用支持跨域的 Response:

pyape_app: PyapeFlask = pyape.app.init(gconfig, create_app, cls_config={'ResponseClass': CustomResponse})

加强版的完整内容 wsgi.py

from pathlib import Path

import pyape.app
import pyape.config
from pyape.flask_extend import PyapeFlask, PyapeRespons

work_dir = Path(__file__).parent.resolve()
gconfig = pyape.config.GlobalConfig(work_dir, 'config.toml')

class CustomResponse(PyapeResponse):
    @property
    def cors_config(self):
        return PyapeResponse.CORS_DEFAUL

pyape_app: PyapeFlask = pyape.app.init(gconfig, None, cls_config={'ResponseClass': CustomResponse})

wsgi.py 加加强版

基于加强版,可以做更多事。 例如增加可以在 flask shell 环境中调用的上下文方法。 以及对数据库进行初始化:

from pathlib import Path
from functools import partial

import pyape.app
import pyape.config
from pyape.flask_extend import PyapeFlask, PyapeRespons

work_dir = Path(__file__).parent.resolve()
gconfig = pyape.config.GlobalConfig(work_dir, 'config.toml')

class CustomResponse(PyapeResponse):
    @property
    def cors_config(self):
        return PyapeResponse.CORS_DEFAUL


def setup_app(pyape_app: PyapeFlask, **kwargs):
""" 初始化 app 项目,这个方法被嵌入 flask shell 上下文中执行,可以使用 kwargs 传递参数
"""
# 在这里可以进行数据库的初始化工作
# pyape_app._gdb.create_all()
return pyape_app


def create_app(pyape_app: PyapeFlask):
    """ 被 pyape.app.init 调用,用于处理 app 初始化
    """
    # 加入上下文处理器
    pyape_app.shell_context_processor(lambda: {
        'gdb': pyape_app._gdb,
        # 这里可以传递更多促使给 setup_app
        'setup': partial(setup_app, pyape_app),
    })
    pyape.app.logger.info(pyape_app.config)

pyape_app: PyapeFlask = pyape.app.init(gconfig, create_app, cls_config={'ResponseClass': CustomResponse})

使用 SQLAlchemy 操作数据库

在使用 wsgi.py 初始化框架的时候,数据库会自动创建。可以在 pyape.app.init_db 中找到创建代码:

def init_db(pyape_app: PyapeFlask):
""" 初始化 SQLAlchemy
"""
    sql_uri = pyape_app._gconf.getcfg('SQLALCHEMY', 'URI')
    if sql_uri is None:
        return
    global gdb
    if gdb is not None:
        raise ValueError('gdb 不能重复定义!')
    gdb = PyapeDB(app=pyape_app)
    pyape_app._gdb = gdb

备注

pyape.flask_extend.PyapeDBpyape.db.SQLAlchemy 的子类。

pyape 默认使用 SQLAlchemy 的 ORM 模式工作。让我们构建一个 Model 用于创建 Table。

创建子模块 app/model.py 用于 Table 定义。

# app/model.py
import time
from sqlalchemy import Column, INT, VARCHAR
from pyape.app import gdb

Model = gdb.Model()

class User(Model):
    __tablename__ = 'user'

    id = Column(INT, autoincrement=True, primary_key=True)
    name = Column(VARCHAR(100), nullable=False)
    createtime = Column(INT, nullable=False, default=lambda: int(time.time()))

在模块 app/user.py 中增加两个方法,用于读写数据库。

备注

涉及到 webargs 用法,请参考其 官方文档

SQLAlchemy 语法基于最新的 SQLAlchemy 2.0,请阅读 SQLAlchemy 2.0 Tutorial。 若你是 SQLAlchemy 1.x 用户,请阅读 Migrating to SQLAlchemy 2.0

from flask import Blueprint, abort
from webargs.flaskparser import use_args
from webargs import fields

from pyape.app import gdb, logger
from app.model import User


@user.get('/get')
@use_args({'id': fields.Int(required=True)}, location='query')
def get_user(args):
    id = args['id']
    userobj = gdb.session().get(User, id)
    if userobj is None:
        logger.warning(f'user {id} is not found.')
        abort(404)
    return f'User id: {userobj.id}, name: {userobj.name}'


@user.post('/set')
@use_args({'id': fields.Int(required=True), 'name': fields.Str(required=True)}, location='form')
def set_user(args):
    userobj = User(**args)
    gdb.session().add(userobj)
    gdb.session().commit()
    return f'User id: {userobj.id}, name: {userobj.name}'

测试 Sample 项目的 local 环境(单数据库支持)

若 pyape 项目位于 ~/storage/pyape

# 进入虚拟环境
cd ~/storage/pyape
python3 -m venv venv
source venv/bin/active

# 安装当前环境下的 pyape
(venv) pip install -e .

# 创建本地配置文件,使用 local 环境
(venv) pyape config -FE local config.toml .env

# 运行单元测试
(venv) pytest tests/test_sample_env_local.py

多数据库支持范例

使用 SQLAlchemy 操作数据库 仅包含单数据库支持,得益于 SQLAlchemy 的良好设计 以及 pyape.toml 的多环境支持,我们可以非常容易让不同环境支持不同的数据库。

要理解多数据库支持的原理,请查看: 多数据库支持原理

sample/pyape.toml 中已经包含了多数据库范例的支持。让我们看看 multidb 环境的配置, 我们使用 app.user_multidb 模块替代 app.user

[ENV.multidb.'config.toml'.PATH.modules]
main = ''
user_multidb = '/user2'

[ENV.multidb.'config.toml'.SQLALCHEMY.URI]
# 多数据库配置,直接覆盖默认配置
db1 = 'sqlite:///{WORK_DIR}/sample_db1.sqlite'
db2 = 'sqlite:///{WORK_DIR}/sample_db2.sqlite'

创建子模块 app/model_multidb.py 用于 Table 定义。 在这里,可以使用 Model() 方法,传递 bind_key 参数来获取对应不同数据库的 Model。 下面的 User1User2 两个 Table 分别位于不同的数据库。

# app/model_multidb.py
import time

from sqlalchemy import Column, INT, VARCHAR

from pyape.app import gdb

Model1 = gdb.Model('db1')
Model2 = gdb.Model('db2')


class User1(Model1):
    __tablename__ = 'user1'

    id = Column(INT, autoincrement=True, primary_key=True)
    name = Column(VARCHAR(100), nullable=False)
    createtime = Column(INT, nullable=False, default=lambda: int(time.time()))


class User2(Model2):
    __tablename__ = 'user2'

    id = Column(INT, autoincrement=True, primary_key=True)
    name = Column(VARCHAR(100), nullable=False)
    createtime = Column(INT, nullable=False, default=lambda: int(time.time()))

创建子模块 app/user_multidb.py 提供数据库访问方法。 这里的范例简单传递 bind_key 参数用来指定写入不同的数据库。

from flask import Blueprint, abort
from webargs.flaskparser import use_args
from webargs import fields
from sqlalchemy import select

from pyape.app import gdb, logger
from app.model_multidb import User1, User2


user_multidb = Blueprint('user_multidb', __name__)


@user_multidb.get('/get')
@use_args({'id': fields.Int(required=True), 'bind_key': fields.Str(required=True)}, location='query')
def get_user_db1(args):
    id = args['id']
    bind_key = args['bind_key']
    User = User1 if bind_key == 'db1' else User2
    userobj = gdb.session().get(User, id)
    if userobj is None:
        logger.warning(f'user {id} is not found in {bind_key}.')
        abort(404)
    return f'User in {bind_key} id: {userobj.id}, name: {userobj.name}'


@user_multidb.post('/set')
@use_args({'id': fields.Int(required=True), 'name': fields.Str(required=True), 'bind_key': fields.Str(required=True)}, location='form')
def set_user(args):
    bind_key = args['bind_key']
    User = User1 if bind_key == 'db1' else User2
    userobj = User(**args)
    gdb.session().add(userobj)
    gdb.session().commit()
    return f'User in {bind_key} id: {userobj.id}, name: {userobj.name}'

测试 Sample 项目的 multidb 环境(多数据库支持)

若 pyape 项目位于 ~/storage/pyape

# 进入虚拟环境
cd ~/storage/pyape
python3 -m venv venv
source venv/bin/active

# 安装当前环境下的 pyape
(venv) pip install -e .

# 创建本地配置文件,使用 multidb 环境
(venv) pyape config -FE multidb config.toml .env

# 运行单元测试
(venv) pytest tests/test_sample_env_multidb.py