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,367 @@
1
+ """``LocaleResolver`` 区域解析器(对齐 Spring ``org.springframework.web.servlet.i18n``)。
2
+
3
+ 核心抽象:
4
+ - ``LocaleResolver``:策略接口,从 HTTP 请求解析 ``LocaleContext`` / 写回响应。
5
+ - ``LocaleContext``:携带当前 locale(可选时区)的上下文对象。
6
+ - ``AcceptHeaderLocaleResolver``:从请求 ``Accept-Language`` 头解析(默认推荐)。
7
+ - ``FixedLocaleResolver``:永远返回固定 locale(对齐 Spring 同名类)。
8
+ - ``SessionLocaleResolver``:从会话读写 locale(Starlette SessionMiddleware 风格)。
9
+ - ``CookieLocaleResolver``:从 Cookie 读写 locale。
10
+
11
+ 设计要点:
12
+ - 复用 Starlette ``Request``/``Response`` 抽象,不直接依赖 FastAPI。
13
+ - ``AcceptHeaderLocaleResolver`` 实现 RFC 7231 简化版:解析 q 值排序 + locale 前缀回退。
14
+ - ``SessionLocaleResolver`` 需要应用先注册 ``SessionMiddleware``(Starlette 标准做法)。
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import re
19
+ from typing import Iterable, List, Optional, Sequence, Tuple
20
+
21
+ from .locale import Locale
22
+
23
+
24
+ # ==================== LocaleContext ====================
25
+
26
+ class LocaleContext:
27
+ """区域上下文(对齐 Spring ``LocaleContext``)。"""
28
+
29
+ def get_locale(self) -> Locale:
30
+ raise NotImplementedError
31
+
32
+ def is_fallback(self) -> bool:
33
+ """是否为回退 locale(部分实现标记)。默认 False。"""
34
+ return False
35
+
36
+
37
+ class SimpleLocaleContext(LocaleContext):
38
+ """简单 ``LocaleContext`` 实现(对齐 Spring ``SimpleLocaleContext``)。"""
39
+
40
+ def __init__(self, locale: Optional[Locale]):
41
+ self._locale = locale if locale is not None else Locale()
42
+
43
+ def get_locale(self) -> Locale:
44
+ return self._locale
45
+
46
+ def __repr__(self) -> str:
47
+ return f"SimpleLocaleContext({self._locale!r})"
48
+
49
+
50
+ class SimpleTimeZoneAwareLocaleContext(LocaleContext):
51
+ """带时区的 ``LocaleContext``(对齐 Spring ``SimpleTimeZoneAwareLocaleContext``)。
52
+
53
+ 本实现保留 ``time_zone`` 字段但不参与解析逻辑(仅元数据语义)。
54
+ """
55
+
56
+ def __init__(self, locale: Optional[Locale], time_zone: Optional[str] = None):
57
+ self._locale = locale if locale is not None else Locale()
58
+ self._time_zone = time_zone
59
+
60
+ def get_locale(self) -> Locale:
61
+ return self._locale
62
+
63
+ def get_time_zone(self) -> Optional[str]:
64
+ return self._time_zone
65
+
66
+ def __repr__(self) -> str:
67
+ return f"SimpleTimeZoneAwareLocaleContext({self._locale!r}, tz={self._time_zone!r})"
68
+
69
+
70
+ # ==================== LocaleResolver 接口 ====================
71
+
72
+ class LocaleResolver:
73
+ """区域解析器策略接口(对齐 Spring ``LocaleResolver``)。
74
+
75
+ 子类需实现 ``resolve_locale(request)`` 返回 ``LocaleContext``。
76
+ ``set_locale_context(request, response, context)`` 用于写回(部分实现支持)。
77
+ """
78
+
79
+ def resolve_locale(self, request) -> LocaleContext: # noqa: D401 - 接口方法
80
+ raise NotImplementedError
81
+
82
+ def set_locale_context(self, request, response, context: LocaleContext) -> None:
83
+ """写回 locale 到响应(``FixedLocaleResolver`` 不支持,抛 ``UnsupportedOperation``)。"""
84
+ raise NotImplementedError(f"{type(self).__name__} does not support set_locale_context")
85
+
86
+
87
+ # ==================== AcceptHeaderLocaleResolver ====================
88
+
89
+ # Accept-Language 头解析:zh-CN,zh;q=0.9,en;q=0.8
90
+ _ACCEPT_LANG_RE = re.compile(
91
+ r"\s*([a-zA-Z]{1,8}(?:-[a-zA-Z0-9]{1,8})*)\s*(?:;\s*q\s*=\s*([0-9.]+))?\s*,?",
92
+ re.IGNORECASE,
93
+ )
94
+
95
+
96
+ def parse_accept_language(header: str) -> List[Tuple[Locale, float]]:
97
+ """解析 ``Accept-Language`` 头,返回 ``[(Locale, q), ...]`` 按 q 降序。
98
+
99
+ q 默认 1.0;q=0 表示不可接受(仍保留在列表中,调用方可过滤)。
100
+ """
101
+ if not header:
102
+ return []
103
+ result: List[Tuple[Locale, float]] = []
104
+ for match in _ACCEPT_LANG_RE.finditer(header):
105
+ tag = match.group(1)
106
+ q_str = match.group(2)
107
+ try:
108
+ q = float(q_str) if q_str else 1.0
109
+ except ValueError:
110
+ q = 1.0
111
+ result.append((Locale.parse(tag), q))
112
+ # q 降序,稳定排序(保留声明顺序)
113
+ result.sort(key=lambda x: -x[1])
114
+ return result
115
+
116
+
117
+ class AcceptHeaderLocaleResolver(LocaleResolver):
118
+ """从 ``Accept-Language`` 头解析 locale(对齐 Spring ``AcceptHeaderLocaleResolver``)。
119
+
120
+ Args:
121
+ supported_locales: 支持的 locale 列表;为空时返回请求 locale 原样。
122
+ default_locale: 无匹配时返回的默认 locale;``None`` 时返回 ``Locale("")``。
123
+
124
+ 匹配算法(简化 RFC 4647 过滤):
125
+ 1. 按 q 降序遍历 Accept-Language;
126
+ 2. 精确匹配 supported(language + country);
127
+ 3. language 前缀匹配 supported;
128
+ 4. 全部不匹配返回 ``default_locale``。
129
+ """
130
+
131
+ def __init__(
132
+ self,
133
+ supported_locales: Optional[Sequence[Locale]] = None,
134
+ default_locale: Optional[Locale] = None,
135
+ ):
136
+ self._supported: List[Locale] = list(supported_locales) if supported_locales else []
137
+ self._default: Locale = default_locale if default_locale is not None else Locale("")
138
+
139
+ @property
140
+ def supported_locales(self) -> List[Locale]:
141
+ return list(self._supported)
142
+
143
+ def set_supported_locales(self, locales: Sequence[Locale]) -> None:
144
+ self._supported = list(locales)
145
+
146
+ def set_default_locale(self, locale: Locale) -> None:
147
+ self._default = locale
148
+
149
+ def resolve_locale(self, request) -> LocaleContext:
150
+ header = _get_header(request, "accept-language", "")
151
+ candidates = parse_accept_language(header)
152
+ if not candidates:
153
+ return SimpleLocaleContext(self._default)
154
+ if not self._supported:
155
+ # 无 supported 列表:直接返回最高 q 的 locale
156
+ best = candidates[0][0]
157
+ return SimpleLocaleContext(best if best.is_empty is False else self._default)
158
+
159
+ for cand, q in candidates:
160
+ if q <= 0:
161
+ continue
162
+ # 精确匹配
163
+ for sup in self._supported:
164
+ if cand == sup:
165
+ return SimpleLocaleContext(sup)
166
+ # language 前缀匹配
167
+ for sup in self._supported:
168
+ if cand.language and cand.language == sup.language:
169
+ return SimpleLocaleContext(sup)
170
+ return SimpleLocaleContext(self._default)
171
+
172
+ def set_locale_context(self, request, response, context: LocaleContext) -> None:
173
+ # Accept-Header 解析器不支持写回(对齐 Spring 同名行为)
174
+ raise NotImplementedError(
175
+ "AcceptHeaderLocaleResolver 不支持 set_locale_context;"
176
+ "如需写回请使用 SessionLocaleResolver 或 CookieLocaleResolver"
177
+ )
178
+
179
+
180
+ # ==================== FixedLocaleResolver ====================
181
+
182
+ class FixedLocaleResolver(LocaleResolver):
183
+ """固定 locale 解析器(对齐 Spring ``FixedLocaleResolver``)。
184
+
185
+ 所有请求返回同一 ``LocaleContext``,``set_locale_context`` 抛 ``UnsupportedOperation``。
186
+ """
187
+
188
+ def __init__(
189
+ self,
190
+ locale: Optional[Locale] = None,
191
+ time_zone: Optional[str] = None,
192
+ ):
193
+ self._locale = locale if locale is not None else Locale("")
194
+ self._time_zone = time_zone
195
+
196
+ def resolve_locale(self, request) -> LocaleContext:
197
+ return SimpleTimeZoneAwareLocaleContext(self._locale, self._time_zone)
198
+
199
+ def set_locale_context(self, request, response, context: LocaleContext) -> None:
200
+ raise NotImplementedError(
201
+ "FixedLocaleResolver 不支持 set_locale_context(locale 固定不可变)"
202
+ )
203
+
204
+
205
+ # ==================== SessionLocaleResolver ====================
206
+
207
+ class SessionLocaleResolver(LocaleResolver):
208
+ """会话级 locale 解析器(对齐 Spring ``SessionLocaleResolver``)。
209
+
210
+ 依赖 Starlette ``SessionMiddleware``;通过 ``request.session`` 读写 locale 字符串。
211
+ """
212
+
213
+ def __init__(
214
+ self,
215
+ session_attribute_name: str = "spring_locale",
216
+ default_locale: Optional[Locale] = None,
217
+ ):
218
+ self._session_attr = session_attribute_name
219
+ self._default = default_locale if default_locale is not None else Locale("")
220
+
221
+ def resolve_locale(self, request) -> LocaleContext:
222
+ session = _get_session(request)
223
+ loc_str = session.get(self._session_attr) if session else None
224
+ if loc_str:
225
+ return SimpleLocaleContext(Locale.parse(loc_str))
226
+ return SimpleLocaleContext(self._default)
227
+
228
+ def set_locale_context(self, request, response, context: LocaleContext) -> None:
229
+ session = _get_session(request)
230
+ if session is None:
231
+ raise RuntimeError(
232
+ "SessionLocaleResolver 需要 SessionMiddleware;请先注册 starlette SessionMiddleware"
233
+ )
234
+ session[self._session_attr] = context.get_locale().to_string()
235
+
236
+
237
+ # ==================== CookieLocaleResolver ====================
238
+
239
+ class CookieLocaleResolver(LocaleResolver):
240
+ """Cookie 级 locale 解析器(对齐 Spring ``CookieLocaleResolver``)。
241
+
242
+ Args:
243
+ cookie_name: Cookie 名称,默认 ``spring_locale``。
244
+ cookie_max_age: Cookie Max-Age(秒),默认 1 年。
245
+ cookie_path: Cookie Path,默认 ``/``。
246
+ cookie_domain: Cookie Domain,默认 None。
247
+ cookie_secure: 是否 Secure,默认 False。
248
+ cookie_httponly: 是否 HttpOnly,默认 True。
249
+ default_locale: Cookie 不存在时的默认 locale。
250
+ """
251
+
252
+ def __init__(
253
+ self,
254
+ cookie_name: str = "spring_locale",
255
+ cookie_max_age: int = 365 * 24 * 3600,
256
+ cookie_path: str = "/",
257
+ cookie_domain: Optional[str] = None,
258
+ cookie_secure: bool = False,
259
+ cookie_httponly: bool = True,
260
+ default_locale: Optional[Locale] = None,
261
+ ):
262
+ self._cookie_name = cookie_name
263
+ self._max_age = cookie_max_age
264
+ self._path = cookie_path
265
+ self._domain = cookie_domain
266
+ self._secure = cookie_secure
267
+ self._httponly = cookie_httponly
268
+ self._default = default_locale if default_locale is not None else Locale("")
269
+
270
+ def resolve_locale(self, request) -> LocaleContext:
271
+ cookies = _get_cookies(request)
272
+ loc_str = cookies.get(self._cookie_name) if cookies else None
273
+ if loc_str:
274
+ return SimpleLocaleContext(Locale.parse(loc_str))
275
+ return SimpleLocaleContext(self._default)
276
+
277
+ def set_locale_context(self, request, response, context: LocaleContext) -> None:
278
+ _set_cookie(
279
+ response,
280
+ name=self._cookie_name,
281
+ value=context.get_locale().to_language_tag(),
282
+ max_age=self._max_age,
283
+ path=self._path,
284
+ domain=self._domain,
285
+ secure=self._secure,
286
+ httponly=self._httponly,
287
+ )
288
+
289
+
290
+ # ==================== Starlette Request/Response 兼容工具 ====================
291
+
292
+ def _get_header(request, name: str, default: str) -> str:
293
+ """从请求对象取头;兼容 Starlette ``Request`` 与 dict-like。"""
294
+ if request is None:
295
+ return default
296
+ headers = getattr(request, "headers", None)
297
+ if headers is None:
298
+ return default
299
+ try:
300
+ # Starlette Headers 对象支持 .get 大小写不敏感
301
+ return headers.get(name, default)
302
+ except AttributeError:
303
+ try:
304
+ return dict(headers).get(name, default)
305
+ except Exception:
306
+ return default
307
+
308
+
309
+ def _get_session(request):
310
+ """获取 Starlette ``request.session``;不存在返回 None。"""
311
+ if request is None:
312
+ return None
313
+ return getattr(request, "session", None)
314
+
315
+
316
+ def _get_cookies(request):
317
+ """获取 Starlette ``request.cookies``;不存在返回 None。"""
318
+ if request is None:
319
+ return None
320
+ cookies = getattr(request, "cookies", None)
321
+ if cookies is None:
322
+ return None
323
+ try:
324
+ return dict(cookies)
325
+ except Exception:
326
+ return cookies
327
+
328
+
329
+ def _set_cookie(response, name: str, value: str, max_age: int, path: str,
330
+ domain: Optional[str], secure: bool, httponly: bool) -> None:
331
+ """写回 Cookie;兼容 Starlette ``Response`` 与自定义响应对象。"""
332
+ if response is None:
333
+ return
334
+ set_cookie = getattr(response, "set_cookie", None)
335
+ if callable(set_cookie):
336
+ set_cookie(
337
+ key=name, value=value, max_age=max_age, path=path,
338
+ domain=domain, secure=secure, httponly=httponly,
339
+ )
340
+ return
341
+ # 兜底:手动拼 Set-Cookie 头
342
+ parts = [f"{name}={value}", f"Path={path}", f"Max-Age={max_age}"]
343
+ if domain:
344
+ parts.append(f"Domain={domain}")
345
+ if secure:
346
+ parts.append("Secure")
347
+ if httponly:
348
+ parts.append("HttpOnly")
349
+ headers = getattr(response, "headers", None)
350
+ if headers is not None:
351
+ try:
352
+ headers.append("set-cookie", "; ".join(parts))
353
+ except AttributeError:
354
+ pass
355
+
356
+
357
+ __all__ = [
358
+ "LocaleContext",
359
+ "SimpleLocaleContext",
360
+ "SimpleTimeZoneAwareLocaleContext",
361
+ "LocaleResolver",
362
+ "AcceptHeaderLocaleResolver",
363
+ "FixedLocaleResolver",
364
+ "SessionLocaleResolver",
365
+ "CookieLocaleResolver",
366
+ "parse_accept_language",
367
+ ]
@@ -0,0 +1,250 @@
1
+ """``MessageSource`` 接口与抽象基类(对齐 Spring ``org.springframework.context.MessageSource``)。
2
+
3
+ 核心抽象:
4
+ - ``MessageSource``:策略接口,按 ``code`` + ``Locale`` 解析消息。
5
+ - ``AbstractMessageSource``:实现公共逻辑——参数格式化、默认消息回退、父级委派、
6
+ ``MessageSourceResolvable`` 多 code 解析。子类只需实现 ``resolve_code``。
7
+ - ``NoSuchMessageException``:找不到消息时抛出(对齐 Spring 同名异常)。
8
+ - ``MessageSourceResolvable`` / ``DefaultMessageSourceResolvable``:多 code + 参数 + 默认消息
9
+ 的可解析对象,用于 ``ObjectError``/``FieldError`` 等校验场景。
10
+
11
+ 消息格式(对齐 ``java.text.MessageFormat`` 的常见用法):
12
+ - 位置占位符 ``{0}``/``{1}``:用 ``args`` 列表按序替换。
13
+ - 类型子模式 ``{0,number,#.##}``:忽略类型符,等价于 ``{0}``(Python 无等价格式化)。
14
+ - 关键字占位符 ``{name}``:当 ``args`` 为字典时使用。
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import re
19
+ from typing import Any, Iterable, List, Optional, Sequence, Union
20
+
21
+ from .locale import Locale
22
+
23
+
24
+ class NoSuchMessageException(Exception):
25
+ """找不到消息时抛出(对齐 Spring ``NoSuchMessageException``)。"""
26
+
27
+ def __init__(self, code: str, locale: Optional[Locale] = None):
28
+ self.code = code
29
+ self.locale = locale
30
+ loc_str = locale.to_string() if locale else "default"
31
+ super().__init__(f"No message found under code '{code}' for locale '{loc_str}'.")
32
+
33
+
34
+ # ==================== 消息参数类型 ====================
35
+ # args 可以是:列表/元组(位置参数 {0}{1}),或字典(关键字参数 {name})
36
+ MessageArgs = Optional[Union[Sequence[Any], dict]]
37
+
38
+
39
+ # ==================== MessageFormat 兼容格式化 ====================
40
+
41
+ # 匹配 {0}、{1,number}、{0,date,yyyy-MM-dd} 等 Java MessageFormat 占位符
42
+ _JAVA_MSGFMT = re.compile(r"\{(\d+)(?:,[a-zA-Z]+(?:,[^}]+)?)?\}")
43
+
44
+
45
+ def _format_message(template: str, args: MessageArgs, locale: Optional[Locale]) -> str:
46
+ """按 Java ``MessageFormat`` 兼容方式格式化模板。
47
+
48
+ - ``args`` 为列表/元组:用 ``{0}``/``{1}`` 位置替换;同时剥离 Java 类型子模式
49
+ (``{0,number}`` → ``{0}``),使其兼容 ``str.format``。
50
+ - ``args`` 为字典:用 ``{name}`` 关键字替换(不支持 Java 类型子模式)。
51
+ - ``args`` 为 None:原样返回。
52
+ - 格式化失败(参数不足/类型不匹配)原样返回模板,避免抛异常(对齐 Spring 容错)。
53
+ """
54
+ if args is None:
55
+ return template
56
+ try:
57
+ if isinstance(args, dict):
58
+ return template.format(**args)
59
+ # 序列:先剥离 Java 类型子模式,再 str.format
60
+ seq = list(args)
61
+ stripped = _JAVA_MSGFMT.sub(r"{\1}", template)
62
+ return stripped.format(*seq)
63
+ except (IndexError, KeyError, ValueError, TypeError):
64
+ return template
65
+
66
+
67
+ # ==================== MessageSourceResolvable ====================
68
+
69
+ class MessageSourceResolvable:
70
+ """可解析消息对象(对齐 Spring ``MessageSourceResolvable``)。
71
+
72
+ 持有多个候选 ``codes``(按优先级降序)、可选 ``arguments``、可选默认消息。
73
+ ``MessageSource.getMessage(resolvable, locale)`` 会按顺序尝试每个 code,第一个命中即返回。
74
+ """
75
+
76
+ def __init__(
77
+ self,
78
+ codes: Optional[Sequence[str]],
79
+ arguments: MessageArgs = None,
80
+ default_message: Optional[str] = None,
81
+ ):
82
+ self.codes: List[str] = list(codes) if codes else []
83
+ self.arguments = arguments
84
+ self.default_message = default_message
85
+
86
+ def get_codes(self) -> List[str]:
87
+ return self.codes
88
+
89
+ def get_arguments(self) -> MessageArgs:
90
+ return self.arguments
91
+
92
+ def get_default_message(self) -> Optional[str]:
93
+ return self.default_message
94
+
95
+
96
+ class DefaultMessageSourceResolvable(MessageSourceResolvable):
97
+ """``MessageSourceResolvable`` 的默认实现(对齐 Spring 同名类)。"""
98
+
99
+ def __init__(
100
+ self,
101
+ codes: Optional[Sequence[str]],
102
+ arguments: MessageArgs = None,
103
+ default_message: Optional[str] = None,
104
+ ):
105
+ super().__init__(codes, arguments, default_message)
106
+
107
+
108
+ # ==================== MessageSource 接口 ====================
109
+
110
+ class MessageSource:
111
+ """消息源策略接口(对齐 Spring ``MessageSource``)。
112
+
113
+ 子类必须实现 ``resolve_code``;本类提供 ``getMessage`` 的公共入口与默认消息回退逻辑。
114
+ """
115
+
116
+ def getMessage( # noqa: N802 - 保留 Java 驼峰命名以对齐 Spring API
117
+ self,
118
+ code: str,
119
+ args: MessageArgs = None,
120
+ locale: Optional[Locale] = None,
121
+ ) -> str:
122
+ """按 ``code`` + ``locale`` 解析消息,找不到抛 ``NoSuchMessageException``。"""
123
+ msg = self._resolve_with_fallback(code, args, locale, default=None)
124
+ if msg is None:
125
+ raise NoSuchMessageException(code, locale)
126
+ return msg
127
+
128
+ def getMessageOrDefault( # 便利方法,Python 风格命名
129
+ self,
130
+ code: str,
131
+ args: MessageArgs = None,
132
+ default_message: Optional[str] = None,
133
+ locale: Optional[Locale] = None,
134
+ ) -> Optional[str]:
135
+ """按 ``code`` + ``locale`` 解析消息,找不到返回 ``default_message``(None 表示无默认)。"""
136
+ return self._resolve_with_fallback(code, args, locale, default=default_message)
137
+
138
+ def getMessageFromResolvable( # 对齐 Spring ``getMessage(resolvable, locale)``
139
+ self,
140
+ resolvable: MessageSourceResolvable,
141
+ locale: Optional[Locale] = None,
142
+ ) -> str:
143
+ """按 ``resolvable.codes`` 顺序解析;全部未命中时返回 ``default_message``,
144
+ 若默认消息为 None 则抛 ``NoSuchMessageException``。"""
145
+ loc = locale or Locale("")
146
+ for code in resolvable.get_codes():
147
+ msg = self._resolve_with_fallback(code, resolvable.get_arguments(), loc, default=None)
148
+ if msg is not None:
149
+ return msg
150
+ default = resolvable.get_default_message()
151
+ if default is not None:
152
+ return _format_message(default, resolvable.get_arguments(), loc)
153
+ raise NoSuchMessageException(resolvable.get_codes()[0] if resolvable.get_codes() else "",
154
+ loc)
155
+
156
+ # ==================== 子类实现点 ====================
157
+
158
+ def resolve_code(self, code: str, locale: Locale) -> Optional[str]:
159
+ """子类实现:返回原始(未格式化)消息模板,找不到返回 None。
160
+
161
+ 默认实现总是返回 None(等价于空消息源)。
162
+ """
163
+ return None
164
+
165
+ # ==================== 内部 ====================
166
+
167
+ def _resolve_with_fallback(
168
+ self,
169
+ code: str,
170
+ args: MessageArgs,
171
+ locale: Optional[Locale],
172
+ default: Optional[str],
173
+ ) -> Optional[str]:
174
+ loc = locale or Locale("")
175
+ msg = self.resolve_code(code, loc)
176
+ if msg is None:
177
+ return default
178
+ return _format_message(msg, args, loc)
179
+
180
+
181
+ # ==================== AbstractMessageSource ====================
182
+
183
+ class AbstractMessageSource(MessageSource):
184
+ """``MessageSource`` 抽象基类(对齐 Spring ``AbstractMessageSource``)。
185
+
186
+ 扩展点:
187
+ - ``resolve_code``:子类必须覆盖,返回原始消息模板。
188
+ - ``resolve_code_without_args``:可选覆盖,返回无参数消息模板(默认走 ``resolve_code``)。
189
+
190
+ 特性:
191
+ - ``parent_message_source``:父消息源;当前未命中时委派父级。
192
+ - ``use_code_as_default_message``:找不到时把 ``code`` 作为默认消息(不抛异常)。
193
+ 对齐 Spring ``AbstractMessageSource.setUseCodeAsDefaultMessage``。
194
+ """
195
+
196
+ def __init__(self, parent: Optional[MessageSource] = None):
197
+ self._parent: Optional[MessageSource] = parent
198
+ self._use_code_as_default_message: bool = False
199
+
200
+ @property
201
+ def parent_message_source(self) -> Optional[MessageSource]:
202
+ return self._parent
203
+
204
+ @parent_message_source.setter
205
+ def parent_message_source(self, value: Optional[MessageSource]) -> None:
206
+ self._parent = value
207
+
208
+ def set_use_code_as_default_message(self, flag: bool) -> None:
209
+ self._use_code_as_default_message = flag
210
+
211
+ def resolve_code_without_args(self, code: str, locale: Locale) -> Optional[str]:
212
+ """无参数解析(子类可覆盖以优化性能)。默认走 ``resolve_code``。"""
213
+ return self.resolve_code(code, locale)
214
+
215
+ def resolve_code(self, code: str, locale: Locale) -> Optional[str]: # 子类覆盖
216
+ return None
217
+
218
+ # ==================== 重写公共入口,加入父级委派 ====================
219
+
220
+ def _resolve_with_fallback(
221
+ self,
222
+ code: str,
223
+ args: MessageArgs,
224
+ locale: Optional[Locale],
225
+ default: Optional[str],
226
+ ) -> Optional[str]:
227
+ loc = locale or Locale("")
228
+ # 1. 当前消息源
229
+ if args is None:
230
+ msg = self.resolve_code_without_args(code, loc)
231
+ else:
232
+ msg = self.resolve_code(code, loc)
233
+ # 2. 父级委派
234
+ if msg is None and self._parent is not None:
235
+ return self._parent.getMessageOrDefault(code, args, default, loc)
236
+ if msg is None:
237
+ if self._use_code_as_default_message:
238
+ return code
239
+ return default
240
+ return _format_message(msg, args, loc)
241
+
242
+
243
+ __all__ = [
244
+ "NoSuchMessageException",
245
+ "MessageArgs",
246
+ "MessageSourceResolvable",
247
+ "DefaultMessageSourceResolvable",
248
+ "MessageSource",
249
+ "AbstractMessageSource",
250
+ ]
@@ -0,0 +1,79 @@
1
+ """``LocaleResolverMiddleware`` Starlette 中间件(对齐 Spring ``LocaleChangeInterceptor`` +
2
+ ``DispatcherServlet`` 的 locale 解析逻辑)。
3
+
4
+ 中间件在每个请求开始时:
5
+ 1. 用配置的 ``LocaleResolver`` 从请求解析 ``LocaleContext``;
6
+ 2. 写入 ``LocaleContextHolder``(``ContextVar``,协程安全);
7
+ 3. 把 ``LocaleContext`` 挂到 ``request.state.locale_context`` 供后续路由读取;
8
+ 4. 请求结束 ``reset_locale_context()``,避免泄漏。
9
+
10
+ 中间件不依赖 FastAPI,仅依赖 Starlette ``BaseHTTPMiddleware`` / ``Request``。
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from typing import Optional
15
+
16
+ from starlette.middleware.base import BaseHTTPMiddleware
17
+ from starlette.requests import Request
18
+
19
+ from .holder import LocaleContextHolder
20
+ from .locale import Locale
21
+ from .locale_resolver import (
22
+ AcceptHeaderLocaleResolver,
23
+ LocaleContext,
24
+ LocaleResolver,
25
+ SimpleLocaleContext,
26
+ )
27
+
28
+
29
+ class LocaleResolverMiddleware(BaseHTTPMiddleware):
30
+ """HTTP 中间件:解析 locale 并写入 ``LocaleContextHolder``。
31
+
32
+ Args:
33
+ app: ASGI 应用。
34
+ locale_resolver: 区域解析器;默认 ``AcceptHeaderLocaleResolver``。
35
+ """
36
+
37
+ def __init__(self, app, locale_resolver: Optional[LocaleResolver] = None):
38
+ super().__init__(app)
39
+ self._resolver: LocaleResolver = locale_resolver or AcceptHeaderLocaleResolver()
40
+
41
+ @property
42
+ def locale_resolver(self) -> LocaleResolver:
43
+ return self._resolver
44
+
45
+ async def dispatch(self, request: Request, call_next):
46
+ # 1. 解析 locale
47
+ try:
48
+ context = self._resolver.resolve_locale(request)
49
+ except Exception:
50
+ # 解析失败兜底:空 locale,避免中间件把请求 500
51
+ context = SimpleLocaleContext(Locale(""))
52
+ # 2. 写入 ContextVar(协程安全),返回 token 用于精确复位(支持嵌套)
53
+ token = LocaleContextHolder.set_locale_context(context)
54
+ # 3. 挂到 request.state 供路由读取
55
+ try:
56
+ request.state.locale_context = context
57
+ except Exception:
58
+ pass
59
+ try:
60
+ response = await call_next(request)
61
+ finally:
62
+ # 4. 请求结束用 token 复位(即使异常也复位,支持嵌套调用)
63
+ LocaleContextHolder.reset_locale_context(token)
64
+ return response
65
+
66
+
67
+ def get_request_locale(request: Optional[Request]) -> Locale:
68
+ """从请求 ``state.locale_context`` 或 ``LocaleContextHolder`` 取当前 locale。
69
+
70
+ 优先级:``request.state.locale_context`` > ``LocaleContextHolder`` > 空 ``Locale``。
71
+ """
72
+ if request is not None:
73
+ ctx = getattr(getattr(request, "state", None), "locale_context", None)
74
+ if isinstance(ctx, LocaleContext):
75
+ return ctx.get_locale()
76
+ return LocaleContextHolder.get_locale()
77
+
78
+
79
+ __all__ = ["LocaleResolverMiddleware", "get_request_locale"]