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.
- spring/__init__.py +66 -0
- spring/ai/__init__.py +78 -0
- spring/ai/advisors.py +139 -0
- spring/ai/annotations.py +74 -0
- spring/ai/autoconfig.py +481 -0
- spring/ai/core.py +391 -0
- spring/ai/etl.py +188 -0
- spring/ai/memory.py +109 -0
- spring/ai/observability.py +129 -0
- spring/ai/providers.py +789 -0
- spring/ai/resilience.py +258 -0
- spring/ai/tools.py +106 -0
- spring/ai/vectorstore.py +303 -0
- spring/annotations/__init__.py +188 -0
- spring/annotations/cache.py +126 -0
- spring/annotations/cloud.py +207 -0
- spring/annotations/conditional.py +272 -0
- spring/annotations/core.py +864 -0
- spring/annotations/messaging.py +107 -0
- spring/aop/__init__.py +4 -0
- spring/aop/cloud_aop.py +404 -0
- spring/aop/comprehensive_aop.py +1015 -0
- spring/aop/method_interceptor.py +19 -0
- spring/aop/proxy_factory.py +55 -0
- spring/cloud/__init__.py +76 -0
- spring/cloud/discovery.py +364 -0
- spring/cloud/feign.py +469 -0
- spring/cloud/gateway.py +452 -0
- spring/cloud/load_balancer.py +149 -0
- spring/cloud/seata.py +557 -0
- spring/cloud/sentinel.py +525 -0
- spring/cloud/tracer.py +337 -0
- spring/config/__init__.py +21 -0
- spring/config/binding.py +206 -0
- spring/config/config_loader.py +405 -0
- spring/context/__init__.py +13 -0
- spring/context/application_context.py +589 -0
- spring/context/bean_definition.py +70 -0
- spring/context/bean_factory.py +1052 -0
- spring/context/registry.py +58 -0
- spring/context/scanner.py +106 -0
- spring/core/__init__.py +3 -0
- spring/core/graceful_shutdown.py +196 -0
- spring/core/typing_utils.py +50 -0
- spring/csv/__init__.py +52 -0
- spring/csv/annotations.py +402 -0
- spring/csv/converters.py +69 -0
- spring/csv/easy_csv.py +95 -0
- spring/csv/exceptions.py +27 -0
- spring/csv/reader.py +195 -0
- spring/csv/writer.py +155 -0
- spring/data/__init__.py +54 -0
- spring/data/page.py +181 -0
- spring/data/repository.py +274 -0
- spring/data/specification.py +228 -0
- spring/datasource/__init__.py +66 -0
- spring/datasource/annotations.py +133 -0
- spring/datasource/context.py +69 -0
- spring/datasource/dynamic.py +148 -0
- spring/event/__init__.py +7 -0
- spring/event/publisher.py +69 -0
- spring/excel/__init__.py +51 -0
- spring/excel/annotations.py +405 -0
- spring/excel/converters.py +231 -0
- spring/excel/easy_excel.py +94 -0
- spring/excel/exceptions.py +31 -0
- spring/excel/reader.py +254 -0
- spring/excel/style.py +95 -0
- spring/excel/writer.py +197 -0
- spring/i18n/__init__.py +97 -0
- spring/i18n/accessor.py +94 -0
- spring/i18n/auto_config.py +177 -0
- spring/i18n/holder.py +106 -0
- spring/i18n/locale.py +152 -0
- spring/i18n/locale_resolver.py +367 -0
- spring/i18n/message_source.py +250 -0
- spring/i18n/middleware.py +79 -0
- spring/i18n/properties.py +168 -0
- spring/i18n/sources.py +255 -0
- spring/logging/__init__.py +1 -0
- spring/logging/loguru_logger.py +228 -0
- spring/main.py +378 -0
- spring/messaging/__init__.py +1 -0
- spring/messaging/rabbitmq.py +302 -0
- spring/monitoring/__init__.py +1 -0
- spring/monitoring/prometheus.py +199 -0
- spring/orm/__init__.py +258 -0
- spring/orm/database.py +222 -0
- spring/orm/ddl_auto.py +1217 -0
- spring/orm/migration.py +419 -0
- spring/orm/mybatis_integration.py +400 -0
- spring/orm/pymybatis/__init__.py +86 -0
- spring/orm/pymybatis/annotations/__init__.py +30 -0
- spring/orm/pymybatis/annotations/annotations.py +332 -0
- spring/orm/pymybatis/cache/__init__.py +47 -0
- spring/orm/pymybatis/cache/cache.py +371 -0
- spring/orm/pymybatis/cache/redis_cache.py +434 -0
- spring/orm/pymybatis/circuit_breaker/__init__.py +21 -0
- spring/orm/pymybatis/circuit_breaker/circuit_breaker.py +424 -0
- spring/orm/pymybatis/configuration.py +525 -0
- spring/orm/pymybatis/core/__init__.py +10 -0
- spring/orm/pymybatis/core/sql_session.py +1382 -0
- spring/orm/pymybatis/core/sql_session_factory.py +76 -0
- spring/orm/pymybatis/dialect/__init__.py +9 -0
- spring/orm/pymybatis/dialect/dialect.py +445 -0
- spring/orm/pymybatis/dynamic_sql/__init__.py +9 -0
- spring/orm/pymybatis/dynamic_sql/dynamic_sql.py +900 -0
- spring/orm/pymybatis/interceptor/__init__.py +31 -0
- spring/orm/pymybatis/interceptor/interceptor.py +427 -0
- spring/orm/pymybatis/mapper/__init__.py +9 -0
- spring/orm/pymybatis/mapper/mapper.py +540 -0
- spring/orm/pymybatis/metrics/__init__.py +41 -0
- spring/orm/pymybatis/metrics/metrics.py +595 -0
- spring/orm/pymybatis/pool/__init__.py +9 -0
- spring/orm/pymybatis/pool/connection_pool.py +711 -0
- spring/orm/pymybatis/security/__init__.py +19 -0
- spring/orm/pymybatis/security/access_control.py +415 -0
- spring/orm/pymybatis/security/password_encoder.py +293 -0
- spring/orm/pymybatis/security/sensitive_data_masker.py +326 -0
- spring/orm/pymybatis/security/sql_injection_detector.py +675 -0
- spring/orm/pymybatis/transaction/__init__.py +9 -0
- spring/orm/pymybatis/transaction/transaction.py +288 -0
- spring/orm/pymybatis/type_handler/__init__.py +37 -0
- spring/orm/pymybatis/type_handler/type_handler.py +473 -0
- spring/orm/pymybatis/version.py +9 -0
- spring/orm/pymybatis/xml_parser/__init__.py +9 -0
- spring/orm/pymybatis/xml_parser/xml_parser.py +761 -0
- spring/retry/__init__.py +12 -0
- spring/retry/retry_annotations.py +71 -0
- spring/retry/retry_decorator.py +155 -0
- spring/scheduling/__init__.py +3 -0
- spring/scheduling/scheduler.py +389 -0
- spring/security/__init__.py +39 -0
- spring/security/jwt_utils.py +281 -0
- spring/security/replay_protection.py +206 -0
- spring/security/secret_manager.py +226 -0
- spring/security/security_aop.py +248 -0
- spring/security/security_context.py +172 -0
- spring/test/__init__.py +45 -0
- spring/test/slicing.py +341 -0
- spring/tracing/__init__.py +11 -0
- spring/tracing/skywalking.py +229 -0
- spring/tx/__init__.py +52 -0
- spring/tx/events.py +172 -0
- spring/tx/synchronization.py +143 -0
- spring/utils/__init__.py +5 -0
- spring/utils/banner.py +32 -0
- spring/utils/logger.py +73 -0
- spring/utils/redis_client.py +526 -0
- spring/validation/__init__.py +55 -0
- spring/validation/aop.py +141 -0
- spring/validation/constraints.py +357 -0
- spring/validation/exceptions.py +55 -0
- spring/validation/validator.py +139 -0
- spring/web/__init__.py +12 -0
- spring/web/actuator.py +319 -0
- spring/web/exception_handler.py +61 -0
- spring/web/health.py +399 -0
- spring/web/interceptor.py +91 -0
- spring/web/result.py +44 -0
- spring/web/swagger.py +601 -0
- spring/web/web_context.py +755 -0
- spring/websocket/__init__.py +86 -0
- spring/websocket/annotations.py +169 -0
- spring/websocket/broker.py +238 -0
- spring/websocket/exceptions.py +26 -0
- spring/websocket/handler.py +243 -0
- spring/websocket/router.py +526 -0
- spring/websocket/session.py +216 -0
- springbootai-1.8.0.dist-info/METADATA +2796 -0
- springbootai-1.8.0.dist-info/RECORD +175 -0
- springbootai-1.8.0.dist-info/WHEEL +5 -0
- springbootai-1.8.0.dist-info/entry_points.txt +2 -0
- springbootai-1.8.0.dist-info/licenses/LICENSE +7 -0
- springbootai-1.8.0.dist-info/top_level.txt +1 -0
spring/i18n/__init__.py
ADDED
|
@@ -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
|
+
]
|
spring/i18n/accessor.py
ADDED
|
@@ -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
|
+
]
|