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,241 @@
|
|
|
1
|
+
"""不可变身份、Permission contract 与静态授权 metadata。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Protocol, TypeVar, runtime_checkable
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
from .errors import AuthorizationDefinitionError, PermissionDefinitionError
|
|
11
|
+
|
|
12
|
+
_AUTHORIZATION_ATTRIBUTE = "__python_platform_authorization__"
|
|
13
|
+
_ALLOW_ANONYMOUS_ATTRIBUTE = "__python_platform_allow_anonymous__"
|
|
14
|
+
_TDecorated = TypeVar("_TDecorated")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, order=True, slots=True)
|
|
18
|
+
class PermissionName:
|
|
19
|
+
value: str
|
|
20
|
+
|
|
21
|
+
def __post_init__(self) -> None:
|
|
22
|
+
if (
|
|
23
|
+
not isinstance(self.value, str)
|
|
24
|
+
or not self.value
|
|
25
|
+
or self.value != self.value.strip()
|
|
26
|
+
or self.value.startswith(".")
|
|
27
|
+
or self.value.endswith(".")
|
|
28
|
+
or ".." in self.value
|
|
29
|
+
or any(not (item.isalnum() or item in "._-") for item in self.value)
|
|
30
|
+
):
|
|
31
|
+
raise PermissionDefinitionError(
|
|
32
|
+
permission=self.value if isinstance(self.value, str) else repr(self.value),
|
|
33
|
+
reason="name must be a trimmed dotted identifier",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def __str__(self) -> str:
|
|
37
|
+
return self.value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class PermissionDefinition:
|
|
42
|
+
name: PermissionName
|
|
43
|
+
display_name: str
|
|
44
|
+
|
|
45
|
+
def __post_init__(self) -> None:
|
|
46
|
+
if not isinstance(self.name, PermissionName):
|
|
47
|
+
raise PermissionDefinitionError(
|
|
48
|
+
permission=repr(self.name),
|
|
49
|
+
reason="name must be PermissionName",
|
|
50
|
+
)
|
|
51
|
+
if (
|
|
52
|
+
not isinstance(self.display_name, str)
|
|
53
|
+
or not self.display_name
|
|
54
|
+
or self.display_name != self.display_name.strip()
|
|
55
|
+
):
|
|
56
|
+
raise PermissionDefinitionError(
|
|
57
|
+
permission=str(self.name),
|
|
58
|
+
reason="display_name must be a non-empty trimmed string",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True, order=True, slots=True)
|
|
63
|
+
class RolePermissionVersion:
|
|
64
|
+
role_id: UUID
|
|
65
|
+
version: int
|
|
66
|
+
|
|
67
|
+
def __post_init__(self) -> None:
|
|
68
|
+
if not isinstance(self.role_id, UUID):
|
|
69
|
+
raise TypeError("role_id must be UUID")
|
|
70
|
+
if not isinstance(self.version, int) or isinstance(self.version, bool) or self.version < 0:
|
|
71
|
+
raise ValueError("role permission version must be a non-negative integer")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True, slots=True)
|
|
75
|
+
class CurrentUser:
|
|
76
|
+
"""一次可信入口已验证的身份快照;不携带权限集合。"""
|
|
77
|
+
|
|
78
|
+
user_id: UUID | None = None
|
|
79
|
+
session_id: UUID | None = None
|
|
80
|
+
username: str | None = None
|
|
81
|
+
permission_version: int = 0
|
|
82
|
+
role_versions: tuple[RolePermissionVersion, ...] = ()
|
|
83
|
+
|
|
84
|
+
def __post_init__(self) -> None:
|
|
85
|
+
identity_values = (self.user_id, self.session_id, self.username)
|
|
86
|
+
is_anonymous = all(value is None for value in identity_values)
|
|
87
|
+
if not is_anonymous and (
|
|
88
|
+
not isinstance(self.user_id, UUID)
|
|
89
|
+
or not isinstance(self.session_id, UUID)
|
|
90
|
+
or not isinstance(self.username, str)
|
|
91
|
+
or not self.username
|
|
92
|
+
or self.username != self.username.strip()
|
|
93
|
+
):
|
|
94
|
+
raise ValueError("authenticated CurrentUser requires user_id, session_id and username")
|
|
95
|
+
if is_anonymous and (self.permission_version != 0 or self.role_versions):
|
|
96
|
+
raise ValueError("anonymous CurrentUser cannot carry authorization versions")
|
|
97
|
+
if not isinstance(self.permission_version, int) or isinstance(
|
|
98
|
+
self.permission_version, bool
|
|
99
|
+
):
|
|
100
|
+
raise TypeError("permission_version must be int")
|
|
101
|
+
if self.permission_version < 0:
|
|
102
|
+
raise ValueError("permission_version must be non-negative")
|
|
103
|
+
if not isinstance(self.role_versions, tuple) or any(
|
|
104
|
+
not isinstance(item, RolePermissionVersion) for item in self.role_versions
|
|
105
|
+
):
|
|
106
|
+
raise TypeError("role_versions must be an immutable RolePermissionVersion tuple")
|
|
107
|
+
if tuple(sorted(self.role_versions)) != self.role_versions:
|
|
108
|
+
raise ValueError("role_versions must be sorted by role_id and version")
|
|
109
|
+
if len({item.role_id for item in self.role_versions}) != len(self.role_versions):
|
|
110
|
+
raise ValueError("role_versions cannot contain duplicate role ids")
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def is_authenticated(self) -> bool:
|
|
114
|
+
return self.user_id is not None
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def anonymous(cls) -> CurrentUser:
|
|
118
|
+
return cls()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@runtime_checkable
|
|
122
|
+
class PermissionChecker(Protocol):
|
|
123
|
+
async def is_granted(self, user: CurrentUser, permission: PermissionName) -> bool: ...
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@runtime_checkable
|
|
127
|
+
class AccessTokenAuthenticator(Protocol):
|
|
128
|
+
async def authenticate_access_token(self, token: str) -> CurrentUser: ...
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass(frozen=True, slots=True)
|
|
132
|
+
class _AuthorizationMetadata:
|
|
133
|
+
permissions: tuple[PermissionName, ...]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass(frozen=True, slots=True)
|
|
137
|
+
class _AuthorizationRequirement:
|
|
138
|
+
requires_authenticated_user: bool
|
|
139
|
+
permissions: tuple[PermissionName, ...]
|
|
140
|
+
allows_anonymous: bool
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def authorize(*permissions: PermissionName) -> Callable[[_TDecorated], _TDecorated]:
|
|
144
|
+
"""声明登录或 all-of Permission 要求;只接受集中定义的 PermissionName。"""
|
|
145
|
+
|
|
146
|
+
if any(not isinstance(permission, PermissionName) for permission in permissions):
|
|
147
|
+
raise AuthorizationDefinitionError(
|
|
148
|
+
symbol="<decorator>",
|
|
149
|
+
reason="authorize permissions must be PermissionName values",
|
|
150
|
+
)
|
|
151
|
+
if len(set(permissions)) != len(permissions):
|
|
152
|
+
raise AuthorizationDefinitionError(
|
|
153
|
+
symbol="<decorator>",
|
|
154
|
+
reason="authorize permissions cannot contain duplicates",
|
|
155
|
+
)
|
|
156
|
+
metadata = _AuthorizationMetadata(permissions=permissions)
|
|
157
|
+
|
|
158
|
+
def decorate(target: _TDecorated) -> _TDecorated:
|
|
159
|
+
_validate_authorization_target(target)
|
|
160
|
+
namespace = vars(target)
|
|
161
|
+
symbol = _symbol(target)
|
|
162
|
+
if _AUTHORIZATION_ATTRIBUTE in namespace or _ALLOW_ANONYMOUS_ATTRIBUTE in namespace:
|
|
163
|
+
raise AuthorizationDefinitionError(
|
|
164
|
+
symbol=symbol,
|
|
165
|
+
reason="authorization metadata may only be declared once",
|
|
166
|
+
)
|
|
167
|
+
setattr(target, _AUTHORIZATION_ATTRIBUTE, metadata)
|
|
168
|
+
return target
|
|
169
|
+
|
|
170
|
+
return decorate
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def allow_anonymous(target: _TDecorated) -> _TDecorated:
|
|
174
|
+
"""Method metadata 明确覆盖 class 级授权;不靠命名或默认回退推断。"""
|
|
175
|
+
|
|
176
|
+
_validate_authorization_target(target)
|
|
177
|
+
namespace = vars(target)
|
|
178
|
+
symbol = _symbol(target)
|
|
179
|
+
if _AUTHORIZATION_ATTRIBUTE in namespace or _ALLOW_ANONYMOUS_ATTRIBUTE in namespace:
|
|
180
|
+
raise AuthorizationDefinitionError(
|
|
181
|
+
symbol=symbol,
|
|
182
|
+
reason="authorization metadata may only be declared once",
|
|
183
|
+
)
|
|
184
|
+
setattr(target, _ALLOW_ANONYMOUS_ATTRIBUTE, True)
|
|
185
|
+
return target
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _authorization_requirement(
|
|
189
|
+
implementation: type[object],
|
|
190
|
+
method: object,
|
|
191
|
+
) -> _AuthorizationRequirement:
|
|
192
|
+
# 只从仲裁胜出的实现继承链合并类声明;Contract 不再是策略来源。
|
|
193
|
+
class_metadata = tuple(
|
|
194
|
+
metadata
|
|
195
|
+
for owner in reversed(implementation.__mro__)
|
|
196
|
+
if (metadata := _metadata_for(owner)) is not None
|
|
197
|
+
)
|
|
198
|
+
method_metadata = _metadata_for(method)
|
|
199
|
+
method_allows_anonymous = vars(method).get(_ALLOW_ANONYMOUS_ATTRIBUTE, False)
|
|
200
|
+
if method_allows_anonymous:
|
|
201
|
+
return _AuthorizationRequirement(False, (), True)
|
|
202
|
+
permissions: list[PermissionName] = []
|
|
203
|
+
for metadata in (*class_metadata, method_metadata):
|
|
204
|
+
if metadata is None:
|
|
205
|
+
continue
|
|
206
|
+
for permission in metadata.permissions:
|
|
207
|
+
if permission not in permissions:
|
|
208
|
+
permissions.append(permission)
|
|
209
|
+
requires_user = bool(class_metadata) or method_metadata is not None
|
|
210
|
+
return _AuthorizationRequirement(requires_user, tuple(permissions), False)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _metadata_for(target: object) -> _AuthorizationMetadata | None:
|
|
214
|
+
namespace = vars(target)
|
|
215
|
+
metadata = namespace.get(_AUTHORIZATION_ATTRIBUTE)
|
|
216
|
+
anonymous = namespace.get(_ALLOW_ANONYMOUS_ATTRIBUTE, False)
|
|
217
|
+
if metadata is not None and not isinstance(metadata, _AuthorizationMetadata):
|
|
218
|
+
raise AuthorizationDefinitionError(
|
|
219
|
+
symbol=_symbol(target),
|
|
220
|
+
reason="authorization metadata has an invalid runtime shape",
|
|
221
|
+
)
|
|
222
|
+
if not isinstance(anonymous, bool):
|
|
223
|
+
raise AuthorizationDefinitionError(
|
|
224
|
+
symbol=_symbol(target),
|
|
225
|
+
reason="allow_anonymous metadata has an invalid runtime shape",
|
|
226
|
+
)
|
|
227
|
+
return metadata
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _validate_authorization_target(target: object) -> None:
|
|
231
|
+
if not isinstance(target, type) and not callable(target):
|
|
232
|
+
raise AuthorizationDefinitionError(
|
|
233
|
+
symbol=repr(target),
|
|
234
|
+
reason="authorization decorator target must be a class or callable",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _symbol(target: object) -> str:
|
|
239
|
+
module = getattr(target, "__module__", type(target).__module__)
|
|
240
|
+
qualname = getattr(target, "__qualname__", type(target).__qualname__)
|
|
241
|
+
return f"{module}.{qualname}"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""从冻结类型结果解释权限定义,拥有贡献顺序、来源与冲突检查。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import cast
|
|
8
|
+
|
|
9
|
+
from ..diagnostics import SourceLocation
|
|
10
|
+
from ..modularity import ModuleRegistry
|
|
11
|
+
from ..modularity.discovery import _DiscoveredType
|
|
12
|
+
from .contracts import PermissionDefinition, PermissionName
|
|
13
|
+
from .definitions import PermissionDefinitionContext, PermissionDefinitionProvider
|
|
14
|
+
from .errors import PermissionDefinitionError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _collect_permission_definitions(
|
|
18
|
+
registry: ModuleRegistry,
|
|
19
|
+
discoveries: tuple[_DiscoveredType, ...],
|
|
20
|
+
) -> tuple[PermissionDefinition, ...]:
|
|
21
|
+
positions = {item.key: index for index, item in enumerate(registry.dependency_order)}
|
|
22
|
+
providers: dict[type[object], SourceLocation] = {}
|
|
23
|
+
sources: dict[PermissionName, SourceLocation] = {}
|
|
24
|
+
definitions: list[PermissionDefinition] = []
|
|
25
|
+
for discovery in sorted(
|
|
26
|
+
discoveries,
|
|
27
|
+
key=lambda item: (
|
|
28
|
+
positions[item.owner],
|
|
29
|
+
f"{item.candidate.__module__}.{item.candidate.__qualname__}",
|
|
30
|
+
),
|
|
31
|
+
):
|
|
32
|
+
candidate = discovery.candidate
|
|
33
|
+
if not issubclass(candidate, PermissionDefinitionProvider) or inspect.isabstract(candidate):
|
|
34
|
+
continue
|
|
35
|
+
previous = providers.get(candidate)
|
|
36
|
+
if previous is not None:
|
|
37
|
+
if previous.owner != discovery.source.owner:
|
|
38
|
+
raise PermissionDefinitionError(
|
|
39
|
+
permission=candidate.__qualname__,
|
|
40
|
+
reason=f"provider is already owned by {previous}",
|
|
41
|
+
source=discovery.source,
|
|
42
|
+
)
|
|
43
|
+
continue
|
|
44
|
+
providers[candidate] = discovery.source
|
|
45
|
+
for definition in _define(candidate, discovery.source):
|
|
46
|
+
if definition.name in sources:
|
|
47
|
+
raise PermissionDefinitionError(
|
|
48
|
+
permission=str(definition.name),
|
|
49
|
+
reason=f"permission is already owned by {sources[definition.name]}",
|
|
50
|
+
source=discovery.source,
|
|
51
|
+
)
|
|
52
|
+
sources[definition.name] = discovery.source
|
|
53
|
+
definitions.append(definition)
|
|
54
|
+
return tuple(definitions)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _define(
|
|
58
|
+
provider_type: type[PermissionDefinitionProvider],
|
|
59
|
+
source: SourceLocation,
|
|
60
|
+
) -> tuple[PermissionDefinition, ...]:
|
|
61
|
+
context = PermissionDefinitionContext(source)
|
|
62
|
+
try:
|
|
63
|
+
provider = provider_type()
|
|
64
|
+
callback = cast(Callable[[PermissionDefinitionContext], object], provider.define)
|
|
65
|
+
if inspect.iscoroutinefunction(callback) or inspect.isasyncgenfunction(callback):
|
|
66
|
+
raise TypeError("define must be synchronous")
|
|
67
|
+
result = callback(context)
|
|
68
|
+
if result is not None:
|
|
69
|
+
if inspect.iscoroutine(result) or inspect.isgenerator(result):
|
|
70
|
+
result.close()
|
|
71
|
+
raise TypeError("define must return None")
|
|
72
|
+
except Exception as error:
|
|
73
|
+
raise PermissionDefinitionError(
|
|
74
|
+
permission=provider_type.__qualname__,
|
|
75
|
+
reason=f"definition provider failed: {type(error).__name__}: {error}",
|
|
76
|
+
source=source,
|
|
77
|
+
) from error
|
|
78
|
+
finally:
|
|
79
|
+
# 即使构造或贡献失败,已逸出的上下文也不可再写入。
|
|
80
|
+
definitions = context._close()
|
|
81
|
+
return definitions
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""权限定义的同步贡献契约;上下文由单次 Application build 独占。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
|
|
7
|
+
from ..diagnostics import SourceLocation
|
|
8
|
+
from .contracts import PermissionDefinition
|
|
9
|
+
from .errors import PermissionDefinitionError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PermissionDefinitionProvider(ABC):
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def define(self, context: PermissionDefinitionContext) -> None:
|
|
15
|
+
"""同步贡献静态权限定义;无构造注入、I/O 或运行期服务解析。"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PermissionDefinitionContext:
|
|
19
|
+
__slots__ = ("_closed", "_definitions", "_source")
|
|
20
|
+
|
|
21
|
+
def __init__(self, source: SourceLocation) -> None:
|
|
22
|
+
self._source = source
|
|
23
|
+
self._definitions: list[PermissionDefinition] = []
|
|
24
|
+
self._closed = False
|
|
25
|
+
|
|
26
|
+
def add(self, definition: PermissionDefinition) -> None:
|
|
27
|
+
if self._closed:
|
|
28
|
+
raise PermissionDefinitionError(
|
|
29
|
+
permission="context",
|
|
30
|
+
reason="definition context is closed",
|
|
31
|
+
source=self._source,
|
|
32
|
+
)
|
|
33
|
+
if not isinstance(definition, PermissionDefinition):
|
|
34
|
+
raise PermissionDefinitionError(
|
|
35
|
+
permission="context",
|
|
36
|
+
reason="expected PermissionDefinition",
|
|
37
|
+
source=self._source,
|
|
38
|
+
)
|
|
39
|
+
self._definitions.append(definition)
|
|
40
|
+
|
|
41
|
+
def _close(self) -> tuple[PermissionDefinition, ...]:
|
|
42
|
+
self._closed = True
|
|
43
|
+
return tuple(self._definitions)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Authorization metadata、身份与权限判定的公开失败语义。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..diagnostics import SourceLocation
|
|
6
|
+
from ..errors.base import FrameworkError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PermissionDefinitionError(FrameworkError):
|
|
10
|
+
def __init__(
|
|
11
|
+
self, *, permission: str, reason: str, source: SourceLocation | None = None
|
|
12
|
+
) -> None:
|
|
13
|
+
self.permission = permission
|
|
14
|
+
self.reason = reason
|
|
15
|
+
self.source = source
|
|
16
|
+
location = "" if source is None else f" at {source}"
|
|
17
|
+
super().__init__(f"Invalid permission {permission!r}{location}: {reason}")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AuthorizationDefinitionError(FrameworkError):
|
|
21
|
+
def __init__(self, *, symbol: str, reason: str) -> None:
|
|
22
|
+
self.symbol = symbol
|
|
23
|
+
self.reason = reason
|
|
24
|
+
super().__init__(f"Invalid authorization metadata for {symbol}: {reason}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class UnauthenticatedError(FrameworkError):
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
super().__init__("Authentication is required")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PermissionDeniedError(FrameworkError):
|
|
33
|
+
def __init__(self, *, permission: str) -> None:
|
|
34
|
+
self.permission = permission
|
|
35
|
+
super().__init__(f"Permission {permission!r} is required")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PermissionCheckerUnavailableError(FrameworkError):
|
|
39
|
+
def __init__(self) -> None:
|
|
40
|
+
super().__init__("A protected Application Service requires a PermissionChecker")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""任务执行位置、状态与管理的稳定契约。"""
|
|
2
|
+
|
|
3
|
+
from .contracts import (
|
|
4
|
+
BackgroundExecutionMode,
|
|
5
|
+
BackgroundTaskOptions,
|
|
6
|
+
BackgroundTaskSnapshot,
|
|
7
|
+
BackgroundTaskState,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = (
|
|
11
|
+
"BackgroundExecutionMode",
|
|
12
|
+
"BackgroundTaskOptions",
|
|
13
|
+
"BackgroundTaskSnapshot",
|
|
14
|
+
"BackgroundTaskState",
|
|
15
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""授权后将操作交给 Application owner;不等待任务业务,也不保留 REQUEST 资源。"""
|
|
2
|
+
|
|
3
|
+
from ..application_services import ApplicationService, application_service
|
|
4
|
+
from ..authorization import authorize
|
|
5
|
+
from ..background_workers.runtime import _BackgroundCoordinator
|
|
6
|
+
from ..errors import ResourceConflictError, ResourceNotFoundError
|
|
7
|
+
from .contracts import BackgroundTaskSnapshot
|
|
8
|
+
from .management import BACKGROUND_MANAGE, BACKGROUND_VIEW, BackgroundManagementApplicationService
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@application_service(provides=BackgroundManagementApplicationService)
|
|
12
|
+
class _BackgroundManagementApplicationService(ApplicationService):
|
|
13
|
+
def __init__(self, coordinator: _BackgroundCoordinator) -> None:
|
|
14
|
+
self._coordinator = coordinator
|
|
15
|
+
|
|
16
|
+
@authorize(BACKGROUND_VIEW)
|
|
17
|
+
async def get_list(self) -> tuple[BackgroundTaskSnapshot, ...]:
|
|
18
|
+
return self._coordinator.snapshots()
|
|
19
|
+
|
|
20
|
+
@authorize(BACKGROUND_VIEW)
|
|
21
|
+
async def get(self, task_id: str) -> BackgroundTaskSnapshot:
|
|
22
|
+
for snapshot in self._coordinator.snapshots():
|
|
23
|
+
if snapshot.id == task_id:
|
|
24
|
+
return snapshot
|
|
25
|
+
raise ResourceNotFoundError("Background task does not exist")
|
|
26
|
+
|
|
27
|
+
@authorize(BACKGROUND_MANAGE)
|
|
28
|
+
async def start(self, task_id: str) -> BackgroundTaskSnapshot:
|
|
29
|
+
return self._request(task_id, start=True)
|
|
30
|
+
|
|
31
|
+
@authorize(BACKGROUND_MANAGE)
|
|
32
|
+
async def stop(self, task_id: str) -> BackgroundTaskSnapshot:
|
|
33
|
+
return self._request(task_id, start=False)
|
|
34
|
+
|
|
35
|
+
def _request(self, task_id: str, *, start: bool) -> BackgroundTaskSnapshot:
|
|
36
|
+
try:
|
|
37
|
+
return self._coordinator.request(task_id, start=start)
|
|
38
|
+
except KeyError:
|
|
39
|
+
raise ResourceNotFoundError("Background task does not exist") from None
|
|
40
|
+
except ValueError as error:
|
|
41
|
+
raise ResourceConflictError(str(error)) from None
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""spawn 子进程入口;管道是父进程许可与存活边界,Application 独立重建。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from multiprocessing.connection import _ConnectionBase as Connection
|
|
10
|
+
|
|
11
|
+
from ..application import Application
|
|
12
|
+
from ..application.build_spec import _ApplicationBuildSpec
|
|
13
|
+
from ..errors import ApplicationStartError
|
|
14
|
+
from ..lifecycle import ApplicationState
|
|
15
|
+
from .contracts import BackgroundTaskOptions
|
|
16
|
+
from .locks import _position_lock
|
|
17
|
+
from .processes import _POLL_INTERVAL, _Message
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _run_child(
|
|
21
|
+
spec: _ApplicationBuildSpec,
|
|
22
|
+
position: str,
|
|
23
|
+
tasks: dict[str, BackgroundTaskOptions],
|
|
24
|
+
initial: tuple[str, ...],
|
|
25
|
+
pipe: Connection,
|
|
26
|
+
) -> None:
|
|
27
|
+
try:
|
|
28
|
+
with _position_lock(spec.project, spec.environment, position):
|
|
29
|
+
pipe.send((_Message.LOCKED,))
|
|
30
|
+
if pipe.recv() != (_Message.GO,):
|
|
31
|
+
raise RuntimeError("Background process has no parent permission")
|
|
32
|
+
asyncio.run(_serve_child(spec, tasks, initial, pipe))
|
|
33
|
+
except (EOFError, BrokenPipeError):
|
|
34
|
+
# 父进程已退出,执行许可已撤销;异步入口的 finally 仍先完成 Application 清理。
|
|
35
|
+
pass
|
|
36
|
+
except BaseException as error:
|
|
37
|
+
try:
|
|
38
|
+
if isinstance(error, ApplicationStartError) and error.cleanup_failures:
|
|
39
|
+
pipe.send((_Message.CLEANUP_FAILED, type(error).__name__))
|
|
40
|
+
pipe.send((_Message.FAILED, type(error).__name__))
|
|
41
|
+
except (EOFError, OSError):
|
|
42
|
+
pass
|
|
43
|
+
finally:
|
|
44
|
+
pipe.close()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def _serve_child(
|
|
48
|
+
spec: _ApplicationBuildSpec,
|
|
49
|
+
tasks: dict[str, BackgroundTaskOptions],
|
|
50
|
+
initial: tuple[str, ...],
|
|
51
|
+
pipe: Connection,
|
|
52
|
+
) -> None:
|
|
53
|
+
application = spec.build()
|
|
54
|
+
application._root.require_hosted_service_coordinator().select_child()
|
|
55
|
+
coordinator = application._root.require_background_coordinator()
|
|
56
|
+
coordinator.select_child(tasks, initial)
|
|
57
|
+
try:
|
|
58
|
+
await _start_while_parent_present(application, pipe)
|
|
59
|
+
previous = coordinator.snapshots()
|
|
60
|
+
while True:
|
|
61
|
+
if pipe.poll():
|
|
62
|
+
command = pipe.recv()
|
|
63
|
+
if command[0] == _Message.SHUTDOWN:
|
|
64
|
+
break
|
|
65
|
+
action, serial, key = command
|
|
66
|
+
if action not in (_Message.START, _Message.STOP):
|
|
67
|
+
raise RuntimeError("Unknown background process command")
|
|
68
|
+
try:
|
|
69
|
+
snapshot = coordinator.request(key, start=action == _Message.START)
|
|
70
|
+
except (KeyError, ValueError) as error:
|
|
71
|
+
pipe.send((_Message.REJECTED, serial, type(error).__name__))
|
|
72
|
+
else:
|
|
73
|
+
pipe.send((_Message.RESULT, serial, snapshot))
|
|
74
|
+
snapshots = coordinator.snapshots()
|
|
75
|
+
if snapshots != previous:
|
|
76
|
+
pipe.send((_Message.SNAPSHOTS, snapshots))
|
|
77
|
+
previous = snapshots
|
|
78
|
+
if application.state in (
|
|
79
|
+
ApplicationState.STOPPED,
|
|
80
|
+
ApplicationState.STOPPED_WITH_ERRORS,
|
|
81
|
+
):
|
|
82
|
+
await application.wait_for_termination()
|
|
83
|
+
break
|
|
84
|
+
await asyncio.sleep(_POLL_INTERVAL)
|
|
85
|
+
finally:
|
|
86
|
+
await _stop_child(application, pipe)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def _start_while_parent_present(application: Application, pipe: Connection) -> None:
|
|
90
|
+
async def start() -> None:
|
|
91
|
+
await application.start()
|
|
92
|
+
# 启动确认与 start 返回处于同一 task 连续段;首轮业务崩溃不能被误判为初始化失败。
|
|
93
|
+
pipe.send(
|
|
94
|
+
(_Message.SNAPSHOTS, application._root.require_background_coordinator().snapshots())
|
|
95
|
+
)
|
|
96
|
+
pipe.send((_Message.READY,))
|
|
97
|
+
|
|
98
|
+
starting = asyncio.create_task(start())
|
|
99
|
+
try:
|
|
100
|
+
while not starting.done():
|
|
101
|
+
if pipe.poll():
|
|
102
|
+
if pipe.recv() != (_Message.SHUTDOWN,):
|
|
103
|
+
raise RuntimeError("Unexpected command during child initialization")
|
|
104
|
+
starting.cancel()
|
|
105
|
+
break
|
|
106
|
+
await asyncio.sleep(_POLL_INTERVAL)
|
|
107
|
+
await starting
|
|
108
|
+
finally:
|
|
109
|
+
if not starting.done():
|
|
110
|
+
# EOF/父进程 shutdown 只请求一次取消,Application 仍独占 partial-start 清理。
|
|
111
|
+
starting.cancel()
|
|
112
|
+
await starting
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
async def _stop_child(application: Application, pipe: Connection) -> None:
|
|
116
|
+
try:
|
|
117
|
+
await application.stop()
|
|
118
|
+
except BaseException as error:
|
|
119
|
+
try:
|
|
120
|
+
pipe.send((_Message.CLEANUP_FAILED, type(error).__name__))
|
|
121
|
+
except (EOFError, OSError):
|
|
122
|
+
pass
|
|
123
|
+
raise
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""执行模式与只读状态;不依赖 provider、进程或编排实现。"""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict
|
|
6
|
+
|
|
7
|
+
from ..options import BaseOptions
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BackgroundExecutionMode(str, Enum):
|
|
11
|
+
HOST = "host"
|
|
12
|
+
SHARED = "shared"
|
|
13
|
+
EXCLUSIVE = "exclusive"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BackgroundTaskOptions(BaseOptions):
|
|
17
|
+
enabled: bool = True
|
|
18
|
+
mode: BackgroundExecutionMode = BackgroundExecutionMode.HOST
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BackgroundTaskState(str, Enum):
|
|
22
|
+
DISABLED = "disabled"
|
|
23
|
+
STARTING = "starting"
|
|
24
|
+
RUNNING = "running"
|
|
25
|
+
STOPPING = "stopping"
|
|
26
|
+
STOPPED = "stopped"
|
|
27
|
+
FAILED = "failed"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BackgroundTaskSnapshot(BaseModel):
|
|
31
|
+
model_config = ConfigDict(frozen=True)
|
|
32
|
+
|
|
33
|
+
id: str
|
|
34
|
+
enabled: bool
|
|
35
|
+
mode: BackgroundExecutionMode
|
|
36
|
+
state: BackgroundTaskState
|
|
37
|
+
process_id: int | None = None
|
|
38
|
+
restart_count: int = 0
|
|
39
|
+
error_type: str | None = None
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""本进程执行单元的生命周期回调及失败事实;由 Worker/持久队列提供。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class _BackgroundRuntimeFailure:
|
|
11
|
+
component: str
|
|
12
|
+
error_type: str
|
|
13
|
+
_source: object = field(default_factory=object, repr=False, compare=False)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class _TaskExecution:
|
|
18
|
+
start: Callable[[], Awaitable[None]]
|
|
19
|
+
stop_claiming: Callable[[], None]
|
|
20
|
+
wait: Callable[[], Awaitable[None]]
|
|
21
|
+
wait_for_failure: Callable[[], Awaitable[_BackgroundRuntimeFailure]] | None
|
|
22
|
+
cancel_for_deadline: Callable[[], None]
|