python-platform 0.1.1__py3-none-any.whl
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_platform/__init__.py +445 -0
- python_platform/application/__init__.py +6 -0
- python_platform/application/build_spec.py +37 -0
- python_platform/application/builder.py +260 -0
- python_platform/application/composition.py +219 -0
- python_platform/application/runtime.py +546 -0
- python_platform/application/runtime_hooks.py +327 -0
- python_platform/application/state.py +216 -0
- python_platform/application_services/__init__.py +54 -0
- python_platform/application_services/catalog.py +500 -0
- python_platform/application_services/contracts.py +188 -0
- python_platform/application_services/dispatcher.py +423 -0
- python_platform/application_services/errors.py +89 -0
- python_platform/application_services/execution.py +199 -0
- python_platform/application_services/interceptors.py +44 -0
- python_platform/application_services/invocation.py +427 -0
- python_platform/application_services/policies.py +46 -0
- python_platform/application_services/seeding.py +72 -0
- python_platform/application_services/signature.py +49 -0
- python_platform/application_services/validation.py +84 -0
- python_platform/auditing/__init__.py +12 -0
- python_platform/auditing/contracts.py +39 -0
- python_platform/auditing/control.py +48 -0
- python_platform/auditing/sqlalchemy/__init__.py +6 -0
- python_platform/auditing/sqlalchemy/migrations/0001_platform_auditing.py +45 -0
- python_platform/auditing/sqlalchemy/migrations/__init__.py +1 -0
- python_platform/auditing/sqlalchemy/models.py +31 -0
- python_platform/auditing/sqlalchemy/module.py +32 -0
- python_platform/auditing/sqlalchemy/store.py +63 -0
- python_platform/authorization/__init__.py +38 -0
- python_platform/authorization/catalog.py +63 -0
- python_platform/authorization/contracts.py +241 -0
- python_platform/authorization/definition_discovery.py +81 -0
- python_platform/authorization/definitions.py +43 -0
- python_platform/authorization/errors.py +40 -0
- python_platform/background_execution/__init__.py +15 -0
- python_platform/background_execution/application.py +41 -0
- python_platform/background_execution/child.py +123 -0
- python_platform/background_execution/contracts.py +39 -0
- python_platform/background_execution/lifecycle.py +22 -0
- python_platform/background_execution/local.py +127 -0
- python_platform/background_execution/locks.py +31 -0
- python_platform/background_execution/management.py +17 -0
- python_platform/background_execution/module.py +10 -0
- python_platform/background_execution/permissions.py +14 -0
- python_platform/background_execution/processes.py +331 -0
- python_platform/background_jobs/__init__.py +31 -0
- python_platform/background_jobs/catalog.py +287 -0
- python_platform/background_jobs/contracts.py +202 -0
- python_platform/background_jobs/errors.py +28 -0
- python_platform/background_jobs/execution.py +201 -0
- python_platform/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
- python_platform/background_jobs/pgqueuer/__init__.py +6 -0
- python_platform/background_jobs/pgqueuer/enqueue.py +82 -0
- python_platform/background_jobs/pgqueuer/migrations/0001_pgqueuer_1_3_2.py +63 -0
- python_platform/background_jobs/pgqueuer/migrations/__init__.py +1 -0
- python_platform/background_jobs/pgqueuer/module.py +102 -0
- python_platform/background_jobs/pgqueuer/options.py +70 -0
- python_platform/background_jobs/pgqueuer/runtime.py +479 -0
- python_platform/background_jobs/pgqueuer/sql/pgqueuer_1_3_2_install.sql +118 -0
- python_platform/background_jobs/pgqueuer/supervision.py +201 -0
- python_platform/background_workers/__init__.py +22 -0
- python_platform/background_workers/catalog.py +96 -0
- python_platform/background_workers/contracts.py +124 -0
- python_platform/background_workers/errors.py +20 -0
- python_platform/background_workers/execution.py +133 -0
- python_platform/background_workers/runtime.py +203 -0
- python_platform/caching/__init__.py +11 -0
- python_platform/caching/catalog.py +73 -0
- python_platform/caching/contracts.py +73 -0
- python_platform/caching/errors.py +20 -0
- python_platform/cli/__init__.py +87 -0
- python_platform/cli/errors.py +5 -0
- python_platform/cli/project.py +69 -0
- python_platform/cli/runtime.py +68 -0
- python_platform/configuration/__init__.py +21 -0
- python_platform/configuration/composition.py +65 -0
- python_platform/configuration/contracts.py +68 -0
- python_platform/configuration/dotenv_source.py +51 -0
- python_platform/configuration/environment_source.py +38 -0
- python_platform/configuration/immutability.py +124 -0
- python_platform/configuration/input_shape.py +58 -0
- python_platform/configuration/merge.py +103 -0
- python_platform/configuration/root.py +215 -0
- python_platform/configuration/sources.py +519 -0
- python_platform/configuration/values.py +427 -0
- python_platform/configuration/yaml_source.py +51 -0
- python_platform/developer_kit/__init__.py +1 -0
- python_platform/developer_kit/generation.py +182 -0
- python_platform/developer_kit/project_metadata.py +45 -0
- python_platform/developer_kit/source.py +53 -0
- python_platform/developer_kit/templates/module/cookiecutter.json +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/cache.py.jinja +10 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/events.py.jinja +24 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration.py.jinja +46 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +29 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options.py.jinja +8 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/orders.py.jinja +81 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/tasks.py.jinja +86 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/module.py.jinja +10 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/orders.py.jinja +36 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +10 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/orders.py.jinja +57 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repository.py.jinja +12 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding.py.jinja +19 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings.py.jinja +16 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/definitions.py.jinja +24 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +7 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions.py.jinja +15 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/files.py.jinja +74 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +58 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/realtime.py.jinja +37 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/migrations/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models.py.jinja +26 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +32 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repository.py.jinja +49 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +18 -0
- python_platform/developer_kit/templates/project/cookiecutter.json +6 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.dockerignore +8 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.env.jinja +13 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.gitignore +6 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.python-version +1 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/Dockerfile +22 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +86 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/app.yaml.jinja +16 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.dev.yaml.jinja +24 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.production.yaml.jinja +13 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/pyproject.toml.jinja +28 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/main.py.jinja +52 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/module.py.jinja +38 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/conftest.py.jinja +8 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/host/test_http.py.jinja +101 -0
- python_platform/developer_kit/wiring.py +85 -0
- python_platform/diagnostics/__init__.py +23 -0
- python_platform/diagnostics/journal.py +174 -0
- python_platform/diagnostics/model.py +83 -0
- python_platform/diagnostics/source.py +25 -0
- python_platform/distributed_lock/__init__.py +6 -0
- python_platform/distributed_lock/contracts.py +20 -0
- python_platform/distributed_lock/options.py +19 -0
- python_platform/domain/__init__.py +13 -0
- python_platform/domain/aggregates.py +64 -0
- python_platform/domain/entities.py +33 -0
- python_platform/domain/errors.py +12 -0
- python_platform/domain/value_objects.py +31 -0
- python_platform/errors/__init__.py +75 -0
- python_platform/errors/base.py +13 -0
- python_platform/errors/business.py +53 -0
- python_platform/errors/configuration.py +119 -0
- python_platform/errors/diagnostics.py +12 -0
- python_platform/errors/lifecycle.py +164 -0
- python_platform/errors/modularity.py +100 -0
- python_platform/errors/services.py +100 -0
- python_platform/events/__init__.py +25 -0
- python_platform/events/aggregate.py +39 -0
- python_platform/events/catalog.py +136 -0
- python_platform/events/contracts.py +39 -0
- python_platform/events/contribution.py +128 -0
- python_platform/events/discovery.py +105 -0
- python_platform/events/errors.py +48 -0
- python_platform/events/runtime.py +192 -0
- python_platform/fastapi/__init__.py +49 -0
- python_platform/fastapi/action.py +175 -0
- python_platform/fastapi/adapter.py +381 -0
- python_platform/fastapi/application_services.py +647 -0
- python_platform/fastapi/background.py +42 -0
- python_platform/fastapi/contracts.py +278 -0
- python_platform/fastapi/errors.py +32 -0
- python_platform/fastapi/filters.py +111 -0
- python_platform/fastapi/health.py +44 -0
- python_platform/fastapi/http.py +55 -0
- python_platform/fastapi/http_router.py +212 -0
- python_platform/fastapi/manual_action.py +105 -0
- python_platform/fastapi/middleware.py +93 -0
- python_platform/fastapi/parameters.py +97 -0
- python_platform/fastapi/realtime/__init__.py +13 -0
- python_platform/fastapi/realtime/authentication.py +181 -0
- python_platform/fastapi/realtime/connection.py +166 -0
- python_platform/fastapi/realtime/module.py +41 -0
- python_platform/fastapi/realtime/options.py +50 -0
- python_platform/fastapi/realtime/runtime.py +423 -0
- python_platform/fastapi/request_context.py +244 -0
- python_platform/fastapi/route_integrity.py +88 -0
- python_platform/fastapi/routing.py +410 -0
- python_platform/fastapi/server.py +181 -0
- python_platform/fastapi/settings.py +40 -0
- python_platform/fastapi/tracing.py +62 -0
- python_platform/fastapi/transfer.py +128 -0
- python_platform/fastapi/upload_limits.py +63 -0
- python_platform/fastapi/uploads.py +94 -0
- python_platform/hosted_services/__init__.py +29 -0
- python_platform/hosted_services/bridge.py +420 -0
- python_platform/hosted_services/catalog.py +259 -0
- python_platform/hosted_services/contracts.py +55 -0
- python_platform/hosted_services/errors.py +53 -0
- python_platform/hosted_services/options.py +17 -0
- python_platform/hosted_services/runtime.py +267 -0
- python_platform/hosted_services/state.py +40 -0
- python_platform/hosting/__init__.py +4 -0
- python_platform/hosting/instance.py +40 -0
- python_platform/identity/__init__.py +73 -0
- python_platform/identity/application.py +287 -0
- python_platform/identity/contracts.py +283 -0
- python_platform/identity/errors.py +28 -0
- python_platform/identity/http_api.py +90 -0
- python_platform/identity/module.py +46 -0
- python_platform/identity/passwords.py +34 -0
- python_platform/identity/permissions.py +14 -0
- python_platform/identity/services.py +108 -0
- python_platform/identity/sqlalchemy/__init__.py +5 -0
- python_platform/identity/sqlalchemy/migrations/0001_platform_identity.py +151 -0
- python_platform/identity/sqlalchemy/migrations/0002_physical_delete.py +42 -0
- python_platform/identity/sqlalchemy/migrations/__init__.py +1 -0
- python_platform/identity/sqlalchemy/models.py +85 -0
- python_platform/identity/sqlalchemy/module.py +62 -0
- python_platform/identity/sqlalchemy/stores.py +526 -0
- python_platform/identity/tokens.py +106 -0
- python_platform/invocation/__init__.py +3 -0
- python_platform/invocation/callables.py +171 -0
- python_platform/invocation/contracts.py +33 -0
- python_platform/invocation/entries.py +54 -0
- python_platform/invocation/function_runtime.py +48 -0
- python_platform/invocation/interception.py +184 -0
- python_platform/lifecycle/__init__.py +25 -0
- python_platform/lifecycle/composition.py +35 -0
- python_platform/lifecycle/context.py +31 -0
- python_platform/lifecycle/runtime.py +43 -0
- python_platform/lifecycle/state.py +20 -0
- python_platform/modularity/__init__.py +15 -0
- python_platform/modularity/contracts.py +90 -0
- python_platform/modularity/discovery.py +143 -0
- python_platform/modularity/graph.py +334 -0
- python_platform/modularity/registry.py +100 -0
- python_platform/modularity/selection.py +16 -0
- python_platform/notifications/__init__.py +19 -0
- python_platform/notifications/catalog.py +46 -0
- python_platform/notifications/contracts.py +107 -0
- python_platform/observability/__init__.py +5 -0
- python_platform/observability/context.py +43 -0
- python_platform/observability/export.py +73 -0
- python_platform/observability/formatting.py +89 -0
- python_platform/observability/logging.py +118 -0
- python_platform/observability/options.py +45 -0
- python_platform/observability/tracing.py +80 -0
- python_platform/options/__init__.py +19 -0
- python_platform/options/aliases.py +331 -0
- python_platform/options/contribution.py +62 -0
- python_platform/options/immutability.py +48 -0
- python_platform/options/input_keys.py +214 -0
- python_platform/options/issues.py +335 -0
- python_platform/options/models.py +114 -0
- python_platform/options/registry.py +280 -0
- python_platform/options/schema.py +488 -0
- python_platform/options/validation.py +120 -0
- python_platform/py.typed +0 -0
- python_platform/realtime/__init__.py +12 -0
- python_platform/realtime/contracts.py +28 -0
- python_platform/realtime/diagnostics.py +37 -0
- python_platform/realtime/messages.py +113 -0
- python_platform/redis/__init__.py +6 -0
- python_platform/redis/distributed_lock.py +212 -0
- python_platform/redis/lease_lock.py +36 -0
- python_platform/redis/module.py +50 -0
- python_platform/redis/notification_runtime.py +182 -0
- python_platform/redis/notifications.py +25 -0
- python_platform/redis/options.py +35 -0
- python_platform/redis/runtime.py +116 -0
- python_platform/services/__init__.py +39 -0
- python_platform/services/application_bindings.py +221 -0
- python_platform/services/arbitration.py +381 -0
- python_platform/services/binding.py +102 -0
- python_platform/services/contribution.py +438 -0
- python_platform/services/convention.py +292 -0
- python_platform/services/convention_contracts.py +94 -0
- python_platform/services/exposure.py +23 -0
- python_platform/services/framework_provider.py +178 -0
- python_platform/services/native_graph.py +172 -0
- python_platform/services/provider.py +204 -0
- python_platform/services/registration.py +101 -0
- python_platform/services/runtime.py +298 -0
- python_platform/settings/__init__.py +30 -0
- python_platform/settings/application.py +95 -0
- python_platform/settings/binding.py +33 -0
- python_platform/settings/catalog.py +121 -0
- python_platform/settings/changes.py +14 -0
- python_platform/settings/contracts.py +115 -0
- python_platform/settings/definition_discovery.py +71 -0
- python_platform/settings/definitions.py +41 -0
- python_platform/settings/errors.py +22 -0
- python_platform/settings/handlers.py +43 -0
- python_platform/settings/management.py +57 -0
- python_platform/settings/manager.py +89 -0
- python_platform/settings/module.py +7 -0
- python_platform/settings/notifications.py +39 -0
- python_platform/settings/permissions.py +14 -0
- python_platform/settings/provider.py +74 -0
- python_platform/settings/refresh.py +140 -0
- python_platform/settings/refresh_module.py +45 -0
- python_platform/settings/sqlalchemy/__init__.py +5 -0
- python_platform/settings/sqlalchemy/migrations/0001_platform_settings.py +31 -0
- python_platform/settings/sqlalchemy/migrations/0002_physical_delete.py +32 -0
- python_platform/settings/sqlalchemy/migrations/0003_version_tokens.py +39 -0
- python_platform/settings/sqlalchemy/migrations/__init__.py +1 -0
- python_platform/settings/sqlalchemy/models.py +36 -0
- python_platform/settings/sqlalchemy/module.py +36 -0
- python_platform/settings/sqlalchemy/store.py +86 -0
- python_platform/settings/store.py +16 -0
- python_platform/settings/values.py +39 -0
- python_platform/sqlalchemy/__init__.py +51 -0
- python_platform/sqlalchemy/alembic_runtime/__init__.py +1 -0
- python_platform/sqlalchemy/alembic_runtime/env.py +27 -0
- python_platform/sqlalchemy/alembic_runtime/script.py.mako +14 -0
- python_platform/sqlalchemy/auditing.py +48 -0
- python_platform/sqlalchemy/errors.py +85 -0
- python_platform/sqlalchemy/metadata.py +538 -0
- python_platform/sqlalchemy/migration.py +346 -0
- python_platform/sqlalchemy/module.py +48 -0
- python_platform/sqlalchemy/module_migration.py +148 -0
- python_platform/sqlalchemy/options.py +66 -0
- python_platform/sqlalchemy/repository.py +33 -0
- python_platform/sqlalchemy/runtime.py +85 -0
- python_platform/sqlalchemy/session_provider.py +45 -0
- python_platform/sqlalchemy/unit_of_work.py +97 -0
- python_platform/testing/__init__.py +5 -0
- python_platform/testing/runtime.py +112 -0
- python_platform/unit_of_work/__init__.py +14 -0
- python_platform/unit_of_work/contracts.py +223 -0
- python_platform/unit_of_work/errors.py +27 -0
- python_platform/unit_of_work/manager.py +210 -0
- python_platform/unit_of_work/options.py +92 -0
- python_platform-0.1.1.dist-info/METADATA +122 -0
- python_platform-0.1.1.dist-info/RECORD +344 -0
- python_platform-0.1.1.dist-info/WHEEL +4 -0
- python_platform-0.1.1.dist-info/entry_points.txt +9 -0
- python_platform-0.1.1.dist-info/licenses/LICENSE +7 -0
- python_platform-0.1.1.dist-info/licenses/src/python_platform/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Configuration source loading 与 merge 的 Application composition owner。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import replace
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import cast
|
|
9
|
+
|
|
10
|
+
from .contracts import _ConfigurationDefaultContribution
|
|
11
|
+
from .merge import _ConfigurationMerger
|
|
12
|
+
from .root import ConfigurationRoot
|
|
13
|
+
from .sources import _ConfigurationSourceLoader
|
|
14
|
+
from .values import freeze_value, normalize_mapping
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _ConfigurationComposer:
|
|
18
|
+
"""组合已验证参数,不暴露 source 或 mutable merge state 给 Application。"""
|
|
19
|
+
|
|
20
|
+
__slots__ = ("_loader",)
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
*,
|
|
25
|
+
environment: str,
|
|
26
|
+
base_path: Path,
|
|
27
|
+
env_prefix: str,
|
|
28
|
+
dotenv_path: Path | None,
|
|
29
|
+
explicit_overrides: Mapping[str, object],
|
|
30
|
+
cli_args: tuple[str, ...],
|
|
31
|
+
) -> None:
|
|
32
|
+
self._loader = _ConfigurationSourceLoader(
|
|
33
|
+
environment=environment,
|
|
34
|
+
base_path=base_path,
|
|
35
|
+
env_prefix=env_prefix,
|
|
36
|
+
dotenv_path=dotenv_path,
|
|
37
|
+
explicit_overrides=explicit_overrides,
|
|
38
|
+
cli_args=cli_args,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def build(
|
|
42
|
+
self,
|
|
43
|
+
defaults: tuple[_ConfigurationDefaultContribution, ...],
|
|
44
|
+
) -> ConfigurationRoot:
|
|
45
|
+
payloads, diagnostics = self._loader.load(defaults)
|
|
46
|
+
frozen_payloads = tuple(
|
|
47
|
+
replace(payload, values=cast(Mapping[str, object], freeze_value(payload.values)))
|
|
48
|
+
for payload in payloads
|
|
49
|
+
)
|
|
50
|
+
initial = _ConfigurationMerger().merge(
|
|
51
|
+
payloads=tuple(
|
|
52
|
+
replace(payload, values=normalize_mapping(payload.values, pending=True))
|
|
53
|
+
for payload in frozen_payloads
|
|
54
|
+
),
|
|
55
|
+
diagnostics=diagnostics,
|
|
56
|
+
secret_paths=frozenset(key for payload in payloads for key in payload.values),
|
|
57
|
+
)
|
|
58
|
+
# 临时配置视图用于 composition;类型明确后只重解释此快照,绝不重读来源。
|
|
59
|
+
return ConfigurationRoot(
|
|
60
|
+
tree=initial,
|
|
61
|
+
provenance=initial._provenance,
|
|
62
|
+
secret_paths=frozenset(initial),
|
|
63
|
+
sources=initial.sources,
|
|
64
|
+
payloads=frozen_payloads,
|
|
65
|
+
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Configuration source、provenance 与 diagnostic value contracts。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
from ..diagnostics import SourceLocation
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ConfigurationSource(str, Enum):
|
|
13
|
+
MODULE_DEFAULTS = "module-defaults"
|
|
14
|
+
YAML_BASE = "yaml-base"
|
|
15
|
+
YAML_ENVIRONMENT = "yaml-environment"
|
|
16
|
+
DOTENV = "dotenv"
|
|
17
|
+
OS_ENVIRONMENT = "os-environment"
|
|
18
|
+
CLI = "cli"
|
|
19
|
+
EXPLICIT = "explicit"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConfigurationSourceStatus(str, Enum):
|
|
23
|
+
LOADED = "loaded"
|
|
24
|
+
SKIPPED = "skipped"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class ConfigurationSourceDiagnostic:
|
|
29
|
+
source: ConfigurationSource
|
|
30
|
+
status: ConfigurationSourceStatus
|
|
31
|
+
location: SourceLocation
|
|
32
|
+
reason: str | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True, slots=True)
|
|
36
|
+
class ConfigurationContribution:
|
|
37
|
+
source_id: ConfigurationSource
|
|
38
|
+
source_location: SourceLocation
|
|
39
|
+
raw_kind: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True, slots=True)
|
|
43
|
+
class ConfigurationProvenance:
|
|
44
|
+
path: str
|
|
45
|
+
chain: tuple[ConfigurationContribution, ...]
|
|
46
|
+
effective_source: ConfigurationContribution
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True, slots=True)
|
|
50
|
+
class ConfigurationValueDiagnostic:
|
|
51
|
+
path: str
|
|
52
|
+
value: object
|
|
53
|
+
redacted: bool
|
|
54
|
+
provenance: ConfigurationProvenance
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True, slots=True)
|
|
58
|
+
class _ConfigurationDefaultContribution:
|
|
59
|
+
source_location: SourceLocation
|
|
60
|
+
values: Mapping[str, object]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True, slots=True)
|
|
64
|
+
class _SourcePayload:
|
|
65
|
+
source: ConfigurationSource
|
|
66
|
+
location: SourceLocation
|
|
67
|
+
values: Mapping[str, object]
|
|
68
|
+
input_depth: int = 0
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""dotenv 文件单次读取;语法预检与原生 value 解码消费同一份文本。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from io import StringIO
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from dotenv import dotenv_values
|
|
10
|
+
from dotenv.parser import parse_stream
|
|
11
|
+
from pydantic_settings import DotEnvSettingsSource
|
|
12
|
+
from pydantic_settings.sources.utils import parse_env_vars
|
|
13
|
+
|
|
14
|
+
from ..diagnostics import SourceLocation
|
|
15
|
+
from ..errors import ConfigurationSourceError
|
|
16
|
+
from .contracts import ConfigurationSource
|
|
17
|
+
from .environment_source import _LayeredEnvSource
|
|
18
|
+
from .input_shape import _environment_input_name
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _SnapshotDotEnvSource(DotEnvSettingsSource, _LayeredEnvSource):
|
|
22
|
+
def _read_env_file(self, file_path: Path) -> Mapping[str, str | None]:
|
|
23
|
+
text = file_path.read_text(encoding=self.env_file_encoding or "utf-8")
|
|
24
|
+
seen: set[str] = set()
|
|
25
|
+
for binding in parse_stream(StringIO(text)):
|
|
26
|
+
if binding.error:
|
|
27
|
+
raise ConfigurationSourceError(
|
|
28
|
+
source_id=ConfigurationSource.DOTENV.value,
|
|
29
|
+
location=SourceLocation(
|
|
30
|
+
owner=ConfigurationSource.DOTENV.value,
|
|
31
|
+
file=str(file_path),
|
|
32
|
+
line=binding.original.line,
|
|
33
|
+
symbol=None,
|
|
34
|
+
),
|
|
35
|
+
original_error=ValueError("invalid dotenv syntax"),
|
|
36
|
+
)
|
|
37
|
+
key = binding.key
|
|
38
|
+
if key is not None and key.casefold().startswith(self.env_prefix.casefold()):
|
|
39
|
+
parts = _environment_input_name(key, self.env_prefix)
|
|
40
|
+
if parts in seen:
|
|
41
|
+
raise ValueError("duplicate dotenv path")
|
|
42
|
+
seen.add(parts)
|
|
43
|
+
return parse_env_vars(
|
|
44
|
+
{
|
|
45
|
+
_environment_input_name(key, self.env_prefix): value
|
|
46
|
+
for key, value in dotenv_values(stream=StringIO(text)).items()
|
|
47
|
+
},
|
|
48
|
+
True,
|
|
49
|
+
self.env_ignore_empty,
|
|
50
|
+
self.env_parse_none_str,
|
|
51
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""原生环境 source 的读取期分层;保留 JSON 与嵌套变量的覆盖身份。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic.fields import FieldInfo
|
|
8
|
+
from pydantic_settings import EnvSettingsSource
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _LayeredEnvSource(EnvSettingsSource):
|
|
12
|
+
def __call__(self) -> dict[str, Any]:
|
|
13
|
+
self.input_layers: dict[int, dict[str, object]] = {}
|
|
14
|
+
return super().__call__()
|
|
15
|
+
|
|
16
|
+
def prepare_field_value(
|
|
17
|
+
self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool
|
|
18
|
+
) -> Any:
|
|
19
|
+
snapshot = self.env_vars
|
|
20
|
+
delimiter = self.env_nested_delimiter
|
|
21
|
+
assert delimiter is not None
|
|
22
|
+
depths = sorted({name.count(delimiter) for name in snapshot})
|
|
23
|
+
prepared: object = None
|
|
24
|
+
try:
|
|
25
|
+
for depth in depths:
|
|
26
|
+
self.env_vars = {
|
|
27
|
+
name: raw for name, raw in snapshot.items() if name.count(delimiter) == depth
|
|
28
|
+
}
|
|
29
|
+
layer_value, _, complex_value = self.get_field_value(field, field_name)
|
|
30
|
+
decoded = super().prepare_field_value(field_name, field, layer_value, complex_value)
|
|
31
|
+
if decoded is not None:
|
|
32
|
+
self.input_layers.setdefault(depth, {})[field_name] = decoded
|
|
33
|
+
prepared = decoded
|
|
34
|
+
finally:
|
|
35
|
+
self.env_vars = snapshot
|
|
36
|
+
# __call__ 仍只执行一次;最终合并消费 input_layers,不消费这个原生返回视图。
|
|
37
|
+
# 这里不校验 Options,也不重读 OS 或 dotenv。
|
|
38
|
+
return prepared
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Configuration value 所有的 typed snapshot 规则,供 Options 与 Settings 复用。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Mapping, MutableMapping, MutableSequence, MutableSet
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from functools import cached_property
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
from typing import Annotated, Any, Literal, cast, get_args, get_origin
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Secret, SecretBytes, SecretStr
|
|
12
|
+
|
|
13
|
+
from .values import freeze_value, is_frozen_secret, is_supported_scalar
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def validate_model_shape(model_type: type[BaseModel]) -> None:
|
|
17
|
+
# frozen 只保护字段赋值,不能保护 extra 和 PrivateAttr。
|
|
18
|
+
if model_type.model_config.get("frozen") is not True:
|
|
19
|
+
raise TypeError("snapshot models must be frozen")
|
|
20
|
+
if model_type.model_config.get("extra") == "allow":
|
|
21
|
+
raise TypeError("snapshot models must not allow mutable extra fields")
|
|
22
|
+
if model_type.__private_attributes__:
|
|
23
|
+
raise TypeError("snapshot models must not declare private attributes")
|
|
24
|
+
if any(
|
|
25
|
+
isinstance(member, cached_property)
|
|
26
|
+
for base in model_type.__mro__
|
|
27
|
+
for member in vars(base).values()
|
|
28
|
+
):
|
|
29
|
+
# cached_property 可绕过 frozen 在读取时向 __dict__ 写入缓存,不能发布为共享默认值。
|
|
30
|
+
raise TypeError("snapshot models must not declare cached properties")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def validate_immutable_annotation(
|
|
34
|
+
annotation: object, seen: frozenset[object] = frozenset()
|
|
35
|
+
) -> None:
|
|
36
|
+
if annotation in seen:
|
|
37
|
+
return
|
|
38
|
+
origin = get_origin(annotation)
|
|
39
|
+
if annotation in (Any, object):
|
|
40
|
+
raise TypeError("snapshot annotations must not use Any/object")
|
|
41
|
+
mutable = (list, dict, set, bytearray, MutableMapping, MutableSequence, MutableSet)
|
|
42
|
+
if annotation in mutable or origin in mutable:
|
|
43
|
+
raise TypeError("snapshot annotations must not use mutable containers")
|
|
44
|
+
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
|
45
|
+
validate_model_shape(annotation)
|
|
46
|
+
for field in annotation.model_fields.values():
|
|
47
|
+
validate_immutable_annotation(field.annotation, seen | {annotation})
|
|
48
|
+
elif isinstance(annotation, type) and issubclass(annotation, Enum):
|
|
49
|
+
if any(not is_supported_scalar(member) for member in annotation):
|
|
50
|
+
raise TypeError("snapshot enum members must be deeply immutable")
|
|
51
|
+
elif origin is Annotated:
|
|
52
|
+
validate_immutable_annotation(get_args(annotation)[0], seen)
|
|
53
|
+
elif origin is not Literal:
|
|
54
|
+
_validate_annotation_arguments(get_args(annotation), seen)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _validate_annotation_arguments(arguments: tuple[object, ...], seen: frozenset[object]) -> None:
|
|
58
|
+
for argument in arguments:
|
|
59
|
+
if argument is not Ellipsis:
|
|
60
|
+
validate_immutable_annotation(argument, seen)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def freeze_typed_value(value: object, *, allow_secrets: bool = True) -> object:
|
|
64
|
+
if isinstance(value, (Secret, SecretStr, SecretBytes)):
|
|
65
|
+
if not allow_secrets:
|
|
66
|
+
raise TypeError("runtime settings cannot contain secrets")
|
|
67
|
+
return freeze_value(value)
|
|
68
|
+
if isinstance(value, BaseModel):
|
|
69
|
+
return _freeze_model(value, allow_secrets=allow_secrets)
|
|
70
|
+
if isinstance(value, Mapping):
|
|
71
|
+
return MappingProxyType(
|
|
72
|
+
{
|
|
73
|
+
freeze_typed_value(key, allow_secrets=allow_secrets): freeze_typed_value(
|
|
74
|
+
item, allow_secrets=allow_secrets
|
|
75
|
+
)
|
|
76
|
+
for key, item in value.items()
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
if isinstance(value, tuple):
|
|
80
|
+
items = tuple(freeze_typed_value(item, allow_secrets=allow_secrets) for item in value)
|
|
81
|
+
if type(value) is tuple:
|
|
82
|
+
return items
|
|
83
|
+
fields = getattr(type(value), "_fields", None)
|
|
84
|
+
if isinstance(fields, tuple) and not hasattr(value, "__dict__"):
|
|
85
|
+
return cast(Callable[..., tuple[object, ...]], type(value))(*items)
|
|
86
|
+
raise TypeError("unsupported tuple subtype")
|
|
87
|
+
if isinstance(value, frozenset):
|
|
88
|
+
return frozenset(freeze_typed_value(item, allow_secrets=allow_secrets) for item in value)
|
|
89
|
+
if is_supported_scalar(value) or (allow_secrets and is_frozen_secret(value)):
|
|
90
|
+
return value
|
|
91
|
+
raise TypeError(f"validated object graph contains unsupported {type(value).__name__}")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _freeze_model(value: BaseModel, *, allow_secrets: bool) -> BaseModel:
|
|
95
|
+
validate_model_shape(type(value))
|
|
96
|
+
# model_copy 会保留 __dict__ 中的额外状态,因此复制前必须验证实例边界。
|
|
97
|
+
if set(vars(value)) - type(value).model_fields.keys():
|
|
98
|
+
raise TypeError("snapshot models must not contain undeclared instance state")
|
|
99
|
+
updates = {
|
|
100
|
+
name: freeze_typed_value(getattr(value, name), allow_secrets=allow_secrets)
|
|
101
|
+
for name in type(value).model_fields
|
|
102
|
+
}
|
|
103
|
+
return value.model_copy(update=updates)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _serialization_value(value: object) -> object:
|
|
107
|
+
"""仅在原生 serializer 边界复制冻结容器,不发布这个可变副本。"""
|
|
108
|
+
if isinstance(value, BaseModel):
|
|
109
|
+
return value.model_copy(
|
|
110
|
+
update={
|
|
111
|
+
name: _serialization_value(getattr(value, name))
|
|
112
|
+
for name in type(value).model_fields
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
if isinstance(value, Mapping):
|
|
116
|
+
return {key: _serialization_value(item) for key, item in value.items()}
|
|
117
|
+
if isinstance(value, tuple):
|
|
118
|
+
items = tuple(_serialization_value(item) for item in value)
|
|
119
|
+
if type(value) is tuple:
|
|
120
|
+
return items
|
|
121
|
+
return cast(Callable[..., tuple[object, ...]], type(value))(*items)
|
|
122
|
+
if isinstance(value, frozenset):
|
|
123
|
+
return frozenset(_serialization_value(item) for item in value)
|
|
124
|
+
return value
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""从实际输入路径构造原生 source 的读取形状,不持有 Options schema。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from typing import TypeAlias
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from pydantic_settings import BaseSettings
|
|
10
|
+
|
|
11
|
+
from .values import canonical_segment
|
|
12
|
+
|
|
13
|
+
_PathTree: TypeAlias = dict[str, "_PathTree"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _environment_input_name(name: str, prefix: str) -> str:
|
|
17
|
+
if not name.casefold().startswith(prefix.casefold()):
|
|
18
|
+
return name
|
|
19
|
+
first, separator, tail = name[len(prefix) :].partition("__")
|
|
20
|
+
# section 是已知字段;后续路径的类型尚未声明,不能提前折叠 Mapping 的数据键。
|
|
21
|
+
return prefix.casefold() + canonical_segment(first) + separator + tail
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _input_sections(names: Iterable[str], prefix: str) -> frozenset[str]:
|
|
25
|
+
return frozenset(
|
|
26
|
+
canonical_segment(name[len(prefix) :].split("__")[0])
|
|
27
|
+
for name in names
|
|
28
|
+
if name.casefold().startswith(prefix.casefold())
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _cli_settings_type(args: tuple[str, ...]) -> type[BaseSettings]:
|
|
33
|
+
paths: _PathTree = {}
|
|
34
|
+
seen: set[tuple[str, ...]] = set()
|
|
35
|
+
for argument in args:
|
|
36
|
+
if argument in ("-h", "--help"):
|
|
37
|
+
# Builder 只接收应用配置参数;Host 处理 help,原生 argparse 不得退出宿主进程。
|
|
38
|
+
raise ValueError("help flags belong to the Host")
|
|
39
|
+
if not argument.startswith("--"):
|
|
40
|
+
continue
|
|
41
|
+
parts = tuple(canonical_segment(part) for part in argument[2:].split("=", 1)[0].split("."))
|
|
42
|
+
# 原生 CLI source 会合并对象或覆盖叶子;平台在解码前拒绝同一路径的重复声明。
|
|
43
|
+
if parts in seen:
|
|
44
|
+
raise ValueError("duplicate canonical CLI path")
|
|
45
|
+
seen.add(parts)
|
|
46
|
+
node = paths
|
|
47
|
+
for part in parts:
|
|
48
|
+
node = node.setdefault(part, {})
|
|
49
|
+
annotations = {key: _shape_annotation(value) for key, value in paths.items()}
|
|
50
|
+
return type("_ApplicationCliSettings", (BaseSettings,), {"__annotations__": annotations})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _shape_annotation(value: object) -> object:
|
|
54
|
+
if isinstance(value, dict) and value:
|
|
55
|
+
annotations = {key: _shape_annotation(child) for key, child in value.items()}
|
|
56
|
+
return type("_CliSection", (BaseModel,), {"__annotations__": annotations})
|
|
57
|
+
# Union 保留普通字符串,同时让原生 source 解码 JSON mapping/sequence。
|
|
58
|
+
return dict[str, object] | list[object] | str
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Configuration precedence、provenance chain 与 immutable root 发布。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
|
|
7
|
+
from .contracts import (
|
|
8
|
+
ConfigurationContribution,
|
|
9
|
+
ConfigurationProvenance,
|
|
10
|
+
ConfigurationSource,
|
|
11
|
+
ConfigurationSourceDiagnostic,
|
|
12
|
+
_SourcePayload,
|
|
13
|
+
)
|
|
14
|
+
from .root import ConfigurationRoot
|
|
15
|
+
from .values import MISSING, canonical_path, raw_kind
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _ConfigurationMerger:
|
|
19
|
+
"""拥有一次 merge 的 mutable working tree;完成后只发布 immutable root。"""
|
|
20
|
+
|
|
21
|
+
__slots__ = ("_chains", "_merged")
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self._merged: dict[str, object] = {}
|
|
25
|
+
self._chains: dict[tuple[str, ...], list[ConfigurationContribution]] = {}
|
|
26
|
+
|
|
27
|
+
def merge(
|
|
28
|
+
self,
|
|
29
|
+
*,
|
|
30
|
+
payloads: tuple[_SourcePayload, ...],
|
|
31
|
+
diagnostics: tuple[ConfigurationSourceDiagnostic, ...],
|
|
32
|
+
secret_paths: frozenset[str],
|
|
33
|
+
) -> ConfigurationRoot:
|
|
34
|
+
# Enum declaration 是唯一 priority metadata;source 调用顺序不参与覆盖裁决。
|
|
35
|
+
for payload in sorted(
|
|
36
|
+
payloads, key=lambda item: (_source_ordinal(item.source), item.input_depth)
|
|
37
|
+
):
|
|
38
|
+
self._merge_mapping(self._merged, payload.values, prefix=(), payload=payload)
|
|
39
|
+
provenance = {
|
|
40
|
+
path: ConfigurationProvenance(
|
|
41
|
+
path=".".join(path),
|
|
42
|
+
chain=tuple(chain),
|
|
43
|
+
effective_source=chain[-1],
|
|
44
|
+
)
|
|
45
|
+
for path, chain in self._chains.items()
|
|
46
|
+
}
|
|
47
|
+
canonical_secrets = frozenset(".".join(canonical_path(path)) for path in secret_paths)
|
|
48
|
+
return ConfigurationRoot(
|
|
49
|
+
tree=self._merged,
|
|
50
|
+
provenance=provenance,
|
|
51
|
+
secret_paths=canonical_secrets,
|
|
52
|
+
sources=tuple(sorted(diagnostics, key=lambda item: _source_ordinal(item.source))),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def _merge_mapping(
|
|
56
|
+
self,
|
|
57
|
+
target: dict[str, object],
|
|
58
|
+
incoming: Mapping[str, object],
|
|
59
|
+
*,
|
|
60
|
+
prefix: tuple[str, ...],
|
|
61
|
+
payload: _SourcePayload,
|
|
62
|
+
) -> None:
|
|
63
|
+
for key, incoming_value in incoming.items():
|
|
64
|
+
path_parts = (*prefix, key)
|
|
65
|
+
path = path_parts
|
|
66
|
+
contribution = ConfigurationContribution(
|
|
67
|
+
source_id=payload.source,
|
|
68
|
+
source_location=payload.location,
|
|
69
|
+
raw_kind=raw_kind(incoming_value),
|
|
70
|
+
)
|
|
71
|
+
current = target.get(key, MISSING)
|
|
72
|
+
if isinstance(current, dict) and isinstance(incoming_value, Mapping):
|
|
73
|
+
self._chains.setdefault(path, []).append(contribution)
|
|
74
|
+
self._merge_mapping(
|
|
75
|
+
current,
|
|
76
|
+
incoming_value,
|
|
77
|
+
prefix=path_parts,
|
|
78
|
+
payload=payload,
|
|
79
|
+
)
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
self._remove_descendant_chains(path)
|
|
83
|
+
self._chains.setdefault(path, []).append(contribution)
|
|
84
|
+
if isinstance(incoming_value, Mapping):
|
|
85
|
+
nested: dict[str, object] = {}
|
|
86
|
+
target[key] = nested
|
|
87
|
+
self._merge_mapping(
|
|
88
|
+
nested,
|
|
89
|
+
incoming_value,
|
|
90
|
+
prefix=path_parts,
|
|
91
|
+
payload=payload,
|
|
92
|
+
)
|
|
93
|
+
else:
|
|
94
|
+
target[key] = incoming_value
|
|
95
|
+
|
|
96
|
+
def _remove_descendant_chains(self, path: tuple[str, ...]) -> None:
|
|
97
|
+
for descendant in tuple(self._chains):
|
|
98
|
+
if len(descendant) > len(path) and descendant[: len(path)] == path:
|
|
99
|
+
del self._chains[descendant]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _source_ordinal(source: ConfigurationSource) -> int:
|
|
103
|
+
return tuple(ConfigurationSource).index(source)
|