springbootAI 1.8.0__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 (175) hide show
  1. spring/__init__.py +66 -0
  2. spring/ai/__init__.py +78 -0
  3. spring/ai/advisors.py +139 -0
  4. spring/ai/annotations.py +74 -0
  5. spring/ai/autoconfig.py +481 -0
  6. spring/ai/core.py +391 -0
  7. spring/ai/etl.py +188 -0
  8. spring/ai/memory.py +109 -0
  9. spring/ai/observability.py +129 -0
  10. spring/ai/providers.py +789 -0
  11. spring/ai/resilience.py +258 -0
  12. spring/ai/tools.py +106 -0
  13. spring/ai/vectorstore.py +303 -0
  14. spring/annotations/__init__.py +188 -0
  15. spring/annotations/cache.py +126 -0
  16. spring/annotations/cloud.py +207 -0
  17. spring/annotations/conditional.py +272 -0
  18. spring/annotations/core.py +864 -0
  19. spring/annotations/messaging.py +107 -0
  20. spring/aop/__init__.py +4 -0
  21. spring/aop/cloud_aop.py +404 -0
  22. spring/aop/comprehensive_aop.py +1015 -0
  23. spring/aop/method_interceptor.py +19 -0
  24. spring/aop/proxy_factory.py +55 -0
  25. spring/cloud/__init__.py +76 -0
  26. spring/cloud/discovery.py +364 -0
  27. spring/cloud/feign.py +469 -0
  28. spring/cloud/gateway.py +452 -0
  29. spring/cloud/load_balancer.py +149 -0
  30. spring/cloud/seata.py +557 -0
  31. spring/cloud/sentinel.py +525 -0
  32. spring/cloud/tracer.py +337 -0
  33. spring/config/__init__.py +21 -0
  34. spring/config/binding.py +206 -0
  35. spring/config/config_loader.py +405 -0
  36. spring/context/__init__.py +13 -0
  37. spring/context/application_context.py +589 -0
  38. spring/context/bean_definition.py +70 -0
  39. spring/context/bean_factory.py +1052 -0
  40. spring/context/registry.py +58 -0
  41. spring/context/scanner.py +106 -0
  42. spring/core/__init__.py +3 -0
  43. spring/core/graceful_shutdown.py +196 -0
  44. spring/core/typing_utils.py +50 -0
  45. spring/csv/__init__.py +52 -0
  46. spring/csv/annotations.py +402 -0
  47. spring/csv/converters.py +69 -0
  48. spring/csv/easy_csv.py +95 -0
  49. spring/csv/exceptions.py +27 -0
  50. spring/csv/reader.py +195 -0
  51. spring/csv/writer.py +155 -0
  52. spring/data/__init__.py +54 -0
  53. spring/data/page.py +181 -0
  54. spring/data/repository.py +274 -0
  55. spring/data/specification.py +228 -0
  56. spring/datasource/__init__.py +66 -0
  57. spring/datasource/annotations.py +133 -0
  58. spring/datasource/context.py +69 -0
  59. spring/datasource/dynamic.py +148 -0
  60. spring/event/__init__.py +7 -0
  61. spring/event/publisher.py +69 -0
  62. spring/excel/__init__.py +51 -0
  63. spring/excel/annotations.py +405 -0
  64. spring/excel/converters.py +231 -0
  65. spring/excel/easy_excel.py +94 -0
  66. spring/excel/exceptions.py +31 -0
  67. spring/excel/reader.py +254 -0
  68. spring/excel/style.py +95 -0
  69. spring/excel/writer.py +197 -0
  70. spring/i18n/__init__.py +97 -0
  71. spring/i18n/accessor.py +94 -0
  72. spring/i18n/auto_config.py +177 -0
  73. spring/i18n/holder.py +106 -0
  74. spring/i18n/locale.py +152 -0
  75. spring/i18n/locale_resolver.py +367 -0
  76. spring/i18n/message_source.py +250 -0
  77. spring/i18n/middleware.py +79 -0
  78. spring/i18n/properties.py +168 -0
  79. spring/i18n/sources.py +255 -0
  80. spring/logging/__init__.py +1 -0
  81. spring/logging/loguru_logger.py +228 -0
  82. spring/main.py +378 -0
  83. spring/messaging/__init__.py +1 -0
  84. spring/messaging/rabbitmq.py +302 -0
  85. spring/monitoring/__init__.py +1 -0
  86. spring/monitoring/prometheus.py +199 -0
  87. spring/orm/__init__.py +258 -0
  88. spring/orm/database.py +222 -0
  89. spring/orm/ddl_auto.py +1217 -0
  90. spring/orm/migration.py +419 -0
  91. spring/orm/mybatis_integration.py +400 -0
  92. spring/orm/pymybatis/__init__.py +86 -0
  93. spring/orm/pymybatis/annotations/__init__.py +30 -0
  94. spring/orm/pymybatis/annotations/annotations.py +332 -0
  95. spring/orm/pymybatis/cache/__init__.py +47 -0
  96. spring/orm/pymybatis/cache/cache.py +371 -0
  97. spring/orm/pymybatis/cache/redis_cache.py +434 -0
  98. spring/orm/pymybatis/circuit_breaker/__init__.py +21 -0
  99. spring/orm/pymybatis/circuit_breaker/circuit_breaker.py +424 -0
  100. spring/orm/pymybatis/configuration.py +525 -0
  101. spring/orm/pymybatis/core/__init__.py +10 -0
  102. spring/orm/pymybatis/core/sql_session.py +1382 -0
  103. spring/orm/pymybatis/core/sql_session_factory.py +76 -0
  104. spring/orm/pymybatis/dialect/__init__.py +9 -0
  105. spring/orm/pymybatis/dialect/dialect.py +445 -0
  106. spring/orm/pymybatis/dynamic_sql/__init__.py +9 -0
  107. spring/orm/pymybatis/dynamic_sql/dynamic_sql.py +900 -0
  108. spring/orm/pymybatis/interceptor/__init__.py +31 -0
  109. spring/orm/pymybatis/interceptor/interceptor.py +427 -0
  110. spring/orm/pymybatis/mapper/__init__.py +9 -0
  111. spring/orm/pymybatis/mapper/mapper.py +540 -0
  112. spring/orm/pymybatis/metrics/__init__.py +41 -0
  113. spring/orm/pymybatis/metrics/metrics.py +595 -0
  114. spring/orm/pymybatis/pool/__init__.py +9 -0
  115. spring/orm/pymybatis/pool/connection_pool.py +711 -0
  116. spring/orm/pymybatis/security/__init__.py +19 -0
  117. spring/orm/pymybatis/security/access_control.py +415 -0
  118. spring/orm/pymybatis/security/password_encoder.py +293 -0
  119. spring/orm/pymybatis/security/sensitive_data_masker.py +326 -0
  120. spring/orm/pymybatis/security/sql_injection_detector.py +675 -0
  121. spring/orm/pymybatis/transaction/__init__.py +9 -0
  122. spring/orm/pymybatis/transaction/transaction.py +288 -0
  123. spring/orm/pymybatis/type_handler/__init__.py +37 -0
  124. spring/orm/pymybatis/type_handler/type_handler.py +473 -0
  125. spring/orm/pymybatis/version.py +9 -0
  126. spring/orm/pymybatis/xml_parser/__init__.py +9 -0
  127. spring/orm/pymybatis/xml_parser/xml_parser.py +761 -0
  128. spring/retry/__init__.py +12 -0
  129. spring/retry/retry_annotations.py +71 -0
  130. spring/retry/retry_decorator.py +155 -0
  131. spring/scheduling/__init__.py +3 -0
  132. spring/scheduling/scheduler.py +389 -0
  133. spring/security/__init__.py +39 -0
  134. spring/security/jwt_utils.py +281 -0
  135. spring/security/replay_protection.py +206 -0
  136. spring/security/secret_manager.py +226 -0
  137. spring/security/security_aop.py +248 -0
  138. spring/security/security_context.py +172 -0
  139. spring/test/__init__.py +45 -0
  140. spring/test/slicing.py +341 -0
  141. spring/tracing/__init__.py +11 -0
  142. spring/tracing/skywalking.py +229 -0
  143. spring/tx/__init__.py +52 -0
  144. spring/tx/events.py +172 -0
  145. spring/tx/synchronization.py +143 -0
  146. spring/utils/__init__.py +5 -0
  147. spring/utils/banner.py +32 -0
  148. spring/utils/logger.py +73 -0
  149. spring/utils/redis_client.py +526 -0
  150. spring/validation/__init__.py +55 -0
  151. spring/validation/aop.py +141 -0
  152. spring/validation/constraints.py +357 -0
  153. spring/validation/exceptions.py +55 -0
  154. spring/validation/validator.py +139 -0
  155. spring/web/__init__.py +12 -0
  156. spring/web/actuator.py +319 -0
  157. spring/web/exception_handler.py +61 -0
  158. spring/web/health.py +399 -0
  159. spring/web/interceptor.py +91 -0
  160. spring/web/result.py +44 -0
  161. spring/web/swagger.py +601 -0
  162. spring/web/web_context.py +755 -0
  163. spring/websocket/__init__.py +86 -0
  164. spring/websocket/annotations.py +169 -0
  165. spring/websocket/broker.py +238 -0
  166. spring/websocket/exceptions.py +26 -0
  167. spring/websocket/handler.py +243 -0
  168. spring/websocket/router.py +526 -0
  169. spring/websocket/session.py +216 -0
  170. springbootai-1.8.0.dist-info/METADATA +2796 -0
  171. springbootai-1.8.0.dist-info/RECORD +175 -0
  172. springbootai-1.8.0.dist-info/WHEEL +5 -0
  173. springbootai-1.8.0.dist-info/entry_points.txt +2 -0
  174. springbootai-1.8.0.dist-info/licenses/LICENSE +7 -0
  175. springbootai-1.8.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,97 @@
