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.
Files changed (138) hide show
  1. aury/boot/__init__.py +66 -0
  2. aury/boot/_version.py +2 -2
  3. aury/boot/application/__init__.py +120 -0
  4. aury/boot/application/app/__init__.py +39 -0
  5. aury/boot/application/app/base.py +511 -0
  6. aury/boot/application/app/components.py +434 -0
  7. aury/boot/application/app/middlewares.py +101 -0
  8. aury/boot/application/config/__init__.py +44 -0
  9. aury/boot/application/config/settings.py +663 -0
  10. aury/boot/application/constants/__init__.py +19 -0
  11. aury/boot/application/constants/components.py +50 -0
  12. aury/boot/application/constants/scheduler.py +28 -0
  13. aury/boot/application/constants/service.py +29 -0
  14. aury/boot/application/errors/__init__.py +55 -0
  15. aury/boot/application/errors/chain.py +80 -0
  16. aury/boot/application/errors/codes.py +67 -0
  17. aury/boot/application/errors/exceptions.py +238 -0
  18. aury/boot/application/errors/handlers.py +320 -0
  19. aury/boot/application/errors/response.py +120 -0
  20. aury/boot/application/interfaces/__init__.py +76 -0
  21. aury/boot/application/interfaces/egress.py +224 -0
  22. aury/boot/application/interfaces/ingress.py +98 -0
  23. aury/boot/application/middleware/__init__.py +22 -0
  24. aury/boot/application/middleware/logging.py +451 -0
  25. aury/boot/application/migrations/__init__.py +13 -0
  26. aury/boot/application/migrations/manager.py +685 -0
  27. aury/boot/application/migrations/setup.py +237 -0
  28. aury/boot/application/rpc/__init__.py +63 -0
  29. aury/boot/application/rpc/base.py +108 -0
  30. aury/boot/application/rpc/client.py +294 -0
  31. aury/boot/application/rpc/discovery.py +218 -0
  32. aury/boot/application/scheduler/__init__.py +13 -0
  33. aury/boot/application/scheduler/runner.py +123 -0
  34. aury/boot/application/server/__init__.py +296 -0
  35. aury/boot/commands/__init__.py +30 -0
  36. aury/boot/commands/add.py +76 -0
  37. aury/boot/commands/app.py +105 -0
  38. aury/boot/commands/config.py +177 -0
  39. aury/boot/commands/docker.py +367 -0
  40. aury/boot/commands/docs.py +284 -0
  41. aury/boot/commands/generate.py +1277 -0
  42. aury/boot/commands/init.py +892 -0
  43. aury/boot/commands/migrate/__init__.py +37 -0
  44. aury/boot/commands/migrate/app.py +54 -0
  45. aury/boot/commands/migrate/commands.py +303 -0
  46. aury/boot/commands/scheduler.py +124 -0
  47. aury/boot/commands/server/__init__.py +21 -0
  48. aury/boot/commands/server/app.py +541 -0
  49. aury/boot/commands/templates/generate/api.py.tpl +105 -0
  50. aury/boot/commands/templates/generate/model.py.tpl +17 -0
  51. aury/boot/commands/templates/generate/repository.py.tpl +19 -0
  52. aury/boot/commands/templates/generate/schema.py.tpl +29 -0
  53. aury/boot/commands/templates/generate/service.py.tpl +48 -0
  54. aury/boot/commands/templates/project/CLI.md.tpl +92 -0
  55. aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +1397 -0
  56. aury/boot/commands/templates/project/README.md.tpl +111 -0
  57. aury/boot/commands/templates/project/admin_console_init.py.tpl +50 -0
  58. aury/boot/commands/templates/project/config.py.tpl +30 -0
  59. aury/boot/commands/templates/project/conftest.py.tpl +26 -0
  60. aury/boot/commands/templates/project/env.example.tpl +213 -0
  61. aury/boot/commands/templates/project/gitignore.tpl +128 -0
  62. aury/boot/commands/templates/project/main.py.tpl +41 -0
  63. aury/boot/commands/templates/project/modules/api.py.tpl +19 -0
  64. aury/boot/commands/templates/project/modules/exceptions.py.tpl +84 -0
  65. aury/boot/commands/templates/project/modules/schedules.py.tpl +18 -0
  66. aury/boot/commands/templates/project/modules/tasks.py.tpl +20 -0
  67. aury/boot/commands/worker.py +143 -0
  68. aury/boot/common/__init__.py +35 -0
  69. aury/boot/common/exceptions/__init__.py +114 -0
  70. aury/boot/common/i18n/__init__.py +16 -0
  71. aury/boot/common/i18n/translator.py +272 -0
  72. aury/boot/common/logging/__init__.py +716 -0
  73. aury/boot/contrib/__init__.py +10 -0
  74. aury/boot/contrib/admin_console/__init__.py +18 -0
  75. aury/boot/contrib/admin_console/auth.py +137 -0
  76. aury/boot/contrib/admin_console/discovery.py +69 -0
  77. aury/boot/contrib/admin_console/install.py +172 -0
  78. aury/boot/contrib/admin_console/utils.py +44 -0
  79. aury/boot/domain/__init__.py +79 -0
  80. aury/boot/domain/exceptions/__init__.py +132 -0
  81. aury/boot/domain/models/__init__.py +51 -0
  82. aury/boot/domain/models/base.py +69 -0
  83. aury/boot/domain/models/mixins.py +135 -0
  84. aury/boot/domain/models/models.py +96 -0
  85. aury/boot/domain/pagination/__init__.py +279 -0
  86. aury/boot/domain/repository/__init__.py +23 -0
  87. aury/boot/domain/repository/impl.py +423 -0
  88. aury/boot/domain/repository/interceptors.py +47 -0
  89. aury/boot/domain/repository/interface.py +106 -0
  90. aury/boot/domain/repository/query_builder.py +348 -0
  91. aury/boot/domain/service/__init__.py +11 -0
  92. aury/boot/domain/service/base.py +73 -0
  93. aury/boot/domain/transaction/__init__.py +404 -0
  94. aury/boot/infrastructure/__init__.py +104 -0
  95. aury/boot/infrastructure/cache/__init__.py +31 -0
  96. aury/boot/infrastructure/cache/backends.py +348 -0
  97. aury/boot/infrastructure/cache/base.py +68 -0
  98. aury/boot/infrastructure/cache/exceptions.py +37 -0
  99. aury/boot/infrastructure/cache/factory.py +94 -0
  100. aury/boot/infrastructure/cache/manager.py +274 -0
  101. aury/boot/infrastructure/database/__init__.py +39 -0
  102. aury/boot/infrastructure/database/config.py +71 -0
  103. aury/boot/infrastructure/database/exceptions.py +44 -0
  104. aury/boot/infrastructure/database/manager.py +317 -0
  105. aury/boot/infrastructure/database/query_tools/__init__.py +164 -0
  106. aury/boot/infrastructure/database/strategies/__init__.py +198 -0
  107. aury/boot/infrastructure/di/__init__.py +15 -0
  108. aury/boot/infrastructure/di/container.py +393 -0
  109. aury/boot/infrastructure/events/__init__.py +33 -0
  110. aury/boot/infrastructure/events/bus.py +362 -0
  111. aury/boot/infrastructure/events/config.py +52 -0
  112. aury/boot/infrastructure/events/consumer.py +134 -0
  113. aury/boot/infrastructure/events/middleware.py +51 -0
  114. aury/boot/infrastructure/events/models.py +63 -0
  115. aury/boot/infrastructure/monitoring/__init__.py +529 -0
  116. aury/boot/infrastructure/scheduler/__init__.py +19 -0
  117. aury/boot/infrastructure/scheduler/exceptions.py +37 -0
  118. aury/boot/infrastructure/scheduler/manager.py +478 -0
  119. aury/boot/infrastructure/storage/__init__.py +38 -0
  120. aury/boot/infrastructure/storage/base.py +164 -0
  121. aury/boot/infrastructure/storage/exceptions.py +37 -0
  122. aury/boot/infrastructure/storage/factory.py +88 -0
  123. aury/boot/infrastructure/tasks/__init__.py +24 -0
  124. aury/boot/infrastructure/tasks/config.py +45 -0
  125. aury/boot/infrastructure/tasks/constants.py +37 -0
  126. aury/boot/infrastructure/tasks/exceptions.py +37 -0
  127. aury/boot/infrastructure/tasks/manager.py +490 -0
  128. aury/boot/testing/__init__.py +24 -0
  129. aury/boot/testing/base.py +122 -0
  130. aury/boot/testing/client.py +163 -0
  131. aury/boot/testing/factory.py +154 -0
  132. aury/boot/toolkit/__init__.py +21 -0
  133. aury/boot/toolkit/http/__init__.py +367 -0
  134. {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/METADATA +3 -2
  135. aury_boot-0.0.4.dist-info/RECORD +137 -0
  136. aury_boot-0.0.2.dist-info/RECORD +0 -5
  137. {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/WHEEL +0 -0
  138. {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,10 @@
1
+ """可选扩展(Contrib)。
2
+
3
+ 该目录用于放置与核心框架解耦的可选集成能力(例如管理后台、监控面板等)。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ __all__ = []
9
+
10
+
@@ -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
+