python-neva 3.4.0__tar.gz → 3.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. python_neva-3.5.1/.claude/settings.local.json +18 -0
  2. {python_neva-3.4.0 → python_neva-3.5.1}/.gitignore +0 -1
  3. python_neva-3.5.1/CHANGELOG.md +48 -0
  4. python_neva-3.5.1/CLAUDE.md +138 -0
  5. python_neva-3.5.1/PKG-INFO +106 -0
  6. python_neva-3.5.1/README.md +82 -0
  7. {python_neva-3.4.0 → python_neva-3.5.1}/neva/database/connection.py +41 -40
  8. {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/dispatcher.py +4 -5
  9. python_neva-3.5.1/neva/support/facade/app.pyi +47 -0
  10. {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/event.py +2 -2
  11. {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/event.pyi +7 -2
  12. {python_neva-3.4.0 → python_neva-3.5.1}/pyproject.toml +1 -1
  13. {python_neva-3.4.0 → python_neva-3.5.1}/tests/database/test_sqlalchemy_integration.py +34 -0
  14. python_neva-3.5.1/tests/events/test_before_dispatch.py +177 -0
  15. {python_neva-3.4.0 → python_neva-3.5.1}/uv.lock +1 -1
  16. python_neva-3.5.1/wiki/architecture/KNOWN-DISCREPANCIES.md +41 -0
  17. python_neva-3.5.1/wiki/architecture/configuration.md +315 -0
  18. python_neva-3.5.1/wiki/architecture/database-and-transactions.md +404 -0
  19. python_neva-3.5.1/wiki/architecture/dependency-injection.md +583 -0
  20. python_neva-3.5.1/wiki/architecture/events.md +424 -0
  21. python_neva-3.5.1/wiki/architecture/result-option.md +296 -0
  22. python_neva-3.5.1/wiki/architecture/testing.md +367 -0
  23. python_neva-3.4.0/CHANGELOG.md +0 -20
  24. python_neva-3.4.0/PKG-INFO +0 -27
  25. python_neva-3.4.0/README.md +0 -3
  26. python_neva-3.4.0/neva/support/facade/app.pyi +0 -25
  27. {python_neva-3.4.0 → python_neva-3.5.1}/.envrc +0 -0
  28. {python_neva-3.4.0 → python_neva-3.5.1}/.pre-commit-config.yaml +0 -0
  29. {python_neva-3.4.0 → python_neva-3.5.1}/.python-version +0 -0
  30. {python_neva-3.4.0 → python_neva-3.5.1}/neva/arch/__init__.py +0 -0
  31. {python_neva-3.4.0 → python_neva-3.5.1}/neva/arch/application.py +0 -0
  32. {python_neva-3.4.0 → python_neva-3.5.1}/neva/arch/config.py +0 -0
  33. {python_neva-3.4.0 → python_neva-3.5.1}/neva/arch/facade.py +0 -0
  34. {python_neva-3.4.0 → python_neva-3.5.1}/neva/arch/integrations/__init__.py +0 -0
  35. {python_neva-3.4.0 → python_neva-3.5.1}/neva/arch/integrations/faststream.py +0 -0
  36. {python_neva-3.4.0 → python_neva-3.5.1}/neva/arch/markers.py +0 -0
  37. {python_neva-3.4.0 → python_neva-3.5.1}/neva/arch/py.typed +0 -0
  38. {python_neva-3.4.0 → python_neva-3.5.1}/neva/arch/scopes.py +0 -0
  39. {python_neva-3.4.0 → python_neva-3.5.1}/neva/arch/service_provider.py +0 -0
  40. {python_neva-3.4.0 → python_neva-3.5.1}/neva/config/__init__.py +0 -0
  41. {python_neva-3.4.0 → python_neva-3.5.1}/neva/config/base_providers.py +0 -0
  42. {python_neva-3.4.0 → python_neva-3.5.1}/neva/config/loader.py +0 -0
  43. {python_neva-3.4.0 → python_neva-3.5.1}/neva/config/py.typed +0 -0
  44. {python_neva-3.4.0 → python_neva-3.5.1}/neva/config/repository.py +0 -0
  45. {python_neva-3.4.0 → python_neva-3.5.1}/neva/database/__init__.py +0 -0
  46. {python_neva-3.4.0 → python_neva-3.5.1}/neva/database/config.py +0 -0
  47. {python_neva-3.4.0 → python_neva-3.5.1}/neva/database/manager.py +0 -0
  48. {python_neva-3.4.0 → python_neva-3.5.1}/neva/database/provider.py +0 -0
  49. {python_neva-3.4.0 → python_neva-3.5.1}/neva/database/py.typed +0 -0
  50. {python_neva-3.4.0 → python_neva-3.5.1}/neva/database/transaction.py +0 -0
  51. {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/__init__.py +0 -0
  52. {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/contracts/__init__.py +0 -0
  53. {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/contracts/dispatcher.py +0 -0
  54. {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/contracts/event.py +0 -0
  55. {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/contracts/handler.py +0 -0
  56. {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/contracts/listener.py +0 -0
  57. {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/event.py +0 -0
  58. {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/event_registry.py +0 -0
  59. {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/listener.py +0 -0
  60. {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/policy.py +0 -0
  61. {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/provider.py +0 -0
  62. {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/py.typed +0 -0
  63. {python_neva-3.4.0 → python_neva-3.5.1}/neva/obs/__init__.py +0 -0
  64. {python_neva-3.4.0 → python_neva-3.5.1}/neva/obs/instrumentation/__init__.py +0 -0
  65. {python_neva-3.4.0 → python_neva-3.5.1}/neva/obs/instrumentation/sqlalchemy.py +0 -0
  66. {python_neva-3.4.0 → python_neva-3.5.1}/neva/obs/logging/__init__.py +0 -0
  67. {python_neva-3.4.0 → python_neva-3.5.1}/neva/obs/logging/manager.py +0 -0
  68. {python_neva-3.4.0 → python_neva-3.5.1}/neva/obs/logging/provider.py +0 -0
  69. {python_neva-3.4.0 → python_neva-3.5.1}/neva/obs/middleware/__init__.py +0 -0
  70. {python_neva-3.4.0 → python_neva-3.5.1}/neva/obs/middleware/correlation.py +0 -0
  71. {python_neva-3.4.0 → python_neva-3.5.1}/neva/obs/middleware/profiler.py +0 -0
  72. {python_neva-3.4.0 → python_neva-3.5.1}/neva/obs/py.typed +0 -0
  73. {python_neva-3.4.0 → python_neva-3.5.1}/neva/polyfactory/__init__.py +0 -0
  74. {python_neva-3.4.0 → python_neva-3.5.1}/neva/polyfactory/factories.py +0 -0
  75. {python_neva-3.4.0 → python_neva-3.5.1}/neva/polyfactory/persistence.py +0 -0
  76. {python_neva-3.4.0 → python_neva-3.5.1}/neva/polyfactory/py.typed +0 -0
  77. {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/__init__.py +0 -0
  78. {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/encryption/__init__.py +0 -0
  79. {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/encryption/encrypter.py +0 -0
  80. {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/encryption/protocol.py +0 -0
  81. {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/hashing/__init__.py +0 -0
  82. {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/hashing/config.py +0 -0
  83. {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/hashing/hash_manager.py +0 -0
  84. {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/hashing/hashers/__init__.py +0 -0
  85. {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/hashing/hashers/argon2.py +0 -0
  86. {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/hashing/hashers/bcrypt.py +0 -0
  87. {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/hashing/hashers/protocol.py +0 -0
  88. {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/provider.py +0 -0
  89. {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/py.typed +0 -0
  90. {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/tokens/__init__.py +0 -0
  91. {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/tokens/generate_token.py +0 -0
  92. {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/tokens/hash_token.py +0 -0
  93. {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/tokens/verify_token.py +0 -0
  94. {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/__init__.py +0 -0
  95. {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/accessors.py +0 -0
  96. {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/__init__.py +0 -0
  97. {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/app.py +0 -0
  98. {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/config.py +0 -0
  99. {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/config.pyi +0 -0
  100. {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/crypt.py +0 -0
  101. {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/crypt.pyi +0 -0
  102. {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/db.py +0 -0
  103. {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/db.pyi +0 -0
  104. {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/hash.py +0 -0
  105. {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/hash.pyi +0 -0
  106. {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/log.py +0 -0
  107. {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/log.pyi +0 -0
  108. {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/py.typed +0 -0
  109. {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/results.py +0 -0
  110. {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/strategy.py +0 -0
  111. {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/strconv.py +0 -0
  112. {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/time.py +0 -0
  113. {python_neva-3.4.0 → python_neva-3.5.1}/neva/testing/__init__.py +0 -0
  114. {python_neva-3.4.0 → python_neva-3.5.1}/neva/testing/fakes.py +0 -0
  115. {python_neva-3.4.0 → python_neva-3.5.1}/neva/testing/fixtures.py +0 -0
  116. {python_neva-3.4.0 → python_neva-3.5.1}/neva/testing/py.typed +0 -0
  117. {python_neva-3.4.0 → python_neva-3.5.1}/neva/testing/test_case.py +0 -0
  118. {python_neva-3.4.0 → python_neva-3.5.1}/ruff.toml +0 -0
  119. {python_neva-3.4.0 → python_neva-3.5.1}/scripts/retag-with-changelog.sh +0 -0
  120. {python_neva-3.4.0 → python_neva-3.5.1}/tests/__init__.py +0 -0
  121. {python_neva-3.4.0 → python_neva-3.5.1}/tests/arch/__init__.py +0 -0
  122. {python_neva-3.4.0 → python_neva-3.5.1}/tests/arch/test_cache.py +0 -0
  123. {python_neva-3.4.0 → python_neva-3.5.1}/tests/arch/test_context.py +0 -0
  124. {python_neva-3.4.0 → python_neva-3.5.1}/tests/arch/test_extends.py +0 -0
  125. {python_neva-3.4.0 → python_neva-3.5.1}/tests/arch/test_scope.py +0 -0
  126. {python_neva-3.4.0 → python_neva-3.5.1}/tests/config/__init__.py +0 -0
  127. {python_neva-3.4.0 → python_neva-3.5.1}/tests/config/test_loader.py +0 -0
  128. {python_neva-3.4.0 → python_neva-3.5.1}/tests/config/test_repository.py +0 -0
  129. {python_neva-3.4.0 → python_neva-3.5.1}/tests/conftest.py +0 -0
  130. {python_neva-3.4.0 → python_neva-3.5.1}/tests/database/__init__.py +0 -0
  131. {python_neva-3.4.0 → python_neva-3.5.1}/tests/database/test_connection_manager.py +0 -0
  132. {python_neva-3.4.0 → python_neva-3.5.1}/tests/database/test_database_manager.py +0 -0
  133. {python_neva-3.4.0 → python_neva-3.5.1}/tests/database/test_edge_cases.py +0 -0
  134. {python_neva-3.4.0 → python_neva-3.5.1}/tests/database/test_multi_connection.py +0 -0
  135. {python_neva-3.4.0 → python_neva-3.5.1}/tests/database/test_transaction.py +0 -0
  136. {python_neva-3.4.0 → python_neva-3.5.1}/tests/database/test_transaction_context.py +0 -0
  137. {python_neva-3.4.0 → python_neva-3.5.1}/tests/database/test_transaction_registry.py +0 -0
  138. {python_neva-3.4.0 → python_neva-3.5.1}/tests/events/__init__.py +0 -0
  139. {python_neva-3.4.0 → python_neva-3.5.1}/tests/events/conftest.py +0 -0
  140. {python_neva-3.4.0 → python_neva-3.5.1}/tests/events/test_binding.py +0 -0
  141. {python_neva-3.4.0 → python_neva-3.5.1}/tests/events/test_deferred.py +0 -0
  142. {python_neva-3.4.0 → python_neva-3.5.1}/tests/events/test_dispatch.py +0 -0
  143. {python_neva-3.4.0 → python_neva-3.5.1}/tests/events/test_event.py +0 -0
  144. {python_neva-3.4.0 → python_neva-3.5.1}/tests/events/test_function_listener.py +0 -0
  145. {python_neva-3.4.0 → python_neva-3.5.1}/tests/events/test_immediate.py +0 -0
  146. {python_neva-3.4.0 → python_neva-3.5.1}/tests/events/test_listen_on_parent_class.py +0 -0
  147. {python_neva-3.4.0 → python_neva-3.5.1}/tests/obs/__init__.py +0 -0
  148. {python_neva-3.4.0 → python_neva-3.5.1}/tests/obs/test_correlation.py +0 -0
  149. {python_neva-3.4.0 → python_neva-3.5.1}/tests/obs/test_profiler.py +0 -0
  150. {python_neva-3.4.0 → python_neva-3.5.1}/tests/security/__init__.py +0 -0
  151. {python_neva-3.4.0 → python_neva-3.5.1}/tests/security/test_encrypter.py +0 -0
  152. {python_neva-3.4.0 → python_neva-3.5.1}/tests/security/test_hash_manager.py +0 -0
  153. {python_neva-3.4.0 → python_neva-3.5.1}/tests/testing/__init__.py +0 -0
  154. {python_neva-3.4.0 → python_neva-3.5.1}/tests/testing/test_event_fake.py +0 -0
  155. {python_neva-3.4.0 → python_neva-3.5.1}/tests/testing/test_facade_restore.py +0 -0
  156. {python_neva-3.4.0 → python_neva-3.5.1}/tests/testing/test_fixtures.py +0 -0
  157. {python_neva-3.4.0 → python_neva-3.5.1}/tests/testing/test_refresh_database.py +0 -0
  158. {python_neva-3.4.0 → python_neva-3.5.1}/tests/testing/test_test_case.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,48 @@
1
+ ## 3.5.1 (2026-08-24)
2
+
3
+ ### 🐛🚑️ Fixes
4
+
5
+ - **database**: run commit callbacks after the commit, not before
6
+
7
+ ### 📝💡 Documentation
8
+
9
+ - **general**: update docs
10
+ - **README**: very short section on contribution
11
+ - **docs**: extension ideas
12
+
13
+ ## 3.5.0 (2026-06-11)
14
+
15
+ ### ✨ Features
16
+
17
+ - **arch**: improve app facade stub
18
+ - **events**: make the event facade properly depend on the dispatcher contract
19
+ - **events**: new tests for events
20
+
21
+ ### 🐛🚑️ Fixes
22
+
23
+ - **events**: fixing handling of async before-dispatch hooks
24
+
25
+ ### 🏷️ Types
26
+
27
+ - **events**: improve event facade stub
28
+
29
+ ## 3.4.0 (2026-06-10)
30
+
31
+ ### ✨ Features
32
+
33
+ - **arch**: new from_context method
34
+ - **tooling**: placeholder commit
35
+
36
+ ### 💚👷 CI & Build
37
+
38
+ - **tooling**: default to base tag version for commitizen
39
+
40
+ ### 📝💡 Documentation
41
+
42
+ - **project**: update doc
43
+ - **core**: placeholde readme
44
+
45
+ ### 🔧🔨📦️ Configuration, Scripts, Packages
46
+
47
+ - **tooling**: update changelog script
48
+ - **project**: update versioning config
@@ -0,0 +1,138 @@
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:** mypy (with the `pydantic.mypy` plugin and the `TypeForm` incomplete feature)
22
+
23
+ ## Commands
24
+
25
+ ```bash
26
+ # Development (poe task aliases)
27
+ poe lint # ruff check
28
+ poe fmt # ruff format
29
+ poe tc # mypy .
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:
76
+ ```python
77
+ class MyProvider(ServiceProvider):
78
+ def register(self) -> Result[Self, str]:
79
+ self.bind(MyService)
80
+ return Ok(self)
81
+
82
+ @asynccontextmanager
83
+ async def lifespan(self) -> AsyncIterator[None]:
84
+ yield # startup/shutdown
85
+ ```
86
+
87
+ **Events** - Transaction-aware dispatch with IMMEDIATE and DEFERRED policies:
88
+ ```python
89
+ # Events are generic Pydantic models with an event_id and auto-timestamp
90
+ class UserCreated(Event[int]):
91
+ user_id: int
92
+
93
+ # Class-based listener (default policy: IMMEDIATE)
94
+ class SendWelcomeEmail(EventListener[UserCreated]):
95
+ policy = HandlingPolicy.DEFERRED
96
+
97
+ async def handle(self, event: UserCreated) -> Result[None, str]:
98
+ await mailer.send(event.user_id)
99
+ return Ok(None)
100
+
101
+ # Function-based listener via decorator
102
+ @listener(policy=HandlingPolicy.DEFERRED)
103
+ async def log_user_created(event: UserCreated) -> Result[None, str]:
104
+ log.info("user created", user_id=event.user_id)
105
+ return Ok(None)
106
+ ```
107
+
108
+ 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.
109
+
110
+ **Testing** - Async by default with TestCase base:
111
+ ```python
112
+ class TestMyService(TestCase):
113
+ async def test_something(self):
114
+ service = self.app.make(MyService).unwrap()
115
+ result = await service.do_something()
116
+ assert result.is_ok
117
+ ```
118
+
119
+ For database tests, subclass `RefreshDatabase` (wraps each test in a transaction and rolls it back).
120
+
121
+ ## Code Conventions
122
+
123
+ - **Type hints required** - Full annotations, uses Python 3.12+ features
124
+ - **Google-style docstrings** - Enforced by ruff DOC/D rules
125
+ - **Async first** - All I/O operations are async/await
126
+ - **Naming**: Events past tense (UserCreated), Class listeners imperative (SendWelcomeEmail), Function listeners snake_case handlers (log_user_created), Services noun phrases (EmailService)
127
+ - **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
128
+
129
+ ## Commits & releases
130
+
131
+ Conventional Commits with gitmoji via `cz_gitmoji` (`cz commit`). Releases are
132
+ cut with `cz bump`, which writes `CHANGELOG.md` and runs
133
+ `scripts/retag-with-changelog.sh` to annotate the new `v<version>` tag with the
134
+ rendered changelog. See `README.md` for the full flow and the plugin list.
135
+
136
+ ## Documentation
137
+
138
+ - Architecture guides: `wiki/architecture/`
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.5
2
+ Name: python-neva
3
+ Version: 3.5.1
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 (mypy)
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 (mypy)
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
@@ -95,7 +95,7 @@ class ConnectionManager:
95
95
 
96
96
  @asynccontextmanager
97
97
  async def _scoped(self, tx: BoundTransaction) -> AsyncIterator[None]:
98
- """Manage registry, state transitions, and callbacks for a transaction.
98
+ """Expose the transaction on the registry for the duration of the block.
99
99
 
100
100
  Yields:
101
101
  None
@@ -108,40 +108,37 @@ class ConnectionManager:
108
108
  )
109
109
  try:
110
110
  yield
111
- if tx.rollback_requested:
112
- tx.state = TransactionState.ROLLED_BACK
113
- if tx.is_root:
114
- for result in await tx.execute_on_rollback_callbacks():
115
- if result.is_err and self.logger is not None:
116
- self.logger.error(
117
- "rollback callback failed",
118
- error=result.err().unwrap(),
119
- connection=self.name,
120
- )
121
- else:
122
- tx.state = TransactionState.COMMITTED
123
- if tx.is_root:
124
- for result in await tx.execute_on_commit_callbacks():
125
- if result.is_err and self.logger is not None:
126
- self.logger.error(
127
- "commit callback failed",
128
- error=result.err().unwrap(),
129
- connection=self.name,
130
- )
131
- except BaseException:
132
- tx.state = TransactionState.ROLLED_BACK
133
- if tx.is_root:
134
- for result in await tx.execute_on_rollback_callbacks():
135
- if result.is_err and self.logger is not None:
136
- self.logger.error(
137
- "rollback callback failed",
138
- error=result.err().unwrap(),
139
- connection=self.name,
140
- )
141
- raise
142
111
  finally:
143
112
  _tx_registry.reset(token)
144
113
 
114
+ async def _settle(self, tx: BoundTransaction, *, committed: bool) -> None:
115
+ """Mark the transaction settled and drain its callbacks.
116
+
117
+ Callers must have already committed or rolled back the underlying
118
+ session: callbacks observe a settled transaction, and the registry no
119
+ longer exposes it, so a callback touching the database opens its own
120
+ transaction rather than reusing the drained one.
121
+ """
122
+ tx.state = (
123
+ TransactionState.COMMITTED if committed else TransactionState.ROLLED_BACK
124
+ )
125
+ if not tx.is_root:
126
+ return
127
+
128
+ results = (
129
+ await tx.execute_on_commit_callbacks()
130
+ if committed
131
+ else await tx.execute_on_rollback_callbacks()
132
+ )
133
+ label = "commit" if committed else "rollback"
134
+ for result in results:
135
+ if result.is_err and self.logger is not None:
136
+ self.logger.error(
137
+ f"{label} callback failed",
138
+ error=result.err().unwrap(),
139
+ connection=self.name,
140
+ )
141
+
145
142
  @asynccontextmanager
146
143
  async def begin(self) -> AsyncIterator[BoundTransaction]:
147
144
  """Open a new bound transaction with an active database session.
@@ -162,23 +159,27 @@ class ConnectionManager:
162
159
  try:
163
160
  async with self._scoped(tx):
164
161
  yield tx
165
- if tx.rollback_requested:
166
- await sp.rollback()
167
- else:
168
- await sp.commit()
162
+ if tx.rollback_requested:
163
+ await sp.rollback()
164
+ else:
165
+ await sp.commit()
169
166
  except BaseException:
170
167
  await sp.rollback()
168
+ await self._settle(tx, committed=False)
171
169
  raise
170
+ await self._settle(tx, committed=not tx.rollback_requested)
172
171
  case _:
173
172
  async with self.session_factory() as session:
174
173
  tx = BoundTransaction(self.name, session)
175
174
  try:
176
175
  async with self._scoped(tx):
177
176
  yield tx
178
- if tx.rollback_requested:
179
- await session.rollback()
180
- else:
181
- await session.commit()
177
+ if tx.rollback_requested:
178
+ await session.rollback()
179
+ else:
180
+ await session.commit()
182
181
  except BaseException:
183
182
  await session.rollback()
183
+ await self._settle(tx, committed=False)
184
184
  raise
185
+ await self._settle(tx, committed=not tx.rollback_requested)
@@ -1,5 +1,6 @@
1
1
  """Base implementation of the event dispatcher."""
2
2
 
3
+ import inspect
3
4
  from typing import override
4
5
 
5
6
  from neva.arch.application import Application
@@ -35,11 +36,9 @@ class EventDispatcher(contracts.EventDispatcher):
35
36
  event: The event about to be dispatched.
36
37
  """
37
38
  for hook in self._before_dispatch_hooks:
38
- match hook:
39
- case contracts.AsyncBeforeDispatchHook():
40
- await hook(event)
41
- case contracts.SyncBeforeDispatchHook():
42
- hook(event)
39
+ result = hook(event)
40
+ if inspect.isawaitable(result):
41
+ await result
43
42
 
44
43
  @override
45
44
  async def before_dispatch(self, hook: contracts.BeforeDispatchHook) -> None:
@@ -0,0 +1,47 @@
1
+ """Type stub for App facade."""
2
+
3
+ from contextlib import AbstractAsyncContextManager
4
+ from typing import Any, Self, override
5
+
6
+ from neva.arch import Facade
7
+ from neva.arch.scopes import BaseScope
8
+ from neva.support import Result
9
+
10
+ class App(Facade):
11
+ @classmethod
12
+ @override
13
+ def get_facade_accessor(cls) -> type: ...
14
+ @classmethod
15
+ async def make_async[T](cls, interface: type[T]) -> Result[T, str]:
16
+ """Resolve and instanciate a type from the container.
17
+
18
+ Returns:
19
+ Result containing the resolved type instance or an error message.
20
+ """
21
+
22
+ @classmethod
23
+ def make[T](cls, interface: type[T]) -> Result[T, str]:
24
+ """Resolve an interface from the container by its alias.
25
+
26
+ Attempts to retrieve and instantiate an object from the dependency
27
+ injection container.
28
+
29
+ Args:
30
+ interface: The interface to resolve.
31
+
32
+ Returns:
33
+ Result containing the resolved service instance or an error message.
34
+
35
+ """
36
+
37
+ @classmethod
38
+ async def scope(
39
+ cls,
40
+ scope: BaseScope | None = None,
41
+ context: dict[type, Any] | None = None,
42
+ ) -> AbstractAsyncContextManager[Self]:
43
+ """Enter a new scope.
44
+
45
+ Yields:
46
+ The application instance with the new scope.
47
+ """
@@ -5,7 +5,7 @@ from contextlib import contextmanager
5
5
  from typing import TYPE_CHECKING, override
6
6
 
7
7
  from neva.arch import Facade
8
- from neva.events import EventDispatcher
8
+ from neva.events import contracts
9
9
 
10
10
 
11
11
  if TYPE_CHECKING:
@@ -23,7 +23,7 @@ class Event(Facade):
23
23
  Returns:
24
24
  EventDispatcher class.
25
25
  """
26
- return EventDispatcher
26
+ return contracts.EventDispatcher
27
27
 
28
28
  @classmethod
29
29
  def fake(cls) -> "EventFake":
@@ -13,10 +13,15 @@ class Event(Facade):
13
13
  @override
14
14
  def get_facade_accessor(cls) -> type: ...
15
15
  @classmethod
16
- async def dispatch(cls, event: contracts.Event) -> list[Result[None, str]]: ...
16
+ async def before_dispatch(cls, hook: contracts.BeforeDispatchHook) -> None:
17
+ """Register a hook to be called before listeners are invoked."""
18
+ @classmethod
19
+ async def dispatch(cls, event: contracts.Event) -> list[Result[None, str]]:
20
+ """Dispatch an event to all registered listeners."""
17
21
  @classmethod
18
22
  def listen[T: contracts.Event](
19
23
  cls,
20
24
  event_cls: type[T],
21
25
  listener_cls: type[contracts.EventListener[T]],
22
- ) -> None: ...
26
+ ) -> None:
27
+ """Register a listener for an event."""
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-neva"
3
- version = "3.4.0"
3
+ version = "3.5.1"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -118,6 +118,40 @@ class TestOnCommitCallbacks:
118
118
 
119
119
  assert results == ["committed"]
120
120
 
121
+ async def test_on_commit_runs_after_the_root_commit(
122
+ self, db: DatabaseManager
123
+ ) -> None:
124
+ observed: dict[str, bool] = {}
125
+
126
+ async with db.begin() as tx:
127
+ tx.session.add(User(id=21, name="Committed"))
128
+
129
+ async def on_commit() -> Result[None, str]:
130
+ observed["in_transaction"] = tx.session.in_transaction()
131
+ return Ok(None)
132
+
133
+ _ = tx.on_commit(on_commit)
134
+
135
+ assert observed["in_transaction"] is False
136
+
137
+ async def test_on_commit_callback_writes_in_its_own_transaction(
138
+ self, db: DatabaseManager
139
+ ) -> None:
140
+ async with db.begin() as tx:
141
+ tx.session.add(User(id=23, name="Trigger"))
142
+
143
+ async def on_commit() -> Result[None, str]:
144
+ assert db.current().is_nothing
145
+ async with db.begin() as own:
146
+ own.session.add(User(id=24, name="FromCallback"))
147
+ return Ok(None)
148
+
149
+ _ = tx.on_commit(on_commit)
150
+
151
+ async with db.begin() as tx:
152
+ row = await tx.session.execute(select(User).where(User.id == 24))
153
+ assert row.scalar_one().name == "FromCallback"
154
+
121
155
  async def test_on_rollback_fires_on_error(self, db: DatabaseManager) -> None:
122
156
  results: list[str] = []
123
157