python-neva 4.1.0__tar.gz → 5.1.0__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 (190) hide show
  1. {python_neva-4.1.0 → python_neva-5.1.0}/.gitlab-ci.yml +9 -0
  2. {python_neva-4.1.0 → python_neva-5.1.0}/CHANGELOG.md +30 -0
  3. {python_neva-4.1.0 → python_neva-5.1.0}/PKG-INFO +37 -6
  4. {python_neva-4.1.0 → python_neva-5.1.0}/README.md +32 -3
  5. {python_neva-4.1.0 → python_neva-5.1.0}/neva/database/connection.py +3 -6
  6. {python_neva-4.1.0 → python_neva-5.1.0}/neva/database/manager.py +0 -2
  7. python_neva-5.1.0/neva/guidelines/fragments/facades.md +95 -0
  8. python_neva-5.1.0/neva/guidelines/fragments/factories.md +79 -0
  9. python_neva-5.1.0/neva/guidelines/fragments/observability.md +130 -0
  10. python_neva-5.1.0/neva/guidelines/fragments/security.md +142 -0
  11. {python_neva-4.1.0 → python_neva-5.1.0}/neva/guidelines/fragments/service-providers.md +61 -2
  12. python_neva-5.1.0/neva/obs/__init__.py +19 -0
  13. python_neva-5.1.0/neva/obs/config.py +37 -0
  14. python_neva-5.1.0/neva/obs/logging/__init__.py +22 -0
  15. python_neva-5.1.0/neva/obs/logging/channels.py +310 -0
  16. python_neva-5.1.0/neva/obs/logging/contracts.py +40 -0
  17. python_neva-5.1.0/neva/obs/logging/manager.py +196 -0
  18. python_neva-5.1.0/neva/obs/logging/provider.py +36 -0
  19. python_neva-5.1.0/neva/obs/logging/resolver.py +138 -0
  20. python_neva-5.1.0/neva/security/__init__.py +33 -0
  21. {python_neva-4.1.0 → python_neva-5.1.0}/neva/security/hashing/__init__.py +4 -0
  22. {python_neva-4.1.0 → python_neva-5.1.0}/neva/support/facade/log.pyi +46 -1
  23. {python_neva-4.1.0 → python_neva-5.1.0}/pyproject.toml +3 -3
  24. python_neva-5.1.0/tests/arch/test_facade_resolution.py +102 -0
  25. python_neva-5.1.0/tests/arch/test_register_resolution.py +108 -0
  26. {python_neva-4.1.0 → python_neva-5.1.0}/tests/database/test_sqlalchemy_integration.py +32 -1
  27. python_neva-5.1.0/tests/obs/conftest.py +133 -0
  28. python_neva-5.1.0/tests/obs/test_channels.py +213 -0
  29. python_neva-5.1.0/tests/obs/test_facade.py +148 -0
  30. python_neva-5.1.0/tests/obs/test_manager.py +160 -0
  31. python_neva-5.1.0/tests/obs/test_provider_hook.py +107 -0
  32. python_neva-5.1.0/tests/obs/test_resolver.py +149 -0
  33. python_neva-5.1.0/tests/polyfactory/__init__.py +0 -0
  34. python_neva-5.1.0/tests/polyfactory/test_model_factory.py +188 -0
  35. python_neva-5.1.0/tests/security/test_config_shapes.py +44 -0
  36. {python_neva-4.1.0 → python_neva-5.1.0}/tests/security/test_hash_manager.py +62 -0
  37. python_neva-5.1.0/tests/support/__init__.py +0 -0
  38. python_neva-5.1.0/tests/support/test_strategy.py +98 -0
  39. {python_neva-4.1.0 → python_neva-5.1.0}/uv.lock +53 -6
  40. python_neva-4.1.0/neva/obs/__init__.py +0 -9
  41. python_neva-4.1.0/neva/obs/instrumentation/__init__.py +0 -1
  42. python_neva-4.1.0/neva/obs/instrumentation/sqlalchemy.py +0 -15
  43. python_neva-4.1.0/neva/obs/logging/__init__.py +0 -10
  44. python_neva-4.1.0/neva/obs/logging/manager.py +0 -90
  45. python_neva-4.1.0/neva/obs/logging/provider.py +0 -26
  46. python_neva-4.1.0/neva/security/__init__.py +0 -17
  47. {python_neva-4.1.0 → python_neva-5.1.0}/.claude/settings.local.json +0 -0
  48. {python_neva-4.1.0 → python_neva-5.1.0}/.envrc +0 -0
  49. {python_neva-4.1.0 → python_neva-5.1.0}/.gitignore +0 -0
  50. {python_neva-4.1.0 → python_neva-5.1.0}/.pre-commit-config.yaml +0 -0
  51. {python_neva-4.1.0 → python_neva-5.1.0}/.python-version +0 -0
  52. {python_neva-4.1.0 → python_neva-5.1.0}/CLAUDE.md +0 -0
  53. {python_neva-4.1.0 → python_neva-5.1.0}/neva/arch/__init__.py +0 -0
  54. {python_neva-4.1.0 → python_neva-5.1.0}/neva/arch/application.py +0 -0
  55. {python_neva-4.1.0 → python_neva-5.1.0}/neva/arch/config.py +0 -0
  56. {python_neva-4.1.0 → python_neva-5.1.0}/neva/arch/facade.py +0 -0
  57. {python_neva-4.1.0 → python_neva-5.1.0}/neva/arch/integrations/__init__.py +0 -0
  58. {python_neva-4.1.0 → python_neva-5.1.0}/neva/arch/integrations/faststream.py +0 -0
  59. {python_neva-4.1.0 → python_neva-5.1.0}/neva/arch/markers.py +0 -0
  60. {python_neva-4.1.0 → python_neva-5.1.0}/neva/arch/py.typed +0 -0
  61. {python_neva-4.1.0 → python_neva-5.1.0}/neva/arch/scopes.py +0 -0
  62. {python_neva-4.1.0 → python_neva-5.1.0}/neva/arch/service_provider.py +0 -0
  63. {python_neva-4.1.0 → python_neva-5.1.0}/neva/config/__init__.py +0 -0
  64. {python_neva-4.1.0 → python_neva-5.1.0}/neva/config/base_providers.py +0 -0
  65. {python_neva-4.1.0 → python_neva-5.1.0}/neva/config/loader.py +0 -0
  66. {python_neva-4.1.0 → python_neva-5.1.0}/neva/config/py.typed +0 -0
  67. {python_neva-4.1.0 → python_neva-5.1.0}/neva/config/repository.py +0 -0
  68. {python_neva-4.1.0 → python_neva-5.1.0}/neva/database/__init__.py +0 -0
  69. {python_neva-4.1.0 → python_neva-5.1.0}/neva/database/config.py +0 -0
  70. {python_neva-4.1.0 → python_neva-5.1.0}/neva/database/provider.py +0 -0
  71. {python_neva-4.1.0 → python_neva-5.1.0}/neva/database/py.typed +0 -0
  72. {python_neva-4.1.0 → python_neva-5.1.0}/neva/database/transaction.py +0 -0
  73. {python_neva-4.1.0 → python_neva-5.1.0}/neva/events/__init__.py +0 -0
  74. {python_neva-4.1.0 → python_neva-5.1.0}/neva/events/contracts/__init__.py +0 -0
  75. {python_neva-4.1.0 → python_neva-5.1.0}/neva/events/contracts/dispatcher.py +0 -0
  76. {python_neva-4.1.0 → python_neva-5.1.0}/neva/events/contracts/event.py +0 -0
  77. {python_neva-4.1.0 → python_neva-5.1.0}/neva/events/contracts/handler.py +0 -0
  78. {python_neva-4.1.0 → python_neva-5.1.0}/neva/events/contracts/listener.py +0 -0
  79. {python_neva-4.1.0 → python_neva-5.1.0}/neva/events/dispatcher.py +0 -0
  80. {python_neva-4.1.0 → python_neva-5.1.0}/neva/events/event.py +0 -0
  81. {python_neva-4.1.0 → python_neva-5.1.0}/neva/events/event_registry.py +0 -0
  82. {python_neva-4.1.0 → python_neva-5.1.0}/neva/events/listener.py +0 -0
  83. {python_neva-4.1.0 → python_neva-5.1.0}/neva/events/policy.py +0 -0
  84. {python_neva-4.1.0 → python_neva-5.1.0}/neva/events/provider.py +0 -0
  85. {python_neva-4.1.0 → python_neva-5.1.0}/neva/events/py.typed +0 -0
  86. {python_neva-4.1.0 → python_neva-5.1.0}/neva/guidelines/__init__.py +0 -0
  87. {python_neva-4.1.0 → python_neva-5.1.0}/neva/guidelines/fragments/configuration.md +0 -0
  88. {python_neva-4.1.0 → python_neva-5.1.0}/neva/guidelines/fragments/database-transactions.md +0 -0
  89. {python_neva-4.1.0 → python_neva-5.1.0}/neva/guidelines/fragments/events.md +0 -0
  90. {python_neva-4.1.0 → python_neva-5.1.0}/neva/guidelines/fragments/result-option.md +0 -0
  91. {python_neva-4.1.0 → python_neva-5.1.0}/neva/guidelines/fragments/testing.md +0 -0
  92. {python_neva-4.1.0 → python_neva-5.1.0}/neva/guidelines/manifest.py +0 -0
  93. {python_neva-4.1.0 → python_neva-5.1.0}/neva/guidelines/py.typed +0 -0
  94. {python_neva-4.1.0 → python_neva-5.1.0}/neva/obs/py.typed +0 -0
  95. {python_neva-4.1.0 → python_neva-5.1.0}/neva/polyfactory/__init__.py +0 -0
  96. {python_neva-4.1.0 → python_neva-5.1.0}/neva/polyfactory/factories.py +0 -0
  97. {python_neva-4.1.0 → python_neva-5.1.0}/neva/polyfactory/persistence.py +0 -0
  98. {python_neva-4.1.0 → python_neva-5.1.0}/neva/polyfactory/py.typed +0 -0
  99. {python_neva-4.1.0 → python_neva-5.1.0}/neva/security/encryption/__init__.py +0 -0
  100. {python_neva-4.1.0 → python_neva-5.1.0}/neva/security/encryption/encrypter.py +0 -0
  101. {python_neva-4.1.0 → python_neva-5.1.0}/neva/security/encryption/protocol.py +0 -0
  102. {python_neva-4.1.0 → python_neva-5.1.0}/neva/security/hashing/config.py +0 -0
  103. {python_neva-4.1.0 → python_neva-5.1.0}/neva/security/hashing/hash_manager.py +0 -0
  104. {python_neva-4.1.0 → python_neva-5.1.0}/neva/security/hashing/hashers/__init__.py +0 -0
  105. {python_neva-4.1.0 → python_neva-5.1.0}/neva/security/hashing/hashers/argon2.py +0 -0
  106. {python_neva-4.1.0 → python_neva-5.1.0}/neva/security/hashing/hashers/bcrypt.py +0 -0
  107. {python_neva-4.1.0 → python_neva-5.1.0}/neva/security/hashing/hashers/protocol.py +0 -0
  108. {python_neva-4.1.0 → python_neva-5.1.0}/neva/security/provider.py +0 -0
  109. {python_neva-4.1.0 → python_neva-5.1.0}/neva/security/py.typed +0 -0
  110. {python_neva-4.1.0 → python_neva-5.1.0}/neva/security/tokens/__init__.py +0 -0
  111. {python_neva-4.1.0 → python_neva-5.1.0}/neva/security/tokens/generate_token.py +0 -0
  112. {python_neva-4.1.0 → python_neva-5.1.0}/neva/security/tokens/hash_token.py +0 -0
  113. {python_neva-4.1.0 → python_neva-5.1.0}/neva/security/tokens/verify_token.py +0 -0
  114. {python_neva-4.1.0 → python_neva-5.1.0}/neva/support/__init__.py +0 -0
  115. {python_neva-4.1.0 → python_neva-5.1.0}/neva/support/accessors.py +0 -0
  116. {python_neva-4.1.0 → python_neva-5.1.0}/neva/support/facade/__init__.py +0 -0
  117. {python_neva-4.1.0 → python_neva-5.1.0}/neva/support/facade/app.py +0 -0
  118. {python_neva-4.1.0 → python_neva-5.1.0}/neva/support/facade/app.pyi +0 -0
  119. {python_neva-4.1.0 → python_neva-5.1.0}/neva/support/facade/config.py +0 -0
  120. {python_neva-4.1.0 → python_neva-5.1.0}/neva/support/facade/config.pyi +0 -0
  121. {python_neva-4.1.0 → python_neva-5.1.0}/neva/support/facade/crypt.py +0 -0
  122. {python_neva-4.1.0 → python_neva-5.1.0}/neva/support/facade/crypt.pyi +0 -0
  123. {python_neva-4.1.0 → python_neva-5.1.0}/neva/support/facade/db.py +0 -0
  124. {python_neva-4.1.0 → python_neva-5.1.0}/neva/support/facade/db.pyi +0 -0
  125. {python_neva-4.1.0 → python_neva-5.1.0}/neva/support/facade/event.py +0 -0
  126. {python_neva-4.1.0 → python_neva-5.1.0}/neva/support/facade/event.pyi +0 -0
  127. {python_neva-4.1.0 → python_neva-5.1.0}/neva/support/facade/hash.py +0 -0
  128. {python_neva-4.1.0 → python_neva-5.1.0}/neva/support/facade/hash.pyi +0 -0
  129. {python_neva-4.1.0 → python_neva-5.1.0}/neva/support/facade/log.py +0 -0
  130. {python_neva-4.1.0 → python_neva-5.1.0}/neva/support/py.typed +0 -0
  131. {python_neva-4.1.0 → python_neva-5.1.0}/neva/support/results.py +0 -0
  132. {python_neva-4.1.0 → python_neva-5.1.0}/neva/support/strategy.py +0 -0
  133. {python_neva-4.1.0 → python_neva-5.1.0}/neva/support/strconv.py +0 -0
  134. {python_neva-4.1.0 → python_neva-5.1.0}/neva/support/time.py +0 -0
  135. {python_neva-4.1.0 → python_neva-5.1.0}/neva/testing/__init__.py +0 -0
  136. {python_neva-4.1.0 → python_neva-5.1.0}/neva/testing/fakes.py +0 -0
  137. {python_neva-4.1.0 → python_neva-5.1.0}/neva/testing/fixtures.py +0 -0
  138. {python_neva-4.1.0 → python_neva-5.1.0}/neva/testing/py.typed +0 -0
  139. {python_neva-4.1.0 → python_neva-5.1.0}/neva/testing/test_case.py +0 -0
  140. {python_neva-4.1.0 → python_neva-5.1.0}/ruff.toml +0 -0
  141. {python_neva-4.1.0 → python_neva-5.1.0}/scripts/retag-with-changelog.sh +0 -0
  142. {python_neva-4.1.0 → python_neva-5.1.0}/tests/__init__.py +0 -0
  143. {python_neva-4.1.0 → python_neva-5.1.0}/tests/arch/__init__.py +0 -0
  144. {python_neva-4.1.0 → python_neva-5.1.0}/tests/arch/test_cache.py +0 -0
  145. {python_neva-4.1.0 → python_neva-5.1.0}/tests/arch/test_config_shapes.py +0 -0
  146. {python_neva-4.1.0 → python_neva-5.1.0}/tests/arch/test_context.py +0 -0
  147. {python_neva-4.1.0 → python_neva-5.1.0}/tests/arch/test_extends.py +0 -0
  148. {python_neva-4.1.0 → python_neva-5.1.0}/tests/arch/test_facade_root_nesting.py +0 -0
  149. {python_neva-4.1.0 → python_neva-5.1.0}/tests/arch/test_lifetimes.py +0 -0
  150. {python_neva-4.1.0 → python_neva-5.1.0}/tests/arch/test_registration.py +0 -0
  151. {python_neva-4.1.0 → python_neva-5.1.0}/tests/arch/test_scope.py +0 -0
  152. {python_neva-4.1.0 → python_neva-5.1.0}/tests/config/__init__.py +0 -0
  153. {python_neva-4.1.0 → python_neva-5.1.0}/tests/config/test_config_path_resolution.py +0 -0
  154. {python_neva-4.1.0 → python_neva-5.1.0}/tests/config/test_loader.py +0 -0
  155. {python_neva-4.1.0 → python_neva-5.1.0}/tests/config/test_repository.py +0 -0
  156. {python_neva-4.1.0 → python_neva-5.1.0}/tests/conftest.py +0 -0
  157. {python_neva-4.1.0 → python_neva-5.1.0}/tests/database/__init__.py +0 -0
  158. {python_neva-4.1.0 → python_neva-5.1.0}/tests/database/test_connection_manager.py +0 -0
  159. {python_neva-4.1.0 → python_neva-5.1.0}/tests/database/test_database_manager.py +0 -0
  160. {python_neva-4.1.0 → python_neva-5.1.0}/tests/database/test_detached_lifespan.py +0 -0
  161. {python_neva-4.1.0 → python_neva-5.1.0}/tests/database/test_edge_cases.py +0 -0
  162. {python_neva-4.1.0 → python_neva-5.1.0}/tests/database/test_multi_connection.py +0 -0
  163. {python_neva-4.1.0 → python_neva-5.1.0}/tests/database/test_transaction.py +0 -0
  164. {python_neva-4.1.0 → python_neva-5.1.0}/tests/database/test_transaction_callbacks.py +0 -0
  165. {python_neva-4.1.0 → python_neva-5.1.0}/tests/database/test_transaction_context.py +0 -0
  166. {python_neva-4.1.0 → python_neva-5.1.0}/tests/database/test_transaction_registry.py +0 -0
  167. {python_neva-4.1.0 → python_neva-5.1.0}/tests/events/__init__.py +0 -0
  168. {python_neva-4.1.0 → python_neva-5.1.0}/tests/events/conftest.py +0 -0
  169. {python_neva-4.1.0 → python_neva-5.1.0}/tests/events/test_before_dispatch.py +0 -0
  170. {python_neva-4.1.0 → python_neva-5.1.0}/tests/events/test_binding.py +0 -0
  171. {python_neva-4.1.0 → python_neva-5.1.0}/tests/events/test_deferred.py +0 -0
  172. {python_neva-4.1.0 → python_neva-5.1.0}/tests/events/test_dispatch.py +0 -0
  173. {python_neva-4.1.0 → python_neva-5.1.0}/tests/events/test_event.py +0 -0
  174. {python_neva-4.1.0 → python_neva-5.1.0}/tests/events/test_function_listener.py +0 -0
  175. {python_neva-4.1.0 → python_neva-5.1.0}/tests/events/test_immediate.py +0 -0
  176. {python_neva-4.1.0 → python_neva-5.1.0}/tests/events/test_listen_on_parent_class.py +0 -0
  177. {python_neva-4.1.0 → python_neva-5.1.0}/tests/events/test_listener_wiring.py +0 -0
  178. {python_neva-4.1.0/tests/support → python_neva-5.1.0/tests/obs}/__init__.py +0 -0
  179. {python_neva-4.1.0 → python_neva-5.1.0}/tests/security/__init__.py +0 -0
  180. {python_neva-4.1.0 → python_neva-5.1.0}/tests/security/test_encrypter.py +0 -0
  181. {python_neva-4.1.0 → python_neva-5.1.0}/tests/security/test_tokens.py +0 -0
  182. {python_neva-4.1.0 → python_neva-5.1.0}/tests/support/test_results.py +0 -0
  183. {python_neva-4.1.0 → python_neva-5.1.0}/tests/testing/__init__.py +0 -0
  184. {python_neva-4.1.0 → python_neva-5.1.0}/tests/testing/test_application_reuse.py +0 -0
  185. {python_neva-4.1.0 → python_neva-5.1.0}/tests/testing/test_create_config_migration.py +0 -0
  186. {python_neva-4.1.0 → python_neva-5.1.0}/tests/testing/test_event_fake.py +0 -0
  187. {python_neva-4.1.0 → python_neva-5.1.0}/tests/testing/test_facade_restore.py +0 -0
  188. {python_neva-4.1.0 → python_neva-5.1.0}/tests/testing/test_fixtures.py +0 -0
  189. {python_neva-4.1.0 → python_neva-5.1.0}/tests/testing/test_refresh_database.py +0 -0
  190. {python_neva-4.1.0 → python_neva-5.1.0}/tests/testing/test_test_case.py +0 -0
