python-neva 4.1.0__tar.gz → 5.0.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 (181) hide show
  1. {python_neva-4.1.0 → python_neva-5.0.0}/.gitlab-ci.yml +9 -0
  2. {python_neva-4.1.0 → python_neva-5.0.0}/CHANGELOG.md +20 -0
  3. {python_neva-4.1.0 → python_neva-5.0.0}/PKG-INFO +47 -6
  4. {python_neva-4.1.0 → python_neva-5.0.0}/README.md +44 -3
  5. {python_neva-4.1.0 → python_neva-5.0.0}/neva/database/manager.py +0 -2
  6. python_neva-5.0.0/neva/guidelines/fragments/observability.md +116 -0
  7. {python_neva-4.1.0 → python_neva-5.0.0}/neva/guidelines/fragments/service-providers.md +34 -2
  8. python_neva-5.0.0/neva/obs/__init__.py +19 -0
  9. python_neva-5.0.0/neva/obs/config.py +37 -0
  10. python_neva-5.0.0/neva/obs/logging/__init__.py +22 -0
  11. python_neva-5.0.0/neva/obs/logging/channels.py +310 -0
  12. python_neva-5.0.0/neva/obs/logging/contracts.py +40 -0
  13. python_neva-5.0.0/neva/obs/logging/manager.py +196 -0
  14. python_neva-5.0.0/neva/obs/logging/provider.py +36 -0
  15. python_neva-5.0.0/neva/obs/logging/resolver.py +138 -0
  16. {python_neva-4.1.0 → python_neva-5.0.0}/neva/support/facade/log.pyi +40 -0
  17. {python_neva-4.1.0 → python_neva-5.0.0}/pyproject.toml +2 -3
  18. python_neva-5.0.0/tests/arch/test_register_resolution.py +108 -0
  19. python_neva-5.0.0/tests/obs/conftest.py +133 -0
  20. python_neva-5.0.0/tests/obs/test_channels.py +213 -0
  21. python_neva-5.0.0/tests/obs/test_facade.py +90 -0
  22. python_neva-5.0.0/tests/obs/test_manager.py +160 -0
  23. python_neva-5.0.0/tests/obs/test_provider_hook.py +107 -0
  24. python_neva-5.0.0/tests/obs/test_resolver.py +127 -0
  25. python_neva-5.0.0/tests/support/__init__.py +0 -0
  26. {python_neva-4.1.0 → python_neva-5.0.0}/uv.lock +20 -62
  27. python_neva-4.1.0/neva/obs/__init__.py +0 -9
  28. python_neva-4.1.0/neva/obs/instrumentation/__init__.py +0 -1
  29. python_neva-4.1.0/neva/obs/instrumentation/sqlalchemy.py +0 -15
  30. python_neva-4.1.0/neva/obs/logging/__init__.py +0 -10
  31. python_neva-4.1.0/neva/obs/logging/manager.py +0 -90
  32. python_neva-4.1.0/neva/obs/logging/provider.py +0 -26
  33. {python_neva-4.1.0 → python_neva-5.0.0}/.claude/settings.local.json +0 -0
  34. {python_neva-4.1.0 → python_neva-5.0.0}/.envrc +0 -0
  35. {python_neva-4.1.0 → python_neva-5.0.0}/.gitignore +0 -0
  36. {python_neva-4.1.0 → python_neva-5.0.0}/.pre-commit-config.yaml +0 -0
  37. {python_neva-4.1.0 → python_neva-5.0.0}/.python-version +0 -0
  38. {python_neva-4.1.0 → python_neva-5.0.0}/CLAUDE.md +0 -0
  39. {python_neva-4.1.0 → python_neva-5.0.0}/neva/arch/__init__.py +0 -0
  40. {python_neva-4.1.0 → python_neva-5.0.0}/neva/arch/application.py +0 -0
  41. {python_neva-4.1.0 → python_neva-5.0.0}/neva/arch/config.py +0 -0
  42. {python_neva-4.1.0 → python_neva-5.0.0}/neva/arch/facade.py +0 -0
  43. {python_neva-4.1.0 → python_neva-5.0.0}/neva/arch/integrations/__init__.py +0 -0
  44. {python_neva-4.1.0 → python_neva-5.0.0}/neva/arch/integrations/faststream.py +0 -0
  45. {python_neva-4.1.0 → python_neva-5.0.0}/neva/arch/markers.py +0 -0
  46. {python_neva-4.1.0 → python_neva-5.0.0}/neva/arch/py.typed +0 -0
  47. {python_neva-4.1.0 → python_neva-5.0.0}/neva/arch/scopes.py +0 -0
  48. {python_neva-4.1.0 → python_neva-5.0.0}/neva/arch/service_provider.py +0 -0
  49. {python_neva-4.1.0 → python_neva-5.0.0}/neva/config/__init__.py +0 -0
  50. {python_neva-4.1.0 → python_neva-5.0.0}/neva/config/base_providers.py +0 -0
  51. {python_neva-4.1.0 → python_neva-5.0.0}/neva/config/loader.py +0 -0
  52. {python_neva-4.1.0 → python_neva-5.0.0}/neva/config/py.typed +0 -0
  53. {python_neva-4.1.0 → python_neva-5.0.0}/neva/config/repository.py +0 -0
  54. {python_neva-4.1.0 → python_neva-5.0.0}/neva/database/__init__.py +0 -0
  55. {python_neva-4.1.0 → python_neva-5.0.0}/neva/database/config.py +0 -0
  56. {python_neva-4.1.0 → python_neva-5.0.0}/neva/database/connection.py +0 -0
  57. {python_neva-4.1.0 → python_neva-5.0.0}/neva/database/provider.py +0 -0
  58. {python_neva-4.1.0 → python_neva-5.0.0}/neva/database/py.typed +0 -0
  59. {python_neva-4.1.0 → python_neva-5.0.0}/neva/database/transaction.py +0 -0
  60. {python_neva-4.1.0 → python_neva-5.0.0}/neva/events/__init__.py +0 -0
  61. {python_neva-4.1.0 → python_neva-5.0.0}/neva/events/contracts/__init__.py +0 -0
  62. {python_neva-4.1.0 → python_neva-5.0.0}/neva/events/contracts/dispatcher.py +0 -0
  63. {python_neva-4.1.0 → python_neva-5.0.0}/neva/events/contracts/event.py +0 -0
  64. {python_neva-4.1.0 → python_neva-5.0.0}/neva/events/contracts/handler.py +0 -0
  65. {python_neva-4.1.0 → python_neva-5.0.0}/neva/events/contracts/listener.py +0 -0
  66. {python_neva-4.1.0 → python_neva-5.0.0}/neva/events/dispatcher.py +0 -0
  67. {python_neva-4.1.0 → python_neva-5.0.0}/neva/events/event.py +0 -0
  68. {python_neva-4.1.0 → python_neva-5.0.0}/neva/events/event_registry.py +0 -0
  69. {python_neva-4.1.0 → python_neva-5.0.0}/neva/events/listener.py +0 -0
  70. {python_neva-4.1.0 → python_neva-5.0.0}/neva/events/policy.py +0 -0
  71. {python_neva-4.1.0 → python_neva-5.0.0}/neva/events/provider.py +0 -0
  72. {python_neva-4.1.0 → python_neva-5.0.0}/neva/events/py.typed +0 -0
  73. {python_neva-4.1.0 → python_neva-5.0.0}/neva/guidelines/__init__.py +0 -0
  74. {python_neva-4.1.0 → python_neva-5.0.0}/neva/guidelines/fragments/configuration.md +0 -0
  75. {python_neva-4.1.0 → python_neva-5.0.0}/neva/guidelines/fragments/database-transactions.md +0 -0
  76. {python_neva-4.1.0 → python_neva-5.0.0}/neva/guidelines/fragments/events.md +0 -0
  77. {python_neva-4.1.0 → python_neva-5.0.0}/neva/guidelines/fragments/result-option.md +0 -0
  78. {python_neva-4.1.0 → python_neva-5.0.0}/neva/guidelines/fragments/testing.md +0 -0
  79. {python_neva-4.1.0 → python_neva-5.0.0}/neva/guidelines/manifest.py +0 -0
  80. {python_neva-4.1.0 → python_neva-5.0.0}/neva/guidelines/py.typed +0 -0
  81. {python_neva-4.1.0 → python_neva-5.0.0}/neva/obs/py.typed +0 -0
  82. {python_neva-4.1.0 → python_neva-5.0.0}/neva/polyfactory/__init__.py +0 -0
  83. {python_neva-4.1.0 → python_neva-5.0.0}/neva/polyfactory/factories.py +0 -0
  84. {python_neva-4.1.0 → python_neva-5.0.0}/neva/polyfactory/persistence.py +0 -0
  85. {python_neva-4.1.0 → python_neva-5.0.0}/neva/polyfactory/py.typed +0 -0
  86. {python_neva-4.1.0 → python_neva-5.0.0}/neva/security/__init__.py +0 -0
  87. {python_neva-4.1.0 → python_neva-5.0.0}/neva/security/encryption/__init__.py +0 -0
  88. {python_neva-4.1.0 → python_neva-5.0.0}/neva/security/encryption/encrypter.py +0 -0
  89. {python_neva-4.1.0 → python_neva-5.0.0}/neva/security/encryption/protocol.py +0 -0
  90. {python_neva-4.1.0 → python_neva-5.0.0}/neva/security/hashing/__init__.py +0 -0
  91. {python_neva-4.1.0 → python_neva-5.0.0}/neva/security/hashing/config.py +0 -0
  92. {python_neva-4.1.0 → python_neva-5.0.0}/neva/security/hashing/hash_manager.py +0 -0
  93. {python_neva-4.1.0 → python_neva-5.0.0}/neva/security/hashing/hashers/__init__.py +0 -0
  94. {python_neva-4.1.0 → python_neva-5.0.0}/neva/security/hashing/hashers/argon2.py +0 -0
  95. {python_neva-4.1.0 → python_neva-5.0.0}/neva/security/hashing/hashers/bcrypt.py +0 -0
  96. {python_neva-4.1.0 → python_neva-5.0.0}/neva/security/hashing/hashers/protocol.py +0 -0
  97. {python_neva-4.1.0 → python_neva-5.0.0}/neva/security/provider.py +0 -0
  98. {python_neva-4.1.0 → python_neva-5.0.0}/neva/security/py.typed +0 -0
  99. {python_neva-4.1.0 → python_neva-5.0.0}/neva/security/tokens/__init__.py +0 -0
  100. {python_neva-4.1.0 → python_neva-5.0.0}/neva/security/tokens/generate_token.py +0 -0
  101. {python_neva-4.1.0 → python_neva-5.0.0}/neva/security/tokens/hash_token.py +0 -0
  102. {python_neva-4.1.0 → python_neva-5.0.0}/neva/security/tokens/verify_token.py +0 -0
  103. {python_neva-4.1.0 → python_neva-5.0.0}/neva/support/__init__.py +0 -0
  104. {python_neva-4.1.0 → python_neva-5.0.0}/neva/support/accessors.py +0 -0
  105. {python_neva-4.1.0 → python_neva-5.0.0}/neva/support/facade/__init__.py +0 -0
  106. {python_neva-4.1.0 → python_neva-5.0.0}/neva/support/facade/app.py +0 -0
  107. {python_neva-4.1.0 → python_neva-5.0.0}/neva/support/facade/app.pyi +0 -0
  108. {python_neva-4.1.0 → python_neva-5.0.0}/neva/support/facade/config.py +0 -0
  109. {python_neva-4.1.0 → python_neva-5.0.0}/neva/support/facade/config.pyi +0 -0
  110. {python_neva-4.1.0 → python_neva-5.0.0}/neva/support/facade/crypt.py +0 -0
  111. {python_neva-4.1.0 → python_neva-5.0.0}/neva/support/facade/crypt.pyi +0 -0
  112. {python_neva-4.1.0 → python_neva-5.0.0}/neva/support/facade/db.py +0 -0
  113. {python_neva-4.1.0 → python_neva-5.0.0}/neva/support/facade/db.pyi +0 -0
  114. {python_neva-4.1.0 → python_neva-5.0.0}/neva/support/facade/event.py +0 -0
  115. {python_neva-4.1.0 → python_neva-5.0.0}/neva/support/facade/event.pyi +0 -0
  116. {python_neva-4.1.0 → python_neva-5.0.0}/neva/support/facade/hash.py +0 -0
  117. {python_neva-4.1.0 → python_neva-5.0.0}/neva/support/facade/hash.pyi +0 -0
  118. {python_neva-4.1.0 → python_neva-5.0.0}/neva/support/facade/log.py +0 -0
  119. {python_neva-4.1.0 → python_neva-5.0.0}/neva/support/py.typed +0 -0
  120. {python_neva-4.1.0 → python_neva-5.0.0}/neva/support/results.py +0 -0
  121. {python_neva-4.1.0 → python_neva-5.0.0}/neva/support/strategy.py +0 -0
  122. {python_neva-4.1.0 → python_neva-5.0.0}/neva/support/strconv.py +0 -0
  123. {python_neva-4.1.0 → python_neva-5.0.0}/neva/support/time.py +0 -0
  124. {python_neva-4.1.0 → python_neva-5.0.0}/neva/testing/__init__.py +0 -0
  125. {python_neva-4.1.0 → python_neva-5.0.0}/neva/testing/fakes.py +0 -0
  126. {python_neva-4.1.0 → python_neva-5.0.0}/neva/testing/fixtures.py +0 -0
  127. {python_neva-4.1.0 → python_neva-5.0.0}/neva/testing/py.typed +0 -0
  128. {python_neva-4.1.0 → python_neva-5.0.0}/neva/testing/test_case.py +0 -0
  129. {python_neva-4.1.0 → python_neva-5.0.0}/ruff.toml +0 -0
  130. {python_neva-4.1.0 → python_neva-5.0.0}/scripts/retag-with-changelog.sh +0 -0
  131. {python_neva-4.1.0 → python_neva-5.0.0}/tests/__init__.py +0 -0
  132. {python_neva-4.1.0 → python_neva-5.0.0}/tests/arch/__init__.py +0 -0
  133. {python_neva-4.1.0 → python_neva-5.0.0}/tests/arch/test_cache.py +0 -0
  134. {python_neva-4.1.0 → python_neva-5.0.0}/tests/arch/test_config_shapes.py +0 -0
  135. {python_neva-4.1.0 → python_neva-5.0.0}/tests/arch/test_context.py +0 -0
  136. {python_neva-4.1.0 → python_neva-5.0.0}/tests/arch/test_extends.py +0 -0
  137. {python_neva-4.1.0 → python_neva-5.0.0}/tests/arch/test_facade_root_nesting.py +0 -0
  138. {python_neva-4.1.0 → python_neva-5.0.0}/tests/arch/test_lifetimes.py +0 -0
  139. {python_neva-4.1.0 → python_neva-5.0.0}/tests/arch/test_registration.py +0 -0
  140. {python_neva-4.1.0 → python_neva-5.0.0}/tests/arch/test_scope.py +0 -0
  141. {python_neva-4.1.0 → python_neva-5.0.0}/tests/config/__init__.py +0 -0
  142. {python_neva-4.1.0 → python_neva-5.0.0}/tests/config/test_config_path_resolution.py +0 -0
  143. {python_neva-4.1.0 → python_neva-5.0.0}/tests/config/test_loader.py +0 -0
  144. {python_neva-4.1.0 → python_neva-5.0.0}/tests/config/test_repository.py +0 -0
  145. {python_neva-4.1.0 → python_neva-5.0.0}/tests/conftest.py +0 -0
  146. {python_neva-4.1.0 → python_neva-5.0.0}/tests/database/__init__.py +0 -0
  147. {python_neva-4.1.0 → python_neva-5.0.0}/tests/database/test_connection_manager.py +0 -0
  148. {python_neva-4.1.0 → python_neva-5.0.0}/tests/database/test_database_manager.py +0 -0
  149. {python_neva-4.1.0 → python_neva-5.0.0}/tests/database/test_detached_lifespan.py +0 -0
  150. {python_neva-4.1.0 → python_neva-5.0.0}/tests/database/test_edge_cases.py +0 -0
  151. {python_neva-4.1.0 → python_neva-5.0.0}/tests/database/test_multi_connection.py +0 -0
  152. {python_neva-4.1.0 → python_neva-5.0.0}/tests/database/test_sqlalchemy_integration.py +0 -0
  153. {python_neva-4.1.0 → python_neva-5.0.0}/tests/database/test_transaction.py +0 -0
  154. {python_neva-4.1.0 → python_neva-5.0.0}/tests/database/test_transaction_callbacks.py +0 -0
  155. {python_neva-4.1.0 → python_neva-5.0.0}/tests/database/test_transaction_context.py +0 -0
  156. {python_neva-4.1.0 → python_neva-5.0.0}/tests/database/test_transaction_registry.py +0 -0
  157. {python_neva-4.1.0 → python_neva-5.0.0}/tests/events/__init__.py +0 -0
  158. {python_neva-4.1.0 → python_neva-5.0.0}/tests/events/conftest.py +0 -0
  159. {python_neva-4.1.0 → python_neva-5.0.0}/tests/events/test_before_dispatch.py +0 -0
  160. {python_neva-4.1.0 → python_neva-5.0.0}/tests/events/test_binding.py +0 -0
  161. {python_neva-4.1.0 → python_neva-5.0.0}/tests/events/test_deferred.py +0 -0
  162. {python_neva-4.1.0 → python_neva-5.0.0}/tests/events/test_dispatch.py +0 -0
  163. {python_neva-4.1.0 → python_neva-5.0.0}/tests/events/test_event.py +0 -0
  164. {python_neva-4.1.0 → python_neva-5.0.0}/tests/events/test_function_listener.py +0 -0
  165. {python_neva-4.1.0 → python_neva-5.0.0}/tests/events/test_immediate.py +0 -0
  166. {python_neva-4.1.0 → python_neva-5.0.0}/tests/events/test_listen_on_parent_class.py +0 -0
  167. {python_neva-4.1.0 → python_neva-5.0.0}/tests/events/test_listener_wiring.py +0 -0
  168. {python_neva-4.1.0/tests/support → python_neva-5.0.0/tests/obs}/__init__.py +0 -0
  169. {python_neva-4.1.0 → python_neva-5.0.0}/tests/security/__init__.py +0 -0
  170. {python_neva-4.1.0 → python_neva-5.0.0}/tests/security/test_encrypter.py +0 -0
  171. {python_neva-4.1.0 → python_neva-5.0.0}/tests/security/test_hash_manager.py +0 -0
  172. {python_neva-4.1.0 → python_neva-5.0.0}/tests/security/test_tokens.py +0 -0
  173. {python_neva-4.1.0 → python_neva-5.0.0}/tests/support/test_results.py +0 -0
  174. {python_neva-4.1.0 → python_neva-5.0.0}/tests/testing/__init__.py +0 -0
  175. {python_neva-4.1.0 → python_neva-5.0.0}/tests/testing/test_application_reuse.py +0 -0
  176. {python_neva-4.1.0 → python_neva-5.0.0}/tests/testing/test_create_config_migration.py +0 -0
  177. {python_neva-4.1.0 → python_neva-5.0.0}/tests/testing/test_event_fake.py +0 -0
  178. {python_neva-4.1.0 → python_neva-5.0.0}/tests/testing/test_facade_restore.py +0 -0
  179. {python_neva-4.1.0 → python_neva-5.0.0}/tests/testing/test_fixtures.py +0 -0
  180. {python_neva-4.1.0 → python_neva-5.0.0}/tests/testing/test_refresh_database.py +0 -0
  181. {python_neva-4.1.0 → python_neva-5.0.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,23 @@
1
+ ## 5.0.0 (2026-09-01)
2
+
3
+ ### 💥 Boom
4
+
5
+ - **obs**: drop OpenTelemetry and pyinstrument from the core
6
+
7
+ ### ✨ Features
8
+
9
+ - **obs**: resolve logging through named channels
10
+ - **deps**: offer neva-boost through the boost extra
11
+
12
+ ### 💚👷 CI & Build
13
+
14
+ - **guidelines**: fail when a fragment cites a test that is gone
15
+
16
+ ### 📝💡 Documentation
17
+
18
+ - **guidelines**: rule out resolving from the container in register()
19
+ - **guidelines**: cover logging channels, and where OTel now lives
20
+
1
21
  ## 4.1.0 (2026-08-31)
2
22
 
3
23
  ### ✅🤡🧪 Tests
@@ -1,21 +1,21 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-neva
3
- Version: 4.1.0
3
+ Version: 5.0.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
@@ -47,17 +47,59 @@ independent repo, independently versioned and published.
47
47
 
48
48
  | Package | Repo / directory | Role | Status |
49
49
  | ----------------- | ------------------ | ------------------------------------------------------------------------------------- | ------------------------------- |
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/)) |
50
+ | `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
51
  | `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
