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,49 @@
|
|
|
1
|
+
"""Optional FastAPI adapter public capability;不由 Kernel 根包重导出。"""
|
|
2
|
+
|
|
3
|
+
from . import http as http
|
|
4
|
+
from .adapter import FastApiAdapter
|
|
5
|
+
from .contracts import (
|
|
6
|
+
ApplicationServiceRouteOverride,
|
|
7
|
+
RouterCatalog,
|
|
8
|
+
RouterRegistration,
|
|
9
|
+
WebSocketRouteRegistration,
|
|
10
|
+
contributes_routers,
|
|
11
|
+
exposes_application_services,
|
|
12
|
+
)
|
|
13
|
+
from .errors import FastApiAdapterError, RouteConflictError
|
|
14
|
+
from .filters import ActionFilterContext, ServiceFilter, service_filter
|
|
15
|
+
from .http import remote_service
|
|
16
|
+
from .http_router import HttpRouter
|
|
17
|
+
from .middleware import DiMiddleware
|
|
18
|
+
from .realtime import (
|
|
19
|
+
FastApiRealtimeModule,
|
|
20
|
+
FastApiRealtimeOptions,
|
|
21
|
+
WebSocketConnection,
|
|
22
|
+
websocket_connection,
|
|
23
|
+
)
|
|
24
|
+
from .upload_limits import UploadLimits, upload_limits
|
|
25
|
+
|
|
26
|
+
__all__ = (
|
|
27
|
+
"ActionFilterContext",
|
|
28
|
+
"ApplicationServiceRouteOverride",
|
|
29
|
+
"DiMiddleware",
|
|
30
|
+
"FastApiAdapter",
|
|
31
|
+
"FastApiAdapterError",
|
|
32
|
+
"FastApiRealtimeModule",
|
|
33
|
+
"FastApiRealtimeOptions",
|
|
34
|
+
"HttpRouter",
|
|
35
|
+
"RouteConflictError",
|
|
36
|
+
"RouterCatalog",
|
|
37
|
+
"RouterRegistration",
|
|
38
|
+
"ServiceFilter",
|
|
39
|
+
"UploadLimits",
|
|
40
|
+
"WebSocketConnection",
|
|
41
|
+
"WebSocketRouteRegistration",
|
|
42
|
+
"contributes_routers",
|
|
43
|
+
"exposes_application_services",
|
|
44
|
+
"http",
|
|
45
|
+
"remote_service",
|
|
46
|
+
"service_filter",
|
|
47
|
+
"upload_limits",
|
|
48
|
+
"websocket_connection",
|
|
49
|
+
)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""原生 function exit stack 上的 HTTP 调用 owner;响应发送前完成并释放资源。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import inspect
|
|
7
|
+
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
8
|
+
from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager
|
|
9
|
+
from types import TracebackType
|
|
10
|
+
from typing import Annotated, TypeVar, get_args, get_origin
|
|
11
|
+
from uuid import uuid4
|
|
12
|
+
|
|
13
|
+
from anyio import CancelScope
|
|
14
|
+
from dishka import AsyncContainer
|
|
15
|
+
from starlette.requests import Request
|
|
16
|
+
|
|
17
|
+
from ..application_services.contracts import _DEFAULT_CONNECTION_NAME
|
|
18
|
+
from ..application_services.invocation import _InvocationRuntime, _InvocationState, _managed_action
|
|
19
|
+
from ..invocation import ApplicationInvocationContext
|
|
20
|
+
from ..unit_of_work import UnitOfWork, UnitOfWorkManager
|
|
21
|
+
from ..unit_of_work.options import UnitOfWorkDeclaration
|
|
22
|
+
from .errors import FastApiAdapterError
|
|
23
|
+
from .filters import ActionFilterContext, ServiceFilter, _run_filters
|
|
24
|
+
from .transfer import _manage_stream
|
|
25
|
+
|
|
26
|
+
_T = TypeVar("_T")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _FunctionDependencyStack(AsyncExitStack):
|
|
30
|
+
def __init__(self, owner: _HttpActionOwner) -> None:
|
|
31
|
+
super().__init__()
|
|
32
|
+
self._owner = owner
|
|
33
|
+
|
|
34
|
+
async def enter_async_context(self, cm: AbstractAsyncContextManager[_T, bool | None]) -> _T:
|
|
35
|
+
@asynccontextmanager
|
|
36
|
+
async def observe() -> AsyncIterator[_T]:
|
|
37
|
+
try:
|
|
38
|
+
async with cm as value:
|
|
39
|
+
yield value
|
|
40
|
+
except BaseException as error:
|
|
41
|
+
# 保存每个依赖退出失败的事实,不能让更外层依赖吞异常后误提交。
|
|
42
|
+
self._owner.failure = error
|
|
43
|
+
raise
|
|
44
|
+
|
|
45
|
+
return await super().enter_async_context(observe())
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class _HttpActionOwner:
|
|
49
|
+
def __init__(
|
|
50
|
+
self, request: Request, container: AsyncContainer, runtime: _InvocationRuntime
|
|
51
|
+
) -> None:
|
|
52
|
+
self.request = request
|
|
53
|
+
self.container = container
|
|
54
|
+
self.runtime = runtime
|
|
55
|
+
self.stack = AsyncExitStack()
|
|
56
|
+
self.work: UnitOfWork | None = None
|
|
57
|
+
self.action: AsyncContainer | None = None
|
|
58
|
+
self.response_ready = False
|
|
59
|
+
self.failure: BaseException | None = None
|
|
60
|
+
self.short_circuited = False
|
|
61
|
+
self.audits: list[Callable[[BaseException | None], Awaitable[None]]] = []
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
async def install(cls, request: Request) -> _HttpActionOwner:
|
|
65
|
+
# 唯一版本耦合点:原生函数栈尚未登记本次 Depends,因此 owner 最后退出。
|
|
66
|
+
native_stack = request.scope.get("fastapi_function_astack")
|
|
67
|
+
if not isinstance(native_stack, AsyncExitStack):
|
|
68
|
+
raise FastApiAdapterError("native FastAPI function exit stack is unavailable")
|
|
69
|
+
container = request.state.dishka_container
|
|
70
|
+
owner = cls(request, container, await container.get(_InvocationRuntime))
|
|
71
|
+
native_stack.callback(request.scope.__setitem__, "fastapi_function_astack", native_stack)
|
|
72
|
+
native_stack.push_async_exit(owner.finish)
|
|
73
|
+
observed = await native_stack.enter_async_context(_FunctionDependencyStack(owner))
|
|
74
|
+
request.scope["fastapi_function_astack"] = observed
|
|
75
|
+
request.state.python_platform_action = owner
|
|
76
|
+
return owner
|
|
77
|
+
|
|
78
|
+
async def activate(
|
|
79
|
+
self,
|
|
80
|
+
declaration: UnitOfWorkDeclaration,
|
|
81
|
+
method_name: str,
|
|
82
|
+
connection_name: str = _DEFAULT_CONNECTION_NAME,
|
|
83
|
+
) -> AsyncContainer:
|
|
84
|
+
if self.action is not None:
|
|
85
|
+
raise FastApiAdapterError("HTTP action was already activated")
|
|
86
|
+
context = ApplicationInvocationContext(
|
|
87
|
+
application_id=self.runtime.application_id,
|
|
88
|
+
invocation_id=uuid4(),
|
|
89
|
+
parent_invocation_id=None,
|
|
90
|
+
connection_name=connection_name,
|
|
91
|
+
)
|
|
92
|
+
action = await self.stack.enter_async_context(
|
|
93
|
+
_managed_action(
|
|
94
|
+
self.container,
|
|
95
|
+
context,
|
|
96
|
+
committed=lambda: self.work is not None and self.work.committed,
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
self.action = action
|
|
100
|
+
task = asyncio.current_task()
|
|
101
|
+
assert task is not None
|
|
102
|
+
token = self.runtime.bind(_InvocationState(context, task, action))
|
|
103
|
+
self.stack.callback(self.runtime.reset, token)
|
|
104
|
+
manager = await action.get(UnitOfWorkManager)
|
|
105
|
+
if declaration.disabled:
|
|
106
|
+
manager._check_current(connection_name)
|
|
107
|
+
else:
|
|
108
|
+
self.work = await self.stack.enter_async_context(
|
|
109
|
+
manager._begin(manager._resolve_options(connection_name, declaration, method_name))
|
|
110
|
+
)
|
|
111
|
+
return action
|
|
112
|
+
|
|
113
|
+
async def invoke(
|
|
114
|
+
self,
|
|
115
|
+
endpoint: object,
|
|
116
|
+
values: dict[str, object],
|
|
117
|
+
filters: tuple[ServiceFilter, ...],
|
|
118
|
+
callback: Callable[[dict[str, object]], Awaitable[object]],
|
|
119
|
+
) -> object:
|
|
120
|
+
assert self.action is not None
|
|
121
|
+
executed = False
|
|
122
|
+
|
|
123
|
+
async def target() -> object:
|
|
124
|
+
nonlocal executed
|
|
125
|
+
executed = True
|
|
126
|
+
result = await callback(values)
|
|
127
|
+
# 接受业务移交后立即登记,后置 Filter 或响应准备失败也不能遗失 iterator。
|
|
128
|
+
_manage_stream(self.request, result)
|
|
129
|
+
return result
|
|
130
|
+
|
|
131
|
+
result = await _run_filters(
|
|
132
|
+
filters, self.action, ActionFilterContext(self.request, endpoint, values), target
|
|
133
|
+
)
|
|
134
|
+
self.short_circuited = not executed
|
|
135
|
+
return result
|
|
136
|
+
|
|
137
|
+
async def finish(
|
|
138
|
+
self,
|
|
139
|
+
exc_type: type[BaseException] | None,
|
|
140
|
+
error: BaseException | None,
|
|
141
|
+
traceback: TracebackType | None,
|
|
142
|
+
) -> bool:
|
|
143
|
+
primary = error or self.failure
|
|
144
|
+
try:
|
|
145
|
+
try:
|
|
146
|
+
if primary is None and self.response_ready and self.work is not None:
|
|
147
|
+
await self.work.complete()
|
|
148
|
+
except BaseException as failure:
|
|
149
|
+
primary = failure
|
|
150
|
+
with CancelScope(shield=True):
|
|
151
|
+
await self.stack.__aexit__(type(primary) if primary else None, primary, traceback)
|
|
152
|
+
except BaseException as failure:
|
|
153
|
+
primary = failure
|
|
154
|
+
finally:
|
|
155
|
+
with CancelScope(shield=True):
|
|
156
|
+
for audit in self.audits:
|
|
157
|
+
await audit(primary)
|
|
158
|
+
self.request.state.python_platform_action = None
|
|
159
|
+
if primary is not None:
|
|
160
|
+
raise primary
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _request_parameter(parameters: tuple[inspect.Parameter, ...]) -> tuple[inspect.Parameter, bool]:
|
|
165
|
+
for parameter in parameters:
|
|
166
|
+
annotation = parameter.annotation
|
|
167
|
+
base = get_args(annotation)[0] if get_origin(annotation) is Annotated else annotation
|
|
168
|
+
# Request 注入只接受具体类型,不将参数化注解当作可注入的 Request 子类。
|
|
169
|
+
if get_origin(base) is None and isinstance(base, type) and issubclass(base, Request):
|
|
170
|
+
return parameter, False
|
|
171
|
+
name = "_python_platform_request"
|
|
172
|
+
names = {parameter.name for parameter in parameters}
|
|
173
|
+
while name in names:
|
|
174
|
+
name = "_" + name
|
|
175
|
+
return inspect.Parameter(name, inspect.Parameter.KEYWORD_ONLY, annotation=Request), True
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"""Application lifecycle、Dishka FastAPI integration 与 HTTP surface 的单次装配。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import AsyncIterator, Iterator, Sequence
|
|
7
|
+
from contextlib import asynccontextmanager, contextmanager
|
|
8
|
+
from typing import Any, cast, get_args
|
|
9
|
+
|
|
10
|
+
from dishka.exceptions import NoFactoryError
|
|
11
|
+
from fastapi import FastAPI
|
|
12
|
+
from fastapi.exception_handlers import request_validation_exception_handler
|
|
13
|
+
from fastapi.exceptions import RequestValidationError
|
|
14
|
+
from pydantic_core.core_schema import ErrorType
|
|
15
|
+
from starlette.middleware import Middleware
|
|
16
|
+
from starlette.requests import Request
|
|
17
|
+
from starlette.responses import JSONResponse
|
|
18
|
+
|
|
19
|
+
from ..application import Application
|
|
20
|
+
from ..application_services import ApplicationServiceValidationError
|
|
21
|
+
from ..authorization import PermissionDeniedError, UnauthenticatedError
|
|
22
|
+
from ..errors import BusinessError, FrameworkError, ResourceConflictError, ResourceNotFoundError
|
|
23
|
+
from ..lifecycle import ApplicationState
|
|
24
|
+
from .contracts import RouterCatalog
|
|
25
|
+
from .errors import FastApiAdapterError
|
|
26
|
+
from .filters import ServiceFilter, _validate_filters
|
|
27
|
+
from .middleware import DiMiddleware, _compile_middlewares
|
|
28
|
+
from .realtime.runtime import _FastApiRealtimeRuntime
|
|
29
|
+
from .request_context import (
|
|
30
|
+
_RequestContextMiddleware,
|
|
31
|
+
_RequestScopeMiddleware,
|
|
32
|
+
_RequestTaskMiddleware,
|
|
33
|
+
)
|
|
34
|
+
from .route_integrity import RouteFingerprint, _route_fingerprint
|
|
35
|
+
from .routing import (
|
|
36
|
+
_build_route_plan,
|
|
37
|
+
_RouteInstallationPlan,
|
|
38
|
+
_verify_native_openapi,
|
|
39
|
+
)
|
|
40
|
+
from .tracing import _install_tracing, _tracing_rollback
|
|
41
|
+
from .upload_limits import UploadLimits
|
|
42
|
+
|
|
43
|
+
_LOGGER = logging.getLogger(__name__)
|
|
44
|
+
_INSTALLATION_MARKER = "_python_platform_fastapi_installed"
|
|
45
|
+
_DISHKA_CONTAINER_BRIDGE = "dishka_container"
|
|
46
|
+
_SAFE_REQUEST_LOCATION_ROOTS = frozenset({"body", "cookie", "header", "path", "query"})
|
|
47
|
+
_TRUSTED_VALIDATION_ERROR_TYPES = frozenset(get_args(ErrorType))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class FastApiAdapter:
|
|
51
|
+
"""把一个 composed Application 安装到一个 FastAPI instance。"""
|
|
52
|
+
|
|
53
|
+
__slots__ = (
|
|
54
|
+
"_application",
|
|
55
|
+
"_catalog",
|
|
56
|
+
"_filters",
|
|
57
|
+
"_installed_app",
|
|
58
|
+
"_installed_middleware",
|
|
59
|
+
"_installed_stack",
|
|
60
|
+
"_middleware",
|
|
61
|
+
"_route_fingerprint",
|
|
62
|
+
"_upload_limits",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
application: Application,
|
|
68
|
+
*,
|
|
69
|
+
middleware: Sequence[Middleware | DiMiddleware] = (),
|
|
70
|
+
filters: Sequence[ServiceFilter] = (),
|
|
71
|
+
upload_limits: UploadLimits | None = None,
|
|
72
|
+
) -> None:
|
|
73
|
+
if not isinstance(application, Application):
|
|
74
|
+
raise TypeError("application must be python_platform.Application")
|
|
75
|
+
self._filters = tuple(filters)
|
|
76
|
+
if upload_limits is not None and not isinstance(upload_limits, UploadLimits):
|
|
77
|
+
raise TypeError("upload_limits must be UploadLimits")
|
|
78
|
+
self._upload_limits = UploadLimits() if upload_limits is None else upload_limits
|
|
79
|
+
self._middleware = tuple(middleware)
|
|
80
|
+
self._installed_middleware: tuple[Middleware, ...] = ()
|
|
81
|
+
self._installed_stack: _RequestScopeMiddleware | None = None
|
|
82
|
+
self._application = application
|
|
83
|
+
self._catalog: RouterCatalog | None = None
|
|
84
|
+
self._installed_app: FastAPI | None = None
|
|
85
|
+
self._route_fingerprint: RouteFingerprint | None = None
|
|
86
|
+
|
|
87
|
+
def install(self, app: FastAPI) -> RouterCatalog:
|
|
88
|
+
self._validate_install_target(app)
|
|
89
|
+
middlewares = _compile_middlewares(
|
|
90
|
+
(*self._middleware, *app.user_middleware),
|
|
91
|
+
self._application._container_for_integration(),
|
|
92
|
+
)
|
|
93
|
+
_validate_filters(self._filters, self._application._container_for_integration())
|
|
94
|
+
plan = _build_route_plan(app, self._application)
|
|
95
|
+
|
|
96
|
+
with _installation_state(app), _tracing_rollback(app):
|
|
97
|
+
stack = self._apply_installation(app, plan, middlewares)
|
|
98
|
+
fingerprint = _route_fingerprint(app.routes)
|
|
99
|
+
setattr(app.state, _INSTALLATION_MARKER, str(self._application.id))
|
|
100
|
+
|
|
101
|
+
# 安装状态仅在所有原生构造与校验成功后发布;失败可以修正配置后重试。
|
|
102
|
+
self._installed_stack = stack
|
|
103
|
+
self._installed_middleware = tuple(app.user_middleware)
|
|
104
|
+
self._catalog = plan.catalog
|
|
105
|
+
self._installed_app = app
|
|
106
|
+
self._route_fingerprint = fingerprint
|
|
107
|
+
return plan.catalog
|
|
108
|
+
|
|
109
|
+
def _apply_installation(
|
|
110
|
+
self, app: FastAPI, plan: _RouteInstallationPlan, middlewares: tuple[Middleware, ...]
|
|
111
|
+
) -> _RequestScopeMiddleware:
|
|
112
|
+
for router in plan.routers:
|
|
113
|
+
app.include_router(router)
|
|
114
|
+
app.include_router(plan.health_router)
|
|
115
|
+
if (
|
|
116
|
+
BusinessError not in app.exception_handlers
|
|
117
|
+
and FrameworkError not in app.exception_handlers
|
|
118
|
+
):
|
|
119
|
+
app.add_exception_handler(BusinessError, _business_error_handler)
|
|
120
|
+
if (
|
|
121
|
+
ApplicationServiceValidationError not in app.exception_handlers
|
|
122
|
+
and FrameworkError not in app.exception_handlers
|
|
123
|
+
):
|
|
124
|
+
app.add_exception_handler(
|
|
125
|
+
ApplicationServiceValidationError,
|
|
126
|
+
_application_service_validation_handler,
|
|
127
|
+
)
|
|
128
|
+
if (
|
|
129
|
+
UnauthenticatedError not in app.exception_handlers
|
|
130
|
+
and FrameworkError not in app.exception_handlers
|
|
131
|
+
):
|
|
132
|
+
app.add_exception_handler(UnauthenticatedError, _unauthenticated_handler)
|
|
133
|
+
if (
|
|
134
|
+
PermissionDeniedError not in app.exception_handlers
|
|
135
|
+
and FrameworkError not in app.exception_handlers
|
|
136
|
+
):
|
|
137
|
+
app.add_exception_handler(PermissionDeniedError, _permission_denied_handler)
|
|
138
|
+
if (
|
|
139
|
+
ResourceConflictError not in app.exception_handlers
|
|
140
|
+
and FrameworkError not in app.exception_handlers
|
|
141
|
+
):
|
|
142
|
+
app.add_exception_handler(ResourceConflictError, _resource_conflict_handler)
|
|
143
|
+
if (
|
|
144
|
+
ResourceNotFoundError not in app.exception_handlers
|
|
145
|
+
and FrameworkError not in app.exception_handlers
|
|
146
|
+
):
|
|
147
|
+
app.add_exception_handler(ResourceNotFoundError, _resource_not_found_handler)
|
|
148
|
+
if FrameworkError not in app.exception_handlers:
|
|
149
|
+
app.add_exception_handler(FrameworkError, _framework_error_handler)
|
|
150
|
+
request_validation_handler = app.exception_handlers.get(RequestValidationError)
|
|
151
|
+
if request_validation_handler in (None, request_validation_exception_handler):
|
|
152
|
+
app.add_exception_handler(RequestValidationError, _request_validation_handler)
|
|
153
|
+
|
|
154
|
+
app.state.python_platform_filters = self._filters
|
|
155
|
+
app.state.python_platform_upload_limits = self._upload_limits
|
|
156
|
+
app.state.dishka_container = self._application._container_for_integration()
|
|
157
|
+
app.user_middleware = [*middlewares, Middleware(_RequestTaskMiddleware)]
|
|
158
|
+
app.add_middleware(_RequestContextMiddleware, application=self._application)
|
|
159
|
+
_install_tracing(self._application, app)
|
|
160
|
+
app.openapi_schema = None
|
|
161
|
+
_verify_native_openapi(app, plan.schema_candidates)
|
|
162
|
+
|
|
163
|
+
# lease/REQUEST 包住原生 ServerErrorMiddleware;未处理异常的 500 发送也在 drain 内。
|
|
164
|
+
# 直接组装原生栈,不复制错误处理或替换全局 routing 函数。
|
|
165
|
+
stack = _RequestScopeMiddleware(app.build_middleware_stack(), application=self._application)
|
|
166
|
+
app.middleware_stack = stack
|
|
167
|
+
return stack
|
|
168
|
+
|
|
169
|
+
@asynccontextmanager
|
|
170
|
+
async def lifespan(self, app: FastAPI) -> AsyncIterator[None]:
|
|
171
|
+
try:
|
|
172
|
+
self._validate_start_target(app)
|
|
173
|
+
except BaseException:
|
|
174
|
+
if self._installed_app is app:
|
|
175
|
+
try:
|
|
176
|
+
if self._application.state is ApplicationState.COMPOSED:
|
|
177
|
+
await self._application.stop()
|
|
178
|
+
finally:
|
|
179
|
+
self._clear_container_bridge(app)
|
|
180
|
+
raise
|
|
181
|
+
try:
|
|
182
|
+
await self._application.start()
|
|
183
|
+
except BaseException:
|
|
184
|
+
self._clear_container_bridge(app)
|
|
185
|
+
raise
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
yield
|
|
189
|
+
finally:
|
|
190
|
+
try:
|
|
191
|
+
await self._shutdown_realtime()
|
|
192
|
+
finally:
|
|
193
|
+
try:
|
|
194
|
+
await self._application.stop()
|
|
195
|
+
finally:
|
|
196
|
+
# Container 的 close 始终属于 Application;这里只撤销 Web 引用。
|
|
197
|
+
self._clear_container_bridge(app)
|
|
198
|
+
|
|
199
|
+
def _validate_install_target(self, app: FastAPI) -> None:
|
|
200
|
+
if not isinstance(app, FastAPI):
|
|
201
|
+
raise TypeError("app must be fastapi.FastAPI")
|
|
202
|
+
if self._installed_app is not None:
|
|
203
|
+
raise FastApiAdapterError("one adapter can only be installed once")
|
|
204
|
+
if hasattr(app.state, _INSTALLATION_MARKER):
|
|
205
|
+
raise FastApiAdapterError("FastAPI instance already has a Python Platform adapter")
|
|
206
|
+
if hasattr(app.state, _DISHKA_CONTAINER_BRIDGE):
|
|
207
|
+
raise FastApiAdapterError("FastAPI instance already has a Dishka container bridge")
|
|
208
|
+
if app.middleware_stack is not None:
|
|
209
|
+
raise FastApiAdapterError("FastAPI middleware stack was already built before install")
|
|
210
|
+
if self._application.state is not ApplicationState.COMPOSED:
|
|
211
|
+
raise FastApiAdapterError(
|
|
212
|
+
f"install requires composed Application, got {self._application.state.value}"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def _validate_start_target(self, app: FastAPI) -> None:
|
|
216
|
+
if self._installed_app is not app:
|
|
217
|
+
raise FastApiAdapterError(
|
|
218
|
+
"lifespan app is not this adapter's installed FastAPI instance"
|
|
219
|
+
)
|
|
220
|
+
if getattr(app.state, _INSTALLATION_MARKER, None) != str(self._application.id):
|
|
221
|
+
raise FastApiAdapterError("FastAPI installation marker changed before startup")
|
|
222
|
+
if app.state.python_platform_filters != self._filters:
|
|
223
|
+
raise FastApiAdapterError("HTTP filters changed after adapter installation")
|
|
224
|
+
if tuple(app.user_middleware) != self._installed_middleware:
|
|
225
|
+
raise FastApiAdapterError("FastAPI middleware changed after adapter installation")
|
|
226
|
+
if app.middleware_stack is not self._installed_stack:
|
|
227
|
+
raise FastApiAdapterError("FastAPI middleware stack changed after adapter installation")
|
|
228
|
+
expected_fingerprint = self._route_fingerprint
|
|
229
|
+
if expected_fingerprint is None or _route_fingerprint(app.routes) != expected_fingerprint:
|
|
230
|
+
raise FastApiAdapterError("FastAPI routes changed after adapter installation")
|
|
231
|
+
try:
|
|
232
|
+
container = self._application._container_for_integration()
|
|
233
|
+
except RuntimeError as original_error:
|
|
234
|
+
raise FastApiAdapterError("Application container is unavailable before startup") from (
|
|
235
|
+
original_error
|
|
236
|
+
)
|
|
237
|
+
if getattr(app.state, _DISHKA_CONTAINER_BRIDGE, None) is not container:
|
|
238
|
+
raise FastApiAdapterError("Dishka container bridge changed before startup")
|
|
239
|
+
|
|
240
|
+
@staticmethod
|
|
241
|
+
def _clear_container_bridge(app: FastAPI) -> None:
|
|
242
|
+
if hasattr(app.state, _DISHKA_CONTAINER_BRIDGE):
|
|
243
|
+
delattr(app.state, _DISHKA_CONTAINER_BRIDGE)
|
|
244
|
+
|
|
245
|
+
async def _shutdown_realtime(self) -> None:
|
|
246
|
+
try:
|
|
247
|
+
container = self._application._container_for_integration()
|
|
248
|
+
except RuntimeError:
|
|
249
|
+
return
|
|
250
|
+
try:
|
|
251
|
+
runtime = await container.get(_FastApiRealtimeRuntime)
|
|
252
|
+
except NoFactoryError:
|
|
253
|
+
return
|
|
254
|
+
await runtime.shutdown()
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@contextmanager
|
|
258
|
+
def _installation_state(app: FastAPI) -> Iterator[None]:
|
|
259
|
+
# 原生 include_router 还修改 lifespan、回调和路由缓存;从实例状态发现这些字段,
|
|
260
|
+
# 保留既有 list/dict 身份,不维护随 FastAPI 版本漂移的字段第二清单。
|
|
261
|
+
namespaces = (vars(app), vars(app.router), vars(app.state))
|
|
262
|
+
snapshots = [(namespace, dict(namespace)) for namespace in namespaces]
|
|
263
|
+
collections: list[tuple[Any, Any]] = [
|
|
264
|
+
(value, value.copy())
|
|
265
|
+
for _, snapshot in snapshots
|
|
266
|
+
for value in snapshot.values()
|
|
267
|
+
if type(value) in (dict, list)
|
|
268
|
+
]
|
|
269
|
+
try:
|
|
270
|
+
yield
|
|
271
|
+
except BaseException:
|
|
272
|
+
for value, original in collections:
|
|
273
|
+
if isinstance(value, list):
|
|
274
|
+
value[:] = original
|
|
275
|
+
else:
|
|
276
|
+
value.clear()
|
|
277
|
+
value.update(original)
|
|
278
|
+
for namespace, snapshot in snapshots:
|
|
279
|
+
namespace.clear()
|
|
280
|
+
namespace.update(snapshot)
|
|
281
|
+
raise
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
async def _business_error_handler(request: Request, error: Exception) -> JSONResponse:
|
|
285
|
+
business_error = cast(BusinessError, error)
|
|
286
|
+
return JSONResponse(
|
|
287
|
+
status_code=400,
|
|
288
|
+
content={
|
|
289
|
+
"error": {
|
|
290
|
+
"code": business_error.definition.code,
|
|
291
|
+
"message": business_error.definition.message,
|
|
292
|
+
"fields": [field.name for field in business_error.fields],
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
async def _framework_error_handler(request: Request, error: Exception) -> JSONResponse:
|
|
299
|
+
framework_error = cast(FrameworkError, error)
|
|
300
|
+
_LOGGER.error(
|
|
301
|
+
"Unhandled FrameworkError for %s %s",
|
|
302
|
+
request.method,
|
|
303
|
+
request.url.path,
|
|
304
|
+
exc_info=(type(framework_error), framework_error, framework_error.__traceback__),
|
|
305
|
+
)
|
|
306
|
+
return JSONResponse(status_code=500, content={"detail": "Internal Server Error"})
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
async def _application_service_validation_handler(
|
|
310
|
+
request: Request,
|
|
311
|
+
error: Exception,
|
|
312
|
+
) -> JSONResponse:
|
|
313
|
+
validation_error = cast(ApplicationServiceValidationError, error)
|
|
314
|
+
return JSONResponse(
|
|
315
|
+
status_code=422,
|
|
316
|
+
content={
|
|
317
|
+
"detail": [
|
|
318
|
+
{
|
|
319
|
+
"loc": list(issue.location),
|
|
320
|
+
"msg": issue.message,
|
|
321
|
+
"type": issue.error_type,
|
|
322
|
+
}
|
|
323
|
+
for issue in validation_error.issues
|
|
324
|
+
]
|
|
325
|
+
},
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
async def _request_validation_handler(request: Request, error: Exception) -> JSONResponse:
|
|
330
|
+
validation_error = cast(RequestValidationError, error)
|
|
331
|
+
return JSONResponse(
|
|
332
|
+
status_code=422,
|
|
333
|
+
content={
|
|
334
|
+
"detail": [
|
|
335
|
+
{
|
|
336
|
+
"loc": list(_safe_request_validation_location(detail.get("loc"))),
|
|
337
|
+
"msg": "Input validation failed",
|
|
338
|
+
"type": _trusted_request_validation_error_type(detail.get("type")),
|
|
339
|
+
}
|
|
340
|
+
for detail in validation_error.errors()
|
|
341
|
+
]
|
|
342
|
+
},
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _safe_request_validation_location(location: object) -> tuple[str, ...]:
|
|
347
|
+
if (
|
|
348
|
+
isinstance(location, tuple)
|
|
349
|
+
and location
|
|
350
|
+
and isinstance(location[0], str)
|
|
351
|
+
and location[0] in _SAFE_REQUEST_LOCATION_ROOTS
|
|
352
|
+
):
|
|
353
|
+
# 后续 segment 可能来自用户控制的 mapping key;只发布 FastAPI 自有位置类别。
|
|
354
|
+
return (location[0],)
|
|
355
|
+
return ()
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _trusted_request_validation_error_type(error_type: object) -> str:
|
|
359
|
+
if isinstance(error_type, str) and error_type in _TRUSTED_VALIDATION_ERROR_TYPES:
|
|
360
|
+
return error_type
|
|
361
|
+
return "value_error"
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
async def _unauthenticated_handler(request: Request, error: Exception) -> JSONResponse:
|
|
365
|
+
return JSONResponse(
|
|
366
|
+
status_code=401,
|
|
367
|
+
content={"detail": "Not authenticated"},
|
|
368
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
async def _permission_denied_handler(request: Request, error: Exception) -> JSONResponse:
|
|
373
|
+
return JSONResponse(status_code=403, content={"detail": "Forbidden"})
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
async def _resource_conflict_handler(request: Request, error: Exception) -> JSONResponse:
|
|
377
|
+
return JSONResponse(status_code=409, content={"detail": "Resource conflict"})
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
async def _resource_not_found_handler(request: Request, error: Exception) -> JSONResponse:
|
|
381
|
+
return JSONResponse(status_code=404, content={"detail": "Resource not found"})
|