@@ -48,6 +48,15 @@ pyrefly:
48
48
  script:
49
49
  - uv run --frozen pyrefly check
50
50
 
51
+ # The fragments shipped in the wheel cite the tests that prove their claims.
52
+ # Every cited path must still resolve, so a behaviour change that leaves the
53
+ # guidance behind fails here rather than reaching an agent as a wrong answer.
54
+ guidelines:
55
+ extends: [.uv_base, .default_rules]
56
+ stage: code_qa
57
+ script:
58
+ - uv run --frozen neva-boost check neva/guidelines/fragments
59
+
51
60
  ##### Test #####
52
61
  pytest:
53
62
  extends: [.uv_base, .default_rules]
@@ -1,3 +1,33 @@
1
+ ## 5.1.0 (2026-09-02)
2
+
3
+ ### ✨ Features
4
+
5
+ - **deps**: offer neva-otel through the otel extra
6
+
7
+ ### 📝💡 Documentation
8
+
9
+ - **guidelines**: add security, facades and factories fragments
10
+
11
+ ## 5.0.0 (2026-09-01)
12
+
13
+ ### 💥 Boom
14
+
15
+ - **obs**: drop OpenTelemetry and pyinstrument from the core
16
+
17
+ ### ✨ Features
18
+
19
+ - **obs**: resolve logging through named channels
20
+ - **deps**: offer neva-boost through the boost extra
21
+
22
+ ### 💚👷 CI & Build
23
+
24
+ - **guidelines**: fail when a fragment cites a test that is gone
25
+
26
+ ### 📝💡 Documentation
27
+
28
+ - **guidelines**: rule out resolving from the container in register()
29
+ - **guidelines**: cover logging channels, and where OTel now lives
30
+
1
31
  ## 4.1.0 (2026-08-31)
