springbootAI 1.8.4__tar.gz → 1.8.6__tar.gz

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 (223) hide show
  1. {springbootai-1.8.4/springbootAI.egg-info → springbootai-1.8.6}/PKG-INFO +1 -1
  2. {springbootai-1.8.4 → springbootai-1.8.6}/pyproject.toml +1 -1
  3. {springbootai-1.8.4 → springbootai-1.8.6}/spring/__init__.py +1 -1
  4. {springbootai-1.8.4 → springbootai-1.8.6}/spring/cloud/discovery.py +48 -18
  5. {springbootai-1.8.4 → springbootai-1.8.6}/spring/config/config_loader.py +284 -57
  6. {springbootai-1.8.4 → springbootai-1.8.6}/spring/context/application_context.py +8 -0
  7. springbootai-1.8.6/spring/logging/loguru_logger.py +419 -0
  8. {springbootai-1.8.4 → springbootai-1.8.6}/spring/main.py +145 -69
  9. {springbootai-1.8.4 → springbootai-1.8.6}/spring/messaging/rabbitmq.py +50 -14
  10. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/database.py +34 -6
  11. {springbootai-1.8.4 → springbootai-1.8.6}/spring/utils/banner.py +1 -1
  12. {springbootai-1.8.4 → springbootai-1.8.6}/spring/utils/redis_client.py +31 -9
  13. {springbootai-1.8.4 → springbootai-1.8.6/springbootAI.egg-info}/PKG-INFO +1 -1
  14. {springbootai-1.8.4 → springbootai-1.8.6}/springbootAI.egg-info/SOURCES.txt +3 -0
  15. springbootai-1.8.6/tests/test_component_init_error.py +171 -0
  16. springbootai-1.8.6/tests/test_config_fixes_185.py +547 -0
  17. springbootai-1.8.6/tests/test_logging_config.py +152 -0
  18. springbootai-1.8.4/spring/logging/loguru_logger.py +0 -233
  19. {springbootai-1.8.4 → springbootai-1.8.6}/LICENSE +0 -0
  20. {springbootai-1.8.4 → springbootai-1.8.6}/MANIFEST.in +0 -0
  21. {springbootai-1.8.4 → springbootai-1.8.6}/README.md +0 -0
  22. {springbootai-1.8.4 → springbootai-1.8.6}/requirements.txt +0 -0
  23. {springbootai-1.8.4 → springbootai-1.8.6}/setup.cfg +0 -0
  24. {springbootai-1.8.4 → springbootai-1.8.6}/setup.py +0 -0
  25. {springbootai-1.8.4 → springbootai-1.8.6}/spring/ai/__init__.py +0 -0
  26. {springbootai-1.8.4 → springbootai-1.8.6}/spring/ai/advisors.py +0 -0
  27. {springbootai-1.8.4 → springbootai-1.8.6}/spring/ai/annotations.py +0 -0
  28. {springbootai-1.8.4 → springbootai-1.8.6}/spring/ai/autoconfig.py +0 -0
  29. {springbootai-1.8.4 → springbootai-1.8.6}/spring/ai/core.py +0 -0
  30. {springbootai-1.8.4 → springbootai-1.8.6}/spring/ai/etl.py +0 -0
  31. {springbootai-1.8.4 → springbootai-1.8.6}/spring/ai/memory.py +0 -0
  32. {springbootai-1.8.4 → springbootai-1.8.6}/spring/ai/observability.py +0 -0
  33. {springbootai-1.8.4 → springbootai-1.8.6}/spring/ai/providers.py +0 -0
  34. {springbootai-1.8.4 → springbootai-1.8.6}/spring/ai/resilience.py +0 -0
  35. {springbootai-1.8.4 → springbootai-1.8.6}/spring/ai/tools.py +0 -0
  36. {springbootai-1.8.4 → springbootai-1.8.6}/spring/ai/vectorstore.py +0 -0
  37. {springbootai-1.8.4 → springbootai-1.8.6}/spring/annotations/__init__.py +0 -0
  38. {springbootai-1.8.4 → springbootai-1.8.6}/spring/annotations/cache.py +0 -0
  39. {springbootai-1.8.4 → springbootai-1.8.6}/spring/annotations/cloud.py +0 -0
  40. {springbootai-1.8.4 → springbootai-1.8.6}/spring/annotations/conditional.py +0 -0
  41. {springbootai-1.8.4 → springbootai-1.8.6}/spring/annotations/core.py +0 -0
  42. {springbootai-1.8.4 → springbootai-1.8.6}/spring/annotations/messaging.py +0 -0
  43. {springbootai-1.8.4 → springbootai-1.8.6}/spring/aop/__init__.py +0 -0
  44. {springbootai-1.8.4 → springbootai-1.8.6}/spring/aop/cloud_aop.py +0 -0
  45. {springbootai-1.8.4 → springbootai-1.8.6}/spring/aop/comprehensive_aop.py +0 -0
  46. {springbootai-1.8.4 → springbootai-1.8.6}/spring/aop/method_interceptor.py +0 -0
  47. {springbootai-1.8.4 → springbootai-1.8.6}/spring/aop/proxy_factory.py +0 -0
  48. {springbootai-1.8.4 → springbootai-1.8.6}/spring/cloud/__init__.py +0 -0
  49. {springbootai-1.8.4 → springbootai-1.8.6}/spring/cloud/feign.py +0 -0
  50. {springbootai-1.8.4 → springbootai-1.8.6}/spring/cloud/gateway.py +0 -0
  51. {springbootai-1.8.4 → springbootai-1.8.6}/spring/cloud/load_balancer.py +0 -0
  52. {springbootai-1.8.4 → springbootai-1.8.6}/spring/cloud/seata.py +0 -0
  53. {springbootai-1.8.4 → springbootai-1.8.6}/spring/cloud/sentinel.py +0 -0
  54. {springbootai-1.8.4 → springbootai-1.8.6}/spring/cloud/tracer.py +0 -0
  55. {springbootai-1.8.4 → springbootai-1.8.6}/spring/cloud/transaction_store.py +0 -0
  56. {springbootai-1.8.4 → springbootai-1.8.6}/spring/config/__init__.py +0 -0
  57. {springbootai-1.8.4 → springbootai-1.8.6}/spring/config/binding.py +0 -0
  58. {springbootai-1.8.4 → springbootai-1.8.6}/spring/context/__init__.py +0 -0
  59. {springbootai-1.8.4 → springbootai-1.8.6}/spring/context/bean_definition.py +0 -0
  60. {springbootai-1.8.4 → springbootai-1.8.6}/spring/context/bean_factory.py +0 -0
  61. {springbootai-1.8.4 → springbootai-1.8.6}/spring/context/registry.py +0 -0
  62. {springbootai-1.8.4 → springbootai-1.8.6}/spring/context/scanner.py +0 -0
  63. {springbootai-1.8.4 → springbootai-1.8.6}/spring/core/__init__.py +0 -0
  64. {springbootai-1.8.4 → springbootai-1.8.6}/spring/core/graceful_shutdown.py +0 -0
  65. {springbootai-1.8.4 → springbootai-1.8.6}/spring/core/typing_utils.py +0 -0
  66. {springbootai-1.8.4 → springbootai-1.8.6}/spring/csv/__init__.py +0 -0
  67. {springbootai-1.8.4 → springbootai-1.8.6}/spring/csv/annotations.py +0 -0
  68. {springbootai-1.8.4 → springbootai-1.8.6}/spring/csv/converters.py +0 -0
  69. {springbootai-1.8.4 → springbootai-1.8.6}/spring/csv/easy_csv.py +0 -0
  70. {springbootai-1.8.4 → springbootai-1.8.6}/spring/csv/exceptions.py +0 -0
  71. {springbootai-1.8.4 → springbootai-1.8.6}/spring/csv/reader.py +0 -0
  72. {springbootai-1.8.4 → springbootai-1.8.6}/spring/csv/writer.py +0 -0
  73. {springbootai-1.8.4 → springbootai-1.8.6}/spring/data/__init__.py +0 -0
  74. {springbootai-1.8.4 → springbootai-1.8.6}/spring/data/page.py +0 -0
  75. {springbootai-1.8.4 → springbootai-1.8.6}/spring/data/repository.py +0 -0
  76. {springbootai-1.8.4 → springbootai-1.8.6}/spring/data/specification.py +0 -0
  77. {springbootai-1.8.4 → springbootai-1.8.6}/spring/datasource/__init__.py +0 -0
  78. {springbootai-1.8.4 → springbootai-1.8.6}/spring/datasource/annotations.py +0 -0
  79. {springbootai-1.8.4 → springbootai-1.8.6}/spring/datasource/context.py +0 -0
  80. {springbootai-1.8.4 → springbootai-1.8.6}/spring/datasource/dynamic.py +0 -0
  81. {springbootai-1.8.4 → springbootai-1.8.6}/spring/event/__init__.py +0 -0
  82. {springbootai-1.8.4 → springbootai-1.8.6}/spring/event/publisher.py +0 -0
  83. {springbootai-1.8.4 → springbootai-1.8.6}/spring/excel/__init__.py +0 -0
  84. {springbootai-1.8.4 → springbootai-1.8.6}/spring/excel/annotations.py +0 -0
  85. {springbootai-1.8.4 → springbootai-1.8.6}/spring/excel/converters.py +0 -0
  86. {springbootai-1.8.4 → springbootai-1.8.6}/spring/excel/easy_excel.py +0 -0
  87. {springbootai-1.8.4 → springbootai-1.8.6}/spring/excel/exceptions.py +0 -0
  88. {springbootai-1.8.4 → springbootai-1.8.6}/spring/excel/reader.py +0 -0
  89. {springbootai-1.8.4 → springbootai-1.8.6}/spring/excel/style.py +0 -0
  90. {springbootai-1.8.4 → springbootai-1.8.6}/spring/excel/writer.py +0 -0
  91. {springbootai-1.8.4 → springbootai-1.8.6}/spring/i18n/__init__.py +0 -0
  92. {springbootai-1.8.4 → springbootai-1.8.6}/spring/i18n/accessor.py +0 -0
  93. {springbootai-1.8.4 → springbootai-1.8.6}/spring/i18n/auto_config.py +0 -0
  94. {springbootai-1.8.4 → springbootai-1.8.6}/spring/i18n/holder.py +0 -0
  95. {springbootai-1.8.4 → springbootai-1.8.6}/spring/i18n/locale.py +0 -0
  96. {springbootai-1.8.4 → springbootai-1.8.6}/spring/i18n/locale_resolver.py +0 -0
  97. {springbootai-1.8.4 → springbootai-1.8.6}/spring/i18n/message_source.py +0 -0
  98. {springbootai-1.8.4 → springbootai-1.8.6}/spring/i18n/middleware.py +0 -0
  99. {springbootai-1.8.4 → springbootai-1.8.6}/spring/i18n/properties.py +0 -0
  100. {springbootai-1.8.4 → springbootai-1.8.6}/spring/i18n/sources.py +0 -0
  101. {springbootai-1.8.4 → springbootai-1.8.6}/spring/logging/__init__.py +0 -0
  102. {springbootai-1.8.4 → springbootai-1.8.6}/spring/messaging/__init__.py +0 -0
  103. {springbootai-1.8.4 → springbootai-1.8.6}/spring/monitoring/__init__.py +0 -0
  104. {springbootai-1.8.4 → springbootai-1.8.6}/spring/monitoring/prometheus.py +0 -0
  105. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/__init__.py +0 -0
  106. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/ddl_auto.py +0 -0
  107. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/migration.py +0 -0
  108. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/mybatis_integration.py +0 -0
  109. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/__init__.py +0 -0
  110. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/annotations/__init__.py +0 -0
  111. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/annotations/annotations.py +0 -0
  112. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/cache/__init__.py +0 -0
  113. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/cache/cache.py +0 -0
  114. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/cache/redis_cache.py +0 -0
  115. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/circuit_breaker/__init__.py +0 -0
  116. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/circuit_breaker/circuit_breaker.py +0 -0
  117. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/configuration.py +0 -0
  118. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/core/__init__.py +0 -0
  119. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/core/sql_session.py +0 -0
  120. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/core/sql_session_factory.py +0 -0
  121. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/dialect/__init__.py +0 -0
  122. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/dialect/dialect.py +0 -0
  123. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/dynamic_sql/__init__.py +0 -0
  124. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/dynamic_sql/dynamic_sql.py +0 -0
  125. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/interceptor/__init__.py +0 -0
  126. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/interceptor/interceptor.py +0 -0
  127. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/mapper/__init__.py +0 -0
  128. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/mapper/mapper.py +0 -0
  129. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/metrics/__init__.py +0 -0
  130. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/metrics/metrics.py +0 -0
  131. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/pool/__init__.py +0 -0
  132. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/pool/connection_pool.py +0 -0
  133. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/security/__init__.py +0 -0
  134. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/security/access_control.py +0 -0
  135. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/security/password_encoder.py +0 -0
  136. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/security/sensitive_data_masker.py +0 -0
  137. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/security/sql_injection_detector.py +0 -0
  138. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/transaction/__init__.py +0 -0
  139. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/transaction/transaction.py +0 -0
  140. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/type_handler/__init__.py +0 -0
  141. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/type_handler/type_handler.py +0 -0
  142. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/version.py +0 -0
  143. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/xml_parser/__init__.py +0 -0
  144. {springbootai-1.8.4 → springbootai-1.8.6}/spring/orm/pymybatis/xml_parser/xml_parser.py +0 -0
  145. {springbootai-1.8.4 → springbootai-1.8.6}/spring/retry/__init__.py +0 -0
  146. {springbootai-1.8.4 → springbootai-1.8.6}/spring/retry/retry_annotations.py +0 -0
  147. {springbootai-1.8.4 → springbootai-1.8.6}/spring/retry/retry_decorator.py +0 -0
  148. {springbootai-1.8.4 → springbootai-1.8.6}/spring/scheduling/__init__.py +0 -0
  149. {springbootai-1.8.4 → springbootai-1.8.6}/spring/scheduling/scheduler.py +0 -0
  150. {springbootai-1.8.4 → springbootai-1.8.6}/spring/security/__init__.py +0 -0
  151. {springbootai-1.8.4 → springbootai-1.8.6}/spring/security/jwt_utils.py +0 -0
  152. {springbootai-1.8.4 → springbootai-1.8.6}/spring/security/replay_protection.py +0 -0
  153. {springbootai-1.8.4 → springbootai-1.8.6}/spring/security/secret_manager.py +0 -0
  154. {springbootai-1.8.4 → springbootai-1.8.6}/spring/security/security_aop.py +0 -0
  155. {springbootai-1.8.4 → springbootai-1.8.6}/spring/security/security_context.py +0 -0
  156. {springbootai-1.8.4 → springbootai-1.8.6}/spring/test/__init__.py +0 -0
  157. {springbootai-1.8.4 → springbootai-1.8.6}/spring/test/slicing.py +0 -0
  158. {springbootai-1.8.4 → springbootai-1.8.6}/spring/tracing/__init__.py +0 -0
  159. {springbootai-1.8.4 → springbootai-1.8.6}/spring/tracing/skywalking.py +0 -0
  160. {springbootai-1.8.4 → springbootai-1.8.6}/spring/tx/__init__.py +0 -0
  161. {springbootai-1.8.4 → springbootai-1.8.6}/spring/tx/events.py +0 -0
  162. {springbootai-1.8.4 → springbootai-1.8.6}/spring/tx/synchronization.py +0 -0
  163. {springbootai-1.8.4 → springbootai-1.8.6}/spring/utils/__init__.py +0 -0
  164. {springbootai-1.8.4 → springbootai-1.8.6}/spring/utils/bean_utils.py +0 -0
  165. {springbootai-1.8.4 → springbootai-1.8.6}/spring/utils/logger.py +0 -0
  166. {springbootai-1.8.4 → springbootai-1.8.6}/spring/validation/__init__.py +0 -0
  167. {springbootai-1.8.4 → springbootai-1.8.6}/spring/validation/aop.py +0 -0
  168. {springbootai-1.8.4 → springbootai-1.8.6}/spring/validation/constraints.py +0 -0
  169. {springbootai-1.8.4 → springbootai-1.8.6}/spring/validation/exceptions.py +0 -0
  170. {springbootai-1.8.4 → springbootai-1.8.6}/spring/validation/validator.py +0 -0
  171. {springbootai-1.8.4 → springbootai-1.8.6}/spring/web/__init__.py +0 -0
  172. {springbootai-1.8.4 → springbootai-1.8.6}/spring/web/actuator.py +0 -0
  173. {springbootai-1.8.4 → springbootai-1.8.6}/spring/web/exception_handler.py +0 -0
  174. {springbootai-1.8.4 → springbootai-1.8.6}/spring/web/health.py +0 -0
  175. {springbootai-1.8.4 → springbootai-1.8.6}/spring/web/interceptor.py +0 -0
  176. {springbootai-1.8.4 → springbootai-1.8.6}/spring/web/result.py +0 -0
  177. {springbootai-1.8.4 → springbootai-1.8.6}/spring/web/swagger.py +0 -0
  178. {springbootai-1.8.4 → springbootai-1.8.6}/spring/web/web_context.py +0 -0
  179. {springbootai-1.8.4 → springbootai-1.8.6}/spring/websocket/__init__.py +0 -0
  180. {springbootai-1.8.4 → springbootai-1.8.6}/spring/websocket/annotations.py +0 -0
  181. {springbootai-1.8.4 → springbootai-1.8.6}/spring/websocket/broker.py +0 -0
  182. {springbootai-1.8.4 → springbootai-1.8.6}/spring/websocket/exceptions.py +0 -0
  183. {springbootai-1.8.4 → springbootai-1.8.6}/spring/websocket/handler.py +0 -0
  184. {springbootai-1.8.4 → springbootai-1.8.6}/spring/websocket/router.py +0 -0
  185. {springbootai-1.8.4 → springbootai-1.8.6}/spring/websocket/session.py +0 -0
  186. {springbootai-1.8.4 → springbootai-1.8.6}/springbootAI.egg-info/dependency_links.txt +0 -0
  187. {springbootai-1.8.4 → springbootai-1.8.6}/springbootAI.egg-info/entry_points.txt +0 -0
  188. {springbootai-1.8.4 → springbootai-1.8.6}/springbootAI.egg-info/requires.txt +0 -0
  189. {springbootai-1.8.4 → springbootai-1.8.6}/springbootAI.egg-info/top_level.txt +0 -0
  190. {springbootai-1.8.4 → springbootai-1.8.6}/sql/seata_undo_log.sql +0 -0
  191. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_actuator.py +0 -0
  192. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_ai_module.py +0 -0
  193. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_annotation_combinations.py +0 -0
  194. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_annotations_contract.py +0 -0
  195. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_aop_annotations_full.py +0 -0
  196. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_bean_utils.py +0 -0
  197. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_cache_annotations.py +0 -0
  198. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_cloud_embedded_full.py +0 -0
  199. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_conditional_annotations.py +0 -0
  200. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_config_binding.py +0 -0
  201. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_connection_resilience.py +0 -0
  202. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_core_annotations_full.py +0 -0
  203. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_csv_module.py +0 -0
  204. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_data_repository.py +0 -0
  205. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_datasource_routing.py +0 -0
  206. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_ddl_auto.py +0 -0
  207. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_di_config_event_full.py +0 -0
  208. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_excel_module.py +0 -0
  209. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_i18n_module.py +0 -0
  210. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_jpa_version_transient.py +0 -0
  211. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_new_features.py +0 -0
  212. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_orm_pymybatis_full.py +0 -0
  213. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_production_readiness.py +0 -0
  214. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_pymybatis_contract.py +0 -0
  215. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_seata_durable_store.py +0 -0
  216. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_security.py +0 -0
  217. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_security_full.py +0 -0
  218. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_swagger_module.py +0 -0
  219. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_test_slicing.py +0 -0
  220. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_transactional_events.py +0 -0
  221. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_validation_module.py +0 -0
  222. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_web_annotations_full.py +0 -0
  223. {springbootai-1.8.4 → springbootai-1.8.6}/tests/test_websocket_module.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: springbootAI