52
  | `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
53
  | `neva-faststream` | `neva-faststream/` | FastStream (messaging) integration. | scaffolded, early placeholder |
54
54
  | `neva-auth` | `neva-auth/` | Authentication & identity toolkit — token-issuance engine, OAuth2 grants, guards, pluggable formats/backends. | core complete, OAuth-server slice in progress |
55
+ | `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. | planned |
56
+ | `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
57
  | `neva-example` | `neva-example/` | Reference application consuming `python-neva[fastapi]`. Not published; demonstration + integration test bed. | unpublished |
56
58
 
57
59
  > When working across packages locally, each repo can be cloned as a sibling
58
60
  > directory; integration packages depend on `python-neva` from PyPI (or an
59
61
  > editable local path during development).
60
62
 
63
+ ## Logging
64
+
65
+ Logging is a set of named channels, each with its own driver, level and output —
66
+ Laravel's `Log`, configured from one `config/obs.py` namespace.
67
+
68
+ ```python
69
+ config = {
70
+ "logging": {
71
+ "default": "stdout",
72
+ "channels": {
73
+ "stdout": {"driver": "console", "level": "DEBUG"},
74
+ "json": {"driver": "json", "stream": "stderr", "level": "INFO"},
75
+ "audit": {"driver": "file", "path": "var/audit.log"},
76
+ "both": {"driver": "stack", "channels": ["json", "audit"]},
77
+ },
78
+ },
79
+ }
80
+ ```
81
+
82
+ ```python
83
+ from neva.support.facade import Log
84
+
85
+ Log.info("order placed", order_id=42) # default channel
86
+ Log.channel("audit").warning("role granted", actor="root")
87
+ Log.bind(tenant="acme") # every channel, this context
88
+ ```
89
+
90
+ Drivers are `console`, `json`, `file`, `stack` and `null`. With no `config/obs.py`
91
+ at all, logging still works: a console channel on stdout at `DEBUG`.
92
+
93
+ Each channel carries its own structlog processor chain rather than going through
94
+ the process-global `structlog.configure()`, so one application's renderer cannot
95
+ leak into another in the same interpreter. `LogManager.processor(fn)` lets a
96
+ plugin add a processor to every chain — the seam `neva-otel` uses to put the
97
+ current span's ids on every record.
98
+
99
+ **Tracing and metrics are deliberately not here.** They live in `neva-otel`, so
100
+ the core carries no OpenTelemetry dependency and commits no application to an
101
+ observability backend.
102
+
61
103
  ## Develop
62
104
 
63
105
  ```bash
