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
spring/web/swagger.py ADDED
@@ -0,0 +1,601 @@
1
+ """SpringBootAI Swagger / OpenAPI 注解驱动 API 文档模块。
2
+
3
+ 对齐 SpringDoc OpenAPI 3 注解体系(``@Tag``/``@Operation``/``@ApiResponse``/
4
+ ``@Parameter``/``@Schema``/``@SecurityScheme``/``@SecurityRequirement``),
5
+ 同时提供 Swagger 2 风格别名(``@Api``/``@ApiOperation``/``@ApiModel``/
6
+ ``@ApiResponses``/``@ApiParam``)以兼容习惯。
7
+
8
+ 设计原则(对齐项目既有范式):
9
+ - 注解复用 ``SpringAnnotation`` 描述符,元数据存入 ``__spring_annotations__``。
10
+ - ``collect_openapi_metadata`` 从 Controller 类 + 方法注解反射收集 OpenAPI 元数据,
11
+ 供 ``WebApplicationContext._add_route`` 传递给 FastAPI 路由参数。
12
+ - ``configure_swagger`` 自定义 ``app.openapi()``,注入全局 ``securitySchemes`` 与
13
+ ``@Schema`` 模型描述(``title``/``description``/``example``)。
14
+ - 配置由 ``application.yml`` 的 ``spring.swagger.*`` 驱动,对齐 Spring Boot
15
+ ``springdoc.api-docs.*`` / ``springdoc.swagger-ui.*``。
16
+
17
+ 与 Java Spring 的差异:
18
+ - Java 用 ``springdoc-openapi-starter-webmvc-ui`` 自动扫描;本实现由
19
+ ``WebApplicationContext`` 注册路由时同步注入 OpenAPI 元数据,无额外依赖。
20
+ - ``@Schema`` 通过后处理 ``components/schemas`` 注入(Pydantic 模型自动生成 schema
21
+ 的基础上叠加注解元数据),不支持完整的 OpenAPI Schema 属性全集。
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import logging
26
+ from dataclasses import dataclass, field
27
+ from typing import Any, Callable, Dict, List, Optional, Type, Union
28
+
29
+ from spring.annotations.core import SpringAnnotation, get_spring_annotations
30
+
31
+ logger = logging.getLogger("Spring.Web.Swagger")
32
+
33
+
34
+ # ============================================================================
35
+ # 注解定义(对齐 SpringDoc OpenAPI 3 + Swagger 2 别名)
36
+ # ============================================================================
37
+
38
+ class Tag(SpringAnnotation):
39
+ """类级标签,对齐 ``io.swagger.v3.oas.annotations.tags.Tag``。
40
+
41
+ 用在 ``@RestController`` 类上,为该 Controller 所有路由分组。
42
+ """
43
+
44
+ _annotation_type = "swagger_tag"
45
+
46
+ def __init__(self, name: str = "", description: str = ""):
47
+ super().__init__(name=name, description=description)
48
+
49
+
50
+ # Swagger 2 别名
51
+ class Api(Tag):
52
+ """``@Api`` —— Swagger 2 风格的 ``@Tag`` 别名。"""
53
+
54
+ _annotation_type = "swagger_tag"
55
+
56
+
57
+ class Operation(SpringAnnotation):
58
+ """方法级操作描述,对齐 ``io.swagger.v3.oas.annotations.Operation``。
59
+
60
+ 用在 ``@GetMapping``/``@PostMapping`` 等方法上,设置 Swagger UI 的
61
+ ``summary``/``description``/``operationId``/``deprecated``/``tags``。
62
+ """
63
+
64
+ _annotation_type = "swagger_operation"
65
+
66
+ def __init__(
67
+ self,
68
+ summary: str = "",
69
+ description: str = "",
70
+ operation_id: str = "",
71
+ deprecated: bool = False,
72
+ tags: Optional[List[str]] = None,
73
+ ):
74
+ super().__init__(
75
+ summary=summary,
76
+ description=description,
77
+ operation_id=operation_id,
78
+ deprecated=deprecated,
79
+ tags=tags or [],
80
+ )
81
+
82
+
83
+ # Swagger 2 别名
84
+ class ApiOperation(Operation):
85
+ """``@ApiOperation`` —— Swagger 2 风格的 ``@Operation`` 别名。"""
86
+
87
+ _annotation_type = "swagger_operation"
88
+
89
+
90
+ class ApiResponse(SpringAnnotation):
91
+ """方法级响应描述,对齐 ``io.swagger.v3.oas.annotations.responses.ApiResponse``。
92
+
93
+ 可重复使用(多个 ``@ApiResponse`` 描述不同状态码)。``response_model`` 为
94
+ Python 类型(Pydantic 模型或普通类),用于生成响应 Schema。
95
+ """
96
+
97
+ _annotation_type = "swagger_api_response"
98
+
99
+ def __init__(
100
+ self,
101
+ code: Union[int, str] = 200,
102
+ description: str = "",
103
+ response_model: Optional[Type] = None,
104
+ ):
105
+ super().__init__(
106
+ response_code=str(code),
107
+ description=description,
108
+ response_model=response_model,
109
+ )
110
+
111
+
112
+ class ApiResponses(SpringAnnotation):
113
+ """``@ApiResponses`` —— 聚合多个 ``@ApiResponse``(Swagger 2 风格)。
114
+
115
+ 也支持直接多次使用 ``@ApiResponse``,二者等价。
116
+ """
117
+
118
+ _annotation_type = "swagger_api_responses"
119
+
120
+ def __init__(self, responses: Optional[List[ApiResponse]] = None):
121
+ super().__init__(responses=responses or [])
122
+
123
+
124
+ class Parameter(SpringAnnotation):
125
+ """参数描述,对齐 ``io.swagger.v3.oas.annotations.Parameter``。
126
+
127
+ 用在方法上,按 ``name`` 匹配参数(path/query/header),注入 ``description``/
128
+ ``example``/``deprecated``/``required`` 到 OpenAPI schema。
129
+ """
130
+
131
+ _annotation_type = "swagger_parameter"
132
+
133
+ def __init__(
134
+ self,
135
+ name: str = "",
136
+ description: str = "",
137
+ required: Optional[bool] = None,
138
+ deprecated: bool = False,
139
+ example: Any = None,
140
+ ):
141
+ super().__init__(
142
+ name=name,
143
+ description=description,
144
+ required=required,
145
+ deprecated=deprecated,
146
+ example=example,
147
+ )
148
+
149
+
150
+ # Swagger 2 别名
151
+ class ApiParam(Parameter):
152
+ """``@ApiParam`` —— Swagger 2 风格的 ``@Parameter`` 别名。"""
153
+
154
+ _annotation_type = "swagger_parameter"
155
+
156
+
157
+ class Schema(SpringAnnotation):
158
+ """模型描述,对齐 ``io.swagger.v3.oas.annotations.media.Schema``。
159
+
160
+ 用在响应/请求体类型上,设置 ``title``/``description``/``example``。
161
+ 通过后处理 ``components/schemas`` 注入。
162
+ """
163
+
164
+ _annotation_type = "swagger_schema"
165
+
166
+ def __init__(
167
+ self,
168
+ title: str = "",
169
+ description: str = "",
170
+ example: Any = None,
171
+ deprecated: bool = False,
172
+ ):
173
+ super().__init__(
174
+ title=title,
175
+ description=description,
176
+ example=example,
177
+ deprecated=deprecated,
178
+ )
179
+
180
+
181
+ # Swagger 2 别名
182
+ class ApiModel(Schema):
183
+ """``@ApiModel`` —— Swagger 2 风格的 ``@Schema`` 别名。"""
184
+
185
+ _annotation_type = "swagger_schema"
186
+
187
+
188
+ class SecurityScheme(SpringAnnotation):
189
+ """全局安全方案,对齐 ``io.swagger.v3.oas.annotations.security.SecurityScheme``。
190
+
191
+ 用在配置类或主类上,声明全局可用的认证方案。常见用法:
192
+ ``@SecurityScheme(name="BearerAuth", scheme="bearer", bearer_format="JWT")``
193
+ 生成 OpenAPI ``securitySchemes``,Swagger UI 顶部出现 Authorize 按钮。
194
+ """
195
+
196
+ _annotation_type = "swagger_security_scheme"
197
+
198
+ def __init__(
199
+ self,
200
+ name: str = "BearerAuth",
201
+ scheme: str = "bearer", # bearer / basic
202
+ bearer_format: str = "JWT",
203
+ type: str = "http", # http / apiKey
204
+ in_: str = "header", # apiKey 时:header / query / cookie
205
+ header_name: str = "Authorization",
206
+ description: str = "",
207
+ ):
208
+ super().__init__(
209
+ name=name,
210
+ scheme=scheme,
211
+ bearer_format=bearer_format,
212
+ type=type,
213
+ in_=in_,
214
+ header_name=header_name,
215
+ description=description,
216
+ )
217
+
218
+
219
+ class SecurityRequirement(SpringAnnotation):
220
+ """方法级安全要求,对齐 ``io.swagger.v3.oas.annotations.security.SecurityRequirement``。
221
+
222
+ 用在需要认证的方法上,标记该路由需要指定的安全方案。
223
+ Swagger UI 会显示锁图标。
224
+ """
225
+
226
+ _annotation_type = "swagger_security_requirement"
227
+
228
+ def __init__(self, name: str = "BearerAuth", scopes: Optional[List[str]] = None):
229
+ super().__init__(name=name, scopes=scopes or [])
230
+
231
+
232
+ # ============================================================================
233
+ # SwaggerConfig —— 从 application.yml 读取
234
+ # ============================================================================
235
+
236
+ @dataclass
237
+ class SwaggerConfig:
238
+ """Swagger/OpenAPI 配置,对齐 ``springdoc.*`` 配置项。
239
+
240
+ 从 ``application.yml`` 的 ``spring.swagger.*``(或 ``springdoc.*``)读取:
241
+ ``spring.swagger.title`` / ``description`` / ``version`` / ``enabled`` /
242
+ ``docs-url`` / ``redoc-url`` / ``openapi-url`` / ``contact.name`` ...
243
+ """
244
+
245
+ enabled: bool = True
246
+ title: str = "SpringBootAI Application"
247
+ description: str = ""
248
+ version: str = "1.0.0"
249
+ terms_of_service: str = ""
250
+ contact_name: str = ""
251
+ contact_email: str = ""
252
+ contact_url: str = ""
253
+ license_name: str = ""
254
+ license_url: str = ""
255
+ docs_url: Optional[str] = "/docs"
256
+ redoc_url: Optional[str] = "/redoc"
257
+ openapi_url: Optional[str] = "/openapi.json"
258
+
259
+ @classmethod
260
+ def from_config(cls, config: Any) -> "SwaggerConfig":
261
+ """从配置字典构建。读取 ``spring.swagger.*`` 或 ``springdoc.*``。"""
262
+ if not isinstance(config, dict):
263
+ return cls()
264
+ spring = config.get("spring", {}) if isinstance(config.get("spring"), dict) else {}
265
+ swagger = spring.get("swagger", {}) if isinstance(spring, dict) else {}
266
+ if not isinstance(swagger, dict) or not swagger:
267
+ # 兼容 springdoc.* 顶层配置
268
+ swagger = config.get("springdoc", {}) if isinstance(config.get("springdoc"), dict) else {}
269
+ if not isinstance(swagger, dict):
270
+ return cls()
271
+
272
+ def _get(key: str, default: Any = None) -> Any:
273
+ # 松散绑定:kebab-case / snake_case
274
+ if key in swagger:
275
+ return swagger[key]
276
+ alt = key.replace("-", "_")
277
+ if alt in swagger:
278
+ return swagger[alt]
279
+ alt2 = key.replace("_", "-")
280
+ if alt2 in swagger:
281
+ return swagger[alt2]
282
+ return default
283
+
284
+ contact = _get("contact", {}) or {}
285
+ if not isinstance(contact, dict):
286
+ contact = {}
287
+ license_info = _get("license", {}) or {}
288
+ if not isinstance(license_info, dict):
289
+ license_info = {}
290
+
291
+ def _opt(key: str, default: str = "") -> str:
292
+ v = _get(key, default)
293
+ return v if v is not None else default
294
+
295
+ return cls(
296
+ enabled=bool(_get("enabled", True)),
297
+ title=_opt("title", "SpringBootAI Application"),
298
+ description=_opt("description", ""),
299
+ version=_opt("version", "1.0.0"),
300
+ terms_of_service=_opt("terms-of-service", ""),
301
+ contact_name=_opt("contact-name", "") or (contact.get("name", "") if isinstance(contact, dict) else ""),
302
+ contact_email=_opt("contact-email", "") or (contact.get("email", "") if isinstance(contact, dict) else ""),
303
+ contact_url=_opt("contact-url", "") or (contact.get("url", "") if isinstance(contact, dict) else ""),
304
+ license_name=_opt("license-name", "") or (license_info.get("name", "") if isinstance(license_info, dict) else ""),
305
+ license_url=_opt("license-url", "") or (license_info.get("url", "") if isinstance(license_info, dict) else ""),
306
+ docs_url=_get("docs-url", "/docs"),
307
+ redoc_url=_get("redoc-url", "/redoc"),
308
+ openapi_url=_get("openapi-url", "/openapi.json"),
309
+ )
310
+
311
+ def to_fastapi_kwargs(self) -> Dict[str, Any]:
312
+ """转换为 ``FastAPI()`` 构造参数。``enabled=False`` 时禁用所有文档端点。"""
313
+ if not self.enabled:
314
+ return dict(
315
+ title=self.title,
316
+ description=self.description,
317
+ version=self.version,
318
+ docs_url=None,
319
+ redoc_url=None,
320
+ openapi_url=None,
321
+ )
322
+ kwargs: Dict[str, Any] = dict(
323
+ title=self.title,
324
+ description=self.description,
325
+ version=self.version,
326
+ docs_url=self.docs_url,
327
+ redoc_url=self.redoc_url,
328
+ openapi_url=self.openapi_url,
329
+ )
330
+ if self.terms_of_service:
331
+ kwargs["terms_of_service"] = self.terms_of_service
332
+ if self.contact_name or self.contact_email or self.contact_url:
333
+ kwargs["contact"] = {
334
+ k: v for k, v in {
335
+ "name": self.contact_name,
336
+ "email": self.contact_email,
337
+ "url": self.contact_url,
338
+ }.items() if v
339
+ }
340
+ if self.license_name:
341
+ lic: Dict[str, Any] = {"name": self.license_name}
342
+ if self.license_url:
343
+ lic["url"] = self.license_url
344
+ kwargs["license_info"] = lic
345
+ return kwargs
346
+
347
+
348
+ # ============================================================================
349
+ # 元数据收集
350
+ # ============================================================================
351
+
352
+ def collect_openapi_metadata(
353
+ method: Callable,
354
+ controller_class: Optional[Type] = None,
355
+ ) -> Dict[str, Any]:
356
+ """从 Controller 方法 + 类的 Swagger 注解收集 OpenAPI 路由元数据。
357
+
358
+ 返回的 dict 可直接拆包为 FastAPI 路由装饰器参数(``tags``/``summary``/
359
+ ``description``/``operation_id``/``deprecated``/``responses``/``security``)。
360
+ """
361
+ result: Dict[str, Any] = {}
362
+
363
+ # ---- 类级 @Tag ----
364
+ class_tags: List[str] = []
365
+ if controller_class is not None:
366
+ for ann in get_spring_annotations(controller_class):
367
+ if isinstance(ann, Tag):
368
+ class_tags.append(ann.name)
369
+
370
+ # ---- 方法级注解 ----
371
+ method_annotations = get_spring_annotations(method) or getattr(method, "__spring_annotations__", [])
372
+
373
+ operation: Optional[Operation] = None
374
+ responses: Dict[str, Dict[str, Any]] = {}
375
+ security: List[Dict[str, List[str]]] = []
376
+ method_tags: List[str] = []
377
+
378
+ for ann in method_annotations:
379
+ if isinstance(ann, Operation):
380
+ operation = ann
381
+ if ann.tags:
382
+ method_tags.extend(ann.tags)
383
+ elif isinstance(ann, ApiResponse):
384
+ code = ann.response_code
385
+ entry: Dict[str, Any] = {"description": ann.description or ""}
386
+ if ann.response_model is not None:
387
+ entry["model"] = ann.response_model
388
+ responses[code] = entry
389
+ elif isinstance(ann, ApiResponses):
390
+ for sub in ann.responses:
391
+ if isinstance(sub, ApiResponse):
392
+ code = sub.response_code
393
+ e: Dict[str, Any] = {"description": sub.description or ""}
394
+ if sub.response_model is not None:
395
+ e["model"] = sub.response_model
396
+ responses[code] = e
397
+ elif isinstance(ann, SecurityRequirement):
398
+ security.append({ann.name: ann.scopes})
399
+
400
+ # ---- 组装 ----
401
+ tags = class_tags + method_tags
402
+ if tags:
403
+ result["tags"] = tags
404
+
405
+ if operation is not None:
406
+ if operation.summary:
407
+ result["summary"] = operation.summary
408
+ if operation.description:
409
+ result["description"] = operation.description
410
+ if operation.operation_id:
411
+ result["operation_id"] = operation.operation_id
412
+ if operation.deprecated:
413
+ result["deprecated"] = True
414
+
415
+ if responses:
416
+ result["responses"] = responses
417
+
418
+ if security:
419
+ # ``security`` 是 OpenAPI operation 级属性,FastAPI 路由装饰器不支持该参数,
420
+ # 通过 ``openapi_extra`` 合并到 operation schema(对齐 SpringDoc @SecurityRequirement)。
421
+ result["openapi_extra"] = {"security": security}
422
+
423
+ return result
424
+
425
+
426
+ def collect_security_schemes(controller_classes: List[Type]) -> Dict[str, Dict[str, Any]]:
427
+ """从 Controller 类(或配置类)收集全局 ``@SecurityScheme`` 注解。
428
+
429
+ 返回 OpenAPI ``securitySchemes`` 字典:
430
+ ``{"BearerAuth": {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}}``
431
+ """
432
+ schemes: Dict[str, Dict[str, Any]] = {}
433
+ for cls in controller_classes:
434
+ for ann in get_spring_annotations(cls):
435
+ if isinstance(ann, SecurityScheme):
436
+ schemes[ann.name] = _security_scheme_to_dict(ann)
437
+ return schemes
438
+
439
+
440
+ def _security_scheme_to_dict(ann: SecurityScheme) -> Dict[str, Any]:
441
+ """将 ``@SecurityScheme`` 注解转为 OpenAPI securityScheme 对象。"""
442
+ if ann.type == "apiKey":
443
+ return {
444
+ "type": "apiKey",
445
+ "in": ann.in_,
446
+ "name": ann.header_name,
447
+ "description": ann.description,
448
+ }
449
+ # http (bearer / basic)
450
+ scheme: Dict[str, Any] = {
451
+ "type": "http",
452
+ "scheme": ann.scheme,
453
+ "description": ann.description,
454
+ }
455
+ if ann.scheme == "bearer":
456
+ scheme["bearerFormat"] = ann.bearer_format
457
+ return {k: v for k, v in scheme.items() if v}
458
+
459
+
460
+ # ============================================================================
461
+ # Schema 后处理
462
+ # ============================================================================
463
+
464
+ # 注册的 @Schema 元数据:{类: Schema注解}
465
+ _SCHEMA_REGISTRY: Dict[Type, Schema] = {}
466
+
467
+
468
+ def register_schema(model_class: Type, schema_ann: Schema) -> None:
469
+ """注册模型类的 ``@Schema`` 元数据,供 ``configure_swagger`` 后处理注入。"""
470
+ _SCHEMA_REGISTRY[model_class] = schema_ann
471
+
472
+
473
+ def _scan_registered_schemas() -> Dict[str, Schema]:
474
+ """扫描所有已注册 ``@Schema`` 的类,返回 ``{类名: Schema注解}``。"""
475
+ result: Dict[str, Schema] = {}
476
+ for cls, ann in _SCHEMA_REGISTRY.items():
477
+ result[cls.__name__] = ann
478
+ return result
479
+
480
+
481
+ def _apply_schema_metadata(openapi_schema: Dict[str, Any]) -> None:
482
+ """后处理:将 ``@Schema`` 注解的 title/description/example 注入到
483
+ ``components/schemas`` 中对应模型。"""
484
+ components = openapi_schema.get("components", {})
485
+ schemas = components.get("schemas", {})
486
+ if not schemas:
487
+ return
488
+ for cls_name, schema_ann in _scan_registered_schemas().items():
489
+ if cls_name in schemas:
490
+ model_schema = schemas[cls_name]
491
+ if schema_ann.title:
492
+ model_schema["title"] = schema_ann.title
493
+ if schema_ann.description:
494
+ model_schema["description"] = schema_ann.description
495
+ if schema_ann.example is not None:
496
+ model_schema["example"] = schema_ann.example
497
+ if schema_ann.deprecated:
498
+ model_schema["deprecated"] = True
499
+
500
+
501
+ def _apply_parameter_metadata(
502
+ openapi_schema: Dict[str, Any],
503
+ method_param_meta: Dict[str, List[Parameter]],
504
+ ) -> None:
505
+ """后处理:将 ``@Parameter`` 注解的 description/example 注入到对应 path 的
506
+ parameters 中。
507
+
508
+ ``method_param_meta``: ``{path:method: [Parameter...]}``,key 为
509
+ ``f"{path}:{http_method}"``(与 ``WebApplicationContext`` 注册时一致)。
510
+ """
511
+ paths = openapi_schema.get("paths", {})
512
+ for path, path_item in paths.items():
513
+ for http_method, operation in path_item.items():
514
+ if not isinstance(operation, dict):
515
+ continue
516
+ # 用 path:method 作为 key 查找(与注册时一致)
517
+ key = f"{path}:{http_method}"
518
+ params_meta = method_param_meta.get(key, [])
519
+ if not params_meta:
520
+ continue
521
+ params = operation.get("parameters", [])
522
+ for p_meta in params_meta:
523
+ for p in params:
524
+ if p.get("name") == p_meta.name:
525
+ if p_meta.description:
526
+ p["description"] = p_meta.description
527
+ if p_meta.example is not None:
528
+ p["example"] = p_meta.example
529
+ if p_meta.deprecated:
530
+ p["deprecated"] = True
531
+ if p_meta.required is not None:
532
+ p["required"] = p_meta.required
533
+ break
534
+
535
+
536
+ # ============================================================================
537
+ # configure_swagger —— 配置 FastAPI 应用
538
+ # ============================================================================
539
+
540
+ def configure_swagger(
541
+ app: Any,
542
+ swagger_config: Optional[SwaggerConfig] = None,
543
+ security_schemes: Optional[Dict[str, Dict[str, Any]]] = None,
544
+ method_param_meta: Optional[Dict[str, List[Parameter]]] = None,
545
+ ) -> None:
546
+ """自定义 ``app.openapi()``,注入全局 ``securitySchemes`` 与 ``@Schema``/
547
+ ``@Parameter`` 后处理。
548
+
549
+ 在 ``WebApplicationContext.init()`` 末尾调用(路由注册完成后)。
550
+ """
551
+ if swagger_config is not None and not swagger_config.enabled:
552
+ # 已在 FastAPI 创建时禁用 docs_url/openapi_url,无需后处理
553
+ return
554
+
555
+ security_schemes = security_schemes or {}
556
+ method_param_meta = method_param_meta or {}
557
+
558
+ original_openapi = app.openapi
559
+
560
+ def custom_openapi():
561
+ if app.openapi_schema:
562
+ return app.openapi_schema
563
+ try:
564
+ schema = original_openapi()
565
+ except Exception:
566
+ logger.debug("openapi() 生成失败,跳过 Swagger 后处理", exc_info=True)
567
+ return app.openapi_schema or {}
568
+ # 注入全局 securitySchemes
569
+ if security_schemes:
570
+ components = schema.setdefault("components", {})
571
+ components.setdefault("securitySchemes", {}).update(security_schemes)
572
+ # @Schema 后处理
573
+ try:
574
+ _apply_schema_metadata(schema)
575
+ except Exception:
576
+ logger.debug("@Schema 后处理失败", exc_info=True)
577
+ # @Parameter 后处理
578
+ try:
579
+ _apply_parameter_metadata(schema, method_param_meta)
580
+ except Exception:
581
+ logger.debug("@Parameter 后处理失败", exc_info=True)
582
+ app.openapi_schema = schema
583
+ return schema
584
+
585
+ app.openapi = custom_openapi
586
+
587
+
588
+ __all__ = [
589
+ # 注解(OpenAPI 3)
590
+ "Tag", "Operation", "ApiResponse", "ApiResponses",
591
+ "Parameter", "Schema", "SecurityScheme", "SecurityRequirement",
592
+ # 别名(Swagger 2)
593
+ "Api", "ApiOperation", "ApiModel", "ApiParam",
594
+ # 配置
595
+ "SwaggerConfig",
596
+ # 元数据收集
597
+ "collect_openapi_metadata", "collect_security_schemes",
598
+ "register_schema",
599
+ # 配置函数
600
+ "configure_swagger",
601
+ ]