python-neva 3.6.0__tar.gz → 4.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 (174) hide show
  1. python_neva-4.0.0/.gitlab-ci.yml +79 -0
  2. {python_neva-3.6.0 → python_neva-4.0.0}/.pre-commit-config.yaml +1 -1
  3. {python_neva-3.6.0 → python_neva-4.0.0}/CHANGELOG.md +50 -0
  4. {python_neva-3.6.0 → python_neva-4.0.0}/CLAUDE.md +1 -1
  5. {python_neva-3.6.0 → python_neva-4.0.0}/PKG-INFO +11 -5
  6. {python_neva-3.6.0 → python_neva-4.0.0}/README.md +1 -0
  7. {python_neva-3.6.0 → python_neva-4.0.0}/neva/arch/__init__.py +3 -0
  8. {python_neva-3.6.0 → python_neva-4.0.0}/neva/arch/application.py +64 -35
  9. python_neva-4.0.0/neva/arch/config.py +49 -0
  10. {python_neva-3.6.0 → python_neva-4.0.0}/neva/arch/integrations/faststream.py +9 -6
  11. {python_neva-3.6.0 → python_neva-4.0.0}/neva/arch/service_provider.py +11 -11
  12. {python_neva-3.6.0 → python_neva-4.0.0}/neva/database/connection.py +8 -3
  13. {python_neva-3.6.0 → python_neva-4.0.0}/neva/database/manager.py +7 -3
  14. {python_neva-3.6.0 → python_neva-4.0.0}/neva/database/transaction.py +31 -13
  15. python_neva-4.0.0/neva/obs/__init__.py +9 -0
  16. python_neva-4.0.0/neva/security/tokens/generate_token.py +18 -0
  17. {python_neva-3.6.0 → python_neva-4.0.0}/neva/support/strategy.py +2 -2
  18. python_neva-4.0.0/neva/testing/__init__.py +7 -0
  19. python_neva-4.0.0/neva/testing/test_case.py +208 -0
  20. {python_neva-3.6.0 → python_neva-4.0.0}/pyproject.toml +9 -5
  21. python_neva-4.0.0/ruff.toml +47 -0
  22. {python_neva-3.6.0 → python_neva-4.0.0}/tests/arch/test_cache.py +2 -1
  23. python_neva-4.0.0/tests/arch/test_config_shapes.py +54 -0
  24. {python_neva-3.6.0 → python_neva-4.0.0}/tests/arch/test_context.py +2 -1
  25. {python_neva-3.6.0 → python_neva-4.0.0}/tests/arch/test_extends.py +2 -1
  26. python_neva-4.0.0/tests/arch/test_facade_root_nesting.py +46 -0
  27. {python_neva-3.6.0 → python_neva-4.0.0}/tests/arch/test_lifetimes.py +7 -10
  28. {python_neva-3.6.0 → python_neva-4.0.0}/tests/arch/test_registration.py +2 -6
  29. {python_neva-3.6.0 → python_neva-4.0.0}/tests/arch/test_scope.py +12 -6
  30. python_neva-4.0.0/tests/config/test_config_path_resolution.py +48 -0
  31. {python_neva-3.6.0 → python_neva-4.0.0}/tests/config/test_loader.py +2 -6
  32. {python_neva-3.6.0 → python_neva-4.0.0}/tests/database/test_database_manager.py +0 -1
  33. python_neva-4.0.0/tests/database/test_transaction_callbacks.py +114 -0
  34. {python_neva-3.6.0 → python_neva-4.0.0}/tests/events/conftest.py +1 -1
  35. {python_neva-3.6.0 → python_neva-4.0.0}/tests/events/test_listen_on_parent_class.py +1 -1
  36. python_neva-4.0.0/tests/events/test_listener_wiring.py +108 -0
  37. {python_neva-3.6.0 → python_neva-4.0.0}/tests/security/test_encrypter.py +15 -8
  38. {python_neva-3.6.0 → python_neva-4.0.0}/tests/security/test_hash_manager.py +16 -12
  39. python_neva-4.0.0/tests/security/test_tokens.py +77 -0
  40. python_neva-4.0.0/tests/testing/test_application_reuse.py +67 -0
  41. python_neva-4.0.0/tests/testing/test_create_config_migration.py +76 -0
  42. {python_neva-3.6.0 → python_neva-4.0.0}/tests/testing/test_refresh_database.py +9 -3
  43. {python_neva-3.6.0 → python_neva-4.0.0}/tests/testing/test_test_case.py +8 -7
  44. python_neva-4.0.0/uv.lock +1901 -0
  45. python_neva-4.0.0/wiki/architecture/KNOWN-DISCREPANCIES.md +41 -0
  46. {python_neva-3.6.0 → python_neva-4.0.0}/wiki/architecture/configuration.md +32 -7
  47. {python_neva-3.6.0 → python_neva-4.0.0}/wiki/architecture/database-and-transactions.md +4 -1
  48. {python_neva-3.6.0 → python_neva-4.0.0}/wiki/architecture/dependency-injection.md +6 -5
  49. {python_neva-3.6.0 → python_neva-4.0.0}/wiki/architecture/testing.md +72 -14
  50. python_neva-3.6.0/neva/arch/config.py +0 -37
  51. python_neva-3.6.0/neva/obs/__init__.py +0 -13
  52. python_neva-3.6.0/neva/obs/middleware/__init__.py +0 -7
  53. python_neva-3.6.0/neva/obs/middleware/correlation.py +0 -68
  54. python_neva-3.6.0/neva/obs/middleware/profiler.py +0 -68
  55. python_neva-3.6.0/neva/security/tokens/generate_token.py +0 -16
  56. python_neva-3.6.0/neva/testing/__init__.py +0 -7
  57. python_neva-3.6.0/neva/testing/test_case.py +0 -101
  58. python_neva-3.6.0/ruff.toml +0 -43
  59. python_neva-3.6.0/tests/obs/__init__.py +0 -1
  60. python_neva-3.6.0/tests/obs/test_correlation.py +0 -127
  61. python_neva-3.6.0/tests/obs/test_profiler.py +0 -90
  62. python_neva-3.6.0/uv.lock +0 -2293
  63. python_neva-3.6.0/wiki/architecture/KNOWN-DISCREPANCIES.md +0 -23
  64. {python_neva-3.6.0 → python_neva-4.0.0}/.claude/settings.local.json +0 -0
  65. {python_neva-3.6.0 → python_neva-4.0.0}/.envrc +0 -0
  66. {python_neva-3.6.0 → python_neva-4.0.0}/.gitignore +0 -0
  67. {python_neva-3.6.0 → python_neva-4.0.0}/.python-version +0 -0
  68. {python_neva-3.6.0 → python_neva-4.0.0}/neva/arch/facade.py +0 -0
  69. {python_neva-3.6.0 → python_neva-4.0.0}/neva/arch/integrations/__init__.py +0 -0
  70. {python_neva-3.6.0 → python_neva-4.0.0}/neva/arch/markers.py +0 -0
  71. {python_neva-3.6.0 → python_neva-4.0.0}/neva/arch/py.typed +0 -0
  72. {python_neva-3.6.0 → python_neva-4.0.0}/neva/arch/scopes.py +0 -0
  73. {python_neva-3.6.0 → python_neva-4.0.0}/neva/config/__init__.py +0 -0
  74. {python_neva-3.6.0 → python_neva-4.0.0}/neva/config/base_providers.py +0 -0
  75. {python_neva-3.6.0 → python_neva-4.0.0}/neva/config/loader.py +0 -0
  76. {python_neva-3.6.0 → python_neva-4.0.0}/neva/config/py.typed +0 -0
  77. {python_neva-3.6.0 → python_neva-4.0.0}/neva/config/repository.py +0 -0
  78. {python_neva-3.6.0 → python_neva-4.0.0}/neva/database/__init__.py +0 -0
  79. {python_neva-3.6.0 → python_neva-4.0.0}/neva/database/config.py +0 -0
  80. {python_neva-3.6.0 → python_neva-4.0.0}/neva/database/provider.py +0 -0
  81. {python_neva-3.6.0 → python_neva-4.0.0}/neva/database/py.typed +0 -0
  82. {python_neva-3.6.0 → python_neva-4.0.0}/neva/events/__init__.py +0 -0
  83. {python_neva-3.6.0 → python_neva-4.0.0}/neva/events/contracts/__init__.py +0 -0
  84. {python_neva-3.6.0 → python_neva-4.0.0}/neva/events/contracts/dispatcher.py +0 -0
  85. {python_neva-3.6.0 → python_neva-4.0.0}/neva/events/contracts/event.py +0 -0
  86. {python_neva-3.6.0 → python_neva-4.0.0}/neva/events/contracts/handler.py +0 -0
  87. {python_neva-3.6.0 → python_neva-4.0.0}/neva/events/contracts/listener.py +0 -0
  88. {python_neva-3.6.0 → python_neva-4.0.0}/neva/events/dispatcher.py +0 -0
  89. {python_neva-3.6.0 → python_neva-4.0.0}/neva/events/event.py +0 -0
  90. {python_neva-3.6.0 → python_neva-4.0.0}/neva/events/event_registry.py +0 -0
  91. {python_neva-3.6.0 → python_neva-4.0.0}/neva/events/listener.py +0 -0
  92. {python_neva-3.6.0 → python_neva-4.0.0}/neva/events/policy.py +0 -0
  93. {python_neva-3.6.0 → python_neva-4.0.0}/neva/events/provider.py +0 -0
  94. {python_neva-3.6.0 → python_neva-4.0.0}/neva/events/py.typed +0 -0
  95. {python_neva-3.6.0 → python_neva-4.0.0}/neva/obs/instrumentation/__init__.py +0 -0
  96. {python_neva-3.6.0 → python_neva-4.0.0}/neva/obs/instrumentation/sqlalchemy.py +0 -0
  97. {python_neva-3.6.0 → python_neva-4.0.0}/neva/obs/logging/__init__.py +0 -0
  98. {python_neva-3.6.0 → python_neva-4.0.0}/neva/obs/logging/manager.py +0 -0
  99. {python_neva-3.6.0 → python_neva-4.0.0}/neva/obs/logging/provider.py +0 -0
  100. {python_neva-3.6.0 → python_neva-4.0.0}/neva/obs/py.typed +0 -0
  101. {python_neva-3.6.0 → python_neva-4.0.0}/neva/polyfactory/__init__.py +0 -0
  102. {python_neva-3.6.0 → python_neva-4.0.0}/neva/polyfactory/factories.py +0 -0
  103. {python_neva-3.6.0 → python_neva-4.0.0}/neva/polyfactory/persistence.py +0 -0
  104. {python_neva-3.6.0 → python_neva-4.0.0}/neva/polyfactory/py.typed +0 -0
  105. {python_neva-3.6.0 → python_neva-4.0.0}/neva/security/__init__.py +0 -0
  106. {python_neva-3.6.0 → python_neva-4.0.0}/neva/security/encryption/__init__.py +0 -0
  107. {python_neva-3.6.0 → python_neva-4.0.0}/neva/security/encryption/encrypter.py +0 -0
  108. {python_neva-3.6.0 → python_neva-4.0.0}/neva/security/encryption/protocol.py +0 -0
  109. {python_neva-3.6.0 → python_neva-4.0.0}/neva/security/hashing/__init__.py +0 -0
  110. {python_neva-3.6.0 → python_neva-4.0.0}/neva/security/hashing/config.py +0 -0
  111. {python_neva-3.6.0 → python_neva-4.0.0}/neva/security/hashing/hash_manager.py +0 -0
  112. {python_neva-3.6.0 → python_neva-4.0.0}/neva/security/hashing/hashers/__init__.py +0 -0
  113. {python_neva-3.6.0 → python_neva-4.0.0}/neva/security/hashing/hashers/argon2.py +0 -0
  114. {python_neva-3.6.0 → python_neva-4.0.0}/neva/security/hashing/hashers/bcrypt.py +0 -0
  115. {python_neva-3.6.0 → python_neva-4.0.0}/neva/security/hashing/hashers/protocol.py +0 -0
  116. {python_neva-3.6.0 → python_neva-4.0.0}/neva/security/provider.py +0 -0
  117. {python_neva-3.6.0 → python_neva-4.0.0}/neva/security/py.typed +0 -0
  118. {python_neva-3.6.0 → python_neva-4.0.0}/neva/security/tokens/__init__.py +0 -0
  119. {python_neva-3.6.0 → python_neva-4.0.0}/neva/security/tokens/hash_token.py +0 -0
  120. {python_neva-3.6.0 → python_neva-4.0.0}/neva/security/tokens/verify_token.py +0 -0
  121. {python_neva-3.6.0 → python_neva-4.0.0}/neva/support/__init__.py +0 -0
  122. {python_neva-3.6.0 → python_neva-4.0.0}/neva/support/accessors.py +0 -0
  123. {python_neva-3.6.0 → python_neva-4.0.0}/neva/support/facade/__init__.py +0 -0
  124. {python_neva-3.6.0 → python_neva-4.0.0}/neva/support/facade/app.py +0 -0
  125. {python_neva-3.6.0 → python_neva-4.0.0}/neva/support/facade/app.pyi +0 -0
  126. {python_neva-3.6.0 → python_neva-4.0.0}/neva/support/facade/config.py +0 -0
  127. {python_neva-3.6.0 → python_neva-4.0.0}/neva/support/facade/config.pyi +0 -0
  128. {python_neva-3.6.0 → python_neva-4.0.0}/neva/support/facade/crypt.py +0 -0
  129. {python_neva-3.6.0 → python_neva-4.0.0}/neva/support/facade/crypt.pyi +0 -0
  130. {python_neva-3.6.0 → python_neva-4.0.0}/neva/support/facade/db.py +0 -0
  131. {python_neva-3.6.0 → python_neva-4.0.0}/neva/support/facade/db.pyi +0 -0
  132. {python_neva-3.6.0 → python_neva-4.0.0}/neva/support/facade/event.py +0 -0
  133. {python_neva-3.6.0 → python_neva-4.0.0}/neva/support/facade/event.pyi +0 -0
  134. {python_neva-3.6.0 → python_neva-4.0.0}/neva/support/facade/hash.py +0 -0
  135. {python_neva-3.6.0 → python_neva-4.0.0}/neva/support/facade/hash.pyi +0 -0
  136. {python_neva-3.6.0 → python_neva-4.0.0}/neva/support/facade/log.py +0 -0
  137. {python_neva-3.6.0 → python_neva-4.0.0}/neva/support/facade/log.pyi +0 -0
  138. {python_neva-3.6.0 → python_neva-4.0.0}/neva/support/py.typed +0 -0
  139. {python_neva-3.6.0 → python_neva-4.0.0}/neva/support/results.py +0 -0
  140. {python_neva-3.6.0 → python_neva-4.0.0}/neva/support/strconv.py +0 -0
  141. {python_neva-3.6.0 → python_neva-4.0.0}/neva/support/time.py +0 -0
  142. {python_neva-3.6.0 → python_neva-4.0.0}/neva/testing/fakes.py +0 -0
  143. {python_neva-3.6.0 → python_neva-4.0.0}/neva/testing/fixtures.py +0 -0
  144. {python_neva-3.6.0 → python_neva-4.0.0}/neva/testing/py.typed +0 -0
  145. {python_neva-3.6.0 → python_neva-4.0.0}/scripts/retag-with-changelog.sh +0 -0
  146. {python_neva-3.6.0 → python_neva-4.0.0}/tests/__init__.py +0 -0
  147. {python_neva-3.6.0 → python_neva-4.0.0}/tests/arch/__init__.py +0 -0
  148. {python_neva-3.6.0 → python_neva-4.0.0}/tests/config/__init__.py +0 -0
  149. {python_neva-3.6.0 → python_neva-4.0.0}/tests/config/test_repository.py +0 -0
  150. {python_neva-3.6.0 → python_neva-4.0.0}/tests/conftest.py +0 -0
  151. {python_neva-3.6.0 → python_neva-4.0.0}/tests/database/__init__.py +0 -0
  152. {python_neva-3.6.0 → python_neva-4.0.0}/tests/database/test_connection_manager.py +0 -0
  153. {python_neva-3.6.0 → python_neva-4.0.0}/tests/database/test_detached_lifespan.py +0 -0
  154. {python_neva-3.6.0 → python_neva-4.0.0}/tests/database/test_edge_cases.py +0 -0
  155. {python_neva-3.6.0 → python_neva-4.0.0}/tests/database/test_multi_connection.py +0 -0
  156. {python_neva-3.6.0 → python_neva-4.0.0}/tests/database/test_sqlalchemy_integration.py +0 -0
  157. {python_neva-3.6.0 → python_neva-4.0.0}/tests/database/test_transaction.py +0 -0
  158. {python_neva-3.6.0 → python_neva-4.0.0}/tests/database/test_transaction_context.py +0 -0
  159. {python_neva-3.6.0 → python_neva-4.0.0}/tests/database/test_transaction_registry.py +0 -0
  160. {python_neva-3.6.0 → python_neva-4.0.0}/tests/events/__init__.py +0 -0
  161. {python_neva-3.6.0 → python_neva-4.0.0}/tests/events/test_before_dispatch.py +0 -0
  162. {python_neva-3.6.0 → python_neva-4.0.0}/tests/events/test_binding.py +0 -0
  163. {python_neva-3.6.0 → python_neva-4.0.0}/tests/events/test_deferred.py +0 -0
  164. {python_neva-3.6.0 → python_neva-4.0.0}/tests/events/test_dispatch.py +0 -0
  165. {python_neva-3.6.0 → python_neva-4.0.0}/tests/events/test_event.py +0 -0
  166. {python_neva-3.6.0 → python_neva-4.0.0}/tests/events/test_function_listener.py +0 -0
  167. {python_neva-3.6.0 → python_neva-4.0.0}/tests/events/test_immediate.py +0 -0
  168. {python_neva-3.6.0 → python_neva-4.0.0}/tests/security/__init__.py +0 -0
  169. {python_neva-3.6.0 → python_neva-4.0.0}/tests/testing/__init__.py +0 -0
  170. {python_neva-3.6.0 → python_neva-4.0.0}/tests/testing/test_event_fake.py +0 -0
  171. {python_neva-3.6.0 → python_neva-4.0.0}/tests/testing/test_facade_restore.py +0 -0
  172. {python_neva-3.6.0 → python_neva-4.0.0}/tests/testing/test_fixtures.py +0 -0
  173. {python_neva-3.6.0 → python_neva-4.0.0}/wiki/architecture/events.md +0 -0
  174. {python_neva-3.6.0 → python_neva-4.0.0}/wiki/architecture/result-option.md +0 -0