@@ -103,10 +145,9 @@ rewrite the new tag with the rendered changelog as its annotation.
103
145
  ## Feature ideas
104
146
 
105
147
  - 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
148
  - Improved router registration (auto-discovery OR provider-based? both?)
108
149
  - Improved security tooling (performance improvements, better defaults, etc.)
109
- - Better OpenTelemetry integration
150
+ - OpenTelemetry as a plugin (`neva-otel`) — the core keeps logging and takes no OTel dependency
110
151
  - Improved factory module (based on Polyfactory)
111
152
  - Queue/Jobs system
112
153
  - CLI integration
@@ -18,17 +18,59 @@ 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. | planned |
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
+ config = {
41
+ "logging": {
42
+ "default": "stdout",
43
+ "channels": {
44
+ "stdout": {"driver": "console", "level": "DEBUG"},
45
+ "json": {"driver": "json", "stream": "stderr", "level": "INFO"},
46
+ "audit": {"driver": "file", "path": "var/audit.log"},
47
+ "both": {"driver": "stack", "channels": ["json", "audit"]},
48
+ },
49
+ },
50
+ }
51
+ ```
52
+
53
+ ```python
54
+ from neva.support.facade import Log
55
+
56
+ Log.info("order placed", order_id=42) # default channel
57
+ Log.channel("audit").warning("role granted", actor="root")
58
+ Log.bind(tenant="acme") # every channel, this context
59
+ ```
60
+
61
+ Drivers are `console`, `json`, `file`, `stack` and `null`. With no `config/obs.py`
62
+ at all, logging still works: a console channel on stdout at `DEBUG`.
63
+
64
+ Each channel carries its own structlog processor chain rather than going through
65
+ the process-global `structlog.configure()`, so one application's renderer cannot
66
+ leak into another in the same interpreter. `LogManager.processor(fn)` lets a
67
+ plugin add a processor to every chain — the seam `neva-otel` uses to put the
68
+ current span's ids on every record.
69
+
70
+ **Tracing and metrics are deliberately not here.** They live in `neva-otel`, so
71
+ the core carries no OpenTelemetry dependency and commits no application to an
72
+ observability backend.
73
+
32
74
  ## Develop
33
75
 
34
76
  ```bash
