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,199 @@
|
|
|
1
|
+
"""一次调用的参数与扩展链执行;静态规则已在 build 冻结。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
7
|
+
from dataclasses import dataclass, field, fields, is_dataclass
|
|
8
|
+
from datetime import date, datetime, time, timedelta
|
|
9
|
+
from decimal import Decimal
|
|
10
|
+
from uuid import UUID
|
|
11
|
+
|
|
12
|
+
from dishka import AsyncContainer
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
from .catalog import ApplicationServiceMethod
|
|
16
|
+
from .errors import ApplicationInvocationRejectedError, CrossTaskInvocationError
|
|
17
|
+
from .interceptors import InterceptionStage, InterceptorRegistration
|
|
18
|
+
|
|
19
|
+
_ARGUMENT_SNAPSHOT_MAX_DEPTH = 64
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _InterceptorPipeline:
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
method: ApplicationServiceMethod,
|
|
26
|
+
steps: Mapping[InterceptionStage, tuple[InterceptorRegistration, ...]],
|
|
27
|
+
args: tuple[object, ...],
|
|
28
|
+
kwargs: dict[str, object],
|
|
29
|
+
) -> None:
|
|
30
|
+
self.method = method
|
|
31
|
+
self.steps = steps
|
|
32
|
+
self.arguments = _InvocationArguments(args, dict(kwargs))
|
|
33
|
+
|
|
34
|
+
async def run(
|
|
35
|
+
self,
|
|
36
|
+
stage: InterceptionStage,
|
|
37
|
+
container: AsyncContainer,
|
|
38
|
+
callback: Callable[[], Awaitable[object]],
|
|
39
|
+
) -> object:
|
|
40
|
+
registrations = self.steps[stage]
|
|
41
|
+
|
|
42
|
+
async def proceed(index: int) -> object:
|
|
43
|
+
if index == len(registrations):
|
|
44
|
+
return await callback()
|
|
45
|
+
interceptor = await container.get(registrations[index].interceptor)
|
|
46
|
+
invocation = MethodInvocation(self.method, self.arguments, lambda: proceed(index + 1))
|
|
47
|
+
try:
|
|
48
|
+
return await interceptor.intercept(invocation)
|
|
49
|
+
finally:
|
|
50
|
+
invocation._close()
|
|
51
|
+
|
|
52
|
+
return await proceed(0)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(slots=True)
|
|
56
|
+
class _InvocationArguments:
|
|
57
|
+
args: tuple[object, ...]
|
|
58
|
+
kwargs: dict[str, object]
|
|
59
|
+
_validated_args: tuple[object, ...] | None = field(default=None, init=False)
|
|
60
|
+
_validated_kwargs: dict[str, object] = field(default_factory=dict, init=False)
|
|
61
|
+
_validated_structure: object = field(default=None, init=False)
|
|
62
|
+
_validated_nodes: list[object] = field(default_factory=list, init=False)
|
|
63
|
+
exposed: bool = field(default=False, init=False)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def needs_validation(self) -> bool:
|
|
67
|
+
# 比较绑定值的身份,不调用业务对象的 __eq__;观察者不会重复执行非幂等 validator。
|
|
68
|
+
return (
|
|
69
|
+
self._validated_args is None
|
|
70
|
+
or len(self.args) != len(self._validated_args)
|
|
71
|
+
or any(
|
|
72
|
+
left is not right
|
|
73
|
+
for left, right in zip(self.args, self._validated_args, strict=True)
|
|
74
|
+
)
|
|
75
|
+
or self.kwargs.keys() != self._validated_kwargs.keys()
|
|
76
|
+
or any(value is not self._validated_kwargs[key] for key, value in self.kwargs.items())
|
|
77
|
+
or (self.exposed and self._structure([]) != self._validated_structure)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def mark_validated(self) -> None:
|
|
81
|
+
# 只在验证成功后记录规范化结果;快照与调用共同归属当前 pipeline。
|
|
82
|
+
self._validated_args = self.args
|
|
83
|
+
self._validated_kwargs = dict(self.kwargs)
|
|
84
|
+
self._validated_nodes = []
|
|
85
|
+
self._validated_structure = self._structure(self._validated_nodes)
|
|
86
|
+
# 不重置 exposed:早期扩展可能保留参数引用,并在后续阶段原地修改同一对象。
|
|
87
|
+
|
|
88
|
+
def _structure(self, retained: list[object]) -> object:
|
|
89
|
+
return (
|
|
90
|
+
_argument_structure(self.args, set(), retained),
|
|
91
|
+
_argument_structure(self.kwargs, set(), retained),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _argument_structure(value: object, active: set[int], retained: list[object]) -> object:
|
|
96
|
+
# 只检查原生值结构,不调用业务对象的比较、复制或序列化;保留引用防止 id 回收复用。
|
|
97
|
+
retained.append(value)
|
|
98
|
+
identity = id(value)
|
|
99
|
+
if type(value) in (
|
|
100
|
+
type(None),
|
|
101
|
+
bool,
|
|
102
|
+
int,
|
|
103
|
+
float,
|
|
104
|
+
complex,
|
|
105
|
+
str,
|
|
106
|
+
bytes,
|
|
107
|
+
date,
|
|
108
|
+
datetime,
|
|
109
|
+
time,
|
|
110
|
+
timedelta,
|
|
111
|
+
Decimal,
|
|
112
|
+
UUID,
|
|
113
|
+
):
|
|
114
|
+
return identity
|
|
115
|
+
# Any/object 的内部深度不受 Pydantic 约束;快照不能让合法输入因递归耗尽而失败。
|
|
116
|
+
if identity in active or len(active) >= _ARGUMENT_SNAPSHOT_MAX_DEPTH:
|
|
117
|
+
return object()
|
|
118
|
+
active.add(identity)
|
|
119
|
+
try:
|
|
120
|
+
if type(value) in (tuple, list):
|
|
121
|
+
assert isinstance(value, (tuple, list))
|
|
122
|
+
return identity, tuple(_argument_structure(item, active, retained) for item in value)
|
|
123
|
+
if type(value) is dict:
|
|
124
|
+
return identity, tuple(
|
|
125
|
+
(
|
|
126
|
+
_argument_structure(key, active, retained),
|
|
127
|
+
_argument_structure(item, active, retained),
|
|
128
|
+
)
|
|
129
|
+
for key, item in value.items()
|
|
130
|
+
)
|
|
131
|
+
if type(value) in (set, frozenset):
|
|
132
|
+
assert isinstance(value, (set, frozenset))
|
|
133
|
+
return identity, frozenset(
|
|
134
|
+
_argument_structure(item, active, retained) for item in value
|
|
135
|
+
)
|
|
136
|
+
if isinstance(value, BaseModel):
|
|
137
|
+
return (
|
|
138
|
+
identity,
|
|
139
|
+
_argument_structure(value.__dict__, active, retained),
|
|
140
|
+
_argument_structure(value.__pydantic_extra__, active, retained),
|
|
141
|
+
)
|
|
142
|
+
if is_dataclass(value) and not isinstance(value, type):
|
|
143
|
+
return identity, tuple(
|
|
144
|
+
(item.name, _argument_structure(getattr(value, item.name), active, retained))
|
|
145
|
+
for item in fields(value)
|
|
146
|
+
)
|
|
147
|
+
# 不透明对象、容器子类和循环无法证明未变时保守重验,不能用身份放行可变值。
|
|
148
|
+
return object()
|
|
149
|
+
finally:
|
|
150
|
+
active.remove(identity)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class MethodInvocation:
|
|
154
|
+
"""每个 continuation 独占一次许可;链共享参数,但不共享继续执行的状态。"""
|
|
155
|
+
|
|
156
|
+
def __init__(
|
|
157
|
+
self,
|
|
158
|
+
method: ApplicationServiceMethod,
|
|
159
|
+
arguments: _InvocationArguments,
|
|
160
|
+
continuation: Callable[[], Awaitable[object]],
|
|
161
|
+
) -> None:
|
|
162
|
+
self._method = method
|
|
163
|
+
self._arguments = arguments
|
|
164
|
+
self._continuation = continuation
|
|
165
|
+
self._task = asyncio.current_task()
|
|
166
|
+
self._consumed = False
|
|
167
|
+
self._active = True
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def method(self) -> ApplicationServiceMethod:
|
|
171
|
+
return self._method
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def args(self) -> tuple[object, ...]:
|
|
175
|
+
self._arguments.exposed = True
|
|
176
|
+
return self._arguments.args
|
|
177
|
+
|
|
178
|
+
@args.setter
|
|
179
|
+
def args(self, value: tuple[object, ...]) -> None:
|
|
180
|
+
self._arguments.exposed = True
|
|
181
|
+
self._arguments.args = value
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def kwargs(self) -> dict[str, object]:
|
|
185
|
+
self._arguments.exposed = True
|
|
186
|
+
return self._arguments.kwargs
|
|
187
|
+
|
|
188
|
+
async def proceed(self) -> object:
|
|
189
|
+
if asyncio.current_task() is not self._task:
|
|
190
|
+
raise CrossTaskInvocationError
|
|
191
|
+
if self._consumed or not self._active:
|
|
192
|
+
raise ApplicationInvocationRejectedError(
|
|
193
|
+
reason="interceptor continuation is no longer available"
|
|
194
|
+
)
|
|
195
|
+
self._consumed = True
|
|
196
|
+
return await self._continuation()
|
|
197
|
+
|
|
198
|
+
def _close(self) -> None:
|
|
199
|
+
self._active = False
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""拦截扩展契约及不可变注册事实;不依赖容器或 HTTP。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from ..diagnostics import SourceLocation
|
|
12
|
+
from ..modularity import ModuleKey
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from .catalog import ApplicationServiceMethod
|
|
16
|
+
from .execution import MethodInvocation
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class InterceptionStage(Enum):
|
|
20
|
+
TRACING = "tracing"
|
|
21
|
+
AUDITING = "auditing"
|
|
22
|
+
AUTHORIZATION = "authorization"
|
|
23
|
+
VALIDATION = "validation"
|
|
24
|
+
UNIT_OF_WORK = "unit_of_work"
|
|
25
|
+
METHOD = "method"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Interceptor(ABC):
|
|
29
|
+
@abstractmethod
|
|
30
|
+
async def intercept(self, invocation: MethodInvocation) -> object: ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class InterceptorRegistration:
|
|
35
|
+
interceptor: type[Interceptor]
|
|
36
|
+
before: InterceptionStage
|
|
37
|
+
owner: ModuleKey
|
|
38
|
+
source: SourceLocation
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True, slots=True)
|
|
42
|
+
class _InterceptorRule:
|
|
43
|
+
registration: InterceptorRegistration
|
|
44
|
+
match: Callable[[ApplicationServiceMethod], bool]
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
"""Application-local admission、调用身份、REQUEST 入场与现有 ACTION/UoW owner。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Iterator
|
|
8
|
+
from contextlib import asynccontextmanager, contextmanager
|
|
9
|
+
from contextvars import ContextVar, Token
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import TYPE_CHECKING, cast
|
|
12
|
+
from uuid import UUID, uuid4
|
|
13
|
+
|
|
14
|
+
from dishka import AsyncContainer, Scope
|
|
15
|
+
|
|
16
|
+
from ..authorization import CurrentUser
|
|
17
|
+
from ..invocation import ApplicationInvocationContext
|
|
18
|
+
from ..invocation.contracts import _is_safe_correlation_id
|
|
19
|
+
from ..lifecycle import ApplicationState
|
|
20
|
+
from ..unit_of_work import UnitOfWork, UnitOfWorkManager
|
|
21
|
+
from ..unit_of_work.options import UnitOfWorkDeclaration
|
|
22
|
+
from .catalog import (
|
|
23
|
+
ApplicationServiceCatalog,
|
|
24
|
+
)
|
|
25
|
+
from .errors import (
|
|
26
|
+
ApplicationInvocationCleanupError,
|
|
27
|
+
ApplicationInvocationRejectedError,
|
|
28
|
+
CrossTaskInvocationError,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
_LOGGER = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from opentelemetry.trace import Tracer
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(slots=True)
|
|
38
|
+
class _InvocationState:
|
|
39
|
+
context: ApplicationInvocationContext
|
|
40
|
+
owner_task: asyncio.Task[object]
|
|
41
|
+
action_container: AsyncContainer
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True, slots=True)
|
|
45
|
+
class _CallerContext:
|
|
46
|
+
current_user: CurrentUser
|
|
47
|
+
correlation_id: str
|
|
48
|
+
owner_task: asyncio.Task[object]
|
|
49
|
+
transferable: bool = False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True, slots=True)
|
|
53
|
+
class _OuterInvocationLease:
|
|
54
|
+
owner_task: asyncio.Task[object]
|
|
55
|
+
allows_invocation: bool
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True, slots=True)
|
|
59
|
+
class _CallScope:
|
|
60
|
+
container: AsyncContainer
|
|
61
|
+
owner_task: asyncio.Task[object]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class _InvocationRuntime:
|
|
65
|
+
"""每个 Application 独占 admission 与 ContextVar,禁止进程级 current app。"""
|
|
66
|
+
|
|
67
|
+
__slots__ = (
|
|
68
|
+
"_active",
|
|
69
|
+
"_ambient",
|
|
70
|
+
"_application_id",
|
|
71
|
+
"_authenticating",
|
|
72
|
+
"_call_scope",
|
|
73
|
+
"_caller",
|
|
74
|
+
"_condition",
|
|
75
|
+
"_http_method",
|
|
76
|
+
"_is_accepting",
|
|
77
|
+
"_lease",
|
|
78
|
+
"_state_getter",
|
|
79
|
+
"tracer",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
*,
|
|
85
|
+
application_id: UUID,
|
|
86
|
+
state_getter: Callable[[], ApplicationState],
|
|
87
|
+
) -> None:
|
|
88
|
+
self._application_id = application_id
|
|
89
|
+
self._authenticating: ContextVar[bool] = ContextVar(
|
|
90
|
+
f"python_platform_authenticating_{application_id}", default=False
|
|
91
|
+
)
|
|
92
|
+
self._http_method: ContextVar[str | None] = ContextVar(
|
|
93
|
+
f"python_platform_http_method_{application_id}", default=None
|
|
94
|
+
)
|
|
95
|
+
self.tracer: Tracer | None = None
|
|
96
|
+
self._state_getter = state_getter
|
|
97
|
+
self._ambient: ContextVar[_InvocationState | None] = ContextVar(
|
|
98
|
+
f"python_platform_invocation_{application_id}",
|
|
99
|
+
default=None,
|
|
100
|
+
)
|
|
101
|
+
self._caller: ContextVar[_CallerContext | None] = ContextVar(
|
|
102
|
+
f"python_platform_caller_{application_id}",
|
|
103
|
+
default=None,
|
|
104
|
+
)
|
|
105
|
+
self._lease: ContextVar[_OuterInvocationLease | None] = ContextVar(
|
|
106
|
+
f"python_platform_invocation_lease_{application_id}",
|
|
107
|
+
default=None,
|
|
108
|
+
)
|
|
109
|
+
self._condition = asyncio.Condition()
|
|
110
|
+
self._is_accepting = False
|
|
111
|
+
self._active = 0
|
|
112
|
+
self._call_scope: ContextVar[_CallScope | None] = ContextVar(
|
|
113
|
+
f"python_platform_call_scope_{application_id}",
|
|
114
|
+
default=None,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
@contextmanager
|
|
118
|
+
def call_scope(self, container: AsyncContainer) -> Iterator[None]:
|
|
119
|
+
token = self._call_scope.set(_CallScope(container, _current_task()))
|
|
120
|
+
try:
|
|
121
|
+
yield
|
|
122
|
+
finally:
|
|
123
|
+
self._call_scope.reset(token)
|
|
124
|
+
|
|
125
|
+
def current_call_scope(self) -> AsyncContainer | None:
|
|
126
|
+
scope = self._call_scope.get()
|
|
127
|
+
if scope is None:
|
|
128
|
+
return None
|
|
129
|
+
if scope.owner_task is not _current_task():
|
|
130
|
+
raise CrossTaskInvocationError
|
|
131
|
+
return scope.container
|
|
132
|
+
|
|
133
|
+
def http_method(self) -> str | None:
|
|
134
|
+
return self._http_method.get()
|
|
135
|
+
|
|
136
|
+
@contextmanager
|
|
137
|
+
def http_scope(self, method: str) -> Iterator[None]:
|
|
138
|
+
token = self._http_method.set(method)
|
|
139
|
+
try:
|
|
140
|
+
yield
|
|
141
|
+
finally:
|
|
142
|
+
self._http_method.reset(token)
|
|
143
|
+
|
|
144
|
+
@contextmanager
|
|
145
|
+
def detached_execution(self, container: AsyncContainer) -> Iterator[None]:
|
|
146
|
+
token = self._ambient.set(None)
|
|
147
|
+
try:
|
|
148
|
+
with self.call_scope(container):
|
|
149
|
+
yield
|
|
150
|
+
finally:
|
|
151
|
+
self._ambient.reset(token)
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def application_id(self) -> UUID:
|
|
155
|
+
return self._application_id
|
|
156
|
+
|
|
157
|
+
def start_accepting(self) -> None:
|
|
158
|
+
self._is_accepting = True
|
|
159
|
+
|
|
160
|
+
async def stop_accepting_and_wait(self) -> None:
|
|
161
|
+
async with self._condition:
|
|
162
|
+
self._is_accepting = False
|
|
163
|
+
await self._condition.wait_for(lambda: self._active == 0)
|
|
164
|
+
|
|
165
|
+
@asynccontextmanager
|
|
166
|
+
async def outer_lease(self, *, allow_composed_request: bool = False) -> AsyncIterator[None]:
|
|
167
|
+
owner_task = _current_task()
|
|
168
|
+
inherited = self._lease.get()
|
|
169
|
+
if inherited is not None:
|
|
170
|
+
if inherited.owner_task is not owner_task:
|
|
171
|
+
# asyncio.create_task 会复制 ContextVar;child task 不得继承 parent admission。
|
|
172
|
+
raise CrossTaskInvocationError
|
|
173
|
+
if not inherited.allows_invocation and not allow_composed_request:
|
|
174
|
+
raise ApplicationInvocationRejectedError(
|
|
175
|
+
reason=(
|
|
176
|
+
"Application must be running and accepting invocations, got "
|
|
177
|
+
f"{self._state_getter().value}"
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
yield
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
async with self._condition:
|
|
184
|
+
state = self._state_getter()
|
|
185
|
+
allows_invocation = self._is_accepting and state is ApplicationState.RUNNING
|
|
186
|
+
allows_composed_request = allow_composed_request and state is ApplicationState.COMPOSED
|
|
187
|
+
if not allows_invocation and not allows_composed_request:
|
|
188
|
+
raise ApplicationInvocationRejectedError(
|
|
189
|
+
reason=(
|
|
190
|
+
f"Application must be running and accepting invocations, got {state.value}"
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
self._active += 1
|
|
194
|
+
token = self._lease.set(_OuterInvocationLease(owner_task, allows_invocation))
|
|
195
|
+
try:
|
|
196
|
+
yield
|
|
197
|
+
finally:
|
|
198
|
+
self._lease.reset(token)
|
|
199
|
+
async with self._condition:
|
|
200
|
+
assert self._active > 0
|
|
201
|
+
self._active -= 1
|
|
202
|
+
if self._active == 0:
|
|
203
|
+
self._condition.notify_all()
|
|
204
|
+
|
|
205
|
+
def current(self) -> _InvocationState | None:
|
|
206
|
+
return self._ambient.get()
|
|
207
|
+
|
|
208
|
+
def bind(self, state: _InvocationState) -> Token[_InvocationState | None]:
|
|
209
|
+
return self._ambient.set(state)
|
|
210
|
+
|
|
211
|
+
def reset(self, token: Token[_InvocationState | None]) -> None:
|
|
212
|
+
self._ambient.reset(token)
|
|
213
|
+
|
|
214
|
+
def current_user(self) -> CurrentUser:
|
|
215
|
+
if self._authenticating.get():
|
|
216
|
+
raise ApplicationInvocationRejectedError(
|
|
217
|
+
reason="authentication cannot depend on the final CurrentUser"
|
|
218
|
+
)
|
|
219
|
+
caller = self._caller.get()
|
|
220
|
+
if caller is None:
|
|
221
|
+
return CurrentUser.anonymous()
|
|
222
|
+
if not caller.transferable and caller.owner_task is not _current_task():
|
|
223
|
+
raise CrossTaskInvocationError
|
|
224
|
+
return caller.current_user
|
|
225
|
+
|
|
226
|
+
def correlation_id(self) -> str | None:
|
|
227
|
+
caller = self._caller.get()
|
|
228
|
+
if caller is None:
|
|
229
|
+
return None
|
|
230
|
+
if not caller.transferable and caller.owner_task is not _current_task():
|
|
231
|
+
raise CrossTaskInvocationError
|
|
232
|
+
return caller.correlation_id
|
|
233
|
+
|
|
234
|
+
def diagnostic_correlation_id(self) -> str | None:
|
|
235
|
+
# 日志允许只读关联复制到线程的上下文;这不授予 child task/线程调用业务的 lease。
|
|
236
|
+
caller = self._caller.get()
|
|
237
|
+
return None if caller is None else caller.correlation_id
|
|
238
|
+
|
|
239
|
+
def bind_caller(
|
|
240
|
+
self,
|
|
241
|
+
current_user: CurrentUser,
|
|
242
|
+
correlation_id: str,
|
|
243
|
+
) -> Token[_CallerContext | None]:
|
|
244
|
+
if not isinstance(current_user, CurrentUser):
|
|
245
|
+
raise TypeError("current_user must be CurrentUser")
|
|
246
|
+
if not _is_safe_correlation_id(correlation_id):
|
|
247
|
+
raise ValueError("correlation_id must be a safe non-empty identifier")
|
|
248
|
+
return self._caller.set(
|
|
249
|
+
_CallerContext(
|
|
250
|
+
current_user=current_user,
|
|
251
|
+
correlation_id=correlation_id,
|
|
252
|
+
owner_task=_current_task(),
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def reset_caller(self, token: Token[_CallerContext | None]) -> None:
|
|
257
|
+
self._caller.reset(token)
|
|
258
|
+
|
|
259
|
+
@contextmanager
|
|
260
|
+
def authenticating(self) -> Iterator[None]:
|
|
261
|
+
token = self._authenticating.set(True)
|
|
262
|
+
try:
|
|
263
|
+
yield
|
|
264
|
+
finally:
|
|
265
|
+
self._authenticating.reset(token)
|
|
266
|
+
|
|
267
|
+
@contextmanager
|
|
268
|
+
def trusted_http_task(
|
|
269
|
+
self, container: AsyncContainer, user: CurrentUser, correlation_id: str, method: str
|
|
270
|
+
) -> Iterator[None]:
|
|
271
|
+
# 只有 adapter 的受监督入口可交接许可;普通 create_task 仍受原有 task guard 约束。
|
|
272
|
+
lease = self._lease.get()
|
|
273
|
+
if lease is None or self.current() is not None:
|
|
274
|
+
raise CrossTaskInvocationError
|
|
275
|
+
task = _current_task()
|
|
276
|
+
lease_token = self._lease.set(_OuterInvocationLease(task, lease.allows_invocation))
|
|
277
|
+
caller_token = self._caller.set(_CallerContext(user, correlation_id, task, True))
|
|
278
|
+
try:
|
|
279
|
+
with self.call_scope(container), self.http_scope(method):
|
|
280
|
+
yield
|
|
281
|
+
finally:
|
|
282
|
+
self._caller.reset(caller_token)
|
|
283
|
+
self._lease.reset(lease_token)
|
|
284
|
+
|
|
285
|
+
async def run_action(
|
|
286
|
+
self,
|
|
287
|
+
container: AsyncContainer,
|
|
288
|
+
*,
|
|
289
|
+
connection_name: str,
|
|
290
|
+
callback: Callable[[AsyncContainer], Awaitable[object]],
|
|
291
|
+
declaration: UnitOfWorkDeclaration,
|
|
292
|
+
method_name: str = "execute",
|
|
293
|
+
) -> object:
|
|
294
|
+
"""执行 scope 独立于数据库 UoW;自动边界只完成自己创建或加入的工作。"""
|
|
295
|
+
context = ApplicationInvocationContext(
|
|
296
|
+
application_id=self.application_id,
|
|
297
|
+
invocation_id=uuid4(),
|
|
298
|
+
parent_invocation_id=None,
|
|
299
|
+
connection_name=connection_name,
|
|
300
|
+
)
|
|
301
|
+
work: UnitOfWork | None = None
|
|
302
|
+
async with _managed_action(
|
|
303
|
+
container, context, committed=lambda: work is not None and work.committed
|
|
304
|
+
) as action:
|
|
305
|
+
state = _InvocationState(context, _current_task(), action)
|
|
306
|
+
token = self.bind(state)
|
|
307
|
+
try:
|
|
308
|
+
manager = await action.get(UnitOfWorkManager)
|
|
309
|
+
if declaration.disabled:
|
|
310
|
+
manager._check_current(connection_name)
|
|
311
|
+
return await callback(action)
|
|
312
|
+
options = manager._resolve_options(connection_name, declaration, method_name)
|
|
313
|
+
async with manager._begin(options) as work:
|
|
314
|
+
result = await callback(action)
|
|
315
|
+
await work.complete()
|
|
316
|
+
return result
|
|
317
|
+
finally:
|
|
318
|
+
self.reset(token)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@asynccontextmanager
|
|
322
|
+
async def _managed_action(
|
|
323
|
+
parent: AsyncContainer,
|
|
324
|
+
context: ApplicationInvocationContext,
|
|
325
|
+
*,
|
|
326
|
+
committed: Callable[[], bool],
|
|
327
|
+
) -> AsyncIterator[AsyncContainer]:
|
|
328
|
+
action = parent(context={ApplicationInvocationContext: context}, scope=Scope.ACTION)
|
|
329
|
+
primary: BaseException | None = None
|
|
330
|
+
try:
|
|
331
|
+
yield action
|
|
332
|
+
except BaseException as error:
|
|
333
|
+
primary = error
|
|
334
|
+
primary, failures = await _close_action(action, primary)
|
|
335
|
+
_raise_scope_failure(primary, failures, committed())
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
async def _close_action(
|
|
339
|
+
action: AsyncContainer, primary: BaseException | None
|
|
340
|
+
) -> tuple[BaseException | None, tuple[BaseException, ...]]:
|
|
341
|
+
# Dishka generator 可能拥有 ContextVar token,进入与退出必须保持同一 task。
|
|
342
|
+
# 具体 provider 负责保护自己的原生 I/O(SQL UoW 已在 scope 关闭前回收);
|
|
343
|
+
# finalizer 的 BaseException 仍需继续原生 close,穷尽其余资源。
|
|
344
|
+
failures: list[BaseException] = []
|
|
345
|
+
while True:
|
|
346
|
+
try:
|
|
347
|
+
await action.close(primary)
|
|
348
|
+
return primary, tuple(failures)
|
|
349
|
+
except asyncio.CancelledError as cancellation:
|
|
350
|
+
if primary is None:
|
|
351
|
+
primary = cancellation
|
|
352
|
+
else:
|
|
353
|
+
failures.append(cancellation)
|
|
354
|
+
except BaseException as error:
|
|
355
|
+
failures.append(error)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _raise_scope_failure(
|
|
359
|
+
primary: BaseException | None, failures: tuple[BaseException, ...], committed: bool
|
|
360
|
+
) -> None:
|
|
361
|
+
if failures:
|
|
362
|
+
inherited = primary.__cause__ if isinstance(primary, asyncio.CancelledError) else primary
|
|
363
|
+
if isinstance(inherited, ApplicationInvocationCleanupError):
|
|
364
|
+
failures = (*inherited.cleanup_failures, *failures)
|
|
365
|
+
if not isinstance(primary, asyncio.CancelledError):
|
|
366
|
+
primary = inherited.primary
|
|
367
|
+
failure = ApplicationInvocationCleanupError(
|
|
368
|
+
primary=primary, cleanup_failures=failures, committed=committed
|
|
369
|
+
)
|
|
370
|
+
if isinstance(primary, asyncio.CancelledError):
|
|
371
|
+
raise primary from failure
|
|
372
|
+
raise failure from (primary or failures[0])
|
|
373
|
+
if primary is not None:
|
|
374
|
+
raise primary
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class _ScopedApplicationServiceInvoker:
|
|
378
|
+
"""从 Application root 或其 SESSION 建立 REQUEST,不向 caller 暴露 resolver。"""
|
|
379
|
+
|
|
380
|
+
__slots__ = ("_catalog", "_root_container", "_runtime")
|
|
381
|
+
|
|
382
|
+
def __init__(
|
|
383
|
+
self,
|
|
384
|
+
container: AsyncContainer,
|
|
385
|
+
runtime: _InvocationRuntime,
|
|
386
|
+
catalog: ApplicationServiceCatalog,
|
|
387
|
+
) -> None:
|
|
388
|
+
self._root_container = container
|
|
389
|
+
self._runtime = runtime
|
|
390
|
+
self._catalog = catalog
|
|
391
|
+
|
|
392
|
+
async def invoke(
|
|
393
|
+
self,
|
|
394
|
+
parent: AsyncContainer,
|
|
395
|
+
current_user: CurrentUser,
|
|
396
|
+
correlation_id: str,
|
|
397
|
+
method: Callable[..., Awaitable[object]],
|
|
398
|
+
args: tuple[object, ...],
|
|
399
|
+
kwargs: dict[str, object],
|
|
400
|
+
) -> object:
|
|
401
|
+
self._require_owned_parent(parent)
|
|
402
|
+
service, compiled = self._catalog._resolve_method(method)
|
|
403
|
+
async with self._runtime.outer_lease():
|
|
404
|
+
caller_token = self._runtime.bind_caller(current_user, correlation_id)
|
|
405
|
+
try:
|
|
406
|
+
async with parent(scope=Scope.REQUEST) as request_container:
|
|
407
|
+
proxy = await request_container.get(service.registration.contract)
|
|
408
|
+
call = cast(
|
|
409
|
+
Callable[..., Awaitable[object]],
|
|
410
|
+
getattr(proxy, compiled.name),
|
|
411
|
+
)
|
|
412
|
+
return await call(*args, **kwargs)
|
|
413
|
+
finally:
|
|
414
|
+
self._runtime.reset_caller(caller_token)
|
|
415
|
+
|
|
416
|
+
def _require_owned_parent(self, parent: AsyncContainer) -> None:
|
|
417
|
+
if parent is self._root_container:
|
|
418
|
+
return
|
|
419
|
+
if parent.scope is not Scope.SESSION or parent.parent_container is not self._root_container:
|
|
420
|
+
raise RuntimeError("scoped invocation parent is not owned by this Application")
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _current_task() -> asyncio.Task[object]:
|
|
424
|
+
task = asyncio.current_task()
|
|
425
|
+
if task is None:
|
|
426
|
+
raise RuntimeError("Application Service invocation requires an asyncio Task")
|
|
427
|
+
return cast(asyncio.Task[object], task)
|