python-neva 5.4.0__tar.gz → 5.4.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-5.4.0 → python_neva-5.4.1}/CHANGELOG.md +7 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/PKG-INFO +1 -1
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/arch/application.py +39 -6
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/fragments/configuration.md +14 -3
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/fragments/drivers.md +1 -1
- {python_neva-5.4.0 → python_neva-5.4.1}/pyproject.toml +1 -1
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/config/test_provider_declarations.py +111 -2
- {python_neva-5.4.0 → python_neva-5.4.1}/uv.lock +1 -1
- {python_neva-5.4.0 → python_neva-5.4.1}/.envrc +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/.gitignore +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/.gitlab-ci.yml +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/.pre-commit-config.yaml +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/.python-version +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/CLAUDE.md +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/README.md +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/arch/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/arch/config.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/arch/facade.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/arch/integrations/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/arch/integrations/faststream.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/arch/markers.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/arch/py.typed +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/arch/scopes.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/arch/service_provider.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/config/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/config/base_providers.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/config/loader.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/config/py.typed +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/config/repository.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/database/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/database/config.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/database/connection.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/database/manager.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/database/provider.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/database/py.typed +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/database/transaction.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/contracts/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/contracts/dispatcher.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/contracts/event.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/contracts/handler.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/contracts/listener.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/dispatcher.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/event.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/event_registry.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/listener.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/policy.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/provider.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/events/py.typed +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/fragments/database-transactions.md +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/fragments/events.md +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/fragments/facades.md +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/fragments/factories.md +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/fragments/observability.md +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/fragments/result-option.md +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/fragments/security.md +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/fragments/service-providers.md +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/fragments/testing.md +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/manifest.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/guidelines/py.typed +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/obs/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/obs/config.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/obs/logging/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/obs/logging/channels.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/obs/logging/contracts.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/obs/logging/manager.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/obs/logging/provider.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/obs/logging/resolver.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/obs/py.typed +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/polyfactory/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/polyfactory/factories.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/polyfactory/persistence.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/polyfactory/py.typed +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/encryption/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/encryption/encrypter.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/encryption/protocol.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/hashing/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/hashing/config.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/hashing/hash_manager.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/hashing/hashers/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/hashing/hashers/argon2.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/hashing/hashers/bcrypt.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/hashing/hashers/protocol.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/provider.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/py.typed +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/tokens/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/tokens/generate_token.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/tokens/hash_token.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/security/tokens/verify_token.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/accessors.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/app.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/app.pyi +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/config.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/config.pyi +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/crypt.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/crypt.pyi +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/db.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/db.pyi +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/event.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/event.pyi +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/hash.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/hash.pyi +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/log.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/facade/log.pyi +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/py.typed +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/results/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/results/collect.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/results/convert.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/results/exceptions.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/results/option.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/results/pipeline.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/results/result.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/strategy.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/strconv.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/support/time.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/testing/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/testing/fakes.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/testing/fixtures.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/testing/py.typed +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/neva/testing/test_case.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/ruff.toml +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/scripts/retag-with-changelog.sh +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/arch/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/arch/test_application_create.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/arch/test_cache.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/arch/test_config_shapes.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/arch/test_context.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/arch/test_extends.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/arch/test_facade_resolution.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/arch/test_facade_root_nesting.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/arch/test_lifetimes.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/arch/test_register_resolution.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/arch/test_registration.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/arch/test_scope.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/config/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/config/test_base_providers.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/config/test_config_path_resolution.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/config/test_loader.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/config/test_repository.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/conftest.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/database/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/database/test_connection_manager.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/database/test_database_manager.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/database/test_detached_lifespan.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/database/test_edge_cases.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/database/test_multi_connection.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/database/test_provider_lifespan.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/database/test_sqlalchemy_integration.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/database/test_transaction.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/database/test_transaction_callbacks.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/database/test_transaction_context.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/database/test_transaction_registry.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/conftest.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/test_before_dispatch.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/test_binding.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/test_concurrent.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/test_deferred.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/test_dispatch.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/test_event.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/test_function_listener.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/test_immediate.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/test_listen_on_parent_class.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/test_listener_wiring.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/events/test_unimplemented_handle.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/guidelines/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/guidelines/test_manifest.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/obs/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/obs/conftest.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/obs/test_channels.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/obs/test_facade.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/obs/test_manager.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/obs/test_provider_hook.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/obs/test_resolver.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/polyfactory/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/polyfactory/test_model_factory.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/security/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/security/test_config_shapes.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/security/test_encrypter.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/security/test_hash_manager.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/security/test_tokens.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/support/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/support/test_accessors.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/support/test_collect.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/support/test_pipeline.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/support/test_results.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/support/test_results_imports.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/support/test_strategy.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/support/test_strategy_imports.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/testing/__init__.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/testing/test_application_reuse.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/testing/test_boot_application.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/testing/test_create_config_migration.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/testing/test_event_fake.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/testing/test_facade_restore.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/testing/test_fixtures.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/testing/test_refresh_database.py +0 -0
- {python_neva-5.4.0 → python_neva-5.4.1}/tests/testing/test_test_case.py +0 -0
|
@@ -68,6 +68,7 @@ class Application:
|
|
|
68
68
|
self._extra_providers: list[BaseProvider] = []
|
|
69
69
|
self._container: dishka.AsyncContainer | None = None
|
|
70
70
|
self._booted: bool = False
|
|
71
|
+
self._declarations_applied: bool = False
|
|
71
72
|
|
|
72
73
|
configuration_path = _resolve_config_path(config_path)
|
|
73
74
|
match ConfigLoader(configuration_path).load_all():
|
|
@@ -164,6 +165,15 @@ class Application:
|
|
|
164
165
|
would orphan. Registering after boot is an error rather than a
|
|
165
166
|
silently ignored call.
|
|
166
167
|
|
|
168
|
+
A provider arriving after construction has its declared config
|
|
169
|
+
namespaces validated here, and its secrets marked once it registers, so
|
|
170
|
+
that it is held to the same shape as one named in the `providers`
|
|
171
|
+
config namespace. Validation runs before the provider is instantiated:
|
|
172
|
+
a malformed namespace leaves nothing registered and no secret marked.
|
|
173
|
+
Providers registered *during* construction are skipped here and
|
|
174
|
+
validated together by `_apply_provider_config_declarations`, which
|
|
175
|
+
reports every bad namespace in one message.
|
|
176
|
+
|
|
167
177
|
Returns:
|
|
168
178
|
Result containing the registered provider instance or an error message.
|
|
169
179
|
"""
|
|
@@ -177,12 +187,18 @@ class Application:
|
|
|
177
187
|
"or declare them in the 'providers' config namespace."
|
|
178
188
|
)
|
|
179
189
|
|
|
190
|
+
late = self._declarations_applied
|
|
191
|
+
if late and (failures := self._declaration_failures(provider)):
|
|
192
|
+
return Err("Invalid configuration: " + "; ".join(failures))
|
|
193
|
+
|
|
180
194
|
registered = (
|
|
181
195
|
provider(self)
|
|
182
196
|
.register()
|
|
183
197
|
.map(lambda p: self.providers.setdefault(provider, p))
|
|
184
198
|
)
|
|
185
199
|
if registered.is_ok:
|
|
200
|
+
if late:
|
|
201
|
+
self.config.mark_sensitive(*provider.sensitive)
|
|
186
202
|
self._container = None
|
|
187
203
|
return registered
|
|
188
204
|
|
|
@@ -194,22 +210,39 @@ class Application:
|
|
|
194
210
|
configuration error. Every failure is reported together, so a boot
|
|
195
211
|
naming three bad namespaces takes one run rather than three.
|
|
196
212
|
|
|
213
|
+
Providers registered after this point go through `register`, which
|
|
214
|
+
applies the same declarations one provider at a time.
|
|
215
|
+
|
|
197
216
|
Raises:
|
|
198
217
|
RuntimeError: If any declared namespace fails its shape.
|
|
199
218
|
"""
|
|
200
|
-
|
|
219
|
+
failures: list[str] = []
|
|
201
220
|
for provider in self.providers.values():
|
|
202
221
|
declaring = type(provider)
|
|
203
222
|
self.config.mark_sensitive(*declaring.sensitive)
|
|
204
|
-
|
|
205
|
-
self._validate_namespace(namespace, schema)
|
|
206
|
-
for namespace, schema in declaring.config_schema.items()
|
|
207
|
-
)
|
|
223
|
+
failures.extend(self._declaration_failures(declaring))
|
|
208
224
|
|
|
209
|
-
_, failures = partition(checks)
|
|
210
225
|
if failures:
|
|
211
226
|
raise RuntimeError("Invalid configuration: " + "; ".join(failures))
|
|
212
227
|
|
|
228
|
+
self._declarations_applied = True
|
|
229
|
+
|
|
230
|
+
def _declaration_failures(self, declaring: type[ServiceProvider]) -> list[str]:
|
|
231
|
+
"""Check one provider's declared namespaces, applying nothing.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
declaring: The provider type whose `config_schema` to check.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
One message per malformed namespace, empty when every declared
|
|
238
|
+
namespace is absent or valid.
|
|
239
|
+
"""
|
|
240
|
+
_, failures = partition(
|
|
241
|
+
self._validate_namespace(namespace, schema)
|
|
242
|
+
for namespace, schema in declaring.config_schema.items()
|
|
243
|
+
)
|
|
244
|
+
return failures
|
|
245
|
+
|
|
213
246
|
def _validate_namespace(
|
|
214
247
|
self, namespace: str, schema: TypeForm[Any]
|
|
215
248
|
) -> Result[None, str]:
|
|
@@ -104,9 +104,9 @@ reaches the repository, frozen or not.
|
|
|
104
104
|
|
|
105
105
|
## Validating a namespace at boot
|
|
106
106
|
|
|
107
|
-
A provider declares the shape its namespace must have, and it is checked
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
A provider declares the shape its namespace must have, and it is checked at boot (since 5.4).
|
|
108
|
+
The declaration reuses the `TypedDict` shapes above — nothing needs to become a pydantic
|
|
109
|
+
model.
|
|
110
110
|
|
|
111
111
|
```python
|
|
112
112
|
class DatabaseServiceProvider(ServiceProvider):
|
|
@@ -120,10 +120,21 @@ class DatabaseServiceProvider(ServiceProvider):
|
|
|
120
120
|
- A provider that declares nothing is validated not at all — the behaviour before 5.4.
|
|
121
121
|
- The constructor raises, as it does for any config failure; `Application.create()` returns it
|
|
122
122
|
as an `Err` instead.
|
|
123
|
+
- **A provider registered after construction is checked too, and reports as an `Err` from
|
|
124
|
+
`Application.register` rather than raising** (since 5.4.1). That is the path
|
|
125
|
+
`App.register` on `neva-fastapi` and `neva-faststream` delegates to; before 5.4.1 it skipped
|
|
126
|
+
validation and marked no secrets. A rejected provider is not registered at all.
|
|
127
|
+
|
|
128
|
+
**`type_` is a cast, not validation, so the value a malformed key holds is still whatever the
|
|
129
|
+
file put there.** Validation only *rejects* it — nothing is coerced and written back, and
|
|
130
|
+
pydantic's default lax mode accepts `"false"` for a `bool`. A namespace whose values must be
|
|
131
|
+
exactly typed declares `__pydantic_config__ = ConfigDict(strict=True)` on its `TypedDict`.
|
|
123
132
|
|
|
124
133
|
## Secrets
|
|
125
134
|
|
|
126
135
|
A provider marks the keys that hold secrets, and they are replaced in the display form only.
|
|
136
|
+
Marking happens wherever the provider is registered — at construction, or through
|
|
137
|
+
`Application.register` (since 5.4.1).
|
|
127
138
|
|
|
128
139
|
```python
|
|
129
140
|
class SecurityProvider(ServiceProvider):
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
id: drivers
|
|
3
3
|
title: Swappable drivers
|
|
4
|
-
requires: python-neva>=5.
|
|
4
|
+
requires: python-neva>=5.0
|
|
5
5
|
triggers: [swappable backend, driver manager, multiple implementations, choosing an implementation from config, registering a driver, plugin extension point]
|
|
6
6
|
priority: 70
|
|
7
7
|
verified_by: [tests/support/test_strategy.py, tests/support/test_strategy_imports.py]
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
"""Config namespaces a provider declares are validated and redacted at boot."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Any, ClassVar
|
|
4
|
+
from typing import Any, ClassVar, NotRequired, TypedDict, override
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
7
|
from typing_extensions import TypeForm
|
|
8
8
|
|
|
9
|
-
from neva.arch import Application
|
|
9
|
+
from neva.arch import Application, ServiceProvider
|
|
10
10
|
from neva.config.repository import REDACTED, ConfigRepository
|
|
11
|
+
from neva.support import Ok, Result
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
def _config_dir(tmp_path: Path, **files: str) -> Path:
|
|
@@ -28,6 +29,34 @@ config = {"connections": {"default": {"url": "sqlite+aiosqlite:///:memory:"}}}
|
|
|
28
29
|
BAD_DATABASE = """
|
|
29
30
|
config = {"connections": {"default": {"pool_size": 5}}}
|
|
30
31
|
"""
|
|
32
|
+
GOOD_WIDGETS = """
|
|
33
|
+
config = {"per_page": 25, "token": "s3cret"}
|
|
34
|
+
"""
|
|
35
|
+
BAD_WIDGETS = """
|
|
36
|
+
config = {"per_page": "many"}
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class WidgetConfig(TypedDict):
|
|
41
|
+
"""A namespace owned by a provider that is registered after construction."""
|
|
42
|
+
|
|
43
|
+
per_page: NotRequired[int]
|
|
44
|
+
token: NotRequired[str]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class WidgetProvider(ServiceProvider):
|
|
48
|
+
config_schema: ClassVar[dict[str, TypeForm[Any]]] = {"widgets": WidgetConfig}
|
|
49
|
+
sensitive: ClassVar[tuple[str, ...]] = ("widgets.token",)
|
|
50
|
+
|
|
51
|
+
@override
|
|
52
|
+
def register(self) -> Result["WidgetProvider", str]:
|
|
53
|
+
return Ok(self)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SilentProvider(ServiceProvider):
|
|
57
|
+
@override
|
|
58
|
+
def register(self) -> Result["SilentProvider", str]:
|
|
59
|
+
return Ok(self)
|
|
31
60
|
|
|
32
61
|
|
|
33
62
|
class TestValidationAtBoot:
|
|
@@ -145,3 +174,83 @@ class TestDeclarationsAreTyped:
|
|
|
145
174
|
|
|
146
175
|
assert Declaring.config_schema == {"x": dict[str, int]}
|
|
147
176
|
assert Declaring.sensitive == ("x.token",)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class TestDeclarationsApplyToALateRegistration:
|
|
180
|
+
"""A provider registered after construction is held to the same shape.
|
|
181
|
+
|
|
182
|
+
The declarations pass runs in `__init__` over the providers construction
|
|
183
|
+
knows about. Every integration package exposes `Application.register` as
|
|
184
|
+
its own `register`, so a provider reaching the application that way used
|
|
185
|
+
to skip validation entirely and never mark its secrets.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
@staticmethod
|
|
189
|
+
def _app(tmp_path: Path, **files: str) -> Application:
|
|
190
|
+
return Application.create(config_path=_config_dir(tmp_path, **files)).unwrap()
|
|
191
|
+
|
|
192
|
+
def test_a_malformed_namespace_fails_the_registration(self, tmp_path: Path) -> None:
|
|
193
|
+
app = self._app(tmp_path, widgets=BAD_WIDGETS)
|
|
194
|
+
|
|
195
|
+
result = app.register(WidgetProvider)
|
|
196
|
+
|
|
197
|
+
assert result.is_err
|
|
198
|
+
assert "'widgets' is malformed" in result.unwrap_err()
|
|
199
|
+
assert "per_page" in result.unwrap_err()
|
|
200
|
+
|
|
201
|
+
def test_a_rejected_provider_is_not_registered(self, tmp_path: Path) -> None:
|
|
202
|
+
app = self._app(tmp_path, widgets=BAD_WIDGETS)
|
|
203
|
+
|
|
204
|
+
_ = app.register(WidgetProvider)
|
|
205
|
+
|
|
206
|
+
assert WidgetProvider not in app.providers
|
|
207
|
+
|
|
208
|
+
def test_a_rejected_provider_marks_no_secret(self, tmp_path: Path) -> None:
|
|
209
|
+
"""Validation runs before anything is applied, secrets included."""
|
|
210
|
+
app = self._app(tmp_path, widgets=BAD_WIDGETS)
|
|
211
|
+
|
|
212
|
+
_ = app.register(WidgetProvider)
|
|
213
|
+
|
|
214
|
+
assert "widgets.token" not in app.config.sensitive
|
|
215
|
+
|
|
216
|
+
def test_a_valid_namespace_registers(self, tmp_path: Path) -> None:
|
|
217
|
+
app = self._app(tmp_path, widgets=GOOD_WIDGETS)
|
|
218
|
+
|
|
219
|
+
assert app.register(WidgetProvider).is_ok
|
|
220
|
+
|
|
221
|
+
def test_an_absent_namespace_registers(self, tmp_path: Path) -> None:
|
|
222
|
+
app = self._app(tmp_path)
|
|
223
|
+
|
|
224
|
+
assert app.register(WidgetProvider).is_ok
|
|
225
|
+
|
|
226
|
+
def test_a_registered_provider_marks_its_secrets(self, tmp_path: Path) -> None:
|
|
227
|
+
app = self._app(tmp_path, widgets=GOOD_WIDGETS)
|
|
228
|
+
|
|
229
|
+
_ = app.register(WidgetProvider).unwrap()
|
|
230
|
+
|
|
231
|
+
assert "widgets.token" in app.config.sensitive
|
|
232
|
+
assert app.config.redacted()["widgets"]["token"] == REDACTED
|
|
233
|
+
assert app.config.get("widgets.token", type_=str).unwrap() == "s3cret"
|
|
234
|
+
|
|
235
|
+
def test_a_provider_declaring_nothing_still_registers(self, tmp_path: Path) -> None:
|
|
236
|
+
"""The additive guarantee, on this path too."""
|
|
237
|
+
app = self._app(tmp_path, widgets=BAD_WIDGETS)
|
|
238
|
+
|
|
239
|
+
assert app.register(SilentProvider).is_ok
|
|
240
|
+
|
|
241
|
+
def test_registering_twice_is_still_idempotent(self, tmp_path: Path) -> None:
|
|
242
|
+
app = self._app(tmp_path, widgets=GOOD_WIDGETS)
|
|
243
|
+
|
|
244
|
+
first = app.register(WidgetProvider).unwrap()
|
|
245
|
+
|
|
246
|
+
assert app.register(WidgetProvider).unwrap() is first
|
|
247
|
+
|
|
248
|
+
def test_a_construction_time_provider_is_still_reported_in_one_message(
|
|
249
|
+
self, tmp_path: Path
|
|
250
|
+
) -> None:
|
|
251
|
+
"""The batch path keeps its behaviour: one message, every namespace."""
|
|
252
|
+
config_dir = _config_dir(tmp_path, database=BAD_DATABASE)
|
|
253
|
+
|
|
254
|
+
message = Application.create(config_path=config_dir).unwrap_err()
|
|
255
|
+
|
|
256
|
+
assert message.startswith("Invalid configuration: ")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|