@@ -0,0 +1,79 @@
1
+ ---
2
+ variables:
3
+ PYTHON_VERSION: "3.12"
4
+ UV_VERSION: "0.9.28"
5
+ BASE_LAYER: bookworm-slim
6
+ UV_LINK_MODE: copy
7
+ UV_CACHE_DIR: .cache/uv
8
+
9
+ stages:
10
+ - code_qa
11
+ - test
12
+ - deps
13
+
14
+ ##### Deps #####
15
+ .uv_base:
16
+ image: ghcr.io/astral-sh/uv:$UV_VERSION-python$PYTHON_VERSION-$BASE_LAYER
17
+ cache:
18
+ key:
19
+ files:
20
+ - uv.lock
21
+ paths:
22
+ - .cache/uv
23
+ - .venv
24
+ before_script:
25
+ - uv sync --frozen --all-extras
26
+
27
+ .default_rules:
28
+ rules:
29
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
30
+ - if: $CI_COMMIT_BRANCH == "main"
31
+
32
+ ##### Code QA #####
33
+ ruff:lint:
34
+ extends: [.uv_base, .default_rules]
35
+ stage: code_qa
36
+ script:
37
+ - uv run --frozen ruff check .
38
+
39
+ ruff:format:
40
+ extends: [.uv_base, .default_rules]
41
+ stage: code_qa
42
+ script:
43
+ - uv run --frozen ruff format --check .
44
+
45
+ pyrefly:
46
+ extends: [.uv_base, .default_rules]
47
+ stage: code_qa
48
+ script:
49
+ - uv run --frozen pyrefly check
50
+
51
+ ##### Test #####
52
+ pytest:
53
+ extends: [.uv_base, .default_rules]
54
+ stage: test
55
+ script:
56
+ - uv run --frozen pytest
57
+
58
+ ##### Deps contract #####
59
+ # Resolves every direct dependency at its declared floor, so a `>=` lower than
60
+ # what the code needs fails here instead of in a user's install. Three things
61
+ # keep this job honest, and dropping any one makes it pass without testing
62
+ # anything: it must not extend .uv_base, whose before_script installs the
63
+ # committed lock; --upgrade discards that lock as a preference set, without
64
+ # which uv reuses it and lowers nothing; --frozen on sync and run stops uv
65
+ # re-resolving at the default highest strategy and discarding the floors it
66
+ # just computed. Rewriting uv.lock here is expected and must never be
67
+ # committed.
68
+ deps:floors:
69
+ extends: .default_rules
70
+ image: ghcr.io/astral-sh/uv:$UV_VERSION-python$PYTHON_VERSION-$BASE_LAYER
71
+ stage: deps
72
+ cache:
73
+ key: floors
74
+ paths:
75
+ - .cache/uv
76
+ script:
77
+ - uv lock --upgrade --resolution lowest-direct
78
+ - uv sync --frozen --all-extras
79
+ - uv run --frozen pytest
@@ -16,7 +16,7 @@ repos:
16
16
  - id: debug-statements
