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/actuator.py ADDED
@@ -0,0 +1,319 @@
1
+ """Spring Boot Actuator 风格运维端点(扩展既有 ``/actuator/health``)。
2
+
3
+ 在既有 health 端点(``spring.web.health``)基础上补齐标准 Actuator 端点:
4
+ ``/actuator``(端点目录)、``/env``、``/loggers``、``/metrics``、``/beans``、
5
+ ``/configprops``、``/mappings``、``/threaddump``。
6
+
7
+ 设计:
8
+ - **纯函数 + 薄端点**:核心逻辑为接收 ``context`` 的纯函数,便于单测;路由函数仅做 HTTP 包装。
9
+ - **复用现有上下文**:与 ``health.py`` 共享 ``_application_context`` 全局(``configure_actuator`` 设置)。
10
+ - **脱敏**:``/env`` 自动屏蔽 password/secret/key/token/credential 等敏感键值(对齐 Spring Boot)。
11
+ - **loggers 动态调整**:GET 列出,POST ``/loggers/{name}`` 实时修改日志级别。
12
+
13
+ 与 Java 差异:
14
+ - Python ``logging`` 无全局 logger 注册表,``/loggers`` 仅列出 root + ``Logger.manager.loggerDict`` 已实例化的 logger。
15
+ - ``/metrics`` 返回 JSON 指标名列表(Prometheus 文本格式仍由 ``/actuator/prometheus`` 提供)。
16
+ """
17
+ import logging
18
+ import threading
19
+ import time
20
+ import traceback
21
+ from typing import Any, Dict, List, Optional
22
+
23
+ from fastapi import APIRouter, Body
24
+ from fastapi.responses import JSONResponse
25
+
26
+ actuator_router = APIRouter()
27
+ _application_context = None
28
+
29
+ # 敏感键关键词(命中即脱敏,对齐 Spring Boot env 脱敏)
30
+ _SENSITIVE_KEYS = ("password", "secret", "token", "credential", "passwd", "api_key", "apikey")
31
+
32
+
33
+ def configure_actuator(application_context) -> None:
34
+ """注入应用上下文(与 ``health.configure_health_checks`` 平行调用)。"""
35
+ global _application_context
36
+ _application_context = application_context
37
+
38
+
39
+ def _get_context():
40
+ return _application_context
41
+
42
+
43
+ # ==================== 端点目录 ====================
44
+
45
+ def get_endpoint_directory() -> dict:
46
+ """列出所有可用的 Actuator 端点(对齐 ``/actuator`` 根端点)。"""
47
+ endpoints = {
48
+ "health": {"href": "/actuator/health", "methods": ["GET"]},
49
+ "health-liveness": {"href": "/actuator/health/liveness", "methods": ["GET"]},
50
+ "health-readiness": {"href": "/actuator/health/readiness", "methods": ["GET"]},
51
+ "info": {"href": "/actuator/info", "methods": ["GET"]},
52
+ "env": {"href": "/actuator/env", "methods": ["GET"]},
53
+ "loggers": {"href": "/actuator/loggers", "methods": ["GET", "POST"]},
54
+ "metrics": {"href": "/actuator/metrics", "methods": ["GET"]},
55
+ "beans": {"href": "/actuator/beans", "methods": ["GET"]},
56
+ "configprops": {"href": "/actuator/configprops", "methods": ["GET"]},
57
+ "mappings": {"href": "/actuator/mappings", "methods": ["GET"]},
58
+ "threaddump": {"href": "/actuator/threaddump", "methods": ["GET"]},
59
+ "prometheus": {"href": "/actuator/prometheus", "methods": ["GET"]},
60
+ }
61
+ return {"_links": {k: v for k, v in endpoints.items()}}
62
+
63
+
64
+ # ==================== /env 环境配置 ====================
65
+
66
+ def _sanitize(obj: Any) -> Any:
67
+ """递归脱敏:键名命中敏感关键词的值替换为 ``******``。"""
68
+ if isinstance(obj, dict):
69
+ return {
70
+ k: ("******" if any(s in k.lower() for s in _SENSITIVE_KEYS) else _sanitize(v))
71
+ for k, v in obj.items()
72
+ }
73
+ if isinstance(obj, list):
74
+ return [_sanitize(item) for item in obj]
75
+ return obj
76
+
77
+
78
+ def get_env_info(context) -> dict:
79
+ """返回环境配置快照(脱敏)+ active profile。
80
+
81
+ 对齐 Spring ``/actuator/env``:展示 property sources 与 active profile。
82
+ """
83
+ result: Dict[str, Any] = {"activeProfiles": [], "propertySources": []}
84
+ if context is None:
85
+ return result
86
+ try:
87
+ active = context.config_loader.get_active_profile() if hasattr(context, "config_loader") else None
88
+ if active:
89
+ result["activeProfiles"] = [active]
90
+ except Exception:
91
+ pass
92
+ try:
93
+ config = context.get_config() if hasattr(context, "get_config") else {}
94
+ result["propertySources"].append({
95
+ "name": "applicationConfig",
96
+ "properties": _sanitize(config) if isinstance(config, dict) else {},
97
+ })
98
+ except Exception:
99
+ pass
100
+ return result
101
+
102
+
103
+ # ==================== /loggers 日志级别 ====================
104
+
105
+ _LEVEL_NAMES = {
106
+ logging.DEBUG: "DEBUG", logging.INFO: "INFO", logging.WARNING: "WARNING",
107
+ logging.ERROR: "ERROR", logging.CRITICAL: "CRITICAL", logging.NOTSET: "NOTSET",
108
+ }
109
+
110
+
111
+ def get_loggers() -> dict:
112
+ """列出 root + 已实例化的 logger 及其有效级别。"""
113
+ loggers: Dict[str, str] = {"ROOT": _LEVEL_NAMES.get(logging.getLogger().getEffectiveLevel(), "NOTSET")}
114
+ manager_dict = logging.Logger.manager.loggerDict
115
+ for name, logger in manager_dict.items():
116
+ if isinstance(logger, logging.Logger):
117
+ loggers[name] = _LEVEL_NAMES.get(logger.getEffectiveLevel(), "NOTSET")
118
+ return {"levels": ["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "FATAL", "OFF"],
119
+ "loggers": loggers}
120
+
121
+
122
+ def get_logger_level(name: str) -> Optional[dict]:
123
+ logger = logging.getLogger(name)
124
+ return {"configuredLevel": _LEVEL_NAMES.get(logger.level, "NOTSET") if logger.level else None,
125
+ "effectiveLevel": _LEVEL_NAMES.get(logger.getEffectiveLevel(), "NOTSET")}
126
+
127
+
128
+ def set_logger_level(name: str, level: str) -> dict:
129
+ """动态修改 logger 级别。``name=root`` 修改 root logger。"""
130
+ target_name = "" if name.lower() == "root" else name
131
+ logger = logging.getLogger(target_name)
132
+ level_value = getattr(logging, level.upper(), None)
133
+ if level_value is None and level.upper() != "OFF":
134
+ raise ValueError(f"不支持的日志级别: {level}")
135
+ if level.upper() == "OFF":
136
+ logger.setLevel(logging.CRITICAL + 100)
137
+ else:
138
+ logger.setLevel(level_value)
139
+ return {"configuredLevel": level.upper()}
140
+
141
+
142
+ # ==================== /metrics 指标列表 ====================
143
+
144
+ def get_metrics() -> dict:
145
+ """返回可用指标名列表(JSON 视图;Prometheus 文本格式见 /actuator/prometheus)。"""
146
+ names: List[str] = []
147
+ try:
148
+ from spring.monitoring.prometheus import prometheus_metrics
149
+ metrics_map = prometheus_metrics.get_metrics()
150
+ names = list(metrics_map.keys())
151
+ except Exception:
152
+ pass
153
+ return {"names": names}
154
+
155
+
156
+ # ==================== /beans Bean 列表 ====================
157
+
158
+ def get_beans(context) -> dict:
159
+ """列出 IoC 容器中所有 Bean 的元信息。"""
160
+ beans: Dict[str, Any] = {}
161
+ if context is not None:
162
+ try:
163
+ factory = context.bean_factory
164
+ except AttributeError:
165
+ factory = None
166
+ if factory is not None:
167
+ for name in factory.get_bean_names():
168
+ definition = factory.get_bean_definition(name)
169
+ if definition is None:
170
+ beans[name] = {"type": "unknown", "scope": "unknown"}
171
+ continue
172
+ bean_class = definition.bean_class
173
+ type_name = getattr(bean_class, "__name__", str(bean_class)) if bean_class else "unknown"
174
+ beans[name] = {
175
+ "type": type_name,
176
+ "scope": getattr(definition, "scope", "singleton"),
177
+ "singleton": getattr(definition, "is_singleton", True),
178
+ }
179
+ return {"contexts": {"application": {"beans": beans}}}
180
+
181
+
182
+ # ==================== /configprops 配置属性绑定 ====================
183
+
184
+ def get_configprops(context) -> dict:
185
+ """列出 ``@ConfigurationProperties`` 绑定的配置前缀与值(脱敏)。"""
186
+ result: Dict[str, Any] = {}
187
+ if context is None:
188
+ return result
189
+ try:
190
+ factory = context.bean_factory
191
+ except AttributeError:
192
+ return result
193
+ for name in factory.get_bean_names():
194
+ definition = factory.get_bean_definition(name)
195
+ if definition is None:
196
+ continue
197
+ props = definition.annotations.get("properties", []) if hasattr(definition, "annotations") else []
198
+ if not props:
199
+ continue
200
+ for prop_ann in props:
201
+ prefix = getattr(prop_ann, "prefix", None)
202
+ if not prefix:
203
+ continue
204
+ try:
205
+ config = context.get_config()
206
+ bound = context.config_loader.get_prefix_config(prefix) if hasattr(context, "config_loader") else {}
207
+ except Exception:
208
+ bound = {}
209
+ result[prefix] = {"prefix": prefix, "properties": _sanitize(bound)}
210
+ return {"contexts": {"application": {"beans": result}}}
211
+
212
+
213
+ # ==================== /mappings 路由映射 ====================
214
+
215
+ def get_mappings(app) -> dict:
216
+ """列出 FastAPI 应用的 HTTP 路由映射。"""
217
+ mappings: List[Dict[str, Any]] = []
218
+ if app is None:
219
+ return {"contexts": {"application": {"mappings": {"dispatcherServlets": {"dispatcherServlet": mappings}}}}}
220
+ for route in getattr(app, "routes", []):
221
+ path = getattr(route, "path", None)
222
+ methods = getattr(route, "methods", None)
223
+ if path is None:
224
+ continue
225
+ mappings.append({
226
+ "path": path,
227
+ "methods": sorted(list(methods)) if methods else ["GET"],
228
+ "name": getattr(route, "name", ""),
229
+ })
230
+ return {"contexts": {"application": {"mappings": {"dispatcherServlets": {"dispatcherServlet": mappings}}}}}
231
+
232
+
233
+ # ==================== /threaddump 线程转储 ====================
234
+
235
+ def get_threaddump() -> dict:
236
+ """返回当前进程所有线程的转储(id/name/alive/daemon/stack)。"""
237
+ import sys
238
+ frames_map = sys._current_frames()
239
+ threads = []
240
+ for thread in threading.enumerate():
241
+ frames = []
242
+ frame = frames_map.get(thread.ident)
243
+ if frame is not None:
244
+ try:
245
+ for filename, lineno, name, _line in traceback.extract_stack(frame):
246
+ frames.append({"file": filename, "line": lineno, "method": name})
247
+ except Exception:
248
+ pass
249
+ threads.append({
250
+ "threadId": thread.ident,
251
+ "threadName": thread.name,
252
+ "threadState": "RUNNABLE" if thread.is_alive() else "TERMINATED",
253
+ "daemon": thread.daemon,
254
+ "stack": frames,
255
+ })
256
+ return {"threads": threads}
257
+
258
+
259
+ # ==================== HTTP 端点(薄包装) ====================
260
+
261
+ @actuator_router.get('')
262
+ @actuator_router.get('/')
263
+ def actuator_root():
264
+ return JSONResponse(content=get_endpoint_directory(), status_code=200)
265
+
266
+
267
+ @actuator_router.get('/env')
268
+ def env_endpoint():
269
+ return JSONResponse(content=get_env_info(_get_context()), status_code=200)
270
+
271
+
272
+ @actuator_router.get('/loggers')
273
+ def loggers_endpoint():
274
+ return JSONResponse(content=get_loggers(), status_code=200)
275
+
276
+
277
+ @actuator_router.get('/loggers/{name}')
278
+ def logger_detail(name: str):
279
+ return JSONResponse(content=get_logger_level(name), status_code=200)
280
+
281
+
282
+ @actuator_router.post('/loggers/{name}')
283
+ def logger_update(name: str, body: dict = Body(default={})):
284
+ """请求体 ``{"configuredLevel": "DEBUG"}`` 动态修改级别。"""
285
+ level = (body or {}).get("configuredLevel", "INFO")
286
+ try:
287
+ result = set_logger_level(name, level)
288
+ return JSONResponse(content=result, status_code=200)
289
+ except ValueError as e:
290
+ return JSONResponse(content={"error": str(e)}, status_code=400)
291
+
292
+
293
+ @actuator_router.get('/metrics')
294
+ def metrics_endpoint():
295
+ return JSONResponse(content=get_metrics(), status_code=200)
296
+
297
+
298
+ @actuator_router.get('/beans')
299
+ def beans_endpoint():
300
+ return JSONResponse(content=get_beans(_get_context()), status_code=200)
301
+
302
+
303
+ @actuator_router.get('/configprops')
304
+ def configprops_endpoint():
305
+ return JSONResponse(content=get_configprops(_get_context()), status_code=200)
306
+
307
+
308
+ @actuator_router.get('/mappings')
309
+ def mappings_endpoint():
310
+ app = None
311
+ ctx = _get_context()
312
+ if ctx is not None and hasattr(ctx, "web_context"):
313
+ app = ctx.web_context.get_app()
314
+ return JSONResponse(content=get_mappings(app), status_code=200)
315
+
316
+
317
+ @actuator_router.get('/threaddump')
318
+ def threaddump_endpoint():
319
+ return JSONResponse(content=get_threaddump(), status_code=200)
@@ -0,0 +1,61 @@
1
+ from typing import Type, Callable, Dict, Any, Optional
2
+ from fastapi import Request
3
+ from spring.web.result import Result
4
+ import logging
5
+
6
+
7
+ class GlobalExceptionHandler:
8
+ def __init__(self, show_details: bool = False):
9
+ self._handlers: Dict[Type[Exception], Callable] = {}
10
+ self._show_details = show_details
11
+ self._logger = logging.getLogger("Spring.ExceptionHandler")
12
+
13
+ def add_exception_handler(self, exception_type: Type[Exception], handler: Callable) -> None:
14
+ self._handlers[exception_type] = handler
15
+
16
+ def get_handler(self, exception_type: Type[Exception]) -> Optional[Callable]:
17
+ if exception_type in self._handlers:
18
+ return self._handlers[exception_type]
19
+
20
+ for registered_type, handler in self._handlers.items():
21
+ if issubclass(exception_type, registered_type):
22
+ return handler
23
+
24
+ return None
25
+
26
+ def handle(self, exception: Exception) -> Result:
27
+ handler = self.get_handler(type(exception))
28
+ if handler:
29
+ try:
30
+ result = handler(exception)
31
+ if isinstance(result, Result):
32
+ return result
33
+ return Result.error(message=str(result))
34
+ except Exception as e:
35
+ self._logger.error(f"Exception handler failed: {str(e)}")
36
+ return Result.error(message="Internal server error")
37
+
38
+ return self._default_handler(exception)
39
+
40
+ def _default_handler(self, exception: Exception) -> Result:
41
+ import traceback
42
+ self._logger.error(f"Unexpected error: {str(exception)}")
43
+ self._logger.error(traceback.format_exc())
44
+
45
+ if self._show_details:
46
+ return Result.error(message=f"Unexpected error: {str(exception)}")
47
+ return Result.error(message="Internal server error")
48
+
49
+ def register_default_handlers(self) -> None:
50
+ self.add_exception_handler(ValueError, self._handle_value_error)
51
+ self.add_exception_handler(TypeError, self._handle_type_error)
52
+ self.add_exception_handler(Exception, self._default_handler)
53
+
54
+ def _handle_value_error(self, exception: ValueError) -> Result:
55
+ return Result.bad_request(message=str(exception))
56
+
57
+ def _handle_type_error(self, exception: TypeError) -> Result:
58
+ return Result.bad_request(message=str(exception))
59
+
60
+ def set_show_details(self, show_details: bool) -> None:
61
+ self._show_details = show_details