@@ -74,10 +116,9 @@ rewrite the new tag with the rendered changelog as its annotation.
74
116
  ## Feature ideas
75
117
 
76
118
  - 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
119
  - Improved router registration (auto-discovery OR provider-based? both?)
79
120
  - Improved security tooling (performance improvements, better defaults, etc.)
80
- - Better OpenTelemetry integration
121
+ - OpenTelemetry as a plugin (`neva-otel`) — the core keeps logging and takes no OTel dependency
81
122
  - Improved factory module (based on Polyfactory)
82
123
  - Queue/Jobs system
83
124
  - CLI integration
@@ -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,116 @@
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]
8
+ ---
9
+
10
+ # Observability
11
+
12
+ The core ships **logging only**. Tracing, metrics, exporters and instrumentors live in a
13
+ separate plugin, so installing the core commits an application to no observability backend.
14
+ There is no `Trace` facade, no tracer, and no OpenTelemetry dependency here — do not add one.
15
+
16
+ ## Channels
17
+
18
+ Logging is a set of named channels, each with its own driver, level and output. One
19
+ `config/obs.py` namespace configures them.
20
+
21
+ ```python
22
+ config = {
23
+ "logging": {
24
+ "default": "stdout",
25
+ "channels": {
26
+ "stdout": {"driver": "console", "level": "DEBUG"},
27
+ "json": {"driver": "json", "stream": "stderr", "level": "INFO"},
28
+ "audit": {"driver": "file", "path": "var/audit.log"},
29
+ "both": {"driver": "stack", "channels": ["json", "audit"]},
30
+ "quiet": {"driver": "null"},
31
+ },
32
+ },
33
+ }
34
+ ```
35
+
36
+ | Driver | Writes | Driver-specific keys |
37
+ | --- | --- | --- |
38
+ | `console` | human-readable, to a stream | `stream` |
39
+ | `json` | one JSON object per line, to a stream | `stream` |
40
+ | `file` | JSON lines appended to a path | `path` (required) |
41
+ | `stack` | fans one record out to other channels | `channels` (required) |
42
+ | `null` | nothing | — |
43
+
44
+ `level` is one of `DEBUG`/`INFO`/`WARNING`/`ERROR`/`CRITICAL`, default `DEBUG`. `stream` is
45
+ `stdout` (default) or `stderr`. A `file` channel creates parent directories and holds its
46
+ handle open for the process's life.
47
+
48
+ **With no `config/obs.py` at all**, logging still works: a `console` channel named `stdout` on
49
+ stdout at `DEBUG`.
50
+
51
+ ## Writing
52
+
53
+ ```python
54
+ from neva.support.facade import Log
55
+
56
+ Log.info("order placed", order_id=42) # default channel
57
+ Log.channel("audit").warning("role granted", actor="root")
58
+ ```
59
+
60
+ Level methods go to the default channel. Everything after the message is structured context,
61
+ not format arguments — never build the message with an f-string when the value belongs in a
62
+ field.
63
+
64
+ `Log.channel(name)` **raises** on a name no channel is configured for, carrying the reason.
65
+ A typo is a configuration mistake, and serving the default instead would hide it behind
66
+ working output. Where the failure has to be a value, `Log.channels` gives the resolver and
67
+ `use(name)` returns `Result`.
68
+
69
+ ## Ambient context
70
+
71
+ ```python
72
+ Log.bind(tenant="acme") # every subsequent record in this context, every channel
73
+ Log.unbind("tenant")
74
+ ```
75
+
76
+ Backed by structlog's contextvars, so a binding follows the async task rather than the
77
+ channel. This is how a request's correlation ID reaches log lines without being passed
78
+ around — `neva-asgi`'s middleware binds it the same way.
79
+
80
+ ## Enriching records from a plugin
81
+
82
+ `LogManager.processor(fn)` appends a structlog processor to **every** channel's chain. This is
83
+ the seam a plugin adds derived fields through — the OpenTelemetry plugin injects the current
84
+ span's ids this way.
85
+
86
+ ```python
87
+ class MyProvider(ServiceProvider):
88
+ @asynccontextmanager
89
+ async def lifespan(self) -> AsyncIterator[None]:
90
+ manager = (await self.app.make_async(LogManager)).unwrap()
91
+ _ = manager.processor(add_my_field)
92
+ yield
93
+ ```
94
+
95
+ **Call it from `lifespan()`, never from `register()`.** Resolving anything during
96
+ registration hands back an instance the booted application does not use, so a processor
97
+ registered there silently never runs. See the service-providers fragment.
98
+
99
+ Records emitted by base providers' own startup are not enriched — their lifespans run before
100
+ a plugin's. Everything after is. Already-built channels are discarded on registration, so the
101
+ next resolution picks the processor up.
102
+
103
+ Custom drivers register the same way, through `resolver.driver(name, builder)`.
104
+
105
+ ## Rules
106
+
107
+ - **Never call `structlog.configure()`.** It is process-global; channels carry their own
108
+ chains precisely so one application's renderer cannot leak into another in the same
109
+ interpreter. Configuring it globally breaks that isolation and every channel's level.
110
+ - **`merge_contextvars` must lead every chain.** A custom driver that omits it silently drops
111
+ the correlation ID from every line it renders.
112
+ - **A broken default channel fails at boot**, not at the first log call — the framework logs
113
+ during provider startup, so a `file` channel with no `path` surfaces there. The error names
114
+ the channel and the reason.
115
+ - `LogManager()` with no application is legitimate and serves one console channel; it is what
116
+ a test needing somewhere for records to go should use.
@@ -4,7 +4,7 @@ title: Service providers and bindings
4
4
  requires: python-neva>=4.0