3
- Version: 1.8.4
3
+ Version: 1.8.6
4
4
  Summary: Python版Spring Boot框架,集成PyMyBatis ORM
5
5
  Author-email: YuConggen <1516933915@qq.com>
6
6
  License-Expression: MIT
@@ -12,7 +12,7 @@ build-backend = "setuptools.build_meta"
12
12
  [project]
13
13
  # 发行名 springbootAI(PyPI 包名,pip install springbootAI);导入包名仍为 spring
14
14
  name = "springbootAI"
15
- version = "1.8.4"
15
+ version = "1.8.6"
16
16
  description = "Python版Spring Boot框架,集成PyMyBatis ORM"
17
17
  readme = "README.md"
18
18
  license = "MIT"
@@ -5,7 +5,7 @@ from .config import *
5
5
  from .utils import *
6
6
  from .main import create_app, run, SpringApplication, run_cli
7
7
 
8
- __version__ = "1.8.4"
8
+ __version__ = "1.8.6"
9
9
  __author__ = "yuconggen"
10
10
  __license__ = "MIT"
11
11
 
@@ -32,20 +32,9 @@ class NacosDiscoveryClient:
32
32
 
33
33
  def __init__(self, server_addr: str = "localhost:8848", namespace: str = "", group: str = "DEFAULT_GROUP", username: str = "", password: str = ""):
