python-neva 3.5.0__tar.gz → 3.6.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 (162) hide show
  1. python_neva-3.6.0/.claude/settings.local.json +18 -0
  2. {python_neva-3.5.0 → python_neva-3.6.0}/.gitignore +0 -1
  3. python_neva-3.6.0/.pre-commit-config.yaml +33 -0
  4. python_neva-3.6.0/CHANGELOG.md +74 -0
  5. python_neva-3.6.0/CLAUDE.md +141 -0
  6. python_neva-3.6.0/PKG-INFO +106 -0
  7. python_neva-3.6.0/README.md +82 -0
  8. {python_neva-3.5.0 → python_neva-3.6.0}/neva/arch/application.py +88 -22
  9. {python_neva-3.5.0 → python_neva-3.6.0}/neva/arch/config.py +2 -2
  10. {python_neva-3.5.0 → python_neva-3.6.0}/neva/arch/scopes.py +0 -1
  11. {python_neva-3.5.0 → python_neva-3.6.0}/neva/arch/service_provider.py +82 -7
  12. {python_neva-3.5.0 → python_neva-3.6.0}/neva/config/loader.py +14 -2
  13. {python_neva-3.5.0 → python_neva-3.6.0}/neva/config/repository.py +24 -5
  14. {python_neva-3.5.0 → python_neva-3.6.0}/neva/database/connection.py +63 -46
  15. {python_neva-3.5.0 → python_neva-3.6.0}/neva/database/manager.py +11 -0
  16. {python_neva-3.5.0 → python_neva-3.6.0}/neva/database/provider.py +8 -6
  17. {python_neva-3.5.0 → python_neva-3.6.0}/neva/database/transaction.py +1 -2
  18. {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/contracts/__init__.py +2 -0
  19. {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/dispatcher.py +12 -9
  20. {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/event_registry.py +40 -3
  21. {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/listener.py +2 -3
  22. {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/provider.py +3 -3
  23. {python_neva-3.5.0 → python_neva-3.6.0}/neva/obs/logging/provider.py +1 -1
  24. {python_neva-3.5.0 → python_neva-3.6.0}/neva/polyfactory/__init__.py +2 -0
  25. {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/hashing/hash_manager.py +49 -2
  26. {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/hashing/hashers/bcrypt.py +1 -1
  27. {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/provider.py +2 -2
  28. {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/config.pyi +6 -3
  29. {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/hash.pyi +45 -1
  30. {python_neva-3.5.0 → python_neva-3.6.0}/pyproject.toml +14 -8
  31. {python_neva-3.5.0 → python_neva-3.6.0}/tests/arch/test_context.py +0 -2
  32. {python_neva-3.5.0 → python_neva-3.6.0}/tests/arch/test_extends.py +1 -1
  33. python_neva-3.6.0/tests/arch/test_lifetimes.py +179 -0
  34. python_neva-3.6.0/tests/arch/test_registration.py +156 -0
  35. {python_neva-3.5.0 → python_neva-3.6.0}/tests/arch/test_scope.py +15 -0
  36. {python_neva-3.5.0 → python_neva-3.6.0}/tests/config/test_loader.py +40 -0
  37. {python_neva-3.5.0 → python_neva-3.6.0}/tests/config/test_repository.py +48 -0
  38. python_neva-3.6.0/tests/database/test_detached_lifespan.py +124 -0
  39. {python_neva-3.5.0 → python_neva-3.6.0}/tests/database/test_sqlalchemy_integration.py +34 -0
  40. {python_neva-3.5.0 → python_neva-3.6.0}/tests/database/test_transaction_context.py +27 -1
  41. {python_neva-3.5.0 → python_neva-3.6.0}/tests/events/conftest.py +16 -0
  42. {python_neva-3.5.0 → python_neva-3.6.0}/tests/events/test_deferred.py +35 -1
  43. {python_neva-3.5.0 → python_neva-3.6.0}/tests/events/test_dispatch.py +1 -3
  44. {python_neva-3.5.0 → python_neva-3.6.0}/tests/events/test_listen_on_parent_class.py +39 -0
  45. {python_neva-3.5.0 → python_neva-3.6.0}/tests/security/test_encrypter.py +3 -3
  46. python_neva-3.6.0/tests/security/test_hash_manager.py +206 -0
  47. {python_neva-3.5.0 → python_neva-3.6.0}/tests/testing/test_event_fake.py +1 -0
  48. {python_neva-3.5.0 → python_neva-3.6.0}/tests/testing/test_fixtures.py +1 -1
  49. {python_neva-3.5.0 → python_neva-3.6.0}/tests/testing/test_refresh_database.py +2 -1
  50. {python_neva-3.5.0 → python_neva-3.6.0}/tests/testing/test_test_case.py +2 -0
  51. {python_neva-3.5.0 → python_neva-3.6.0}/uv.lock +22 -165
  52. python_neva-3.6.0/wiki/architecture/KNOWN-DISCREPANCIES.md +23 -0
  53. python_neva-3.6.0/wiki/architecture/configuration.md +315 -0
  54. python_neva-3.6.0/wiki/architecture/database-and-transactions.md +404 -0
  55. python_neva-3.6.0/wiki/architecture/dependency-injection.md +583 -0
  56. python_neva-3.6.0/wiki/architecture/events.md +424 -0
  57. python_neva-3.6.0/wiki/architecture/result-option.md +296 -0
  58. python_neva-3.6.0/wiki/architecture/testing.md +367 -0
  59. python_neva-3.5.0/.pre-commit-config.yaml +0 -45
  60. python_neva-3.5.0/CHANGELOG.md +0 -36
  61. python_neva-3.5.0/PKG-INFO +0 -27
  62. python_neva-3.5.0/README.md +0 -3
  63. python_neva-3.5.0/tests/security/test_hash_manager.py +0 -92
  64. {python_neva-3.5.0 → python_neva-3.6.0}/.envrc +0 -0
  65. {python_neva-3.5.0 → python_neva-3.6.0}/.python-version +0 -0
  66. {python_neva-3.5.0 → python_neva-3.6.0}/neva/arch/__init__.py +0 -0
  67. {python_neva-3.5.0 → python_neva-3.6.0}/neva/arch/facade.py +0 -0
  68. {python_neva-3.5.0 → python_neva-3.6.0}/neva/arch/integrations/__init__.py +0 -0
  69. {python_neva-3.5.0 → python_neva-3.6.0}/neva/arch/integrations/faststream.py +0 -0
  70. {python_neva-3.5.0 → python_neva-3.6.0}/neva/arch/markers.py +0 -0
  71. {python_neva-3.5.0 → python_neva-3.6.0}/neva/arch/py.typed +0 -0
  72. {python_neva-3.5.0 → python_neva-3.6.0}/neva/config/__init__.py +0 -0
  73. {python_neva-3.5.0 → python_neva-3.6.0}/neva/config/base_providers.py +0 -0
  74. {python_neva-3.5.0 → python_neva-3.6.0}/neva/config/py.typed +0 -0
  75. {python_neva-3.5.0 → python_neva-3.6.0}/neva/database/__init__.py +0 -0
  76. {python_neva-3.5.0 → python_neva-3.6.0}/neva/database/config.py +0 -0
  77. {python_neva-3.5.0 → python_neva-3.6.0}/neva/database/py.typed +0 -0
  78. {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/__init__.py +0 -0
  79. {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/contracts/dispatcher.py +0 -0
  80. {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/contracts/event.py +0 -0
  81. {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/contracts/handler.py +0 -0
  82. {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/contracts/listener.py +0 -0
  83. {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/event.py +0 -0
  84. {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/policy.py +0 -0
  85. {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/py.typed +0 -0
  86. {python_neva-3.5.0 → python_neva-3.6.0}/neva/obs/__init__.py +0 -0
  87. {python_neva-3.5.0 → python_neva-3.6.0}/neva/obs/instrumentation/__init__.py +0 -0
  88. {python_neva-3.5.0 → python_neva-3.6.0}/neva/obs/instrumentation/sqlalchemy.py +0 -0
  89. {python_neva-3.5.0 → python_neva-3.6.0}/neva/obs/logging/__init__.py +0 -0
  90. {python_neva-3.5.0 → python_neva-3.6.0}/neva/obs/logging/manager.py +0 -0
  91. {python_neva-3.5.0 → python_neva-3.6.0}/neva/obs/middleware/__init__.py +0 -0
  92. {python_neva-3.5.0 → python_neva-3.6.0}/neva/obs/middleware/correlation.py +0 -0
  93. {python_neva-3.5.0 → python_neva-3.6.0}/neva/obs/middleware/profiler.py +0 -0
  94. {python_neva-3.5.0 → python_neva-3.6.0}/neva/obs/py.typed +0 -0
  95. {python_neva-3.5.0 → python_neva-3.6.0}/neva/polyfactory/factories.py +0 -0
  96. {python_neva-3.5.0 → python_neva-3.6.0}/neva/polyfactory/persistence.py +0 -0
  97. {python_neva-3.5.0 → python_neva-3.6.0}/neva/polyfactory/py.typed +0 -0
  98. {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/__init__.py +0 -0
  99. {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/encryption/__init__.py +0 -0
  100. {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/encryption/encrypter.py +0 -0
  101. {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/encryption/protocol.py +0 -0
  102. {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/hashing/__init__.py +0 -0
  103. {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/hashing/config.py +0 -0
  104. {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/hashing/hashers/__init__.py +0 -0
  105. {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/hashing/hashers/argon2.py +0 -0
  106. {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/hashing/hashers/protocol.py +0 -0
  107. {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/py.typed +0 -0
  108. {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/tokens/__init__.py +0 -0
  109. {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/tokens/generate_token.py +0 -0
  110. {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/tokens/hash_token.py +0 -0
  111. {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/tokens/verify_token.py +0 -0
  112. {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/__init__.py +0 -0
  113. {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/accessors.py +0 -0
  114. {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/__init__.py +0 -0
  115. {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/app.py +0 -0
  116. {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/app.pyi +0 -0
  117. {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/config.py +0 -0
  118. {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/crypt.py +0 -0
  119. {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/crypt.pyi +0 -0
  120. {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/db.py +0 -0
  121. {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/db.pyi +0 -0
  122. {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/event.py +0 -0
  123. {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/event.pyi +0 -0
  124. {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/hash.py +0 -0
  125. {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/log.py +0 -0
  126. {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/log.pyi +0 -0
  127. {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/py.typed +0 -0
  128. {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/results.py +0 -0
  129. {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/strategy.py +0 -0
  130. {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/strconv.py +0 -0
  131. {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/time.py +0 -0
  132. {python_neva-3.5.0 → python_neva-3.6.0}/neva/testing/__init__.py +0 -0
  133. {python_neva-3.5.0 → python_neva-3.6.0}/neva/testing/fakes.py +0 -0
  134. {python_neva-3.5.0 → python_neva-3.6.0}/neva/testing/fixtures.py +0 -0
  135. {python_neva-3.5.0 → python_neva-3.6.0}/neva/testing/py.typed +0 -0
  136. {python_neva-3.5.0 → python_neva-3.6.0}/neva/testing/test_case.py +0 -0
  137. {python_neva-3.5.0 → python_neva-3.6.0}/ruff.toml +0 -0
  138. {python_neva-3.5.0 → python_neva-3.6.0}/scripts/retag-with-changelog.sh +0 -0
  139. {python_neva-3.5.0 → python_neva-3.6.0}/tests/__init__.py +0 -0
  140. {python_neva-3.5.0 → python_neva-3.6.0}/tests/arch/__init__.py +0 -0
  141. {python_neva-3.5.0 → python_neva-3.6.0}/tests/arch/test_cache.py +0 -0
  142. {python_neva-3.5.0 → python_neva-3.6.0}/tests/config/__init__.py +0 -0
  143. {python_neva-3.5.0 → python_neva-3.6.0}/tests/conftest.py +0 -0
  144. {python_neva-3.5.0 → python_neva-3.6.0}/tests/database/__init__.py +0 -0
  145. {python_neva-3.5.0 → python_neva-3.6.0}/tests/database/test_connection_manager.py +0 -0
  146. {python_neva-3.5.0 → python_neva-3.6.0}/tests/database/test_database_manager.py +0 -0
  147. {python_neva-3.5.0 → python_neva-3.6.0}/tests/database/test_edge_cases.py +0 -0
  148. {python_neva-3.5.0 → python_neva-3.6.0}/tests/database/test_multi_connection.py +0 -0
  149. {python_neva-3.5.0 → python_neva-3.6.0}/tests/database/test_transaction.py +0 -0
  150. {python_neva-3.5.0 → python_neva-3.6.0}/tests/database/test_transaction_registry.py +0 -0
  151. {python_neva-3.5.0 → python_neva-3.6.0}/tests/events/__init__.py +0 -0
  152. {python_neva-3.5.0 → python_neva-3.6.0}/tests/events/test_before_dispatch.py +0 -0
  153. {python_neva-3.5.0 → python_neva-3.6.0}/tests/events/test_binding.py +0 -0
  154. {python_neva-3.5.0 → python_neva-3.6.0}/tests/events/test_event.py +0 -0
  155. {python_neva-3.5.0 → python_neva-3.6.0}/tests/events/test_function_listener.py +0 -0
  156. {python_neva-3.5.0 → python_neva-3.6.0}/tests/events/test_immediate.py +0 -0
  157. {python_neva-3.5.0 → python_neva-3.6.0}/tests/obs/__init__.py +0 -0
  158. {python_neva-3.5.0 → python_neva-3.6.0}/tests/obs/test_correlation.py +0 -0
  159. {python_neva-3.5.0 → python_neva-3.6.0}/tests/obs/test_profiler.py +0 -0
  160. {python_neva-3.5.0 → python_neva-3.6.0}/tests/security/__init__.py +0 -0
  161. {python_neva-3.5.0 → python_neva-3.6.0}/tests/testing/__init__.py +0 -0
  162. {python_neva-3.5.0 → python_neva-3.6.0}/tests/testing/test_facade_restore.py +0 -0
@@ -0,0 +1,18 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Read(//home/m.bigaignon/.claude/**)",
5
+ "Read(//home/m.bigaignon/projects/neva/**)",
6
+ "Bash(git -C /home/m.bigaignon/projects/neva/neva tag)",
7
+ "Bash(git *)",
8
+ "Bash(python -c \"import dishka; print\\(dishka.__version__ if hasattr\\(dishka,'__version__'\\) else 'n/a'\\); print\\([n for n in dir\\(dishka\\) if not n.startswith\\('_'\\)]\\)\")",
9
+ "Bash(.venv/bin/python -c \"import dishka; print\\([n for n in dir\\(dishka\\) if not n.startswith\\('_'\\)]\\)\")",
10
+ "Bash(python .github/scripts/validate-skill-frontmatter.py)",
11
+ "Bash(npx --yes markdownlint-cli2 \"python/skills/**/*.md\")",
12
+ "Read(//home/m.bigaignon/projects/xefi/claude-skills/**)",
13
+ "Bash(python -)",
14
+ "Bash(npx --yes markdownlint-cli2 \"python/**/*.md\")",
15
+ "Bash(BASE_REF=origin/main python .github/scripts/check-version-bump.py)"
16
+ ]
17
+ }
18
+ }
@@ -18,4 +18,3 @@ profiles/
18
18
 
19
19
  # LLM Contexts
20
20
  llm-context.md
21
- CLAUDE.md
@@ -0,0 +1,33 @@
1
+ repos:
2
+ - repo: https://github.com/gitleaks/gitleaks
3
+ rev: v8.30.1
4
+ hooks:
5
+ - id: gitleaks
6
+
7
+ - repo: https://github.com/pre-commit/pre-commit-hooks
8
+ rev: v6.0.0
9
+ hooks:
10
+ - id: trailing-whitespace
11
+ - id: end-of-file-fixer
12
+ - id: check-yaml
13
+ - id: check-added-large-files
14
+ - id: check-case-conflict
15
+ - id: check-merge-conflict
16
+ - id: debug-statements
17
+
18
+ - repo: https://github.com/astral-sh/ruff-pre-commit
19
+ rev: v0.15.6
20
+ hooks:
21
+ - id: ruff
22
+ args: [--fix, --exit-non-zero-on-fix]
23
+ - id: ruff-format
24
+
25
+ - repo: local
26
+ hooks:
27
+ - id: pyrefly
28
+ name: Pyrefly (type checking)
29
+ entry: uv run pyrefly check
30
+ language: system
31
+ types_or: [python, pyi]
32
+ pass_filenames: false
33
+ require_serial: true
@@ -0,0 +1,74 @@
1
+ ## 3.6.0 (2026-08-28)
2
+
3
+ ### ✨ Features
4
+
5
+ - **arch**: name the binding lifetimes, deprecate the implicit one
6
+ - **security**: add make_async and check_async
7
+ - **arch**: expose the container backing the current scope
8
+
9
+ ### 🐛🚑️ Fixes
10
+
11
+ - **events**: dispatch once per registration, and defer to the outermost transaction
12
+ - **config**: make freeze() actually freeze and merge() recursive
13
+ - **config**: stop config files from displacing real modules
14
+ - **arch**: run lifespan teardown even when the body raises
15
+ - **security**: stop requiring the optional hasher tuning config
16
+ - **arch**: make register() bind, or say why it cannot
17
+ - **database**: keep the transaction when the registry contextvar is unset
18
+
19
+ ### 📝💡 Documentation
20
+
21
+ - **general**: correct the Config stub, a lying docstring and a wrong annotation
22
+
23
+ ### 🔧🔨📦️ Configuration, Scripts, Packages
24
+
25
+ - **tooling**: replace mypy with pyrefly at strict
26
+
27
+ ## 3.5.1 (2026-08-24)
28
+
29
+ ### 🐛🚑️ Fixes
30
+
31
+ - **database**: run commit callbacks after the commit, not before
32
+
33
+ ### 📝💡 Documentation
34
+
35
+ - **general**: update docs
36
+ - **README**: very short section on contribution
37
+ - **docs**: extension ideas
38
+
39
+ ## 3.5.0 (2026-06-11)
40
+
41
+ ### ✨ Features
42
+
43
+ - **arch**: improve app facade stub
44
+ - **events**: make the event facade properly depend on the dispatcher contract
45
+ - **events**: new tests for events
46
+
47
+ ### 🐛🚑️ Fixes
48
+
49
+ - **events**: fixing handling of async before-dispatch hooks
50
+
51
+ ### 🏷️ Types
52
+
53
+ - **events**: improve event facade stub
54
+
55
+ ## 3.4.0 (2026-06-10)
56
+
57
+ ### ✨ Features
58
+
59
+ - **arch**: new from_context method
60
+ - **tooling**: placeholder commit
61
+
62
+ ### 💚👷 CI & Build
63
+
64
+ - **tooling**: default to base tag version for commitizen
65
+
66
+ ### 📝💡 Documentation
67
+
68
+ - **project**: update doc
69
+ - **core**: placeholde readme
70
+
71
+ ### 🔧🔨📦️ Configuration, Scripts, Packages
72
+
73
+ - **tooling**: update changelog script
74
+ - **project**: update versioning config
@@ -0,0 +1,141 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ `python-neva` is the **core** of the Neva framework — a Laravel-inspired toolkit
8
+ for Python. It provides dependency injection (dishka), service providers,
9
+ facades, Result/Option types for error handling, security features, a
10
+ transaction-aware event system, and a SQLAlchemy-based database layer.
11
+
12
+ **Critical rule — the core MUST stay framework-agnostic.** No web/HTTP types,
13
+ no messaging concerns in this package. FastAPI-specific code belongs in
14
+ `neva-fastapi`, FastStream in `neva-faststream`, auth in `neva-auth`. If you
15
+ find yourself reaching for `fastapi.*` (or any framework binding) inside
16
+ `neva/`, that's a smell — it belongs in an integration package. The plugin
17
+ ecosystem is listed in `README.md`.
18
+
19
+ **Python Version:** 3.12+
20
+ **Build System:** uv + hatchling
21
+ **Type checker:** pyrefly (`preset = "strict"`, configured in `[tool.pyrefly]`)
22
+
23
+ ## Commands
24
+
25
+ ```bash
26
+ # Development (poe task aliases)
27
+ poe lint # ruff check
28
+ poe fmt # ruff format
29
+ poe tc # pyrefly check
30
+ poe test # pytest
31
+ poe test-cov # tests with coverage (--cov-report=term-missing)
32
+ poe test-full # coverage over the neva package
33
+
34
+ # Run a single test
35
+ pytest tests/test_file.py::TestClass::test_method -v
36
+ ```
37
+
38
+ `asyncio_mode = "auto"` — async tests need no `@pytest.mark.asyncio`.
39
+
40
+ ## Architecture
41
+
42
+ ```
43
+ neva/
44
+ ├── arch/ # Core: DI container, application, facades, service providers, scopes
45
+ ├── config/ # Configuration system (repository, loader, providers)
46
+ ├── database/ # Database layer (SQLAlchemy async, transactions, manager)
47
+ ├── events/ # Transaction-aware event system with dispatch policies
48
+ ├── security/ # Hashing (argon2/bcrypt), encryption (AES-GCM), tokens
49
+ ├── obs/ # Observability: structured logging (structlog), profiling
50
+ ├── polyfactory/ # Factory helpers built on Polyfactory
51
+ ├── support/ # Utilities: Result/Option types, facades, time/string helpers
52
+ └── testing/ # TestCase base, RefreshDatabase, fixtures
53
+ ```
54
+
55
+ ## Key Patterns
56
+
57
+ **Error Handling** - Always return `Result[T, str]` or `Option[T]` for fallible operations:
58
+ ```python
59
+ def resolve(self, interface: type[T]) -> Result[T, str]:
60
+ try:
61
+ return Ok(self.container.get(interface))
62
+ except Exception as e:
63
+ return Err(f"Failed: {e}")
64
+ ```
65
+
66
+ **Facades** - Static interfaces to services:
67
+ ```python
68
+ from neva.support.facade import DB, Hash, Config, Crypt, Log, Event
69
+
70
+ hashed = Hash.make("password")
71
+ config = Config.get("app.name").unwrap_or("MyApp")
72
+ await Event.dispatch(UserCreated(event_id=1, user_id=42))
73
+ ```
74
+
75
+ **Service Providers** - Register services with lifecycle hooks. Bindings
76
+ declare a lifetime by name — `singleton` (one per process), `scoped` (one per
77
+ REQUEST scope), `transient` (one per resolution), `instance` (an object you
78
+ already have). Bare `bind` without an explicit `scope`/`cache` is deprecated:
79
+ ```python
80
+ class MyProvider(ServiceProvider):
81
+ def register(self) -> Result[Self, str]:
82
+ _ = self.singleton(MyService)
83
+ return Ok(self)
84
+
85
+ @asynccontextmanager
86
+ async def lifespan(self) -> AsyncIterator[None]:
87
+ yield # startup/shutdown
88
+ ```
89
+
90
+ **Events** - Transaction-aware dispatch with IMMEDIATE and DEFERRED policies:
91
+ ```python
92
+ # Events are generic Pydantic models with an event_id and auto-timestamp
93
+ class UserCreated(Event[int]):
94
+ user_id: int
95
+
96
+ # Class-based listener (default policy: IMMEDIATE)
97
+ class SendWelcomeEmail(EventListener[UserCreated]):
98
+ policy = HandlingPolicy.DEFERRED
99
+
100
+ async def handle(self, event: UserCreated) -> Result[None, str]:
101
+ await mailer.send(event.user_id)
102
+ return Ok(None)
103
+
104
+ # Function-based listener via decorator
105
+ @listener(policy=HandlingPolicy.DEFERRED)
106
+ async def log_user_created(event: UserCreated) -> Result[None, str]:
107
+ log.info("user created", user_id=event.user_id)
108
+ return Ok(None)
109
+ ```
110
+
111
+ DEFERRED listeners execute after the enclosing transaction commits; outside a transaction they fire immediately. Listeners are resolved from the DI container with fallback to direct instantiation.
112
+
113
+ **Testing** - Async by default with TestCase base:
114
+ ```python
115
+ class TestMyService(TestCase):
116
+ async def test_something(self):
117
+ service = self.app.make(MyService).unwrap()
118
+ result = await service.do_something()
119
+ assert result.is_ok
120
+ ```
121
+
122
+ For database tests, subclass `RefreshDatabase` (wraps each test in a transaction and rolls it back).
123
+
124
+ ## Code Conventions
125
+
126
+ - **Type hints required** - Full annotations, uses Python 3.12+ features
127
+ - **Google-style docstrings** - Enforced by ruff DOC/D rules
128
+ - **Async first** - All I/O operations are async/await
129
+ - **Naming**: Events past tense (UserCreated), Class listeners imperative (SendWelcomeEmail), Function listeners snake_case handlers (log_user_created), Services noun phrases (EmailService)
130
+ - **Ruff rules** (config in a separate `ruff.toml`, `preview = true`): FAST, ANN, ASYNC, S, FBT, B, A, C4, DTZ, ISC, ICN, SIM, SLOT, I, E, W, DOC, D, F, RUF
131
+
132
+ ## Commits & releases
133
+
134
+ Conventional Commits with gitmoji via `cz_gitmoji` (`cz commit`). Releases are
135
+ cut with `cz bump`, which writes `CHANGELOG.md` and runs
136
+ `scripts/retag-with-changelog.sh` to annotate the new `v<version>` tag with the
137
+ rendered changelog. See `README.md` for the full flow and the plugin list.
138
+
139
+ ## Documentation
140
+
141
+ - Architecture guides: `wiki/architecture/`
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.5
2
+ Name: python-neva
3
+ Version: 3.6.0
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: aiosqlite>=0.20.0
7
+ Requires-Dist: asyncpg>=0.30.0
8
+ Requires-Dist: cryptography>=46.0.3
9
+ Requires-Dist: dishka>=1.10.0
10
+ Requires-Dist: fastapi[all]>=0.129.0
11
+ Requires-Dist: faststream>=0.6.6
12
+ Requires-Dist: opentelemetry-instrumentation-sqlalchemy>=0.62b0
13
+ Requires-Dist: pwdlib[argon2,bcrypt]>=0.3.0
14
+ Requires-Dist: pyinstrument>=5.1.1
15
+ Requires-Dist: sqlalchemy[asyncio]>=2.0.0
16
+ Requires-Dist: structlog>=25.5.0
17
+ Requires-Dist: typer>=0.21.1
18
+ Provides-Extra: fastapi
19
+ Requires-Dist: neva-fastapi>=1.0.0; extra == 'fastapi'
20
+ Provides-Extra: testing
21
+ Requires-Dist: pytest-asyncio>=0.25.3; extra == 'testing'
22
+ Requires-Dist: pytest>=9.0.2; extra == 'testing'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # python-neva
26
+
27
+ The core of the **Neva** framework — a Laravel-inspired toolkit for Python that
28
+ brings Laravel's philosophy, terminology, and conventions to the Python
29
+ ecosystem: service providers, facades, `bind`/`make` dependency injection (on
30
+ [dishka](https://github.com/reagento/dishka)), `Hash`/`Crypt`/`Config`/`Log`/`Event`
31
+ facades, a transaction-aware event system, and Result/Option error handling.
32
+
33
+ `python-neva` is the **framework-agnostic core**. Web- and protocol-specific
34
+ concerns live in separate integration packages (the plugins below) so the core
35
+ never depends on a particular HTTP or messaging framework.
36
+
37
+ ## Plugins & ecosystem
38
+
39
+ Neva is split into one repository per package. The core is published as
40
+ [`python-neva`](https://pypi.org/project/python-neva/); each plugin is an
41
+ independent repo, independently versioned and published.
42
+
43
+ | Package | Repo / directory | Role | Status |
44
+ | ----------------- | ------------------ | ------------------------------------------------------------------------------------- | ------------------------------- |
45
+ | `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
+ | `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/)) |
47
+ | `neva-faststream` | `neva-faststream/` | FastStream (messaging) integration. | scaffolded, early placeholder |
48
+ | `neva-auth` | `neva-auth/` | Authentication & identity toolkit — token-issuance engine, OAuth2 grants, guards, pluggable formats/backends. | core complete, OAuth-server slice in progress |
49
+ | `neva-example` | `neva-example/` | Reference application consuming `python-neva[fastapi]`. Not published; demonstration + integration test bed. | unpublished |
50
+
51
+ > When working across packages locally, each repo can be cloned as a sibling
52
+ > directory; integration packages depend on `python-neva` from PyPI (or an
53
+ > editable local path during development).
54
+
55
+ ## Develop
56
+
57
+ ```bash
58
+ uv sync # install/refresh deps from uv.lock
59
+ poe lint # ruff check
60
+ poe fmt # ruff format
61
+ poe tc # type-check (pyrefly)
62
+ poe test # pytest
63
+ poe test-cov # tests with coverage
64
+
65
+ # Run a single test
66
+ pytest tests/test_file.py::TestClass::test_method -v
67
+ ```
68
+
69
+ `asyncio_mode = "auto"` is set, so async tests need no `@pytest.mark.asyncio`.
70
+
71
+ ## Contributing
72
+
73
+ **Commits** follow [Conventional Commits](https://www.conventionalcommits.org/)
74
+ with [gitmoji](https://gitmoji.dev/) prefixes, enforced by
75
+ [`cz_gitmoji`](https://github.com/ljnsn/cz-conventional-gitmoji). Commitizen is
76
+ provided as a dev dependency — no separate install needed. Run the guided
77
+ wizard:
78
+
79
+ ```bash
80
+ cz commit
81
+ ```
82
+
83
+ or format manually as `:gitmoji: type(scope): subject`.
84
+
85
+ **Releases** are cut per-repo with commitizen, from the repo root:
86
+
87
+ ```bash
88
+ cz bump # bump version in pyproject, write CHANGELOG, tag v<version>
89
+ git push --follow-tags origin main
90
+ uv build && uv publish # build + publish the wheel/sdist
91
+ ```
92
+
93
+ `cz bump` derives the bump level (major/minor/patch) from the commits since the
94
+ last tag, updates `CHANGELOG.md`, and runs `scripts/retag-with-changelog.sh` to
95
+ rewrite the new tag with the rendered changelog as its annotation.
96
+
97
+ ## Feature ideas
98
+
99
+ - Feature-parity FastStream integration (now scaffolded as `neva-faststream`)
100
+ - Finish cleaning up the core package (moving any remaining ASGI-dependent code into the appropriate integration packages)
101
+ - Improved router registration (auto-discovery OR provider-based? both?)
102
+ - Improved security tooling (performance improvements, better defaults, etc.)
103
+ - Better OpenTelemetry integration
104
+ - Improved factory module (based on Polyfactory)
105
+ - Queue/Jobs system
106
+ - CLI integration
@@ -0,0 +1,82 @@
1
+ # python-neva
2
+
3
+ The core of the **Neva** framework — a Laravel-inspired toolkit for Python that
4
+ brings Laravel's philosophy, terminology, and conventions to the Python
5
+ ecosystem: service providers, facades, `bind`/`make` dependency injection (on
6
+ [dishka](https://github.com/reagento/dishka)), `Hash`/`Crypt`/`Config`/`Log`/`Event`
7
+ facades, a transaction-aware event system, and Result/Option error handling.
8
+
9
+ `python-neva` is the **framework-agnostic core**. Web- and protocol-specific
10
+ concerns live in separate integration packages (the plugins below) so the core
11
+ never depends on a particular HTTP or messaging framework.
12
+
13
+ ## Plugins & ecosystem
14
+
15
+ Neva is split into one repository per package. The core is published as
16
+ [`python-neva`](https://pypi.org/project/python-neva/); each plugin is an
17
+ independent repo, independently versioned and published.
18
+
19
+ | Package | Repo / directory | Role | Status |
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/)) |
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-faststream` | `neva-faststream/` | FastStream (messaging) integration. | scaffolded, early placeholder |
24
+ | `neva-auth` | `neva-auth/` | Authentication & identity toolkit — token-issuance engine, OAuth2 grants, guards, pluggable formats/backends. | core complete, OAuth-server slice in progress |
25
+ | `neva-example` | `neva-example/` | Reference application consuming `python-neva[fastapi]`. Not published; demonstration + integration test bed. | unpublished |
26
+
27
+ > When working across packages locally, each repo can be cloned as a sibling
28
+ > directory; integration packages depend on `python-neva` from PyPI (or an
29
+ > editable local path during development).
30
+
31
+ ## Develop
32
+
33
+ ```bash
34
+ uv sync # install/refresh deps from uv.lock
35
+ poe lint # ruff check
36
+ poe fmt # ruff format
37
+ poe tc # type-check (pyrefly)
38
+ poe test # pytest
39
+ poe test-cov # tests with coverage
40
+
41
+ # Run a single test
42
+ pytest tests/test_file.py::TestClass::test_method -v
43
+ ```
44
+
45
+ `asyncio_mode = "auto"` is set, so async tests need no `@pytest.mark.asyncio`.
46
+
47
+ ## Contributing
48
+
49
+ **Commits** follow [Conventional Commits](https://www.conventionalcommits.org/)
50
+ with [gitmoji](https://gitmoji.dev/) prefixes, enforced by
51
+ [`cz_gitmoji`](https://github.com/ljnsn/cz-conventional-gitmoji). Commitizen is
52
+ provided as a dev dependency — no separate install needed. Run the guided
53
+ wizard:
54
+
55
+ ```bash
56
+ cz commit
57
+ ```
58
+
59
+ or format manually as `:gitmoji: type(scope): subject`.
60
+
61
+ **Releases** are cut per-repo with commitizen, from the repo root:
62
+
63
+ ```bash
64
+ cz bump # bump version in pyproject, write CHANGELOG, tag v<version>
65
+ git push --follow-tags origin main
66
+ uv build && uv publish # build + publish the wheel/sdist
67
+ ```
68
+
69
+ `cz bump` derives the bump level (major/minor/patch) from the commits since the
70
+ last tag, updates `CHANGELOG.md`, and runs `scripts/retag-with-changelog.sh` to
71
+ rewrite the new tag with the rendered changelog as its annotation.
72
+
73
+ ## Feature ideas
74
+
75
+ - Feature-parity FastStream integration (now scaffolded as `neva-faststream`)
76
+ - Finish cleaning up the core package (moving any remaining ASGI-dependent code into the appropriate integration packages)
77
+ - Improved router registration (auto-discovery OR provider-based? both?)
78
+ - Improved security tooling (performance improvements, better defaults, etc.)
79
+ - Better OpenTelemetry integration
80
+ - Improved factory module (based on Polyfactory)
81
+ - Queue/Jobs system
82
+ - CLI integration
@@ -43,6 +43,9 @@ class Application:
43
43
  self.config: ConfigRepository = ConfigRepository()
44
44
  self.providers: OrderedDict[type, ServiceProvider] = OrderedDict()
45
45
  self.root_provider: dishka.Provider = dishka.Provider(scope=Scope.APP)
46
+ self._extra_providers: list[BaseProvider] = []
47
+ self._container: dishka.AsyncContainer | None = None
48
+ self._booted: bool = False
46
49
 
47
50
  configuration_path = (
48
51
  config_path
@@ -77,30 +80,70 @@ class Application:
77
80
  _ = self.root_provider.provide(source=lambda: self, provides=Application)
78
81
  self.register_providers(providers)
79
82
  self._bind_event_listeners()
80
- self.build_container(self.root_provider)
83
+ self.build_container()
81
84
 
82
- def build_container(self, *providers: BaseProvider) -> None:
83
- """Build the application container."""
84
- self.container: dishka.AsyncContainer = dishka.make_async_container(
85
+ @property
86
+ def container(self) -> dishka.AsyncContainer:
87
+ """The application container, built on first access.
88
+
89
+ Registering a provider discards the built container so the next access
90
+ rebuilds the graph with that provider's bindings in it.
91
+
92
+ Returns:
93
+ The application container.
94
+ """
95
+ if self._container is not None:
96
+ return self._container
97
+ return self.build_container()
98
+
99
+ def build_container(self, *providers: BaseProvider) -> dishka.AsyncContainer:
100
+ """Build the application container.
101
+
102
+ Args:
103
+ providers: Extra dishka providers to compose into the graph. They
104
+ are remembered, so a later rebuild keeps them.
105
+
106
+ Returns:
107
+ The freshly built container.
108
+ """
109
+ self._extra_providers.extend(providers)
110
+ self._container = dishka.make_async_container(
85
111
  self.root_provider,
86
112
  *[p.provider for p in self.providers.values()],
87
- *providers,
113
+ *self._extra_providers,
88
114
  )
115
+ return self._container
89
116
 
90
117
  def register(self, provider: type[ServiceProvider]) -> Result[ServiceProvider, str]:
91
118
  """Registers a service provider with the application.
92
119
 
120
+ Providers must be registered before the application boots: dishka
121
+ validates the dependency graph when the container is built, and the
122
+ booted container already holds APP-scoped instances that a rebuild
123
+ would orphan. Registering after boot is an error rather than a
124
+ silently ignored call.
125
+
93
126
  Returns:
94
127
  Result containing the registered provider instance or an error message.
95
128
  """
96
129
  if provider in self.providers:
97
130
  return Ok(self.providers[provider])
98
131
 
99
- return (
132
+ if self._booted:
133
+ return Err(
134
+ f"Cannot register '{provider.__name__}': the application is "
135
+ "already booted. Register providers before entering lifespan(), "
136
+ "or declare them in the 'providers' config namespace."
137
+ )
138
+
139
+ registered = (
100
140
  provider(self)
101
141
  .register()
102
142
  .map(lambda p: self.providers.setdefault(provider, p))
103
143
  )
144
+ if registered.is_ok:
145
+ self._container = None
146
+ return registered
104
147
 
105
148
  def register_providers(self, providers: Sequence[type[ServiceProvider]]) -> None:
106
149
  """Registers a set of providers."""
@@ -122,15 +165,28 @@ class Application:
122
165
  provides=interface,
123
166
  )
124
167
 
168
+ @property
169
+ def current_container(self) -> dishka.AsyncContainer:
170
+ """The container backing resolution in the current context.
171
+
172
+ Inside ``scope()`` this is the child container for that scope; outside
173
+ it, the application container. Integrations that hand a container to
174
+ another framework — a request-scoped container to a route layer, say —
175
+ want this one, not ``container``.
176
+
177
+ Returns:
178
+ The innermost container entered in this context.
179
+ """
180
+ return _current_container.get(self.container)
181
+
125
182
  async def make_async[T](self, interface: type[T]) -> Result[T, str]:
126
183
  """Resolve and instanciate a type from the container.
127
184
 
128
185
  Returns:
129
186
  Result containing the resolved type instance or an error message.
130
187
  """
131
- container = _current_container.get(self.container)
132
188
  try:
133
- return Ok(await container.get(interface))
189
+ return Ok(await self.current_container.get(interface))
134
190
  except Exception as e:
135
191
  return Err(f"Failed to resolve service '{interface.__name__}': {e}")
136
192
 
@@ -140,7 +196,7 @@ class Application:
140
196
  Returns:
141
197
  Result containing the resolved type instance or an error message.
142
198
  """
143
- container = _current_container.get(self.container)
199
+ container = self.current_container
144
200
  try:
145
201
  return Ok(container.get_sync(interface))
146
202
  except Exception as e:
@@ -154,11 +210,13 @@ class Application:
154
210
  ) -> AsyncGenerator[Self]:
155
211
  """Enter a new scope.
156
212
 
213
+ The application is yielded, not the child container; read
214
+ ``current_container`` inside the block to reach it.
215
+
157
216
  Yields:
158
217
  The application instance with the new scope.
159
218
  """
160
- parent = _current_container.get(self.container)
161
- async with parent(scope=scope, context=context) as container:
219
+ async with self.current_container(scope=scope, context=context) as container:
162
220
  token = _current_container.set(container)
163
221
  try:
164
222
  yield self
@@ -169,24 +227,32 @@ class Application:
169
227
  async def lifespan(self) -> AsyncGenerator[None]:
170
228
  """Wire the facades and providers."""
171
229
  Facade.set_facade_application(self)
230
+ self._booted = True
172
231
 
173
- async with AsyncExitStack() as stack:
174
- for provider in self.providers.values():
175
- if isinstance(provider, Bootable):
176
- await stack.enter_async_context(provider.lifespan())
177
-
178
- await self._wire_event_listeners()
179
- yield
180
-
181
- await self.container.close()
182
- Facade.reset_facade_application()
232
+ try:
233
+ async with AsyncExitStack() as stack:
234
+ for provider in self.providers.values():
235
+ if isinstance(provider, Bootable):
236
+ await stack.enter_async_context(provider.lifespan())
237
+
238
+ await self._wire_event_listeners()
239
+ yield
240
+ finally:
241
+ # Read `_container` rather than the property: the container may
242
+ # never have been built, and closing is no reason to build one.
243
+ if self._container is not None:
244
+ await self._container.close()
245
+ self._booted = False
246
+ Facade.reset_facade_application()
183
247
 
184
248
  def _bind_event_listeners(self) -> None:
185
249
  """Bind all listener classes declared in provider ``listen`` dicts."""
186
250
  for provider in self.providers.values():
187
251
  for listeners in provider.listen.values():
188
252
  for listener_cls in listeners:
189
- self.bind(listener_cls)
253
+ # Straight onto the root provider: routing this through the
254
+ # deprecated `bind` warned on every boot.
255
+ _ = self.root_provider.provide(listener_cls, scope=Scope.APP)
190
256
 
191
257
  async def _wire_event_listeners(self) -> None:
192
258
  """Wire event-listener mappings from all providers onto the dispatcher."""
@@ -24,7 +24,7 @@ class AppConfig(TypedDict):
24
24
  openapi_url: NotRequired[str]
25
25
  docs_url: NotRequired[str]
26
26
  redoc_url: NotRequired[str]
27
- providers: NotRequired[list[ServiceProvider]]
27
+ providers: NotRequired[list[type[ServiceProvider]]]
28
28
 
29
29
 
30
30
  class ProviderConfig(TypedDict):
@@ -34,4 +34,4 @@ class ProviderConfig(TypedDict):
34
34
  providers: List of service providers to register.
35
35
  """
36
36
 
37
- providers: NotRequired[list[ServiceProvider]]
37
+ providers: NotRequired[list[type[ServiceProvider]]]
@@ -2,4 +2,3 @@ from dishka.entities.scope import BaseScope, Scope
2
2
 
3
3
 
4
4
  __all__ = ["BaseScope", "Scope"]
5
-