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,203 @@
|
|
|
1
|
+
"""Application-owned 任务编排;目录提供任务事实,执行 owner 各自管理本地或子进程资源。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
|
|
9
|
+
from dishka import AsyncContainer
|
|
10
|
+
|
|
11
|
+
from ..application.build_spec import _ApplicationBuildSpec
|
|
12
|
+
from ..application_services.invocation import _InvocationRuntime
|
|
13
|
+
from ..background_execution import (
|
|
14
|
+
BackgroundExecutionMode,
|
|
15
|
+
BackgroundTaskOptions,
|
|
16
|
+
BackgroundTaskSnapshot,
|
|
17
|
+
)
|
|
18
|
+
from ..background_execution.lifecycle import _BackgroundRuntimeFailure, _TaskExecution
|
|
19
|
+
from ..background_execution.local import _LocalTask
|
|
20
|
+
from ..background_execution.locks import _check_previous_owners
|
|
21
|
+
from ..background_execution.processes import _ProcessSlot
|
|
22
|
+
from ..background_jobs import BackgroundJobCatalog
|
|
23
|
+
from ..invocation import BackgroundSystemCaller
|
|
24
|
+
from .catalog import BackgroundWorkerCatalog
|
|
25
|
+
from .contracts import BackgroundExecutionOptions, BackgroundExecutionProfile
|
|
26
|
+
from .execution import _WorkerExecution
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _BackgroundCoordinator:
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
*,
|
|
33
|
+
worker_catalog: BackgroundWorkerCatalog,
|
|
34
|
+
job_catalog: BackgroundJobCatalog,
|
|
35
|
+
options: BackgroundExecutionOptions,
|
|
36
|
+
invocation_runtime: _InvocationRuntime,
|
|
37
|
+
build_spec: _ApplicationBuildSpec,
|
|
38
|
+
) -> None:
|
|
39
|
+
self._worker_catalog = worker_catalog
|
|
40
|
+
self._job_catalog = job_catalog
|
|
41
|
+
self._options = options
|
|
42
|
+
self._invocation_runtime = invocation_runtime
|
|
43
|
+
self._spec = build_spec
|
|
44
|
+
self._caller = BackgroundSystemCaller()
|
|
45
|
+
self._container: AsyncContainer | None = None
|
|
46
|
+
self._started = False
|
|
47
|
+
self._prepared = False
|
|
48
|
+
self._child = False
|
|
49
|
+
self._database_only = False
|
|
50
|
+
self._failures: asyncio.Queue[_BackgroundRuntimeFailure] = asyncio.Queue()
|
|
51
|
+
self._tasks: dict[str, _LocalTask] = {}
|
|
52
|
+
self._slots: dict[str, _ProcessSlot] = {}
|
|
53
|
+
self._task_slots: dict[str, _ProcessSlot] = {}
|
|
54
|
+
self._register_tasks(
|
|
55
|
+
"worker", {item.name for item in worker_catalog.definitions}, options.workers
|
|
56
|
+
)
|
|
57
|
+
self._register_tasks("job", {item.name for item in job_catalog.definitions}, options.jobs)
|
|
58
|
+
self._local = {
|
|
59
|
+
key
|
|
60
|
+
for key, task in self._tasks.items()
|
|
61
|
+
if task.options.mode is BackgroundExecutionMode.HOST
|
|
62
|
+
}
|
|
63
|
+
self._initial = {key for key in self._local if self._tasks[key].options.enabled}
|
|
64
|
+
|
|
65
|
+
def _register_tasks(
|
|
66
|
+
self, kind: str, names: set[str], configured: Mapping[str, BackgroundTaskOptions]
|
|
67
|
+
) -> None:
|
|
68
|
+
unknown = set(configured) - names
|
|
69
|
+
if unknown:
|
|
70
|
+
raise ValueError(f"Unknown background {kind} configuration: {sorted(unknown)}")
|
|
71
|
+
for name in sorted(names):
|
|
72
|
+
key = f"{kind}:{name}"
|
|
73
|
+
options = configured.get(name, BackgroundTaskOptions())
|
|
74
|
+
self._tasks[key] = _LocalTask(
|
|
75
|
+
key,
|
|
76
|
+
options,
|
|
77
|
+
self._options.graceful_shutdown_timeout.total_seconds(),
|
|
78
|
+
self._failures.put_nowait,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def profile(self) -> BackgroundExecutionProfile:
|
|
83
|
+
return BackgroundExecutionProfile.DISABLED if self._database_only else self._options.profile
|
|
84
|
+
|
|
85
|
+
def disable_for_database(self) -> None:
|
|
86
|
+
if self._started or self._prepared:
|
|
87
|
+
raise RuntimeError("Database execution must be selected before initialization")
|
|
88
|
+
self._database_only = True
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def system_caller(self) -> BackgroundSystemCaller:
|
|
92
|
+
return self._caller
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def has_runtime_failure_source(self) -> bool:
|
|
96
|
+
return any(
|
|
97
|
+
task.execution is not None and task.execution.wait_for_failure is not None
|
|
98
|
+
for key, task in self._tasks.items()
|
|
99
|
+
if key in self._local
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def local_job_names(self) -> tuple[str, ...]:
|
|
104
|
+
if self.profile is BackgroundExecutionProfile.DISABLED:
|
|
105
|
+
return ()
|
|
106
|
+
return tuple(
|
|
107
|
+
key.removeprefix("job:")
|
|
108
|
+
for key in self._local
|
|
109
|
+
if key.startswith("job:") and self._tasks[key].options.enabled
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def select_child(
|
|
113
|
+
self, tasks: dict[str, BackgroundTaskOptions], initial: tuple[str, ...]
|
|
114
|
+
) -> None:
|
|
115
|
+
if self._prepared or self._started:
|
|
116
|
+
raise RuntimeError("Child task assignment is frozen before startup")
|
|
117
|
+
for key, options in tasks.items():
|
|
118
|
+
if key not in self._tasks or self._tasks[key].options != options:
|
|
119
|
+
raise RuntimeError("Child task catalog/configuration differs from parent")
|
|
120
|
+
self._child = True
|
|
121
|
+
self._local = set(tasks)
|
|
122
|
+
self._initial = set(initial)
|
|
123
|
+
|
|
124
|
+
def prepare(self) -> None:
|
|
125
|
+
if self._prepared:
|
|
126
|
+
raise RuntimeError("Background coordinator is already prepared")
|
|
127
|
+
self._prepared = True
|
|
128
|
+
self._worker_catalog._assert_unchanged()
|
|
129
|
+
self._job_catalog._assert_unchanged()
|
|
130
|
+
if self._child or self.profile is BackgroundExecutionProfile.DISABLED:
|
|
131
|
+
return
|
|
132
|
+
groups: dict[str, dict[str, BackgroundTaskOptions]] = defaultdict(dict)
|
|
133
|
+
for key, task in self._tasks.items():
|
|
134
|
+
mode = task.options.mode
|
|
135
|
+
if mode is not BackgroundExecutionMode.HOST:
|
|
136
|
+
position = "shared" if mode is BackgroundExecutionMode.SHARED else key
|
|
137
|
+
groups[position][key] = task.options
|
|
138
|
+
if groups:
|
|
139
|
+
_check_previous_owners(self._spec.project, self._spec.environment)
|
|
140
|
+
for position, tasks in groups.items():
|
|
141
|
+
slot = _ProcessSlot(position, tasks, self._spec, self._options)
|
|
142
|
+
slot.reserve()
|
|
143
|
+
self._slots[position] = slot
|
|
144
|
+
self._task_slots.update({key: slot for key in tasks})
|
|
145
|
+
|
|
146
|
+
def attach_container(self, container: AsyncContainer) -> None:
|
|
147
|
+
if self._container is not None:
|
|
148
|
+
raise RuntimeError("Background coordinator container is already attached")
|
|
149
|
+
self._container = container
|
|
150
|
+
for definition in self._worker_catalog.definitions:
|
|
151
|
+
self._tasks[f"worker:{definition.name}"].execution = _WorkerExecution(
|
|
152
|
+
definition, container, self._invocation_runtime, self._caller
|
|
153
|
+
).binding
|
|
154
|
+
|
|
155
|
+
def bind_durable(self, name: str, binding: _TaskExecution) -> None:
|
|
156
|
+
task = self._tasks[f"job:{name}"]
|
|
157
|
+
if self._started or task.execution is not None:
|
|
158
|
+
raise RuntimeError("Durable background task may only be bound once before start")
|
|
159
|
+
task.execution = binding
|
|
160
|
+
|
|
161
|
+
async def start(self) -> None:
|
|
162
|
+
if self._started:
|
|
163
|
+
raise RuntimeError("Background coordinator is already started")
|
|
164
|
+
self._started = True
|
|
165
|
+
self.prepare()
|
|
166
|
+
if self.profile is BackgroundExecutionProfile.DISABLED:
|
|
167
|
+
return
|
|
168
|
+
for key in self._tasks:
|
|
169
|
+
if key in self._initial:
|
|
170
|
+
await self._tasks[key].start()
|
|
171
|
+
for slot in self._slots.values():
|
|
172
|
+
await slot.start()
|
|
173
|
+
|
|
174
|
+
def snapshots(self) -> tuple[BackgroundTaskSnapshot, ...]:
|
|
175
|
+
remote = {item.id: item for slot in self._slots.values() for item in slot.snapshots()}
|
|
176
|
+
return tuple(
|
|
177
|
+
remote.get(key, task.snapshot())
|
|
178
|
+
for key, task in self._tasks.items()
|
|
179
|
+
if not self._child or key in self._local
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def request(self, key: str, *, start: bool) -> BackgroundTaskSnapshot:
|
|
183
|
+
if not self._started or self.profile is BackgroundExecutionProfile.DISABLED:
|
|
184
|
+
raise ValueError("Background execution is not active")
|
|
185
|
+
if key not in self._tasks or (self._child and key not in self._local):
|
|
186
|
+
raise KeyError(key)
|
|
187
|
+
slot = self._task_slots.get(key)
|
|
188
|
+
if slot is not None:
|
|
189
|
+
return slot.request(key, start)
|
|
190
|
+
return self._tasks[key].request(start)
|
|
191
|
+
|
|
192
|
+
async def stop(self) -> None:
|
|
193
|
+
# 即使启动前的预占或 Module 初始化失败,也必须释放已取得的位置。
|
|
194
|
+
operations = [task.shutdown() for key, task in self._tasks.items() if key in self._local]
|
|
195
|
+
operations.extend(slot.shutdown() for slot in self._slots.values())
|
|
196
|
+
outcomes = await asyncio.gather(*operations, return_exceptions=True)
|
|
197
|
+
self._started = False
|
|
198
|
+
failures = tuple(item for item in outcomes if isinstance(item, BaseException))
|
|
199
|
+
if failures:
|
|
200
|
+
raise BaseExceptionGroup("Background runtime drain failed", failures)
|
|
201
|
+
|
|
202
|
+
async def wait_for_runtime_failure(self) -> _BackgroundRuntimeFailure:
|
|
203
|
+
return await self._failures.get()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .catalog import CacheCatalog
|
|
2
|
+
from .contracts import CacheDefinition, DistributedCache
|
|
3
|
+
from .errors import CacheDefinitionError, DistributedCacheError
|
|
4
|
+
|
|
5
|
+
__all__ = (
|
|
6
|
+
"CacheCatalog",
|
|
7
|
+
"CacheDefinition",
|
|
8
|
+
"CacheDefinitionError",
|
|
9
|
+
"DistributedCache",
|
|
10
|
+
"DistributedCacheError",
|
|
11
|
+
)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Cache metadata 的 Application-local 动态聚合与 immutable view。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator, Mapping
|
|
6
|
+
from types import MappingProxyType
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ..modularity import ModuleRegistry
|
|
10
|
+
from .contracts import CacheDefinition
|
|
11
|
+
from .errors import CacheDefinitionError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CacheCatalog:
|
|
15
|
+
__slots__ = ("_definitions",)
|
|
16
|
+
|
|
17
|
+
def __init__(self, definitions: tuple[CacheDefinition[Any], ...]) -> None:
|
|
18
|
+
by_name: dict[str, CacheDefinition[Any]] = {}
|
|
19
|
+
for definition in definitions:
|
|
20
|
+
if not isinstance(definition, CacheDefinition):
|
|
21
|
+
raise CacheDefinitionError(
|
|
22
|
+
cache=repr(definition),
|
|
23
|
+
reason="catalog entries must be CacheDefinition values",
|
|
24
|
+
)
|
|
25
|
+
if definition.name in by_name:
|
|
26
|
+
raise CacheDefinitionError(
|
|
27
|
+
cache=definition.name,
|
|
28
|
+
reason="cache is declared more than once",
|
|
29
|
+
)
|
|
30
|
+
by_name[definition.name] = definition
|
|
31
|
+
self._definitions: Mapping[str, CacheDefinition[Any]] = MappingProxyType(
|
|
32
|
+
dict(sorted(by_name.items()))
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def definitions(self) -> tuple[CacheDefinition[Any], ...]:
|
|
37
|
+
return tuple(self._definitions.values())
|
|
38
|
+
|
|
39
|
+
def __iter__(self) -> Iterator[str]:
|
|
40
|
+
return iter(self._definitions)
|
|
41
|
+
|
|
42
|
+
def get(self, name: str) -> CacheDefinition[Any] | None:
|
|
43
|
+
return self._definitions.get(name)
|
|
44
|
+
|
|
45
|
+
def owns(self, definition: CacheDefinition[Any]) -> bool:
|
|
46
|
+
return self._definitions.get(definition.name) is definition
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _build_cache_catalog(registry: ModuleRegistry) -> CacheCatalog:
|
|
50
|
+
definitions: list[CacheDefinition[Any]] = []
|
|
51
|
+
owners: dict[str, str] = {}
|
|
52
|
+
for descriptor in registry.dependency_order:
|
|
53
|
+
declared = descriptor.module_type.cache_definitions
|
|
54
|
+
if not isinstance(declared, tuple):
|
|
55
|
+
raise CacheDefinitionError(
|
|
56
|
+
cache=str(descriptor.key),
|
|
57
|
+
reason="Module cache_definitions must be an immutable tuple",
|
|
58
|
+
)
|
|
59
|
+
for definition in declared:
|
|
60
|
+
if not isinstance(definition, CacheDefinition):
|
|
61
|
+
raise CacheDefinitionError(
|
|
62
|
+
cache=str(descriptor.key),
|
|
63
|
+
reason="Module cache_definitions contains an invalid item",
|
|
64
|
+
)
|
|
65
|
+
previous = owners.get(definition.name)
|
|
66
|
+
if previous is not None:
|
|
67
|
+
raise CacheDefinitionError(
|
|
68
|
+
cache=definition.name,
|
|
69
|
+
reason=f"cache is already owned by {previous}",
|
|
70
|
+
)
|
|
71
|
+
owners[definition.name] = str(descriptor.key)
|
|
72
|
+
definitions.append(definition)
|
|
73
|
+
return CacheCatalog(tuple(definitions))
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""强制 TTL 的 typed cache definition 与最小读写契约。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import timedelta
|
|
6
|
+
from typing import Any, Generic, Protocol, TypeVar
|
|
7
|
+
|
|
8
|
+
from pydantic import TypeAdapter
|
|
9
|
+
|
|
10
|
+
from .errors import CacheDefinitionError
|
|
11
|
+
|
|
12
|
+
_T = TypeVar("_T")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CacheDefinition(Generic[_T]):
|
|
16
|
+
__slots__ = ("_adapter", "_name", "_ttl", "_value_type")
|
|
17
|
+
|
|
18
|
+
def __init__(self, name: str, value_type: type[_T], *, ttl: timedelta) -> None:
|
|
19
|
+
if (
|
|
20
|
+
not isinstance(name, str)
|
|
21
|
+
or not name
|
|
22
|
+
or name != name.strip()
|
|
23
|
+
or name.startswith(":")
|
|
24
|
+
or name.endswith(":")
|
|
25
|
+
or "::" in name
|
|
26
|
+
# ':' 是 Redis key 的结构分隔符,definition name 不得制造跨 cache key 歧义。
|
|
27
|
+
or any(not (item.isalnum() or item in "._-") for item in name)
|
|
28
|
+
):
|
|
29
|
+
raise CacheDefinitionError(
|
|
30
|
+
cache=name if isinstance(name, str) else repr(name),
|
|
31
|
+
reason="name must be a trimmed cache identifier",
|
|
32
|
+
)
|
|
33
|
+
if not isinstance(value_type, type):
|
|
34
|
+
raise CacheDefinitionError(cache=name, reason="value_type must be a runtime type")
|
|
35
|
+
if not isinstance(ttl, timedelta) or ttl.total_seconds() <= 0:
|
|
36
|
+
raise CacheDefinitionError(cache=name, reason="ttl must be a positive timedelta")
|
|
37
|
+
if ttl.total_seconds() > 2_147_483_647:
|
|
38
|
+
raise CacheDefinitionError(cache=name, reason="ttl exceeds the supported Redis range")
|
|
39
|
+
self._name = name
|
|
40
|
+
self._value_type = value_type
|
|
41
|
+
self._ttl = ttl
|
|
42
|
+
self._adapter: TypeAdapter[_T] = TypeAdapter(value_type)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def name(self) -> str:
|
|
46
|
+
return self._name
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def value_type(self) -> type[_T]:
|
|
50
|
+
return self._value_type
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def ttl(self) -> timedelta:
|
|
54
|
+
return self._ttl
|
|
55
|
+
|
|
56
|
+
def __repr__(self) -> str:
|
|
57
|
+
return (
|
|
58
|
+
f"CacheDefinition(name={self.name!r}, value_type={self.value_type!r}, ttl={self.ttl!r})"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def _serialize(self, value: _T) -> bytes:
|
|
62
|
+
return self._adapter.dump_json(self._adapter.validate_python(value))
|
|
63
|
+
|
|
64
|
+
def _deserialize(self, value: bytes | str) -> _T:
|
|
65
|
+
return self._adapter.validate_json(value)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class DistributedCache(Protocol):
|
|
69
|
+
async def get(self, definition: CacheDefinition[_T], key: str) -> _T | None: ...
|
|
70
|
+
|
|
71
|
+
async def set(self, definition: CacheDefinition[_T], key: str, value: _T) -> None: ...
|
|
72
|
+
|
|
73
|
+
async def remove(self, definition: CacheDefinition[Any], key: str) -> None: ...
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Typed distributed cache definition 与 serialization failures。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..errors.base import FrameworkError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CacheDefinitionError(FrameworkError):
|
|
9
|
+
def __init__(self, *, cache: str, reason: str) -> None:
|
|
10
|
+
self.cache = cache
|
|
11
|
+
self.reason = reason
|
|
12
|
+
super().__init__(f"Invalid cache {cache!r}: {reason}")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DistributedCacheError(FrameworkError):
|
|
16
|
+
def __init__(self, *, operation: str, cache: str, error_type: str) -> None:
|
|
17
|
+
self.operation = operation
|
|
18
|
+
self.cache = cache
|
|
19
|
+
self.error_type = error_type
|
|
20
|
+
super().__init__(f"Distributed cache {operation} failed for {cache!r} with {error_type}")
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""pp 的轻量 argparse 入口;只有生成命令才加载 Developer Kit。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from ..hosting import ENVIRONMENT_VARIABLE as ENVIRONMENT_VARIABLE
|
|
14
|
+
from .errors import CommandError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _Command(str, Enum):
|
|
18
|
+
NEW = "new"
|
|
19
|
+
ADD = "add"
|
|
20
|
+
DEV = "dev"
|
|
21
|
+
DEV_INIT = "dev-init"
|
|
22
|
+
DB = "db"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _parser() -> argparse.ArgumentParser:
|
|
26
|
+
parser = argparse.ArgumentParser(prog="pp")
|
|
27
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
28
|
+
new = commands.add_parser(_Command.NEW.value, help="Create a project from bundled templates")
|
|
29
|
+
new.add_argument("name")
|
|
30
|
+
new.add_argument("--framework-wheel", type=Path, help="Validate with a local framework wheel")
|
|
31
|
+
new.add_argument("--dry-run", action="store_true")
|
|
32
|
+
add = commands.add_parser(_Command.ADD.value).add_subparsers(dest="kind", required=True)
|
|
33
|
+
module = add.add_parser("module")
|
|
34
|
+
module.add_argument("name")
|
|
35
|
+
module.add_argument("--dry-run", action="store_true")
|
|
36
|
+
dev = commands.add_parser(_Command.DEV.value)
|
|
37
|
+
dev.add_argument("--host", default="127.0.0.1")
|
|
38
|
+
dev.add_argument("--port", type=int, default=8000)
|
|
39
|
+
for command in (_Command.DEV_INIT, _Command.DB):
|
|
40
|
+
action = commands.add_parser(command.value)
|
|
41
|
+
action.add_argument(
|
|
42
|
+
"--environment", default=os.environ.get(ENVIRONMENT_VARIABLE, "development")
|
|
43
|
+
)
|
|
44
|
+
if command is _Command.DB:
|
|
45
|
+
action.add_argument("operation", choices=("revision", "upgrade", "status", "seed"))
|
|
46
|
+
action.add_argument("--module", required=True)
|
|
47
|
+
return parser
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
51
|
+
parser = _parser()
|
|
52
|
+
args = parser.parse_args(argv)
|
|
53
|
+
try:
|
|
54
|
+
return _execute(args)
|
|
55
|
+
except CommandError as error:
|
|
56
|
+
parser.exit(2, f"pp: {error}\n")
|
|
57
|
+
except subprocess.CalledProcessError as error:
|
|
58
|
+
parser.exit(
|
|
59
|
+
error.returncode, "pp: external command failed; diagnostic files were retained\n"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _execute(args: argparse.Namespace) -> int:
|
|
64
|
+
command = _Command(args.command)
|
|
65
|
+
if command in (_Command.NEW, _Command.ADD):
|
|
66
|
+
try:
|
|
67
|
+
from ..developer_kit.generation import add_module, new_project
|
|
68
|
+
except ModuleNotFoundError as error:
|
|
69
|
+
if error.name not in {"cookiecutter", "libcst"}:
|
|
70
|
+
raise
|
|
71
|
+
raise CommandError(
|
|
72
|
+
"Install python-platform[developer-kit] to use generation commands"
|
|
73
|
+
) from None
|
|
74
|
+
if command is _Command.NEW:
|
|
75
|
+
new_project(args.name, args.framework_wheel, args.dry_run)
|
|
76
|
+
else:
|
|
77
|
+
add_module(args.name, args.dry_run)
|
|
78
|
+
else:
|
|
79
|
+
from .runtime import database, dev_init, development_server
|
|
80
|
+
|
|
81
|
+
if command is _Command.DEV:
|
|
82
|
+
development_server(args.host, args.port)
|
|
83
|
+
elif command is _Command.DEV_INIT:
|
|
84
|
+
asyncio.run(dev_init(args.environment))
|
|
85
|
+
else:
|
|
86
|
+
asyncio.run(database(args.operation, args.environment, args.module))
|
|
87
|
+
return 0
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""当前项目的标准 pyproject/distribution/entry points,禁止第二份 Host 清单。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.metadata as metadata
|
|
6
|
+
import tomllib
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from ..application import Application
|
|
12
|
+
from ..modularity import AppModule
|
|
13
|
+
from .errors import CommandError
|
|
14
|
+
|
|
15
|
+
HOST_ENTRY_POINT_GROUP = "python_platform.hosts"
|
|
16
|
+
MODULE_ENTRY_POINT_GROUP = "python_platform.modules"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def read_project(path: Path) -> dict[str, Any]:
|
|
20
|
+
try:
|
|
21
|
+
with path.open("rb") as stream:
|
|
22
|
+
return tomllib.load(stream)
|
|
23
|
+
except (OSError, ValueError) as error:
|
|
24
|
+
raise CommandError(
|
|
25
|
+
f"Cannot read project metadata at {path}: {type(error).__name__}"
|
|
26
|
+
) from None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class Project:
|
|
31
|
+
root: Path
|
|
32
|
+
name: str
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def current(cls) -> Project:
|
|
36
|
+
root = Path.cwd().resolve()
|
|
37
|
+
project = read_project(root / "pyproject.toml").get("project", {})
|
|
38
|
+
name = project.get("name") if isinstance(project, dict) else None
|
|
39
|
+
if not isinstance(name, str) or not name:
|
|
40
|
+
raise CommandError("Current pyproject.toml must declare project.name")
|
|
41
|
+
return cls(root, name)
|
|
42
|
+
|
|
43
|
+
def host(self, name: str) -> metadata.EntryPoint:
|
|
44
|
+
try:
|
|
45
|
+
distribution = metadata.distribution(self.name)
|
|
46
|
+
except metadata.PackageNotFoundError:
|
|
47
|
+
raise CommandError(
|
|
48
|
+
"Install the current project with uv sync, then use uv run pp"
|
|
49
|
+
) from None
|
|
50
|
+
matches = tuple(
|
|
51
|
+
entry
|
|
52
|
+
for entry in distribution.entry_points
|
|
53
|
+
if entry.group == HOST_ENTRY_POINT_GROUP and entry.name == name
|
|
54
|
+
)
|
|
55
|
+
if len(matches) != 1:
|
|
56
|
+
raise CommandError(f"Project must declare exactly one {HOST_ENTRY_POINT_GROUP}:{name}")
|
|
57
|
+
return matches[0]
|
|
58
|
+
|
|
59
|
+
def module(self, name: str, application: Application) -> type[AppModule]:
|
|
60
|
+
# entry point 只命名已在当前图中的正式 Module;不导入未启用的 optional provider。
|
|
61
|
+
live = {str(item.key): item.module_type for item in application.modules.dependency_order}
|
|
62
|
+
matches = {
|
|
63
|
+
entry.value
|
|
64
|
+
for entry in metadata.entry_points(group=MODULE_ENTRY_POINT_GROUP)
|
|
65
|
+
if entry.name == name and entry.value in live
|
|
66
|
+
}
|
|
67
|
+
if len(matches) != 1:
|
|
68
|
+
raise CommandError(f"Module alias {name!r} must select exactly one live Module")
|
|
69
|
+
return live[matches.pop()]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""项目运行命令只编排正式 Host、迁移与 seed,不拥有业务初始化规则。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
from . import ENVIRONMENT_VARIABLE
|
|
9
|
+
from .errors import CommandError
|
|
10
|
+
from .project import Project
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def database(operation: str, environment: str, module: str) -> None:
|
|
14
|
+
from ..application import Application
|
|
15
|
+
from ..application_services.seeding import DataSeeder
|
|
16
|
+
from ..sqlalchemy import SqlAlchemyMigrator
|
|
17
|
+
|
|
18
|
+
project = Project.current()
|
|
19
|
+
factory = project.host("application").load()
|
|
20
|
+
application = factory(environment=environment)
|
|
21
|
+
if not isinstance(application, Application):
|
|
22
|
+
raise CommandError("The application entry point must return Application")
|
|
23
|
+
try:
|
|
24
|
+
selected = project.module(module, application)
|
|
25
|
+
migrator = SqlAlchemyMigrator(application)
|
|
26
|
+
if operation == "revision":
|
|
27
|
+
await migrator.revision(selected)
|
|
28
|
+
elif operation == "upgrade":
|
|
29
|
+
await migrator.upgrade_heads(module=selected)
|
|
30
|
+
elif operation == "status":
|
|
31
|
+
await migrator.check_current_heads(module=selected)
|
|
32
|
+
elif operation == "seed":
|
|
33
|
+
await migrator.check_current_heads(module=selected)
|
|
34
|
+
await application._start(run_services=False)
|
|
35
|
+
await DataSeeder(application).seed_all(module=selected)
|
|
36
|
+
else:
|
|
37
|
+
raise CommandError(f"Unknown database operation: {operation}")
|
|
38
|
+
finally:
|
|
39
|
+
await application.stop()
|
|
40
|
+
print(f"Database {operation} completed")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def dev_init(environment: str) -> None:
|
|
44
|
+
if environment != "development":
|
|
45
|
+
raise CommandError("dev-init is only available in the development environment")
|
|
46
|
+
project = Project.current()
|
|
47
|
+
subprocess.run(
|
|
48
|
+
["docker", "compose", "-f", str(project.root / "compose.dev.yaml"), "up", "-d", "--wait"],
|
|
49
|
+
cwd=project.root,
|
|
50
|
+
check=True,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def development_server(host: str, port: int) -> None:
|
|
55
|
+
from ..fastapi.server import _run_host
|
|
56
|
+
|
|
57
|
+
project = Project.current()
|
|
58
|
+
entry = project.host("web")
|
|
59
|
+
os.environ[ENVIRONMENT_VARIABLE] = "development"
|
|
60
|
+
# 原生 access handler 不传播到 Host 的安全 formatter,不能记录含凭据的 query。
|
|
61
|
+
_run_host(
|
|
62
|
+
entry.value,
|
|
63
|
+
host=host,
|
|
64
|
+
port=port,
|
|
65
|
+
reload=True,
|
|
66
|
+
project=project.root,
|
|
67
|
+
environment="development",
|
|
68
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Configuration public contracts and Application-internal composition owners."""
|
|
2
|
+
|
|
3
|
+
from .contracts import (
|
|
4
|
+
ConfigurationContribution,
|
|
5
|
+
ConfigurationProvenance,
|
|
6
|
+
ConfigurationSource,
|
|
7
|
+
ConfigurationSourceDiagnostic,
|
|
8
|
+
ConfigurationSourceStatus,
|
|
9
|
+
ConfigurationValueDiagnostic,
|
|
10
|
+
)
|
|
11
|
+
from .root import ConfigurationRoot
|
|
12
|
+
|
|
13
|
+
__all__ = (
|
|
14
|
+
"ConfigurationContribution",
|
|
15
|
+
"ConfigurationProvenance",
|
|
16
|
+
"ConfigurationRoot",
|
|
17
|
+
"ConfigurationSource",
|
|
18
|
+
"ConfigurationSourceDiagnostic",
|
|
19
|
+
"ConfigurationSourceStatus",
|
|
20
|
+
"ConfigurationValueDiagnostic",
|
|
21
|
+
)
|