2
32
 
3
33
  ### ✅🤡🧪 Tests
@@ -1,25 +1,27 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-neva
3
- Version: 4.1.0
3
+ Version: 5.1.0
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: aiosqlite>=0.20.0
7
7
  Requires-Dist: asyncpg>=0.30.0
8
8
  Requires-Dist: cryptography>=46.0.3
9
9
  Requires-Dist: dishka>=1.10.0
10
- Requires-Dist: opentelemetry-instrumentation-sqlalchemy>=0.62b0
11
10
  Requires-Dist: pwdlib[argon2,bcrypt]>=0.3.0
12
11
  Requires-Dist: pydantic>=2.7
13
- Requires-Dist: pyinstrument>=5.1.1
14
12
  Requires-Dist: sqlalchemy[asyncio]>=2.0.0
15
13
  Requires-Dist: structlog>=25.5.0
16
14
  Requires-Dist: typing-extensions>=4.13
17
15
  Provides-Extra: asgi
18
16
  Requires-Dist: neva-asgi>=0.1.1; extra == 'asgi'
17
+ Provides-Extra: boost
18
+ Requires-Dist: neva-boost>=0.2.1; extra == 'boost'
19
19
  Provides-Extra: fastapi
20
20
  Requires-Dist: neva-fastapi>=1.1.1; extra == 'fastapi'