34
34
  if hasattr(self, '_initialized'):
35
- if (
36
- self.server_addr != server_addr
37
- or self.namespace != namespace
38
- or self.group != group
39
- or self.username != username
40
- or self.password != password
41
- ):
42
- self.server_addr = server_addr
43
- self.namespace = namespace
44
- self.group = group
45
- self.username = username
46
- self.password = password
47
- self._client = None
48
- self._ready = False
35
+ # 已初始化:委托给 configure() 做原地更新,保持单一更新路径
36
+ self.configure(server_addr=server_addr, namespace=namespace,
37
+ group=group, username=username, password=password)
49
38
  return
50
39
  self.server_addr = server_addr
51
40
  self.namespace = namespace
@@ -58,6 +47,44 @@ class NacosDiscoveryClient:
58
47
  self._ip: str = "127.0.0.1"
59
48
  self._port: int = 8080
60
49
  self._initialized = True
50
+
51
+ def configure(self, server_addr: Optional[str] = None, namespace: Optional[str] = None,
52
+ group: Optional[str] = None, username: Optional[str] = None,
53
+ password: Optional[str] = None) -> None:
54
+ """重新配置单例的 Nacos 连接参数(读取配置后调用)。
55
+
56
+ ``NacosDiscoveryClient`` 为单例,``__init__`` 的 ``_initialized`` 守卫会
57
+ 阻止后续 ``__init__`` 更新参数。``init_discovery`` 读取 ``application.yml``
58
+ 的 ``discovery.*`` 后,必须通过本方法重新配置,否则连接参数停留在默认值。
59
+
60
+ 参数变化时重置已建立的客户端连接,强制下次 ``connect()`` 重建。
61
+
62
+ Args:
63
+ server_addr: Nacos 服务地址,None 表示保留原值
64
+ namespace: 命名空间,None 表示保留原值
65
+ group: 分组,None 表示保留原值
66
+ username: 用户名,None 表示保留原值
67
+ password: 密码,None 表示保留原值
68
+ """
69
+ changed = False
70
+ if server_addr is not None and self.server_addr != server_addr:
71
+ self.server_addr = server_addr
72
+ changed = True
73
+ if namespace is not None and self.namespace != namespace:
74
+ self.namespace = namespace
75
+ changed = True
76
+ if group is not None and self.group != group:
77
+ self.group = group
78
+ changed = True
79
+ if username is not None and self.username != username:
80
+ self.username = username
81
+ changed = True
82
+ if password is not None and self.password != password:
83
+ self.password = password
84
+ changed = True
85
+ if changed:
86
+ self._client = None
87
+ self._ready = False
61
88
 