1
+ """SpringBootAI i18n 国际化模块 —— 注解驱动的消息源与区域解析(对齐 Spring ``MessageSource`` +
2
+ ``LocaleResolver`` 体系)。
3
+
4
+ 模块组成(镜像 ``org.springframework.context.*`` / ``org.springframework.web.servlet.i18n.*``):
5
+
6
+ - **locale**: ``Locale`` 区域对象(``language``/``country``/``variant``),对齐
7
+ ``java.util.Locale`` 的字符串表示(``en``/``en_US``/``zh_CN``)
8
+ - **message_source**: ``MessageSource`` 接口 + ``AbstractMessageSource`` 抽象基类 +
9
+ ``NoSuchMessageException`` + ``MessageSourceResolvable``
10
+ - **sources**: ``StaticMessageSource``(编程式)/ ``ResourceBundleMessageSource``
11
+ (资源包加载,支持 ``.properties`` 与 ``.yml``)/ ``DelegatingMessageSource``
12
+ (父级回退,对齐 ``AbstractApplicationContext`` 的内嵌实现)
13
+ - **locale_resolver**: ``LocaleResolver`` 接口 + ``LocaleContext`` + ``AcceptHeaderLocaleResolver``
14
+ / ``FixedLocaleResolver`` / ``SessionLocaleResolver`` / ``CookieLocaleResolver``
15
+ - **holder**: ``LocaleContextHolder`` 线程/协程安全上下文持有器(``ContextVar``)
16
+ - **accessor**: ``MessageSourceAccessor`` 便捷访问器(提供无异常 ``getMessage`` 变体)
17
+ - **properties**: Java 风格 ``.properties`` 文件解析器(UTF-8,支持转义/续行)
18
+ - **middleware**: ``LocaleResolverMiddleware`` Starlette 中间件,从请求解析并设置 ``LocaleContext``
19
+ - **auto_config**: ``MessageSourceAutoConfiguration`` 默认装配(``spring.messages.basename``)
20
+
21
+ 设计原则:**复用项目既有范式,不重复造轮子**。本模块:
22
+ - 不依赖任何第三方库(``.properties`` 解析自实现,``.yml`` 复用项目核心依赖 ``pyyaml``)。
23
+ - 注解描述符范式与 ``spring.excel`` / ``spring.csv`` 一致。
24
+ - ``LocaleContextHolder`` 复用 ``ContextVar`` 模式,与 ``spring.datasource`` 动态路由一致。
25
+
26
+ 与 Java 的差异:
27
+ - Spring 用 ``ResourceBundle`` 加载类路径资源;本实现用文件系统路径 + ``basenames`` 列表。
28
+ - ``MessageFormat`` 类型子模式(``{0,number,#.##}``)降级为 ``str.format``,类型符忽略;
29
+ 位置占位符 ``{0}``/``{1}`` 与 Java 行为一致。
30
+ - 不支持 ``ResourceBundle.Control`` 自定义加载策略(可按需扩展)。
31
+ """
32
+ from .locale import (
33
+ Locale,
34
+ LOCALE_EN, LOCALE_US, LOCALE_UK,
35
+ LOCALE_CHINA, LOCALE_TAIWAN, LOCALE_JAPAN, LOCALE_KOREA,
36
+ LOCALE_GERMANY, LOCALE_FRANCE,
37
+ parse_locale,
38
+ )
39
+ from .message_source import (
40
+ MessageSource,
41
+ AbstractMessageSource,
42
+ NoSuchMessageException,
43
+ MessageSourceResolvable,
44
+ DefaultMessageSourceResolvable,
45
+ )
46
+ from .sources import (
47
+ StaticMessageSource,
48
+ ResourceBundleMessageSource,
49
+ DelegatingMessageSource,
50
+ )
51
+ from .locale_resolver import (
52
+ LocaleResolver,
53
+ LocaleContext,
54
+ SimpleLocaleContext,
55
+ SimpleTimeZoneAwareLocaleContext,
56
+ AcceptHeaderLocaleResolver,
57
+ FixedLocaleResolver,
58
+ SessionLocaleResolver,
59
+ CookieLocaleResolver,
60
+ parse_accept_language,
61
+ )
62
+ from .holder import LocaleContextHolder
63
+ from .accessor import MessageSourceAccessor
64
+ from .properties import load_properties, parse_properties
65
+ from .middleware import LocaleResolverMiddleware, get_request_locale
66
+ from .auto_config import MessageSourceAutoConfiguration, configure_message_source
67
+
68
+ __version__ = "1.0.0"
69
+
70
+ __all__ = [
71
+ # Locale
72
+ "Locale",
73
+ "LOCALE_EN", "LOCALE_US", "LOCALE_UK",
74
+ "LOCALE_CHINA", "LOCALE_TAIWAN", "LOCALE_JAPAN", "LOCALE_KOREA",
75
+ "LOCALE_GERMANY", "LOCALE_FRANCE",
76
+ "parse_locale",
77
+ # MessageSource
78
+ "MessageSource", "AbstractMessageSource",
79
+ "NoSuchMessageException", "MessageSourceResolvable", "DefaultMessageSourceResolvable",
80
+ # Sources
81
+ "StaticMessageSource", "ResourceBundleMessageSource", "DelegatingMessageSource",
82
+ # LocaleResolver
83
+ "LocaleResolver", "LocaleContext", "SimpleLocaleContext",
84
+ "SimpleTimeZoneAwareLocaleContext",
85
+ "AcceptHeaderLocaleResolver", "FixedLocaleResolver",
86
+ "SessionLocaleResolver", "CookieLocaleResolver",
87
+ "parse_accept_language",
88
+ # Holder / Accessor
89
+ "LocaleContextHolder", "MessageSourceAccessor",
90
+ # Properties
91
+ "load_properties", "parse_properties",
92
+ # Middleware
93
+ "LocaleResolverMiddleware", "get_request_locale",
94
+ # Auto config
95
+ "MessageSourceAutoConfiguration", "configure_message_source",
96
+ "__version__",
97
+ ]
@@ -0,0 +1,94 @@
1
+ """``MessageSourceAccessor`` 便捷访问器(对齐 Spring ``MessageSourceAccessor``)。
2
+
3
+ 包装一个 ``MessageSource`` + 默认 ``Locale``,提供更简洁的 API:
4
+
5
+ - ``getMessage(code, args, locale)`` — 抛异常变体
6
+ - ``getMessage(code, args, default, locale)`` — 默认消息变体
7
+ - ``getMessage(resolvable, locale)`` — resolvable 变体
8
+
9
+ 所有 ``locale`` 参数可选,缺省用构造时传入的默认 locale(对齐 Spring 同名行为)。
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from typing import Optional
14
+
15
+ from .locale import Locale
16
+ from .message_source import (
17
+ MessageArgs,
18
+ MessageSource,
19
+ MessageSourceResolvable,
20
+ NoSuchMessageException,
21
+ )
22
+
23
+
24
+ class MessageSourceAccessor:
25
+ """``MessageSource`` 便捷访问器。
26
+
27
+ Args:
28
+ message_source: 被包装的消息源。
29
+ default_locale: 默认 locale(请求未指定时使用)。
30
+ """
31
+
32
+ def __init__(self, message_source: MessageSource, default_locale: Optional[Locale] = None):
33
+ self._source = message_source
34
+ self._default_locale = default_locale if default_locale is not None else Locale("")
35
+
36
+ @property
37
+ def message_source(self) -> MessageSource:
38
+ return self._source
39
+
40
+ @property
41
+ def default_locale(self) -> Locale:
42
+ return self._default_locale
43
+
44
+ def set_default_locale(self, locale: Locale) -> None:
45
+ self._default_locale = locale
46
+
47
+ # ==================== 便捷方法 ====================
48
+
49
+ def getMessage( # noqa: N802 - 保留 Java 驼峰命名以对齐 Spring API
50
+ self,
51
+ code: str,
52
+ args: MessageArgs = None,
53
+ locale: Optional[Locale] = None,
54
+ ) -> str:
55
+ """按 code 解析消息,找不到抛 ``NoSuchMessageException``。"""
56
+ return self._source.getMessage(code, args, self._locale(locale))
57
+
58
+ def getMessageOrDefault(
59
+ self,
60
+ code: str,
61
+ args: MessageArgs = None,
62
+ default_message: Optional[str] = None,
63
+ locale: Optional[Locale] = None,
64
+ ) -> Optional[str]:
65
+ """按 code 解析消息,找不到返回 ``default_message``。"""
66
+ return self._source.getMessageOrDefault(code, args, default_message, self._locale(locale))
67
+
68
+ def getMessageFromResolvable(
69
+ self,
70
+ resolvable: MessageSourceResolvable,
71
+ locale: Optional[Locale] = None,
72
+ ) -> str:
73
+ return self._source.getMessageFromResolvable(resolvable, self._locale(locale))
74
+
75
+ # 别名(Python 风格)
76
+ def get_message(self, code: str, args: MessageArgs = None, locale: Optional[Locale] = None) -> str:
77
+ return self.getMessage(code, args, locale)
78
+
79
+ def get_message_or_default(
80
+ self,
81
+ code: str,
82
+ args: MessageArgs = None,
83
+ default_message: Optional[str] = None,
84
+ locale: Optional[Locale] = None,
85
+ ) -> Optional[str]:
86
+ return self.getMessageOrDefault(code, args, default_message, locale)
87
+
88
+ # ==================== 内部 ====================
89
+
90
+ def _locale(self, locale: Optional[Locale]) -> Locale:
91
+ return locale if locale is not None else self._default_locale
92
+
93
+
94
+ __all__ = ["MessageSourceAccessor"]
@@ -0,0 +1,177 @@
1
+ """``MessageSourceAutoConfiguration`` 默认装配(对齐 Spring Boot
2
+ ``MessageSourceAutoConfiguration``)。
3
+
4
+ 从 ``application.yml`` 读取 ``spring.messages.basename`` / ``spring.messages.encoding``
5
+ 等配置,构造默认 ``ResourceBundleMessageSource`` 并注册为容器单例 Bean。
6
+
7
+ 配置示例(``application.yml``)::
8
+
9
+ spring:
10
+ messages:
11
+ basename: messages,errors # 资源基名列表(逗号分隔),默认 messages
12
+ encoding: UTF-8 # 资源文件编码,默认 UTF-8
13
+ base-dir: classpath:i18n # 资源根目录,默认 classpath:i18n
14
+ fallback-to-system-locale: true
15
+ use-code-as-default-message: false
16
+
17
+ ``base-dir`` 取值:
18
+ - ``classpath:xxx`` → 相对当前工作目录的 ``xxx`` 子目录(对齐 Spring ``classpath:`` 前缀语义)
19
+ - 绝对/相对路径 → 直接使用
20
+ - 未配置 → 默认 ``i18n``(即 ``./i18n/messages_*.properties``)
21
+
22
+ 集成方式:
23
+ 1. ``configure_message_source(config_loader)`` 工厂函数:从配置构造消息源。
24
+ 2. ``ApplicationContext`` 启动时调用本模块注册 ``messageSource`` Bean(见下方钩子)。
25
+ 3. 应用层通过 ``context.get_bean("messageSource")`` 或 ``get_bean_by_type(MessageSource)`` 获取。
26
+ """
27
+ from __future__ import annotations
28
+
29
+ import os
30
+ from typing import Any, Iterable, List, Optional
31
+
32
+ from .locale import Locale
33
+ from .message_source import MessageSource
34
+ from .sources import ResourceBundleMessageSource
35
+
36
+
37
+ # Bean 名称(对齐 Spring ``messageSource``)
38
+ MESSAGE_SOURCE_BEAN_NAME = "messageSource"
39
+
40
+
41
+ def _split_basenames(raw: str) -> List[str]:
42
+ """逗号分隔的 basename 列表解析;去除空白与空项。"""
43
+ if not raw:
44
+ return ["messages"]
45
+ return [b.strip() for b in raw.split(",") if b.strip()]
46
+
47
+
48
+ def _resolve_base_dir(raw: str) -> str:
49
+ """解析 ``base-dir`` 配置。
50
+
51
+ - ``classpath:xxx`` → ``xxx``(相对工作目录;Spring ``classpath:`` 在此实现为文件系统等价)
52
+ - 其他 → 原样返回
53
+ - 未提供 → ``i18n``
54
+ """
55
+ if not raw:
56
+ return "i18n"
57
+ if raw.startswith("classpath:"):
58
+ return raw[len("classpath:"):]
59
+ return raw
60
+
61
+
62
+ def configure_message_source(
63
+ config_loader: Optional[Any] = None,
64
+ basenames: Optional[Iterable[str]] = None,
65
+ base_dir: Optional[str] = None,
66
+ encoding: Optional[str] = None,
67
+ fallback_to_system_locale: Optional[bool] = None,
68
+ use_code_as_default_message: Optional[bool] = None,
69
+ default_locale: Optional[Locale] = None,
70
+ ) -> ResourceBundleMessageSource:
71
+ """从 ``config_loader`` 与显式参数构造 ``ResourceBundleMessageSource``。
72
+
73
+ 优先级:显式参数 > ``config_loader`` 读取的 ``spring.messages.*`` > 默认值。
74
+
75
+ Args:
76
+ config_loader: ``spring.config.ConfigLoader`` 实例(可为 None)。
77
+ basenames: 资源基名列表。
78
+ base_dir: 资源根目录。
79
+ encoding: 资源编码。
80
+ fallback_to_system_locale: locale 未命中是否回退到系统 locale。
81
+ use_code_as_default_message: 找不到消息时是否把 code 作为默认消息。
82
+ default_locale: 默认 locale。
83
+
84
+ Returns:
85
+ 配置好的 ``ResourceBundleMessageSource`` 单例(每次调用返回新实例)。
86
+ """
87
+ # 从 config_loader 读取默认值
88
+ cfg_basenames = ["messages"]
89
+ cfg_base_dir = "i18n"
90
+ cfg_encoding = "utf-8"
91
+ cfg_fallback = True
92
+ cfg_use_code = False
93
+
94
+ if config_loader is not None:
95
+ try:
96
+ msgs_cfg = config_loader.get_prefix_config("spring.messages") or {}
97
+ if msgs_cfg.get("basename"):
98
+ cfg_basenames = _split_basenames(msgs_cfg["basename"])
99
+ if msgs_cfg.get("base-dir") or msgs_cfg.get("base_dir"):
100
+ cfg_base_dir = _resolve_base_dir(
101
+ msgs_cfg.get("base-dir") or msgs_cfg.get("base_dir")
102
+ )
103
+ if msgs_cfg.get("encoding"):
104
+ cfg_encoding = msgs_cfg["encoding"]
105
+ if msgs_cfg.get("fallback-to-system-locale") is not None:
106
+ cfg_fallback = bool(msgs_cfg.get("fallback-to-system-locale"))
107
+ if msgs_cfg.get("use-code-as-default-message") is not None:
108
+ cfg_use_code = bool(msgs_cfg.get("use-code-as-default-message"))
109
+ except Exception:
110
+ # 配置缺失或异常:用默认值
111
+ pass
112
+
113
+ final_basenames = list(basenames) if basenames is not None else cfg_basenames
114
+ final_base_dir = base_dir if base_dir is not None else cfg_base_dir
115
+ final_encoding = encoding if encoding is not None else cfg_encoding
116
+ final_fallback = fallback_to_system_locale if fallback_to_system_locale is not None else cfg_fallback
117
+ final_use_code = use_code_as_default_message if use_code_as_default_message is not None else cfg_use_code
118
+ final_default_locale = default_locale if default_locale is not None else Locale("")
119
+
120
+ source = ResourceBundleMessageSource(
121
+ basenames=final_basenames,
122
+ base_dir=final_base_dir,
123
+ default_encoding=final_encoding,
124
+ fallback_to_system_locale=final_fallback,
125
+ default_locale=final_default_locale,
126
+ )
127
+ if final_use_code:
128
+ source.set_use_code_as_default_message(True)
129
+ return source
130
+
131
+
132
+ class MessageSourceAutoConfiguration:
133
+ """消息源自动配置(对齐 Spring Boot ``MessageSourceAutoConfiguration``)。
134
+
135
+ 静态方法 ``register(context)``:从 ``context.config_loader`` 读取配置,构造
136
+ ``ResourceBundleMessageSource`` 并注册为 ``messageSource`` Bean。若已存在同名 Bean 则跳过。
137
+ """
138
+
139
+ @staticmethod
140
+ def register(context: Any) -> Optional[MessageSource]:
141
+ """向 ``ApplicationContext`` 注册 ``messageSource`` Bean。
142
+
143
+ Returns:
144
+ 注册的消息源实例;若已存在同名 Bean 则返回该实例。
145
+ """
146
+ bean_factory = getattr(context, "bean_factory", None) or getattr(context, "_bean_factory", None)
147
+ if bean_factory is None:
148
+ return None
149
+ # 已存在同名 Bean:跳过(对齐 Spring ``@ConditionalOnMissingBean``)
150
+ existing = bean_factory._bean_definitions.get(MESSAGE_SOURCE_BEAN_NAME) \
151
+ if hasattr(bean_factory, "_bean_definitions") else None
152
+ if existing is not None:
153
+ try:
154
+ return bean_factory.get_bean(MESSAGE_SOURCE_BEAN_NAME)
155
+ except Exception:
156
+ return None
157
+
158
+ config_loader = getattr(context, "config_loader", None)
159
+ source = configure_message_source(config_loader)
160
+
161
+ # 注册为实例(BeanFactory.register_instance 直接放 _bean_instances)
162
+ try:
163
+ bean_factory.register_instance(MESSAGE_SOURCE_BEAN_NAME, source)
164
+ # 同步 type_to_name 索引(register_instance 已做,保险起见)
165
+ from .message_source import MessageSource as _MS
166
+ bean_factory._type_to_name.setdefault(_MS, MESSAGE_SOURCE_BEAN_NAME)
167
+ bean_factory._type_to_name.setdefault(ResourceBundleMessageSource, MESSAGE_SOURCE_BEAN_NAME)
168
+ except Exception:
169
+ pass
170
+ return source
171
+
172
+
173
+ __all__ = [
174
+ "MESSAGE_SOURCE_BEAN_NAME",
175
+ "configure_message_source",
176
+ "MessageSourceAutoConfiguration",
177
+ ]
spring/i18n/holder.py ADDED
@@ -0,0 +1,106 @@
1
+ """``LocaleContextHolder`` 区域上下文持有器(对齐 Spring ``LocaleContextHolder``)。
2
+
3
+ 使用 ``ContextVar`` 保证线程与 ``asyncio`` 协程间的隔离(与 ``spring.datasource`` 动态路由
4
+ 的 ``ContextVar`` 模式一致)。
5
+
6
+ 核心 API:
7
+ - ``set_locale_context(ctx)`` / ``get_locale_context()``:设置/获取 ``LocaleContext``
8
+ - ``set_locale_context`` 返回 token,可用 ``reset_locale_context(token)`` 精确复位
9
+ - ``set_locale(locale)`` / ``get_locale()``:便捷方法,等价于包装/解包 ``SimpleLocaleContext``
10
+ - ``reset_locale_context(token=None)``:清除当前上下文(对齐 Spring ``resetLocaleContext``)
11
+ - ``set_default_locale(locale)``:设置全局默认 locale(对齐 Spring 同名静态方法)
12
+
13
+ 嵌套调用:``ContextVar.reset(token)`` 天然支持嵌套——内层请求退出后自动恢复外层 locale,
14
+ 与 ``spring.datasource.DataSourceContextHolder`` 完全一致。
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import contextvars
19
+ import threading
20
+ from typing import Optional
21
+
22
+ from .locale import Locale
23
+ from .locale_resolver import LocaleContext, SimpleLocaleContext
24
+
25
+
26
+ # ContextVar:协程安全 + 线程安全(Python 3.10+ 推荐)
27
+ _locale_context_var: "contextvars.ContextVar[Optional[LocaleContext]]" = contextvars.ContextVar(
28
+ "spring_locale_context", default=None
29
+ )
30
+
31
+ # 全局默认 locale(进程级,所有线程/协程共享)
32
+ _default_locale_lock = threading.Lock()
33
+ _default_locale: Optional[Locale] = None
34
+
35
+
36
+ class LocaleContextHolder:
37
+ """区域上下文持有器(对齐 Spring ``LocaleContextHolder``)。
38
+
39
+ 所有方法均为 ``@staticmethod``,状态保存在 ``ContextVar`` 与全局默认变量中。
40
+ """
41
+
42
+ @staticmethod
43
+ def set_locale_context(context: Optional[LocaleContext]):
44
+ """设置当前上下文的 ``LocaleContext``;``None`` 等价于清除。
45
+
46
+ 返回 ``ContextVar.set`` 的 token,可用 ``reset_locale_context(token)`` 精确复位。
47
+ """
48
+ return _locale_context_var.set(context)
49
+
50
+ @staticmethod
51
+ def get_locale_context() -> LocaleContext:
52
+ """获取当前 ``LocaleContext``;不存在时返回 ``SimpleLocaleContext(default_locale)``。"""
53
+ ctx = _locale_context_var.get()
54
+ if ctx is not None:
55
+ return ctx
56
+ return SimpleLocaleContext(LocaleContextHolder.get_default_locale())
57
+
58
+ @staticmethod
59
+ def set_locale(locale: Optional[Locale]):
60
+ """便捷方法:用 ``SimpleLocaleContext`` 包装 locale 设置到上下文。返回 token。"""
61
+ return LocaleContextHolder.set_locale_context(
62
+ SimpleLocaleContext(locale) if locale is not None else None
63
+ )
64
+
65
+ @staticmethod
66
+ def get_locale() -> Locale:
67
+ """获取当前 locale;上下文未设置时返回全局默认 locale(可能为空 ``Locale``)。"""
68
+ return LocaleContextHolder.get_locale_context().get_locale()
69
+
70
+ @staticmethod
71
+ def reset_locale_context(token=None) -> None:
72
+ """清除当前上下文(对齐 Spring ``resetLocaleContext``)。
73
+
74
+ - 传入 ``token``:精确复位到 ``set_locale_context`` 之前的值(支持嵌套)。
75
+ - 不传 ``token``:直接置为 None(慎用,会丢失嵌套层级)。
76
+ """
77
+ if token is not None:
78
+ try:
79
+ _locale_context_var.reset(token)
80
+ return
81
+ except (ValueError, LookupError):
82
+ # token 不属于当前上下文(跨协程误用),兜底置 None
83
+ pass
84
+ _locale_context_var.set(None)
85
+
86
+ # 兼容简短别名
87
+ @staticmethod
88
+ def reset(token=None) -> None:
89
+ LocaleContextHolder.reset_locale_context(token)
90
+
91
+ @staticmethod
92
+ def set_default_locale(locale: Optional[Locale]) -> None:
93
+ """设置进程级默认 locale;未设置上下文时 ``get_locale`` 返回此值。"""
94
+ global _default_locale
95
+ with _default_locale_lock:
96
+ _default_locale = locale
97
+
98
+ @staticmethod
99
+ def get_default_locale() -> Locale:
100
+ """获取进程级默认 locale;未设置返回空 ``Locale``。"""
101
+ with _default_locale_lock:
102
+ loc = _default_locale
103
+ return loc if loc is not None else Locale()
104
+
105
+
106
+ __all__ = ["LocaleContextHolder"]
spring/i18n/locale.py ADDED
@@ -0,0 +1,152 @@
1
+ """``Locale`` 区域对象(对齐 ``java.util.Locale``)。
2
+
3
+ 表示一个特定的地理、政治或文化区域。本实现采用最常见的 ``language_country`` 字符串
4
+ 表示(如 ``en``/``en_US``/``zh_CN``),同时支持 BCP 47 语言标签(``en-US``/``zh-CN``)。
5
+
6
+ 与 Java 的差异:
7
+ - Java ``Locale`` 是不可变重量级对象(含 ``Locale.Builder``);本实现是轻量不可变 dataclass 风格。
8
+ - 不支持 ``Locale.LanguageRange`` 列表解析的完整 RFC 4647 算法,仅实现最常见的
9
+ 前缀匹配(``AcceptHeaderLocaleResolver`` 用)。
10
+ - ``getDisplayLanguage`` 等显示名 API 不实现(依赖 ``Locale`` 数据集),使用方可按需扩展。
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from typing import Optional
15
+
16
+
17
+ class Locale:
18
+ """不可变区域对象。
19
+
20
+ Args:
21
+ language: ISO 639 语言代码(小写,如 ``en``/``zh``)。
22
+ country: ISO 3166 国家代码(大写,如 ``US``/``CN``)。可选。
23
+ variant: 变体(任意大小写,如 ``POSIX``/``Traditional_WIN``)。可选。
24
+
25
+ 规范化:
26
+ - ``language`` 转小写;``country`` 转大写;``variant`` 保留原样。
27
+ - 空/None 视为未设置。
28
+ """
29
+
30
+ __slots__ = ("language", "country", "variant")
31
+
32
+ def __init__(self, language: str = "", country: str = "", variant: str = ""):
33
+ self.language = (language or "").lower()
34
+ self.country = (country or "").upper()
35
+ self.variant = variant or ""
36
+
37
+ # ==================== 工厂 ====================
38
+
39
+ @classmethod
40
+ def parse(cls, tag: str) -> "Locale":
41
+ """从字符串解析 ``Locale``,兼容以下格式:
42
+
43
+ - ``en`` / ``zh`` → ``Locale("en")``
44
+ - ``en_US`` / ``zh_CN`` → ``Locale("en", "US")``
45
+ - ``en_US_POSIX`` → ``Locale("en", "US", "POSIX")``
46
+ - ``en-US`` / ``zh-CN``(BCP 47,``-`` 分隔)→ 同上
47
+ - ``en-US-x-posix``(BCP 47 私有用) → variant 取最后一段
48
+ """
49
+ if not tag:
50
+ return cls()
51
+ tag = tag.strip()
52
+ # 统一分隔符:BCP 47 用 '-',Java 用 '_';优先拆 '_' 再拆 '-'
53
+ # 仅当不含 '_' 时才替换 '-',避免误处理含 '_' 的 variant
54
+ if "_" in tag:
55
+ parts = tag.split("_")
56
+ elif "-" in tag:
57
+ parts = tag.split("-")
58
+ else:
59
+ parts = [tag]
60
+ language = parts[0] if len(parts) >= 1 else ""
61
+ country = parts[1] if len(parts) >= 2 else ""
62
+ # variant:第 3 段及以上拼接(Java 风格单段 variant 取最后一段即可)
63
+ variant = parts[2] if len(parts) >= 3 else ""
64
+ # 处理 BCP 47 扩展:x-private 子段拼到 variant
65
+ if len(parts) > 3:
66
+ variant = "_".join(parts[2:])
67
+ return cls(language, country, variant)
68
+
69
+ # ==================== 表示 ====================
70
+
71
+ def to_string(self) -> str:
72
+ """Java ``toString`` 风格:``en``/``en_US``/``en_US_POSIX``。"""
73
+ if self.variant:
74
+ return f"{self.language}_{self.country}_{self.variant}" if self.country else \
75
+ f"{self.language}__{self.variant}"
76
+ if self.country:
77
+ return f"{self.language}_{self.country}"
78
+ return self.language
79
+
80
+ def to_language_tag(self) -> str:
81
+ """BCP 47 语言标签:``en``/``en-US``/``zh-CN``。"""
82
+ if self.variant:
83
+ return f"{self.language}-{self.country}-{self.variant}" if self.country else \
84
+ f"{self.language}-x-{self.variant}"
85
+ if self.country:
86
+ return f"{self.language}-{self.country}"
87
+ return self.language
88
+
89
+ def __str__(self) -> str:
90
+ return self.to_string()
91
+
92
+ def __repr__(self) -> str:
93
+ return f"Locale({self.to_string()!r})"
94
+
95
+ # ==================== 比较 / 哈希 ====================
96
+
97
+ def __eq__(self, other: object) -> bool:
98
+ if not isinstance(other, Locale):
99
+ return NotImplemented
100
+ return (self.language, self.country, self.variant) == \
101
+ (other.language, other.country, other.variant)
102
+
103
+ def __hash__(self) -> int:
104
+ return hash((self.language, self.country, self.variant))
105
+
106
+ # ==================== 工具 ====================
107
+
108
+ @property
109
+ def is_empty(self) -> bool:
110
+ return not (self.language or self.country or self.variant)
111
+
112
+ def matches(self, other: "Locale") -> bool:
113
+ """前缀匹配:``Locale("en")`` matches ``Locale("en_US")``,反之不成立。
114
+
115
+ 用于 ``AcceptHeaderLocaleResolver`` 的回退匹配。
116
+ """
117
+ if self.is_empty or other.is_empty:
118
+ return False
119
+ if self.language != other.language:
120
+ return False
121
+ if self.country and other.country and self.country != other.country:
122
+ return False
123
+ if self.country and not other.country:
124
+ return False # self 更具体,不能 matches 更宽泛的 other
125
+ return True
126
+
127
+
128
+ # ==================== 预定义常量(对齐 Java ``Locale.*``) ====================
129
+
130
+ LOCALE_EN = Locale("en")
131
+ LOCALE_US = Locale("en", "US")
132
+ LOCALE_UK = Locale("en", "GB")
133
+ LOCALE_CHINA = Locale("zh", "CN")
134
+ LOCALE_TAIWAN = Locale("zh", "TW")
135
+ LOCALE_JAPAN = Locale("ja", "JP")
136
+ LOCALE_KOREA = Locale("ko", "KR")
137
+ LOCALE_GERMANY = Locale("de", "DE")
138
+ LOCALE_FRANCE = Locale("fr", "FR")
139
+
140
+
141
+ def parse_locale(tag: str) -> Locale:
142
+ """``Locale.parse`` 的函数式别名。"""
143
+ return Locale.parse(tag)
144
+
145
+
146
+ __all__ = [
147
+ "Locale",
148
+ "LOCALE_EN", "LOCALE_US", "LOCALE_UK",
149
+ "LOCALE_CHINA", "LOCALE_TAIWAN", "LOCALE_JAPAN", "LOCALE_KOREA",
150
+ "LOCALE_GERMANY", "LOCALE_FRANCE",
151
+ "parse_locale",
152
+ ]