python-neva 5.4.0__tar.gz → 5.4.1__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 (203) hide show
  1. {python_neva-5.4.0 → python_neva-5.4.1}/CHANGELOG.md +7 -0
  2. {python_neva-5.4.0 → python_neva-5.4.1}/PKG-INFO +1 -1
  3. {python_neva-5.4.0 → python_neva-5.4.1}/neva/arch/application.py +39 -6
  4. {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/fragments/configuration.md +14 -3
  5. {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/fragments/drivers.md +1 -1
  6. {python_neva-5.4.0 → python_neva-5.4.1}/pyproject.toml +1 -1
  7. {python_neva-5.4.0 → python_neva-5.4.1}/tests/config/test_provider_declarations.py +111 -2
  8. {python_neva-5.4.0 → python_neva-5.4.1}/uv.lock +1 -1
  9. {python_neva-5.4.0 → python_neva-5.4.1}/.envrc +0 -0
  10. {python_neva-5.4.0 → python_neva-5.4.1}/.gitignore +0 -0
  11. {python_neva-5.4.0 → python_neva-5.4.1}/.gitlab-ci.yml +0 -0
  12. {python_neva-5.4.0 → python_neva-5.4.1}/.pre-commit-config.yaml +0 -0
  13. {python_neva-5.4.0 → python_neva-5.4.1}/.python-version +0 -0
  14. {python_neva-5.4.0 → python_neva-5.4.1}/CLAUDE.md +0 -0
  15. {python_neva-5.4.0 → python_neva-5.4.1}/README.md +0 -0
  16. {python_neva-5.4.0 → python_neva-5.4.1}/neva/arch/__init__.py +0 -0
  17. {python_neva-5.4.0 → python_neva-5.4.1}/neva/arch/config.py +0 -0
  18. {python_neva-5.4.0 → python_neva-5.4.1}/neva/arch/facade.py +0 -0
  19. {python_neva-5.4.0 → python_neva-5.4.1}/neva/arch/integrations/__init__.py +0 -0
  20. {python_neva-5.4.0 → python_neva-5.4.1}/neva/arch/integrations/faststream.py +0 -0
  21. {python_neva-5.4.0 → python_neva-5.4.1}/neva/arch/markers.py +0 -0
  22. {python_neva-5.4.0 → python_neva-5.4.1}/neva/arch/py.typed +0 -0
  23. {python_neva-5.4.0 → python_neva-5.4.1}/neva/arch/scopes.py +0 -0
  24. {python_neva-5.4.0 → python_neva-5.4.1}/neva/arch/service_provider.py +0 -0
  25. {python_neva-5.4.0 → python_neva-5.4.1}/neva/config/__init__.py +0 -0
  26. {python_neva-5.4.0 → python_neva-5.4.1}/neva/config/base_providers.py +0 -0
  27. {python_neva-5.4.0 → python_neva-5.4.1}/neva/config/loader.py +0 -0
  28. {python_neva-5.4.0 → python_neva-5.4.1}/neva/config/py.typed +0 -0
  29. {python_neva-5.4.0 → python_neva-5.4.1}/neva/config/repository.py +0 -0
  30. {python_neva-5.4.0 → python_neva-5.4.1}/neva/database/__init__.py +0 -0
  31. {python_neva-5.4.0 → python_neva-5.4.1}/neva/database/config.py +0 -0
  32. {python_neva-5.4.0 → python_neva-5.4.1}/neva/database/connection.py +0 -0
  33. {python_neva-5.4.0 → python_neva-5.4.1}/neva/database/manager.py +0 -0
  34. {python_neva-5.4.0 → python_neva-5.4.1}/neva/database/provider.py +0 -0
  35. {python_neva-5.4.0 → python_neva-5.4.1}/neva/database/py.typed +0 -0
  36. {python_neva-5.4.0 → python_neva-5.4.1}/neva/database/transaction.py +0 -0
  37. {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/__init__.py +0 -0
  38. {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/contracts/__init__.py +0 -0
  39. {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/contracts/dispatcher.py +0 -0
  40. {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/contracts/event.py +0 -0
  41. {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/contracts/handler.py +0 -0
  42. {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/contracts/listener.py +0 -0
  43. {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/dispatcher.py +0 -0
  44. {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/event.py +0 -0
  45. {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/event_registry.py +0 -0
  46. {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/listener.py +0 -0
  47. {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/policy.py +0 -0
  48. {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/provider.py +0 -0
  49. {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/py.typed +0 -0
  50. {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/__init__.py +0 -0
  51. {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/fragments/database-transactions.md +0 -0
  52. {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/fragments/events.md +0 -0
  53. {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/fragments/facades.md +0 -0
  54. {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/fragments/factories.md +0 -0
  55. {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/fragments/observability.md +0 -0
  56. {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/fragments/result-option.md +0 -0
  57. {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/fragments/security.md +0 -0
  58. {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/fragments/service-providers.md +0 -0
  59. {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/fragments/testing.md +0 -0
  60. {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/manifest.py +0 -0
  61. {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/py.typed +0 -0
  62. {python_neva-5.4.0 → python_neva-5.4.1}/neva/obs/__init__.py +0 -0
  63. {python_neva-5.4.0 → python_neva-5.4.1}/neva/obs/config.py +0 -0
  64. {python_neva-5.4.0 → python_neva-5.4.1}/neva/obs/logging/__init__.py +0 -0
  65. {python_neva-5.4.0 → python_neva-5.4.1}/neva/obs/logging/channels.py +0 -0
  66. {python_neva-5.4.0 → python_neva-5.4.1}/neva/obs/logging/contracts.py +0 -0
  67. {python_neva-5.4.0 → python_neva-5.4.1}/neva/obs/logging/manager.py +0 -0
  68. {python_neva-5.4.0 → python_neva-5.4.1}/neva/obs/logging/provider.py +0 -0
  69. {python_neva-5.4.0 → python_neva-5.4.1}/neva/obs/logging/resolver.py +0 -0
  70. {python_neva-5.4.0 → python_neva-5.4.1}/neva/obs/py.typed +0 -0
  71. {python_neva-5.4.0 → python_neva-5.4.1}/neva/polyfactory/__init__.py +0 -0
  72. {python_neva-5.4.0 → python_neva-5.4.1}/neva/polyfactory/factories.py +0 -0
  73. {python_neva-5.4.0 → python_neva-5.4.1}/neva/polyfactory/persistence.py +0 -0
  74. {python_neva-5.4.0 → python_neva-5.4.1}/neva/polyfactory/py.typed +0 -0
  75. {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/__init__.py +0 -0
  76. {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/encryption/__init__.py +0 -0
  77. {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/encryption/encrypter.py +0 -0
  78. {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/encryption/protocol.py +0 -0
  79. {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/hashing/__init__.py +0 -0
  80. {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/hashing/config.py +0 -0
  81. {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/hashing/hash_manager.py +0 -0
  82. {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/hashing/hashers/__init__.py +0 -0
  83. {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/hashing/hashers/argon2.py +0 -0
  84. {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/hashing/hashers/bcrypt.py +0 -0
  85. {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/hashing/hashers/protocol.py +0 -0
  86. {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/provider.py +0 -0
  87. {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/py.typed +0 -0
  88. {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/tokens/__init__.py +0 -0
  89. {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/tokens/generate_token.py +0 -0
  90. {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/tokens/hash_token.py +0 -0
  91. {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/tokens/verify_token.py +0 -0
  92. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/__init__.py +0 -0
  93. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/accessors.py +0 -0
  94. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/__init__.py +0 -0
  95. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/app.py +0 -0
  96. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/app.pyi +0 -0
  97. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/config.py +0 -0
  98. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/config.pyi +0 -0
  99. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/crypt.py +0 -0
  100. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/crypt.pyi +0 -0
  101. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/db.py +0 -0
  102. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/db.pyi +0 -0
  103. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/event.py +0 -0
  104. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/event.pyi +0 -0
  105. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/hash.py +0 -0
  106. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/hash.pyi +0 -0
  107. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/log.py +0 -0
  108. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/log.pyi +0 -0
  109. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/py.typed +0 -0
  110. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/results/__init__.py +0 -0
  111. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/results/collect.py +0 -0
  112. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/results/convert.py +0 -0
  113. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/results/exceptions.py +0 -0
  114. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/results/option.py +0 -0
  115. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/results/pipeline.py +0 -0
  116. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/results/result.py +0 -0
  117. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/strategy.py +0 -0
  118. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/strconv.py +0 -0
  119. {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/time.py +0 -0
  120. {python_neva-5.4.0 → python_neva-5.4.1}/neva/testing/__init__.py +0 -0
  121. {python_neva-5.4.0 → python_neva-5.4.1}/neva/testing/fakes.py +0 -0
  122. {python_neva-5.4.0 → python_neva-5.4.1}/neva/testing/fixtures.py +0 -0
  123. {python_neva-5.4.0 → python_neva-5.4.1}/neva/testing/py.typed +0 -0
  124. {python_neva-5.4.0 → python_neva-5.4.1}/neva/testing/test_case.py +0 -0
  125. {python_neva-5.4.0 → python_neva-5.4.1}/ruff.toml +0 -0
  126. {python_neva-5.4.0 → python_neva-5.4.1}/scripts/retag-with-changelog.sh +0 -0
  127. {python_neva-5.4.0 → python_neva-5.4.1}/tests/__init__.py +0 -0
  128. {python_neva-5.4.0 → python_neva-5.4.1}/tests/arch/__init__.py +0 -0
  129. {python_neva-5.4.0 → python_neva-5.4.1}/tests/arch/test_application_create.py +0 -0
  130. {python_neva-5.4.0 → python_neva-5.4.1}/tests/arch/test_cache.py +0 -0
  131. {python_neva-5.4.0 → python_neva-5.4.1}/tests/arch/test_config_shapes.py +0 -0
  132. {python_neva-5.4.0 → python_neva-5.4.1}/tests/arch/test_context.py +0 -0
  133. {python_neva-5.4.0 → python_neva-5.4.1}/tests/arch/test_extends.py +0 -0
  134. {python_neva-5.4.0 → python_neva-5.4.1}/tests/arch/test_facade_resolution.py +0 -0
  135. {python_neva-5.4.0 → python_neva-5.4.1}/tests/arch/test_facade_root_nesting.py +0 -0
  136. {python_neva-5.4.0 → python_neva-5.4.1}/tests/arch/test_lifetimes.py +0 -0
  137. {python_neva-5.4.0 → python_neva-5.4.1}/tests/arch/test_register_resolution.py +0 -0
  138. {python_neva-5.4.0 → python_neva-5.4.1}/tests/arch/test_registration.py +0 -0
  139. {python_neva-5.4.0 → python_neva-5.4.1}/tests/arch/test_scope.py +0 -0
  140. {python_neva-5.4.0 → python_neva-5.4.1}/tests/config/__init__.py +0 -0
  141. {python_neva-5.4.0 → python_neva-5.4.1}/tests/config/test_base_providers.py +0 -0
  142. {python_neva-5.4.0 → python_neva-5.4.1}/tests/config/test_config_path_resolution.py +0 -0
  143. {python_neva-5.4.0 → python_neva-5.4.1}/tests/config/test_loader.py +0 -0
  144. {python_neva-5.4.0 → python_neva-5.4.1}/tests/config/test_repository.py +0 -0
  145. {python_neva-5.4.0 → python_neva-5.4.1}/tests/conftest.py +0 -0
  146. {python_neva-5.4.0 → python_neva-5.4.1}/tests/database/__init__.py +0 -0
  147. {python_neva-5.4.0 → python_neva-5.4.1}/tests/database/test_connection_manager.py +0 -0
  148. {python_neva-5.4.0 → python_neva-5.4.1}/tests/database/test_database_manager.py +0 -0
  149. {python_neva-5.4.0 → python_neva-5.4.1}/tests/database/test_detached_lifespan.py +0 -0
  150. {python_neva-5.4.0 → python_neva-5.4.1}/tests/database/test_edge_cases.py +0 -0
  151. {python_neva-5.4.0 → python_neva-5.4.1}/tests/database/test_multi_connection.py +0 -0
  152. {python_neva-5.4.0 → python_neva-5.4.1}/tests/database/test_provider_lifespan.py +0 -0
  153. {python_neva-5.4.0 → python_neva-5.4.1}/tests/database/test_sqlalchemy_integration.py +0 -0
  154. {python_neva-5.4.0 → python_neva-5.4.1}/tests/database/test_transaction.py +0 -0
  155. {python_neva-5.4.0 → python_neva-5.4.1}/tests/database/test_transaction_callbacks.py +0 -0
  156. {python_neva-5.4.0 → python_neva-5.4.1}/tests/database/test_transaction_context.py +0 -0
  157. {python_neva-5.4.0 → python_neva-5.4.1}/tests/database/test_transaction_registry.py +0 -0
  158. {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/__init__.py +0 -0
  159. {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/conftest.py +0 -0
  160. {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/test_before_dispatch.py +0 -0
  161. {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/test_binding.py +0 -0
  162. {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/test_concurrent.py +0 -0
  163. {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/test_deferred.py +0 -0
  164. {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/test_dispatch.py +0 -0
  165. {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/test_event.py +0 -0
  166. {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/test_function_listener.py +0 -0
  167. {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/test_immediate.py +0 -0
  168. {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/test_listen_on_parent_class.py +0 -0
  169. {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/test_listener_wiring.py +0 -0
  170. {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/test_unimplemented_handle.py +0 -0
  171. {python_neva-5.4.0 → python_neva-5.4.1}/tests/guidelines/__init__.py +0 -0
  172. {python_neva-5.4.0 → python_neva-5.4.1}/tests/guidelines/test_manifest.py +0 -0
  173. {python_neva-5.4.0 → python_neva-5.4.1}/tests/obs/__init__.py +0 -0
  174. {python_neva-5.4.0 → python_neva-5.4.1}/tests/obs/conftest.py +0 -0
  175. {python_neva-5.4.0 → python_neva-5.4.1}/tests/obs/test_channels.py +0 -0
  176. {python_neva-5.4.0 → python_neva-5.4.1}/tests/obs/test_facade.py +0 -0
  177. {python_neva-5.4.0 → python_neva-5.4.1}/tests/obs/test_manager.py +0 -0
  178. {python_neva-5.4.0 → python_neva-5.4.1}/tests/obs/test_provider_hook.py +0 -0
  179. {python_neva-5.4.0 → python_neva-5.4.1}/tests/obs/test_resolver.py +0 -0
  180. {python_neva-5.4.0 → python_neva-5.4.1}/tests/polyfactory/__init__.py +0 -0
  181. {python_neva-5.4.0 → python_neva-5.4.1}/tests/polyfactory/test_model_factory.py +0 -0
  182. {python_neva-5.4.0 → python_neva-5.4.1}/tests/security/__init__.py +0 -0
  183. {python_neva-5.4.0 → python_neva-5.4.1}/tests/security/test_config_shapes.py +0 -0
  184. {python_neva-5.4.0 → python_neva-5.4.1}/tests/security/test_encrypter.py +0 -0
  185. {python_neva-5.4.0 → python_neva-5.4.1}/tests/security/test_hash_manager.py +0 -0
  186. {python_neva-5.4.0 → python_neva-5.4.1}/tests/security/test_tokens.py +0 -0
  187. {python_neva-5.4.0 → python_neva-5.4.1}/tests/support/__init__.py +0 -0
  188. {python_neva-5.4.0 → python_neva-5.4.1}/tests/support/test_accessors.py +0 -0
  189. {python_neva-5.4.0 → python_neva-5.4.1}/tests/support/test_collect.py +0 -0
  190. {python_neva-5.4.0 → python_neva-5.4.1}/tests/support/test_pipeline.py +0 -0
  191. {python_neva-5.4.0 → python_neva-5.4.1}/tests/support/test_results.py +0 -0
  192. {python_neva-5.4.0 → python_neva-5.4.1}/tests/support/test_results_imports.py +0 -0
  193. {python_neva-5.4.0 → python_neva-5.4.1}/tests/support/test_strategy.py +0 -0
  194. {python_neva-5.4.0 → python_neva-5.4.1}/tests/support/test_strategy_imports.py +0 -0
  195. {python_neva-5.4.0 → python_neva-5.4.1}/tests/testing/__init__.py +0 -0
  196. {python_neva-5.4.0 → python_neva-5.4.1}/tests/testing/test_application_reuse.py +0 -0
  197. {python_neva-5.4.0 → python_neva-5.4.1}/tests/testing/test_boot_application.py +0 -0
  198. {python_neva-5.4.0 → python_neva-5.4.1}/tests/testing/test_create_config_migration.py +0 -0
  199. {python_neva-5.4.0 → python_neva-5.4.1}/tests/testing/test_event_fake.py +0 -0
  200. {python_neva-5.4.0 → python_neva-5.4.1}/tests/testing/test_facade_restore.py +0 -0
  201. {python_neva-5.4.0 → python_neva-5.4.1}/tests/testing/test_fixtures.py +0 -0
  202. {python_neva-5.4.0 → python_neva-5.4.1}/tests/testing/test_refresh_database.py +0 -0
  203. {python_neva-5.4.0 → python_neva-5.4.1}/tests/testing/test_test_case.py +0 -0
@@ -1,3 +1,10 @@
1
+ ## 5.4.1 (2026-09-09)
2
+
3
+ ### 🐛🚑️ Fixes
4
+
5
+ - **config**: apply provider declarations on a late registration
6
+ - **guidelines**: render the drivers fragment for apps below 5.3
7
+
1
8
  ## 5.4.0 (2026-09-07)
2
9
 
3
10
  ### ✨ Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-neva
3
- Version: 5.4.0
3
+ Version: 5.4.1
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: aiosqlite>=0.20.0
@@ -68,6 +68,7 @@ class Application:
68
68
  self._extra_providers: list[BaseProvider] = []
69
69
  self._container: dishka.AsyncContainer | None = None
70
70
  self._booted: bool = False
71
+ self._declarations_applied: bool = False
71
72
 
72
73
  configuration_path = _resolve_config_path(config_path)
73
74
  match ConfigLoader(configuration_path).load_all():
@@ -164,6 +165,15 @@ class Application:
164
165
  would orphan. Registering after boot is an error rather than a
165
166
  silently ignored call.
166
167
 
168
+ A provider arriving after construction has its declared config
169
+ namespaces validated here, and its secrets marked once it registers, so
170
+ that it is held to the same shape as one named in the `providers`
171
+ config namespace. Validation runs before the provider is instantiated:
172
+ a malformed namespace leaves nothing registered and no secret marked.
173
+ Providers registered *during* construction are skipped here and
174
+ validated together by `_apply_provider_config_declarations`, which
175
+ reports every bad namespace in one message.
176
+
167
177
  Returns:
168
178
  Result containing the registered provider instance or an error message.
169
179
  """
@@ -177,12 +187,18 @@ class Application:
177
187
  "or declare them in the 'providers' config namespace."
178
188
  )
179
189
 
190
+ late = self._declarations_applied
191
+ if late and (failures := self._declaration_failures(provider)):
192
+ return Err("Invalid configuration: " + "; ".join(failures))
193
+
180
194
  registered = (
181
195
  provider(self)
182
196
  .register()
183
197
  .map(lambda p: self.providers.setdefault(provider, p))
184
198
  )
185
199
  if registered.is_ok:
200
+ if late:
201
+ self.config.mark_sensitive(*provider.sensitive)
186
202
  self._container = None
187
203
  return registered
188
204
 
@@ -194,22 +210,39 @@ class Application:
194
210
  configuration error. Every failure is reported together, so a boot
195
211
  naming three bad namespaces takes one run rather than three.
196
212
 
213
+ Providers registered after this point go through `register`, which
214
+ applies the same declarations one provider at a time.
215
+
197
216
  Raises:
198
217
  RuntimeError: If any declared namespace fails its shape.
199
218
  """
200
- checks: list[Result[None, str]] = []
219
+ failures: list[str] = []
201
220
  for provider in self.providers.values():
202
221
  declaring = type(provider)
203
222
  self.config.mark_sensitive(*declaring.sensitive)
204
- checks.extend(
205
- self._validate_namespace(namespace, schema)
206
- for namespace, schema in declaring.config_schema.items()
207
- )
223
+ failures.extend(self._declaration_failures(declaring))
208
224
 
209
- _, failures = partition(checks)
210
225
  if failures:
211
226
  raise RuntimeError("Invalid configuration: " + "; ".join(failures))
212
227
 
228
+ self._declarations_applied = True
229
+
230
+ def _declaration_failures(self, declaring: type[ServiceProvider]) -> list[str]:
231
+ """Check one provider's declared namespaces, applying nothing.
232
+
233
+ Args:
234
+ declaring: The provider type whose `config_schema` to check.
235
+
236
+ Returns:
237
+ One message per malformed namespace, empty when every declared
238
+ namespace is absent or valid.
239
+ """
240
+ _, failures = partition(
241
+ self._validate_namespace(namespace, schema)
242
+ for namespace, schema in declaring.config_schema.items()
243
+ )
244
+ return failures
245
+
213
246
  def _validate_namespace(
214
247
  self, namespace: str, schema: TypeForm[Any]
215
248
  ) -> Result[None, str]:
@@ -104,9 +104,9 @@ reaches the repository, frozen or not.
104
104
 
105
105
  ## Validating a namespace at boot
106
106
 
107
- A provider declares the shape its namespace must have, and it is checked once during
108
- construction (since 5.4). The declaration reuses the `TypedDict` shapes above — nothing needs
109
- to become a pydantic model.
107
+ A provider declares the shape its namespace must have, and it is checked at boot (since 5.4).
108
+ The declaration reuses the `TypedDict` shapes above — nothing needs to become a pydantic
109
+ model.
110
110
 
111
111
  ```python
112
112
  class DatabaseServiceProvider(ServiceProvider):
@@ -120,10 +120,21 @@ class DatabaseServiceProvider(ServiceProvider):
120
120
  - A provider that declares nothing is validated not at all — the behaviour before 5.4.
121
121
  - The constructor raises, as it does for any config failure; `Application.create()` returns it
122
122
  as an `Err` instead.
123
+ - **A provider registered after construction is checked too, and reports as an `Err` from
124
+ `Application.register` rather than raising** (since 5.4.1). That is the path
125
+ `App.register` on `neva-fastapi` and `neva-faststream` delegates to; before 5.4.1 it skipped
126
+ validation and marked no secrets. A rejected provider is not registered at all.
127
+
128
+ **`type_` is a cast, not validation, so the value a malformed key holds is still whatever the
129
+ file put there.** Validation only *rejects* it — nothing is coerced and written back, and
130
+ pydantic's default lax mode accepts `"false"` for a `bool`. A namespace whose values must be
131
+ exactly typed declares `__pydantic_config__ = ConfigDict(strict=True)` on its `TypedDict`.
123
132
 
124
133
  ## Secrets
125
134
 
126
135
  A provider marks the keys that hold secrets, and they are replaced in the display form only.
136
+ Marking happens wherever the provider is registered — at construction, or through
137
+ `Application.register` (since 5.4.1).
127
138
 
128
139
  ```python
129
140
  class SecurityProvider(ServiceProvider):
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  id: drivers
3
3
  title: Swappable drivers
4
- requires: python-neva>=5.3
4
+ requires: python-neva>=5.0
5
5
  triggers: [swappable backend, driver manager, multiple implementations, choosing an implementation from config, registering a driver, plugin extension point]
6
6
  priority: 70
7
7
  verified_by: [tests/support/test_strategy.py, tests/support/test_strategy_imports.py]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-neva"
3
- version = "5.4.0"
3
+ version = "5.4.1"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -1,13 +1,14 @@
1
1
  """Config namespaces a provider declares are validated and redacted at boot."""
2
2
 
3
3
  from pathlib import Path
4
- from typing import Any, ClassVar
4
+ from typing import Any, ClassVar, NotRequired, TypedDict, override
5
5
 
6
6
  import pytest
7
7
  from typing_extensions import TypeForm
8
8
 
9
- from neva.arch import Application
9
+ from neva.arch import Application, ServiceProvider
10
10
  from neva.config.repository import REDACTED, ConfigRepository
11
+ from neva.support import Ok, Result
11
12
 
12
13
 
13
14
  def _config_dir(tmp_path: Path, **files: str) -> Path:
@@ -28,6 +29,34 @@ config = {"connections": {"default": {"url": "sqlite+aiosqlite:///:memory:"}}}
28
29
  BAD_DATABASE = """
29
30
  config = {"connections": {"default": {"pool_size": 5}}}
30
31
  """
32
+ GOOD_WIDGETS = """
33
+ config = {"per_page": 25, "token": "s3cret"}
34
+ """
35
+ BAD_WIDGETS = """
36
+ config = {"per_page": "many"}
37
+ """
38
+
39
+
40
+ class WidgetConfig(TypedDict):
41
+ """A namespace owned by a provider that is registered after construction."""
42
+
43
+ per_page: NotRequired[int]
44
+ token: NotRequired[str]
45
+
46
+
47
+ class WidgetProvider(ServiceProvider):
48
+ config_schema: ClassVar[dict[str, TypeForm[Any]]] = {"widgets": WidgetConfig}
49
+ sensitive: ClassVar[tuple[str, ...]] = ("widgets.token",)
50
+
51
+ @override
52
+ def register(self) -> Result["WidgetProvider", str]:
53
+ return Ok(self)
54
+
55
+
56
+ class SilentProvider(ServiceProvider):
57
+ @override
58
+ def register(self) -> Result["SilentProvider", str]:
59
+ return Ok(self)
31
60
 
32
61
 
33
62
  class TestValidationAtBoot:
@@ -145,3 +174,83 @@ class TestDeclarationsAreTyped:
145
174
 
146
175
  assert Declaring.config_schema == {"x": dict[str, int]}
147
176
  assert Declaring.sensitive == ("x.token",)
177
+
178
+
179
+ class TestDeclarationsApplyToALateRegistration:
180
+ """A provider registered after construction is held to the same shape.
181
+
182
+ The declarations pass runs in `__init__` over the providers construction
183
+ knows about. Every integration package exposes `Application.register` as
184
+ its own `register`, so a provider reaching the application that way used
185
+ to skip validation entirely and never mark its secrets.
186
+ """
187
+
188
+ @staticmethod
189
+ def _app(tmp_path: Path, **files: str) -> Application:
190
+ return Application.create(config_path=_config_dir(tmp_path, **files)).unwrap()
191
+
192
+ def test_a_malformed_namespace_fails_the_registration(self, tmp_path: Path) -> None:
193
+ app = self._app(tmp_path, widgets=BAD_WIDGETS)
194
+
195
+ result = app.register(WidgetProvider)
196
+
197
+ assert result.is_err
198
+ assert "'widgets' is malformed" in result.unwrap_err()
199
+ assert "per_page" in result.unwrap_err()
200
+
201
+ def test_a_rejected_provider_is_not_registered(self, tmp_path: Path) -> None:
202
+ app = self._app(tmp_path, widgets=BAD_WIDGETS)
203
+
204
+ _ = app.register(WidgetProvider)
205
+
206
+ assert WidgetProvider not in app.providers
207
+
208
+ def test_a_rejected_provider_marks_no_secret(self, tmp_path: Path) -> None:
209
+ """Validation runs before anything is applied, secrets included."""
210
+ app = self._app(tmp_path, widgets=BAD_WIDGETS)
211
+
212
+ _ = app.register(WidgetProvider)
213
+
214
+ assert "widgets.token" not in app.config.sensitive
215
+
216
+ def test_a_valid_namespace_registers(self, tmp_path: Path) -> None:
217
+ app = self._app(tmp_path, widgets=GOOD_WIDGETS)
218
+
219
+ assert app.register(WidgetProvider).is_ok
220
+
221
+ def test_an_absent_namespace_registers(self, tmp_path: Path) -> None:
222
+ app = self._app(tmp_path)
223
+
224
+ assert app.register(WidgetProvider).is_ok
225
+
226
+ def test_a_registered_provider_marks_its_secrets(self, tmp_path: Path) -> None:
227
+ app = self._app(tmp_path, widgets=GOOD_WIDGETS)
228
+
229
+ _ = app.register(WidgetProvider).unwrap()
230
+
231
+ assert "widgets.token" in app.config.sensitive
232
+ assert app.config.redacted()["widgets"]["token"] == REDACTED
233
+ assert app.config.get("widgets.token", type_=str).unwrap() == "s3cret"
234
+
235
+ def test_a_provider_declaring_nothing_still_registers(self, tmp_path: Path) -> None:
236
+ """The additive guarantee, on this path too."""
237
+ app = self._app(tmp_path, widgets=BAD_WIDGETS)
238
+
239
+ assert app.register(SilentProvider).is_ok
240
+
241
+ def test_registering_twice_is_still_idempotent(self, tmp_path: Path) -> None:
242
+ app = self._app(tmp_path, widgets=GOOD_WIDGETS)
243
+
244
+ first = app.register(WidgetProvider).unwrap()
245
+
246
+ assert app.register(WidgetProvider).unwrap() is first
247
+
248
+ def test_a_construction_time_provider_is_still_reported_in_one_message(
249
+ self, tmp_path: Path
250
+ ) -> None:
251
+ """The batch path keeps its behaviour: one message, every namespace."""
252
+ config_dir = _config_dir(tmp_path, database=BAD_DATABASE)
253
+
254
+ message = Application.create(config_path=config_dir).unwrap_err()
255
+
256
+ assert message.startswith("Invalid configuration: ")
@@ -1539,7 +1539,7 @@ wheels = [
1539
1539
 
1540
1540
  [[package]]
1541
1541
  name = "python-neva"
1542
- version = "5.4.0"
1542
+ version = "5.4.1"
1543
1543
  source = { editable = "." }
1544
1544
  dependencies = [
1545
1545
  { name = "aiosqlite" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes