- 新增图像生成接口,支持试用、积分和自定义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): 添加示例系统日志文件 - 记录用户请求、验证码发送成功与失败的日志信息
289 lines
9.0 KiB
Python
289 lines
9.0 KiB
Python
# util/_concurrency_py3k.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: allow-untyped-defs, allow-untyped-calls
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from contextvars import Context
|
|
import sys
|
|
import typing
|
|
from typing import Any
|
|
from typing import Awaitable
|
|
from typing import Callable
|
|
from typing import Coroutine
|
|
from typing import Optional
|
|
from typing import TYPE_CHECKING
|
|
from typing import TypeVar
|
|
from typing import Union
|
|
|
|
from .langhelpers import memoized_property
|
|
from .. import exc
|
|
from ..util import py311
|
|
from ..util.typing import Literal
|
|
from ..util.typing import Protocol
|
|
from ..util.typing import Self
|
|
from ..util.typing import TypeGuard
|
|
|
|
_T = TypeVar("_T")
|
|
|
|
if typing.TYPE_CHECKING:
|
|
|
|
class greenlet(Protocol):
|
|
dead: bool
|
|
gr_context: Optional[Context]
|
|
|
|
def __init__(self, fn: Callable[..., Any], driver: greenlet): ...
|
|
|
|
def throw(self, *arg: Any) -> Any:
|
|
return None
|
|
|
|
def switch(self, value: Any) -> Any:
|
|
return None
|
|
|
|
def getcurrent() -> greenlet: ...
|
|
|
|
else:
|
|
from greenlet import getcurrent
|
|
from greenlet import greenlet
|
|
|
|
|
|
# If greenlet.gr_context is present in current version of greenlet,
|
|
# it will be set with the current context on creation.
|
|
# Refs: https://github.com/python-greenlet/greenlet/pull/198
|
|
_has_gr_context = hasattr(getcurrent(), "gr_context")
|
|
|
|
|
|
def is_exit_exception(e: BaseException) -> bool:
|
|
# note asyncio.CancelledError is already BaseException
|
|
# so was an exit exception in any case
|
|
return not isinstance(e, Exception) or isinstance(
|
|
e, (asyncio.TimeoutError, asyncio.CancelledError)
|
|
)
|
|
|
|
|
|
# implementation based on snaury gist at
|
|
# https://gist.github.com/snaury/202bf4f22c41ca34e56297bae5f33fef
|
|
# Issue for context: https://github.com/python-greenlet/greenlet/issues/173
|
|
|
|
|
|
class _AsyncIoGreenlet(greenlet):
|
|
dead: bool
|
|
|
|
__sqlalchemy_greenlet_provider__ = True
|
|
|
|
def __init__(self, fn: Callable[..., Any], driver: greenlet):
|
|
greenlet.__init__(self, fn, driver)
|
|
if _has_gr_context:
|
|
self.gr_context = driver.gr_context
|
|
|
|
|
|
_T_co = TypeVar("_T_co", covariant=True)
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
def iscoroutine(
|
|
awaitable: Awaitable[_T_co],
|
|
) -> TypeGuard[Coroutine[Any, Any, _T_co]]: ...
|
|
|
|
else:
|
|
iscoroutine = asyncio.iscoroutine
|
|
|
|
|
|
def _safe_cancel_awaitable(awaitable: Awaitable[Any]) -> None:
|
|
# https://docs.python.org/3/reference/datamodel.html#coroutine.close
|
|
|
|
if iscoroutine(awaitable):
|
|
awaitable.close()
|
|
|
|
|
|
def in_greenlet() -> bool:
|
|
current = getcurrent()
|
|
return getattr(current, "__sqlalchemy_greenlet_provider__", False)
|
|
|
|
|
|
def await_only(awaitable: Awaitable[_T]) -> _T:
|
|
"""Awaits an async function in a sync method.
|
|
|
|
The sync method must be inside a :func:`greenlet_spawn` context.
|
|
:func:`await_only` calls cannot be nested.
|
|
|
|
:param awaitable: The coroutine to call.
|
|
|
|
"""
|
|
# this is called in the context greenlet while running fn
|
|
current = getcurrent()
|
|
if not getattr(current, "__sqlalchemy_greenlet_provider__", False):
|
|
_safe_cancel_awaitable(awaitable)
|
|
|
|
raise exc.MissingGreenlet(
|
|
"greenlet_spawn has not been called; can't call await_only() "
|
|
"here. Was IO attempted in an unexpected place?"
|
|
)
|
|
|
|
# returns the control to the driver greenlet passing it
|
|
# a coroutine to run. Once the awaitable is done, the driver greenlet
|
|
# switches back to this greenlet with the result of awaitable that is
|
|
# then returned to the caller (or raised as error)
|
|
return current.parent.switch(awaitable) # type: ignore[no-any-return,attr-defined] # noqa: E501
|
|
|
|
|
|
def await_fallback(awaitable: Awaitable[_T]) -> _T:
|
|
"""Awaits an async function in a sync method.
|
|
|
|
The sync method must be inside a :func:`greenlet_spawn` context.
|
|
:func:`await_fallback` calls cannot be nested.
|
|
|
|
:param awaitable: The coroutine to call.
|
|
|
|
.. deprecated:: 2.0.24 The ``await_fallback()`` function will be removed
|
|
in SQLAlchemy 2.1. Use :func:`_util.await_only` instead, running the
|
|
function / program / etc. within a top-level greenlet that is set up
|
|
using :func:`_util.greenlet_spawn`.
|
|
|
|
"""
|
|
|
|
# this is called in the context greenlet while running fn
|
|
current = getcurrent()
|
|
if not getattr(current, "__sqlalchemy_greenlet_provider__", False):
|
|
loop = get_event_loop()
|
|
if loop.is_running():
|
|
_safe_cancel_awaitable(awaitable)
|
|
|
|
raise exc.MissingGreenlet(
|
|
"greenlet_spawn has not been called and asyncio event "
|
|
"loop is already running; can't call await_fallback() here. "
|
|
"Was IO attempted in an unexpected place?"
|
|
)
|
|
return loop.run_until_complete(awaitable)
|
|
|
|
return current.parent.switch(awaitable) # type: ignore[no-any-return,attr-defined] # noqa: E501
|
|
|
|
|
|
async def greenlet_spawn(
|
|
fn: Callable[..., _T],
|
|
*args: Any,
|
|
_require_await: bool = False,
|
|
**kwargs: Any,
|
|
) -> _T:
|
|
"""Runs a sync function ``fn`` in a new greenlet.
|
|
|
|
The sync function can then use :func:`await_only` to wait for async
|
|
functions.
|
|
|
|
:param fn: The sync callable to call.
|
|
:param \\*args: Positional arguments to pass to the ``fn`` callable.
|
|
:param \\*\\*kwargs: Keyword arguments to pass to the ``fn`` callable.
|
|
"""
|
|
|
|
result: Any
|
|
context = _AsyncIoGreenlet(fn, getcurrent())
|
|
# runs the function synchronously in gl greenlet. If the execution
|
|
# is interrupted by await_only, context is not dead and result is a
|
|
# coroutine to wait. If the context is dead the function has
|
|
# returned, and its result can be returned.
|
|
switch_occurred = False
|
|
result = context.switch(*args, **kwargs)
|
|
while not context.dead:
|
|
switch_occurred = True
|
|
try:
|
|
# wait for a coroutine from await_only and then return its
|
|
# result back to it.
|
|
value = await result
|
|
except BaseException:
|
|
# this allows an exception to be raised within
|
|
# the moderated greenlet so that it can continue
|
|
# its expected flow.
|
|
result = context.throw(*sys.exc_info())
|
|
else:
|
|
result = context.switch(value)
|
|
|
|
if _require_await and not switch_occurred:
|
|
raise exc.AwaitRequired(
|
|
"The current operation required an async execution but none was "
|
|
"detected. This will usually happen when using a non compatible "
|
|
"DBAPI driver. Please ensure that an async DBAPI is used."
|
|
)
|
|
return result # type: ignore[no-any-return]
|
|
|
|
|
|
class AsyncAdaptedLock:
|
|
@memoized_property
|
|
def mutex(self) -> asyncio.Lock:
|
|
# there should not be a race here for coroutines creating the
|
|
# new lock as we are not using await, so therefore no concurrency
|
|
return asyncio.Lock()
|
|
|
|
def __enter__(self) -> bool:
|
|
# await is used to acquire the lock only after the first calling
|
|
# coroutine has created the mutex.
|
|
return await_fallback(self.mutex.acquire())
|
|
|
|
def __exit__(self, *arg: Any, **kw: Any) -> None:
|
|
self.mutex.release()
|
|
|
|
|
|
def get_event_loop() -> asyncio.AbstractEventLoop:
|
|
"""vendor asyncio.get_event_loop() for python 3.7 and above.
|
|
|
|
Python 3.10 deprecates get_event_loop() as a standalone.
|
|
|
|
"""
|
|
try:
|
|
return asyncio.get_running_loop()
|
|
except RuntimeError:
|
|
# avoid "During handling of the above exception, another exception..."
|
|
pass
|
|
return asyncio.get_event_loop_policy().get_event_loop()
|
|
|
|
|
|
if not TYPE_CHECKING and py311:
|
|
_Runner = asyncio.Runner
|
|
else:
|
|
|
|
class _Runner:
|
|
"""Runner implementation for test only"""
|
|
|
|
_loop: Union[None, asyncio.AbstractEventLoop, Literal[False]]
|
|
|
|
def __init__(self) -> None:
|
|
self._loop = None
|
|
|
|
def __enter__(self) -> Self:
|
|
self._lazy_init()
|
|
return self
|
|
|
|
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
self.close()
|
|
|
|
def close(self) -> None:
|
|
if self._loop:
|
|
try:
|
|
self._loop.run_until_complete(
|
|
self._loop.shutdown_asyncgens()
|
|
)
|
|
finally:
|
|
self._loop.close()
|
|
self._loop = False
|
|
|
|
def get_loop(self) -> asyncio.AbstractEventLoop:
|
|
"""Return embedded event loop."""
|
|
self._lazy_init()
|
|
assert self._loop
|
|
return self._loop
|
|
|
|
def run(self, coro: Coroutine[Any, Any, _T]) -> _T:
|
|
self._lazy_init()
|
|
assert self._loop
|
|
return self._loop.run_until_complete(coro)
|
|
|
|
def _lazy_init(self) -> None:
|
|
if self._loop is False:
|
|
raise RuntimeError("Runner is closed")
|
|
if self._loop is None:
|
|
self._loop = asyncio.new_event_loop()
|