aury-boot 0.0.2__py3-none-any.whl → 0.0.3__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 +890 -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.3.dist-info}/METADATA +3 -2
  135. aury_boot-0.0.3.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.3.dist-info}/WHEEL +0 -0
  138. {aury_boot-0.0.2.dist-info → aury_boot-0.0.3.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,84 @@
1
+ """自定义异常模块。
2
+
3
+ 在此文件中定义业务异常类,继承自 BaseError。
4
+ 框架的全局异常处理器会自动将这些异常转换为 HTTP 响应。
5
+
6
+ ============================================================
7
+ 框架已有的异常类(可直接使用)
8
+ ============================================================
9
+ - BaseError: 异常基类
10
+ - ValidationError: 验证失败 (400)
11
+ - NotFoundError: 资源不存在 (404)
12
+ - AlreadyExistsError: 资源已存在 (409)
13
+ - UnauthorizedError: 未授权 (401)
14
+ - ForbiddenError: 禁止访问 (403)
15
+ - DatabaseError: 数据库错误 (500)
16
+ - BusinessError: 业务错误 (400)
17
+ - VersionConflictError: 乐观锁冲突 (409)
18
+
19
+ ============================================================
20
+ 框架预留错误码范围(ErrorCode 枚举)
21
+ ============================================================
22
+ - 1xxx: 通用错误
23
+ 1000: UNKNOWN_ERROR
24
+ 1001: VALIDATION_ERROR
25
+ 1002: NOT_FOUND
26
+ 1003: ALREADY_EXISTS
27
+ 1004: UNAUTHORIZED
28
+ 1005: FORBIDDEN
29
+ - 2xxx: 数据库错误
30
+ 2000: DATABASE_ERROR
31
+ 2001: DUPLICATE_KEY
32
+ 2002: CONSTRAINT_VIOLATION
33
+ 2003: VERSION_CONFLICT
34
+ - 3xxx: 业务错误
35
+ 3000: BUSINESS_ERROR
36
+ 3001: INVALID_OPERATION
37
+ 3002: INSUFFICIENT_PERMISSION
38
+ - 4xxx: 外部服务错误
39
+ 4000: EXTERNAL_SERVICE_ERROR
40
+ 4001: TIMEOUT_ERROR
41
+ 4002: NETWORK_ERROR
42
+
43
+ ============================================================
44
+ 自定义错误码建议从 5000 开始,使用字符串格式
45
+ ============================================================
46
+
47
+ 继承示例:
48
+ class OrderError(BaseError):
49
+ default_message = "订单错误"
50
+ default_code = "5001" # 字符串格式
51
+ default_status_code = 400
52
+
53
+ class OrderNotPaidError(OrderError):
54
+ default_message = "订单未支付"
55
+ default_code = "5002"
56
+ """
57
+
58
+ from fastapi import status
59
+
60
+ from aury.boot.application.errors import BaseError
61
+
62
+ # ============================================================
63
+ # 在此定义业务异常类
64
+ # ============================================================
65
+
66
+
67
+ # class OrderError(BaseError):
68
+ # """订单相关异常基类。"""
69
+ # default_message = "订单错误"
70
+ # default_code = "ORDER_ERROR"
71
+ # default_status_code = status.HTTP_400_BAD_REQUEST
72
+ #
73
+ #
74
+ # class OrderNotFoundError(OrderError):
75
+ # """订单不存在。"""
76
+ # default_message = "订单不存在"
77
+ # default_code = "ORDER_NOT_FOUND"
78
+ # default_status_code = status.HTTP_404_NOT_FOUND
79
+ #
80
+ #
81
+ # class OrderNotPaidError(OrderError):
82
+ # """订单未支付。"""
83
+ # default_message = "订单未支付"
84
+ # default_code = "ORDER_NOT_PAID"
@@ -0,0 +1,18 @@
1
+ """定时任务模块(Scheduler)。
2
+
3
+ 在此文件中定义定时任务,使用 @scheduler.scheduled_job() 装饰器。
4
+
5
+ 框架会自动发现并加载本模块,无需在 main.py 中手动导入。
6
+ 也可通过 SCHEDULER_SCHEDULE_MODULES 环境变量指定自定义模块。
7
+ """
8
+
9
+ # from aury.boot.common.logging import logger
10
+ # from aury.boot.infrastructure.scheduler import SchedulerManager
11
+ #
12
+ # scheduler = SchedulerManager.get_instance()
13
+ #
14
+ #
15
+ # @scheduler.scheduled_job("interval", seconds=60)
16
+ # async def example_job():
17
+ # """示例定时任务,每 60 秒执行一次。"""
18
+ # logger.info("定时任务执行中...")
@@ -0,0 +1,20 @@
1
+ """异步任务模块(Dramatiq Worker)。
2
+
3
+ 取消注释下面的示例代码即可启用异步任务。
4
+
5
+ 使用方式:
6
+ 1. 配置 TASK_BROKER_URL 环境变量
7
+ 2. 取消注释下面的代码
8
+ 3. 运行 Worker:aury worker
9
+ 4. 在代码中调用:example_task.send()
10
+ """
11
+
12
+ # from aury.boot.common.logging import logger
13
+ # from aury.boot.infrastructure.tasks import conditional_task
14
+ #
15
+ #
16
+ # @conditional_task
17
+ # def example_task():
18
+ # """示例异步任务。"""
19
+ # logger.info("异步任务执行中...")
20
+ # return {"status": "completed"}
@@ -0,0 +1,143 @@
1
+ """Worker 命令 - 运行任务队列 Worker。
2
+
3
+ 使用示例:
4
+ aury worker # 运行 worker
5
+ aury worker --app main:app # 指定应用模块
6
+ aury worker -c 4 # 指定并发数
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+ import sys
13
+
14
+ from rich.console import Console
15
+ import typer
16
+
17
+ console = Console()
18
+
19
+ app = typer.Typer(
20
+ name="worker",
21
+ help="⚙️ 运行任务队列 Worker",
22
+ no_args_is_help=False,
23
+ )
24
+
25
+
26
+ def _detect_app_module() -> str:
27
+ """自动检测应用模块路径。"""
28
+ import os
29
+
30
+ # 1. 环境变量
31
+ if app_module := os.environ.get("APP_MODULE"):
32
+ return app_module
33
+
34
+ # 2. pyproject.toml 中的 [tool.aury] 配置
35
+ pyproject_path = Path.cwd() / "pyproject.toml"
36
+ if pyproject_path.exists():
37
+ try:
38
+ import tomllib
39
+ with open(pyproject_path, "rb") as f:
40
+ data = tomllib.load(f)
41
+ if package := data.get("tool", {}).get("aury", {}).get("package"):
42
+ return f"{package}.main:app"
43
+ except Exception:
44
+ pass
45
+
46
+ # 3. 默认
47
+ return "main:app"
48
+
49
+
50
+ @app.callback(invoke_without_command=True)
51
+ def run_worker(
52
+ ctx: typer.Context,
53
+ app_path: str | None = typer.Option(
54
+ None,
55
+ "--app",
56
+ "-a",
57
+ help="应用模块路径(默认自动检测,如 main:app)",
58
+ ),
59
+ concurrency: int = typer.Option(
60
+ 4,
61
+ "--concurrency",
62
+ "-c",
63
+ help="并发 worker 数量",
64
+ ),
65
+ queues: str | None = typer.Option(
66
+ None,
67
+ "--queues",
68
+ "-q",
69
+ help="要处理的队列名称(逗号分隔)",
70
+ ),
71
+ ) -> None:
72
+ """运行任务队列 Worker 进程。
73
+
74
+ Worker 会消费任务队列中的异步任务并执行。
75
+
76
+ 示例:
77
+ aury worker # 默认配置
78
+ aury worker --app main:app # 指定应用模块
79
+ aury worker -c 8 # 8 个并发
80
+ aury worker -q high,default # 只处理指定队列
81
+ """
82
+ if ctx.invoked_subcommand is not None:
83
+ return
84
+
85
+ # 确保当前目录在 Python 路径中
86
+ cwd = str(Path.cwd())
87
+ if cwd not in sys.path:
88
+ sys.path.insert(0, cwd)
89
+
90
+ app_module = app_path or _detect_app_module()
91
+ console.print("[bold cyan]⚙️ 启动 Worker[/bold cyan]")
92
+ console.print(f" 应用: [green]{app_module}[/green]")
93
+ console.print(f" 并发: [green]{concurrency}[/green]")
94
+ if queues:
95
+ console.print(f" 队列: [green]{queues}[/green]")
96
+
97
+ try:
98
+ # 导入应用(确保任务被注册)
99
+ module_path, app_name = app_module.rsplit(":", 1)
100
+ module = __import__(module_path, fromlist=[app_name])
101
+ application = getattr(module, app_name)
102
+
103
+ # 设置日志上下文
104
+ from aury.boot.common.logging import set_service_context
105
+ set_service_context("worker")
106
+
107
+ # 尝试导入 dramatiq
108
+ try:
109
+ import dramatiq
110
+ from dramatiq.cli import main as dramatiq_main
111
+ except ImportError:
112
+ console.print("[red]❌ dramatiq 未安装[/red]")
113
+ console.print("[dim]请安装: uv add 'aury-boot[tasks]'[/dim]")
114
+ raise typer.Exit(1)
115
+
116
+ console.print("[bold green]✅ Worker 启动中...[/bold green]")
117
+ console.print("[dim]按 Ctrl+C 停止[/dim]")
118
+
119
+ # 构建 dramatiq 参数
120
+ args = [
121
+ module_path, # 模块路径
122
+ "--processes", "1",
123
+ "--threads", str(concurrency),
124
+ ]
125
+ if queues:
126
+ args.extend(["--queues", queues])
127
+
128
+ # 运行 dramatiq worker
129
+ sys.argv = ["dramatiq", *args]
130
+ dramatiq_main()
131
+
132
+ except KeyboardInterrupt:
133
+ console.print("\n[yellow]Worker 已停止[/yellow]")
134
+ except ImportError as e:
135
+ console.print(f"[red]❌ 无法导入应用: {e}[/red]")
136
+ console.print("[dim]请确保应用模块路径正确,如 main:app[/dim]")
137
+ raise typer.Exit(1)
138
+ except Exception as e:
139
+ console.print(f"[red]❌ 启动失败: {e}[/red]")
140
+ raise typer.Exit(1)
141
+
142
+
143
+ __all__ = ["app"]
@@ -0,0 +1,35 @@
1
+ """Common 层模块。
2
+
3
+ 最基础层,提供:
4
+ - 异常基类
5
+ - 日志系统
6
+ - 国际化(i18n)
7
+ """
8
+
9
+ from .exceptions import FoundationError
10
+ from .i18n import Translator, get_locale, load_translations, set_locale, translate
11
+ from .logging import (
12
+ get_class_logger,
13
+ log_exceptions,
14
+ log_performance,
15
+ logger,
16
+ setup_logging,
17
+ )
18
+
19
+ __all__ = [
20
+ # 异常
21
+ "FoundationError",
22
+ # 国际化
23
+ "Translator",
24
+ "get_class_logger",
25
+ "get_locale",
26
+ "load_translations",
27
+ "log_exceptions",
28
+ "log_performance",
29
+ # 日志
30
+ "logger",
31
+ "set_locale",
32
+ "setup_logging",
33
+ "translate",
34
+ ]
35
+
@@ -0,0 +1,114 @@
1
+ """基础异常定义。
2
+
3
+ Common 层是最底层,不依赖任何业务层。
4
+ 所有框架异常都应该继承 FoundationError。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+
12
+ class FoundationError(Exception):
13
+ """基础异常类(最底层)。
14
+
15
+ 所有框架异常都应该继承此类。
16
+ 这是整个异常体系的根类。
17
+
18
+ 提供可选的元数据和原因链支持,方便调试和异常包装。
19
+
20
+ 注意:虽然命名为 FoundationError,但它是异常体系的根类,
21
+ 所有其他异常都继承它。
22
+
23
+ Attributes:
24
+ message: 错误消息
25
+ metadata: 可选的元数据字典,用于存储额外的上下文信息
26
+ cause: 可选的原始异常,用于异常链
27
+
28
+ 使用示例:
29
+ # 基本用法
30
+ raise FoundationError("操作失败")
31
+
32
+ # 带元数据
33
+ raise FoundationError(
34
+ "操作失败",
35
+ metadata={"user_id": 123, "operation": "update"}
36
+ )
37
+
38
+ # 包装其他异常
39
+ try:
40
+ risky_operation()
41
+ except ValueError as e:
42
+ raise FoundationError(
43
+ "操作失败",
44
+ cause=e,
45
+ metadata={"context": "data_processing"}
46
+ ) from e
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ message: str,
52
+ *args: object,
53
+ metadata: dict[str, Any] | None = None,
54
+ cause: Exception | None = None,
55
+ ) -> None:
56
+ """初始化异常。
57
+
58
+ Args:
59
+ message: 错误消息
60
+ *args: 其他参数(传递给父类)
61
+ metadata: 可选的元数据字典,用于存储额外的上下文信息
62
+ cause: 可选的原始异常,用于异常链
63
+ """
64
+ self.message = message
65
+ self.metadata = metadata or {}
66
+ self.cause = cause
67
+
68
+ super().__init__(message, *args)
69
+
70
+ # 如果提供了 cause,设置异常链
71
+ if cause is not None:
72
+ self.__cause__ = cause
73
+
74
+ def __str__(self) -> str:
75
+ """返回异常字符串表示。"""
76
+ return self.message
77
+
78
+ def __repr__(self) -> str:
79
+ """返回异常的详细表示,包含元数据信息。"""
80
+ parts = [f"{self.__class__.__name__}(message={self.message!r}"]
81
+
82
+ if self.metadata:
83
+ parts.append(f", metadata={self.metadata}")
84
+
85
+ if self.cause:
86
+ parts.append(f", cause={self.cause.__class__.__name__}")
87
+
88
+ parts.append(")")
89
+ return "".join(parts)
90
+
91
+ def with_metadata(self, **kwargs: Any) -> FoundationError:
92
+ """添加或更新元数据(链式调用)。
93
+
94
+ Args:
95
+ **kwargs: 要添加的元数据键值对
96
+
97
+ Returns:
98
+ FoundationError: 返回自身,支持链式调用
99
+
100
+ 使用示例:
101
+ raise FoundationError("操作失败").with_metadata(
102
+ user_id=123,
103
+ operation="update"
104
+ )
105
+ """
106
+ self.metadata.update(kwargs)
107
+ return self
108
+
109
+
110
+ __all__ = [
111
+ "FoundationError",
112
+ ]
113
+
114
+
@@ -0,0 +1,16 @@
1
+ """国际化模块。
2
+
3
+ 提供多语言翻译、日期/数字本地化等功能。
4
+
5
+ 注意:框架不提供默认翻译字典,用户需要调用 load_translations() 加载翻译资源。
6
+ """
7
+
8
+ from .translator import Translator, get_locale, load_translations, set_locale, translate
9
+
10
+ __all__ = [
11
+ "Translator",
12
+ "get_locale",
13
+ "load_translations",
14
+ "set_locale",
15
+ "translate",
16
+ ]
@@ -0,0 +1,272 @@
1
+ """翻译器。
2
+
3
+ 提供多语言翻译和本地化功能。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from datetime import datetime
9
+ import os
10
+ from typing import Any
11
+
12
+ from babel import Locale, dates, numbers
13
+ from babel.support import Format
14
+
15
+ from aury.boot.common.logging import logger
16
+
17
+ # 全局语言环境
18
+ _current_locale: str | None = None
19
+
20
+ # 翻译字典(由用户提供,框架不提供默认翻译)
21
+ _translations: dict[str, dict[str, str]] = {}
22
+
23
+
24
+ def get_locale() -> str:
25
+ """获取当前语言环境。
26
+
27
+ Returns:
28
+ str: 当前语言环境代码(如 "zh_CN")
29
+ """
30
+ global _current_locale
31
+ if _current_locale is None:
32
+ # 从环境变量获取,默认为中文
33
+ _current_locale = os.getenv("LANG", "zh_CN").split(".")[0] or "zh_CN"
34
+ return _current_locale
35
+
36
+
37
+ def set_locale(locale: str) -> None:
38
+ """设置当前语言环境。
39
+
40
+ Args:
41
+ locale: 语言环境代码(如 "zh_CN", "en_US")
42
+ """
43
+ global _current_locale
44
+ _current_locale = locale
45
+ logger.debug(f"语言环境已设置为: {locale}")
46
+
47
+
48
+ class Translator:
49
+ """翻译器。
50
+
51
+ 提供多语言翻译和本地化功能。
52
+
53
+ 注意:框架不提供默认翻译字典,用户需要自行加载翻译资源。
54
+
55
+ 使用示例:
56
+ # 加载翻译字典
57
+ from aury.boot.common.i18n.translator import Translator, load_translations
58
+
59
+ load_translations({
60
+ "zh_CN": {
61
+ "user.created": "用户 {name} 创建成功",
62
+ "welcome": "欢迎",
63
+ },
64
+ "en_US": {
65
+ "user.created": "User {name} created successfully",
66
+ "welcome": "Welcome",
67
+ },
68
+ })
69
+
70
+ # 使用翻译器
71
+ translator = Translator(locale="zh_CN")
72
+ message = translator.translate("user.created", name="张三")
73
+ # "用户 张三 创建成功"
74
+
75
+ # 日期本地化
76
+ date_str = translator.format_date(datetime.now())
77
+
78
+ # 数字本地化
79
+ number_str = translator.format_number(1234.56)
80
+ """
81
+
82
+ def __init__(self, locale: str | None = None) -> None:
83
+ """初始化翻译器。
84
+
85
+ Args:
86
+ locale: 语言环境代码(如 "zh_CN"),如果为 None 则使用全局设置
87
+ """
88
+ self._locale_str = locale or get_locale()
89
+ try:
90
+ self._locale = Locale.parse(self._locale_str)
91
+ except Exception:
92
+ # 如果解析失败,使用默认值
93
+ logger.warning(f"无法解析语言环境 {self._locale_str},使用默认值 zh_CN")
94
+ self._locale = Locale.parse("zh_CN")
95
+ self._locale_str = "zh_CN"
96
+
97
+ self._formatter = Format(self._locale)
98
+ logger.debug(f"翻译器已初始化: {self._locale_str}")
99
+
100
+ @property
101
+ def locale(self) -> str:
102
+ """获取语言环境代码。"""
103
+ return self._locale_str
104
+
105
+ def translate(self, key: str, default: str | None = None, **kwargs: Any) -> str:
106
+ """翻译文本。
107
+
108
+ Args:
109
+ key: 翻译键
110
+ default: 默认值(如果找不到翻译)
111
+ **kwargs: 格式化参数
112
+
113
+ Returns:
114
+ str: 翻译后的文本
115
+ """
116
+ translations = _translations.get(self._locale_str, {})
117
+ template = translations.get(key, default)
118
+
119
+ if template is None:
120
+ logger.warning(f"翻译键未找到: {key} (locale: {self._locale_str})")
121
+ return key
122
+
123
+ try:
124
+ return template.format(**kwargs)
125
+ except KeyError as e:
126
+ logger.error(f"翻译格式化失败: {key}, 缺少参数: {e}")
127
+ return template
128
+
129
+ def format_date(
130
+ self,
131
+ date: datetime,
132
+ format: str = "medium",
133
+ timezone: str | None = None,
134
+ ) -> str:
135
+ """格式化日期。
136
+
137
+ Args:
138
+ date: 日期时间对象
139
+ format: 格式("short", "medium", "long", "full" 或自定义格式)
140
+ timezone: 时区(可选)
141
+
142
+ Returns:
143
+ str: 格式化后的日期字符串
144
+ """
145
+ try:
146
+ if format in ("short", "medium", "long", "full"):
147
+ return dates.format_date(date, format=format, locale=self._locale)
148
+ else:
149
+ return dates.format_date(date, format=format, locale=self._locale)
150
+ except Exception as e:
151
+ logger.error(f"日期格式化失败: {e}")
152
+ return str(date)
153
+
154
+ def format_datetime(
155
+ self,
156
+ dt: datetime,
157
+ format: str = "medium",
158
+ timezone: str | None = None,
159
+ ) -> str:
160
+ """格式化日期时间。
161
+
162
+ Args:
163
+ dt: 日期时间对象
164
+ format: 格式("short", "medium", "long", "full" 或自定义格式)
165
+ timezone: 时区(可选)
166
+
167
+ Returns:
168
+ str: 格式化后的日期时间字符串
169
+ """
170
+ try:
171
+ if format in ("short", "medium", "long", "full"):
172
+ return dates.format_datetime(dt, format=format, locale=self._locale)
173
+ else:
174
+ return dates.format_datetime(dt, format=format, locale=self._locale)
175
+ except Exception as e:
176
+ logger.error(f"日期时间格式化失败: {e}")
177
+ return str(dt)
178
+
179
+ def format_number(self, number: float, format: str | None = None) -> str:
180
+ """格式化数字。
181
+
182
+ Args:
183
+ number: 数字
184
+ format: 格式(可选,如 "#,##0.00")
185
+
186
+ Returns:
187
+ str: 格式化后的数字字符串
188
+ """
189
+ try:
190
+ if format:
191
+ return numbers.format_number(number, format=format, locale=self._locale)
192
+ else:
193
+ return numbers.format_number(number, locale=self._locale)
194
+ except Exception as e:
195
+ logger.error(f"数字格式化失败: {e}")
196
+ return str(number)
197
+
198
+ def format_currency(self, amount: float, currency: str = "CNY") -> str:
199
+ """格式化货币。
200
+
201
+ Args:
202
+ amount: 金额
203
+ currency: 货币代码(如 "CNY", "USD")
204
+
205
+ Returns:
206
+ str: 格式化后的货币字符串
207
+ """
208
+ try:
209
+ return numbers.format_currency(amount, currency, locale=self._locale)
210
+ except Exception as e:
211
+ logger.error(f"货币格式化失败: {e}")
212
+ return f"{currency} {amount}"
213
+
214
+
215
+ def load_translations(translations: dict[str, dict[str, str]]) -> None:
216
+ """加载翻译字典。
217
+
218
+ 此函数用于加载用户提供的翻译资源。框架本身不提供默认翻译。
219
+
220
+ Args:
221
+ translations: 翻译字典,格式为 {locale: {key: message}}
222
+
223
+ 使用示例:
224
+ load_translations({
225
+ "zh_CN": {
226
+ "user.created": "用户 {name} 创建成功",
227
+ "welcome": "欢迎",
228
+ },
229
+ "en_US": {
230
+ "user.created": "User {name} created successfully",
231
+ "welcome": "Welcome",
232
+ },
233
+ })
234
+ """
235
+ global _translations
236
+ _translations = translations
237
+ logger.info(f"翻译字典已加载,支持语言: {', '.join(translations.keys())}")
238
+
239
+
240
+ def translate(key: str, default: str | None = None, locale: str | None = None, **kwargs: Any) -> str:
241
+ """翻译文本(便捷函数)。
242
+
243
+ Args:
244
+ key: 翻译键
245
+ default: 默认值
246
+ locale: 语言环境(可选,默认使用全局设置)
247
+ **kwargs: 格式化参数
248
+
249
+ Returns:
250
+ str: 翻译后的文本
251
+ """
252
+ translator = Translator(locale=locale)
253
+ return translator.translate(key, default=default, **kwargs)
254
+
255
+
256
+ # 装饰器支持
257
+ def translate_decorator(locale: str | None = None):
258
+ """翻译装饰器。
259
+
260
+ 使用示例:
261
+ @translate_decorator("zh_CN")
262
+ def get_message():
263
+ return _("Welcome") # 会被翻译
264
+ """
265
+ def decorator(func):
266
+ def wrapper(*args, **kwargs):
267
+ result = func(*args, **kwargs)
268
+ if isinstance(result, str):
269
+ return translate(result, locale=locale)
270
+ return result
271
+ return wrapper
272
+ return decorator