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.
- python_neva-3.6.0/.claude/settings.local.json +18 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/.gitignore +0 -1
- python_neva-3.6.0/.pre-commit-config.yaml +33 -0
- python_neva-3.6.0/CHANGELOG.md +74 -0
- python_neva-3.6.0/CLAUDE.md +141 -0
- python_neva-3.6.0/PKG-INFO +106 -0
- python_neva-3.6.0/README.md +82 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/arch/application.py +88 -22
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/arch/config.py +2 -2
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/arch/scopes.py +0 -1
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/arch/service_provider.py +82 -7
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/config/loader.py +14 -2
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/config/repository.py +24 -5
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/database/connection.py +63 -46
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/database/manager.py +11 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/database/provider.py +8 -6
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/database/transaction.py +1 -2
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/contracts/__init__.py +2 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/dispatcher.py +12 -9
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/event_registry.py +40 -3
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/listener.py +2 -3
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/provider.py +3 -3
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/obs/logging/provider.py +1 -1
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/polyfactory/__init__.py +2 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/hashing/hash_manager.py +49 -2
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/hashing/hashers/bcrypt.py +1 -1
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/provider.py +2 -2
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/config.pyi +6 -3
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/hash.pyi +45 -1
- {python_neva-3.5.0 → python_neva-3.6.0}/pyproject.toml +14 -8
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/arch/test_context.py +0 -2
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/arch/test_extends.py +1 -1
- python_neva-3.6.0/tests/arch/test_lifetimes.py +179 -0
- python_neva-3.6.0/tests/arch/test_registration.py +156 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/arch/test_scope.py +15 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/config/test_loader.py +40 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/config/test_repository.py +48 -0
- python_neva-3.6.0/tests/database/test_detached_lifespan.py +124 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/database/test_sqlalchemy_integration.py +34 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/database/test_transaction_context.py +27 -1
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/events/conftest.py +16 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/events/test_deferred.py +35 -1
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/events/test_dispatch.py +1 -3
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/events/test_listen_on_parent_class.py +39 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/security/test_encrypter.py +3 -3
- python_neva-3.6.0/tests/security/test_hash_manager.py +206 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/testing/test_event_fake.py +1 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/testing/test_fixtures.py +1 -1
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/testing/test_refresh_database.py +2 -1
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/testing/test_test_case.py +2 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/uv.lock +22 -165
- python_neva-3.6.0/wiki/architecture/KNOWN-DISCREPANCIES.md +23 -0
- python_neva-3.6.0/wiki/architecture/configuration.md +315 -0
- python_neva-3.6.0/wiki/architecture/database-and-transactions.md +404 -0
- python_neva-3.6.0/wiki/architecture/dependency-injection.md +583 -0
- python_neva-3.6.0/wiki/architecture/events.md +424 -0
- python_neva-3.6.0/wiki/architecture/result-option.md +296 -0
- python_neva-3.6.0/wiki/architecture/testing.md +367 -0
- python_neva-3.5.0/.pre-commit-config.yaml +0 -45
- python_neva-3.5.0/CHANGELOG.md +0 -36
- python_neva-3.5.0/PKG-INFO +0 -27
- python_neva-3.5.0/README.md +0 -3
- python_neva-3.5.0/tests/security/test_hash_manager.py +0 -92
- {python_neva-3.5.0 → python_neva-3.6.0}/.envrc +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/.python-version +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/arch/__init__.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/arch/facade.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/arch/integrations/__init__.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/arch/integrations/faststream.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/arch/markers.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/arch/py.typed +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/config/__init__.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/config/base_providers.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/config/py.typed +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/database/__init__.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/database/config.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/database/py.typed +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/__init__.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/contracts/dispatcher.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/contracts/event.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/contracts/handler.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/contracts/listener.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/event.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/policy.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/events/py.typed +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/obs/__init__.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/obs/instrumentation/__init__.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/obs/instrumentation/sqlalchemy.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/obs/logging/__init__.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/obs/logging/manager.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/obs/middleware/__init__.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/obs/middleware/correlation.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/obs/middleware/profiler.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/obs/py.typed +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/polyfactory/factories.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/polyfactory/persistence.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/polyfactory/py.typed +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/__init__.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/encryption/__init__.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/encryption/encrypter.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/encryption/protocol.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/hashing/__init__.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/hashing/config.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/hashing/hashers/__init__.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/hashing/hashers/argon2.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/hashing/hashers/protocol.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/py.typed +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/tokens/__init__.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/tokens/generate_token.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/tokens/hash_token.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/security/tokens/verify_token.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/__init__.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/accessors.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/__init__.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/app.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/app.pyi +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/config.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/crypt.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/crypt.pyi +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/db.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/db.pyi +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/event.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/event.pyi +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/hash.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/log.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/facade/log.pyi +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/py.typed +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/results.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/strategy.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/strconv.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/support/time.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/testing/__init__.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/testing/fakes.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/testing/fixtures.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/testing/py.typed +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/neva/testing/test_case.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/ruff.toml +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/scripts/retag-with-changelog.sh +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/__init__.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/arch/__init__.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/arch/test_cache.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/config/__init__.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/conftest.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/database/__init__.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/database/test_connection_manager.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/database/test_database_manager.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/database/test_edge_cases.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/database/test_multi_connection.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/database/test_transaction.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/database/test_transaction_registry.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/events/__init__.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/events/test_before_dispatch.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/events/test_binding.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/events/test_event.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/events/test_function_listener.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/events/test_immediate.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/obs/__init__.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/obs/test_correlation.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/obs/test_profiler.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/security/__init__.py +0 -0
- {python_neva-3.5.0 → python_neva-3.6.0}/tests/testing/__init__.py +0 -0
- {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
|
+
}
|
|
@@ -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(
|
|
83
|
+
self.build_container()
|
|
81
84
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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]]]
|