python-neva 3.5.1__tar.gz → 3.7.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.7.0/.gitlab-ci.yml +79 -0
- python_neva-3.7.0/.pre-commit-config.yaml +33 -0
- python_neva-3.7.0/CHANGELOG.md +92 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/CLAUDE.md +7 -4
- {python_neva-3.5.1 → python_neva-3.7.0}/PKG-INFO +10 -6
- {python_neva-3.5.1 → python_neva-3.7.0}/README.md +1 -1
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/arch/application.py +88 -22
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/arch/config.py +2 -2
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/arch/scopes.py +0 -1
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/arch/service_provider.py +82 -7
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/config/loader.py +14 -2
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/config/repository.py +24 -5
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/database/connection.py +23 -7
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/database/manager.py +11 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/database/provider.py +8 -6
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/database/transaction.py +2 -3
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/events/contracts/__init__.py +2 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/events/dispatcher.py +12 -9
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/events/event_registry.py +40 -3
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/events/listener.py +2 -3
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/events/provider.py +3 -3
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/obs/logging/provider.py +1 -1
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/polyfactory/__init__.py +2 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/security/hashing/hash_manager.py +49 -2
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/security/hashing/hashers/bcrypt.py +1 -1
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/security/provider.py +2 -2
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/support/facade/config.pyi +6 -3
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/support/facade/hash.pyi +45 -1
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/support/strategy.py +2 -2
- {python_neva-3.5.1 → python_neva-3.7.0}/pyproject.toml +22 -12
- python_neva-3.7.0/ruff.toml +47 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/arch/test_context.py +0 -2
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/arch/test_extends.py +1 -1
- python_neva-3.7.0/tests/arch/test_lifetimes.py +179 -0
- python_neva-3.7.0/tests/arch/test_registration.py +152 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/arch/test_scope.py +15 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/config/test_loader.py +36 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/config/test_repository.py +48 -0
- python_neva-3.7.0/tests/database/test_detached_lifespan.py +124 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/database/test_transaction_context.py +27 -1
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/events/conftest.py +17 -1
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/events/test_deferred.py +35 -1
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/events/test_dispatch.py +1 -3
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/events/test_listen_on_parent_class.py +40 -1
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/security/test_encrypter.py +4 -4
- python_neva-3.7.0/tests/security/test_hash_manager.py +206 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/testing/test_event_fake.py +1 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/testing/test_fixtures.py +1 -1
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/testing/test_refresh_database.py +2 -1
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/testing/test_test_case.py +2 -0
- python_neva-3.7.0/uv.lock +1855 -0
- python_neva-3.7.0/wiki/architecture/KNOWN-DISCREPANCIES.md +23 -0
- python_neva-3.5.1/.pre-commit-config.yaml +0 -45
- python_neva-3.5.1/CHANGELOG.md +0 -48
- python_neva-3.5.1/ruff.toml +0 -43
- python_neva-3.5.1/tests/security/test_hash_manager.py +0 -92
- python_neva-3.5.1/uv.lock +0 -2436
- python_neva-3.5.1/wiki/architecture/KNOWN-DISCREPANCIES.md +0 -41
- {python_neva-3.5.1 → python_neva-3.7.0}/.claude/settings.local.json +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/.envrc +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/.gitignore +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/.python-version +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/arch/__init__.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/arch/facade.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/arch/integrations/__init__.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/arch/integrations/faststream.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/arch/markers.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/arch/py.typed +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/config/__init__.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/config/base_providers.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/config/py.typed +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/database/__init__.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/database/config.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/database/py.typed +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/events/__init__.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/events/contracts/dispatcher.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/events/contracts/event.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/events/contracts/handler.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/events/contracts/listener.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/events/event.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/events/policy.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/events/py.typed +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/obs/__init__.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/obs/instrumentation/__init__.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/obs/instrumentation/sqlalchemy.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/obs/logging/__init__.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/obs/logging/manager.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/obs/middleware/__init__.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/obs/middleware/correlation.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/obs/middleware/profiler.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/obs/py.typed +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/polyfactory/factories.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/polyfactory/persistence.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/polyfactory/py.typed +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/security/__init__.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/security/encryption/__init__.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/security/encryption/encrypter.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/security/encryption/protocol.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/security/hashing/__init__.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/security/hashing/config.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/security/hashing/hashers/__init__.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/security/hashing/hashers/argon2.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/security/hashing/hashers/protocol.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/security/py.typed +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/security/tokens/__init__.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/security/tokens/generate_token.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/security/tokens/hash_token.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/security/tokens/verify_token.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/support/__init__.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/support/accessors.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/support/facade/__init__.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/support/facade/app.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/support/facade/app.pyi +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/support/facade/config.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/support/facade/crypt.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/support/facade/crypt.pyi +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/support/facade/db.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/support/facade/db.pyi +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/support/facade/event.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/support/facade/event.pyi +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/support/facade/hash.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/support/facade/log.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/support/facade/log.pyi +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/support/py.typed +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/support/results.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/support/strconv.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/support/time.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/testing/__init__.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/testing/fakes.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/testing/fixtures.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/testing/py.typed +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/neva/testing/test_case.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/scripts/retag-with-changelog.sh +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/__init__.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/arch/__init__.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/arch/test_cache.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/config/__init__.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/conftest.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/database/__init__.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/database/test_connection_manager.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/database/test_database_manager.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/database/test_edge_cases.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/database/test_multi_connection.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/database/test_sqlalchemy_integration.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/database/test_transaction.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/database/test_transaction_registry.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/events/__init__.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/events/test_before_dispatch.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/events/test_binding.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/events/test_event.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/events/test_function_listener.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/events/test_immediate.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/obs/__init__.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/obs/test_correlation.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/obs/test_profiler.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/security/__init__.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/testing/__init__.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/tests/testing/test_facade_restore.py +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/wiki/architecture/configuration.md +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/wiki/architecture/database-and-transactions.md +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/wiki/architecture/dependency-injection.md +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/wiki/architecture/events.md +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/wiki/architecture/result-option.md +0 -0
- {python_neva-3.5.1 → python_neva-3.7.0}/wiki/architecture/testing.md +0 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
variables:
|
|
3
|
+
PYTHON_VERSION: "3.12"
|
|
4
|
+
UV_VERSION: "0.9.28"
|
|
5
|
+
BASE_LAYER: bookworm-slim
|
|
6
|
+
UV_LINK_MODE: copy
|
|
7
|
+
UV_CACHE_DIR: .cache/uv
|
|
8
|
+
|
|
9
|
+
stages:
|
|
10
|
+
- code_qa
|
|
11
|
+
- test
|
|
12
|
+
- deps
|
|
13
|
+
|
|
14
|
+
##### Deps #####
|
|
15
|
+
.uv_base:
|
|
16
|
+
image: ghcr.io/astral-sh/uv:$UV_VERSION-python$PYTHON_VERSION-$BASE_LAYER
|
|
17
|
+
cache:
|
|
18
|
+
key:
|
|
19
|
+
files:
|
|
20
|
+
- uv.lock
|
|
21
|
+
paths:
|
|
22
|
+
- .cache/uv
|
|
23
|
+
- .venv
|
|
24
|
+
before_script:
|
|
25
|
+
- uv sync --frozen --all-extras
|
|
26
|
+
|
|
27
|
+
.default_rules:
|
|
28
|
+
rules:
|
|
29
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
30
|
+
- if: $CI_COMMIT_BRANCH == "main"
|
|
31
|
+
|
|
32
|
+
##### Code QA #####
|
|
33
|
+
ruff:lint:
|
|
34
|
+
extends: [.uv_base, .default_rules]
|
|
35
|
+
stage: code_qa
|
|
36
|
+
script:
|
|
37
|
+
- uv run --frozen ruff check .
|
|
38
|
+
|
|
39
|
+
ruff:format:
|
|
40
|
+
extends: [.uv_base, .default_rules]
|
|
41
|
+
stage: code_qa
|
|
42
|
+
script:
|
|
43
|
+
- uv run --frozen ruff format --check .
|
|
44
|
+
|
|
45
|
+
pyrefly:
|
|
46
|
+
extends: [.uv_base, .default_rules]
|
|
47
|
+
stage: code_qa
|
|
48
|
+
script:
|
|
49
|
+
- uv run --frozen pyrefly check
|
|
50
|
+
|
|
51
|
+
##### Test #####
|
|
52
|
+
pytest:
|
|
53
|
+
extends: [.uv_base, .default_rules]
|
|
54
|
+
stage: test
|
|
55
|
+
script:
|
|
56
|
+
- uv run --frozen pytest
|
|
57
|
+
|
|
58
|
+
##### Deps contract #####
|
|
59
|
+
# Resolves every direct dependency at its declared floor, so a `>=` lower than
|
|
60
|
+
# what the code needs fails here instead of in a user's install. Three things
|
|
61
|
+
# keep this job honest, and dropping any one makes it pass without testing
|
|
62
|
+
# anything: it must not extend .uv_base, whose before_script installs the
|
|
63
|
+
# committed lock; --upgrade discards that lock as a preference set, without
|
|
64
|
+
# which uv reuses it and lowers nothing; --frozen on sync and run stops uv
|
|
65
|
+
# re-resolving at the default highest strategy and discarding the floors it
|
|
66
|
+
# just computed. Rewriting uv.lock here is expected and must never be
|
|
67
|
+
# committed.
|
|
68
|
+
deps:floors:
|
|
69
|
+
extends: .default_rules
|
|
70
|
+
image: ghcr.io/astral-sh/uv:$UV_VERSION-python$PYTHON_VERSION-$BASE_LAYER
|
|
71
|
+
stage: deps
|
|
72
|
+
cache:
|
|
73
|
+
key: floors
|
|
74
|
+
paths:
|
|
75
|
+
- .cache/uv
|
|
76
|
+
script:
|
|
77
|
+
- uv lock --upgrade --resolution lowest-direct
|
|
78
|
+
- uv sync --frozen --all-extras
|
|
79
|
+
- uv run --frozen pytest
|
|
@@ -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.16.5
|
|
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,92 @@
|
|
|
1
|
+
## 3.7.0 (2026-08-28)
|
|
2
|
+
|
|
3
|
+
### ✨ Features
|
|
4
|
+
|
|
5
|
+
- **deps**: require only what the core imports
|
|
6
|
+
|
|
7
|
+
### 🐛🚑️ Fixes
|
|
8
|
+
|
|
9
|
+
- **deps**: declare starlette directly
|
|
10
|
+
|
|
11
|
+
### 💚👷 CI & Build
|
|
12
|
+
|
|
13
|
+
- add the pipeline with a lowest-direct floors gate
|
|
14
|
+
|
|
15
|
+
### 📌➕⬇️➖⬆️ Dependencies
|
|
16
|
+
|
|
17
|
+
- upgrade all dependencies and adapt to ruff 0.16
|
|
18
|
+
|
|
19
|
+
## 3.6.0 (2026-08-28)
|
|
20
|
+
|
|
21
|
+
### ✨ Features
|
|
22
|
+
|
|
23
|
+
- **arch**: name the binding lifetimes, deprecate the implicit one
|
|
24
|
+
- **security**: add make_async and check_async
|
|
25
|
+
- **arch**: expose the container backing the current scope
|
|
26
|
+
|
|
27
|
+
### 🐛🚑️ Fixes
|
|
28
|
+
|
|
29
|
+
- **events**: dispatch once per registration, and defer to the outermost transaction
|
|
30
|
+
- **config**: make freeze() actually freeze and merge() recursive
|
|
31
|
+
- **config**: stop config files from displacing real modules
|
|
32
|
+
- **arch**: run lifespan teardown even when the body raises
|
|
33
|
+
- **security**: stop requiring the optional hasher tuning config
|
|
34
|
+
- **arch**: make register() bind, or say why it cannot
|
|
35
|
+
- **database**: keep the transaction when the registry contextvar is unset
|
|
36
|
+
|
|
37
|
+
### 📝💡 Documentation
|
|
38
|
+
|
|
39
|
+
- **general**: correct the Config stub, a lying docstring and a wrong annotation
|
|
40
|
+
|
|
41
|
+
### 🔧🔨📦️ Configuration, Scripts, Packages
|
|
42
|
+
|
|
43
|
+
- **tooling**: replace mypy with pyrefly at strict
|
|
44
|
+
|
|
45
|
+
## 3.5.1 (2026-08-24)
|
|
46
|
+
|
|
47
|
+
### 🐛🚑️ Fixes
|
|
48
|
+
|
|
49
|
+
- **database**: run commit callbacks after the commit, not before
|
|
50
|
+
|
|
51
|
+
### 📝💡 Documentation
|
|
52
|
+
|
|
53
|
+
- **general**: update docs
|
|
54
|
+
- **README**: very short section on contribution
|
|
55
|
+
- **docs**: extension ideas
|
|
56
|
+
|
|
57
|
+
## 3.5.0 (2026-06-11)
|
|
58
|
+
|
|
59
|
+
### ✨ Features
|
|
60
|
+
|
|
61
|
+
- **arch**: improve app facade stub
|
|
62
|
+
- **events**: make the event facade properly depend on the dispatcher contract
|
|
63
|
+
- **events**: new tests for events
|
|
64
|
+
|
|
65
|
+
### 🐛🚑️ Fixes
|
|
66
|
+
|
|
67
|
+
- **events**: fixing handling of async before-dispatch hooks
|
|
68
|
+
|
|
69
|
+
### 🏷️ Types
|
|
70
|
+
|
|
71
|
+
- **events**: improve event facade stub
|
|
72
|
+
|
|
73
|
+
## 3.4.0 (2026-06-10)
|
|
74
|
+
|
|
75
|
+
### ✨ Features
|
|
76
|
+
|
|
77
|
+
- **arch**: new from_context method
|
|
78
|
+
- **tooling**: placeholder commit
|
|
79
|
+
|
|
80
|
+
### 💚👷 CI & Build
|
|
81
|
+
|
|
82
|
+
- **tooling**: default to base tag version for commitizen
|
|
83
|
+
|
|
84
|
+
### 📝💡 Documentation
|
|
85
|
+
|
|
86
|
+
- **project**: update doc
|
|
87
|
+
- **core**: placeholde readme
|
|
88
|
+
|
|
89
|
+
### 🔧🔨📦️ Configuration, Scripts, Packages
|
|
90
|
+
|
|
91
|
+
- **tooling**: update changelog script
|
|
92
|
+
- **project**: update versioning config
|
|
@@ -18,7 +18,7 @@ ecosystem is listed in `README.md`.
|
|
|
18
18
|
|
|
19
19
|
**Python Version:** 3.12+
|
|
20
20
|
**Build System:** uv + hatchling
|
|
21
|
-
**Type checker:**
|
|
21
|
+
**Type checker:** pyrefly (`preset = "strict"`, configured in `[tool.pyrefly]`)
|
|
22
22
|
|
|
23
23
|
## Commands
|
|
24
24
|
|
|
@@ -26,7 +26,7 @@ ecosystem is listed in `README.md`.
|
|
|
26
26
|
# Development (poe task aliases)
|
|
27
27
|
poe lint # ruff check
|
|
28
28
|
poe fmt # ruff format
|
|
29
|
-
poe tc #
|
|
29
|
+
poe tc # pyrefly check
|
|
30
30
|
poe test # pytest
|
|
31
31
|
poe test-cov # tests with coverage (--cov-report=term-missing)
|
|
32
32
|
poe test-full # coverage over the neva package
|
|
@@ -72,11 +72,14 @@ config = Config.get("app.name").unwrap_or("MyApp")
|
|
|
72
72
|
await Event.dispatch(UserCreated(event_id=1, user_id=42))
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
**Service Providers** - Register services with lifecycle hooks
|
|
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:
|
|
76
79
|
```python
|
|
77
80
|
class MyProvider(ServiceProvider):
|
|
78
81
|
def register(self) -> Result[Self, str]:
|
|
79
|
-
self.
|
|
82
|
+
_ = self.singleton(MyService)
|
|
80
83
|
return Ok(self)
|
|
81
84
|
|
|
82
85
|
@asynccontextmanager
|
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-neva
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.7.0
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Requires-Dist: aiosqlite>=0.20.0
|
|
7
7
|
Requires-Dist: asyncpg>=0.30.0
|
|
8
8
|
Requires-Dist: cryptography>=46.0.3
|
|
9
9
|
Requires-Dist: dishka>=1.10.0
|
|
10
|
-
Requires-Dist: fastapi[all]>=0.129.0
|
|
11
|
-
Requires-Dist: faststream>=0.6.6
|
|
12
10
|
Requires-Dist: opentelemetry-instrumentation-sqlalchemy>=0.62b0
|
|
13
11
|
Requires-Dist: pwdlib[argon2,bcrypt]>=0.3.0
|
|
12
|
+
Requires-Dist: pydantic>=2.7
|
|
14
13
|
Requires-Dist: pyinstrument>=5.1.1
|
|
15
14
|
Requires-Dist: sqlalchemy[asyncio]>=2.0.0
|
|
15
|
+
Requires-Dist: starlette>=0.41
|
|
16
16
|
Requires-Dist: structlog>=25.5.0
|
|
17
|
-
Requires-Dist:
|
|
17
|
+
Requires-Dist: typing-extensions>=4.13
|
|
18
18
|
Provides-Extra: fastapi
|
|
19
|
-
Requires-Dist: neva-fastapi>=1.
|
|
19
|
+
Requires-Dist: neva-fastapi>=1.1.1; extra == 'fastapi'
|
|
20
|
+
Provides-Extra: faststream
|
|
21
|
+
Requires-Dist: faststream>=0.6.6; extra == 'faststream'
|
|
22
|
+
Provides-Extra: polyfactory
|
|
23
|
+
Requires-Dist: polyfactory>=3.1.0; extra == 'polyfactory'
|
|
20
24
|
Provides-Extra: testing
|
|
21
25
|
Requires-Dist: pytest-asyncio>=0.25.3; extra == 'testing'
|
|
22
26
|
Requires-Dist: pytest>=9.0.2; extra == 'testing'
|
|
@@ -58,7 +62,7 @@ independent repo, independently versioned and published.
|
|
|
58
62
|
uv sync # install/refresh deps from uv.lock
|
|
59
63
|
poe lint # ruff check
|
|
60
64
|
poe fmt # ruff format
|
|
61
|
-
poe tc # type-check (
|
|
65
|
+
poe tc # type-check (pyrefly)
|
|
62
66
|
poe test # pytest
|
|
63
67
|
poe test-cov # tests with coverage
|
|
64
68
|
|
|
@@ -34,7 +34,7 @@ independent repo, independently versioned and published.
|
|
|
34
34
|
uv sync # install/refresh deps from uv.lock
|
|
35
35
|
poe lint # ruff check
|
|
36
36
|
poe fmt # ruff format
|
|
37
|
-
poe tc # type-check (
|
|
37
|
+
poe tc # type-check (pyrefly)
|
|
38
38
|
poe test # pytest
|
|
39
39
|
poe test-cov # tests with coverage
|
|
40
40
|
|
|
@@ -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]]]
|
|
@@ -8,6 +8,7 @@ container and optionally managing their lifecycle.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import abc
|
|
11
|
+
import warnings
|
|
11
12
|
from contextlib import AbstractAsyncContextManager
|
|
12
13
|
from typing import (
|
|
13
14
|
TYPE_CHECKING,
|
|
@@ -70,7 +71,7 @@ class ServiceProvider(abc.ABC):
|
|
|
70
71
|
"""
|
|
71
72
|
|
|
72
73
|
app: "Application"
|
|
73
|
-
listen: ClassVar[dict[type[Event], list[type[EventListener[Any]]]]] = {}
|
|
74
|
+
listen: ClassVar[dict[type[Event[Any]], list[type[EventListener[Any]]]]] = {}
|
|
74
75
|
when: ClassVar[Marker | None] = None
|
|
75
76
|
|
|
76
77
|
def __init__(self, app: "Application") -> None:
|
|
@@ -106,17 +107,91 @@ class ServiceProvider(abc.ABC):
|
|
|
106
107
|
interface: type | None = None,
|
|
107
108
|
scope: BaseScope | None = None,
|
|
108
109
|
when: BaseMarker | None = None,
|
|
109
|
-
cache: bool =
|
|
110
|
+
cache: bool | None = None,
|
|
110
111
|
) -> None:
|
|
111
|
-
"""Binds a source to the container.
|
|
112
|
+
"""Binds a source to the container.
|
|
113
|
+
|
|
114
|
+
Prefer the named lifetimes — `singleton`, `scoped`, `transient`,
|
|
115
|
+
`instance` — which say what they mean. Use `bind` directly only to
|
|
116
|
+
reach a scope those do not cover, and then pass `scope` explicitly.
|
|
117
|
+
|
|
118
|
+
Calling this with neither `scope` nor `cache` is deprecated: the
|
|
119
|
+
resulting binding is a process-wide singleton, which is the opposite of
|
|
120
|
+
Laravel's transient `bind`, and nothing at the call site says so.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
source: The class or factory to bind.
|
|
124
|
+
interface: The type to resolve it as. Defaults to `source`.
|
|
125
|
+
scope: The dishka scope. Defaults to the provider's (APP).
|
|
126
|
+
when: Marker gating this binding.
|
|
127
|
+
cache: Whether to reuse one instance per scope. Defaults to True.
|
|
128
|
+
"""
|
|
129
|
+
if scope is None and cache is None:
|
|
130
|
+
warnings.warn(
|
|
131
|
+
"bind() without an explicit lifetime is deprecated and will "
|
|
132
|
+
"require one in 4.0. It currently creates a process-wide "
|
|
133
|
+
"singleton: use singleton() for that, or scoped() / "
|
|
134
|
+
"transient() / instance(), or pass scope= or cache= yourself.",
|
|
135
|
+
DeprecationWarning,
|
|
136
|
+
stacklevel=2,
|
|
137
|
+
)
|
|
138
|
+
|
|
112
139
|
_ = self.provider.provide(
|
|
113
140
|
source=source,
|
|
114
141
|
scope=scope,
|
|
115
142
|
provides=interface,
|
|
116
|
-
cache=cache,
|
|
143
|
+
cache=True if cache is None else cache,
|
|
117
144
|
when=when,
|
|
118
145
|
)
|
|
119
146
|
|
|
147
|
+
def singleton(
|
|
148
|
+
self,
|
|
149
|
+
source: type | Callable[..., Any],
|
|
150
|
+
*,
|
|
151
|
+
interface: type | None = None,
|
|
152
|
+
when: BaseMarker | None = None,
|
|
153
|
+
) -> Self:
|
|
154
|
+
"""Binds the source at APP scope, as one instance for the process.
|
|
155
|
+
|
|
156
|
+
This is what a bare `bind` has always done; naming it makes the
|
|
157
|
+
lifetime visible at the call site.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
the provider itself for chaining purposes
|
|
161
|
+
"""
|
|
162
|
+
self.bind(source, interface=interface, scope=Scope.APP, when=when, cache=True)
|
|
163
|
+
return self
|
|
164
|
+
|
|
165
|
+
def instance(
|
|
166
|
+
self,
|
|
167
|
+
value: object,
|
|
168
|
+
*,
|
|
169
|
+
interface: type | None = None,
|
|
170
|
+
when: BaseMarker | None = None,
|
|
171
|
+
) -> Self:
|
|
172
|
+
"""Binds an object that already exists.
|
|
173
|
+
|
|
174
|
+
Nothing is constructed: the value handed in is what every resolution
|
|
175
|
+
returns. Use it for configuration objects, clients built during boot,
|
|
176
|
+
and test doubles.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
value: The object to hand out.
|
|
180
|
+
interface: The type to resolve it as. Defaults to `type(value)`.
|
|
181
|
+
when: Marker gating this binding.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
the provider itself for chaining purposes
|
|
185
|
+
"""
|
|
186
|
+
self.bind(
|
|
187
|
+
lambda: value,
|
|
188
|
+
interface=interface if interface is not None else type(value),
|
|
189
|
+
scope=Scope.APP,
|
|
190
|
+
when=when,
|
|
191
|
+
cache=True,
|
|
192
|
+
)
|
|
193
|
+
return self
|
|
194
|
+
|
|
120
195
|
def scoped(
|
|
121
196
|
self,
|
|
122
197
|
source: type | Callable[..., Any],
|
|
@@ -124,10 +199,10 @@ class ServiceProvider(abc.ABC):
|
|
|
124
199
|
interface: type | None = None,
|
|
125
200
|
when: BaseMarker | None = None,
|
|
126
201
|
) -> Self:
|
|
127
|
-
"""Binds the source
|
|
202
|
+
"""Binds the source at REQUEST scope.
|
|
128
203
|
|
|
129
|
-
|
|
130
|
-
|
|
204
|
+
One instance is created per request scope and cached for its duration.
|
|
205
|
+
For any other scope, call `bind` with an explicit `scope`.
|
|
131
206
|
|
|
132
207
|
Returns:
|
|
133
208
|
the provider itself for chaining purposes
|
|
@@ -14,6 +14,11 @@ from neva.support import Err, Ok, Option, Result
|
|
|
14
14
|
from neva.support.accessors import get_attr
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
# Deliberately not "neva.config": a config file named repository.py or
|
|
18
|
+
# loader.py would otherwise displace the real module of that name.
|
|
19
|
+
_CONFIG_MODULE_NAMESPACE = "neva._app_config"
|
|
20
|
+
|
|
21
|
+
|
|
17
22
|
class ConfigLoader:
|
|
18
23
|
"""Loads configuration files from a directory.
|
|
19
24
|
|
|
@@ -90,15 +95,22 @@ class ConfigLoader:
|
|
|
90
95
|
"""
|
|
91
96
|
try:
|
|
92
97
|
spec = importlib.util.spec_from_file_location(
|
|
93
|
-
f"
|
|
98
|
+
f"{_CONFIG_MODULE_NAMESPACE}.{module_name}", file_path
|
|
94
99
|
)
|
|
95
100
|
|
|
96
101
|
if spec is None or spec.loader is None:
|
|
97
102
|
return Err(f"Could not load module spec for {file_path}")
|
|
98
103
|
|
|
99
104
|
module = importlib.util.module_from_spec(spec)
|
|
105
|
+
# Registered so dataclasses, pickle and typing lookups inside the
|
|
106
|
+
# config file can resolve it, then removed: these are data files,
|
|
107
|
+
# not importable modules, and leaving them in place would let a
|
|
108
|
+
# second config directory shadow the first.
|
|
100
109
|
sys.modules[spec.name] = module
|
|
101
|
-
|
|
110
|
+
try:
|
|
111
|
+
spec.loader.exec_module(module)
|
|
112
|
+
finally:
|
|
113
|
+
_ = sys.modules.pop(spec.name, None)
|
|
102
114
|
|
|
103
115
|
config_dict = self._extract_config(module)
|
|
104
116
|
return Ok(config_dict)
|