17
17
 
18
18
  - repo: https://github.com/astral-sh/ruff-pre-commit
19
- rev: v0.15.6
19
+ rev: v0.16.5
20
20
  hooks:
21
21
  - id: ruff
22
22
  args: [--fix, --exit-non-zero-on-fix]
@@ -1,3 +1,53 @@
1
+ ## 4.0.0 (2026-08-28)
2
+
3
+ ### 💥 Boom
4
+
5
+ - **testing**: require create_config overrides to be classmethods
6
+ - **arch**: require an explicit lifetime, drop Application.bind
7
+
8
+ ### ✨ Features
9
+
10
+ - **deps**: offer the ASGI middleware through the asgi extra
11
+ - **config**: scope the core config shapes to what the core reads, export them
12
+
13
+ ### 🐛🚑️ Fixes
14
+
15
+ - **security**: name generate_token's argument nbytes, cover the token helpers
16
+ - **arch**: fail the boot when declared listeners cannot be wired
17
+ - **config**: default the config directory to ./config, not the project root
18
+ - **database**: accept sync transaction callbacks, never lose their failures
19
+
20
+ ### ♻️ Refactorings
21
+
22
+ - **obs**: move the ASGI middleware to neva-asgi
23
+ - **arch**: describe the FastStream lifespan without starlette
24
+
25
+ ### ⚡️ Performance
26
+
27
+ - **GED-16633**: build the TestCase application once per class
28
+
29
+ ### 📝💡 Documentation
30
+
31
+ - **database**: un-deprecate register_engine, say what it is for
32
+
33
+ ## 3.7.0 (2026-08-28)
34
+
35
+ ### ✨ Features
36
+
37
+ - **deps**: require only what the core imports
38
+
39
+ ### 🐛🚑️ Fixes
40
+
41
+ - **deps**: declare starlette directly
42
+
43
+ ### 💚👷 CI & Build
44
+
45
+ - add the pipeline with a lowest-direct floors gate
46
+
47
+ ### 📌➕⬇️➖⬆️ Dependencies
48
+
49
+ - upgrade all dependencies and adapt to ruff 0.16
50
+
1
51
  ## 3.6.0 (2026-08-28)
