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/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
|
+
]
|