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/health.py ADDED
@@ -0,0 +1,399 @@
1
+ """
2
+ 健康检查模块
3
+ 提供Actuator风格的健康检查端点
4
+ """
5
+ import time
6
+ import logging
7
+ import threading
8
+ import queue
9
+ import platform
10
+ from concurrent.futures import ThreadPoolExecutor
11
+ from fastapi import APIRouter
12
+ from fastapi.responses import JSONResponse, Response
13
+ from spring.utils.redis_client import redis_client
14
+ from spring.cloud.discovery import nacos_client
15
+
16
+ try:
17
+ from spring.orm.database import db_manager
18
+ except ImportError:
19
+ db_manager = None
20
+
21
+ logger = logging.getLogger("Spring.Web.Health")
22
+
23
+ health_router = APIRouter()
24
+ _application_context = None
25
+
26
+
27
+ def configure_health_checks(application_context) -> None:
28
+ global _application_context
29
+ _application_context = application_context
30
+
31
+ # 单个组件健康检查的最大耗时(秒)。
32
+ # 避免某个组件(如 Nacos/RabbitMQ 未配置但尝试连接默认地址)卡死整个 /actuator/health 端点,
33
+ # 进而拖垮 Docker HEALTHCHECK 与运维监控。
34
+ _CHECK_TIMEOUT_SECONDS = 2.0
35
+
36
+ _COMPONENT_CHECKS = {
37
+ 'redis': lambda: _check_redis(),
38
+ 'database': lambda: _check_database(),
39
+ 'nacos': lambda: _check_nacos(),
40
+ 'rabbitmq': lambda: _check_rabbitmq(),
41
+ 'seata': lambda: _check_seata(),
42
+ }
43
+
44
+
45
+ def _run_with_timeout(func, timeout: float = _CHECK_TIMEOUT_SECONDS):
46
+ """
47
+ 在独立 daemon 线程中执行健康检查,超时则立即返回 DOWN,不阻塞主请求。
48
+
49
+ 使用 daemon 线程而非 concurrent.futures.ThreadPoolExecutor,
50
+ 因为后者在 with 块退出时会 shutdown(wait=True) 阻塞等待卡住的任务完成,
51
+ 反而会让超时机制失效。daemon 线程超时后主线程立即返回,
52
+ 卡住的线程在后台继续运行但不影响响应,进程退出时自动清理。
53
+
54
+ Args:
55
+ func: 无参的可调用对象,返回状态字典
56
+ timeout: 最大等待秒数
57
+
58
+ Returns:
59
+ 检查结果字典;若超时则返回 DOWN + reason
60
+ """
61
+ result_q: "queue.Queue" = queue.Queue()
62
+
63
+ def _worker():
64
+ try:
65
+ result_q.put(('ok', func()))
66
+ except Exception as e:
67
+ result_q.put(('err', e))
68
+
69
+ t = threading.Thread(target=_worker, daemon=True)
70
+ t.start()
71
+ t.join(timeout=timeout)
72
+
73
+ if t.is_alive():
74
+ # 超时:daemon 线程继续在后台跑,主线程立即返回
75
+ return {
76
+ 'status': 'DOWN',
77
+ 'enabled': True,
78
+ 'reason': f'health check timeout after {timeout}s'
79
+ }
80
+
81
+ try:
82
+ kind, val = result_q.get_nowait()
83
+ if kind == 'ok':
84
+ return val
85
+ return {'status': 'DOWN', 'enabled': True, 'reason': str(val)}
86
+ except queue.Empty:
87
+ return {'status': 'DOWN', 'enabled': True, 'reason': 'health check returned no result'}
88
+
89
+
90
+ def _collect_component_health() -> dict:
91
+ """并发执行各组件检查,使探针耗时接近单个检查的最大超时。"""
92
+ with ThreadPoolExecutor(max_workers=len(_COMPONENT_CHECKS)) as executor:
93
+ futures = {
94
+ name: executor.submit(_run_with_timeout, check)
95
+ for name, check in _COMPONENT_CHECKS.items()
96
+ }
97
+ return {name: future.result() for name, future in futures.items()}
98
+
99
+
100
+ def _enabled_down_components(components: dict) -> list:
101
+ return [
102
+ name for name, status in components.items()
103
+ if status.get('enabled', False) and status.get('status') == 'DOWN'
104
+ ]
105
+
106
+
107
+ @health_router.get('/health')
108
+ def health_check():
109
+ """
110
+ 健康检查端点
111
+ 返回所有组件的健康状态
112
+
113
+ Returns:
114
+ JSON格式的健康状态信息
115
+ """
116
+ health_status = {
117
+ 'status': 'UP',
118
+ 'timestamp': time.time(),
119
+ 'components': {}
120
+ }
121
+
122
+ health_status['components'] = _collect_component_health()
123
+ if _enabled_down_components(health_status['components']):
124
+ health_status['status'] = 'DEGRADED'
125
+
126
+ status_code = 200 if health_status['status'] == 'UP' else 503
127
+ return JSONResponse(content=health_status, status_code=status_code)
128
+
129
+
130
+ @health_router.get('/health/liveness')
131
+ def liveness_check():
132
+ """
133
+ 存活检查端点
134
+ 检查应用是否正在运行
135
+
136
+ Returns:
137
+ JSON格式的存活状态
138
+ """
139
+ return JSONResponse(content={
140
+ 'status': 'UP',
141
+ 'timestamp': time.time()
142
+ }, status_code=200)
143
+
144
+
145
+ @health_router.get('/health/readiness')
146
+ def readiness_check():
147
+ """
148
+ 就绪检查端点
149
+ 检查应用是否准备好处理请求
150
+
151
+ Returns:
152
+ JSON格式的就绪状态
153
+ """
154
+ components = _collect_component_health()
155
+ unavailable = _enabled_down_components(components)
156
+ if unavailable:
157
+ return JSONResponse(content={
158
+ 'status': 'NOT_READY',
159
+ 'timestamp': time.time(),
160
+ 'reason': f"Required components unavailable: {', '.join(unavailable)}",
161
+ 'components': components,
162
+ }, status_code=503)
163
+
164
+ return JSONResponse(content={
165
+ 'status': 'READY',
166
+ 'timestamp': time.time(),
167
+ 'components': components,
168
+ }, status_code=200)
169
+
170
+
171
+ @health_router.get('/prometheus')
172
+ def prometheus_metrics():
173
+ config = _application_context.get_config() if _application_context is not None else {}
174
+ if not config.get('prometheus', {}).get('enabled', False):
175
+ return JSONResponse({'detail': 'Prometheus metrics are disabled'}, status_code=404)
176
+ from spring.monitoring.prometheus import CONTENT_TYPE_LATEST, prometheus_metrics as metrics
177
+ return Response(
178
+ content=metrics.generate_metrics_data(),
179
+ headers={'Content-Type': CONTENT_TYPE_LATEST},
180
+ )
181
+
182
+
183
+ @health_router.get('/info')
184
+ def info_check():
185
+ """返回不包含密钥和连接凭据的应用基本信息。"""
186
+ config = _application_context.get_config() if _application_context is not None else {}
187
+ spring_config = config.get('spring', {}) or {}
188
+ application_config = spring_config.get('application', {}) or {}
189
+ profile_config = spring_config.get('profiles', {}) or {}
190
+ try:
191
+ from spring import __version__ as spring_version
192
+ except ImportError:
193
+ spring_version = 'unknown'
194
+
195
+ return JSONResponse(content={
196
+ 'application': {
197
+ 'name': application_config.get('name', 'springpy-application'),
198
+ 'profile': profile_config.get('active', 'default'),
199
+ },
200
+ 'framework': {
201
+ 'name': 'SpringBootAI',
202
+ 'version': spring_version,
203
+ 'python': platform.python_version(),
204
+ },
205
+ }, status_code=200)
206
+
207
+
208
+ def _check_redis() -> dict:
209
+ """检查Redis健康状态"""
210
+ try:
211
+ # 尊重 application.yml 的 redis.enabled 配置:
212
+ # 未启用时不尝试连接,直接返回 DISABLED,避免拖累整体健康状态
213
+ try:
214
+ redis_cfg = (
215
+ _application_context.get_config().get('redis', {})
216
+ if _application_context is not None else {}
217
+ ) or {}
218
+ if not redis_cfg.get('enabled', False):
219
+ return {
220
+ 'status': 'DISABLED',
221
+ 'enabled': False,
222
+ 'reason': 'Redis not configured (redis.enabled=false)'
223
+ }
224
+ except Exception:
225
+ # 配置读取失败时回退到原有行为(尝试连接)
226
+ pass
227
+
228
+ client = redis_client.get_client()
229
+ if client:
230
+ client.ping()
231
+ info = client.info()
232
+ return {
233
+ 'status': 'UP',
234
+ 'enabled': True,
235
+ 'version': info.get('redis_version', 'unknown'),
236
+ 'used_memory': info.get('used_memory_human', 'unknown')
237
+ }
238
+ else:
239
+ return {
240
+ 'status': 'DISABLED',
241
+ 'enabled': False,
242
+ 'reason': 'Redis not configured'
243
+ }
244
+ except Exception as e:
245
+ return {
246
+ 'status': 'DOWN',
247
+ 'enabled': True,
248
+ 'reason': str(e)
249
+ }
250
+
251
+
252
+ def _check_database() -> dict:
253
+ """检查数据库健康状态"""
254
+ try:
255
+ if _application_context is not None:
256
+ database_config = (
257
+ _application_context.get_config().get('database', {}) or {}
258
+ )
259
+ if not database_config.get('enabled', False):
260
+ return {
261
+ 'status': 'DISABLED',
262
+ 'enabled': False,
263
+ 'reason': 'Database not configured (database.enabled=false)',
264
+ }
265
+
266
+ if _application_context is not None and _application_context.contains_bean(
267
+ 'sqlSessionFactory'
268
+ ):
269
+ factory = _application_context.get_bean('sqlSessionFactory')
270
+ pooled_connection = factory.connection_pool.get_connection()
271
+ factory.connection_pool.return_connection(pooled_connection)
272
+ return {
273
+ 'status': 'UP',
274
+ 'enabled': True,
275
+ 'type': 'mybatis',
276
+ 'pool': factory.connection_pool.get_pool_stats(),
277
+ }
278
+
279
+ engine = db_manager.get_engine() if db_manager is not None else None
280
+ if engine:
281
+ connection = engine.connect()
282
+ connection.close()
283
+ return {
284
+ 'status': 'UP',
285
+ 'enabled': True,
286
+ 'url': db_manager.db_url
287
+ }
288
+ else:
289
+ return {
290
+ 'status': 'DISABLED',
291
+ 'enabled': False,
292
+ 'reason': 'Database not configured'
293
+ }
294
+ except Exception as e:
295
+ return {
296
+ 'status': 'DOWN',
297
+ 'enabled': True,
298
+ 'reason': str(e)
299
+ }
300
+
301
+
302
+ def _check_nacos() -> dict:
303
+ """检查Nacos健康状态"""
304
+ try:
305
+ config = _application_context.get_config() if _application_context is not None else {}
306
+ if not config.get('discovery', {}).get('enabled', False):
307
+ return {'status': 'DISABLED', 'enabled': False, 'reason': 'Nacos not configured'}
308
+ if nacos_client.is_healthy():
309
+ services = nacos_client.get_services()
310
+ return {
311
+ 'status': 'UP',
312
+ 'enabled': True,
313
+ 'server_addr': nacos_client.server_addr,
314
+ 'service_count': len(services) if services else 0
315
+ }
316
+ return {
317
+ 'status': 'DOWN',
318
+ 'enabled': True,
319
+ 'server_addr': nacos_client.server_addr,
320
+ 'reason': 'Nacos liveness check failed'
321
+ }
322
+ except Exception as e:
323
+ return {
324
+ 'status': 'DOWN',
325
+ 'enabled': True,
326
+ 'reason': str(e)
327
+ }
328
+
329
+
330
+ def _check_rabbitmq() -> dict:
331
+ """检查RabbitMQ健康状态"""
332
+ try:
333
+ config = _application_context.get_config() if _application_context is not None else {}
334
+ if not config.get('rabbitmq', {}).get('enabled', False):
335
+ return {'status': 'DISABLED', 'enabled': False, 'reason': 'RabbitMQ not configured'}
336
+ from spring.messaging.rabbitmq import rabbitmq_client
337
+ channel = rabbitmq_client._channel
338
+ if channel:
339
+ # 尝试声明一个临时队列
340
+ result = channel.queue_declare(queue='', exclusive=True)
341
+ channel.queue_delete(queue=result.method.queue)
342
+ return {
343
+ 'status': 'UP',
344
+ 'enabled': True,
345
+ 'host': rabbitmq_client.host,
346
+ 'port': rabbitmq_client.port
347
+ }
348
+ return {'status': 'DOWN', 'enabled': True, 'reason': 'RabbitMQ channel is unavailable'}
349
+ except ImportError:
350
+ return {
351
+ 'status': 'DISABLED',
352
+ 'enabled': False,
353
+ 'reason': 'RabbitMQ not available (pika not installed)'
354
+ }
355
+ except Exception as e:
356
+ return {
357
+ 'status': 'DOWN',
358
+ 'enabled': True,
359
+ 'reason': str(e)
360
+ }
361
+
362
+
363
+ def _check_seata() -> dict:
364
+ """检查Seata健康状态"""
365
+ from spring.cloud.seata import seata_manager
366
+ try:
367
+ config = _application_context.get_config() if _application_context is not None else {}
368
+ seata_config = config.get('seata', {}) or {}
369
+ if not seata_config.get('enabled', False):
370
+ return {'status': 'DISABLED', 'enabled': False, 'reason': 'Seata not configured'}
371
+ mode = str(seata_config.get('mode', 'local')).lower()
372
+ if mode == 'http':
373
+ return {
374
+ 'status': 'DOWN', 'enabled': True,
375
+ 'reason': 'Experimental HTTP compensation mode is not production-ready',
376
+ }
377
+ if mode == 'local':
378
+ return {
379
+ 'status': 'DOWN', 'enabled': True,
380
+ 'reason': 'Local mode does not provide distributed transaction guarantees',
381
+ }
382
+ if seata_manager._seata_client_initialized:
383
+ return {
384
+ 'status': 'UP',
385
+ 'enabled': True,
386
+ 'server_addr': seata_manager.server_addr,
387
+ 'application_id': seata_manager.application_id,
388
+ 'transaction_group': seata_manager.transaction_group
389
+ }
390
+ return {
391
+ 'status': 'DOWN', 'enabled': True,
392
+ 'reason': 'Distributed Seata client is not initialized',
393
+ }
394
+ except Exception as e:
395
+ return {
396
+ 'status': 'DOWN',
397
+ 'enabled': True,
398
+ 'reason': str(e)
399
+ }
@@ -0,0 +1,91 @@
1
+ from typing import Callable, Dict, Any, List, Optional
2
+ from abc import ABC
3
+ import fnmatch
4
+ import inspect
5
+ from fastapi import Request, Response
6
+
7
+
8
+ class HandlerInterceptor(ABC):
9
+ def pre_handle(self, request: Request, handler: Callable) -> bool:
10
+ return True
11
+
12
+ def post_handle(self, request: Request, response: Response, handler: Callable) -> None:
13
+ pass
14
+
15
+ def after_completion(self, request: Request, response: Response, handler: Callable, exception: Optional[Exception] = None) -> None:
16
+ pass
17
+
18
+
19
+ class InterceptorRegistry:
20
+ def __init__(self):
21
+ self._interceptors: List[HandlerInterceptor] = []
22
+ self._exclude_paths: List[str] = []
23
+ self._include_paths: List[str] = []
24
+
25
+ def add_interceptor(self, interceptor: HandlerInterceptor) -> 'InterceptorRegistry':
26
+ self._interceptors.append(interceptor)
27
+ return self
28
+
29
+ def exclude_path_patterns(self, *patterns: str) -> 'InterceptorRegistry':
30
+ self._exclude_paths.extend(patterns)
31
+ return self
32
+
33
+ def include_path_patterns(self, *patterns: str) -> 'InterceptorRegistry':
34
+ self._include_paths.extend(patterns)
35
+ return self
36
+
37
+ def get_interceptors(self) -> List[HandlerInterceptor]:
38
+ return list(self._interceptors)
39
+
40
+ def should_intercept(self, path: str) -> bool:
41
+ if self._include_paths:
42
+ if not any(self._matches(path, p) for p in self._include_paths):
43
+ return False
44
+
45
+ if self._exclude_paths:
46
+ if any(self._matches(path, p) for p in self._exclude_paths):
47
+ return False
48
+
49
+ return True
50
+
51
+ @staticmethod
52
+ def _matches(path: str, pattern: str) -> bool:
53
+ """Match Spring-style ``/**`` patterns and ordinary glob patterns."""
54
+ if pattern in {'/**', '**', '*'}:
55
+ return True
56
+ if pattern.endswith('/**'):
57
+ prefix = pattern[:-3].rstrip('/')
58
+ return path == prefix or path.startswith(prefix + '/')
59
+ return fnmatch.fnmatchcase(path, pattern) or path.startswith(pattern.rstrip('/') + '/')
60
+
61
+
62
+ class InterceptorManager:
63
+ def __init__(self, registry: InterceptorRegistry):
64
+ self.registry = registry
65
+
66
+ async def apply_pre_handle(self, request: Request, handler: Callable) -> bool:
67
+ for interceptor in self.registry.get_interceptors():
68
+ if not self.registry.should_intercept(request.url.path):
69
+ continue
70
+ result = interceptor.pre_handle(request, handler)
71
+ if inspect.isawaitable(result):
72
+ result = await result
73
+ if not result:
74
+ return False
75
+ return True
76
+
77
+ async def apply_post_handle(self, request: Request, response: Response, handler: Callable) -> None:
78
+ for interceptor in self.registry.get_interceptors():
79
+ if not self.registry.should_intercept(request.url.path):
80
+ continue
81
+ result = interceptor.post_handle(request, response, handler)
82
+ if inspect.isawaitable(result):
83
+ await result
84
+
85
+ async def apply_after_completion(self, request: Request, response: Response, handler: Callable, exception: Optional[Exception] = None) -> None:
86
+ for interceptor in self.registry.get_interceptors():
87
+ if not self.registry.should_intercept(request.url.path):
88
+ continue
89
+ result = interceptor.after_completion(request, response, handler, exception)
90
+ if inspect.isawaitable(result):
91
+ await result
spring/web/result.py ADDED
@@ -0,0 +1,44 @@
1
+ from typing import Generic, TypeVar, Optional, Any
2
+ from pydantic import BaseModel
3
+
4
+ T = TypeVar('T')
5
+
6
+
7
+ class Result(BaseModel, Generic[T]):
8
+ code: int
9
+ message: str
10
+ data: Optional[T] = None
11
+
12
+ @classmethod
13
+ def success(cls, data: Optional[T] = None, message: str = "success") -> "Result[T]":
14
+ return cls(code=200, message=message, data=data)
15
+
16
+ @classmethod
17
+ def error(cls, code: int = 500, message: str = "error") -> "Result[T]":
18
+ return cls(code=code, message=message, data=None)
19
+
20
+ @classmethod
21
+ def bad_request(cls, message: str = "Bad Request") -> "Result[T]":
22
+ return cls(code=400, message=message, data=None)
23
+
24
+ @classmethod
25
+ def unauthorized(cls, message: str = "Unauthorized") -> "Result[T]":
26
+ return cls(code=401, message=message, data=None)
27
+
28
+ @classmethod
29
+ def forbidden(cls, message: str = "Forbidden") -> "Result[T]":
30
+ return cls(code=403, message=message, data=None)
31
+
32
+ @classmethod
33
+ def not_found(cls, message: str = "Not Found") -> "Result[T]":
34
+ return cls(code=404, message=message, data=None)
35
+
36
+ @classmethod
37
+ def internal_error(cls, message: str = "Internal Server Error") -> "Result[T]":
38
+ return cls(code=500, message=message, data=None)
39
+
40
+ def is_success(self) -> bool:
41
+ return self.code == 200
42
+
43
+ def is_error(self) -> bool:
44
+ return self.code != 200