21
21
  Provides-Extra: faststream
22
22
  Requires-Dist: faststream>=0.6.6; extra == 'faststream'
23
+ Provides-Extra: otel
24
+ Requires-Dist: neva-otel>=0.2.0; extra == 'otel'
23
25
  Provides-Extra: polyfactory
24
26
  Requires-Dist: polyfactory>=3.1.0; extra == 'polyfactory'
25
27
  Provides-Extra: testing
@@ -47,17 +49,48 @@ independent repo, independently versioned and published.
47
49
 
48
50
  | Package | Repo / directory | Role | Status |
49
51
  | ----------------- | ------------------ | ------------------------------------------------------------------------------------- | ------------------------------- |
50
- | `python-neva` | `neva/` | **Core, framework-agnostic.** DI, service providers, facades, Result/Option, events, security, observability, SQLAlchemy database layer. | published ([PyPI](https://pypi.org/project/python-neva/)) |
52
+ | `python-neva` | `neva/` | **Core, framework-agnostic.** DI, service providers, facades, Result/Option, events, security, structured logging, SQLAlchemy database layer. | published ([PyPI](https://pypi.org/project/python-neva/)) |
51
53
  | `neva-fastapi` | `neva-fastapi/` | FastAPI integration — `App` extends `FastAPI` and wires dishka into routes. Pulled in via the `python-neva[fastapi]` extra. | published ([PyPI](https://pypi.org/project/neva-fastapi/)) |
52
54
  | `neva-asgi` | `neva-asgi/` | **ASGI middleware** — correlation IDs and per-request profiling. Pure ASGI, so both the HTTP and messaging integrations can consume it. Pulled in via the `python-neva[asgi]` extra. | published ([PyPI](https://pypi.org/project/neva-asgi/)) |
53
55
  | `neva-faststream` | `neva-faststream/` | FastStream (messaging) integration. | scaffolded, early placeholder |
54
56
  | `neva-auth` | `neva-auth/` | Authentication & identity toolkit — token-issuance engine, OAuth2 grants, guards, pluggable formats/backends. | core complete, OAuth-server slice in progress |
57
+ | `neva-otel` | `neva-otel/` | **OpenTelemetry** — SDK wiring, tracer and meter providers, exporters, samplers and instrumentors. Kept out of the core so nothing is committed to an observability backend. Pulled in via the `python-neva[otel]` extra. | published ([PyPI](https://pypi.org/project/neva-otel/)) |
58
+ | `neva-boost` | `neva-boost/` | **Agent guidelines** — composes each installed package's versioned guideline fragments into a project's agent configuration (Claude Code skills or `AGENTS.md`). Dev-time tooling; pulled in via the `python-neva[boost]` extra. | published ([PyPI](https://pypi.org/project/neva-boost/)) |
55
59
  | `neva-example` | `neva-example/` | Reference application consuming `python-neva[fastapi]`. Not published; demonstration + integration test bed. | unpublished |
56
60
 
57
61
  > When working across packages locally, each repo can be cloned as a sibling
58
62
  > directory; integration packages depend on `python-neva` from PyPI (or an
59
63
  > editable local path during development).
60
64
 
65
+ ## Logging
66
+
67
+ Logging is a set of named channels, each with its own driver, level and output —
68
+ Laravel's `Log`, configured from one `config/obs.py` namespace.
69
+
70
+ ```python
71
+ from neva.support.facade import Log
72
+
73
+ Log.info("order placed", order_id=42) # default channel
74
+ Log.channel("audit").warning("role granted", actor="root")
75
+ Log.bind(tenant="acme") # every channel, this context
76
+ ```
77
+
78
+ Drivers are `console`, `json`, `file`, `stack` and `null`. With no `config/obs.py`
79
+ at all, logging still works: a console channel on stdout at `DEBUG`.
80
+
81
+ Each channel carries its own structlog processor chain rather than going through
82
+ the process-global `structlog.configure()`, so one application's renderer cannot
83
+ leak into another in the same interpreter. `LogManager.processor(fn)` lets a
84
+ plugin add a processor to every chain — the seam `neva-otel` uses to put the
85
+ current span's ids on every record.
86
+
87
+ **Tracing and metrics are deliberately not here.** They live in `neva-otel`, so
88
+ the core carries no OpenTelemetry dependency and commits no application to an
89
+ observability backend.
90
+
91
+ Channel configuration, the driver keys and the failure modes are documented in
92
+ `neva/guidelines/fragments/observability.md`, which CI checks against the tests.
93
+
61
94
  ## Develop
62
95
 
63
96
  ```bash
@@ -103,10 +136,8 @@ rewrite the new tag with the rendered changelog as its annotation.
103
136
  ## Feature ideas
104
137
 
105
138
  - Feature-parity FastStream integration (now scaffolded as `neva-faststream`)
106
- - Finish cleaning up the core package (moving any remaining ASGI-dependent code into the appropriate integration packages)
107
139
  - Improved router registration (auto-discovery OR provider-based? both?)
108
140
  - Improved security tooling (performance improvements, better defaults, etc.)
109
- - Better OpenTelemetry integration
110
141
  - Improved factory module (based on Polyfactory)
111
142
  - Queue/Jobs system
112
143
  - CLI integration
@@ -18,17 +18,48 @@ independent repo, independently versioned and published.
18
18
 
19
19
  | Package | Repo / directory | Role | Status |
20
20
  | ----------------- | ------------------ | ------------------------------------------------------------------------------------- | ------------------------------- |
21
- | `python-neva` | `neva/` | **Core, framework-agnostic.** DI, service providers, facades, Result/Option, events, security, observability, SQLAlchemy database layer. | published ([PyPI](https://pypi.org/project/python-neva/)) |
21
+ | `python-neva` | `neva/` | **Core, framework-agnostic.** DI, service providers, facades, Result/Option, events, security, structured logging, SQLAlchemy database layer. | published ([PyPI](https://pypi.org/project/python-neva/)) |
22
22
  | `neva-fastapi` | `neva-fastapi/` | FastAPI integration — `App` extends `FastAPI` and wires dishka into routes. Pulled in via the `python-neva[fastapi]` extra. | published ([PyPI](https://pypi.org/project/neva-fastapi/)) |
23
23
  | `neva-asgi` | `neva-asgi/` | **ASGI middleware** — correlation IDs and per-request profiling. Pure ASGI, so both the HTTP and messaging integrations can consume it. Pulled in via the `python-neva[asgi]` extra. | published ([PyPI](https://pypi.org/project/neva-asgi/)) |
24
24
  | `neva-faststream` | `neva-faststream/` | FastStream (messaging) integration. | scaffolded, early placeholder |
25
25
  | `neva-auth` | `neva-auth/` | Authentication & identity toolkit — token-issuance engine, OAuth2 grants, guards, pluggable formats/backends. | core complete, OAuth-server slice in progress |
26
+ | `neva-otel` | `neva-otel/` | **OpenTelemetry** — SDK wiring, tracer and meter providers, exporters, samplers and instrumentors. Kept out of the core so nothing is committed to an observability backend. Pulled in via the `python-neva[otel]` extra. | published ([PyPI](https://pypi.org/project/neva-otel/)) |
27
+ | `neva-boost` | `neva-boost/` | **Agent guidelines** — composes each installed package's versioned guideline fragments into a project's agent configuration (Claude Code skills or `AGENTS.md`). Dev-time tooling; pulled in via the `python-neva[boost]` extra. | published ([PyPI](https://pypi.org/project/neva-boost/)) |
26
28
  | `neva-example` | `neva-example/` | Reference application consuming `python-neva[fastapi]`. Not published; demonstration + integration test bed. | unpublished |
27
29
 
28
30
  > When working across packages locally, each repo can be cloned as a sibling
29
31
  > directory; integration packages depend on `python-neva` from PyPI (or an
30
32
  > editable local path during development).
31
33
 
34
+ ## Logging
35
+
36
+ Logging is a set of named channels, each with its own driver, level and output —
37
+ Laravel's `Log`, configured from one `config/obs.py` namespace.
38
+
39
+ ```python
40
+ from neva.support.facade import Log
41
+
42
+ Log.info("order placed", order_id=42) # default channel
43
+ Log.channel("audit").warning("role granted", actor="root")
44
+ Log.bind(tenant="acme") # every channel, this context
45
+ ```
46
+
47
+ Drivers are `console`, `json`, `file`, `stack` and `null`. With no `config/obs.py`
48
+ at all, logging still works: a console channel on stdout at `DEBUG`.
49
+
50
+ Each channel carries its own structlog processor chain rather than going through
51
+ the process-global `structlog.configure()`, so one application's renderer cannot
52
+ leak into another in the same interpreter. `LogManager.processor(fn)` lets a
53
+ plugin add a processor to every chain — the seam `neva-otel` uses to put the
54
+ current span's ids on every record.
55
+
56
+ **Tracing and metrics are deliberately not here.** They live in `neva-otel`, so
57
+ the core carries no OpenTelemetry dependency and commits no application to an
58
+ observability backend.
59
+
60
+ Channel configuration, the driver keys and the failure modes are documented in
61
+ `neva/guidelines/fragments/observability.md`, which CI checks against the tests.
62
+
32
63
  ## Develop
33
64
 
34
65
  ```bash
@@ -74,10 +105,8 @@ rewrite the new tag with the rendered changelog as its annotation.
74
105
  ## Feature ideas
75
106
 
76
107
  - Feature-parity FastStream integration (now scaffolded as `neva-faststream`)
77
- - Finish cleaning up the core package (moving any remaining ASGI-dependent code into the appropriate integration packages)
78
108
  - Improved router registration (auto-discovery OR provider-based? both?)
79
109
  - Improved security tooling (performance improvements, better defaults, etc.)
80
- - Better OpenTelemetry integration
81
110
  - Improved factory module (based on Polyfactory)
82
111
  - Queue/Jobs system
83
112
  - CLI integration
@@ -7,7 +7,6 @@ from dataclasses import dataclass, field
7
7
  from typing import final
8
8
 
9
9
  from sqlalchemy.ext import asyncio
10
- from structlog.stdlib import get_logger
11
10
 
12
11
  from neva.database.transaction import BoundTransaction, TransactionState
13
12
  from neva.obs import LogManager
@@ -87,7 +86,7 @@ class ConnectionManager:
87
86
  self,
88
87
  name: str,
89
88
  tx_context: TransactionContext,
90
- logger: LogManager | None,
89
+ logger: LogManager,
91
90
  engine: asyncio.AsyncEngine,
92
91
  ) -> None:
93
92
  self.name = name
@@ -149,12 +148,10 @@ class ConnectionManager:
149
148
  )
150
149
  label = "commit" if committed else "rollback"
151
150
  # A post-commit callback cannot un-commit the transaction, so its
152
- # failure is reported rather than raised. It must never be lost: fall
153
- # back to a module logger when the manager has none.
154
- logger = self.logger if self.logger is not None else get_logger(__name__)
151
+ # failure is reported rather than raised.
155
152
  for result in results:
156
153
  if result.is_err:
157
- logger.error(
154
+ self.logger.error(
158
155
  f"{label} callback failed",
159
156
  error=result.err().unwrap(),
160
157
  connection=self.name,
@@ -10,7 +10,6 @@ from neva.database.config import ConnectionConfig
10
10
  from neva.database.connection import ConnectionManager, TransactionContext
11
11
  from neva.database.transaction import BoundTransaction
12
12
  from neva.obs import LogManager
13
- from neva.obs.instrumentation.sqlalchemy import instrument
14
13
  from neva.support import Nothing, Option, Some, from_optional
15
14
 
16
15
 
@@ -36,7 +35,6 @@ class DatabaseManager:
36
35
  name: The connection name.
37
36
  engine: The async engine.
38
37
  """
39
- instrument(engine)
40
38
  self._connections[name] = ConnectionManager(
41
39
  name,
42
40
  self._tx_context,
@@ -0,0 +1,95 @@
1
+ ---
2
+ id: facades
3
+ title: Facades
4
+ requires: python-neva>=5.0
5
+ triggers: [Log facade, DB facade, static access to a service, adding a facade, facade root, why does my facade raise AttributeError, pyi stub]
6
+ priority: 25
7
+ verified_by: [tests/arch/test_facade_resolution.py, tests/arch/test_facade_root_nesting.py, tests/testing/test_facade_restore.py]
8
+ ---
9
+
10
+ # Facades
11
+
12
+ A static front for a container-bound service. `Log.info(...)` resolves `LogManager` from the
13
+ application and calls `info` on it.
14
+
15
+ ```python
16
+ from neva.support.facade import App, Config, Crypt, DB, Event, Hash, Log
17
+ ```
18
+
19
+ Each names one service through `get_facade_accessor()`. Nothing else is special: a facade holds
20
+ no state and caches no instance.
21
+
22
+ ## When to use one
23
+
24
+ Use a facade for genuinely cross-cutting access — logging, config, the ambient transaction.
25
+ **Inject the service anywhere it is part of a unit's contract.** A class taking
26
+ `DatabaseManager` in its constructor says what it needs and can be unit-tested by passing a
27
+ double; the same class reaching for `DB` hides the dependency and can only be tested by
28
+ installing a global. Actions, repositories and services take constructor parameters.
29
+
30
+ ## Resolution is per access
31
+
32
+ `FacadeMeta.__getattr__` resolves the root on **every** attribute access. A swap installed
33
+ mid-test therefore takes effect immediately, and a facade never pins a stale instance across a
34
+ rebuild.
35
+
36
+ The cost is that failures arrive as `AttributeError`, not as a `Result`:
37
+
38
+ | Situation | What you get |
39
+ | --- | --- |
40
+ | No application set | `AttributeError: A facade root (App instance) has not been set for <Facade>. Call Facade.set_facade_application(app) first.` |
41
+ | Service not bound | `AttributeError` carrying the container's resolution error. |
42
+ | Attribute the service lacks | `AttributeError: 'LogManager' object has no attribute '...'` |
43
+
44
+ An `AttributeError` from a facade almost always means a missing provider, not a typo. Check the
45
+ `providers` config namespace first.
46
+
47
+ **A swap bypasses the container but not the root check.** `DB.swap(manager)` outside a booted
48
+ application still raises "a facade root has not been set" — the double is only consulted once
49
+ an application is present. A test installing a double therefore needs an application anyway,
50
+ which is what `TestCase` and the `application` fixture provide.
51
+
52
+ ## The facade root
53
+
54
+ `Application.lifespan()` sets the root on entry and restores it on exit — you never call
55
+ `set_facade_application` yourself outside a bespoke harness.
56
+
57
+ The root is a single process-global slot, and teardown **hands it back rather than clearing
58
+ it**: an application booted inside a longer-lived one restores the outer application on exit,
59
+ and only the outermost teardown leaves it unset. Without that, a short-lived inner application
60
+ would disarm every facade for whatever the outer one still had to do — exactly what a test
61
+ harness does when its application outlives a single test.
62
+
63
+ ## Type checking needs a stub
64
+
65
+ Forwarding through `__getattr__` is invisible to a type checker, so every facade ships a
66
+ sibling `.pyi` declaring its methods as classmethods (`neva/support/facade/log.pyi`). Two
67
+ consequences:
68
+
69
+ - **Adding a method to a service does not add it to the facade's public API.** Update the stub
70
+ in the same change, or callers get an error on a call that works at runtime.
71
+ - A stub can drift the other way too — the events fragment documents one such gap, where the
72
+ dispatcher accepts a list its contract does not declare.
73
+
74
+ ## Adding a facade
75
+
76
+ ```python
77
+ class Cache(Facade):
78
+ @classmethod
79
+ @override
80
+ def get_facade_accessor(cls) -> type:
81
+ return CacheManager
82
+ ```
83
+
84
+ Import the service inside the method when the module would otherwise import at package-import
85
+ time — the shipped facades do this to keep `neva.support.facade` cheap. Bind the service in a
86
+ provider, write the `.pyi`, and export it from your facade package.
87
+
88
+ ## Testing
89
+
90
+ Doubles are installed on the concrete facade, never on `Facade` itself, and live in one global
91
+ registry keyed by facade class. See the testing fragment for the full table — `fake()`,
92
+ `swap()`, `spy()`, `faking()`, `restore()`, `restore_all()`.
93
+
94
+ `TestCase` calls `Facade.restore_all()` after every test. Outside it, prefer `faking()` so a
95
+ failing assertion cannot leak a double into the next test.
@@ -0,0 +1,79 @@
1
+ ---
2
+ id: factories
3
+ title: Model factories
4
+ requires: python-neva>=5.0
5
+ triggers: [building test data, ModelFactory, polyfactory, seeding a model, create_async, factory for a SQLAlchemy model]
6
+ priority: 65
7
+ verified_by: [tests/polyfactory/test_model_factory.py]
8
+ ---
9
+
10
+ # Model factories
11
+
12
+ `ModelFactory` builds SQLAlchemy models for tests and seeds, persisting them through the
13
+ **caller's** transaction. It is a thin base over Polyfactory's `SQLAlchemyFactory`.
14
+
15
+ Ships behind an extra — `python-neva[polyfactory]`. Importing `neva.polyfactory` without it
16
+ fails, so it belongs in a dev dependency group, not in application code paths.
17
+
18
+ ```python
19
+ from neva.polyfactory import ModelFactory
20
+
21
+ class ActorFactory(ModelFactory[Actor]):
22
+ __model__ = Actor
23
+ ```
24
+
25
+ ## Persisting
26
+
27
+ `create_async()` and `create_batch_async(n)` write through the ambient session — they read it
28
+ from the `DB` facade rather than opening a transaction of their own, and they **flush without
29
+ committing**. The enclosing block still owns settling, so a factory call inside a rolled-back
30
+ test leaves nothing behind.
31
+
32
+ ```python
33
+ async with DB.begin() as tx:
34
+ actor = await ActorFactory.create_async()
35
+ actors = await ActorFactory.create_batch_async(3)
36
+ ```
37
+
38
+ **There must be an open transaction.** Outside one, `create_async` raises `UnwrapError` from
39
+ the unwrapped `DB.session()` rather than quietly opening its own — the same rule the database
40
+ fragment states for actions. Under `RefreshDatabase` the wrapper transaction already satisfies
41
+ this.
42
+
43
+ `build()` and `batch()` are inherited unchanged and touch no database. Use them when the model
44
+ never needs to be persisted.
45
+
46
+ ## Relationships are not populated — but foreign keys still are
47
+
48
+ `__set_relationships__` is `False` on the base, so a factory never sets a relationship
49
+ attribute and never invents the related row.
50
+
51
+ **It does still fill the foreign-key column**, because that is an ordinary column: a required
52
+ `author_id` comes back a random integer pointing at no row. SQLite does not check the
53
+ constraint by default and lets it through; Postgres rejects it. This is the failure to expect
54
+ when a factory that passed locally breaks against a real database.
55
+
56
+ Always supply the parent — the relationship or the id — rather than letting the factory invent
57
+ one:
58
+
59
+ ```python
60
+ author = await AuthorFactory.create_async()
61
+ book = await BookFactory.create_async(author=author) # or author_id=author.id
62
+ ```
63
+
64
+ For a nullable key with no parent in the scenario, say so explicitly:
65
+
66
+ ```python
67
+ book = await BookFactory.create_async(author_id=None)
68
+ ```
69
+
70
+ Set `__set_relationships__ = True` on one factory where a fixture genuinely wants the whole
71
+ graph built for it.
72
+
73
+ ## Rules
74
+
75
+ - One factory per model, named `<Model>Factory`, beside the component's other test support.
76
+ - Never call `session.commit()` from a factory or a seeder — the caller's block settles.
77
+ - Do not give a factory its own `DB.begin()`; it inherits the caller's unit of work.
78
+ - Override a field with a keyword argument rather than editing the instance afterwards, so the
79
+ value is present when the row is flushed.
@@ -0,0 +1,130 @@
1
+ ---
2
+ id: observability
3
+ title: Observability
4
+ requires: python-neva>=5.0
5
+ triggers: [logging, Log facade, log channel, structured logging, log level, JSON logs, audit log, obs config, tracing, OpenTelemetry, metrics, correlation id]
6
+ priority: 55
7
+ verified_by: [tests/obs/test_channels.py, tests/obs/test_resolver.py, tests/obs/test_manager.py, tests/obs/test_facade.py, tests/obs/test_provider_hook.py, tests/support/test_strategy.py]
8
+ ---
9
+
10
+ # Observability
11
+
12
+ The core ships **logging only**. Tracing, metrics, exporters and instrumentors live in the
13
+ `neva-otel` plugin (`python-neva[otel]`), so installing the core commits an application to no
14
+ observability backend. There is no `Trace` facade, no tracer, and no OpenTelemetry dependency
15
+ here — do not add one. Asked for tracing, install the plugin; never reach for
16
+ `opentelemetry-*` inside this package.
17
+
18
+ ## Channels
19
+
20
+ Logging is a set of named channels, each with its own driver, level and output. One
21
+ `config/obs.py` namespace configures them.
22
+
23
+ ```python
24
+ config = {
25
+ "logging": {
26
+ "default": "stdout",
27
+ "channels": {
28
+ "stdout": {"driver": "console", "level": "DEBUG"},
29
+ "json": {"driver": "json", "stream": "stderr", "level": "INFO"},
30
+ "audit": {"driver": "file", "path": "var/audit.log"},
31
+ "both": {"driver": "stack", "channels": ["json", "audit"]},
32
+ "quiet": {"driver": "null"},
33
+ },
34
+ },
35
+ }
36
+ ```
37
+
38
+ | Driver | Writes | Driver-specific keys |
39
+ | --- | --- | --- |
40
+ | `console` | human-readable, to a stream | `stream` |
41
+ | `json` | one JSON object per line, to a stream | `stream` |
42
+ | `file` | JSON lines appended to a path | `path` (required) |
43
+ | `stack` | fans one record out to other channels | `channels` (required) |
44
+ | `null` | nothing | — |
45
+
46
+ `level` is one of `DEBUG`/`INFO`/`WARNING`/`ERROR`/`CRITICAL`, default `DEBUG`. `stream` is
47
+ `stdout` (default) or `stderr`. A `file` channel creates parent directories and holds its
48
+ handle open for the process's life.
49
+
50
+ **With no `config/obs.py` at all**, logging still works: a `console` channel named `stdout` on
51
+ stdout at `DEBUG`.
52
+
53
+ ## Writing
54
+
55
+ ```python
56
+ from neva.support.facade import Log
57
+
58
+ Log.info("order placed", order_id=42) # default channel
59
+ Log.channel("audit").warning("role granted", actor="root")
60
+ ```
61
+
62
+ Level methods are `debug`, `info`, `warning`, `error`, `critical` and `exception`, and go to the
63
+ default channel. **Use `Log.exception(...)` inside an `except` block** — it attaches the
64
+ traceback, which `Log.error(str(e))` throws away. Everything after the message is structured
65
+ context, not format arguments — never build the message with an f-string when the value belongs
66
+ in a field.
67
+
68
+ `Log.channel(name)` **raises `UnwrapError`** on a name no channel is configured for, carrying
69
+ the reason. A typo is a configuration mistake, and serving the default instead would hide it
70
+ behind working output. Where the failure has to be a value, go through the resolver:
71
+ `Log.channels` returns `Option[ChannelResolver]` — `Nothing` for a manager built with no
72
+ application — and the resolver's `use(name)` returns `Result[Channel, str]`.
73
+
74
+ ```python
75
+ channel = Log.channels.and_then(lambda r: r.use("audit").ok())
76
+ ```
77
+
78
+ Three failures are told apart by their message: no channel of that name, an unknown `driver`
79
+ (the error lists the drivers that are registered), and a `stack` channel that reaches itself,
80
+ which reports being "defined in terms of itself" rather than recursing.
81
+
82
+ ## Ambient context
83
+
84
+ ```python
85
+ Log.bind(tenant="acme") # every subsequent record in this context, every channel
86
+ Log.unbind("tenant")
87
+ ```
88
+
89
+ Backed by structlog's contextvars, so a binding follows the async task rather than the
90
+ channel. This is how a request's correlation ID reaches log lines without being passed
91
+ around — `neva-asgi`'s middleware binds it the same way.
92
+
93
+ ## Enriching records from a plugin
94
+
95
+ `LogManager.processor(fn)` appends a structlog processor to **every** channel's chain. This is
96
+ the seam a plugin adds derived fields through — `neva-otel` injects the current span's ids
97
+ this way.
98
+
99
+ ```python
100
+ class MyProvider(ServiceProvider):
101
+ @asynccontextmanager
102
+ async def lifespan(self) -> AsyncIterator[None]:
103
+ manager = (await self.app.make_async(LogManager)).unwrap()
104
+ _ = manager.processor(add_my_field)
105
+ yield
106
+ ```
107
+
108
+ **Call it from `lifespan()`, never from `register()`.** Resolving anything during
109
+ registration hands back an instance the booted application does not use, so a processor
110
+ registered there silently never runs. See the service-providers fragment.
111
+
112
+ Records emitted by base providers' own startup are not enriched — their lifespans run before
113
+ a plugin's. Everything after is. Already-built channels are discarded on registration, so the
114
+ next resolution picks the processor up.
115
+
116
+ Custom drivers register the same way, through `resolver.driver(name, builder)` — the
117
+ resolver is a `StrategyResolver`, described in the service-providers fragment.
118
+
119
+ ## Rules
120
+
121
+ - **Never call `structlog.configure()`.** It is process-global; channels carry their own
122
+ chains precisely so one application's renderer cannot leak into another in the same
123
+ interpreter. Configuring it globally breaks that isolation and every channel's level.
124
+ - **`merge_contextvars` must lead every chain.** A custom driver that omits it silently drops
125
+ the correlation ID from every line it renders.
126
+ - **A broken default channel fails at boot**, not at the first log call — the framework logs
127
+ during provider startup, so a `file` channel with no `path` surfaces there. The error names
128
+ the channel and the reason.
129
+ - `LogManager()` with no application is legitimate and serves one console channel; it is what
130
+ a test needing somewhere for records to go should use.