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,168 @@
1
+ """Java 风格 ``.properties`` 文件解析器。
2
+
3
+ 对齐 ``java.util.Properties.load`` 的常见行为:
4
+ - ``key=value`` / ``key:value`` / ``key value`` 三种分隔符。
5
+ - ``#`` / ``!`` 开头为注释行。
6
+ - 反斜杠续行:行尾 ``\\`` 与下一行拼接(去除前导空白)。
7
+ - 转义序列:``\\n`` ``\\t`` ``\\r`` ``\\f`` ``\\\\`` ``\\:`` ``\\=`` ``\\uXXXX``。
8
+ - 默认 UTF-8 编码(Java 9+ ``Properties`` 默认 ISO-8859-1,但 Spring
9
+ ``ResourceBundleMessageSource.setDefaultEncoding("UTF-8")`` 是事实标准)。
10
+
11
+ 不实现:
12
+ - ``Properties.store`` 写出(本框架仅读取国际化资源)。
13
+ - XML 属性文件(``Properties.loadFromXML``)。
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import io
18
+ import re
19
+ from typing import Dict, Mapping, TextIO, Union
20
+
21
+
22
+ # ==================== 转义序列处理 ====================
23
+
24
+ _ESCAPES = {
25
+ "n": "\n", "t": "\t", "r": "\r", "f": "\f",
26
+ "\\": "\\", ":": ":", "=": "=", "#": "#", "!": "!", "\"": "\"", "'": "'",
27
+ "0": "\0",
28
+ # 单引号在 Java Properties 中并非转义,但 MessageFormat 中是;此处保守保留原样
29
+ }
30
+
31
+ _UNICODE_ESCAPE = re.compile(r"\\u([0-9a-fA-F]{4})")
32
+
33
+
34
+ def _unescape(text: str) -> str:
35
+ """反转义 ``\\n``/``\\t``/``\\uXXXX`` 等序列。"""
36
+ # 先处理 \uXXXX(4 位十六进制 Unicode)
37
+ text = _UNICODE_ESCAPE.sub(lambda m: chr(int(m.group(1), 16)), text)
38
+ out = []
39
+ i = 0
40
+ n = len(text)
41
+ while i < n:
42
+ ch = text[i]
43
+ if ch == "\\" and i + 1 < n:
44
+ nxt = text[i + 1]
45
+ out.append(_ESCAPES.get(nxt, "\\" + nxt))
46
+ i += 2
47
+ else:
48
+ out.append(ch)
49
+ i += 1
50
+ return "".join(out)
51
+
52
+
53
+ def _split_kv(line: str) -> "tuple[str, str]":
54
+ """按首个未转义的 ``=``/``:`` 或连续空白拆分 key/value。
55
+
56
+ 对齐 Java ``Properties`` 的宽松分隔规则。
57
+ """
58
+ i = 0
59
+ n = len(line)
60
+ # 跳过前导空白
61
+ while i < n and line[i] in " \t\f":
62
+ i += 1
63
+ key_chars = []
64
+ # key 中允许转义分隔符(\= \:)
65
+ while i < n:
66
+ ch = line[i]
67
+ if ch == "\\" and i + 1 < n:
68
+ key_chars.append(ch)
69
+ key_chars.append(line[i + 1])
70
+ i += 2
71
+ continue
72
+ if ch in "=: \t\f":
73
+ break
74
+ key_chars.append(ch)
75
+ i += 1
76
+ # 跳过 key 后的分隔符(一个 = 或 : 或空白序列)
77
+ sep_seen = False
78
+ while i < n and line[i] in "=: \t\f":
79
+ if line[i] in "=:":
80
+ if sep_seen:
81
+ break
82
+ sep_seen = True
83
+ i += 1
84
+ # 分隔符后的空白也吞掉
85
+ while i < n and line[i] in " \t\f":
86
+ i += 1
87
+ break
88
+ else:
89
+ i += 1
90
+ value = line[i:]
91
+ return _unescape("".join(key_chars)).strip(), _unescape(value)
92
+
93
+
94
+ def _read_logical_lines(stream: TextIO) -> "list[str]":
95
+ """读取逻辑行:合并续行(行尾 ``\\``),跳过注释与空行。
96
+
97
+ 返回每条逻辑行的原始字符串(已合并续行、未拆 key/value)。
98
+ """
99
+ logical: list = []
100
+ buf = ""
101
+ in_continuation = False
102
+ for raw in stream:
103
+ # 统一换行
104
+ line = raw.rstrip("\r\n")
105
+ # 注释/空行仅在非续行状态下生效
106
+ if not in_continuation:
107
+ stripped = line.lstrip(" \t\f")
108
+ if not stripped or stripped[0] in "#!":
109
+ continue
110
+ buf = line
111
+ else:
112
+ # 续行:拼接,去除前导空白
113
+ buf += line.lstrip(" \t\f")
114
+ # 续行判定:行尾奇数个反斜杠表示续行
115
+ backslashes = 0
116
+ idx = len(buf) - 1
117
+ while idx >= 0 and buf[idx] == "\\":
118
+ backslashes += 1
119
+ idx -= 1
120
+ if backslashes % 2 == 1:
121
+ # 去掉最后一个反斜杠,进入续行
122
+ buf = buf[:-1]
123
+ in_continuation = True
124
+ continue
125
+ logical.append(buf)
126
+ buf = ""
127
+ in_continuation = False
128
+ # 文件末尾仍在续行(异常文件)—— 兜底加入
129
+ if buf:
130
+ logical.append(buf)
131
+ return logical
132
+
133
+
134
+ def parse_properties(content: str) -> Dict[str, str]:
135
+ """解析 properties 文本内容,返回 ``{key: value}`` 字典。"""
136
+ result: Dict[str, str] = {}
137
+ with io.StringIO(content) as s:
138
+ for line in _read_logical_lines(s):
139
+ key, value = _split_kv(line)
140
+ if key:
141
+ result[key] = value
142
+ return result
143
+
144
+
145
+ def load_properties(path: str, encoding: str = "utf-8") -> Dict[str, str]:
146
+ """从文件路径加载 properties,返回 ``{key: value}`` 字典。
147
+
148
+ Args:
149
+ path: 文件路径。
150
+ encoding: 文件编码,默认 UTF-8(对齐 Spring ``defaultEncoding``)。
151
+ """
152
+ with open(path, "r", encoding=encoding, newline="") as f:
153
+ return parse_properties(f.read())
154
+
155
+
156
+ def merge_properties(*mappings: Mapping[str, str]) -> Dict[str, str]:
157
+ """合并多个 properties 映射,后者覆盖前者(用于多 basename 合并)。"""
158
+ merged: Dict[str, str] = {}
159
+ for m in mappings:
160
+ merged.update(m)
161
+ return merged
162
+
163
+
164
+ __all__ = [
165
+ "parse_properties",
166
+ "load_properties",
167
+ "merge_properties",
168
+ ]
spring/i18n/sources.py ADDED
@@ -0,0 +1,255 @@
1
+ """具体 ``MessageSource`` 实现:``StaticMessageSource`` / ``ResourceBundleMessageSource`` /
2
+ ``DelegatingMessageSource``(对齐 Spring 同名类)。
3
+
4
+ - ``StaticMessageSource``:编程式注册消息,用于测试或动态消息。
5
+ - ``ResourceBundleMessageSource``:从 ``basenames`` 加载资源文件(``messages`` →
6
+ ``messages.properties`` / ``messages_en.properties`` / ``messages_zh_CN.properties``),
7
+ 支持 ``.properties``(Java 风格)与 ``.yml``/``.yaml``(复用项目 ``pyyaml`` 依赖)。
8
+ - ``DelegatingMessageSource``:未配置父级时的占位实现,所有调用委派父级或返回默认消息。
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ from typing import Dict, Iterable, List, Optional, Tuple
14
+
15
+ from .locale import Locale
16
+ from .message_source import (
17
+ AbstractMessageSource,
18
+ MessageArgs,
19
+ MessageSource,
20
+ NoSuchMessageException,
21
+ _format_message,
22
+ )
23
+ from .properties import load_properties, parse_properties
24
+
25
+
26
+ # ==================== StaticMessageSource ====================
27
+
28
+ class StaticMessageSource(AbstractMessageSource):
29
+ """编程式消息源(对齐 Spring ``StaticMessageSource``)。
30
+
31
+ 内部存储 ``{(code, locale_str): template}``,``locale_str`` 为空表示默认(无 locale)。
32
+ 解析时按 ``locale`` → ``locale_country`` → ``language`` → 默认 顺序回退。
33
+ """
34
+
35
+ def __init__(self, parent: Optional[MessageSource] = None):
36
+ super().__init__(parent=parent)
37
+ # key: (code, locale_string) -> template
38
+ self._messages: Dict[Tuple[str, str], str] = {}
39
+
40
+ def add_message(self, code: str, locale: Locale, template: str) -> None:
41
+ """注册单条消息。"""
42
+ self._messages[(code, locale.to_string())] = template
43
+
44
+ def add_messages(self, messages: Dict[str, str], locale: Locale) -> None:
45
+ """批量注册消息。"""
46
+ loc_str = locale.to_string()
47
+ for code, template in messages.items():
48
+ self._messages[(code, loc_str)] = template
49
+
50
+ def resolve_code(self, code: str, locale: Locale) -> Optional[str]:
51
+ loc_str = locale.to_string()
52
+ # 1. 精确匹配
53
+ if (code, loc_str) in self._messages:
54
+ return self._messages[(code, loc_str)]
55
+ # 2. language_COUNTRY → language 回退
56
+ if locale.country:
57
+ lang_key = (code, locale.language)
58
+ if lang_key in self._messages:
59
+ return self._messages[lang_key]
60
+ # 3. 默认(无 locale)
61
+ default_key = (code, "")
62
+ if default_key in self._messages:
63
+ return self._messages[default_key]
64
+ return None
65
+
66
+
67
+ # ==================== ResourceBundleMessageSource ====================
68
+
69
+
70
+ def _load_yaml(path: str, encoding: str) -> Dict[str, str]:
71
+ """加载 YAML 资源文件;顶层必须是扁平 ``{key: value}`` 映射。"""
72
+ import yaml # 项目核心依赖(pyyaml)
73
+ with open(path, "r", encoding=encoding, newline="") as f:
74
+ data = yaml.safe_load(f.read()) or {}
75
+ if not isinstance(data, dict):
76
+ return {}
77
+ # 值统一转字符串(消息模板必须是字符串)
78
+ return {str(k): "" if v is None else str(v) for k, v in data.items()}
79
+
80
+
81
+ # 支持的文件扩展名及加载器
82
+ _EXT_LOADERS = (
83
+ (".properties", lambda path, enc: load_properties(path, enc)),
84
+ (".yml", _load_yaml),
85
+ (".yaml", _load_yaml),
86
+ )
87
+
88
+
89
+ class ResourceBundleMessageSource(AbstractMessageSource):
90
+ """资源包消息源(对齐 Spring ``ResourceBundleMessageSource``)。
91
+
92
+ Args:
93
+ basenames: 资源基名列表,如 ``["messages", "errors"]``。
94
+ 解析时按 basename + locale 后缀搜索文件。
95
+ base_dir: 资源根目录,默认 ``"."``。搜索文件时拼接 ``base_dir/basename_locale.ext``。
96
+ default_encoding: 文件编码,默认 UTF-8(对齐 Spring ``defaultEncoding``)。
97
+ fallback_to_system_locale: 当请求 locale 找不到时,是否回退到系统 locale。
98
+ 默认 True(对齐 Spring 同名开关)。
99
+ default_locale: 默认 locale(``None`` 时用 ``Locale("")``)。
100
+
101
+ 文件命名约定(与 Java ``ResourceBundle`` 一致):
102
+ ``messages.properties`` — 默认
103
+ ``messages_en.properties`` — 英语
104
+ ``messages_en_US.properties`` — 英语(美国)
105
+ ``messages_zh_CN.properties`` — 中文(中国)
106
+
107
+ YML 等价:
108
+ ``messages.yml`` / ``messages_en.yml`` / ``messages_zh_CN.yml``
109
+
110
+ 解析顺序:``locale`` → ``locale_country`` → ``language`` → 默认。
111
+ 多 ``basename`` 时,前者优先(后者作为补充,不覆盖前者已命中的 code)。
112
+ """
113
+
114
+ def __init__(
115
+ self,
116
+ basenames: Optional[Iterable[str]] = None,
117
+ base_dir: str = ".",
118
+ default_encoding: str = "utf-8",
119
+ fallback_to_system_locale: bool = True,
120
+ default_locale: Optional[Locale] = None,
121
+ parent: Optional[MessageSource] = None,
122
+ ):
123
+ super().__init__(parent=parent)
124
+ self._basenames: List[str] = list(basenames) if basenames else ["messages"]
125
+ self._base_dir = base_dir
126
+ self._default_encoding = default_encoding
127
+ self._fallback_to_system_locale = fallback_to_system_locale
128
+ self._default_locale = default_locale or Locale("")
129
+ # 缓存:{(basename, locale_str): {code: template}}
130
+ self._cached_bundles: Dict[Tuple[str, str], Dict[str, str]] = {}
131
+
132
+ # ---- 配置 ----
133
+
134
+ def add_basename(self, basename: str) -> None:
135
+ self._basenames.append(basename)
136
+ self._cached_bundles.clear()
137
+
138
+ def set_default_encoding(self, encoding: str) -> None:
139
+ self._default_encoding = encoding
140
+ self._cached_bundles.clear()
141
+
142
+ def set_base_dir(self, base_dir: str) -> None:
143
+ self._base_dir = base_dir
144
+ self._cached_bundles.clear()
145
+
146
+ def set_default_locale(self, locale: Locale) -> None:
147
+ self._default_locale = locale
148
+ self._cached_bundles.clear()
149
+
150
+ # ---- 解析 ----
151
+
152
+ def resolve_code(self, code: str, locale: Locale) -> Optional[str]:
153
+ # 优先精确 locale,再 country 回退到 language,再默认
154
+ for loc in self._locale_fallback_chain(locale):
155
+ for basename in self._basenames:
156
+ bundle = self._get_bundle(basename, loc)
157
+ if bundle and code in bundle:
158
+ return bundle[code]
159
+ return None
160
+
161
+ def resolve_code_without_args(self, code: str, locale: Locale) -> Optional[str]:
162
+ # 复用同一解析路径(无参数优化空间不大,保持一致性)
163
+ return self.resolve_code(code, locale)
164
+
165
+ # ---- 内部 ----
166
+
167
+ def _locale_fallback_chain(self, locale: Locale) -> List[Locale]:
168
+ """构造 locale 回退链:locale → locale(country 去掉) → language → 默认。"""
169
+ chain: List[Locale] = []
170
+ if not locale.is_empty:
171
+ chain.append(locale)
172
+ if locale.country:
173
+ chain.append(Locale(locale.language))
174
+ elif locale.variant:
175
+ chain.append(Locale(locale.language))
176
+ if self._fallback_to_system_locale and not self._default_locale.is_empty:
177
+ if self._default_locale not in chain:
178
+ chain.append(self._default_locale)
179
+ # 默认(空 locale)
180
+ if Locale("") not in chain:
181
+ chain.append(Locale(""))
182
+ return chain
183
+
184
+ def _get_bundle(self, basename: str, locale: Locale) -> Dict[str, str]:
185
+ """获取指定 basename + locale 的消息字典(带缓存)。"""
186
+ cache_key = (basename, locale.to_string())
187
+ if cache_key in self._cached_bundles:
188
+ return self._cached_bundles[cache_key]
189
+ bundle = self._load_bundle(basename, locale)
190
+ self._cached_bundles[cache_key] = bundle
191
+ return bundle
192
+
193
+ def _load_bundle(self, basename: str, locale: Locale) -> Dict[str, str]:
194
+ """在 ``base_dir`` 下查找 basename + locale 后缀的资源文件。"""
195
+ # locale 后缀:locale.to_string() 为空则无后缀
196
+ loc_str = locale.to_string()
197
+ suffix = f"_{loc_str}" if loc_str else ""
198
+ # 依次尝试 .properties / .yml / .yaml
199
+ for ext, loader in _EXT_LOADERS:
200
+ path = os.path.join(self._base_dir, f"{basename}{suffix}{ext}")
201
+ if os.path.isfile(path):
202
+ try:
203
+ return loader(path, self._default_encoding)
204
+ except Exception:
205
+ # 加载失败:跳过,回退到下一个扩展名
206
+ continue
207
+ return {}
208
+
209
+
210
+ # ==================== DelegatingMessageSource ====================
211
+
212
+ class DelegatingMessageSource(MessageSource):
213
+ """委派消息源(对齐 Spring ``DelegatingMessageSource``)。
214
+
215
+ ApplicationContext 初始化前用作占位 ``MessageSource``:所有调用委派给父级;
216
+ 父级为 None 时返回 ``default_message`` 或抛 ``NoSuchMessageException``。
217
+ """
218
+
219
+ def __init__(self, parent: Optional[MessageSource] = None):
220
+ self._parent: Optional[MessageSource] = parent
221
+
222
+ @property
223
+ def parent_message_source(self) -> Optional[MessageSource]:
224
+ return self._parent
225
+
226
+ @parent_message_source.setter
227
+ def parent_message_source(self, value: Optional[MessageSource]) -> None:
228
+ self._parent = value
229
+
230
+ def getMessage(self, code, args=None, locale=None): # noqa: N802
231
+ if self._parent is not None:
232
+ return self._parent.getMessage(code, args, locale)
233
+ raise NoSuchMessageException(code, locale)
234
+
235
+ def getMessageOrDefault(self, code, args=None, default_message=None, locale=None):
236
+ if self._parent is not None:
237
+ return self._parent.getMessageOrDefault(code, args, default_message, locale)
238
+ return default_message
239
+
240
+ def getMessageFromResolvable(self, resolvable, locale=None):
241
+ if self._parent is not None:
242
+ return self._parent.getMessageFromResolvable(resolvable, locale)
243
+ default = resolvable.get_default_message()
244
+ if default is not None:
245
+ return _format_message(default, resolvable.get_arguments(), locale)
246
+ raise NoSuchMessageException(
247
+ resolvable.get_codes()[0] if resolvable.get_codes() else "", locale
248
+ )
249
+
250
+
251
+ __all__ = [
252
+ "StaticMessageSource",
253
+ "ResourceBundleMessageSource",
254
+ "DelegatingMessageSource",
255
+ ]
@@ -0,0 +1 @@
1
+ """Application logging integrations."""
@@ -0,0 +1,228 @@
1
+ """
2
+ Loguru结构化日志模块
3
+ 提供企业级日志功能
4
+ """
5
+ import logging
6
+ import sys
7
+ import os
8
+ from datetime import datetime
9
+ from typing import Optional
10
+
11
+ # 尝试导入loguru,失败则使用标准logging
12
+ try:
13
+ from loguru import logger as loguru_logger
14
+ _loguru_available = True
15
+ except ImportError:
16
+ _loguru_available = False
17
+ loguru_logger = None
18
+
19
+
20
+ class SpringLogger:
21
+ """Spring日志管理器"""
22
+
23
+ _instance = None
24
+ _lock = __import__('threading').Lock()
25
+
26
+ def __new__(cls, *args, **kwargs):
27
+ if cls._instance is None:
28
+ with cls._lock:
29
+ if cls._instance is None:
30
+ cls._instance = super().__new__(cls)
31
+ return cls._instance
32
+
33
+ def __init__(self, level: str = "INFO", log_format: str = None,
34
+ log_dir: str = "logs", retention: str = "30 days",
35
+ rotation: str = "100 MB"):
36
+ if hasattr(self, '_initialized'):
37
+ return
38
+ self.level = level.upper()
39
+ self.log_dir = log_dir
40
+ self.retention = retention
41
+ self.rotation = rotation
42
+ self._initialized = True
43
+ self._use_loguru = _loguru_available
44
+ self.log_format = log_format or self._default_format()
45
+
46
+ # 初始化日志配置
47
+ if self._use_loguru:
48
+ self._setup_loguru()
49
+ else:
50
+ self._setup_std_logging()
51
+
52
+ def _default_format(self) -> str:
53
+ """默认日志格式"""
54
+ if self._use_loguru:
55
+ return (
56
+ "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
57
+ "<level>{level: <8}</level> | "
58
+ "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | "
59
+ "<level>{message}</level>"
60
+ )
61
+ return "%(asctime)s | %(levelname)-8s | %(name)s:%(funcName)s:%(lineno)d | %(message)s"
62
+
63
+ def _setup_loguru(self):
64
+ """配置Loguru"""
65
+ # 清除默认处理器
66
+ loguru_logger.remove()
67
+
68
+ # 添加控制台输出
69
+ loguru_logger.add(
70
+ sys.stdout,
71
+ format=self.log_format,
72
+ level=self.level,
73
+ colorize=True,
74
+ )
75
+
76
+ # 创建日志目录
77
+ if not os.path.exists(self.log_dir):
78
+ os.makedirs(self.log_dir)
79
+
80
+ # 添加文件输出(按日期轮转)
81
+ loguru_logger.add(
82
+ os.path.join(self.log_dir, "application_{time:YYYY-MM-DD}.log"),
83
+ format=self.log_format,
84
+ level=self.level,
85
+ rotation=self.rotation,
86
+ retention=self.retention,
87
+ compression="zip",
88
+ encoding="utf-8",
89
+ )
90
+
91
+ # 添加错误日志单独输出
92
+ loguru_logger.add(
93
+ os.path.join(self.log_dir, "error_{time:YYYY-MM-DD}.log"),
94
+ format=self.log_format,
95
+ level="ERROR",
96
+ rotation=self.rotation,
97
+ retention=self.retention,
98
+ compression="zip",
99
+ encoding="utf-8",
100
+ )
101
+
102
+ def _setup_std_logging(self):
103
+ """配置标准logging(fallback)"""
104
+ self._logger = logging.getLogger("Spring")
105
+ self._logger.setLevel(getattr(logging, self.level))
106
+
107
+ # 创建日志目录
108
+ if not os.path.exists(self.log_dir):
109
+ os.makedirs(self.log_dir)
110
+
111
+ # 添加控制台输出
112
+ console_handler = logging.StreamHandler(sys.stdout)
113
+ console_handler.setFormatter(logging.Formatter(self.log_format))
114
+ self._logger.addHandler(console_handler)
115
+
116
+ # 添加文件输出
117
+ file_handler = logging.FileHandler(
118
+ os.path.join(self.log_dir, "application.log"),
119
+ encoding="utf-8"
120
+ )
121
+ file_handler.setFormatter(logging.Formatter(self.log_format))
122
+ self._logger.addHandler(file_handler)
123
+
124
+ def get_logger(self):
125
+ """获取日志实例"""
126
+ if self._use_loguru:
127
+ return loguru_logger
128
+ return self._logger
129
+
130
+ def info(self, message: str, **kwargs):
131
+ """记录INFO级别日志"""
132
+ if self._use_loguru:
133
+ loguru_logger.info(message, **kwargs)
134
+ else:
135
+ self._logger.info(message)
136
+
137
+ def debug(self, message: str, **kwargs):
138
+ """记录DEBUG级别日志"""
139
+ if self._use_loguru:
140
+ loguru_logger.debug(message, **kwargs)
141
+ else:
142
+ self._logger.debug(message)
143
+
144
+ def warning(self, message: str, **kwargs):
145
+ """记录WARNING级别日志"""
146
+ if self._use_loguru:
147
+ loguru_logger.warning(message, **kwargs)
148
+ else:
149
+ self._logger.warning(message)
150
+
151
+ def error(self, message: str, **kwargs):
152
+ """记录ERROR级别日志"""
153
+ if self._use_loguru:
154
+ loguru_logger.error(message, **kwargs)
155
+ else:
156
+ self._logger.error(message)
157
+
158
+ def critical(self, message: str, **kwargs):
159
+ """记录CRITICAL级别日志"""
160
+ if self._use_loguru:
161
+ loguru_logger.critical(message, **kwargs)
162
+ else:
163
+ self._logger.critical(message)
164
+
165
+ def exception(self, message: str, **kwargs):
166
+ """记录异常日志"""
167
+ if self._use_loguru:
168
+ loguru_logger.exception(message, **kwargs)
169
+ else:
170
+ self._logger.exception(message)
171
+
172
+ def log(self, level: str, message: str, **kwargs):
173
+ """记录指定级别日志"""
174
+ if self._use_loguru:
175
+ loguru_logger.log(level.upper(), message, **kwargs)
176
+ else:
177
+ self._logger.log(getattr(logging, level.upper()), message)
178
+
179
+ def bind(self, **extra):
180
+ """绑定额外字段到日志上下文"""
181
+ if self._use_loguru:
182
+ return loguru_logger.bind(**extra)
183
+ return self._logger
184
+
185
+ def patch(self, function):
186
+ """添加额外字段到日志消息"""
187
+ if self._use_loguru:
188
+ return loguru_logger.patch(function)
189
+ return self._logger
190
+
191
+
192
+ # 创建全局日志管理器实例
193
+ spring_logger = SpringLogger()
194
+
195
+
196
+ def init_logging(config: dict) -> None:
197
+ """
198
+ 初始化日志配置
199
+
200
+ Args:
201
+ config: 配置字典,包含level, log_dir, retention, rotation等
202
+ """
203
+ global spring_logger
204
+ spring_logger = SpringLogger(
205
+ level=config.get('level', 'INFO'),
206
+ log_format=config.get('log_format'),
207
+ log_dir=config.get('log_dir', 'logs'),
208
+ retention=config.get('retention', '30 days'),
209
+ rotation=config.get('rotation', '100 MB'),
210
+ )
211
+
212
+
213
+ # 兼容标准logging模块
214
+ if _loguru_available:
215
+ class LoguruHandler(logging.Handler):
216
+ """Loguru处理器,用于将标准logging日志转发到Loguru"""
217
+
218
+ def emit(self, record: logging.LogRecord):
219
+ """处理日志记录"""
220
+ try:
221
+ level = loguru_logger.level(record.levelname).name
222
+ message = self.format(record)
223
+ loguru_logger.log(level, message)
224
+ except Exception:
225
+ self.handleError(record)
226
+
227
+ # 将标准logging日志转发到Loguru
228
+ logging.basicConfig(handlers=[LoguruHandler()], level=logging.INFO)