aury-boot 0.0.2__py3-none-any.whl → 0.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aury/boot/__init__.py +66 -0
- aury/boot/_version.py +2 -2
- aury/boot/application/__init__.py +120 -0
- aury/boot/application/app/__init__.py +39 -0
- aury/boot/application/app/base.py +511 -0
- aury/boot/application/app/components.py +434 -0
- aury/boot/application/app/middlewares.py +101 -0
- aury/boot/application/config/__init__.py +44 -0
- aury/boot/application/config/settings.py +663 -0
- aury/boot/application/constants/__init__.py +19 -0
- aury/boot/application/constants/components.py +50 -0
- aury/boot/application/constants/scheduler.py +28 -0
- aury/boot/application/constants/service.py +29 -0
- aury/boot/application/errors/__init__.py +55 -0
- aury/boot/application/errors/chain.py +80 -0
- aury/boot/application/errors/codes.py +67 -0
- aury/boot/application/errors/exceptions.py +238 -0
- aury/boot/application/errors/handlers.py +320 -0
- aury/boot/application/errors/response.py +120 -0
- aury/boot/application/interfaces/__init__.py +76 -0
- aury/boot/application/interfaces/egress.py +224 -0
- aury/boot/application/interfaces/ingress.py +98 -0
- aury/boot/application/middleware/__init__.py +22 -0
- aury/boot/application/middleware/logging.py +451 -0
- aury/boot/application/migrations/__init__.py +13 -0
- aury/boot/application/migrations/manager.py +685 -0
- aury/boot/application/migrations/setup.py +237 -0
- aury/boot/application/rpc/__init__.py +63 -0
- aury/boot/application/rpc/base.py +108 -0
- aury/boot/application/rpc/client.py +294 -0
- aury/boot/application/rpc/discovery.py +218 -0
- aury/boot/application/scheduler/__init__.py +13 -0
- aury/boot/application/scheduler/runner.py +123 -0
- aury/boot/application/server/__init__.py +296 -0
- aury/boot/commands/__init__.py +30 -0
- aury/boot/commands/add.py +76 -0
- aury/boot/commands/app.py +105 -0
- aury/boot/commands/config.py +177 -0
- aury/boot/commands/docker.py +367 -0
- aury/boot/commands/docs.py +284 -0
- aury/boot/commands/generate.py +1277 -0
- aury/boot/commands/init.py +892 -0
- aury/boot/commands/migrate/__init__.py +37 -0
- aury/boot/commands/migrate/app.py +54 -0
- aury/boot/commands/migrate/commands.py +303 -0
- aury/boot/commands/scheduler.py +124 -0
- aury/boot/commands/server/__init__.py +21 -0
- aury/boot/commands/server/app.py +541 -0
- aury/boot/commands/templates/generate/api.py.tpl +105 -0
- aury/boot/commands/templates/generate/model.py.tpl +17 -0
- aury/boot/commands/templates/generate/repository.py.tpl +19 -0
- aury/boot/commands/templates/generate/schema.py.tpl +29 -0
- aury/boot/commands/templates/generate/service.py.tpl +48 -0
- aury/boot/commands/templates/project/CLI.md.tpl +92 -0
- aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +1397 -0
- aury/boot/commands/templates/project/README.md.tpl +111 -0
- aury/boot/commands/templates/project/admin_console_init.py.tpl +50 -0
- aury/boot/commands/templates/project/config.py.tpl +30 -0
- aury/boot/commands/templates/project/conftest.py.tpl +26 -0
- aury/boot/commands/templates/project/env.example.tpl +213 -0
- aury/boot/commands/templates/project/gitignore.tpl +128 -0
- aury/boot/commands/templates/project/main.py.tpl +41 -0
- aury/boot/commands/templates/project/modules/api.py.tpl +19 -0
- aury/boot/commands/templates/project/modules/exceptions.py.tpl +84 -0
- aury/boot/commands/templates/project/modules/schedules.py.tpl +18 -0
- aury/boot/commands/templates/project/modules/tasks.py.tpl +20 -0
- aury/boot/commands/worker.py +143 -0
- aury/boot/common/__init__.py +35 -0
- aury/boot/common/exceptions/__init__.py +114 -0
- aury/boot/common/i18n/__init__.py +16 -0
- aury/boot/common/i18n/translator.py +272 -0
- aury/boot/common/logging/__init__.py +716 -0
- aury/boot/contrib/__init__.py +10 -0
- aury/boot/contrib/admin_console/__init__.py +18 -0
- aury/boot/contrib/admin_console/auth.py +137 -0
- aury/boot/contrib/admin_console/discovery.py +69 -0
- aury/boot/contrib/admin_console/install.py +172 -0
- aury/boot/contrib/admin_console/utils.py +44 -0
- aury/boot/domain/__init__.py +79 -0
- aury/boot/domain/exceptions/__init__.py +132 -0
- aury/boot/domain/models/__init__.py +51 -0
- aury/boot/domain/models/base.py +69 -0
- aury/boot/domain/models/mixins.py +135 -0
- aury/boot/domain/models/models.py +96 -0
- aury/boot/domain/pagination/__init__.py +279 -0
- aury/boot/domain/repository/__init__.py +23 -0
- aury/boot/domain/repository/impl.py +423 -0
- aury/boot/domain/repository/interceptors.py +47 -0
- aury/boot/domain/repository/interface.py +106 -0
- aury/boot/domain/repository/query_builder.py +348 -0
- aury/boot/domain/service/__init__.py +11 -0
- aury/boot/domain/service/base.py +73 -0
- aury/boot/domain/transaction/__init__.py +404 -0
- aury/boot/infrastructure/__init__.py +104 -0
- aury/boot/infrastructure/cache/__init__.py +31 -0
- aury/boot/infrastructure/cache/backends.py +348 -0
- aury/boot/infrastructure/cache/base.py +68 -0
- aury/boot/infrastructure/cache/exceptions.py +37 -0
- aury/boot/infrastructure/cache/factory.py +94 -0
- aury/boot/infrastructure/cache/manager.py +274 -0
- aury/boot/infrastructure/database/__init__.py +39 -0
- aury/boot/infrastructure/database/config.py +71 -0
- aury/boot/infrastructure/database/exceptions.py +44 -0
- aury/boot/infrastructure/database/manager.py +317 -0
- aury/boot/infrastructure/database/query_tools/__init__.py +164 -0
- aury/boot/infrastructure/database/strategies/__init__.py +198 -0
- aury/boot/infrastructure/di/__init__.py +15 -0
- aury/boot/infrastructure/di/container.py +393 -0
- aury/boot/infrastructure/events/__init__.py +33 -0
- aury/boot/infrastructure/events/bus.py +362 -0
- aury/boot/infrastructure/events/config.py +52 -0
- aury/boot/infrastructure/events/consumer.py +134 -0
- aury/boot/infrastructure/events/middleware.py +51 -0
- aury/boot/infrastructure/events/models.py +63 -0
- aury/boot/infrastructure/monitoring/__init__.py +529 -0
- aury/boot/infrastructure/scheduler/__init__.py +19 -0
- aury/boot/infrastructure/scheduler/exceptions.py +37 -0
- aury/boot/infrastructure/scheduler/manager.py +478 -0
- aury/boot/infrastructure/storage/__init__.py +38 -0
- aury/boot/infrastructure/storage/base.py +164 -0
- aury/boot/infrastructure/storage/exceptions.py +37 -0
- aury/boot/infrastructure/storage/factory.py +88 -0
- aury/boot/infrastructure/tasks/__init__.py +24 -0
- aury/boot/infrastructure/tasks/config.py +45 -0
- aury/boot/infrastructure/tasks/constants.py +37 -0
- aury/boot/infrastructure/tasks/exceptions.py +37 -0
- aury/boot/infrastructure/tasks/manager.py +490 -0
- aury/boot/testing/__init__.py +24 -0
- aury/boot/testing/base.py +122 -0
- aury/boot/testing/client.py +163 -0
- aury/boot/testing/factory.py +154 -0
- aury/boot/toolkit/__init__.py +21 -0
- aury/boot/toolkit/http/__init__.py +367 -0
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/METADATA +3 -2
- aury_boot-0.0.4.dist-info/RECORD +137 -0
- aury_boot-0.0.2.dist-info/RECORD +0 -5
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""SQLAdmin 管理后台(Admin Console)集成。
|
|
2
|
+
|
|
3
|
+
设计目标:
|
|
4
|
+
- 默认路径 `/api/admin-console`,避免与业务 URL 冲突
|
|
5
|
+
- 默认只内置 basic / bearer 两种可用认证模式
|
|
6
|
+
- 允许通过 settings 或项目模块显式覆盖认证/视图注册
|
|
7
|
+
- 不依赖 CLI command,适合生产快速集成
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from .install import install_admin_console
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"install_admin_console",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from starlette.requests import Request
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
try: # pragma: no cover
|
|
10
|
+
from sqladmin.authentication import AuthenticationBackend as _SQLAdminAuthenticationBackend
|
|
11
|
+
except Exception: # pragma: no cover
|
|
12
|
+
_SQLAdminAuthenticationBackend = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _require_sqladmin():
|
|
16
|
+
try:
|
|
17
|
+
from sqladmin.authentication import AuthenticationBackend # noqa: F401
|
|
18
|
+
except Exception as exc: # pragma: no cover
|
|
19
|
+
raise ImportError(
|
|
20
|
+
"未安装 sqladmin。请先安装: uv add \"aury-boot[admin]\" 或 uv add sqladmin"
|
|
21
|
+
) from exc
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_bearer_token(request: Request) -> str | None:
|
|
25
|
+
auth = request.headers.get("authorization", "")
|
|
26
|
+
if not auth:
|
|
27
|
+
return None
|
|
28
|
+
parts = auth.split(None, 1)
|
|
29
|
+
if len(parts) != 2:
|
|
30
|
+
return None
|
|
31
|
+
scheme, token = parts
|
|
32
|
+
if scheme.lower() != "bearer":
|
|
33
|
+
return None
|
|
34
|
+
token = token.strip()
|
|
35
|
+
return token or None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BasicAdminAuthBackend(_SQLAdminAuthenticationBackend or object):
|
|
39
|
+
"""SQLAdmin 登录页 + session 的最简用户名/密码认证(推荐默认)。"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, *, username: str, password: str, secret_key: str, session_key: str = "aury_admin") -> None:
|
|
42
|
+
_require_sqladmin()
|
|
43
|
+
# 若 sqladmin 不存在,此类的 base 会是 object,这里会提前抛错,避免静默不兼容
|
|
44
|
+
super().__init__(secret_key=secret_key) # type: ignore[misc]
|
|
45
|
+
self._username = username
|
|
46
|
+
self._password = password
|
|
47
|
+
self._session_key = session_key
|
|
48
|
+
|
|
49
|
+
async def login(self, request: Request) -> bool:
|
|
50
|
+
form = await request.form()
|
|
51
|
+
username = str(form.get("username", "")).strip()
|
|
52
|
+
password = str(form.get("password", "")).strip()
|
|
53
|
+
if username == self._username and password == self._password:
|
|
54
|
+
request.session.update({self._session_key: True})
|
|
55
|
+
return True
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
async def logout(self, request: Request) -> bool:
|
|
59
|
+
request.session.clear()
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
async def authenticate(self, request: Request) -> bool:
|
|
63
|
+
return bool(request.session.get(self._session_key))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class BearerWhitelistAdminAuthBackend(_SQLAdminAuthenticationBackend or object):
|
|
67
|
+
"""Bearer 白名单认证。
|
|
68
|
+
|
|
69
|
+
支持两种方式:
|
|
70
|
+
- Authorization: Bearer <token>(适合反向代理注入/自动化)
|
|
71
|
+
- 登录页输入 token(用户名任意,password/token 字段均可)
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
*,
|
|
77
|
+
tokens: list[str],
|
|
78
|
+
secret_key: str,
|
|
79
|
+
session_key: str = "aury_admin_token",
|
|
80
|
+
) -> None:
|
|
81
|
+
_require_sqladmin()
|
|
82
|
+
super().__init__(secret_key=secret_key) # type: ignore[misc]
|
|
83
|
+
self._tokens = {t.strip() for t in tokens if t and t.strip()}
|
|
84
|
+
self._session_key = session_key
|
|
85
|
+
|
|
86
|
+
async def login(self, request: Request) -> bool:
|
|
87
|
+
form = await request.form()
|
|
88
|
+
# 兼容不同表单字段:优先 token,其次 password
|
|
89
|
+
token = str(form.get("token") or form.get("password") or "").strip()
|
|
90
|
+
if token and token in self._tokens:
|
|
91
|
+
request.session.update({self._session_key: token})
|
|
92
|
+
return True
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
async def logout(self, request: Request) -> bool:
|
|
96
|
+
request.session.clear()
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
async def authenticate(self, request: Request) -> bool:
|
|
100
|
+
header_token = _get_bearer_token(request)
|
|
101
|
+
if header_token and header_token in self._tokens:
|
|
102
|
+
return True
|
|
103
|
+
session_token = request.session.get(self._session_key)
|
|
104
|
+
return bool(session_token and session_token in self._tokens)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def wrap_authenticate(
|
|
108
|
+
*,
|
|
109
|
+
secret_key: str,
|
|
110
|
+
authenticate: Callable[[Request], Any],
|
|
111
|
+
session_key: str = "aury_admin",
|
|
112
|
+
):
|
|
113
|
+
"""将一个 authenticate(request) 可调用对象包装为 SQLAdmin AuthenticationBackend。
|
|
114
|
+
|
|
115
|
+
用于让用户以最小成本自定义(不必继承 SQLAdmin 类)。
|
|
116
|
+
"""
|
|
117
|
+
_require_sqladmin()
|
|
118
|
+
from sqladmin.authentication import AuthenticationBackend
|
|
119
|
+
|
|
120
|
+
class _Wrapped(AuthenticationBackend):
|
|
121
|
+
async def login(self, request: Request) -> bool: # noqa: D401
|
|
122
|
+
# 默认不提供登录页逻辑;用户可自己实现更复杂版本
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
async def logout(self, request: Request) -> bool:
|
|
126
|
+
request.session.pop(session_key, None)
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
async def authenticate(self, request: Request) -> bool:
|
|
130
|
+
result = authenticate(request)
|
|
131
|
+
if hasattr(result, "__await__"):
|
|
132
|
+
result = await result
|
|
133
|
+
return bool(result)
|
|
134
|
+
|
|
135
|
+
return _Wrapped(secret_key=secret_key)
|
|
136
|
+
|
|
137
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from aury.boot.common.logging import logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _candidate_modules(app: Any, config: Any) -> list[str]:
|
|
10
|
+
"""生成项目侧 admin-console 模块候选列表(多策略自动发现)。
|
|
11
|
+
|
|
12
|
+
参考 SchedulerComponent._autodiscover_schedules 的风格:
|
|
13
|
+
- settings 显式指定优先
|
|
14
|
+
- 读取 [tool.aury].package
|
|
15
|
+
- service.name / 调用者模块推断
|
|
16
|
+
- 最后尝试根模块 admin_console/admin
|
|
17
|
+
"""
|
|
18
|
+
modules: list[str] = []
|
|
19
|
+
|
|
20
|
+
# 策略 0:显式指定
|
|
21
|
+
views_module = getattr(getattr(config, "admin", None), "views_module", None)
|
|
22
|
+
if views_module:
|
|
23
|
+
modules.append(str(views_module).strip())
|
|
24
|
+
|
|
25
|
+
# 策略 1:读取 pyproject.toml 的 [tool.aury].package
|
|
26
|
+
try:
|
|
27
|
+
from aury.boot.commands.config import get_project_config
|
|
28
|
+
|
|
29
|
+
cfg = get_project_config()
|
|
30
|
+
if getattr(cfg, "has_package", False):
|
|
31
|
+
pkg = cfg.package
|
|
32
|
+
modules.extend([f"{pkg}.admin_console", f"{pkg}.admin"])
|
|
33
|
+
except Exception:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
# 策略 2:service.name 推断
|
|
37
|
+
service_name = (getattr(getattr(config, "service", None), "name", None) or "").strip()
|
|
38
|
+
if service_name and service_name not in {"app", "main"}:
|
|
39
|
+
modules.extend([f"{service_name}.admin_console", f"{service_name}.admin"])
|
|
40
|
+
|
|
41
|
+
# 策略 3:从调用者模块推断
|
|
42
|
+
caller = getattr(app, "_caller_module", "__main__")
|
|
43
|
+
if caller in ("__main__", "main"):
|
|
44
|
+
modules.extend(["admin_console", "admin"])
|
|
45
|
+
elif "." in str(caller):
|
|
46
|
+
package = str(caller).rsplit(".", 1)[0]
|
|
47
|
+
modules.extend([f"{package}.admin_console", f"{package}.admin", "admin_console", "admin"])
|
|
48
|
+
else:
|
|
49
|
+
modules.extend([f"{caller}.admin_console", f"{caller}.admin", "admin_console", "admin"])
|
|
50
|
+
|
|
51
|
+
# 去重,保持顺序
|
|
52
|
+
seen: set[str] = set()
|
|
53
|
+
return [m for m in modules if m and not (m in seen or seen.add(m))]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def load_project_admin_module(app: Any, config: Any):
|
|
57
|
+
"""尝试导入项目侧 admin-console 模块,成功则返回 module,否则返回 None。"""
|
|
58
|
+
for module_name in _candidate_modules(app, config):
|
|
59
|
+
try:
|
|
60
|
+
module = importlib.import_module(module_name)
|
|
61
|
+
logger.info(f"已加载管理后台模块: {module_name}")
|
|
62
|
+
return module
|
|
63
|
+
except ImportError:
|
|
64
|
+
logger.debug(f"管理后台模块不存在: {module_name}")
|
|
65
|
+
except Exception as exc:
|
|
66
|
+
logger.warning(f"加载管理后台模块失败 ({module_name}): {exc}")
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import create_engine
|
|
6
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
7
|
+
|
|
8
|
+
from aury.boot.common.logging import logger
|
|
9
|
+
|
|
10
|
+
from .auth import BasicAdminAuthBackend, BearerWhitelistAdminAuthBackend
|
|
11
|
+
from .discovery import load_project_admin_module
|
|
12
|
+
from .utils import import_from_string
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _require_sqladmin():
|
|
16
|
+
try:
|
|
17
|
+
from sqladmin import Admin # noqa: F401
|
|
18
|
+
except Exception as exc: # pragma: no cover
|
|
19
|
+
raise ImportError(
|
|
20
|
+
"未安装 sqladmin。请先安装: uv add \"aury-boot[admin]\" 或 uv add sqladmin"
|
|
21
|
+
) from exc
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _resolve_auth_backend(app: Any, config: Any, module: Any | None):
|
|
25
|
+
"""解析 SQLAdmin authentication_backend。
|
|
26
|
+
|
|
27
|
+
优先级(与项目现有 autodiscover 风格一致):
|
|
28
|
+
1) settings: ADMIN_AUTH_BACKEND="module:attr"(完全覆盖)
|
|
29
|
+
2) 项目模块: register_admin_auth(config) -> backend(覆盖内置)
|
|
30
|
+
3) 内置 basic/bearer/none
|
|
31
|
+
"""
|
|
32
|
+
admin_cfg = getattr(config, "admin", None)
|
|
33
|
+
auth_cfg = getattr(admin_cfg, "auth", None)
|
|
34
|
+
|
|
35
|
+
mode = getattr(auth_cfg, "mode", "basic")
|
|
36
|
+
backend_path = getattr(auth_cfg, "backend", None)
|
|
37
|
+
secret_key = getattr(auth_cfg, "secret_key", None)
|
|
38
|
+
|
|
39
|
+
# 生产环境安全约束
|
|
40
|
+
if getattr(config, "is_production", False):
|
|
41
|
+
if mode == "none":
|
|
42
|
+
raise ValueError("生产环境不允许 ADMIN_AUTH_MODE=none,请使用 basic/bearer 或自定义 backend")
|
|
43
|
+
if not secret_key or str(secret_key).strip() in {"CHANGE_ME", "changeme"}:
|
|
44
|
+
raise ValueError("生产环境启用管理后台时必须设置 ADMIN_AUTH_SECRET_KEY(且不能为 CHANGE_ME)")
|
|
45
|
+
|
|
46
|
+
# 1) 显式 backend 覆盖
|
|
47
|
+
if backend_path:
|
|
48
|
+
obj = import_from_string(str(backend_path).strip())
|
|
49
|
+
backend = obj(config) if callable(obj) else obj
|
|
50
|
+
if backend is None:
|
|
51
|
+
raise ValueError(f"ADMIN_AUTH_BACKEND={backend_path!r} 返回 None")
|
|
52
|
+
logger.info("管理后台认证:使用 settings 指定的自定义 backend")
|
|
53
|
+
return backend
|
|
54
|
+
|
|
55
|
+
# 2) 项目模块覆盖
|
|
56
|
+
if module is not None and hasattr(module, "register_admin_auth"):
|
|
57
|
+
backend = module.register_admin_auth(config) # type: ignore[attr-defined]
|
|
58
|
+
if backend is None:
|
|
59
|
+
raise ValueError("register_admin_auth(config) 返回 None")
|
|
60
|
+
logger.info("管理后台认证:使用项目模块 register_admin_auth 提供的 backend")
|
|
61
|
+
return backend
|
|
62
|
+
|
|
63
|
+
# 3) 内置兜底
|
|
64
|
+
if mode in {"custom", "jwt"}:
|
|
65
|
+
raise ValueError(
|
|
66
|
+
f"ADMIN_AUTH_MODE={mode!r} 需要提供 ADMIN_AUTH_BACKEND 或在项目模块实现 register_admin_auth(config)"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if mode == "none":
|
|
70
|
+
logger.warning("管理后台认证:已关闭认证(ADMIN_AUTH_MODE=none)")
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
if not secret_key:
|
|
74
|
+
raise ValueError("启用管理后台认证需要设置 ADMIN_AUTH_SECRET_KEY")
|
|
75
|
+
|
|
76
|
+
if mode == "basic":
|
|
77
|
+
username = getattr(auth_cfg, "basic_username", None)
|
|
78
|
+
password = getattr(auth_cfg, "basic_password", None)
|
|
79
|
+
if not username or not password:
|
|
80
|
+
raise ValueError("ADMIN_AUTH_MODE=basic 需要设置 ADMIN_AUTH_BASIC_USERNAME/ADMIN_AUTH_BASIC_PASSWORD")
|
|
81
|
+
return BasicAdminAuthBackend(username=username, password=password, secret_key=secret_key)
|
|
82
|
+
|
|
83
|
+
if mode == "bearer":
|
|
84
|
+
tokens = list(getattr(auth_cfg, "bearer_tokens", []) or [])
|
|
85
|
+
if not tokens:
|
|
86
|
+
raise ValueError("ADMIN_AUTH_MODE=bearer 需要设置 ADMIN_AUTH_BEARER_TOKENS(token 白名单)")
|
|
87
|
+
return BearerWhitelistAdminAuthBackend(tokens=tokens, secret_key=secret_key)
|
|
88
|
+
|
|
89
|
+
raise ValueError(f"未知的 ADMIN_AUTH_MODE: {mode!r}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _register_views(admin: Any, module: Any | None) -> None:
|
|
93
|
+
"""注册项目侧 views。"""
|
|
94
|
+
if module is None:
|
|
95
|
+
logger.info("管理后台:未发现项目 admin-console 模块,跳过 views 注册")
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
if hasattr(module, "register_admin"):
|
|
99
|
+
module.register_admin(admin) # type: ignore[attr-defined]
|
|
100
|
+
logger.info("管理后台:已通过 register_admin(admin) 注册 views")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
views = getattr(module, "ADMIN_VIEWS", None)
|
|
104
|
+
if views:
|
|
105
|
+
for view_cls in list(views):
|
|
106
|
+
admin.add_view(view_cls)
|
|
107
|
+
logger.info("管理后台:已通过 ADMIN_VIEWS 注册 views")
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
logger.info("管理后台:项目模块已加载,但未提供 register_admin/ADMIN_VIEWS,跳过 views 注册")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def install_admin_console(app: Any, config: Any | None = None):
|
|
114
|
+
"""安装 SQLAdmin 管理后台到 FastAPI/FoundationApp。
|
|
115
|
+
|
|
116
|
+
- 默认路径:/api/admin-console(可通过 ADMIN_PATH 覆盖)
|
|
117
|
+
- 认证:默认 basic/bearer(可通过 ADMIN_AUTH_* 配置,或自定义 backend 覆盖)
|
|
118
|
+
- 视图:可通过项目模块 register_admin(admin) 或 ADMIN_VIEWS 提供
|
|
119
|
+
- 引擎:支持同步或异步 SQLAlchemy Engine(AsyncEngine)
|
|
120
|
+
|
|
121
|
+
返回:
|
|
122
|
+
sqladmin.Admin 实例;若未启用(ADMIN_ENABLED=false)返回 None
|
|
123
|
+
"""
|
|
124
|
+
_require_sqladmin()
|
|
125
|
+
from sqladmin import Admin
|
|
126
|
+
|
|
127
|
+
if config is None:
|
|
128
|
+
from aury.boot.application.config import BaseConfig
|
|
129
|
+
|
|
130
|
+
config = BaseConfig()
|
|
131
|
+
|
|
132
|
+
admin_cfg = getattr(config, "admin", None)
|
|
133
|
+
if not getattr(admin_cfg, "enabled", False):
|
|
134
|
+
logger.debug("管理后台未启用(ADMIN_ENABLED=false),跳过安装")
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
# 1) 自动发现项目模块(用于 auth/views)
|
|
138
|
+
module = load_project_admin_module(app, config)
|
|
139
|
+
|
|
140
|
+
# 2) 解析认证后端(可能为 None)
|
|
141
|
+
auth_backend = _resolve_auth_backend(app, config, module)
|
|
142
|
+
|
|
143
|
+
# 3) 构建 Engine(支持同步/异步)
|
|
144
|
+
db_url = getattr(admin_cfg, "database_url", None) or getattr(config.database, "url", "")
|
|
145
|
+
if not db_url:
|
|
146
|
+
raise ValueError("无法确定管理后台数据库 URL:请设置 ADMIN_DATABASE_URL 或 DATABASE_URL")
|
|
147
|
+
|
|
148
|
+
def _is_async_url(url: str) -> bool:
|
|
149
|
+
return any(s in url for s in ["+asyncpg", "+aiosqlite", "+aiomysql", "+asyncmy"])
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
if _is_async_url(str(db_url)):
|
|
153
|
+
engine = create_async_engine(str(db_url), future=True)
|
|
154
|
+
else:
|
|
155
|
+
engine = create_engine(str(db_url), future=True)
|
|
156
|
+
except Exception as exc:
|
|
157
|
+
raise RuntimeError(
|
|
158
|
+
"创建管理后台数据库 Engine 失败。请检查 ADMIN_DATABASE_URL/DATABASE_URL 与对应驱动是否可用。"
|
|
159
|
+
) from exc
|
|
160
|
+
|
|
161
|
+
base_url = getattr(admin_cfg, "path", "/api/admin-console")
|
|
162
|
+
|
|
163
|
+
# 4) 安装 admin(engine 可为同步或异步)
|
|
164
|
+
admin = Admin(app=app, engine=engine, base_url=base_url, authentication_backend=auth_backend)
|
|
165
|
+
|
|
166
|
+
# 5) 注册 views
|
|
167
|
+
_register_views(admin, module)
|
|
168
|
+
|
|
169
|
+
logger.info(f"✅ 管理后台已启用:{base_url}")
|
|
170
|
+
return admin
|
|
171
|
+
|
|
172
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def import_from_string(path: str):
|
|
7
|
+
"""从 'module:attr' 动态导入对象。
|
|
8
|
+
|
|
9
|
+
与 commands/server 的 app 导入风格保持一致。
|
|
10
|
+
"""
|
|
11
|
+
if ":" not in path:
|
|
12
|
+
raise ValueError(f"无效导入路径: {path!r}(应为 'module:attr')")
|
|
13
|
+
module_path, attr = path.rsplit(":", 1)
|
|
14
|
+
module = importlib.import_module(module_path)
|
|
15
|
+
try:
|
|
16
|
+
return getattr(module, attr)
|
|
17
|
+
except AttributeError as exc:
|
|
18
|
+
raise AttributeError(f"模块 {module_path!r} 中不存在 {attr!r}") from exc
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def derive_sync_database_url(database_url: str) -> str:
|
|
22
|
+
"""从异步 URL 推导同步 URL(用于 SQLAdmin)。
|
|
23
|
+
|
|
24
|
+
说明:
|
|
25
|
+
- sqladmin 通常要求同步 SQLAlchemy Engine(create_engine)
|
|
26
|
+
- 本函数只做最常见的 driver 映射;如需完全自定义请用 ADMIN_DATABASE_URL 覆盖
|
|
27
|
+
"""
|
|
28
|
+
# SQLite
|
|
29
|
+
if database_url.startswith("sqlite+aiosqlite://"):
|
|
30
|
+
return database_url.replace("sqlite+aiosqlite://", "sqlite://", 1)
|
|
31
|
+
|
|
32
|
+
# PostgreSQL
|
|
33
|
+
if database_url.startswith("postgresql+asyncpg://"):
|
|
34
|
+
# 推荐 psycopg(psycopg3)
|
|
35
|
+
return database_url.replace("postgresql+asyncpg://", "postgresql+psycopg://", 1)
|
|
36
|
+
|
|
37
|
+
# MySQL
|
|
38
|
+
if database_url.startswith("mysql+aiomysql://"):
|
|
39
|
+
return database_url.replace("mysql+aiomysql://", "mysql+pymysql://", 1)
|
|
40
|
+
|
|
41
|
+
# 其他情况:认为已经是同步 URL
|
|
42
|
+
return database_url
|
|
43
|
+
|
|
44
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""领域层模块。
|
|
2
|
+
|
|
3
|
+
提供领域模型和业务逻辑的基础类,包括:
|
|
4
|
+
- ORM 模型基类
|
|
5
|
+
- Repository 接口
|
|
6
|
+
- Service 模式
|
|
7
|
+
- 异常定义
|
|
8
|
+
- 事务管理
|
|
9
|
+
|
|
10
|
+
注意:Event 基类定义在 infrastructure.events 层,不在 domain 层。
|
|
11
|
+
使用事件时请直接从 infrastructure 导入。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .exceptions import CoreException, ModelError, VersionConflictError
|
|
15
|
+
from .models import (
|
|
16
|
+
GUID,
|
|
17
|
+
AuditableStateMixin,
|
|
18
|
+
AuditableStateModel,
|
|
19
|
+
Base,
|
|
20
|
+
FullFeaturedModel,
|
|
21
|
+
FullFeaturedUUIDModel,
|
|
22
|
+
IDMixin,
|
|
23
|
+
Model,
|
|
24
|
+
TimestampMixin,
|
|
25
|
+
UUIDAuditableStateModel,
|
|
26
|
+
UUIDMixin,
|
|
27
|
+
UUIDModel,
|
|
28
|
+
VersionedModel,
|
|
29
|
+
VersionedTimestampedModel,
|
|
30
|
+
VersionedUUIDModel,
|
|
31
|
+
VersionMixin,
|
|
32
|
+
)
|
|
33
|
+
from .repository import (
|
|
34
|
+
IRepository,
|
|
35
|
+
QueryInterceptor,
|
|
36
|
+
)
|
|
37
|
+
from .service import BaseService
|
|
38
|
+
from .transaction import (
|
|
39
|
+
TransactionManager,
|
|
40
|
+
TransactionRequiredError,
|
|
41
|
+
ensure_transaction,
|
|
42
|
+
transactional,
|
|
43
|
+
transactional_context,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"GUID",
|
|
48
|
+
"AuditableStateMixin",
|
|
49
|
+
"AuditableStateModel",
|
|
50
|
+
# 模型基类
|
|
51
|
+
"Base",
|
|
52
|
+
# Service
|
|
53
|
+
"BaseService",
|
|
54
|
+
# 异常
|
|
55
|
+
"CoreException",
|
|
56
|
+
"FullFeaturedModel",
|
|
57
|
+
"FullFeaturedUUIDModel",
|
|
58
|
+
"IDMixin",
|
|
59
|
+
# Repository (接口)
|
|
60
|
+
"IRepository",
|
|
61
|
+
"Model",
|
|
62
|
+
"ModelError",
|
|
63
|
+
"QueryInterceptor",
|
|
64
|
+
"TimestampMixin",
|
|
65
|
+
"TransactionManager",
|
|
66
|
+
"TransactionRequiredError",
|
|
67
|
+
"UUIDAuditableStateModel",
|
|
68
|
+
"UUIDMixin",
|
|
69
|
+
"UUIDModel",
|
|
70
|
+
"VersionConflictError",
|
|
71
|
+
"VersionMixin",
|
|
72
|
+
"VersionedModel",
|
|
73
|
+
"VersionedTimestampedModel",
|
|
74
|
+
"VersionedUUIDModel",
|
|
75
|
+
"ensure_transaction",
|
|
76
|
+
# Transaction
|
|
77
|
+
"transactional",
|
|
78
|
+
"transactional_context",
|
|
79
|
+
]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Domain 层异常定义。
|
|
2
|
+
|
|
3
|
+
Domain 层异常,继承自 FoundationError。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from aury.boot.common.exceptions import FoundationError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CoreException(FoundationError): # noqa: N818
|
|
12
|
+
"""Domain 层异常基类。
|
|
13
|
+
|
|
14
|
+
所有 Domain 层的异常都应该继承此类。
|
|
15
|
+
|
|
16
|
+
注意:虽然命名为 CoreException,但它是 Domain 层的异常基类。
|
|
17
|
+
保持 Exception 后缀以与 FoundationError 区分。
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ModelError(CoreException):
|
|
24
|
+
"""模型相关错误基类。
|
|
25
|
+
|
|
26
|
+
所有模型相关的异常都应该继承此类。
|
|
27
|
+
包括:版本冲突、约束违反、验证错误等。
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class VersionConflictError(ModelError):
|
|
34
|
+
"""版本冲突异常(乐观锁)。
|
|
35
|
+
|
|
36
|
+
当使用 VersionedModel 时,如果更新时版本号不匹配,抛出此异常。
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
current_version: 当前数据库中的版本号
|
|
40
|
+
expected_version: 期望的版本号
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
message: str = "数据已被其他操作修改,请刷新后重试",
|
|
46
|
+
current_version: int | None = None,
|
|
47
|
+
expected_version: int | None = None,
|
|
48
|
+
*args: object,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""初始化版本冲突异常。
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
message: 错误消息
|
|
54
|
+
current_version: 当前数据库中的版本号
|
|
55
|
+
expected_version: 期望的版本号
|
|
56
|
+
*args: 其他参数
|
|
57
|
+
"""
|
|
58
|
+
super().__init__(message, *args)
|
|
59
|
+
self.current_version = current_version
|
|
60
|
+
self.expected_version = expected_version
|
|
61
|
+
|
|
62
|
+
def __str__(self) -> str:
|
|
63
|
+
"""返回异常字符串表示。"""
|
|
64
|
+
if self.current_version is not None and self.expected_version is not None:
|
|
65
|
+
return (
|
|
66
|
+
f"{self.message} "
|
|
67
|
+
f"(当前版本: {self.current_version}, 期望版本: {self.expected_version})"
|
|
68
|
+
)
|
|
69
|
+
return self.message
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class TransactionRequiredError(CoreException):
|
|
73
|
+
"""事务必需异常。
|
|
74
|
+
|
|
75
|
+
当方法需要在事务中执行,但当前不在事务中时,抛出此异常。
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
message: str = "此操作需要在事务中执行",
|
|
81
|
+
*args: object,
|
|
82
|
+
) -> None:
|
|
83
|
+
"""初始化事务必需异常。
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
message: 错误消息
|
|
87
|
+
*args: 其他参数
|
|
88
|
+
"""
|
|
89
|
+
super().__init__(message, *args)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ServiceException(CoreException):
|
|
93
|
+
"""服务层异常基类。
|
|
94
|
+
|
|
95
|
+
所有服务层的业务异常都应该继承此类。
|
|
96
|
+
用于标识业务逻辑错误,区别于系统错误。
|
|
97
|
+
|
|
98
|
+
Attributes:
|
|
99
|
+
message: 错误消息
|
|
100
|
+
code: 业务错误代码(可选,用于错误分类)
|
|
101
|
+
metadata: 额外的元数据(可选)
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
message: str,
|
|
107
|
+
code: str | None = None,
|
|
108
|
+
metadata: dict[str, object] | None = None,
|
|
109
|
+
*args: object,
|
|
110
|
+
) -> None:
|
|
111
|
+
"""初始化服务异常。
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
message: 错误消息
|
|
115
|
+
code: 业务错误代码(可选)
|
|
116
|
+
metadata: 额外的元数据(可选)
|
|
117
|
+
*args: 其他参数
|
|
118
|
+
"""
|
|
119
|
+
super().__init__(message, *args)
|
|
120
|
+
self.code = code
|
|
121
|
+
self.metadata = metadata or {}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
__all__ = [
|
|
125
|
+
"CoreException",
|
|
126
|
+
"ModelError",
|
|
127
|
+
"ServiceException",
|
|
128
|
+
"TransactionRequiredError",
|
|
129
|
+
"VersionConflictError",
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
|