2
52
 
3
53
  ### ✨ Features
@@ -75,7 +75,7 @@ await Event.dispatch(UserCreated(event_id=1, user_id=42))
75
75
  **Service Providers** - Register services with lifecycle hooks. Bindings
76
76
  declare a lifetime by name — `singleton` (one per process), `scoped` (one per
77
77
  REQUEST scope), `transient` (one per resolution), `instance` (an object you
78
- already have). Bare `bind` without an explicit `scope`/`cache` is deprecated:
78
+ already have). Bare `bind` with neither `scope` nor `cache` raises `TypeError`:
79
79
  ```python
80
80
  class MyProvider(ServiceProvider):
81
81
  def register(self) -> Result[Self, str]:
@@ -1,22 +1,27 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-neva
3
- Version: 3.6.0
3
+ Version: 4.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: fastapi[all]>=0.129.0
11
- Requires-Dist: faststream>=0.6.6
12
10
  Requires-Dist: opentelemetry-instrumentation-sqlalchemy>=0.62b0
13
11
  Requires-Dist: pwdlib[argon2,bcrypt]>=0.3.0
12
+ Requires-Dist: pydantic>=2.7
14
13
  Requires-Dist: pyinstrument>=5.1.1
15
14
  Requires-Dist: sqlalchemy[asyncio]>=2.0.0
16
15
  Requires-Dist: structlog>=25.5.0
17
- Requires-Dist: typer>=0.21.1
16
+ Requires-Dist: typing-extensions>=4.13
17
+ Provides-Extra: asgi
18
+ Requires-Dist: neva-asgi>=0.1.1; extra == 'asgi'
18
19
  Provides-Extra: fastapi
19
- Requires-Dist: neva-fastapi>=1.0.0; extra == 'fastapi'
20
+ Requires-Dist: neva-fastapi>=1.1.1; extra == 'fastapi'
21
+ Provides-Extra: faststream
22
+ Requires-Dist: faststream>=0.6.6; extra == 'faststream'
23
+ Provides-Extra: polyfactory
24
+ Requires-Dist: polyfactory>=3.1.0; extra == 'polyfactory'
20
25
  Provides-Extra: testing
21
26
  Requires-Dist: pytest-asyncio>=0.25.3; extra == 'testing'
22
27
  Requires-Dist: pytest>=9.0.2; extra == 'testing'
@@ -44,6 +49,7 @@ independent repo, independently versioned and published.
44
49
  | ----------------- | ------------------ | ------------------------------------------------------------------------------------- | ------------------------------- |
45
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/)) |
46
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
+ | `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/)) |
47
53
  | `neva-faststream` | `neva-faststream/` | FastStream (messaging) integration. | scaffolded, early placeholder |
