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.
- python_neva-3.5.1/.claude/settings.local.json +18 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/.gitignore +0 -1
- python_neva-3.5.1/CHANGELOG.md +48 -0
- python_neva-3.5.1/CLAUDE.md +138 -0
- python_neva-3.5.1/PKG-INFO +106 -0
- python_neva-3.5.1/README.md +82 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/database/connection.py +41 -40
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/dispatcher.py +4 -5
- python_neva-3.5.1/neva/support/facade/app.pyi +47 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/event.py +2 -2
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/event.pyi +7 -2
- {python_neva-3.4.0 → python_neva-3.5.1}/pyproject.toml +1 -1
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/database/test_sqlalchemy_integration.py +34 -0
- python_neva-3.5.1/tests/events/test_before_dispatch.py +177 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/uv.lock +1 -1
- python_neva-3.5.1/wiki/architecture/KNOWN-DISCREPANCIES.md +41 -0
- python_neva-3.5.1/wiki/architecture/configuration.md +315 -0
- python_neva-3.5.1/wiki/architecture/database-and-transactions.md +404 -0
- python_neva-3.5.1/wiki/architecture/dependency-injection.md +583 -0
- python_neva-3.5.1/wiki/architecture/events.md +424 -0
- python_neva-3.5.1/wiki/architecture/result-option.md +296 -0
- python_neva-3.5.1/wiki/architecture/testing.md +367 -0
- python_neva-3.4.0/CHANGELOG.md +0 -20
- python_neva-3.4.0/PKG-INFO +0 -27
- python_neva-3.4.0/README.md +0 -3
- python_neva-3.4.0/neva/support/facade/app.pyi +0 -25
- {python_neva-3.4.0 → python_neva-3.5.1}/.envrc +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/.pre-commit-config.yaml +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/.python-version +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/arch/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/arch/application.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/arch/config.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/arch/facade.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/arch/integrations/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/arch/integrations/faststream.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/arch/markers.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/arch/py.typed +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/arch/scopes.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/arch/service_provider.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/config/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/config/base_providers.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/config/loader.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/config/py.typed +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/config/repository.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/database/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/database/config.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/database/manager.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/database/provider.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/database/py.typed +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/database/transaction.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/contracts/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/contracts/dispatcher.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/contracts/event.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/contracts/handler.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/contracts/listener.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/event.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/event_registry.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/listener.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/policy.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/provider.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/events/py.typed +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/obs/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/obs/instrumentation/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/obs/instrumentation/sqlalchemy.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/obs/logging/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/obs/logging/manager.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/obs/logging/provider.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/obs/middleware/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/obs/middleware/correlation.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/obs/middleware/profiler.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/obs/py.typed +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/polyfactory/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/polyfactory/factories.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/polyfactory/persistence.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/polyfactory/py.typed +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/encryption/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/encryption/encrypter.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/encryption/protocol.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/hashing/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/hashing/config.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/hashing/hash_manager.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/hashing/hashers/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/hashing/hashers/argon2.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/hashing/hashers/bcrypt.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/hashing/hashers/protocol.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/provider.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/py.typed +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/tokens/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/tokens/generate_token.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/tokens/hash_token.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/security/tokens/verify_token.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/accessors.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/app.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/config.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/config.pyi +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/crypt.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/crypt.pyi +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/db.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/db.pyi +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/hash.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/hash.pyi +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/log.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/facade/log.pyi +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/py.typed +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/results.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/strategy.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/strconv.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/support/time.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/testing/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/testing/fakes.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/testing/fixtures.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/testing/py.typed +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/neva/testing/test_case.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/ruff.toml +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/scripts/retag-with-changelog.sh +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/arch/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/arch/test_cache.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/arch/test_context.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/arch/test_extends.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/arch/test_scope.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/config/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/config/test_loader.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/config/test_repository.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/conftest.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/database/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/database/test_connection_manager.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/database/test_database_manager.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/database/test_edge_cases.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/database/test_multi_connection.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/database/test_transaction.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/database/test_transaction_context.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/database/test_transaction_registry.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/events/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/events/conftest.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/events/test_binding.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/events/test_deferred.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/events/test_dispatch.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/events/test_event.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/events/test_function_listener.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/events/test_immediate.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/events/test_listen_on_parent_class.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/obs/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/obs/test_correlation.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/obs/test_profiler.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/security/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/security/test_encrypter.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/security/test_hash_manager.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/testing/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/testing/test_event_fake.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/testing/test_facade_restore.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/testing/test_fixtures.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.1}/tests/testing/test_refresh_database.py +0 -0
- {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
|
+
}
|
|
@@ -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
|
-
"""
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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
|
|
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."""
|
|
@@ -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
|
|