62
89
  def connect(self) -> None:
63
90
  """连接Nacos"""
@@ -347,13 +374,16 @@ nacos_client = NacosDiscoveryClient()
347
374
  def init_discovery(config: dict) -> None:
348
375
  """
349
376
  初始化服务注册发现
350
-
377
+
378
+ 通过 ``configure`` 重新配置单例 ``NacosDiscoveryClient``,使 ``discovery.*``
379
+ 配置生效。直接 ``NacosDiscoveryClient(...)`` 因单例 ``_initialized`` 守卫不会
380
+ 更新参数。
381
+
351
382
  Args:
352
383
  config: 配置字典,包含server_addr, namespace, group等
353
384
  """
354
- # Reconfigure the singleton in place so modules that imported the client
355
- # keep observing the active connection rather than a stale instance.
356
- nacos_client.__init__(
385
+ # 单例原地更新配置,避免 _initialized 守卫导致配置被忽略
386
+ nacos_client.configure(
357
387
  server_addr=config.get('server_addr', 'localhost:8848'),
358
388
  namespace=config.get('namespace', ''),
359
389
  group=config.get('group', 'DEFAULT_GROUP'),
@@ -17,15 +17,46 @@ class ConfigurationError(ValueError):
17
17
  """应用配置缺失、格式错误或不满足生产安全要求。"""
18
18
 
19
19
 
20
+ # 匹配非字母数字字符(kebab/camel/snake 分隔符)用于松散匹配规范化
21
+ _NON_ALNUM = re.compile(r'[^0-9a-zA-Z]+')
22
+
23
+
24
+ def _normalize_name(name: str) -> str:
25
+ """规范化命名:去除分隔符并小写,用于 ``get()`` 的松散匹配。
26
+
27
+ ``log_dir`` / ``log-dir`` / ``LOG_DIR`` / ``log.dir`` → ``logdir``。
28
+ 与 ``spring.config.binding._normalize`` 保持一致,避免循环导入在此独立定义。
29
+ """
30
+ if not isinstance(name, str):
31
+ return str(name).lower()
32
+ return _NON_ALNUM.sub('', name).lower()
33
+
34
+
35
+ def _to_bool(value: Any, default: bool = False) -> bool:
36
+ """统一布尔转换:接受 ``true/1/yes/on``(大小写不敏感),其余为 False。
37
+
38
+ 与 ``spring.config.binding._coerce`` 的布尔规则保持一致,避免
39
+ ``REDIS_ENABLED=1`` 在 ``@ConfigurationProperties`` 绑定为 True 而在
40
+ ``_override_with_env`` 中被识别为 False 的行为不一致。
41
+ """
42
+ if isinstance(value, bool):
43
+ return value
44
+ if value is None:
45
+ return default
46
+ return str(value).strip().lower() in ('true', '1', 'yes', 'on')
47
+
48
+
20
49
  class ConfigLoader:
21
50
  """配置加载器"""
22
51
 
23
52
  # ApplicationContext updates this after startup so later ``ConfigLoader()``
24
53
  # calls resolve the same application.yml instead of depending on CWD.
25
54
  _default_base_path: Optional[str] = None
26
-
27
- # 环境变量替换模式:${ENV_VAR} 或 ${ENV_VAR:default}
55
+
56
+ # 环境变量替换模式:${ENV_VAR} 或 ${ENV_VAR:default}(单层,用于 fullmatch 判定)
28
57
  _ENV_VAR_PATTERN = re.compile(r'\$\{([^}]+)\}')
58
+ # 最内层占位符:内容不含 ${,用于迭代解析嵌套占位符 ${A:${B:default}}
59
+ _INNER_ENV_VAR_PATTERN = re.compile(r'\$\{([^${}]*)\}')
29
60
  _VALUE_EXPRESSION_PATTERN = re.compile(r'^\$\{([^}:]+)(?::(.*))?\}$')
30
61
  _MISSING = object()
31
62
 
@@ -42,47 +73,80 @@ class ConfigLoader:
42
73
  self._config: Dict[str, Any] = {}
43
74
  self._load_config()
44
75
 
76
+ @staticmethod
77
+ def _is_single_placeholder(value: str) -> bool:
78
+ """判断字符串是否为单个完整的占位符(支持嵌套 ``${A:${B:default}}``)。
79
+
80
+ 用于决定解析后是否做类型推断(``yaml.safe_load``)。
81
+ 用括号深度计数识别单个平衡占位符,避免正则 ``[^}]+`` 无法处理嵌套 ``}``。
82
+ """
83
+ value = value.strip()
84
+ if not (value.startswith('${') and value.endswith('}')):
85
+ return False
86
+ depth = 0
87
+ for i, c in enumerate(value):
88
+ if c == '{':
89
+ depth += 1
90
+ elif c == '}':
91
+ depth -= 1
92
+ if depth == 0 and i != len(value) - 1:
93
+ return False # 在结束前就已闭合 → 非单个占位符
94
+ return depth == 0
95
+
45
96
  def _resolve_env_var(self, value: str) -> Any:
46
97
  """
47
98
  解析环境变量引用
48
-
99
+
49
100
  支持格式:
50
101
  - ${ENV_VAR} - 直接读取环境变量
51
102
  - ${ENV_VAR:default} - 读取环境变量,不存在使用默认值
52
-
103
+ - ${A:${B:default}} - 嵌套占位符(迭代解析最内层)
104
+
53
105
  Args:
54
106
  value: 包含环境变量引用的字符串
55
-
107
+
56
108
  Returns:
57
- 解析后的字符串
109
+ 解析后的值(单个占位符时做类型推断)
58
110
  """
59
111
  if not isinstance(value, str):
60
112
  return value
61
-
62
- exact_placeholder = self._ENV_VAR_PATTERN.fullmatch(value)
113
+
114
+ # 判断整个值是否为单个占位符(支持嵌套),用于类型推断
115
+ exact_placeholder = self._is_single_placeholder(value)
63
116
 
64
117
  def replace_env(match):
65
118
  env_spec = match.group(1)
66
-
119
+
67
120
  # 检查是否有默认值
68
121
  if ':' in env_spec:
69
122
  env_name, default_value = env_spec.split(':', 1)
70
123
  else:
71
124
  env_name = env_spec
72
125
  default_value = None
73
-
126
+
74
127
  # 从环境变量获取值
75
128
  env_value = os.environ.get(env_name.strip())
76
-
129
+
77
130
  # 如果环境变量不存在,使用默认值
78
131
  if env_value is None:
79
132
  if default_value is None:
80
133
  raise ConfigurationError(f"必需的环境变量 {env_name.strip()} 未设置")
81
134
  return default_value
82
-
135
+
83
136
  return env_value
84
-
85
- resolved = self._ENV_VAR_PATTERN.sub(replace_env, value)
137
+
138
+ # 迭代解析最内层占位符,支持嵌套 ${A:${B:default}}
139
+ # 每轮用 _INNER_ENV_VAR_PATTERN 替换所有不含 ${ 的最内层占位符,
140
+ # 直到没有变化或达到最大迭代次数(防止无限循环)
141
+ resolved = value
142
+ prev = None
143
+ iterations = 0
144
+ while prev != resolved and iterations < 10:
145
+ prev = resolved
146
+ resolved = self._INNER_ENV_VAR_PATTERN.sub(replace_env, resolved)
147
+ iterations += 1
148
+
149
+ # 整个值是单个占位符时,做类型推断(如 "8080" → 8080, "true" → True)
86
150
  if exact_placeholder:
87
151
  parsed = yaml.safe_load(resolved)
88
152
  if not isinstance(parsed, (dict, list)):
@@ -108,9 +172,37 @@ class ConfigLoader:
108
172
  else:
109
173
  return config
110
174
 
175
+ @staticmethod
176
+ def _deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
177
+ """深度合并两个配置字典:``override`` 覆盖 ``base``,子字典递归合并。
178
+
179
+ 对齐 Spring Boot 的 profile 合并语义:profile 特定配置覆盖主配置,
180
+ 但主配置中未涉及的键保留。返回新字典,不修改入参。
181
+ """
182
+ if not isinstance(base, dict):
183
+ base = {}
184
+ if not isinstance(override, dict):
185
+ return deepcopy(base)
186
+ result = deepcopy(base)
187
+ for key, value in override.items():
188
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
189
+ result[key] = ConfigLoader._deep_merge(result[key], value)
190
+ else:
191
+ result[key] = deepcopy(value)
192
+ return result
193
+
194
+ def _resolve_profile_path(self, profile: str) -> Optional[str]:
195
+ """解析 ``application-{profile}.yml`` 路径,与主配置同目录。"""
196
+ if not profile or profile == 'default':
197
+ return None
198
+ config_dir = os.path.dirname(os.path.abspath(self.config_path))
199
+ profile_file = f"application-{profile}.yml"
200
+ candidate = os.path.join(config_dir, profile_file)
201
+ return candidate if os.path.exists(candidate) else None
202
+
111
203
  def _load_config(self):
112
204
  """加载配置"""
113
- # 1. 尝试从YAML文件加载
205
+ # 1. 尝试从YAML文件加载主配置
114
206
  if os.path.exists(self.config_path):
115
207
  try:
116
208
  with open(self.config_path, 'r', encoding='utf-8') as f:
@@ -121,14 +213,61 @@ class ConfigLoader:
121
213
  except Exception as e:
122
214
  logger.error(f"Failed to load config from {self.config_path}: {e}")
123
215
  raise ConfigurationError(f"无法加载配置文件 {self.config_path}") from e
124
-
216
+
217
+ # 1.5 加载 profile 特定配置文件并深度合并(application-{profile}.yml 覆盖主配置)
218
+ # profile 来自主配置的 spring.profiles.active 或 SPRING_PROFILES_ACTIVE 环境变量。
219
+ # 在占位符解析之前合并,使 profile 文件中的占位符也能被解析。
220
+ profile = (
221
+ os.getenv('SPRING_PROFILES_ACTIVE')
222
+ or self._config.get('spring', {}).get('profiles', {}).get('active', 'default')
223
+ )
224
+ if profile:
225
+ profile = str(profile).strip()
226
+ profile_path = self._resolve_profile_path(profile) if profile else None
227
+ if profile_path:
228
+ try:
229
+ with open(profile_path, 'r', encoding='utf-8') as f:
230
+ profile_config = yaml.safe_load(f) or {}
231
+ if isinstance(profile_config, dict):
232
+ self._config = self._deep_merge(self._config, profile_config)
233
+ logger.info(f"Loaded profile config from {profile_path} (profile={profile})")
234
+ except Exception as e:
235
+ logger.warning(f"Failed to load profile config {profile_path}: {e}")
236
+
125
237
  # 2. 解析配置中的环境变量占位符
126
238
  self._config = self._resolve_config_recursive(self._config)
127
-
239
+
128
240
  # 3. 从环境变量覆盖配置
129
241
  self._override_with_env()
242
+ # 4. 从命令行参数覆盖配置(优先级最高)
243
+ self._override_with_cli_args()
130
244
  self._validate_config()
131
245
 
246
+ @staticmethod
247
+ def _get_env_any(*names: str, default: Any = None) -> Any:
248
+ """返回第一个已设置的环境变量值;都未设置则返回 default。
249
+
250
+ 用于兼容占位符风格与显式 override 风格两套环境变量命名。
251
+ 例如 ``discovery.server_addr`` 同时支持 ``NACOS_SERVER``(占位符风格)
252
+ 和 ``DISCOVERY_SERVER_ADDR``(显式覆盖风格)。
253
+ """
254
+ for name in names:
255
+ value = os.getenv(name)
256
+ if value is not None:
257
+ return value
258
+ return default
259
+
260
+ @staticmethod
261
+ def _get_env_int(name: str, default: int) -> int:
262
+ """读取整型环境变量,转换失败抛 ``ConfigurationError`` 提示友好错误。"""
263
+ raw = os.getenv(name)
264
+ if raw is None:
265
+ return default
266
+ try:
267
+ return int(raw)
268
+ except (ValueError, TypeError):
269
+ raise ConfigurationError(f"环境变量 {name} 必须是整数,实际值: {raw!r}")
270
+
132
271
  def _override_with_env(self):
133
272
  """使用环境变量覆盖配置"""
134
273
  self._config.setdefault('spring', {})
@@ -142,7 +281,7 @@ class ConfigLoader:
142
281
  fail_fast_env = os.getenv('STARTUP_FAIL_FAST')
143
282
  configured_fail_fast = self._config['startup'].get('fail_fast')
144
283
  if fail_fast_env is not None:
145
- self._config['startup']['fail_fast'] = fail_fast_env.lower() == 'true'
284
+ self._config['startup']['fail_fast'] = _to_bool(fail_fast_env)
146
285
  elif configured_fail_fast is None:
147
286
  self._config['startup'].pop('fail_fast', None)
148
287
  else:
@@ -151,71 +290,96 @@ class ConfigLoader:
151
290
  # Redis配置
152
291
  self._config.setdefault('redis', {})
153
292
  self._config['redis']['host'] = os.getenv('REDIS_HOST', self._config['redis'].get('host', 'localhost'))
154
- self._config['redis']['port'] = int(os.getenv('REDIS_PORT', str(self._config['redis'].get('port', 6379))))
155
- self._config['redis']['db'] = int(os.getenv('REDIS_DB', str(self._config['redis'].get('db', 0))))
293
+ self._config['redis']['port'] = self._get_env_int('REDIS_PORT', self._config['redis'].get('port', 6379))
294
+ self._config['redis']['db'] = self._get_env_int('REDIS_DB', self._config['redis'].get('db', 0))
156
295
  self._config['redis']['password'] = os.getenv('REDIS_PASSWORD', self._config['redis'].get('password'))
157
- self._config['redis']['enabled'] = os.getenv('REDIS_ENABLED', str(self._config['redis'].get('enabled', False))).lower() == 'true'
158
-
296
+ self._config['redis']['enabled'] = _to_bool(
297
+ os.getenv('REDIS_ENABLED', self._config['redis'].get('enabled', False)), False)
298
+
159
299
  # JWT配置
160
300
  self._config.setdefault('jwt', {})
161
301
  self._config['jwt']['secret_key'] = os.getenv('JWT_SECRET_KEY', self._config['jwt'].get('secret_key', 'spring-python-secret-key-change-in-production'))
162
302
  self._config['jwt']['algorithm'] = os.getenv('JWT_ALGORITHM', self._config['jwt'].get('algorithm', 'HS256'))
163
-
303
+
164
304
  # 数据库配置
165
305
  self._config.setdefault('database', {})
166
306
  self._config['database']['url'] = os.getenv('DB_URL', self._config['database'].get('url', 'sqlite:///./test.db'))
167
- self._config['database']['echo'] = os.getenv('DB_ECHO', str(self._config['database'].get('echo', False))).lower() == 'true'
168
- self._config['database']['enabled'] = os.getenv('DB_ENABLED', str(self._config['database'].get('enabled', False))).lower() == 'true'
307
+ self._config['database']['echo'] = _to_bool(
308
+ os.getenv('DB_ECHO', self._config['database'].get('echo', False)), False)
309
+ # database.enabled 默认 True(对齐 application.yml 占位符 ${DB_ENABLED:true})
310
+ self._config['database']['enabled'] = _to_bool(
311
+ os.getenv('DB_ENABLED', self._config['database'].get('enabled', True)), True)
169
312
  # PyMyBatis原生数据源配置(host/port/driver等)
170
313
  self._config['database']['driver'] = os.getenv('DB_DRIVER', self._config['database'].get('driver', 'sqlite'))
171
314
  self._config['database']['host'] = os.getenv('DB_HOST', self._config['database'].get('host', 'localhost'))
172
- self._config['database']['port'] = int(os.getenv('DB_PORT', str(self._config['database'].get('port', 3306))))
315
+ self._config['database']['port'] = self._get_env_int('DB_PORT', self._config['database'].get('port', 3306))
173
316
  self._config['database']['database'] = os.getenv('DB_NAME', self._config['database'].get('database', 'test'))
174
317
  self._config['database']['username'] = os.getenv('DB_USERNAME', self._config['database'].get('username', ''))
175
318
  self._config['database']['password'] = os.getenv('DB_PASSWORD', self._config['database'].get('password', ''))
176
-
319
+
177
320
  # 服务发现配置
321
+ # 兼容占位符风格(NACOS_*)与显式覆盖风格(DISCOVERY_*)两套环境变量命名
178
322
  self._config.setdefault('discovery', {})
179
- self._config['discovery']['server_addr'] = os.getenv('DISCOVERY_SERVER_ADDR', self._config['discovery'].get('server_addr', 'localhost:8848'))
180
- self._config['discovery']['namespace'] = os.getenv('DISCOVERY_NAMESPACE', self._config['discovery'].get('namespace', ''))
181
- self._config['discovery']['group'] = os.getenv('DISCOVERY_GROUP', self._config['discovery'].get('group', 'DEFAULT_GROUP'))
323
+ self._config['discovery']['server_addr'] = self._get_env_any(
324
+ 'NACOS_SERVER', 'DISCOVERY_SERVER_ADDR',
325
+ default=self._config['discovery'].get('server_addr', 'localhost:8848'))
326
+ self._config['discovery']['namespace'] = self._get_env_any(
327
+ 'NACOS_NAMESPACE', 'DISCOVERY_NAMESPACE',
328
+ default=self._config['discovery'].get('namespace', ''))
329
+ self._config['discovery']['group'] = self._get_env_any(
330
+ 'NACOS_GROUP', 'DISCOVERY_GROUP',
331
+ default=self._config['discovery'].get('group', 'DEFAULT_GROUP'))
182
332
  self._config['discovery']['username'] = os.getenv('NACOS_USERNAME', self._config['discovery'].get('username', ''))
183
333
  self._config['discovery']['password'] = os.getenv('NACOS_PASSWORD', self._config['discovery'].get('password', ''))
184
- self._config['discovery']['enabled'] = os.getenv('DISCOVERY_ENABLED', str(self._config['discovery'].get('enabled', False))).lower() == 'true'
185
-
334
+ self._config['discovery']['enabled'] = _to_bool(
335
+ os.getenv('DISCOVERY_ENABLED', self._config['discovery'].get('enabled', False)), False)
336
+
186
337
  # Seata配置
338
+ # 兼容占位符风格(SEATA_SERVER/SEATA_APP_ID/SEATA_TX_GROUP)与显式覆盖风格
187
339
  self._config.setdefault('seata', {})
188
- self._config['seata']['server_addr'] = os.getenv('SEATA_SERVER_ADDR', self._config['seata'].get('server_addr', 'localhost:8091'))
189
- self._config['seata']['application_id'] = os.getenv('SEATA_APPLICATION_ID', self._config['seata'].get('application_id', ''))
190
- self._config['seata']['transaction_group'] = os.getenv('SEATA_TRANSACTION_GROUP', self._config['seata'].get('transaction_group', 'my_tx_group'))
191
- self._config['seata']['enabled'] = os.getenv('SEATA_ENABLED', str(self._config['seata'].get('enabled', False))).lower() == 'true'
192
-
340
+ self._config['seata']['server_addr'] = self._get_env_any(
341
+ 'SEATA_SERVER', 'SEATA_SERVER_ADDR',
342
+ default=self._config['seata'].get('server_addr', 'localhost:8091'))
343
+ self._config['seata']['application_id'] = self._get_env_any(
344
+ 'SEATA_APP_ID', 'SEATA_APPLICATION_ID',
345
+ default=self._config['seata'].get('application_id', ''))
346
+ self._config['seata']['transaction_group'] = self._get_env_any(
347
+ 'SEATA_TX_GROUP', 'SEATA_TRANSACTION_GROUP',
348
+ default=self._config['seata'].get('transaction_group', 'my_tx_group'))
349
+ self._config['seata']['enabled'] = _to_bool(
350
+ os.getenv('SEATA_ENABLED', self._config['seata'].get('enabled', False)), False)
351
+
193
352
  # RabbitMQ配置
353
+ # 兼容占位符风格(RABBITMQ_VHOST)与显式覆盖风格(RABBITMQ_VIRTUAL_HOST)
194
354
  self._config.setdefault('rabbitmq', {})
195
355
  self._config['rabbitmq']['host'] = os.getenv('RABBITMQ_HOST', self._config['rabbitmq'].get('host', 'localhost'))
196
- self._config['rabbitmq']['port'] = int(os.getenv('RABBITMQ_PORT', str(self._config['rabbitmq'].get('port', 5672))))
356
+ self._config['rabbitmq']['port'] = self._get_env_int('RABBITMQ_PORT', self._config['rabbitmq'].get('port', 5672))
197
357
  self._config['rabbitmq']['username'] = os.getenv('RABBITMQ_USERNAME', self._config['rabbitmq'].get('username', 'guest'))
198
358
  self._config['rabbitmq']['password'] = os.getenv('RABBITMQ_PASSWORD', self._config['rabbitmq'].get('password', 'guest'))
199
- self._config['rabbitmq']['virtual_host'] = os.getenv('RABBITMQ_VIRTUAL_HOST', self._config['rabbitmq'].get('virtual_host', '/'))
200
- self._config['rabbitmq']['enabled'] = os.getenv('RABBITMQ_ENABLED', str(self._config['rabbitmq'].get('enabled', False))).lower() == 'true'
201
-
359
+ self._config['rabbitmq']['virtual_host'] = self._get_env_any(
360
+ 'RABBITMQ_VHOST', 'RABBITMQ_VIRTUAL_HOST',
361
+ default=self._config['rabbitmq'].get('virtual_host', '/'))
362
+ self._config['rabbitmq']['enabled'] = _to_bool(
363
+ os.getenv('RABBITMQ_ENABLED', self._config['rabbitmq'].get('enabled', False)), False)
364
+
202
365
  # Prometheus配置
203
366
  self._config.setdefault('prometheus', {})
204
367
  self._config['prometheus']['namespace'] = os.getenv('PROMETHEUS_NAMESPACE', self._config['prometheus'].get('namespace', 'spring'))
205
368
  self._config['prometheus']['subsystem'] = os.getenv('PROMETHEUS_SUBSYSTEM', self._config['prometheus'].get('subsystem', 'python'))
206
- self._config['prometheus']['port'] = int(os.getenv('PROMETHEUS_PORT', str(self._config['prometheus'].get('port', 8000))))
207
- self._config['prometheus']['enabled'] = os.getenv('PROMETHEUS_ENABLED', str(self._config['prometheus'].get('enabled', False))).lower() == 'true'
208
-
369
+ self._config['prometheus']['port'] = self._get_env_int('PROMETHEUS_PORT', self._config['prometheus'].get('port', 8000))
370
+ self._config['prometheus']['enabled'] = _to_bool(
371
+ os.getenv('PROMETHEUS_ENABLED', self._config['prometheus'].get('enabled', False)), False)
372
+
209
373
  # 日志配置
210
374
  self._config.setdefault('logging', {})
211
375
  self._config['logging']['level'] = os.getenv('LOG_LEVEL', self._config['logging'].get('level', 'INFO'))
212
376
  self._config['logging']['log_dir'] = os.getenv('LOG_DIR', self._config['logging'].get('log_dir', 'logs'))
213
377
  self._config['logging']['retention'] = os.getenv('LOG_RETENTION', self._config['logging'].get('retention', '30 days'))
214
378
  self._config['logging']['rotation'] = os.getenv('LOG_ROTATION', self._config['logging'].get('rotation', '100 MB'))
215
-
379
+
216
380
  # 服务器配置
217
381
  self._config.setdefault('server', {})
218
- self._config['server']['port'] = int(os.getenv('SERVER_PORT', str(self._config['server'].get('port', 8080))))
382
+ self._config['server']['port'] = self._get_env_int('SERVER_PORT', self._config['server'].get('port', 8080))
219
383
  self._config['server']['host'] = os.getenv('SERVER_HOST', self._config['server'].get('host', '0.0.0.0'))
220
384
 
221
385
  self._config['server'].setdefault('cors', {})
@@ -227,9 +391,52 @@ class ConfigLoader:
227
391
  ]
228
392
  else:
229
393
  cors_config.setdefault('allow_origins', [])
230
- cors_config['allow_credentials'] = os.getenv(
231
- 'CORS_ALLOW_CREDENTIALS', str(cors_config.get('allow_credentials', False))
232
- ).lower() == 'true'
394
+ cors_config['allow_credentials'] = _to_bool(
395
+ os.getenv('CORS_ALLOW_CREDENTIALS', cors_config.get('allow_credentials', False)), False)
396
+
397
+ def _override_with_cli_args(self) -> None:
398
+ """从命令行参数覆盖配置(优先级最高,对齐 Spring Boot ``--key=value``)。
399
+
400
+ 支持两种形式:
401
+ - ``--server.port=9000``
402
+ - ``--server.port 9000``
403
+
404
+ 点分隔键递归写入 ``self._config``,值经 ``yaml.safe_load`` 做类型推断
405
+ (``9000`` → int,``true`` → bool)。在 ``_override_with_env`` 之后调用。
406
+ """
407
+ args = sys.argv[1:]
408
+ i = 0
409
+ while i < len(args):
410
+ arg = args[i]
411
+ if not arg.startswith('--'):
412
+ i += 1
413
+ continue
414
+ body = arg[2:]
415
+ if '=' in body:
416
+ key, value = body.split('=', 1)
417
+ self._set_cli_override(key, value)
418
+ i += 1
419
+ else:
420
+ # --key value 形式:仅当下一个 token 非参数时才取值
421
+ if i + 1 < len(args) and not args[i + 1].startswith('--'):
422
+ self._set_cli_override(body, args[i + 1])
423
+ i += 2
424
+ else:
425
+ i += 1
426
+
427
+ def _set_cli_override(self, dotted_key: str, raw_value: str) -> None:
428
+ """将点分隔键写入 ``self._config``,值做 yaml 类型推断。"""
429
+ keys = dotted_key.split('.')
430
+ node = self._config
431
+ for k in keys[:-1]:
432
+ child = node.get(k)
433
+ if not isinstance(child, dict):
434
+ child = {}
435
+ node[k] = child
436
+ node = child
437
+ parsed = yaml.safe_load(raw_value)
438
+ node[keys[-1]] = parsed
439
+ logger.debug(f"CLI override: {dotted_key}={parsed!r}")
233
440
 
234
441
  def _validate_config(self) -> None:
235
442
  algorithm = str(self._config.get('jwt', {}).get('algorithm', 'HS256')).upper()
@@ -336,26 +543,46 @@ class ConfigLoader:
336
543
  """
337
544
  return self._config.get('spring', {}).get('profiles', {}).get('active', 'default')
338
545
 
546
+ @staticmethod
547
+ def _lookup_key(mapping: Any, key: str):
548
+ """在字典中查找 ``key``:先精确匹配,再松散匹配(大小写/分隔符不敏感)。
549
+
550
+ 与 ``@ConfigurationProperties`` 松散绑定语义一致,使 ``get('logging.log-dir')``
551
+ 也能命中 YAML 中的 ``log_dir``。返回 ``(value, found)``。
552
+ """
553
+ if not isinstance(mapping, dict):
554
+ return None, False
555
+ if key in mapping:
556
+ return mapping[key], True
557
+ # 松散匹配:规范化(去分隔符+小写)后比较
558
+ norm = _normalize_name(key)
559
+ for k, v in mapping.items():
560
+ if _normalize_name(k) == norm:
561
+ return v, True
562
+ return None, False
563
+
339
564
  def get(self, key: str, default: Any = None) -> Any:
340
565
  """
341
566
  获取配置值
342
-
567
+
568
+ 支持点分隔路径与松散绑定(kebab-case / snake_case / 大小写不敏感)。
569
+ 先精确匹配,未命中再松散匹配,避免 ``log-dir`` 找不到 ``log_dir`` 的问题。
570
+
343
571
  Args:
344
- key: 配置键,支持点分隔(如 redis.host)
572
+ key: 配置键,支持点分隔(如 redis.host / logging.log-dir / Logging.Level)
345
573
  default: 默认值
346
-
574
+
347
575
  Returns:
348
576
  配置值
349
577
  """
350
578
  keys = key.split('.')
351
579
  value = self._config
352
-
580
+
353
581
  for k in keys:
354
- if isinstance(value, dict) and k in value:
355
- value = value[k]
356
- else:
582
+ value, found = self._lookup_key(value, k)
583
+ if not found:
357
584
  return default
358
-
585
+
359
586
  return value
360
587
 
361
588
  def load_config(self):
@@ -421,6 +421,14 @@ class ApplicationContext:
421
421
  # 处理字段上的@Value注解
422
422
  for name, field in inspect.getmembers(bean_class):
423
423
  if not name.startswith('_'):
424
+ # @Value/@NacosValue can be attached to a function as
425
+ # metadata alongside route/AOP annotations. Replacing
426
+ # that function on the instance makes the handler
427
+ # non-callable and breaks controller registration.
428
+ # Constructor/default and class-field injection are
429
+ # handled separately by BeanFactory.
430
+ if callable(field):
431
+ continue
424
432
  annotations = getattr(field, '__spring_annotations__', [])
425
433
  for annotation in annotations:
426
434
  if isinstance(annotation, Value):