48
54
  | `neva-auth` | `neva-auth/` | Authentication & identity toolkit — token-issuance engine, OAuth2 grants, guards, pluggable formats/backends. | core complete, OAuth-server slice in progress |
49
55
  | `neva-example` | `neva-example/` | Reference application consuming `python-neva[fastapi]`. Not published; demonstration + integration test bed. | unpublished |
@@ -20,6 +20,7 @@ independent repo, independently versioned and published.
20
20
  | ----------------- | ------------------ | ------------------------------------------------------------------------------------- | ------------------------------- |
21
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/)) |
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
+ | `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/)) |
23
24
  | `neva-faststream` | `neva-faststream/` | FastStream (messaging) integration. | scaffolded, early placeholder |
24
25
  | `neva-auth` | `neva-auth/` | Authentication & identity toolkit — token-issuance engine, OAuth2 grants, guards, pluggable formats/backends. | core complete, OAuth-server slice in progress |
25
26
  | `neva-example` | `neva-example/` | Reference application consuming `python-neva[fastapi]`. Not published; demonstration + integration test bed. | unpublished |
@@ -5,6 +5,7 @@ and facade implementations.
5
5
  """
6
6
 
7
7
  from neva.arch.application import Application
8
+ from neva.arch.config import AppConfig, ProviderConfig
8
9
  from neva.arch.facade import Facade
9
10
  from neva.arch.service_provider import (
10
11
  Bootable,
@@ -13,8 +14,10 @@ from neva.arch.service_provider import (
13
14
 
14
15
 
15
16
  __all__ = [
17
+ "AppConfig",
16
18
  "Application",
17
19
  "Bootable",
18
20
  "Facade",
21
+ "ProviderConfig",
19
22
  "ServiceProvider",
20
23
  ]
@@ -6,17 +6,16 @@ from collections.abc import AsyncGenerator, Sequence
6
6
  from contextlib import AsyncExitStack, asynccontextmanager
7
7
  from contextvars import ContextVar
8
8
  from pathlib import Path
9
- from typing import Any, Callable, Self
9
+ from typing import Any, Self
10
10
 
11
11
  import dishka
12
12
  from dishka.provider import BaseProvider
13
- from typing_extensions import deprecated
14
13
 
15
14
  from neva.arch.facade import Facade
16
15
  from neva.arch.scopes import BaseScope, Scope
17
16
  from neva.arch.service_provider import Bootable, ServiceProvider
18
17
  from neva.config.loader import ConfigLoader
19
- from neva.support import Err, Ok, Result
18
+ from neva.support import Err, Ok, Result, Some
20
19
 
21
20
 
22
21
  _current_container: ContextVar["dishka.AsyncContainer"] = ContextVar(
@@ -24,6 +23,26 @@ _current_container: ContextVar["dishka.AsyncContainer"] = ContextVar(
24
23
  )
25
24
 
26
25
 
26
+ def _resolve_config_path(config_path: str | Path | None) -> Path:
27
+ """Resolve the directory config files are loaded from.
28
+
29
+ The config directory is a dedicated folder, never the project root: every
30
+ ``.py`` in it is imported to read its ``config`` dict, so pointing this at
31
+ a directory holding ordinary modules would execute them.
32
+
33
+ Returns:
34
+ The explicit argument, else $NEVA_CONFIG_PATH, else ./config.
35
+ """
36
+ if config_path is not None:
37
+ return Path(config_path)
38
+
39
+ env_path = os.getenv("NEVA_CONFIG_PATH")
40
+ if env_path:
41
+ return Path(env_path)
42
+
43
+ return Path.cwd() / "config"
44
+
45
+
27
46
  class Application:
28
47
  """Base application for DI and facade injection."""
29
48
 
@@ -31,8 +50,9 @@ class Application:
31
50
  """Initialize the application and its bindings.
32
51
 
33
52
  Args:
34
- config_path: Path to the configuration directory. Defaults to "./config"
35
- relative to the current working directory.
53
+ config_path: Path to the configuration directory. Defaults to
54
+ ``$NEVA_CONFIG_PATH``, else ``./config`` relative to the
55
+ current working directory.
36
56
 
37
57
  Raises:
38
58
  RuntimeError: If the application fails to initialize.
@@ -47,11 +67,7 @@ class Application:
47
67
  self._container: dishka.AsyncContainer | None = None
48
68
  self._booted: bool = False
49
69
 
50
- configuration_path = (
51
- config_path
52
- if config_path is not None
53
- else os.getenv("NEVA_CONFIG_PATH", default=Path.cwd())
54
- )
70
+ configuration_path = _resolve_config_path(config_path)
55
71
  match ConfigLoader(configuration_path).load_all():
56
72
  case Err(e):
57
73
  raise RuntimeError(f"Failed to register config: {e}")
@@ -150,21 +166,6 @@ class Application:
150
166
  for provider in providers:
151
167
  _ = self.register(provider)
152
168
 
153
- @deprecated("Use the new bind() method directly with a Service Provider.")
154
- def bind(
155
- self,
156
- source: type | Callable[..., Any],
157
- *,
158
- interface: type | None = None,
159
- scope: BaseScope | None = None,
160
- ) -> None:
161
- """Binds a source to the container."""
162
- _ = self.root_provider.provide(
163
- source=source,
164
- scope=scope,
165
- provides=interface,
166
- )
167
-
168
169
  @property
169
170
  def current_container(self) -> dishka.AsyncContainer:
170
171
  """The container backing resolution in the current context.
@@ -226,6 +227,11 @@ class Application:
226
227
  @asynccontextmanager
227
228
  async def lifespan(self) -> AsyncGenerator[None]:
228
229
  """Wire the facades and providers."""
230
+ # The facade root is a single process-global slot, so a short-lived
231
+ # application nested inside a longer-lived one must hand it back rather
232
+ # than clear it -- otherwise it disarms every facade for whatever the
233
+ # outer application still has to do.
234
+ previous_facade_application = Facade.get_facade_application()
229
235
  Facade.set_facade_application(self)
230
236
  self._booted = True
231
237
 
@@ -243,27 +249,50 @@ class Application:
243
249
  if self._container is not None:
244
250
  await self._container.close()
245
251
  self._booted = False
246
- Facade.reset_facade_application()
252
+ match previous_facade_application:
253
+ case Some(application):
254
+ Facade.set_facade_application(application)
255
+ case _:
256
+ Facade.reset_facade_application()
247
257
 
248
258
  def _bind_event_listeners(self) -> None:
249
259
  """Bind all listener classes declared in provider ``listen`` dicts."""
250
260
  for provider in self.providers.values():
251
261
  for listeners in provider.listen.values():
252
262
  for listener_cls in listeners:
253
- # Straight onto the root provider: routing this through the
254
- # deprecated `bind` warned on every boot.
263
+ # Straight onto the root provider: these listeners are not
264
+ # a provider's own bindings, so they do not belong in one.
255
265
  _ = self.root_provider.provide(listener_cls, scope=Scope.APP)
256
266
 
257
267
  async def _wire_event_listeners(self) -> None:
258
- """Wire event-listener mappings from all providers onto the dispatcher."""
268
+ """Wire event-listener mappings from all providers onto the dispatcher.
269
+
270
+ Raises:
271
+ RuntimeError: If listeners are declared but the dispatcher cannot
272
+ be resolved. Booting on regardless would leave every one of
273
+ them unwired, so the events would simply never fire — with
274
+ nothing anywhere saying why.
275
+ """
259
276
  from neva.events.dispatcher import EventDispatcher
260
277
 
261
- result = await self.make_async(EventDispatcher)
262
- if result.is_err:
278
+ declared = [
279
+ (event_cls, listener_cls)
280
+ for provider in self.providers.values()
281
+ for event_cls, listeners in provider.listen.items()
282
+ for listener_cls in listeners
283
+ ]
284
+ if not declared:
263
285
  return
264
286
 
265
- dispatcher = result.unwrap()
266
- for provider in self.providers.values():
267
- for event_cls, listeners in provider.listen.items():
268
- for listener_cls in listeners:
287
+ match await self.make_async(EventDispatcher):
288
+ case Err(e):
289
+ msg = (
290
+ f"Cannot wire {len(declared)} declared event listener(s): "
291
+ f"the EventDispatcher could not be resolved ({e}). "
292
+ "Register EventServiceProvider, or remove the 'listen' "
293
+ "mappings that need it."
294
+ )
295
+ raise RuntimeError(msg)
296
+ case Ok(dispatcher):
297
+ for event_cls, listener_cls in declared:
269
298
  dispatcher.listen(event_cls, listener_cls)
@@ -0,0 +1,49 @@
1
+ """Config shapes for the namespaces the core itself reads.
2
+
3
+ Annotate a config file with one of these to have a type checker catch typos
4
+ and wrong value types before boot:
5
+
6
+ ```python
7
+ # src/config/app.py
8
+ from neva.arch import AppConfig
9
+
10
+ config: AppConfig = {"providers": [MyProvider]}
11
+ ```
12
+
13
+ Keys read by an integration package are declared by that package, not here —
14
+ `neva-fastapi` owns the OpenAPI and title keys of the `app` namespace, for
15
+ instance. The core only describes what the core consumes.
16
+ """
17
+
18
+ from typing import NotRequired, TypedDict
19
+
20
+ from neva.arch.service_provider import ServiceProvider
21
+
22
+
23
+ class AppConfig(TypedDict):
24
+ """The `app` namespace keys the core reads.
25
+
26
+ Attributes:
27
+ key: Base64-encoded 32-byte key for `Crypt`. Generate one with
28
+ `AesEncrypter.generate_key()`.
29
+ previous_keys: Superseded keys, still accepted when decrypting so a
30
+ key rotation does not orphan existing ciphertext.
31
+ providers: Service providers to register, appended after those from
32
+ the `providers` namespace.
33
+ """
34
+
35
+ key: NotRequired[str]
36
+ previous_keys: NotRequired[list[str]]
37
+ providers: NotRequired[list[type[ServiceProvider]]]
38
+
39
+
40
+ class ProviderConfig(TypedDict):
41
+ """The `providers` namespace.
42
+
43
+ A dedicated namespace for the provider list, read before `app.providers`.
44
+
45
+ Attributes:
46
+ providers: Service providers to register.
47
+ """
48
+
49
+ providers: NotRequired[list[type[ServiceProvider]]]
@@ -1,18 +1,23 @@
1
1
  """FastStream wrapper for Neva."""
2
2
 
3
- from collections.abc import AsyncGenerator, AsyncIterator
4
- from contextlib import asynccontextmanager
3
+ from collections.abc import AsyncGenerator, AsyncIterator, Callable
4
+ from contextlib import AbstractAsyncContextManager, asynccontextmanager
5
5
  from typing import Any
6
6
 
7
7
  import faststream
8
8
  from faststream._internal.broker import BrokerUsecase
9
9
  from faststream._internal.configs import BrokerConfig
10
- from starlette.types import StatelessLifespan
11
10
 
12
11
  from neva.arch import Application, ServiceProvider
13
12
  from neva.support import Result
14
13
 
15
14
 
15
+ # Spelled out locally rather than borrowed from starlette.types: a messaging
16
+ # app's lifespan is not a web concern, and the core must not import an ASGI
17
+ # framework to describe one.
18
+ type FastStreamLifespan = Callable[["FastStream"], AbstractAsyncContextManager[None]]
19
+
20
+
16
21
  class FastStream(faststream.FastStream):
17
22
  """FastStream wrapper for Neva."""
18
23
 
@@ -42,9 +47,7 @@ class FastStream(faststream.FastStream):
42
47
  async with self.application.lifespan():
43
48
  yield
44
49
 
45
- def _create_lifespan(
46
- self,
47
- ) -> StatelessLifespan["FastStream"]:
50
+ def _create_lifespan(self) -> FastStreamLifespan:
48
51
  @asynccontextmanager
49
52
  async def composed_lifespan(_: faststream.FastStream) -> AsyncIterator[None]:
50
53
  async with self.lifespan():
@@ -8,7 +8,6 @@ container and optionally managing their lifecycle.
8
8
  from __future__ import annotations
9
9
 
10
10
  import abc
11
- import warnings
12
11
  from contextlib import AbstractAsyncContextManager
13
12
  from typing import (
14
13
  TYPE_CHECKING,
@@ -115,9 +114,9 @@ class ServiceProvider(abc.ABC):
115
114
  `instance` — which say what they mean. Use `bind` directly only to
116
115
  reach a scope those do not cover, and then pass `scope` explicitly.
117
116
 
118
- Calling this with neither `scope` nor `cache` is deprecated: the
119
- resulting binding is a process-wide singleton, which is the opposite of
120
- Laravel's transient `bind`, and nothing at the call site says so.
117
+ A lifetime is required. Without one the binding would be a
118
+ process-wide singleton the opposite of Laravel's transient `bind` —
119
+ with nothing at the call site saying so.
121
120
 
122
121
  Args:
123
122
  source: The class or factory to bind.
@@ -125,16 +124,17 @@ class ServiceProvider(abc.ABC):
125
124
  scope: The dishka scope. Defaults to the provider's (APP).
126
125
  when: Marker gating this binding.
127
126
  cache: Whether to reuse one instance per scope. Defaults to True.
127
+
128
+ Raises:
129
+ TypeError: If neither `scope` nor `cache` is given.
128
130
  """
129
131
  if scope is None and cache is None:
130
- warnings.warn(
131
- "bind() without an explicit lifetime is deprecated and will "
132
- "require one in 4.0. It currently creates a process-wide "
133
- "singleton: use singleton() for that, or scoped() / "
134
- "transient() / instance(), or pass scope= or cache= yourself.",
135
- DeprecationWarning,
136
- stacklevel=2,
132
+ msg = (
133
+ "bind() requires an explicit lifetime: use singleton() for a "
134
+ "process-wide instance, or scoped() / transient() / "
135
+ "instance(), or pass scope= or cache= yourself."
137
136
  )
137
+ raise TypeError(msg)
138
138
 
139
139
  _ = self.provider.provide(
140
140
  source=source,
@@ -7,6 +7,7 @@ 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
10
11
 
11
12
  from neva.database.transaction import BoundTransaction, TransactionState
12
13
  from neva.obs import LogManager
@@ -100,7 +101,7 @@ class ConnectionManager:
100
101
 
101
102
  @property
102
103
  def engine(self) -> asyncio.AsyncEngine:
103
- """Returns the underlying engine."""
104
+ """The underlying engine."""
104
105
  return self._engine
105
106
 
106
107
  async def dispose(self) -> None:
@@ -147,9 +148,13 @@ class ConnectionManager:
147
148
  else await tx.execute_on_rollback_callbacks()
148
149
  )
149
150
  label = "commit" if committed else "rollback"
151
+ # 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__)
150
155
  for result in results:
151
- if result.is_err and self.logger is not None:
152
- self.logger.error(
156
+ if result.is_err:
157
+ logger.error(
153
158
  f"{label} callback failed",
154
159
  error=result.err().unwrap(),
155
160
  connection=self.name,
@@ -5,7 +5,6 @@ from contextlib import asynccontextmanager
5
5
  from typing import final
6
6
 
7
7
  from sqlalchemy.ext import asyncio
8
- from typing_extensions import deprecated
9
8
 
10
9
  from neva.database.config import ConnectionConfig
11
10
  from neva.database.connection import ConnectionManager, TransactionContext
@@ -24,9 +23,14 @@ class DatabaseManager:
24
23
  self._logger = logger
25
24
  self._connections: dict[str, ConnectionManager] = {}
26
25
 
27
- @deprecated("Please use DatabaseManager.register_connection instead.")
28
26
  def register_engine(self, name: str, engine: asyncio.AsyncEngine) -> None:
29
- """Register an engine and create a session factory for a connection.
27
+ """Register a connection around an engine you already built.
28
+
29
+ Prefer `register_connection`, which builds the engine from config. Use
30
+ this when the caller must own the engine — a test that creates the
31
+ schema on an in-memory database has to hand over that exact engine,
32
+ since a second engine to `sqlite:///:memory:` is a second, empty
33
+ database.
30
34
 
