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,331 @@
|
|
|
1
|
+
"""多来源 Options input alias 的确定性优先级裁决。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import types
|
|
6
|
+
from collections.abc import Mapping, MutableMapping
|
|
7
|
+
from typing import Annotated, Literal, Union, cast, get_args, get_origin
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
from ..configuration.merge import _source_ordinal
|
|
12
|
+
from ..configuration.root import ConfigurationRoot
|
|
13
|
+
from ..configuration.values import canonical_path
|
|
14
|
+
from .models import MISSING, _OptionPatch
|
|
15
|
+
from .schema import (
|
|
16
|
+
_annotation_discriminator,
|
|
17
|
+
_direct_model_types,
|
|
18
|
+
_field_validation_paths,
|
|
19
|
+
_model_annotation,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _prefer_higher_priority_alias_inputs(
|
|
24
|
+
model_type: type[BaseModel],
|
|
25
|
+
input_value: object,
|
|
26
|
+
*,
|
|
27
|
+
configuration: ConfigurationRoot,
|
|
28
|
+
section: str,
|
|
29
|
+
patches: tuple[_OptionPatch, ...],
|
|
30
|
+
) -> None:
|
|
31
|
+
if not isinstance(input_value, MutableMapping):
|
|
32
|
+
return
|
|
33
|
+
_prefer_model_alias_inputs(
|
|
34
|
+
model_type,
|
|
35
|
+
cast(MutableMapping[str, object], input_value),
|
|
36
|
+
root_model_type=model_type,
|
|
37
|
+
prefix=(),
|
|
38
|
+
configuration=configuration,
|
|
39
|
+
section=section,
|
|
40
|
+
patches=patches,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _prefer_model_alias_inputs(
|
|
45
|
+
model_type: type[BaseModel],
|
|
46
|
+
values: MutableMapping[str, object],
|
|
47
|
+
*,
|
|
48
|
+
root_model_type: type[BaseModel],
|
|
49
|
+
prefix: tuple[str, ...],
|
|
50
|
+
configuration: ConfigurationRoot,
|
|
51
|
+
section: str,
|
|
52
|
+
patches: tuple[_OptionPatch, ...],
|
|
53
|
+
) -> None:
|
|
54
|
+
for field_name, field in model_type.model_fields.items():
|
|
55
|
+
paths = _field_validation_paths(model_type, field_name, field)
|
|
56
|
+
present: list[tuple[int, tuple[str, ...], object]] = []
|
|
57
|
+
for index, path in enumerate(paths):
|
|
58
|
+
value = _lookup_input_path(values, path)
|
|
59
|
+
if value is not MISSING:
|
|
60
|
+
present.append((index, path, value))
|
|
61
|
+
if not present:
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
winner = max(
|
|
65
|
+
present,
|
|
66
|
+
key=lambda item: (
|
|
67
|
+
*_input_path_rank(
|
|
68
|
+
(*prefix, *item[1]),
|
|
69
|
+
root_model_type=root_model_type,
|
|
70
|
+
configuration=configuration,
|
|
71
|
+
section=section,
|
|
72
|
+
patches=patches,
|
|
73
|
+
),
|
|
74
|
+
-item[0],
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
for candidate in present:
|
|
78
|
+
if candidate is not winner:
|
|
79
|
+
_delete_input_path(values, candidate[1])
|
|
80
|
+
|
|
81
|
+
_prefer_annotation_alias_inputs(
|
|
82
|
+
field.annotation,
|
|
83
|
+
winner[2],
|
|
84
|
+
discriminator=field.discriminator,
|
|
85
|
+
root_model_type=root_model_type,
|
|
86
|
+
prefix=(*prefix, *winner[1]),
|
|
87
|
+
configuration=configuration,
|
|
88
|
+
section=section,
|
|
89
|
+
patches=patches,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _prefer_annotation_alias_inputs(
|
|
94
|
+
annotation: object,
|
|
95
|
+
value: object,
|
|
96
|
+
*,
|
|
97
|
+
discriminator: object,
|
|
98
|
+
root_model_type: type[BaseModel],
|
|
99
|
+
prefix: tuple[str, ...],
|
|
100
|
+
configuration: ConfigurationRoot,
|
|
101
|
+
section: str,
|
|
102
|
+
patches: tuple[_OptionPatch, ...],
|
|
103
|
+
) -> None:
|
|
104
|
+
origin = get_origin(annotation)
|
|
105
|
+
if origin is Annotated:
|
|
106
|
+
arguments = get_args(annotation)
|
|
107
|
+
if arguments:
|
|
108
|
+
_prefer_annotation_alias_inputs(
|
|
109
|
+
arguments[0],
|
|
110
|
+
value,
|
|
111
|
+
discriminator=discriminator or _annotation_discriminator(annotation),
|
|
112
|
+
root_model_type=root_model_type,
|
|
113
|
+
prefix=prefix,
|
|
114
|
+
configuration=configuration,
|
|
115
|
+
section=section,
|
|
116
|
+
patches=patches,
|
|
117
|
+
)
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
model_type = _model_annotation(annotation)
|
|
121
|
+
if model_type is not None:
|
|
122
|
+
if isinstance(value, MutableMapping):
|
|
123
|
+
_prefer_model_alias_inputs(
|
|
124
|
+
model_type,
|
|
125
|
+
cast(MutableMapping[str, object], value),
|
|
126
|
+
root_model_type=root_model_type,
|
|
127
|
+
prefix=prefix,
|
|
128
|
+
configuration=configuration,
|
|
129
|
+
section=section,
|
|
130
|
+
patches=patches,
|
|
131
|
+
)
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
if origin in (Union, types.UnionType):
|
|
135
|
+
_prefer_union_alias_inputs(
|
|
136
|
+
annotation,
|
|
137
|
+
value,
|
|
138
|
+
discriminator=discriminator,
|
|
139
|
+
root_model_type=root_model_type,
|
|
140
|
+
prefix=prefix,
|
|
141
|
+
configuration=configuration,
|
|
142
|
+
section=section,
|
|
143
|
+
patches=patches,
|
|
144
|
+
)
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
_prefer_container_alias_inputs(
|
|
148
|
+
origin,
|
|
149
|
+
get_args(annotation),
|
|
150
|
+
value,
|
|
151
|
+
root_model_type=root_model_type,
|
|
152
|
+
prefix=prefix,
|
|
153
|
+
configuration=configuration,
|
|
154
|
+
section=section,
|
|
155
|
+
patches=patches,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _prefer_union_alias_inputs(
|
|
160
|
+
annotation: object,
|
|
161
|
+
value: object,
|
|
162
|
+
*,
|
|
163
|
+
discriminator: object,
|
|
164
|
+
root_model_type: type[BaseModel],
|
|
165
|
+
prefix: tuple[str, ...],
|
|
166
|
+
configuration: ConfigurationRoot,
|
|
167
|
+
section: str,
|
|
168
|
+
patches: tuple[_OptionPatch, ...],
|
|
169
|
+
) -> None:
|
|
170
|
+
model_choices = _direct_model_types(annotation)
|
|
171
|
+
selected: type[BaseModel] | None = None
|
|
172
|
+
if len(model_choices) == 1:
|
|
173
|
+
selected = model_choices[0]
|
|
174
|
+
elif isinstance(discriminator, str) and isinstance(value, Mapping):
|
|
175
|
+
selected = _select_discriminated_model(model_choices, value, discriminator)
|
|
176
|
+
if selected is not None and isinstance(value, MutableMapping):
|
|
177
|
+
_prefer_model_alias_inputs(
|
|
178
|
+
selected,
|
|
179
|
+
cast(MutableMapping[str, object], value),
|
|
180
|
+
root_model_type=root_model_type,
|
|
181
|
+
prefix=prefix,
|
|
182
|
+
configuration=configuration,
|
|
183
|
+
section=section,
|
|
184
|
+
patches=patches,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _prefer_container_alias_inputs(
|
|
189
|
+
origin: object,
|
|
190
|
+
arguments: tuple[object, ...],
|
|
191
|
+
value: object,
|
|
192
|
+
*,
|
|
193
|
+
root_model_type: type[BaseModel],
|
|
194
|
+
prefix: tuple[str, ...],
|
|
195
|
+
configuration: ConfigurationRoot,
|
|
196
|
+
section: str,
|
|
197
|
+
patches: tuple[_OptionPatch, ...],
|
|
198
|
+
) -> None:
|
|
199
|
+
if origin is Mapping and len(arguments) == 2 and isinstance(value, Mapping):
|
|
200
|
+
value_annotation = arguments[1]
|
|
201
|
+
for key, item in value.items():
|
|
202
|
+
if isinstance(key, str):
|
|
203
|
+
_prefer_annotation_alias_inputs(
|
|
204
|
+
value_annotation,
|
|
205
|
+
item,
|
|
206
|
+
discriminator=None,
|
|
207
|
+
root_model_type=root_model_type,
|
|
208
|
+
prefix=(*prefix, key),
|
|
209
|
+
configuration=configuration,
|
|
210
|
+
section=section,
|
|
211
|
+
patches=patches,
|
|
212
|
+
)
|
|
213
|
+
return
|
|
214
|
+
if origin in (tuple, frozenset) and isinstance(value, (list, tuple, frozenset)):
|
|
215
|
+
for index, item in enumerate(value):
|
|
216
|
+
item_annotation = _sequence_item_annotation(arguments, index)
|
|
217
|
+
if item_annotation is not None:
|
|
218
|
+
_prefer_annotation_alias_inputs(
|
|
219
|
+
item_annotation,
|
|
220
|
+
item,
|
|
221
|
+
discriminator=None,
|
|
222
|
+
root_model_type=root_model_type,
|
|
223
|
+
prefix=(*prefix, str(index)),
|
|
224
|
+
configuration=configuration,
|
|
225
|
+
section=section,
|
|
226
|
+
patches=patches,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _select_discriminated_model(
|
|
231
|
+
model_choices: tuple[type[BaseModel], ...],
|
|
232
|
+
values: Mapping[str, object],
|
|
233
|
+
discriminator: str,
|
|
234
|
+
) -> type[BaseModel] | None:
|
|
235
|
+
for model_type in model_choices:
|
|
236
|
+
field = model_type.model_fields.get(discriminator)
|
|
237
|
+
if field is None:
|
|
238
|
+
continue
|
|
239
|
+
tag = MISSING
|
|
240
|
+
for path in _field_validation_paths(model_type, discriminator, field):
|
|
241
|
+
tag = _lookup_input_path(values, path)
|
|
242
|
+
if tag is not MISSING:
|
|
243
|
+
break
|
|
244
|
+
if tag is not MISSING and tag in _literal_values(field.annotation):
|
|
245
|
+
return model_type
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _literal_values(annotation: object) -> tuple[object, ...]:
|
|
250
|
+
origin = get_origin(annotation)
|
|
251
|
+
if origin is Annotated:
|
|
252
|
+
arguments = get_args(annotation)
|
|
253
|
+
return _literal_values(arguments[0]) if arguments else ()
|
|
254
|
+
if origin is Literal:
|
|
255
|
+
return get_args(annotation)
|
|
256
|
+
return ()
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _sequence_item_annotation(
|
|
260
|
+
arguments: tuple[object, ...],
|
|
261
|
+
index: int,
|
|
262
|
+
) -> object | None:
|
|
263
|
+
if not arguments:
|
|
264
|
+
return None
|
|
265
|
+
if len(arguments) == 2 and arguments[1] is Ellipsis:
|
|
266
|
+
return arguments[0]
|
|
267
|
+
if index < len(arguments):
|
|
268
|
+
return arguments[index]
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _input_path_rank(
|
|
273
|
+
relative_path: tuple[str, ...],
|
|
274
|
+
*,
|
|
275
|
+
root_model_type: type[BaseModel],
|
|
276
|
+
configuration: ConfigurationRoot,
|
|
277
|
+
section: str,
|
|
278
|
+
patches: tuple[_OptionPatch, ...],
|
|
279
|
+
) -> tuple[int, int]:
|
|
280
|
+
patch_rank = -1
|
|
281
|
+
for index, patch in enumerate(patches):
|
|
282
|
+
if patch.model_type is root_model_type and _patch_affects_path(
|
|
283
|
+
relative_path,
|
|
284
|
+
patch.field_paths,
|
|
285
|
+
):
|
|
286
|
+
patch_rank = index
|
|
287
|
+
if patch_rank >= 0:
|
|
288
|
+
return (1, patch_rank)
|
|
289
|
+
|
|
290
|
+
full_parts = (*canonical_path(section), *relative_path)
|
|
291
|
+
for length in range(len(full_parts), 0, -1):
|
|
292
|
+
try:
|
|
293
|
+
provenance = configuration._provenance_at(full_parts[:length])
|
|
294
|
+
except (KeyError, ValueError):
|
|
295
|
+
continue
|
|
296
|
+
return (0, _source_ordinal(provenance.effective_source.source_id))
|
|
297
|
+
return (-1, -1)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _lookup_input_path(
|
|
301
|
+
values: Mapping[str, object],
|
|
302
|
+
path: tuple[str, ...],
|
|
303
|
+
) -> object:
|
|
304
|
+
current: object = values
|
|
305
|
+
for part in path:
|
|
306
|
+
if not isinstance(current, Mapping) or part not in current:
|
|
307
|
+
return MISSING
|
|
308
|
+
current = current[part]
|
|
309
|
+
return current
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _delete_input_path(
|
|
313
|
+
values: MutableMapping[str, object],
|
|
314
|
+
path: tuple[str, ...],
|
|
315
|
+
) -> None:
|
|
316
|
+
current: object = values
|
|
317
|
+
for part in path[:-1]:
|
|
318
|
+
if not isinstance(current, MutableMapping) or part not in current:
|
|
319
|
+
return
|
|
320
|
+
current = current[part]
|
|
321
|
+
if isinstance(current, MutableMapping):
|
|
322
|
+
current.pop(path[-1], None)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _patch_affects_path(path: tuple[str, ...], candidates: frozenset[tuple[str, ...]]) -> bool:
|
|
326
|
+
if not path:
|
|
327
|
+
return bool(candidates)
|
|
328
|
+
return any(
|
|
329
|
+
path[: len(candidate)] == candidate or candidate[: len(path)] == path
|
|
330
|
+
for candidate in candidates
|
|
331
|
+
)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""限定到当前 lifecycle invocation 的声明与程序覆盖入口。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from ..diagnostics import LifecycleStage
|
|
9
|
+
from ..errors import OptionsContributionClosedError
|
|
10
|
+
from ..modularity import ModuleDescriptor
|
|
11
|
+
from .models import TOptions
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .registry import _OptionsRegistry
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _OptionsContributor:
|
|
18
|
+
__slots__ = ("_active", "_kind", "_owner", "_registry", "_stage")
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
registry: _OptionsRegistry,
|
|
23
|
+
*,
|
|
24
|
+
owner: ModuleDescriptor,
|
|
25
|
+
stage: LifecycleStage,
|
|
26
|
+
kind: str,
|
|
27
|
+
) -> None:
|
|
28
|
+
self._registry = registry
|
|
29
|
+
self._owner = owner
|
|
30
|
+
self._stage = stage
|
|
31
|
+
self._kind = kind
|
|
32
|
+
self._active = True
|
|
33
|
+
|
|
34
|
+
def __call__(
|
|
35
|
+
self,
|
|
36
|
+
model_type: type[TOptions],
|
|
37
|
+
*,
|
|
38
|
+
section: str | None = None,
|
|
39
|
+
values: Mapping[str, object] | None = None,
|
|
40
|
+
secret_paths: frozenset[str] = frozenset(),
|
|
41
|
+
reason: str | None = None,
|
|
42
|
+
) -> None:
|
|
43
|
+
if not self._active:
|
|
44
|
+
raise OptionsContributionClosedError(option_kind=self._kind, stage=self._stage.value)
|
|
45
|
+
if section is None and (values is None or secret_paths):
|
|
46
|
+
raise ValueError("Options requires a section declaration or values contribution")
|
|
47
|
+
if section is not None:
|
|
48
|
+
self._registry.declare(
|
|
49
|
+
model_type,
|
|
50
|
+
section,
|
|
51
|
+
secret_paths=secret_paths,
|
|
52
|
+
owner=self._owner,
|
|
53
|
+
stage=self._stage,
|
|
54
|
+
reason=reason,
|
|
55
|
+
)
|
|
56
|
+
if values is not None:
|
|
57
|
+
self._registry.patch(
|
|
58
|
+
model_type, values, owner=self._owner, stage=self._stage, reason=reason
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def _close(self) -> None:
|
|
62
|
+
self._active = False
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Options patch 的独立容器副本;不向贡献者暴露已发布 snapshot 的可变引用。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import cast
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _merge_patch_input(base: object, patch: Mapping[str, object]) -> dict[str, object]:
|
|
10
|
+
merged = {} if not isinstance(base, Mapping) else cast(dict[str, object], _thaw_value(base))
|
|
11
|
+
_merge_mapping_values(merged, patch)
|
|
12
|
+
return merged
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _merge_mapping_values(
|
|
16
|
+
target: dict[str, object],
|
|
17
|
+
incoming: Mapping[str, object],
|
|
18
|
+
) -> None:
|
|
19
|
+
for key, value in incoming.items():
|
|
20
|
+
current = target.get(key)
|
|
21
|
+
if isinstance(current, dict) and isinstance(value, Mapping):
|
|
22
|
+
_merge_mapping_values(current, value)
|
|
23
|
+
else:
|
|
24
|
+
target[key] = _thaw_value(value)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _thaw_value(value: object) -> object:
|
|
28
|
+
if isinstance(value, Mapping):
|
|
29
|
+
return {key: _thaw_value(item) for key, item in value.items()}
|
|
30
|
+
if isinstance(value, tuple):
|
|
31
|
+
return tuple(_thaw_value(item) for item in value)
|
|
32
|
+
if isinstance(value, frozenset):
|
|
33
|
+
return frozenset(_thaw_value(item) for item in value)
|
|
34
|
+
return value
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _mapping_paths(
|
|
38
|
+
values: Mapping[str, object],
|
|
39
|
+
prefix: tuple[str, ...] = (),
|
|
40
|
+
) -> set[tuple[str, ...]]:
|
|
41
|
+
paths: set[tuple[str, ...]] = set()
|
|
42
|
+
for key, value in values.items():
|
|
43
|
+
current = (*prefix, key)
|
|
44
|
+
if isinstance(value, Mapping) and value:
|
|
45
|
+
paths.update(_mapping_paths(value, current))
|
|
46
|
+
else:
|
|
47
|
+
paths.add(current)
|
|
48
|
+
return paths
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""沿有限输入和正式 typing 解释 Mapping 边界;分支路径不跨 Union 共享。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import types
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Annotated, Union, get_args, get_origin
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
from ..configuration.values import MISSING, canonical_segment
|
|
13
|
+
from .aliases import _literal_values, _sequence_item_annotation
|
|
14
|
+
from .schema import _annotation_discriminator, _direct_model_types, _field_validation_paths
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class _InputReference:
|
|
19
|
+
"""只读来源层视图;字段按 canonical 查找,数据键按原名查找。"""
|
|
20
|
+
|
|
21
|
+
layers: tuple[object, ...]
|
|
22
|
+
|
|
23
|
+
def child(self, key: str, *, field: bool) -> object:
|
|
24
|
+
found: list[object] = []
|
|
25
|
+
for layer in self.layers:
|
|
26
|
+
if not isinstance(layer, Mapping):
|
|
27
|
+
found.clear()
|
|
28
|
+
continue
|
|
29
|
+
values = _field_view(layer) if field else layer
|
|
30
|
+
if key in values:
|
|
31
|
+
child = values[key]
|
|
32
|
+
if not isinstance(child, Mapping):
|
|
33
|
+
found.clear()
|
|
34
|
+
found.append(child)
|
|
35
|
+
if not found:
|
|
36
|
+
return MISSING
|
|
37
|
+
return _InputReference(tuple(found)) if isinstance(found[-1], Mapping) else found[-1]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _mapping_input_paths(
|
|
41
|
+
model_type: type[BaseModel], value: object, reference: object, selected: object = None
|
|
42
|
+
) -> frozenset[tuple[str | int | None, ...]]:
|
|
43
|
+
paths: set[tuple[str | int | None, ...]] = set()
|
|
44
|
+
_visit(model_type, value, reference, (), paths, selected=selected)
|
|
45
|
+
return frozenset(paths)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _visit(
|
|
49
|
+
annotation: object,
|
|
50
|
+
value: object,
|
|
51
|
+
reference: object,
|
|
52
|
+
prefix: tuple[str | int | None, ...],
|
|
53
|
+
paths: set[tuple[str | int | None, ...]],
|
|
54
|
+
discriminator: object = None,
|
|
55
|
+
selected: object = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
origin, args = get_origin(annotation), get_args(annotation)
|
|
58
|
+
if origin is Annotated:
|
|
59
|
+
_visit(
|
|
60
|
+
args[0],
|
|
61
|
+
value,
|
|
62
|
+
reference,
|
|
63
|
+
prefix,
|
|
64
|
+
paths,
|
|
65
|
+
discriminator or _annotation_discriminator(annotation),
|
|
66
|
+
selected,
|
|
67
|
+
)
|
|
68
|
+
elif origin in (Union, types.UnionType):
|
|
69
|
+
_visit_union(annotation, value, reference, prefix, paths, discriminator, selected)
|
|
70
|
+
elif origin in (Mapping, dict) and isinstance(value, Mapping):
|
|
71
|
+
paths.add(prefix)
|
|
72
|
+
for key, child in value.items():
|
|
73
|
+
hint = (
|
|
74
|
+
reference.child(key, field=False)
|
|
75
|
+
if isinstance(reference, _InputReference)
|
|
76
|
+
else reference.get(key, child)
|
|
77
|
+
if isinstance(reference, Mapping)
|
|
78
|
+
else child
|
|
79
|
+
)
|
|
80
|
+
choice = selected.get(key) if isinstance(selected, Mapping) else None
|
|
81
|
+
_visit(args[1], child, hint, (*prefix, key), paths, selected=choice)
|
|
82
|
+
elif isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
|
83
|
+
_visit_fields(annotation, value, reference, prefix, paths, selected)
|
|
84
|
+
elif origin in (tuple, frozenset, list, set) and isinstance(value, (tuple, list, frozenset)):
|
|
85
|
+
for index, child in enumerate(value):
|
|
86
|
+
item = _sequence_item_annotation(args, index)
|
|
87
|
+
hint = (
|
|
88
|
+
reference[index]
|
|
89
|
+
if isinstance(reference, (tuple, list)) and index < len(reference)
|
|
90
|
+
else child
|
|
91
|
+
)
|
|
92
|
+
if item is not None:
|
|
93
|
+
choice = (
|
|
94
|
+
selected[index]
|
|
95
|
+
if isinstance(selected, tuple) and index < len(selected)
|
|
96
|
+
else None
|
|
97
|
+
)
|
|
98
|
+
_visit(item, child, hint, (*prefix, index), paths, selected=choice)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _visit_union(
|
|
102
|
+
annotation: object,
|
|
103
|
+
value: object,
|
|
104
|
+
reference: object,
|
|
105
|
+
prefix: tuple[str | int | None, ...],
|
|
106
|
+
paths: set[tuple[str | int | None, ...]],
|
|
107
|
+
discriminator: object,
|
|
108
|
+
selected: object,
|
|
109
|
+
) -> None:
|
|
110
|
+
choices = _direct_model_types(annotation)
|
|
111
|
+
if isinstance(discriminator, str) and isinstance(reference, (Mapping, _InputReference)):
|
|
112
|
+
# 只解释标签所属的分支,不在绑定前执行用户 validator 或替代原生 Union 校验。
|
|
113
|
+
branch_type = _select_input_model(choices, reference, discriminator)
|
|
114
|
+
if branch_type is not None:
|
|
115
|
+
_visit(branch_type, value, reference, prefix, paths, selected=selected)
|
|
116
|
+
return
|
|
117
|
+
if isinstance(selected, BaseModel) and type(selected) in choices:
|
|
118
|
+
_visit(type(selected), value, reference, prefix, paths, selected=selected)
|
|
119
|
+
return
|
|
120
|
+
branch_paths = []
|
|
121
|
+
for item in get_args(annotation):
|
|
122
|
+
if item is type(None) or (isinstance(value, Mapping) and item in (str, int, float, bool)):
|
|
123
|
+
continue
|
|
124
|
+
branch: set[tuple[str | int | None, ...]] = set()
|
|
125
|
+
_visit(item, value, reference, prefix, branch)
|
|
126
|
+
branch_paths.append(branch)
|
|
127
|
+
if branch_paths:
|
|
128
|
+
if all(branch == branch_paths[0] for branch in branch_paths):
|
|
129
|
+
paths.update(branch_paths[0])
|
|
130
|
+
else:
|
|
131
|
+
# 未标记 Union 的选择归原生 validator。暂时保留每一层原始输入键,
|
|
132
|
+
# 各候选模型在自己的原生验证入口归一化,成功后按实际选择发布 Configuration。
|
|
133
|
+
_preserve_mapping_paths(value, prefix, paths)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _preserve_mapping_paths(
|
|
137
|
+
value: object,
|
|
138
|
+
prefix: tuple[str | int | None, ...],
|
|
139
|
+
paths: set[tuple[str | int | None, ...]],
|
|
140
|
+
) -> None:
|
|
141
|
+
if isinstance(value, Mapping):
|
|
142
|
+
paths.add(prefix)
|
|
143
|
+
for key, child in value.items():
|
|
144
|
+
_preserve_mapping_paths(child, (*prefix, key), paths)
|
|
145
|
+
elif isinstance(value, (tuple, list)):
|
|
146
|
+
for index, child in enumerate(value):
|
|
147
|
+
_preserve_mapping_paths(child, (*prefix, index), paths)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _select_input_model(
|
|
151
|
+
choices: tuple[type[BaseModel], ...],
|
|
152
|
+
reference: object,
|
|
153
|
+
discriminator: str,
|
|
154
|
+
) -> type[BaseModel] | None:
|
|
155
|
+
for model in choices:
|
|
156
|
+
field = model.model_fields[discriminator]
|
|
157
|
+
for path in _field_validation_paths(model, discriminator, field):
|
|
158
|
+
tag = _field_at(reference, path)
|
|
159
|
+
if tag is not MISSING:
|
|
160
|
+
if tag in _literal_values(field.annotation):
|
|
161
|
+
return model
|
|
162
|
+
break
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _field_view(value: Mapping[object, object]) -> dict[str, object]:
|
|
167
|
+
fields: dict[str, object] = {}
|
|
168
|
+
for key, child in value.items():
|
|
169
|
+
try:
|
|
170
|
+
name = canonical_segment(key)
|
|
171
|
+
except (TypeError, ValueError):
|
|
172
|
+
# 此时可能仍在枚举 Union 候选;其他分支的数据键不是当前模型字段。
|
|
173
|
+
# 真正选用模型时由 normalize_mapping 拒绝无效字段,不在探测路径时裁决。
|
|
174
|
+
continue
|
|
175
|
+
fields[name] = child
|
|
176
|
+
return fields
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _field_at(value: object, path: tuple[str, ...]) -> object:
|
|
180
|
+
for segment in path:
|
|
181
|
+
if isinstance(value, _InputReference):
|
|
182
|
+
value = value.child(segment, field=True)
|
|
183
|
+
elif isinstance(value, Mapping):
|
|
184
|
+
value = _field_view(value).get(segment, MISSING)
|
|
185
|
+
else:
|
|
186
|
+
return MISSING
|
|
187
|
+
return value
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _visit_fields(
|
|
191
|
+
model: type[BaseModel],
|
|
192
|
+
value: object,
|
|
193
|
+
reference: object,
|
|
194
|
+
prefix: tuple[str | int | None, ...],
|
|
195
|
+
paths: set[tuple[str | int | None, ...]],
|
|
196
|
+
selected: object,
|
|
197
|
+
) -> None:
|
|
198
|
+
if not isinstance(value, Mapping):
|
|
199
|
+
return
|
|
200
|
+
# 递归的终点是输入叶子;再次遇到同一模型类型不代表再次遇到同一输入节点。
|
|
201
|
+
for name, field in model.model_fields.items():
|
|
202
|
+
for path in _field_validation_paths(model, name, field):
|
|
203
|
+
child = _field_at(value, path)
|
|
204
|
+
if child is not MISSING:
|
|
205
|
+
hint = _field_at(reference, path)
|
|
206
|
+
_visit(
|
|
207
|
+
field.annotation,
|
|
208
|
+
child,
|
|
209
|
+
child if hint is MISSING else hint,
|
|
210
|
+
(*prefix, *path),
|
|
211
|
+
paths,
|
|
212
|
+
field.discriminator,
|
|
213
|
+
getattr(selected, name) if isinstance(selected, model) else None,
|
|
214
|
+
)
|