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,212 @@
|
|
|
1
|
+
"""延迟编译 HTTP 声明;当前 Application 的最终服务图决定纯类型注入。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
7
|
+
from copy import copy
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from functools import wraps
|
|
10
|
+
from types import MappingProxyType
|
|
11
|
+
from typing import Annotated, Any, get_args, get_origin, get_type_hints
|
|
12
|
+
|
|
13
|
+
from dishka import AsyncContainer, FromComponent, Scope
|
|
14
|
+
from dishka.integrations.base import default_parse_dependency
|
|
15
|
+
from fastapi import APIRouter
|
|
16
|
+
from fastapi.dependencies.models import Dependant
|
|
17
|
+
from fastapi.dependencies.utils import add_non_field_param_to_dependency
|
|
18
|
+
from fastapi.exceptions import FastAPIError
|
|
19
|
+
from fastapi.params import Depends
|
|
20
|
+
from pydantic.fields import FieldInfo
|
|
21
|
+
from starlette.routing import compile_path
|
|
22
|
+
|
|
23
|
+
from ..authorization import PermissionName
|
|
24
|
+
from ..authorization.contracts import _authorization_requirement, _AuthorizationRequirement
|
|
25
|
+
from ..invocation.callables import _dependency_key
|
|
26
|
+
from ..unit_of_work.options import _declaration
|
|
27
|
+
from .errors import FastApiAdapterError
|
|
28
|
+
from .filters import ServiceFilter, _filters_for, _ordered_filters, _validate_filters
|
|
29
|
+
from .manual_action import _action_route_type, _ActionDefinition, _ActionRoute, _wrap_action
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True, slots=True)
|
|
33
|
+
class _HttpDeclaration:
|
|
34
|
+
path: str
|
|
35
|
+
endpoint: Callable[..., Any]
|
|
36
|
+
options: Mapping[str, Any]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class _RouterInclusion:
|
|
41
|
+
router: APIRouter
|
|
42
|
+
options: Mapping[str, Any]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class HttpRouter(APIRouter):
|
|
46
|
+
"""沿用 FastAPI 路由声明 API;安装前只保存声明,不提前解析服务参数。"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
*args: Any,
|
|
51
|
+
permissions: Sequence[PermissionName] | None = None,
|
|
52
|
+
filters: Sequence[ServiceFilter] = (),
|
|
53
|
+
**kwargs: Any,
|
|
54
|
+
) -> None:
|
|
55
|
+
self._permissions = None if permissions is None else tuple(permissions)
|
|
56
|
+
self._filters = tuple(filters)
|
|
57
|
+
kwargs.setdefault("route_class", _ActionRoute)
|
|
58
|
+
super().__init__(*args, **kwargs)
|
|
59
|
+
self._compiled = False
|
|
60
|
+
self._declarations: list[_HttpDeclaration | _RouterInclusion] = []
|
|
61
|
+
|
|
62
|
+
def add_api_route(self, path: str, endpoint: Callable[..., Any], **kwargs: Any) -> None:
|
|
63
|
+
if self._compiled:
|
|
64
|
+
return super().add_api_route(path, endpoint, **kwargs)
|
|
65
|
+
self._declarations.append(_HttpDeclaration(path, endpoint, MappingProxyType(dict(kwargs))))
|
|
66
|
+
|
|
67
|
+
def include_router(self, router: APIRouter, **kwargs: Any) -> None:
|
|
68
|
+
if self._compiled:
|
|
69
|
+
return super().include_router(router, **kwargs)
|
|
70
|
+
self._declarations.append(_RouterInclusion(router, MappingProxyType(dict(kwargs))))
|
|
71
|
+
|
|
72
|
+
def _compile(
|
|
73
|
+
self,
|
|
74
|
+
container: AsyncContainer,
|
|
75
|
+
prefix: str = "",
|
|
76
|
+
inherited_permissions: tuple[PermissionName, ...] | None = None,
|
|
77
|
+
inherited_filters: tuple[ServiceFilter, ...] = (),
|
|
78
|
+
) -> APIRouter:
|
|
79
|
+
# 复制原生 router 配置,避免重新维护 FastAPI 默认值/配置字段第二清单。
|
|
80
|
+
# 每个 Application 只修改自己的 routes 和 endpoint wrapper。
|
|
81
|
+
permissions = (
|
|
82
|
+
None
|
|
83
|
+
if self._permissions is None and inherited_permissions is None
|
|
84
|
+
else (*(inherited_permissions or ()), *(self._permissions or ()))
|
|
85
|
+
)
|
|
86
|
+
filters = (*inherited_filters, *self._filters)
|
|
87
|
+
compiled = copy(self)
|
|
88
|
+
compiled._compiled = True
|
|
89
|
+
compiled.route_class = _action_route_type(self.route_class)
|
|
90
|
+
compiled.routes = list(self.routes)
|
|
91
|
+
compiled._declarations = []
|
|
92
|
+
for declaration in tuple(self._declarations):
|
|
93
|
+
if isinstance(declaration, _RouterInclusion):
|
|
94
|
+
child = declaration.router
|
|
95
|
+
if isinstance(child, HttpRouter):
|
|
96
|
+
child = child._compile(
|
|
97
|
+
container,
|
|
98
|
+
prefix + self.prefix + declaration.options.get("prefix", ""),
|
|
99
|
+
permissions,
|
|
100
|
+
filters,
|
|
101
|
+
)
|
|
102
|
+
elif permissions is not None or filters:
|
|
103
|
+
raise FastApiAdapterError(
|
|
104
|
+
f"HttpRouter at {prefix + self.prefix or '/'} cannot apply inherited "
|
|
105
|
+
f"permissions/filters to native {type(child).__qualname__} at "
|
|
106
|
+
f"{child.prefix or '/'}; use a nested HttpRouter"
|
|
107
|
+
)
|
|
108
|
+
APIRouter.include_router(compiled, child, **declaration.options)
|
|
109
|
+
else:
|
|
110
|
+
path = prefix + self.prefix + declaration.path
|
|
111
|
+
endpoint = _compile_endpoint(declaration.endpoint, path, container)
|
|
112
|
+
authorization = _authorization_requirement(object, declaration.endpoint)
|
|
113
|
+
if permissions is not None and not authorization.allows_anonymous:
|
|
114
|
+
authorization = _AuthorizationRequirement(
|
|
115
|
+
True,
|
|
116
|
+
tuple(dict.fromkeys((*permissions, *authorization.permissions))),
|
|
117
|
+
False,
|
|
118
|
+
)
|
|
119
|
+
definition = _ActionDefinition(
|
|
120
|
+
declaration.endpoint,
|
|
121
|
+
authorization,
|
|
122
|
+
_declaration(type(self), declaration.endpoint),
|
|
123
|
+
_ordered_filters(filters, _filters_for(declaration.endpoint)),
|
|
124
|
+
)
|
|
125
|
+
_validate_filters(definition.filters, container)
|
|
126
|
+
endpoint = _wrap_action(endpoint, definition)
|
|
127
|
+
options = dict(declaration.options)
|
|
128
|
+
if options.get("route_class_override") is not None:
|
|
129
|
+
options["route_class_override"] = _action_route_type(
|
|
130
|
+
options["route_class_override"]
|
|
131
|
+
)
|
|
132
|
+
try:
|
|
133
|
+
APIRouter.add_api_route(
|
|
134
|
+
compiled,
|
|
135
|
+
declaration.path,
|
|
136
|
+
endpoint,
|
|
137
|
+
**options,
|
|
138
|
+
)
|
|
139
|
+
except (TypeError, ValueError, FastAPIError) as error:
|
|
140
|
+
raise FastApiAdapterError(
|
|
141
|
+
f"{endpoint.__qualname__} at {path}: {error}"
|
|
142
|
+
) from error
|
|
143
|
+
return compiled
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _compile_endpoint(
|
|
147
|
+
endpoint: Callable[..., Any],
|
|
148
|
+
path: str,
|
|
149
|
+
container: AsyncContainer,
|
|
150
|
+
) -> Callable[..., Any]:
|
|
151
|
+
try:
|
|
152
|
+
hints = get_type_hints(endpoint, include_extras=True)
|
|
153
|
+
signature = inspect.signature(endpoint)
|
|
154
|
+
_, _, path_parameters = compile_path(path)
|
|
155
|
+
parameters = []
|
|
156
|
+
for parameter in signature.parameters.values():
|
|
157
|
+
hint = hints.get(parameter.name, parameter.annotation)
|
|
158
|
+
parameters.append(_compile_parameter(parameter, hint, path_parameters, container))
|
|
159
|
+
except (TypeError, NameError) as error:
|
|
160
|
+
raise FastApiAdapterError(f"{endpoint.__qualname__} at {path}: {error}") from error
|
|
161
|
+
|
|
162
|
+
if inspect.iscoroutinefunction(endpoint):
|
|
163
|
+
|
|
164
|
+
@wraps(endpoint)
|
|
165
|
+
async def target(*args: Any, **kwargs: Any) -> Any:
|
|
166
|
+
return await endpoint(*args, **kwargs)
|
|
167
|
+
else:
|
|
168
|
+
|
|
169
|
+
@wraps(endpoint)
|
|
170
|
+
def target(*args: Any, **kwargs: Any) -> Any:
|
|
171
|
+
return endpoint(*args, **kwargs)
|
|
172
|
+
|
|
173
|
+
target.__signature__ = signature.replace( # type: ignore[attr-defined]
|
|
174
|
+
parameters=parameters,
|
|
175
|
+
return_annotation=hints.get("return", signature.return_annotation),
|
|
176
|
+
)
|
|
177
|
+
target.__annotations__ = {parameter.name: parameter.annotation for parameter in parameters}
|
|
178
|
+
if "return" in hints:
|
|
179
|
+
target.__annotations__["return"] = hints["return"]
|
|
180
|
+
return target
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _compile_parameter(
|
|
184
|
+
parameter: inspect.Parameter,
|
|
185
|
+
hint: object,
|
|
186
|
+
path_parameters: Mapping[str, object],
|
|
187
|
+
container: AsyncContainer,
|
|
188
|
+
*,
|
|
189
|
+
infer_services: bool = True,
|
|
190
|
+
) -> inspect.Parameter:
|
|
191
|
+
metadata = get_args(hint)[1:] if get_origin(hint) is Annotated else ()
|
|
192
|
+
hint_type = get_args(hint)[0] if metadata else hint
|
|
193
|
+
explicit_di = default_parse_dependency(parameter, hint)
|
|
194
|
+
http_bound = parameter.name in path_parameters or any(
|
|
195
|
+
isinstance(value, (FieldInfo, Depends)) for value in (*metadata, parameter.default)
|
|
196
|
+
)
|
|
197
|
+
# 特殊参数由 FastAPI 的版本匹配原生规则解释,不复制 Request/Response 等类型列表。
|
|
198
|
+
native_special = add_non_field_param_to_dependency(
|
|
199
|
+
param_name=parameter.name,
|
|
200
|
+
type_annotation=hint_type,
|
|
201
|
+
dependant=Dependant(),
|
|
202
|
+
)
|
|
203
|
+
if http_bound or native_special:
|
|
204
|
+
if explicit_di is not None:
|
|
205
|
+
raise TypeError(f"{parameter.name}: explicit HTTP source conflicts with FromComponent")
|
|
206
|
+
return parameter.replace(annotation=hint)
|
|
207
|
+
if not infer_services and explicit_di is None:
|
|
208
|
+
return parameter.replace(annotation=hint)
|
|
209
|
+
key = _dependency_key(parameter, hint, container, Scope.REQUEST)
|
|
210
|
+
if key is not None:
|
|
211
|
+
hint = Annotated[key.type_hint, FromComponent(key.component or "")]
|
|
212
|
+
return parameter.replace(annotation=hint)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""手写 Action 的绑定后执行与绑定前策略,保留原生 FastAPI 依赖和线程池。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from functools import wraps
|
|
9
|
+
from typing import Any, cast
|
|
10
|
+
|
|
11
|
+
from dishka.integrations.fastapi import DishkaRoute
|
|
12
|
+
from fastapi.routing import APIRoute
|
|
13
|
+
from starlette.concurrency import run_in_threadpool
|
|
14
|
+
from starlette.requests import Request
|
|
15
|
+
from starlette.responses import Response
|
|
16
|
+
|
|
17
|
+
from ..application_services.dispatcher import _ApplicationServiceDispatcher
|
|
18
|
+
from ..authorization.contracts import _AuthorizationRequirement
|
|
19
|
+
from ..unit_of_work.options import UnitOfWorkDeclaration
|
|
20
|
+
from .action import _HttpActionOwner, _request_parameter
|
|
21
|
+
from .filters import ServiceFilter, _ordered_filters
|
|
22
|
+
from .parameters import _HttpParameterValidator
|
|
23
|
+
from .transfer import _manage_stream
|
|
24
|
+
from .uploads import _prepare_upload
|
|
25
|
+
|
|
26
|
+
_ACTION = "__python_platform_action_definition__"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class _ActionDefinition:
|
|
31
|
+
endpoint: Callable[..., Any]
|
|
32
|
+
authorization: _AuthorizationRequirement
|
|
33
|
+
work: UnitOfWorkDeclaration
|
|
34
|
+
filters: tuple[ServiceFilter, ...]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _ActionRoute(DishkaRoute):
|
|
38
|
+
def __init__(self, path: str, endpoint: Callable[..., Any], **kwargs: Any) -> None:
|
|
39
|
+
self._definition: _ActionDefinition = getattr(endpoint, _ACTION)
|
|
40
|
+
super().__init__(path, endpoint, **kwargs)
|
|
41
|
+
setattr(self.endpoint, _ACTION, self._definition)
|
|
42
|
+
|
|
43
|
+
def get_route_handler(self) -> Callable[[Request], Any]:
|
|
44
|
+
native = super().get_route_handler()
|
|
45
|
+
definition = self._definition
|
|
46
|
+
|
|
47
|
+
async def handle(request: Request) -> Response:
|
|
48
|
+
owner = await _HttpActionOwner.install(request)
|
|
49
|
+
try:
|
|
50
|
+
dispatcher = await owner.container.get(_ApplicationServiceDispatcher)
|
|
51
|
+
await dispatcher._authorize(definition.authorization)
|
|
52
|
+
await _prepare_upload(request, definition.endpoint, self)
|
|
53
|
+
result = await native(request)
|
|
54
|
+
_manage_stream(request, result)
|
|
55
|
+
owner.response_ready = True
|
|
56
|
+
return result
|
|
57
|
+
except BaseException as failure:
|
|
58
|
+
owner.failure = failure
|
|
59
|
+
raise
|
|
60
|
+
|
|
61
|
+
return handle
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _wrap_action(endpoint: Callable[..., Any], definition: _ActionDefinition) -> Callable[..., Any]:
|
|
65
|
+
signature = inspect.signature(endpoint)
|
|
66
|
+
validator = _HttpParameterValidator(signature)
|
|
67
|
+
parameters = tuple(signature.parameters.values())
|
|
68
|
+
request_parameter, injected_request = _request_parameter(parameters)
|
|
69
|
+
request_name = request_parameter.name
|
|
70
|
+
if injected_request:
|
|
71
|
+
parameters += (request_parameter,)
|
|
72
|
+
|
|
73
|
+
@wraps(endpoint)
|
|
74
|
+
async def target(**values: Any) -> Any:
|
|
75
|
+
request = cast(
|
|
76
|
+
Request, values.pop(request_name) if injected_request else values[request_name]
|
|
77
|
+
)
|
|
78
|
+
owner: _HttpActionOwner = request.state.python_platform_action
|
|
79
|
+
await owner.activate(definition.work, definition.endpoint.__name__)
|
|
80
|
+
snapshots = validator.capture(values)
|
|
81
|
+
|
|
82
|
+
async def invoke(actual: dict[str, object]) -> object:
|
|
83
|
+
if filters:
|
|
84
|
+
validator.validate_changes(actual, snapshots)
|
|
85
|
+
if inspect.iscoroutinefunction(endpoint):
|
|
86
|
+
return await endpoint(**actual)
|
|
87
|
+
return await run_in_threadpool(endpoint, **actual)
|
|
88
|
+
|
|
89
|
+
filters = _ordered_filters(
|
|
90
|
+
getattr(request.app.state, "python_platform_filters", ()), definition.filters
|
|
91
|
+
)
|
|
92
|
+
return await owner.invoke(definition.endpoint, values, filters, invoke)
|
|
93
|
+
|
|
94
|
+
target.__signature__ = signature.replace(parameters=parameters) # type: ignore[attr-defined]
|
|
95
|
+
target.__annotations__ = {p.name: p.annotation for p in parameters}
|
|
96
|
+
target.__annotations__["return"] = signature.return_annotation
|
|
97
|
+
setattr(target, _ACTION, definition)
|
|
98
|
+
return target
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _action_route_type(native: type[APIRoute]) -> type[APIRoute]:
|
|
102
|
+
if issubclass(native, _ActionRoute):
|
|
103
|
+
return native
|
|
104
|
+
# 原生自定义 APIRoute 继续拥有它的 handler 扩展;平台只增加同一 Action 边界。
|
|
105
|
+
return type(f"Managed{native.__name__}", (_ActionRoute, native), {})
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Host 有序 middleware 声明编译;原生实例共享,DI 处理对象按请求解析。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from dishka import AsyncContainer, Scope
|
|
11
|
+
from starlette.middleware import Middleware
|
|
12
|
+
from starlette.requests import Request
|
|
13
|
+
from starlette.types import ASGIApp, Receive, Send
|
|
14
|
+
from starlette.types import Scope as AsgiScope
|
|
15
|
+
|
|
16
|
+
from ..invocation.callables import _CallablePlan, _dependency_key
|
|
17
|
+
from .errors import FastApiAdapterError
|
|
18
|
+
from .request_context import _RequestTaskMiddleware
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class DiMiddleware:
|
|
23
|
+
"""原生 ASGI:async invoke(scope, receive, send, call_next, ...DI参数)。"""
|
|
24
|
+
|
|
25
|
+
handler: type | Callable[..., Awaitable[None]]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _DiMiddleware:
|
|
29
|
+
def __init__(self, app: ASGIApp, *, declaration: DiMiddleware, plan: _CallablePlan) -> None:
|
|
30
|
+
self._app = app
|
|
31
|
+
self._declaration = declaration
|
|
32
|
+
self._plan = plan
|
|
33
|
+
|
|
34
|
+
async def __call__(self, scope: AsgiScope, receive: Receive, send: Send) -> None:
|
|
35
|
+
if scope["type"] != "http":
|
|
36
|
+
await self._app(scope, receive, send)
|
|
37
|
+
return
|
|
38
|
+
# 不创建转发 task;REQUEST generator 与 ContextVar 在解析它们的 task 中退出。
|
|
39
|
+
container = Request(scope).state.dishka_container
|
|
40
|
+
handler = self._declaration.handler
|
|
41
|
+
if isinstance(handler, type):
|
|
42
|
+
instance = await container.get(handler)
|
|
43
|
+
handler = instance.invoke
|
|
44
|
+
await self._plan.invoke(handler, container, (scope, receive, send, self._app), {})
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _NativeMiddleware:
|
|
48
|
+
def __init__(self, app: ASGIApp, *, middleware: Middleware) -> None:
|
|
49
|
+
cls, args, kwargs = middleware
|
|
50
|
+
# 每个下游入口检查前置短 UoW;BaseHTTPMiddleware 切 task 时也走同一入口。
|
|
51
|
+
self._app = _RequestTaskMiddleware(cls(_RequestTaskMiddleware(app), *args, **kwargs))
|
|
52
|
+
|
|
53
|
+
async def __call__(self, *args: Any) -> None:
|
|
54
|
+
await self._app(*args)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _compile_middlewares(
|
|
58
|
+
items: Sequence[Middleware | DiMiddleware], container: AsyncContainer
|
|
59
|
+
) -> tuple[Middleware, ...]:
|
|
60
|
+
compiled: list[Middleware] = []
|
|
61
|
+
for item in items:
|
|
62
|
+
if isinstance(item, DiMiddleware):
|
|
63
|
+
handler = item.handler
|
|
64
|
+
if isinstance(handler, type):
|
|
65
|
+
parameter = inspect.Parameter("middleware", inspect.Parameter.KEYWORD_ONLY)
|
|
66
|
+
if _dependency_key(parameter, handler, container, Scope.REQUEST) is None:
|
|
67
|
+
raise FastApiAdapterError("DI middleware type is not registered")
|
|
68
|
+
function = getattr(handler, "invoke", None)
|
|
69
|
+
if function is None:
|
|
70
|
+
raise FastApiAdapterError("DI middleware requires async invoke")
|
|
71
|
+
unbound = _CallablePlan.compile(
|
|
72
|
+
function, container, Scope.REQUEST, business_parameters=5
|
|
73
|
+
)
|
|
74
|
+
plan = _CallablePlan(
|
|
75
|
+
unbound.signature.replace(
|
|
76
|
+
parameters=tuple(unbound.signature.parameters.values())[1:]
|
|
77
|
+
),
|
|
78
|
+
unbound.dependencies,
|
|
79
|
+
)
|
|
80
|
+
else:
|
|
81
|
+
plan = _CallablePlan.compile(
|
|
82
|
+
handler, container, Scope.REQUEST, business_parameters=4
|
|
83
|
+
)
|
|
84
|
+
if len(plan.signature.parameters) != 4:
|
|
85
|
+
raise FastApiAdapterError(
|
|
86
|
+
"DI middleware must accept scope, receive, send and call_next"
|
|
87
|
+
)
|
|
88
|
+
plan.signature.bind(None, None, None, None)
|
|
89
|
+
item = Middleware(_DiMiddleware, declaration=item, plan=plan)
|
|
90
|
+
elif not isinstance(item, Middleware):
|
|
91
|
+
raise FastApiAdapterError("Host middleware must be Middleware or DiMiddleware")
|
|
92
|
+
compiled.append(Middleware(_NativeMiddleware, middleware=item))
|
|
93
|
+
return tuple(compiled)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""HTTP Filter 改参后的验证边界;validator 由当前应用的路由编译结果拥有。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from typing import Annotated, Any, cast, get_args, get_origin
|
|
7
|
+
|
|
8
|
+
from dishka.integrations.base import default_parse_dependency
|
|
9
|
+
from fastapi.dependencies.models import Dependant
|
|
10
|
+
from fastapi.dependencies.utils import add_non_field_param_to_dependency
|
|
11
|
+
from fastapi.exceptions import RequestValidationError
|
|
12
|
+
from pydantic import ConfigDict, Field, TypeAdapter, ValidationError
|
|
13
|
+
from pydantic.fields import FieldInfo
|
|
14
|
+
from pydantic_core import CoreSchema, SchemaValidator
|
|
15
|
+
|
|
16
|
+
from ..application_services.execution import _InvocationArguments
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _revalidation_schema(value: Any) -> Any:
|
|
20
|
+
# 只复制 schema 的容器,不复制模型 class、validator 或默认业务对象。
|
|
21
|
+
# 显式覆盖每个嵌套模型/数据类,不能改变业务模型的全局配置。
|
|
22
|
+
if isinstance(value, dict):
|
|
23
|
+
result = {
|
|
24
|
+
key: item if key in ("default", "metadata", "config") else _revalidation_schema(item)
|
|
25
|
+
for key, item in value.items()
|
|
26
|
+
}
|
|
27
|
+
if result.get("type") in ("model", "dataclass"):
|
|
28
|
+
result["revalidate_instances"] = "always"
|
|
29
|
+
return result
|
|
30
|
+
if isinstance(value, list):
|
|
31
|
+
return [_revalidation_schema(item) for item in value]
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _HttpParameterValidator:
|
|
36
|
+
def __init__(self, signature: inspect.Signature) -> None:
|
|
37
|
+
self._signature = signature
|
|
38
|
+
self._validators: dict[str, SchemaValidator] = {}
|
|
39
|
+
for name, parameter in signature.parameters.items():
|
|
40
|
+
annotation = parameter.annotation
|
|
41
|
+
if isinstance(parameter.default, FieldInfo):
|
|
42
|
+
annotation = Annotated[annotation, parameter.default]
|
|
43
|
+
base = get_args(annotation)[0] if get_origin(annotation) is Annotated else annotation
|
|
44
|
+
if (
|
|
45
|
+
annotation is inspect.Parameter.empty
|
|
46
|
+
or default_parse_dependency(parameter, annotation) is not None
|
|
47
|
+
or add_non_field_param_to_dependency(
|
|
48
|
+
param_name=name, type_annotation=base, dependant=Dependant()
|
|
49
|
+
)
|
|
50
|
+
):
|
|
51
|
+
continue
|
|
52
|
+
if parameter.default is not inspect.Parameter.empty and not isinstance(
|
|
53
|
+
parameter.default, FieldInfo
|
|
54
|
+
):
|
|
55
|
+
annotation = Annotated[annotation, Field(default=parameter.default)]
|
|
56
|
+
adapter = TypeAdapter(
|
|
57
|
+
tuple[annotation], # type: ignore[valid-type]
|
|
58
|
+
config=ConfigDict(arbitrary_types_allowed=True),
|
|
59
|
+
)
|
|
60
|
+
self._validators[name] = SchemaValidator(
|
|
61
|
+
cast(CoreSchema, _revalidation_schema(adapter.core_schema)), _use_prebuilt=False
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def capture(self, actual: dict[str, object]) -> dict[str, _InvocationArguments]:
|
|
65
|
+
# 只追踪 HTTP 可验证参数;Request/DI 对象不参与 DTO 的变更判断。
|
|
66
|
+
snapshots = {}
|
|
67
|
+
for name in self._validators:
|
|
68
|
+
if name in actual:
|
|
69
|
+
arguments = _InvocationArguments((actual[name],), {})
|
|
70
|
+
arguments.mark_validated()
|
|
71
|
+
arguments.exposed = True
|
|
72
|
+
snapshots[name] = arguments
|
|
73
|
+
return snapshots
|
|
74
|
+
|
|
75
|
+
def validate_changes(
|
|
76
|
+
self, actual: dict[str, object], snapshots: dict[str, _InvocationArguments]
|
|
77
|
+
) -> None:
|
|
78
|
+
try:
|
|
79
|
+
self._signature.bind(**actual)
|
|
80
|
+
except TypeError:
|
|
81
|
+
raise RequestValidationError(
|
|
82
|
+
[{"loc": (), "msg": "Invalid action arguments", "type": "arguments"}]
|
|
83
|
+
) from None
|
|
84
|
+
try:
|
|
85
|
+
for name, validator in self._validators.items():
|
|
86
|
+
values = (actual[name],) if name in actual else ()
|
|
87
|
+
previous = snapshots.get(name)
|
|
88
|
+
if previous is not None:
|
|
89
|
+
if not values or previous.args[0] is not values[0]:
|
|
90
|
+
previous.args = values
|
|
91
|
+
if not previous.needs_validation:
|
|
92
|
+
continue
|
|
93
|
+
# 独立参数各自重验;改动 scalar 不能重复执行未变 DTO 的非幂等 validator。
|
|
94
|
+
# 已绑定实例内部使用字段名;Filter 替换输入仍接受原 schema 的别名。
|
|
95
|
+
actual[name] = validator.validate_python(values, by_name=True)[0]
|
|
96
|
+
except ValidationError as error:
|
|
97
|
+
raise RequestValidationError(error.errors()) from None
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""FastAPI-only managed WebSocket capability。"""
|
|
2
|
+
|
|
3
|
+
from .authentication import websocket_connection
|
|
4
|
+
from .connection import WebSocketConnection
|
|
5
|
+
from .module import FastApiRealtimeModule
|
|
6
|
+
from .options import FastApiRealtimeOptions
|
|
7
|
+
|
|
8
|
+
__all__ = (
|
|
9
|
+
"FastApiRealtimeModule",
|
|
10
|
+
"FastApiRealtimeOptions",
|
|
11
|
+
"WebSocketConnection",
|
|
12
|
+
"websocket_connection",
|
|
13
|
+
)
|