31
35
  Args:
32
36
  name: The connection name.
@@ -1,17 +1,41 @@
1
1
  """Transaction management systems."""
2
2
 
3
3
  import asyncio
4
- from collections.abc import Awaitable
4
+ import inspect
5
+ from collections.abc import Awaitable, Callable
5
6
  from dataclasses import dataclass, field
6
7
  from enum import Enum, auto
7
- from typing import Callable, Self
8
+ from typing import Self
8
9
 
9
10
  from sqlalchemy.ext.asyncio import AsyncSession
10
11
 
11
- from neva.support import Err, Nothing, Option, Result, Some
12
+ from neva.support import Err, Nothing, Ok, Option, Result, Some
12
13
 
13
14
 
14
- type TransactionCallback = Callable[[], Awaitable[Result[None, str]]]
15
+ type TransactionOutcome = Result[None, str] | None
16
+ type TransactionCallback = Callable[
17
+ [], TransactionOutcome | Awaitable[TransactionOutcome]
18
+ ]
19
+
20
+
21
+ async def _run_callback(callback: TransactionCallback) -> Result[None, str]:
22
+ """Invoke a transaction callback, whether it is sync or async.
23
+
24
+ A callback may return a ``Result`` to report its own failure, or ``None``
25
+ to say "done, nothing to report" — which is what a bare side-effecting
26
+ lambda does.
27
+
28
+ Returns:
29
+ The callback's own Result, Ok(None) if it returned None, or Err if it
30
+ raised.
31
+ """
32
+ try:
33
+ outcome = callback()
34
+ if inspect.isawaitable(outcome):
35
+ outcome = await outcome
36
+ except Exception as e:
37
+ return Err(f"Callback raised: {e}")
38
+ return Ok(None) if outcome is None else outcome
15
39
 