5
5
  triggers: [registering a service, binding a dependency, provider.py, wiring a component, DI container, resolving a type]
6
6
  priority: 20
7
- verified_by: [tests/arch/test_lifetimes.py, tests/arch/test_registration.py, tests/arch/test_cache.py, tests/arch/test_scope.py, tests/arch/test_extends.py]
7
+ verified_by: [tests/arch/test_lifetimes.py, tests/arch/test_registration.py, tests/arch/test_cache.py, tests/arch/test_scope.py, tests/arch/test_extends.py, tests/arch/test_register_resolution.py]
8
8
  ---
9
9
 
10
10
  # Service providers and bindings
@@ -28,6 +28,36 @@ class ActorServiceProvider(ServiceProvider):
28
28
  `register()` returns `Ok(self)`, or `Err("...")` to abort boot with a message. Bind only — no
29
29
  I/O, no connections. Startup work goes in `lifespan()`.
30
30
 
31
+ ## Never resolve in register()
32
+
33
+ `register()` **declares**; `lifespan()` **uses**. Calling `make()` or `make_async()` during
34
+ registration cannot work, and fails in one of two ways depending on who bound the type:
35
+
36
+ | Resolving | Outcome |
37
+ | --- | --- |
38
+ | A type **this provider** binds | `Err` — a provider joins the graph only once `register()` has returned, so its own bindings are not there yet. |
39
+ | A type **another provider** bound | Succeeds, then is **orphaned**. Resolving builds the container early; a successful registration discards it, so the booted application serves a *different* instance. |
40
+
41
+ The second is the dangerous one: nothing raises, nothing logs, and the object looks right. A
42
+ provider that configures a manager this way — adding a log processor, registering a driver —
43
+ silently configures a throwaway.
44
+
45
+ ```python
46
+ def register(self) -> Result[Self, str]:
47
+ manager = self.app.make(SomeManager).unwrap() # WRONG: discarded instance
48
+ manager.configure(...) # silently affects nothing
49
+ return Ok(self)
50
+
51
+ @asynccontextmanager
52
+ async def lifespan(self) -> AsyncIterator[None]:
53
+ manager = (await self.app.make_async(SomeManager)).unwrap() # the real one
54
+ manager.configure(...)
55
+ yield
56
+ ```
57
+
58
+ Needing another service in order to *bind* is a sign the binding wants a factory function
59
+ taking it as a parameter — let the container inject it, rather than reaching for it.
60
+
31
61
  ## A lifetime is mandatory
