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,420 @@
|
|
|
1
|
+
"""Hosted Service 到 Application Service pipeline 的限流、跨线程调用桥。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import concurrent.futures
|
|
7
|
+
import functools
|
|
8
|
+
import inspect
|
|
9
|
+
import threading
|
|
10
|
+
from collections.abc import Awaitable, Callable
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Concatenate, ParamSpec, TypeVar, cast
|
|
13
|
+
from uuid import uuid4
|
|
14
|
+
|
|
15
|
+
from dishka import AsyncContainer
|
|
16
|
+
|
|
17
|
+
from ..application_services import ApplicationServiceCatalog, ApplicationServiceContract
|
|
18
|
+
from ..application_services.invocation import _InvocationRuntime, _ScopedApplicationServiceInvoker
|
|
19
|
+
from ..authorization import CurrentUser
|
|
20
|
+
from ..invocation.contracts import _is_safe_correlation_id
|
|
21
|
+
from ..invocation.function_runtime import _invoke_callable
|
|
22
|
+
from .errors import HostedServiceInvocationRejectedError
|
|
23
|
+
|
|
24
|
+
_P = ParamSpec("_P")
|
|
25
|
+
_R = TypeVar("_R")
|
|
26
|
+
_TContract = TypeVar("_TContract", bound=ApplicationServiceContract)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(slots=True)
|
|
30
|
+
class _PendingInvocation:
|
|
31
|
+
external_future: concurrent.futures.Future[object] | None
|
|
32
|
+
task: asyncio.Task[object] | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _HostedServiceBridge:
|
|
36
|
+
"""每个 Runtime 独占 admission/capacity,pending 对象保留到 loop Task 真正结束。"""
|
|
37
|
+
|
|
38
|
+
__slots__ = (
|
|
39
|
+
"_accepting",
|
|
40
|
+
"_catalog",
|
|
41
|
+
"_container",
|
|
42
|
+
"_limit",
|
|
43
|
+
"_lock",
|
|
44
|
+
"_loop",
|
|
45
|
+
"_name",
|
|
46
|
+
"_pending",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
name: str,
|
|
53
|
+
loop: asyncio.AbstractEventLoop,
|
|
54
|
+
container: AsyncContainer,
|
|
55
|
+
catalog: ApplicationServiceCatalog,
|
|
56
|
+
limit: int,
|
|
57
|
+
) -> None:
|
|
58
|
+
self._name = name
|
|
59
|
+
self._loop = loop
|
|
60
|
+
self._container = container
|
|
61
|
+
self._catalog = catalog
|
|
62
|
+
self._limit = limit
|
|
63
|
+
self._lock = threading.RLock()
|
|
64
|
+
self._pending: dict[object, _PendingInvocation] = {}
|
|
65
|
+
self._accepting = False
|
|
66
|
+
|
|
67
|
+
def open(self) -> None:
|
|
68
|
+
with self._lock:
|
|
69
|
+
self._accepting = True
|
|
70
|
+
|
|
71
|
+
def close(self) -> None:
|
|
72
|
+
with self._lock:
|
|
73
|
+
self._accepting = False
|
|
74
|
+
|
|
75
|
+
async def invoke(
|
|
76
|
+
self,
|
|
77
|
+
correlation_id: str,
|
|
78
|
+
method: Callable[..., Awaitable[_R]],
|
|
79
|
+
args: tuple[object, ...],
|
|
80
|
+
kwargs: dict[str, object],
|
|
81
|
+
*,
|
|
82
|
+
callable_entry: bool = False,
|
|
83
|
+
) -> _R:
|
|
84
|
+
operation = "invoke"
|
|
85
|
+
self._require_application_loop(operation)
|
|
86
|
+
self._validate_call(operation, correlation_id, method, callable_entry=callable_entry)
|
|
87
|
+
token = object()
|
|
88
|
+
with self._lock:
|
|
89
|
+
self._admit(token, operation, external_future=None)
|
|
90
|
+
task = asyncio.create_task(
|
|
91
|
+
self._invoke_pipeline(
|
|
92
|
+
correlation_id, method, args, kwargs, callable_entry=callable_entry
|
|
93
|
+
),
|
|
94
|
+
name=f"python-platform-hosted-service-invoke:{self._name}",
|
|
95
|
+
)
|
|
96
|
+
self._pending[token].task = task
|
|
97
|
+
task.add_done_callback(functools.partial(self._release_task, token))
|
|
98
|
+
return cast(_R, await task)
|
|
99
|
+
|
|
100
|
+
def submit(
|
|
101
|
+
self,
|
|
102
|
+
correlation_id: str,
|
|
103
|
+
method: Callable[..., Awaitable[_R]],
|
|
104
|
+
args: tuple[object, ...],
|
|
105
|
+
kwargs: dict[str, object],
|
|
106
|
+
*,
|
|
107
|
+
callable_entry: bool = False,
|
|
108
|
+
) -> concurrent.futures.Future[_R]:
|
|
109
|
+
operation = "submit"
|
|
110
|
+
self._require_external_thread(operation)
|
|
111
|
+
self._validate_call(operation, correlation_id, method, callable_entry=callable_entry)
|
|
112
|
+
token = object()
|
|
113
|
+
with self._lock:
|
|
114
|
+
self._admit(token, operation, external_future=None)
|
|
115
|
+
invocation = self._invoke_submitted(
|
|
116
|
+
token, correlation_id, method, args, kwargs, callable_entry=callable_entry
|
|
117
|
+
)
|
|
118
|
+
try:
|
|
119
|
+
external = asyncio.run_coroutine_threadsafe(
|
|
120
|
+
invocation,
|
|
121
|
+
self._loop,
|
|
122
|
+
)
|
|
123
|
+
except RuntimeError:
|
|
124
|
+
invocation.close()
|
|
125
|
+
self._pending.pop(token, None)
|
|
126
|
+
raise HostedServiceInvocationRejectedError(
|
|
127
|
+
service=self._name,
|
|
128
|
+
operation=operation,
|
|
129
|
+
error_type="EventLoopUnavailable",
|
|
130
|
+
) from None
|
|
131
|
+
pending = self._pending[token]
|
|
132
|
+
pending.external_future = external
|
|
133
|
+
external.add_done_callback(functools.partial(self._observe_external_completion, token))
|
|
134
|
+
return cast(concurrent.futures.Future[_R], external)
|
|
135
|
+
|
|
136
|
+
async def drain(self, timeout: float) -> None:
|
|
137
|
+
"""deadline 前自然 drain;超时后取消 loop Task 并等待 UoW/finalizer 收口。"""
|
|
138
|
+
|
|
139
|
+
self._require_application_loop("drain")
|
|
140
|
+
deadline = self._loop.time() + timeout
|
|
141
|
+
if await self._wait_for_natural_drain(deadline):
|
|
142
|
+
return
|
|
143
|
+
await self._cancel_pending_and_wait()
|
|
144
|
+
|
|
145
|
+
async def _wait_for_natural_drain(self, deadline: float) -> bool:
|
|
146
|
+
while True:
|
|
147
|
+
pending = self._pending_snapshot()
|
|
148
|
+
if not pending:
|
|
149
|
+
return True
|
|
150
|
+
tasks = tuple(item.task for item in pending if item.task is not None)
|
|
151
|
+
remaining = deadline - self._loop.time()
|
|
152
|
+
if remaining <= 0:
|
|
153
|
+
return False
|
|
154
|
+
if not tasks:
|
|
155
|
+
# submit 已占容量但 call_soon_threadsafe callback 尚未建立 Task。
|
|
156
|
+
await asyncio.sleep(0)
|
|
157
|
+
continue
|
|
158
|
+
await asyncio.wait(tasks, timeout=remaining)
|
|
159
|
+
|
|
160
|
+
async def _cancel_pending_and_wait(self) -> None:
|
|
161
|
+
pending = self._pending_snapshot()
|
|
162
|
+
for item in pending:
|
|
163
|
+
if item.external_future is not None:
|
|
164
|
+
item.external_future.cancel()
|
|
165
|
+
if item.task is not None:
|
|
166
|
+
item.task.cancel()
|
|
167
|
+
|
|
168
|
+
# Task cancellation 完成后,invocation owner 才确认 rollback/finalizer 已结束。
|
|
169
|
+
while True:
|
|
170
|
+
pending = self._pending_snapshot()
|
|
171
|
+
if not pending:
|
|
172
|
+
return
|
|
173
|
+
tasks = tuple(item.task for item in pending if item.task is not None)
|
|
174
|
+
if tasks:
|
|
175
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
176
|
+
else:
|
|
177
|
+
await asyncio.sleep(0)
|
|
178
|
+
|
|
179
|
+
def _admit(
|
|
180
|
+
self,
|
|
181
|
+
token: object,
|
|
182
|
+
operation: str,
|
|
183
|
+
*,
|
|
184
|
+
external_future: concurrent.futures.Future[object] | None,
|
|
185
|
+
) -> None:
|
|
186
|
+
if not self._accepting:
|
|
187
|
+
raise HostedServiceInvocationRejectedError(
|
|
188
|
+
service=self._name,
|
|
189
|
+
operation=operation,
|
|
190
|
+
error_type="NotAccepting",
|
|
191
|
+
)
|
|
192
|
+
if len(self._pending) >= self._limit:
|
|
193
|
+
raise HostedServiceInvocationRejectedError(
|
|
194
|
+
service=self._name,
|
|
195
|
+
operation=operation,
|
|
196
|
+
error_type="CapacityExceeded",
|
|
197
|
+
)
|
|
198
|
+
self._pending[token] = _PendingInvocation(external_future=external_future)
|
|
199
|
+
|
|
200
|
+
async def _invoke_submitted(
|
|
201
|
+
self,
|
|
202
|
+
token: object,
|
|
203
|
+
correlation_id: str,
|
|
204
|
+
method: Callable[..., Awaitable[object]],
|
|
205
|
+
args: tuple[object, ...],
|
|
206
|
+
kwargs: dict[str, object],
|
|
207
|
+
*,
|
|
208
|
+
callable_entry: bool = False,
|
|
209
|
+
) -> object:
|
|
210
|
+
task = asyncio.current_task()
|
|
211
|
+
assert task is not None
|
|
212
|
+
with self._lock:
|
|
213
|
+
pending = self._pending.get(token)
|
|
214
|
+
assert pending is not None
|
|
215
|
+
pending.task = cast(asyncio.Task[object], task)
|
|
216
|
+
try:
|
|
217
|
+
return await self._invoke_pipeline(
|
|
218
|
+
correlation_id, method, args, kwargs, callable_entry=callable_entry
|
|
219
|
+
)
|
|
220
|
+
finally:
|
|
221
|
+
# concurrent Future 可先进入 cancelled;只有 loop Task finalizer 完成才释放容量。
|
|
222
|
+
with self._lock:
|
|
223
|
+
self._pending.pop(token, None)
|
|
224
|
+
|
|
225
|
+
def _release_task(self, token: object, task: asyncio.Task[object]) -> None:
|
|
226
|
+
del task
|
|
227
|
+
with self._lock:
|
|
228
|
+
self._pending.pop(token, None)
|
|
229
|
+
|
|
230
|
+
def _observe_external_completion(
|
|
231
|
+
self,
|
|
232
|
+
token: object,
|
|
233
|
+
external: concurrent.futures.Future[object],
|
|
234
|
+
) -> None:
|
|
235
|
+
if not external.cancelled():
|
|
236
|
+
return
|
|
237
|
+
try:
|
|
238
|
+
self._loop.call_soon_threadsafe(
|
|
239
|
+
self._defer_cancelled_release,
|
|
240
|
+
token,
|
|
241
|
+
external,
|
|
242
|
+
)
|
|
243
|
+
except RuntimeError:
|
|
244
|
+
# loop 已关闭时 coroutine 不可能再启动;只释放仍无 Task 的 reservation。
|
|
245
|
+
self._release_cancelled_if_unstarted(token, external)
|
|
246
|
+
|
|
247
|
+
def _defer_cancelled_release(
|
|
248
|
+
self,
|
|
249
|
+
token: object,
|
|
250
|
+
external: concurrent.futures.Future[object],
|
|
251
|
+
) -> None:
|
|
252
|
+
# run_coroutine_threadsafe 的 callback 可能刚创建 Task;再让一轮以观察其启动或取消。
|
|
253
|
+
self._loop.call_soon(self._release_cancelled_if_unstarted, token, external)
|
|
254
|
+
|
|
255
|
+
def _release_cancelled_if_unstarted(
|
|
256
|
+
self,
|
|
257
|
+
token: object,
|
|
258
|
+
external: concurrent.futures.Future[object],
|
|
259
|
+
) -> None:
|
|
260
|
+
with self._lock:
|
|
261
|
+
pending = self._pending.get(token)
|
|
262
|
+
if (
|
|
263
|
+
pending is not None
|
|
264
|
+
and pending.external_future is external
|
|
265
|
+
and pending.task is None
|
|
266
|
+
and external.cancelled()
|
|
267
|
+
):
|
|
268
|
+
self._pending.pop(token, None)
|
|
269
|
+
|
|
270
|
+
async def _invoke_pipeline(
|
|
271
|
+
self,
|
|
272
|
+
correlation_id: str,
|
|
273
|
+
method: Callable[..., Awaitable[object]],
|
|
274
|
+
args: tuple[object, ...],
|
|
275
|
+
kwargs: dict[str, object],
|
|
276
|
+
*,
|
|
277
|
+
callable_entry: bool = False,
|
|
278
|
+
) -> object:
|
|
279
|
+
if callable_entry:
|
|
280
|
+
service = await self._container.get(_InvocationRuntime)
|
|
281
|
+
return await _invoke_callable(
|
|
282
|
+
self._container,
|
|
283
|
+
service,
|
|
284
|
+
CurrentUser.anonymous(),
|
|
285
|
+
correlation_id,
|
|
286
|
+
method,
|
|
287
|
+
args,
|
|
288
|
+
kwargs,
|
|
289
|
+
)
|
|
290
|
+
invoker = await self._container.get(_ScopedApplicationServiceInvoker)
|
|
291
|
+
return await invoker.invoke(
|
|
292
|
+
self._container,
|
|
293
|
+
CurrentUser.anonymous(),
|
|
294
|
+
correlation_id,
|
|
295
|
+
method,
|
|
296
|
+
args,
|
|
297
|
+
kwargs,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
def _validate_call(
|
|
301
|
+
self,
|
|
302
|
+
operation: str,
|
|
303
|
+
correlation_id: str,
|
|
304
|
+
method: object,
|
|
305
|
+
*,
|
|
306
|
+
callable_entry: bool = False,
|
|
307
|
+
) -> None:
|
|
308
|
+
if not _is_safe_correlation_id(correlation_id):
|
|
309
|
+
raise HostedServiceInvocationRejectedError(
|
|
310
|
+
service=self._name,
|
|
311
|
+
operation=operation,
|
|
312
|
+
error_type="InvalidCorrelationId",
|
|
313
|
+
)
|
|
314
|
+
# immutable catalog 可安全在 SDK thread 先验校验,非法 method 不占用容量。
|
|
315
|
+
if callable_entry:
|
|
316
|
+
if not inspect.iscoroutinefunction(method):
|
|
317
|
+
raise TypeError(
|
|
318
|
+
"HostedService callable must be an async function or bound async method"
|
|
319
|
+
)
|
|
320
|
+
else:
|
|
321
|
+
self._catalog._resolve_method(method)
|
|
322
|
+
|
|
323
|
+
def _require_application_loop(self, operation: str) -> None:
|
|
324
|
+
if asyncio.get_running_loop() is not self._loop:
|
|
325
|
+
raise HostedServiceInvocationRejectedError(
|
|
326
|
+
service=self._name,
|
|
327
|
+
operation=operation,
|
|
328
|
+
error_type="WrongEventLoop",
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
def _require_external_thread(self, operation: str) -> None:
|
|
332
|
+
try:
|
|
333
|
+
loop = asyncio.get_running_loop()
|
|
334
|
+
except RuntimeError:
|
|
335
|
+
return
|
|
336
|
+
if loop is self._loop:
|
|
337
|
+
raise HostedServiceInvocationRejectedError(
|
|
338
|
+
service=self._name,
|
|
339
|
+
operation=operation,
|
|
340
|
+
error_type="ApplicationEventLoop",
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
def _pending_snapshot(self) -> tuple[_PendingInvocation, ...]:
|
|
344
|
+
with self._lock:
|
|
345
|
+
return tuple(self._pending.values())
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@dataclass(frozen=True, slots=True)
|
|
349
|
+
class HostedServiceContext:
|
|
350
|
+
"""Runtime 的最小 capability;不暴露 Application、container 或 event loop。"""
|
|
351
|
+
|
|
352
|
+
name: str
|
|
353
|
+
_bridge: _HostedServiceBridge = field(repr=False, compare=False)
|
|
354
|
+
|
|
355
|
+
async def call(
|
|
356
|
+
self,
|
|
357
|
+
function: Callable[..., Awaitable[_R]],
|
|
358
|
+
/,
|
|
359
|
+
*args: object,
|
|
360
|
+
**kwargs: object,
|
|
361
|
+
) -> _R:
|
|
362
|
+
return await self._bridge.invoke(str(uuid4()), function, args, kwargs, callable_entry=True)
|
|
363
|
+
|
|
364
|
+
def submit_call(
|
|
365
|
+
self,
|
|
366
|
+
function: Callable[..., Awaitable[_R]],
|
|
367
|
+
/,
|
|
368
|
+
*args: object,
|
|
369
|
+
**kwargs: object,
|
|
370
|
+
) -> concurrent.futures.Future[_R]:
|
|
371
|
+
"""外部线程继续经过同一 admission/drain bridge,不直接接触异步容器。"""
|
|
372
|
+
return self._bridge.submit(str(uuid4()), function, args, kwargs, callable_entry=True)
|
|
373
|
+
|
|
374
|
+
async def invoke(
|
|
375
|
+
self,
|
|
376
|
+
method: Callable[Concatenate[_TContract, _P], Awaitable[_R]],
|
|
377
|
+
/,
|
|
378
|
+
*args: _P.args,
|
|
379
|
+
**kwargs: _P.kwargs,
|
|
380
|
+
) -> _R:
|
|
381
|
+
return await self.invoke_correlated(str(uuid4()), method, *args, **kwargs)
|
|
382
|
+
|
|
383
|
+
async def invoke_correlated(
|
|
384
|
+
self,
|
|
385
|
+
correlation_id: str,
|
|
386
|
+
method: Callable[Concatenate[_TContract, _P], Awaitable[_R]],
|
|
387
|
+
/,
|
|
388
|
+
*args: _P.args,
|
|
389
|
+
**kwargs: _P.kwargs,
|
|
390
|
+
) -> _R:
|
|
391
|
+
return await self._bridge.invoke(
|
|
392
|
+
correlation_id,
|
|
393
|
+
method,
|
|
394
|
+
cast(tuple[object, ...], args),
|
|
395
|
+
cast(dict[str, object], kwargs),
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
def submit(
|
|
399
|
+
self,
|
|
400
|
+
method: Callable[Concatenate[_TContract, _P], Awaitable[_R]],
|
|
401
|
+
/,
|
|
402
|
+
*args: _P.args,
|
|
403
|
+
**kwargs: _P.kwargs,
|
|
404
|
+
) -> concurrent.futures.Future[_R]:
|
|
405
|
+
return self.submit_correlated(str(uuid4()), method, *args, **kwargs)
|
|
406
|
+
|
|
407
|
+
def submit_correlated(
|
|
408
|
+
self,
|
|
409
|
+
correlation_id: str,
|
|
410
|
+
method: Callable[Concatenate[_TContract, _P], Awaitable[_R]],
|
|
411
|
+
/,
|
|
412
|
+
*args: _P.args,
|
|
413
|
+
**kwargs: _P.kwargs,
|
|
414
|
+
) -> concurrent.futures.Future[_R]:
|
|
415
|
+
return self._bridge.submit(
|
|
416
|
+
correlation_id,
|
|
417
|
+
method,
|
|
418
|
+
cast(tuple[object, ...], args),
|
|
419
|
+
cast(dict[str, object], kwargs),
|
|
420
|
+
)
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Module-owned Hosted Service metadata 的有序、不可变 Application catalog。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import Iterator, Mapping
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from types import MappingProxyType
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from dishka import DEFAULT_COMPONENT, DependencyKey, Provider, Scope
|
|
13
|
+
|
|
14
|
+
from ..diagnostics import SourceLocation
|
|
15
|
+
from ..modularity import AppModule, ModuleKey, ModuleRegistry
|
|
16
|
+
from .contracts import HostedService, _validate_service_type
|
|
17
|
+
from .errors import HostedServiceContractError, HostedServiceDefinitionError
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from ..services.arbitration import _RegistrationDraft
|
|
21
|
+
from ..services.contribution import ServiceContributionRegistry, _ContributionEntry
|
|
22
|
+
from ..services.provider import _CollectionDefinition, _FactoryDefinition
|
|
23
|
+
from ..services.registration import RegistrationKind
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True)
|
|
27
|
+
class HostedServiceRegistration:
|
|
28
|
+
service_type: type[HostedService]
|
|
29
|
+
name: str
|
|
30
|
+
owner: ModuleKey
|
|
31
|
+
source: SourceLocation
|
|
32
|
+
declaration_index: int
|
|
33
|
+
run_in_background_processes: bool
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class HostedServiceCatalog:
|
|
37
|
+
"""顺序来自 Module dependency order 与各 Module 的声明顺序。"""
|
|
38
|
+
|
|
39
|
+
__slots__ = ("_by_name", "_declarations", "_fingerprint", "_registrations")
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
registrations: tuple[HostedServiceRegistration, ...],
|
|
44
|
+
*,
|
|
45
|
+
declarations: tuple[tuple[type[AppModule], tuple[type[HostedService], ...]], ...],
|
|
46
|
+
) -> None:
|
|
47
|
+
self._registrations = registrations
|
|
48
|
+
self._by_name: Mapping[str, HostedServiceRegistration] = MappingProxyType(
|
|
49
|
+
{item.name: item for item in registrations}
|
|
50
|
+
)
|
|
51
|
+
self._declarations = declarations
|
|
52
|
+
encoded = json.dumps(
|
|
53
|
+
[
|
|
54
|
+
{
|
|
55
|
+
"name": item.name,
|
|
56
|
+
"service_type": (
|
|
57
|
+
f"{item.service_type.__module__}.{item.service_type.__qualname__}"
|
|
58
|
+
),
|
|
59
|
+
"owner": str(item.owner),
|
|
60
|
+
"declaration_index": item.declaration_index,
|
|
61
|
+
"run_in_background_processes": item.run_in_background_processes,
|
|
62
|
+
}
|
|
63
|
+
for item in registrations
|
|
64
|
+
],
|
|
65
|
+
sort_keys=True,
|
|
66
|
+
separators=(",", ":"),
|
|
67
|
+
).encode()
|
|
68
|
+
self._fingerprint = hashlib.sha256(encoded).hexdigest()
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def registrations(self) -> tuple[HostedServiceRegistration, ...]:
|
|
72
|
+
return self._registrations
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def service_types(self) -> tuple[type[HostedService], ...]:
|
|
76
|
+
return tuple(item.service_type for item in self._registrations)
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def fingerprint(self) -> str:
|
|
80
|
+
return self._fingerprint
|
|
81
|
+
|
|
82
|
+
def __iter__(self) -> Iterator[str]:
|
|
83
|
+
return iter(self._by_name)
|
|
84
|
+
|
|
85
|
+
def get(self, name: str) -> HostedServiceRegistration | None:
|
|
86
|
+
registration = self._by_name.get(name)
|
|
87
|
+
return registration
|
|
88
|
+
|
|
89
|
+
def owner_for(self, name: str) -> ModuleKey | None:
|
|
90
|
+
registration = self._by_name.get(name)
|
|
91
|
+
return registration.owner if registration is not None else None
|
|
92
|
+
|
|
93
|
+
def _assert_unchanged(self) -> None:
|
|
94
|
+
for item in self._registrations:
|
|
95
|
+
if (
|
|
96
|
+
item.service_type.run_in_background_processes
|
|
97
|
+
is not item.run_in_background_processes
|
|
98
|
+
):
|
|
99
|
+
raise HostedServiceDefinitionError(
|
|
100
|
+
service=item.name,
|
|
101
|
+
reason="HostedService process metadata changed after build",
|
|
102
|
+
)
|
|
103
|
+
for module_type, declared in self._declarations:
|
|
104
|
+
if module_type.__dict__.get("hosted_services", ()) is not declared:
|
|
105
|
+
raise HostedServiceDefinitionError(
|
|
106
|
+
service=f"{module_type.__module__}.{module_type.__qualname__}",
|
|
107
|
+
reason="Module hosted_services metadata changed after build",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _build_hosted_service_catalog(registry: ModuleRegistry) -> HostedServiceCatalog:
|
|
112
|
+
registrations: list[HostedServiceRegistration] = []
|
|
113
|
+
declarations: list[tuple[type[AppModule], tuple[type[HostedService], ...]]] = []
|
|
114
|
+
names: dict[str, ModuleKey] = {}
|
|
115
|
+
for descriptor in registry.dependency_order:
|
|
116
|
+
declared = descriptor.module_type.__dict__.get("hosted_services", ())
|
|
117
|
+
if not isinstance(declared, tuple):
|
|
118
|
+
raise HostedServiceDefinitionError(
|
|
119
|
+
service=str(descriptor.key),
|
|
120
|
+
reason="Module hosted_services must be an immutable tuple",
|
|
121
|
+
)
|
|
122
|
+
declarations.append((descriptor.module_type, declared))
|
|
123
|
+
for declaration_index, candidate in enumerate(declared):
|
|
124
|
+
_validate_service_type(candidate)
|
|
125
|
+
name = f"{candidate.__module__}.{candidate.__qualname__}"
|
|
126
|
+
previous_name_owner = names.get(name)
|
|
127
|
+
if previous_name_owner is not None:
|
|
128
|
+
raise HostedServiceDefinitionError(
|
|
129
|
+
service=name,
|
|
130
|
+
reason=f"name is already owned by {previous_name_owner}",
|
|
131
|
+
)
|
|
132
|
+
names[name] = descriptor.key
|
|
133
|
+
registrations.append(
|
|
134
|
+
HostedServiceRegistration(
|
|
135
|
+
service_type=candidate,
|
|
136
|
+
name=name,
|
|
137
|
+
owner=descriptor.key,
|
|
138
|
+
source=descriptor.declaration_source,
|
|
139
|
+
declaration_index=declaration_index,
|
|
140
|
+
run_in_background_processes=candidate.run_in_background_processes,
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
return HostedServiceCatalog(tuple(registrations), declarations=tuple(declarations))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _contribute_hosted_service_defaults(
|
|
147
|
+
catalog: HostedServiceCatalog,
|
|
148
|
+
contributions: ServiceContributionRegistry,
|
|
149
|
+
) -> None:
|
|
150
|
+
"""默认 class factory 进入既有仲裁;显式 APP binding 因而能原生 back off。"""
|
|
151
|
+
|
|
152
|
+
for registration in catalog.registrations:
|
|
153
|
+
service_type = registration.service_type
|
|
154
|
+
provider = Provider()
|
|
155
|
+
provider.provide(service_type, provides=service_type, scope=Scope.APP)
|
|
156
|
+
contributions._add_framework_default(
|
|
157
|
+
provider=provider,
|
|
158
|
+
owner=registration.owner,
|
|
159
|
+
source=registration.source,
|
|
160
|
+
reason=f"Hosted Service default for {registration.name}",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _validate_hosted_service_entries(
|
|
165
|
+
entries: tuple[_ContributionEntry, ...],
|
|
166
|
+
catalog: HostedServiceCatalog,
|
|
167
|
+
) -> None:
|
|
168
|
+
"""在通用仲裁前识别 wrong-scope/multi,返回 Hosted Service 专属错误。"""
|
|
169
|
+
|
|
170
|
+
from ..services.provider import _NativeProviderInspector
|
|
171
|
+
|
|
172
|
+
names_by_key = {
|
|
173
|
+
DependencyKey(item.service_type, DEFAULT_COMPONENT): item.name
|
|
174
|
+
for item in catalog.registrations
|
|
175
|
+
}
|
|
176
|
+
explicit_counts: dict[DependencyKey, int] = {}
|
|
177
|
+
for entry in entries:
|
|
178
|
+
contribution = entry.contribution
|
|
179
|
+
validated = _NativeProviderInspector(
|
|
180
|
+
source=contribution.source,
|
|
181
|
+
kind=contribution.kind,
|
|
182
|
+
).inspect(contribution.provider)
|
|
183
|
+
for factory in validated.factories:
|
|
184
|
+
_validate_hosted_service_factory(
|
|
185
|
+
factory,
|
|
186
|
+
contribution.kind,
|
|
187
|
+
names_by_key,
|
|
188
|
+
explicit_counts,
|
|
189
|
+
)
|
|
190
|
+
for collection in validated.collections:
|
|
191
|
+
_validate_hosted_service_collection(collection, names_by_key)
|
|
192
|
+
for key, count in explicit_counts.items():
|
|
193
|
+
if count > 1:
|
|
194
|
+
raise HostedServiceContractError(
|
|
195
|
+
service=names_by_key[key],
|
|
196
|
+
operation="bind",
|
|
197
|
+
error_type="MultipleBindings",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _validate_hosted_service_factory(
|
|
202
|
+
factory: _FactoryDefinition,
|
|
203
|
+
kind: RegistrationKind,
|
|
204
|
+
names_by_key: Mapping[DependencyKey, str],
|
|
205
|
+
explicit_counts: dict[DependencyKey, int],
|
|
206
|
+
) -> None:
|
|
207
|
+
from ..services.registration import RegistrationKind
|
|
208
|
+
|
|
209
|
+
name = names_by_key.get(factory.key)
|
|
210
|
+
if name is None:
|
|
211
|
+
return
|
|
212
|
+
if kind is RegistrationKind.MULTI:
|
|
213
|
+
raise HostedServiceContractError(
|
|
214
|
+
service=name,
|
|
215
|
+
operation="bind",
|
|
216
|
+
error_type="MultiBinding",
|
|
217
|
+
)
|
|
218
|
+
if factory.scope is not Scope.APP:
|
|
219
|
+
raise HostedServiceContractError(
|
|
220
|
+
service=name,
|
|
221
|
+
operation="bind",
|
|
222
|
+
error_type="WrongScope",
|
|
223
|
+
)
|
|
224
|
+
if kind is RegistrationKind.EXPLICIT:
|
|
225
|
+
explicit_counts[factory.key] = explicit_counts.get(factory.key, 0) + 1
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _validate_hosted_service_collection(
|
|
229
|
+
collection: _CollectionDefinition,
|
|
230
|
+
names_by_key: Mapping[DependencyKey, str],
|
|
231
|
+
) -> None:
|
|
232
|
+
name = names_by_key.get(collection.key)
|
|
233
|
+
if name is not None:
|
|
234
|
+
raise HostedServiceContractError(
|
|
235
|
+
service=name,
|
|
236
|
+
operation="bind",
|
|
237
|
+
error_type="MultiBinding",
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _validate_hosted_service_drafts(
|
|
242
|
+
drafts: tuple[_RegistrationDraft, ...],
|
|
243
|
+
catalog: HostedServiceCatalog,
|
|
244
|
+
) -> None:
|
|
245
|
+
from ..services.registration import RegistrationDecision
|
|
246
|
+
|
|
247
|
+
for registration in catalog.registrations:
|
|
248
|
+
key = DependencyKey(registration.service_type, DEFAULT_COMPONENT)
|
|
249
|
+
winners = tuple(
|
|
250
|
+
draft
|
|
251
|
+
for draft in drafts
|
|
252
|
+
if draft.key == key and draft.decision is RegistrationDecision.ACCEPTED
|
|
253
|
+
)
|
|
254
|
+
if len(winners) != 1 or winners[0].scope is not Scope.APP or winners[0].is_collection:
|
|
255
|
+
raise HostedServiceContractError(
|
|
256
|
+
service=registration.name,
|
|
257
|
+
operation="bind",
|
|
258
|
+
error_type="InvalidBinding",
|
|
259
|
+
)
|