16
40
 
17
41
  class TransactionState(Enum):
@@ -77,10 +101,7 @@ class Transaction:
77
101
  """
78
102
  results: list[Result[None, str]] = []
79
103
  for callback in self._on_commit:
80
- try:
81
- results.append(await callback())
82
- except Exception as e:
83
- results.append(Err(f"Callback raised: {e}"))
104
+ results.append(await _run_callback(callback))
84
105
  self._on_commit.clear()
85
106
  return results
86
107
 
@@ -92,10 +113,7 @@ class Transaction:
92
113
  """
93
114
  results: list[Result[None, str]] = []
94
115
  for callback in self._on_rollback:
95
- try:
96
- results.append(await callback())
97
- except Exception as e:
98
- results.append(Err(f"Callback raised: {e}"))
116
+ results.append(await _run_callback(callback))
99
117
  self._on_rollback.clear()
100
118
  return results
101
119
 
@@ -134,7 +152,7 @@ class BoundTransaction(Transaction):
134
152
 
135
153
  @property
136
154
  def is_accessible_from_current_task(self) -> bool:
137
- """Return True if the current asyncio task may use this transaction's session.
155
+ """Whether the current asyncio task may use this transaction's session.
138
156
 
139
157
  Returns:
140
158
  True if the transaction was created by the current task, or if
@@ -0,0 +1,9 @@
1
+ """Observability tooling."""
2
+
3
+ from neva.obs.logging import LogManager, LogServiceProvider
4
+
5
+
6
+ __all__ = [
7
+ "LogManager",
8
+ "LogServiceProvider",
9
+ ]