- 新增图像生成接口,支持试用、积分和自定义API Key模式 - 实现生成图片结果异步上传至MinIO存储,带重试机制 - 优化积分预扣除和异常退还逻辑,保障用户积分准确 - 添加获取生成历史记录接口,支持时间范围和分页 - 提供本地字典配置接口,支持模型、比例、提示模板和尺寸 - 实现图片批量上传接口,支持S3兼容对象存储 feat(admin): 增加管理员角色管理与权限分配接口 - 实现角色列表查询、角色创建、更新及删除功能 - 增加权限列表查询接口 - 实现用户角色分配接口,便于统一管理用户权限 - 增加系统字典增删查改接口,支持分类过滤和排序 - 权限控制全面覆盖管理接口,保证安全访问 feat(auth): 完善用户登录注册及权限相关接口与页面 - 实现手机号验证码发送及校验功能,保障注册安全 - 支持手机号注册、登录及退出接口,集成日志记录 - 增加修改密码功能,验证原密码后更新 - 提供动态导航菜单接口,基于权限展示不同菜单 - 实现管理界面路由及日志、角色、字典管理页面访问权限控制 - 添加系统日志查询接口,支持关键词和等级筛选 feat(app): 初始化Flask应用并配置蓝图与数据库 - 创建应用程序工厂,加载配置,初始化数据库和Redis客户端 - 注册认证、API及管理员蓝图,整合路由 - 根路由渲染主页模板 - 应用上下文中自动创建数据库表,保证运行环境准备完毕 feat(database): 提供数据库创建与迁移支持脚本 - 新增数据库创建脚本,支持自动检测是否已存在 - 添加数据库表初始化脚本,支持创建和删除所有表 - 实现RBAC权限初始化,包含基础权限和角色创建 - 新增字段手动修复脚本,添加用户API Key和积分字段 - 强制迁移脚本支持清理连接和修复表结构,初始化默认数据及角色分配 feat(config): 新增系统配置参数 - 配置数据库、Redis、Session和MinIO相关参数 - 添加AI接口地址及试用Key配置 - 集成阿里云短信服务配置及开发模式相关参数 feat(extensions): 初始化数据库、Redis和MinIO客户端 - 创建全局SQLAlchemy数据库实例和Redis客户端 - 配置基于boto3的MinIO兼容S3客户端 chore(logs): 添加示例系统日志文件 - 记录用户请求、验证码发送成功与失败的日志信息
367 lines
12 KiB
Python
367 lines
12 KiB
Python
# testing/fixtures/base.py
|
|
# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
|
|
# <see AUTHORS file>
|
|
#
|
|
# This module is part of SQLAlchemy and is released under
|
|
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
|
# mypy: ignore-errors
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
import sqlalchemy as sa
|
|
from .. import assertions
|
|
from .. import config
|
|
from ..assertions import eq_
|
|
from ..util import drop_all_tables_from_metadata
|
|
from ..util import picklers
|
|
from ... import Column
|
|
from ... import func
|
|
from ... import Integer
|
|
from ... import select
|
|
from ... import Table
|
|
from ...orm import DeclarativeBase
|
|
from ...orm import MappedAsDataclass
|
|
from ...orm import registry
|
|
|
|
|
|
@config.mark_base_test_class()
|
|
class TestBase:
|
|
# A sequence of requirement names matching testing.requires decorators
|
|
__requires__ = ()
|
|
|
|
# A sequence of dialect names to exclude from the test class.
|
|
__unsupported_on__ = ()
|
|
|
|
# If present, test class is only runnable for the *single* specified
|
|
# dialect. If you need multiple, use __unsupported_on__ and invert.
|
|
__only_on__ = None
|
|
|
|
# A sequence of no-arg callables. If any are True, the entire testcase is
|
|
# skipped.
|
|
__skip_if__ = None
|
|
|
|
# if True, the testing reaper will not attempt to touch connection
|
|
# state after a test is completed and before the outer teardown
|
|
# starts
|
|
__leave_connections_for_teardown__ = False
|
|
|
|
def assert_(self, val, msg=None):
|
|
assert val, msg
|
|
|
|
@config.fixture()
|
|
def nocache(self):
|
|
_cache = config.db._compiled_cache
|
|
config.db._compiled_cache = None
|
|
yield
|
|
config.db._compiled_cache = _cache
|
|
|
|
@config.fixture()
|
|
def connection_no_trans(self):
|
|
eng = getattr(self, "bind", None) or config.db
|
|
|
|
with eng.connect() as conn:
|
|
yield conn
|
|
|
|
@config.fixture()
|
|
def connection(self):
|
|
global _connection_fixture_connection
|
|
|
|
eng = getattr(self, "bind", None) or config.db
|
|
|
|
conn = eng.connect()
|
|
trans = conn.begin()
|
|
|
|
_connection_fixture_connection = conn
|
|
yield conn
|
|
|
|
_connection_fixture_connection = None
|
|
|
|
if trans.is_active:
|
|
trans.rollback()
|
|
# trans would not be active here if the test is using
|
|
# the legacy @provide_metadata decorator still, as it will
|
|
# run a close all connections.
|
|
conn.close()
|
|
|
|
@config.fixture()
|
|
def close_result_when_finished(self):
|
|
to_close = []
|
|
to_consume = []
|
|
|
|
def go(result, consume=False):
|
|
to_close.append(result)
|
|
if consume:
|
|
to_consume.append(result)
|
|
|
|
yield go
|
|
for r in to_consume:
|
|
try:
|
|
r.all()
|
|
except:
|
|
pass
|
|
for r in to_close:
|
|
try:
|
|
r.close()
|
|
except:
|
|
pass
|
|
|
|
@config.fixture()
|
|
def registry(self, metadata):
|
|
reg = registry(
|
|
metadata=metadata,
|
|
type_annotation_map={
|
|
str: sa.String().with_variant(
|
|
sa.String(50), "mysql", "mariadb", "oracle"
|
|
)
|
|
},
|
|
)
|
|
yield reg
|
|
reg.dispose()
|
|
|
|
@config.fixture
|
|
def decl_base(self, metadata):
|
|
_md = metadata
|
|
|
|
class Base(DeclarativeBase):
|
|
metadata = _md
|
|
type_annotation_map = {
|
|
str: sa.String().with_variant(
|
|
sa.String(50), "mysql", "mariadb", "oracle"
|
|
)
|
|
}
|
|
|
|
yield Base
|
|
Base.registry.dispose()
|
|
|
|
@config.fixture
|
|
def dc_decl_base(self, metadata):
|
|
_md = metadata
|
|
|
|
class Base(MappedAsDataclass, DeclarativeBase):
|
|
metadata = _md
|
|
type_annotation_map = {
|
|
str: sa.String().with_variant(
|
|
sa.String(50), "mysql", "mariadb"
|
|
)
|
|
}
|
|
|
|
yield Base
|
|
Base.registry.dispose()
|
|
|
|
@config.fixture()
|
|
def future_connection(self, future_engine, connection):
|
|
# integrate the future_engine and connection fixtures so
|
|
# that users of the "connection" fixture will get at the
|
|
# "future" connection
|
|
yield connection
|
|
|
|
@config.fixture()
|
|
def future_engine(self):
|
|
yield
|
|
|
|
@config.fixture()
|
|
def testing_engine(self):
|
|
from .. import engines
|
|
|
|
def gen_testing_engine(
|
|
url=None,
|
|
options=None,
|
|
asyncio=False,
|
|
):
|
|
if options is None:
|
|
options = {}
|
|
options["scope"] = "fixture"
|
|
return engines.testing_engine(
|
|
url=url,
|
|
options=options,
|
|
asyncio=asyncio,
|
|
)
|
|
|
|
yield gen_testing_engine
|
|
|
|
engines.testing_reaper._drop_testing_engines("fixture")
|
|
|
|
@config.fixture()
|
|
def async_testing_engine(self, testing_engine):
|
|
def go(**kw):
|
|
kw["asyncio"] = True
|
|
return testing_engine(**kw)
|
|
|
|
return go
|
|
|
|
@config.fixture(params=picklers())
|
|
def picklers(self, request):
|
|
yield request.param
|
|
|
|
@config.fixture()
|
|
def metadata(self, request):
|
|
"""Provide bound MetaData for a single test, dropping afterwards."""
|
|
|
|
from ...sql import schema
|
|
|
|
metadata = schema.MetaData()
|
|
request.instance.metadata = metadata
|
|
yield metadata
|
|
del request.instance.metadata
|
|
|
|
if (
|
|
_connection_fixture_connection
|
|
and _connection_fixture_connection.in_transaction()
|
|
):
|
|
trans = _connection_fixture_connection.get_transaction()
|
|
trans.rollback()
|
|
with _connection_fixture_connection.begin():
|
|
drop_all_tables_from_metadata(
|
|
metadata, _connection_fixture_connection
|
|
)
|
|
else:
|
|
drop_all_tables_from_metadata(metadata, config.db)
|
|
|
|
@config.fixture(
|
|
params=[
|
|
(rollback, second_operation, begin_nested)
|
|
for rollback in (True, False)
|
|
for second_operation in ("none", "execute", "begin")
|
|
for begin_nested in (
|
|
True,
|
|
False,
|
|
)
|
|
]
|
|
)
|
|
def trans_ctx_manager_fixture(self, request, metadata):
|
|
rollback, second_operation, begin_nested = request.param
|
|
|
|
t = Table("test", metadata, Column("data", Integer))
|
|
eng = getattr(self, "bind", None) or config.db
|
|
|
|
t.create(eng)
|
|
|
|
def run_test(subject, trans_on_subject, execute_on_subject):
|
|
with subject.begin() as trans:
|
|
if begin_nested:
|
|
if not config.requirements.savepoints.enabled:
|
|
config.skip_test("savepoints not enabled")
|
|
if execute_on_subject:
|
|
nested_trans = subject.begin_nested()
|
|
else:
|
|
nested_trans = trans.begin_nested()
|
|
|
|
with nested_trans:
|
|
if execute_on_subject:
|
|
subject.execute(t.insert(), {"data": 10})
|
|
else:
|
|
trans.execute(t.insert(), {"data": 10})
|
|
|
|
# for nested trans, we always commit/rollback on the
|
|
# "nested trans" object itself.
|
|
# only Session(future=False) will affect savepoint
|
|
# transaction for session.commit/rollback
|
|
|
|
if rollback:
|
|
nested_trans.rollback()
|
|
else:
|
|
nested_trans.commit()
|
|
|
|
if second_operation != "none":
|
|
with assertions.expect_raises_message(
|
|
sa.exc.InvalidRequestError,
|
|
"Can't operate on closed transaction "
|
|
"inside context "
|
|
"manager. Please complete the context "
|
|
"manager "
|
|
"before emitting further commands.",
|
|
):
|
|
if second_operation == "execute":
|
|
if execute_on_subject:
|
|
subject.execute(
|
|
t.insert(), {"data": 12}
|
|
)
|
|
else:
|
|
trans.execute(t.insert(), {"data": 12})
|
|
elif second_operation == "begin":
|
|
if execute_on_subject:
|
|
subject.begin_nested()
|
|
else:
|
|
trans.begin_nested()
|
|
|
|
# outside the nested trans block, but still inside the
|
|
# transaction block, we can run SQL, and it will be
|
|
# committed
|
|
if execute_on_subject:
|
|
subject.execute(t.insert(), {"data": 14})
|
|
else:
|
|
trans.execute(t.insert(), {"data": 14})
|
|
|
|
else:
|
|
if execute_on_subject:
|
|
subject.execute(t.insert(), {"data": 10})
|
|
else:
|
|
trans.execute(t.insert(), {"data": 10})
|
|
|
|
if trans_on_subject:
|
|
if rollback:
|
|
subject.rollback()
|
|
else:
|
|
subject.commit()
|
|
else:
|
|
if rollback:
|
|
trans.rollback()
|
|
else:
|
|
trans.commit()
|
|
|
|
if second_operation != "none":
|
|
with assertions.expect_raises_message(
|
|
sa.exc.InvalidRequestError,
|
|
"Can't operate on closed transaction inside "
|
|
"context "
|
|
"manager. Please complete the context manager "
|
|
"before emitting further commands.",
|
|
):
|
|
if second_operation == "execute":
|
|
if execute_on_subject:
|
|
subject.execute(t.insert(), {"data": 12})
|
|
else:
|
|
trans.execute(t.insert(), {"data": 12})
|
|
elif second_operation == "begin":
|
|
if hasattr(trans, "begin"):
|
|
trans.begin()
|
|
else:
|
|
subject.begin()
|
|
elif second_operation == "begin_nested":
|
|
if execute_on_subject:
|
|
subject.begin_nested()
|
|
else:
|
|
trans.begin_nested()
|
|
|
|
expected_committed = 0
|
|
if begin_nested:
|
|
# begin_nested variant, we inserted a row after the nested
|
|
# block
|
|
expected_committed += 1
|
|
if not rollback:
|
|
# not rollback variant, our row inserted in the target
|
|
# block itself would be committed
|
|
expected_committed += 1
|
|
|
|
if execute_on_subject:
|
|
eq_(
|
|
subject.scalar(select(func.count()).select_from(t)),
|
|
expected_committed,
|
|
)
|
|
else:
|
|
with subject.connect() as conn:
|
|
eq_(
|
|
conn.scalar(select(func.count()).select_from(t)),
|
|
expected_committed,
|
|
)
|
|
|
|
return run_test
|
|
|
|
|
|
_connection_fixture_connection = None
|
|
|
|
|
|
class FutureEngineMixin:
|
|
"""alembic's suite still using this"""
|