32
62
 
33
63
  Bare `bind()` with neither `scope` nor `cache` **raises `TypeError`** since 4.0. Use the named
@@ -111,4 +141,6 @@ catches typos before boot. Each package declares the shape for the keys it reads
111
141
  - A provider absent from the `providers` config never boots, and nothing says so.
112
142
  - Resolution is by **type**. There are no string keys; for two implementations of one interface
113
143
  use distinct types or markers.
114
- - `register()` must be pure binding. Connections belong in `lifespan()`.
144
+ - `register()` must be pure binding. Connections **and any `make()` call** belong in
145
+ `lifespan()` — see above; resolving during registration silently yields a discarded
146
+ instance.
@@ -0,0 +1,19 @@
1
+ """Observability tooling.
2
+
3
+ Logging only: tracing, metrics and their exporters live in the `neva-otel`
4
+ plugin, so the core commits no application to an observability backend.
5
+ """
6
+
7
+ from neva.obs.config import ChannelConfig, LoggingConfig, ObsConfig
8
+ from neva.obs.logging import Channel, ChannelResolver, LogManager, LogServiceProvider
9
+
10
+
11
+ __all__ = [
12
+ "Channel",
13
+ "ChannelConfig",
14
+ "ChannelResolver",
15
+ "LogManager",
16
+ "LogServiceProvider",
17
+ "LoggingConfig",
18
+ "ObsConfig",
19
+ ]
@@ -0,0 +1,37 @@
1
+ """Observability configs."""
2
+
3
+ from typing import Literal, NotRequired, TypedDict
4
+
5
+
6
+ LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
7
+ ChannelDriver = Literal["console", "json", "file", "stack", "null"]
8
+ Stream = Literal["stdout", "stderr"]
9
+
10
+
11
+ class ChannelConfig(TypedDict):
12
+ """Configuration for a single log channel.
13
+
14
+ Three keys are driver-specific and carry no meaning elsewhere: `stream`
15
+ belongs to `console` and `json`, `path` to `file`, and `channels` to
16
+ `stack`. A `file` channel without `path`, or a `stack` without `channels`,
17
+ fails to resolve rather than logging nowhere.
18
+ """
19
+
20
+ driver: ChannelDriver
21
+ level: NotRequired[LogLevel]
22
+ stream: NotRequired[Stream]
23
+ path: NotRequired[str]
24
+ channels: NotRequired[list[str]]
25
+
26
+
27
+ class LoggingConfig(TypedDict):
28
+ """Logging config."""
29
+
30
+ default: NotRequired[str]
31
+ channels: NotRequired[dict[str, ChannelConfig]]
32
+
33
+
34
+ class ObsConfig(TypedDict):
35
+ """Observability config."""
36
+
37
+ logging: NotRequired[LoggingConfig]
@@ -0,0 +1,22 @@
1
+ """Logging module.
2
+
3
+ Structured logging over structlog, shaped as named channels resolved from
4
+ configuration, reached through the `Log` facade.
5
+ """
6
+
7
+ from neva.obs.logging.channels import NullChannel, StackChannel
8
+ from neva.obs.logging.contracts import Channel, Processor
9
+ from neva.obs.logging.manager import LogManager
10
+ from neva.obs.logging.provider import LogServiceProvider
11
+ from neva.obs.logging.resolver import ChannelResolver
12
+
13
+
14
+ __all__ = [
15
+ "Channel",
16
+ "ChannelResolver",
17
+ "LogManager",
18
+ "LogServiceProvider",
19
+ "NullChannel",
20
+ "Processor",
21
+ "StackChannel",
22
+ ]