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,115 @@
|
|
|
1
|
+
"""SettingDefinition 的名称、类型校验与不可变默认值。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Generic, TypeVar, cast, get_args, get_origin
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Secret, SecretBytes, SecretStr, TypeAdapter, ValidationError
|
|
8
|
+
|
|
9
|
+
from ..configuration.immutability import (
|
|
10
|
+
_serialization_value,
|
|
11
|
+
freeze_typed_value,
|
|
12
|
+
validate_immutable_annotation,
|
|
13
|
+
)
|
|
14
|
+
from ..configuration.values import _looks_secret_name, canonical_path
|
|
15
|
+
from .errors import RuntimeSettingError, SettingDefinitionError
|
|
16
|
+
|
|
17
|
+
_T = TypeVar("_T")
|
|
18
|
+
_MISSING = object()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SettingDefinition(Generic[_T]):
|
|
22
|
+
__slots__ = ("_adapter", "_default", "_name", "_value_type")
|
|
23
|
+
|
|
24
|
+
def __init__(self, name: str, value_type: type[_T], *, default: object = _MISSING) -> None:
|
|
25
|
+
_validate_setting_name(name)
|
|
26
|
+
if not isinstance(value_type, type):
|
|
27
|
+
raise SettingDefinitionError(setting=name, reason="value_type must be a runtime type")
|
|
28
|
+
if _contains_secret_type(value_type):
|
|
29
|
+
raise SettingDefinitionError(
|
|
30
|
+
setting=name, reason="runtime settings cannot contain secrets"
|
|
31
|
+
)
|
|
32
|
+
try:
|
|
33
|
+
validate_immutable_annotation(value_type)
|
|
34
|
+
adapter: TypeAdapter[_T] = TypeAdapter(value_type)
|
|
35
|
+
validated_default = (
|
|
36
|
+
_MISSING
|
|
37
|
+
if default is _MISSING
|
|
38
|
+
else freeze_typed_value(adapter.validate_python(default), allow_secrets=False)
|
|
39
|
+
)
|
|
40
|
+
except (TypeError, ValidationError) as original_error:
|
|
41
|
+
raise SettingDefinitionError(
|
|
42
|
+
setting=name,
|
|
43
|
+
reason=f"default is invalid: {type(original_error).__name__}",
|
|
44
|
+
) from None
|
|
45
|
+
self._name = name
|
|
46
|
+
self._value_type = value_type
|
|
47
|
+
self._adapter = adapter
|
|
48
|
+
self._default = validated_default
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def name(self) -> str:
|
|
52
|
+
return self._name
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def value_type(self) -> type[_T]:
|
|
56
|
+
return self._value_type
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def has_default(self) -> bool:
|
|
60
|
+
return self._default is not _MISSING
|
|
61
|
+
|
|
62
|
+
def _validate_python(self, value: object) -> _T:
|
|
63
|
+
return cast(
|
|
64
|
+
_T, freeze_typed_value(self._adapter.validate_python(value), allow_secrets=False)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def _validate_json(self, value: str) -> _T:
|
|
68
|
+
return cast(_T, freeze_typed_value(self._adapter.validate_json(value), allow_secrets=False))
|
|
69
|
+
|
|
70
|
+
def _dump_json(self, value: object) -> tuple[_T, str]:
|
|
71
|
+
validated = self._validate_python(value)
|
|
72
|
+
# frozen snapshot 中的 MappingProxyType 不是 Pydantic 原生 dict serializer 输入。
|
|
73
|
+
# 只在 I/O 边界复制容器,继续用同一 TypeAdapter 编码,不暴露可变副本。
|
|
74
|
+
serialized = self._adapter.dump_json(cast(_T, _serialization_value(validated))).decode(
|
|
75
|
+
"utf-8"
|
|
76
|
+
)
|
|
77
|
+
return validated, serialized
|
|
78
|
+
|
|
79
|
+
def _default_value(self) -> _T:
|
|
80
|
+
if self._default is _MISSING:
|
|
81
|
+
raise RuntimeSettingError(
|
|
82
|
+
setting=self.name,
|
|
83
|
+
operation="get",
|
|
84
|
+
error_type="MissingSettingValue",
|
|
85
|
+
)
|
|
86
|
+
return cast(_T, self._default)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _validate_setting_name(name: object) -> None:
|
|
90
|
+
if isinstance(name, str) and _looks_secret_name(name):
|
|
91
|
+
raise SettingDefinitionError(setting=name, reason="runtime settings cannot store secrets")
|
|
92
|
+
try:
|
|
93
|
+
if not isinstance(name, str) or ".".join(canonical_path(name)) != name:
|
|
94
|
+
raise ValueError("setting name must already be canonical")
|
|
95
|
+
except (TypeError, ValueError):
|
|
96
|
+
raise SettingDefinitionError(
|
|
97
|
+
setting=repr(name), reason="name must be a canonical configuration path"
|
|
98
|
+
) from None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _contains_secret_type(value_type: object, seen: frozenset[object] = frozenset()) -> bool:
|
|
102
|
+
if value_type in seen:
|
|
103
|
+
return False
|
|
104
|
+
if isinstance(value_type, type) and issubclass(value_type, (Secret, SecretStr, SecretBytes)):
|
|
105
|
+
return True
|
|
106
|
+
if isinstance(value_type, type) and issubclass(value_type, BaseModel):
|
|
107
|
+
next_seen = seen | {value_type}
|
|
108
|
+
return any(
|
|
109
|
+
_contains_secret_type(field.annotation, next_seen)
|
|
110
|
+
for field in value_type.model_fields.values()
|
|
111
|
+
)
|
|
112
|
+
origin = get_origin(value_type)
|
|
113
|
+
return origin is not None and any(
|
|
114
|
+
_contains_secret_type(argument, seen) for argument in get_args(value_type)
|
|
115
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""把受控类型发现解释为 Settings 定义;校验只发生在用户贡献边界。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Any, cast
|
|
8
|
+
|
|
9
|
+
from ..diagnostics import SourceLocation
|
|
10
|
+
from ..modularity.discovery import _DiscoveredType
|
|
11
|
+
from .contracts import SettingDefinition
|
|
12
|
+
from .definitions import SettingDefinitionContext, SettingDefinitionProvider
|
|
13
|
+
from .errors import SettingDefinitionError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _collect_setting_definitions(
|
|
17
|
+
discoveries: tuple[_DiscoveredType, ...],
|
|
18
|
+
) -> tuple[tuple[SettingDefinition[Any], ...], dict[str, SourceLocation]]:
|
|
19
|
+
providers: dict[type[object], SourceLocation] = {}
|
|
20
|
+
sources: dict[str, SourceLocation] = {}
|
|
21
|
+
definitions: list[SettingDefinition[Any]] = []
|
|
22
|
+
for discovery in discoveries:
|
|
23
|
+
candidate = discovery.candidate
|
|
24
|
+
if not issubclass(candidate, SettingDefinitionProvider) or inspect.isabstract(candidate):
|
|
25
|
+
continue
|
|
26
|
+
previous = providers.get(candidate)
|
|
27
|
+
if previous is not None:
|
|
28
|
+
if previous.owner != discovery.source.owner:
|
|
29
|
+
raise SettingDefinitionError(
|
|
30
|
+
setting=candidate.__qualname__,
|
|
31
|
+
reason=f"provider is already owned by {previous}",
|
|
32
|
+
source=discovery.source,
|
|
33
|
+
)
|
|
34
|
+
continue
|
|
35
|
+
providers[candidate] = discovery.source
|
|
36
|
+
for definition in _define(candidate, discovery.source):
|
|
37
|
+
if definition.name in sources:
|
|
38
|
+
raise SettingDefinitionError(
|
|
39
|
+
setting=definition.name,
|
|
40
|
+
reason=f"setting is already owned by {sources[definition.name]}",
|
|
41
|
+
source=discovery.source,
|
|
42
|
+
)
|
|
43
|
+
sources[definition.name] = discovery.source
|
|
44
|
+
definitions.append(definition)
|
|
45
|
+
return tuple(definitions), sources
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _define(
|
|
49
|
+
provider_type: type[SettingDefinitionProvider], source: SourceLocation
|
|
50
|
+
) -> tuple[SettingDefinition[Any], ...]:
|
|
51
|
+
context = SettingDefinitionContext(source)
|
|
52
|
+
try:
|
|
53
|
+
provider = provider_type()
|
|
54
|
+
callback = cast(Callable[[SettingDefinitionContext], object], provider.define)
|
|
55
|
+
if inspect.iscoroutinefunction(callback) or inspect.isasyncgenfunction(callback):
|
|
56
|
+
raise TypeError("define must be synchronous")
|
|
57
|
+
result = callback(context)
|
|
58
|
+
if result is not None:
|
|
59
|
+
# 拒绝同步函数偷渡 coroutine,同时释放尚未运行的原生 coroutine/generator。
|
|
60
|
+
if inspect.iscoroutine(result) or inspect.isgenerator(result):
|
|
61
|
+
result.close()
|
|
62
|
+
raise TypeError("define must return None")
|
|
63
|
+
except Exception as error:
|
|
64
|
+
raise SettingDefinitionError(
|
|
65
|
+
setting=provider_type.__qualname__,
|
|
66
|
+
reason=f"definition provider failed: {type(error).__name__}: {error}",
|
|
67
|
+
source=source,
|
|
68
|
+
) from error
|
|
69
|
+
finally:
|
|
70
|
+
definitions = context._close()
|
|
71
|
+
return definitions
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""同步定义贡献的契约与一次调用独占的上下文;不接触运行期容器。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..diagnostics import SourceLocation
|
|
9
|
+
from .contracts import SettingDefinition
|
|
10
|
+
from .errors import SettingDefinitionError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SettingDefinitionProvider(ABC):
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def define(self, context: SettingDefinitionContext) -> None:
|
|
16
|
+
"""向所属 Application 贡献定义;必须同步完成并返回 None。"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SettingDefinitionContext:
|
|
20
|
+
__slots__ = ("_closed", "_definitions", "_source")
|
|
21
|
+
|
|
22
|
+
def __init__(self, source: SourceLocation) -> None:
|
|
23
|
+
self._source = source
|
|
24
|
+
self._definitions: list[SettingDefinition[Any]] = []
|
|
25
|
+
self._closed = False
|
|
26
|
+
|
|
27
|
+
def add(self, definition: SettingDefinition[Any]) -> None:
|
|
28
|
+
if self._closed:
|
|
29
|
+
raise SettingDefinitionError(
|
|
30
|
+
setting="context", reason="definition context is closed", source=self._source
|
|
31
|
+
)
|
|
32
|
+
if not isinstance(definition, SettingDefinition):
|
|
33
|
+
raise SettingDefinitionError(
|
|
34
|
+
setting="context", reason="expected SettingDefinition", source=self._source
|
|
35
|
+
)
|
|
36
|
+
self._definitions.append(definition)
|
|
37
|
+
|
|
38
|
+
def _close(self) -> tuple[SettingDefinition[Any], ...]:
|
|
39
|
+
# 成功或异常都封闭上下文;外部保留的引用不能再贡献定义。
|
|
40
|
+
self._closed = True
|
|
41
|
+
return tuple(self._definitions)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Runtime Settings definition、lookup 与 provider failures。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..diagnostics import SourceLocation
|
|
6
|
+
from ..errors.base import FrameworkError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SettingDefinitionError(FrameworkError):
|
|
10
|
+
def __init__(self, *, setting: str, reason: str, source: SourceLocation | None = None) -> None:
|
|
11
|
+
self.setting = setting
|
|
12
|
+
self.reason = reason
|
|
13
|
+
self.source = source
|
|
14
|
+
super().__init__(f"Invalid setting {setting!r}: {reason}; source={source}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RuntimeSettingError(FrameworkError):
|
|
18
|
+
def __init__(self, *, setting: str, operation: str, error_type: str) -> None:
|
|
19
|
+
self.setting = setting
|
|
20
|
+
self.operation = operation
|
|
21
|
+
self.error_type = error_type
|
|
22
|
+
super().__init__(f"Runtime setting {operation} failed for {setting!r} with {error_type}")
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""长期组件的 typed 应用契约;执行上下文由组件自身负责。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Generic, Protocol, TypeVar
|
|
8
|
+
|
|
9
|
+
from .contracts import SettingDefinition
|
|
10
|
+
from .errors import SettingDefinitionError
|
|
11
|
+
from .values import SettingSnapshot
|
|
12
|
+
|
|
13
|
+
_T = TypeVar("_T")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SettingHandler(Protocol[_T]):
|
|
17
|
+
async def apply(self, snapshot: SettingSnapshot[_T]) -> None: ...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class SettingHandlerDefinition(Generic[_T]):
|
|
22
|
+
setting: SettingDefinition[_T]
|
|
23
|
+
handler_type: type[SettingHandler[_T]]
|
|
24
|
+
|
|
25
|
+
def __post_init__(self) -> None:
|
|
26
|
+
if not isinstance(self.setting, SettingDefinition):
|
|
27
|
+
raise SettingDefinitionError(setting="handler", reason="setting must be a definition")
|
|
28
|
+
if (
|
|
29
|
+
not isinstance(self.handler_type, type)
|
|
30
|
+
or inspect.isabstract(self.handler_type)
|
|
31
|
+
or not inspect.iscoroutinefunction(getattr(self.handler_type, "apply", None))
|
|
32
|
+
):
|
|
33
|
+
raise SettingDefinitionError(
|
|
34
|
+
setting=self.setting.name, reason="handler must have an async apply method"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class SettingApplicationStatus:
|
|
40
|
+
setting: str
|
|
41
|
+
handler: str
|
|
42
|
+
last_applied: SettingSnapshot[object] | None
|
|
43
|
+
error_type: str | None
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Settings 管理的权限、输入与 HTTP 可序列化结果契约。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal, Protocol
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, JsonValue
|
|
9
|
+
|
|
10
|
+
from ..application_services import ApplicationServiceContract
|
|
11
|
+
from ..authorization import PermissionName
|
|
12
|
+
from ..notifications import NotificationPublishStatus
|
|
13
|
+
from .values import SettingValueSource
|
|
14
|
+
|
|
15
|
+
SETTINGS_READ = PermissionName("settings.read")
|
|
16
|
+
SETTINGS_WRITE = PermissionName("settings.write")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class UpdateSettingCommand(BaseModel):
|
|
20
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
21
|
+
|
|
22
|
+
value: JsonValue
|
|
23
|
+
expected_version: UUID | None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ResetSettingCommand(BaseModel):
|
|
27
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
28
|
+
|
|
29
|
+
expected_version: UUID | None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SettingView(BaseModel):
|
|
33
|
+
model_config = ConfigDict(frozen=True)
|
|
34
|
+
|
|
35
|
+
name: str
|
|
36
|
+
value: JsonValue
|
|
37
|
+
source: SettingValueSource
|
|
38
|
+
version: UUID | None
|
|
39
|
+
effect: str = "New operations read the saved value; running components apply it separately."
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SettingWriteResult(BaseModel):
|
|
43
|
+
model_config = ConfigDict(frozen=True)
|
|
44
|
+
|
|
45
|
+
saved: Literal[True] = True
|
|
46
|
+
setting: SettingView
|
|
47
|
+
notification: NotificationPublishStatus
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SettingsManagementApplicationService(ApplicationServiceContract, Protocol):
|
|
51
|
+
async def get_list(self) -> tuple[SettingView, ...]: ...
|
|
52
|
+
|
|
53
|
+
async def get(self, name: str) -> SettingView: ...
|
|
54
|
+
|
|
55
|
+
async def update(self, name: str, command: UpdateSettingCommand) -> SettingWriteResult: ...
|
|
56
|
+
|
|
57
|
+
async def reset(self, name: str, command: ResetSettingCommand) -> SettingWriteResult: ...
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""条件写入与提交后通知的 owner;读取解析由 SettingProvider 负责。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING, Any, TypeVar, cast
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from pydantic import ValidationError
|
|
10
|
+
from pydantic_core import PydanticSerializationError
|
|
11
|
+
|
|
12
|
+
from ..notifications import NotificationPayloadError, NotificationPublishStatus, Notifications
|
|
13
|
+
from .catalog import SettingCatalog
|
|
14
|
+
from .changes import SETTINGS_CHANGED, SettingChanged
|
|
15
|
+
from .contracts import SettingDefinition
|
|
16
|
+
from .errors import RuntimeSettingError
|
|
17
|
+
from .provider import SettingProvider
|
|
18
|
+
from .store import RuntimeSettingStore
|
|
19
|
+
from .values import SettingSnapshot, SettingValueSource, SettingWriteOutcome, StoredSetting
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from .refresh import SettingsRefreshRuntime
|
|
23
|
+
|
|
24
|
+
_T = TypeVar("_T")
|
|
25
|
+
_LOGGER = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SettingManager:
|
|
29
|
+
__slots__ = ("_catalog", "_notifications", "_provider", "_refresh", "_store")
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self, catalog: SettingCatalog, provider: SettingProvider, store: RuntimeSettingStore
|
|
33
|
+
) -> None:
|
|
34
|
+
self._catalog = catalog
|
|
35
|
+
self._provider = provider
|
|
36
|
+
self._store = store
|
|
37
|
+
self._refresh: SettingsRefreshRuntime | None = None
|
|
38
|
+
self._notifications: Notifications | None = None
|
|
39
|
+
|
|
40
|
+
async def set_global(self, definition: SettingDefinition[_T], value: _T) -> _T:
|
|
41
|
+
current = await self._provider.snapshot(definition)
|
|
42
|
+
saved = await self.update(definition, value, expected_version=current.version)
|
|
43
|
+
return cast(_T, saved.snapshot.value)
|
|
44
|
+
|
|
45
|
+
async def reset_global(self, definition: SettingDefinition[Any]) -> None:
|
|
46
|
+
current = await self._provider.snapshot(definition)
|
|
47
|
+
await self.reset(definition, expected_version=current.version)
|
|
48
|
+
|
|
49
|
+
async def update(
|
|
50
|
+
self, definition: SettingDefinition[_T], value: object, *, expected_version: UUID | None
|
|
51
|
+
) -> SettingWriteOutcome[_T]:
|
|
52
|
+
self._catalog.require_owned(definition)
|
|
53
|
+
try:
|
|
54
|
+
validated, serialized = definition._dump_json(value)
|
|
55
|
+
except (TypeError, ValidationError, PydanticSerializationError):
|
|
56
|
+
raise RuntimeSettingError(
|
|
57
|
+
setting=definition.name, operation="set", error_type="ValidationError"
|
|
58
|
+
) from None
|
|
59
|
+
saved = await self._store.write(
|
|
60
|
+
definition.name, serialized, expected_version=expected_version
|
|
61
|
+
)
|
|
62
|
+
# 返回本次已经提交的值,不在提交后重读,避免把另一次并发写入误报为本次结果。
|
|
63
|
+
snapshot = SettingSnapshot(
|
|
64
|
+
definition.name, validated, SettingValueSource.RUNTIME, saved.version
|
|
65
|
+
)
|
|
66
|
+
return SettingWriteOutcome(snapshot, await self._committed(definition.name))
|
|
67
|
+
|
|
68
|
+
async def reset(
|
|
69
|
+
self, definition: SettingDefinition[_T], *, expected_version: UUID | None
|
|
70
|
+
) -> SettingWriteOutcome[_T]:
|
|
71
|
+
self._catalog.require_owned(definition)
|
|
72
|
+
# 默认值先校验,不能在物理删除已经提交之后再抛出配置校验失败。
|
|
73
|
+
fallback = self._provider._resolve(definition, StoredSetting(None, expected_version))
|
|
74
|
+
saved = await self._store.write(definition.name, None, expected_version=expected_version)
|
|
75
|
+
snapshot = SettingSnapshot(definition.name, fallback.value, fallback.source, saved.version)
|
|
76
|
+
return SettingWriteOutcome(snapshot, await self._committed(definition.name))
|
|
77
|
+
|
|
78
|
+
async def _committed(self, name: str) -> NotificationPublishStatus:
|
|
79
|
+
# 本地刷新先入队;Redis 降级不会撤销短事务已经提交的结果。
|
|
80
|
+
if self._refresh is not None:
|
|
81
|
+
self._refresh.request_refresh(name)
|
|
82
|
+
if self._notifications is None:
|
|
83
|
+
return NotificationPublishStatus.NOT_CONFIGURED
|
|
84
|
+
try:
|
|
85
|
+
return await self._notifications.publish(SETTINGS_CHANGED, SettingChanged(name=name))
|
|
86
|
+
except NotificationPayloadError:
|
|
87
|
+
# 发布大小/编码限制是独立结果;不能把已提交的写入重新表现为保存失败。
|
|
88
|
+
_LOGGER.warning("Committed setting notification rejected for %s", name)
|
|
89
|
+
return NotificationPublishStatus.REJECTED
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Settings 是通用通知的使用方,重连和漏消息的补偿由 Settings 自身拥有。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import AsyncExitStack
|
|
6
|
+
|
|
7
|
+
from ..lifecycle import PreInitializeContext, ShutdownContext
|
|
8
|
+
from ..modularity import AppModule
|
|
9
|
+
from ..notifications import Notifications
|
|
10
|
+
from .changes import SETTINGS_CHANGED, SettingChanged
|
|
11
|
+
from .manager import SettingManager
|
|
12
|
+
from .refresh import SettingsRefreshRuntime
|
|
13
|
+
from .refresh_module import SettingsRefreshModule
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SettingsNotificationsModule(AppModule):
|
|
17
|
+
dependencies = (SettingsRefreshModule,)
|
|
18
|
+
notification_definitions = (SETTINGS_CHANGED,)
|
|
19
|
+
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
self._subscriptions = AsyncExitStack()
|
|
22
|
+
|
|
23
|
+
async def pre_initialize(self, context: PreInitializeContext) -> None:
|
|
24
|
+
refresh = await context.container.get(SettingsRefreshRuntime)
|
|
25
|
+
notifications = await context.container.get(Notifications)
|
|
26
|
+
settings = await context.container.get(SettingManager)
|
|
27
|
+
|
|
28
|
+
async def changed(message: SettingChanged) -> None:
|
|
29
|
+
refresh.request_refresh(message.name)
|
|
30
|
+
|
|
31
|
+
await self._subscriptions.enter_async_context(
|
|
32
|
+
notifications.subscribe(SETTINGS_CHANGED, changed, on_reconnect=refresh.request_refresh)
|
|
33
|
+
)
|
|
34
|
+
settings._notifications = notifications
|
|
35
|
+
|
|
36
|
+
async def shutdown(self, context: ShutdownContext) -> None:
|
|
37
|
+
settings = await context.container.get(SettingManager)
|
|
38
|
+
settings._notifications = None
|
|
39
|
+
await self._subscriptions.aclose()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""settings 权限定义;持久化名称继续来自所属正式契约。"""
|
|
2
|
+
|
|
3
|
+
from ..authorization import (
|
|
4
|
+
PermissionDefinition,
|
|
5
|
+
PermissionDefinitionContext,
|
|
6
|
+
PermissionDefinitionProvider,
|
|
7
|
+
)
|
|
8
|
+
from .management import SETTINGS_READ, SETTINGS_WRITE
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SettingsPermissionDefinitionProvider(PermissionDefinitionProvider):
|
|
12
|
+
def define(self, context: PermissionDefinitionContext) -> None:
|
|
13
|
+
context.add(PermissionDefinition(SETTINGS_READ, "Read settings"))
|
|
14
|
+
context.add(PermissionDefinition(SETTINGS_WRITE, "Write settings"))
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""有效值读取:存储覆盖、Application 配置快照、不可变定义默认值。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TypeVar, cast
|
|
6
|
+
|
|
7
|
+
from pydantic import ValidationError
|
|
8
|
+
from pydantic_core import PydanticSerializationError
|
|
9
|
+
|
|
10
|
+
from ..configuration import ConfigurationRoot
|
|
11
|
+
from .catalog import SettingCatalog
|
|
12
|
+
from .contracts import SettingDefinition
|
|
13
|
+
from .errors import RuntimeSettingError
|
|
14
|
+
from .store import RuntimeSettingStore
|
|
15
|
+
from .values import SettingSnapshot, SettingValueSource, StoredSetting
|
|
16
|
+
|
|
17
|
+
_T = TypeVar("_T")
|
|
18
|
+
_MISSING = object()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SettingProvider:
|
|
22
|
+
__slots__ = ("_catalog", "_configuration", "_store")
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
catalog: SettingCatalog,
|
|
27
|
+
configuration: ConfigurationRoot,
|
|
28
|
+
store: RuntimeSettingStore | None = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
self._catalog = catalog
|
|
31
|
+
self._configuration = configuration
|
|
32
|
+
self._store = store
|
|
33
|
+
|
|
34
|
+
async def get(self, definition: SettingDefinition[_T]) -> _T:
|
|
35
|
+
snapshot = await self.snapshot(definition)
|
|
36
|
+
if snapshot.source is SettingValueSource.UNSET:
|
|
37
|
+
raise RuntimeSettingError(
|
|
38
|
+
setting=definition.name, operation="get", error_type="MissingSettingValue"
|
|
39
|
+
)
|
|
40
|
+
return cast(_T, snapshot.value)
|
|
41
|
+
|
|
42
|
+
async def snapshot(self, definition: SettingDefinition[_T]) -> SettingSnapshot[_T]:
|
|
43
|
+
self._catalog.require_owned(definition)
|
|
44
|
+
stored = (
|
|
45
|
+
await self._store.read(definition.name)
|
|
46
|
+
if self._store is not None
|
|
47
|
+
else StoredSetting(None, None)
|
|
48
|
+
)
|
|
49
|
+
return self._resolve(definition, stored)
|
|
50
|
+
|
|
51
|
+
def _resolve(
|
|
52
|
+
self, definition: SettingDefinition[_T], stored: StoredSetting
|
|
53
|
+
) -> SettingSnapshot[_T]:
|
|
54
|
+
try:
|
|
55
|
+
if stored.value_json is not None:
|
|
56
|
+
value = definition._validate_json(stored.value_json)
|
|
57
|
+
source = SettingValueSource.RUNTIME
|
|
58
|
+
else:
|
|
59
|
+
configured = self._configuration.get_path(f"settings.{definition.name}", _MISSING)
|
|
60
|
+
if configured is not _MISSING:
|
|
61
|
+
value = definition._validate_python(configured)
|
|
62
|
+
source = SettingValueSource.CONFIGURATION
|
|
63
|
+
elif definition.has_default:
|
|
64
|
+
value = definition._default_value()
|
|
65
|
+
source = SettingValueSource.DEFINITION
|
|
66
|
+
else:
|
|
67
|
+
return SettingSnapshot(
|
|
68
|
+
definition.name, None, SettingValueSource.UNSET, stored.version
|
|
69
|
+
)
|
|
70
|
+
return SettingSnapshot(definition.name, value, source, stored.version)
|
|
71
|
+
except (TypeError, ValidationError, PydanticSerializationError):
|
|
72
|
+
raise RuntimeSettingError(
|
|
73
|
+
setting=definition.name, operation="get", error_type="ValidationError"
|
|
74
|
+
) from None
|