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,201 @@
|
|
|
1
|
+
"""Durable delivery envelope 与现有 REQUEST/ACTION pipeline 的 provider-neutral bridge。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Any
|
|
10
|
+
from uuid import uuid4
|
|
11
|
+
|
|
12
|
+
from dishka import AsyncContainer, Scope
|
|
13
|
+
from pydantic import BaseModel, ValidationError
|
|
14
|
+
|
|
15
|
+
from ..application_services.invocation import _InvocationRuntime
|
|
16
|
+
from ..authorization import CurrentUser
|
|
17
|
+
from ..invocation import BackgroundSystemCaller
|
|
18
|
+
from ..observability.context import invocation_span
|
|
19
|
+
from .catalog import BackgroundJobCatalog
|
|
20
|
+
from .contracts import BackgroundJobContext, BackgroundJobDefinition
|
|
21
|
+
from .errors import BackgroundJobDefinitionError, BackgroundJobPayloadError
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _BackgroundJobExecutor:
|
|
25
|
+
"""Queue provider 只交付稳定 facts;事务、scope 与 event 顺序仍由 framework owner 执行。"""
|
|
26
|
+
|
|
27
|
+
__slots__ = ("_caller", "_catalog", "_container", "_runtime")
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
container: AsyncContainer,
|
|
32
|
+
runtime: _InvocationRuntime,
|
|
33
|
+
catalog: BackgroundJobCatalog,
|
|
34
|
+
caller: BackgroundSystemCaller,
|
|
35
|
+
) -> None:
|
|
36
|
+
self._container = container
|
|
37
|
+
self._runtime = runtime
|
|
38
|
+
self._catalog = catalog
|
|
39
|
+
self._caller = caller
|
|
40
|
+
|
|
41
|
+
async def execute(
|
|
42
|
+
self,
|
|
43
|
+
payload: bytes | None,
|
|
44
|
+
*,
|
|
45
|
+
job_id: str,
|
|
46
|
+
attempt: int,
|
|
47
|
+
scheduled_at: datetime,
|
|
48
|
+
cancellation_event: asyncio.Event,
|
|
49
|
+
connection_name: str,
|
|
50
|
+
) -> None:
|
|
51
|
+
name, version, raw_payload, trace_context = _decode_envelope(payload)
|
|
52
|
+
definition = self._catalog.get(name, version)
|
|
53
|
+
if definition is None:
|
|
54
|
+
raise BackgroundJobDefinitionError(
|
|
55
|
+
job=name,
|
|
56
|
+
reason=f"version {version} is not registered in this Application",
|
|
57
|
+
)
|
|
58
|
+
try:
|
|
59
|
+
validated = definition.payload_type.model_validate(raw_payload)
|
|
60
|
+
except ValidationError:
|
|
61
|
+
raise BackgroundJobPayloadError(
|
|
62
|
+
job=name,
|
|
63
|
+
version=version,
|
|
64
|
+
reason="delivery payload failed schema validation",
|
|
65
|
+
) from None
|
|
66
|
+
context = BackgroundJobContext(
|
|
67
|
+
job_id=job_id,
|
|
68
|
+
name=name,
|
|
69
|
+
version=version,
|
|
70
|
+
attempt=attempt,
|
|
71
|
+
scheduled_at=scheduled_at,
|
|
72
|
+
system_caller=self._caller,
|
|
73
|
+
_cancellation_event=cancellation_event,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
async def run() -> object:
|
|
77
|
+
async with self._container(scope=Scope.REQUEST) as request_container:
|
|
78
|
+
from ..invocation.interception import _EntryInterceptionCatalog
|
|
79
|
+
|
|
80
|
+
entries = await request_container.get(_EntryInterceptionCatalog)
|
|
81
|
+
return await entries.invoke(
|
|
82
|
+
self._runtime,
|
|
83
|
+
request_container,
|
|
84
|
+
definition.handler_type,
|
|
85
|
+
"execute",
|
|
86
|
+
(validated, context),
|
|
87
|
+
connection_name=connection_name,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
async def execute_with_ownership() -> None:
|
|
91
|
+
# lease/caller 必须由实际执行 Task 持有,且在业务及清理完成后才释放。
|
|
92
|
+
async with self._runtime.outer_lease():
|
|
93
|
+
token = self._runtime.bind_caller(CurrentUser.anonymous(), str(uuid4()))
|
|
94
|
+
try:
|
|
95
|
+
with invocation_span(
|
|
96
|
+
self._runtime.tracer, definition.name, carrier=trace_context
|
|
97
|
+
):
|
|
98
|
+
await run()
|
|
99
|
+
finally:
|
|
100
|
+
self._runtime.reset_caller(token)
|
|
101
|
+
|
|
102
|
+
await asyncio.wait_for(
|
|
103
|
+
execute_with_ownership(),
|
|
104
|
+
timeout=definition.timeout.total_seconds(),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _encode_envelope(
|
|
109
|
+
definition: BackgroundJobDefinition[BaseModel],
|
|
110
|
+
payload: BaseModel,
|
|
111
|
+
*,
|
|
112
|
+
trace_context: Mapping[str, str] | None = None,
|
|
113
|
+
) -> bytes:
|
|
114
|
+
try:
|
|
115
|
+
validated = definition.payload_type.model_validate(payload)
|
|
116
|
+
except ValidationError:
|
|
117
|
+
raise BackgroundJobPayloadError(
|
|
118
|
+
job=definition.name,
|
|
119
|
+
version=definition.version,
|
|
120
|
+
reason="enqueue payload failed schema validation",
|
|
121
|
+
) from None
|
|
122
|
+
return json.dumps(
|
|
123
|
+
{
|
|
124
|
+
"name": definition.name,
|
|
125
|
+
"version": definition.version,
|
|
126
|
+
"payload": validated.model_dump(mode="json"),
|
|
127
|
+
**({"trace_context": dict(trace_context)} if trace_context else {}),
|
|
128
|
+
},
|
|
129
|
+
sort_keys=True,
|
|
130
|
+
separators=(",", ":"),
|
|
131
|
+
ensure_ascii=False,
|
|
132
|
+
).encode()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _encode_scheduled_envelope(
|
|
136
|
+
definition: BackgroundJobDefinition[BaseModel],
|
|
137
|
+
canonical_payload: bytes,
|
|
138
|
+
) -> bytes:
|
|
139
|
+
try:
|
|
140
|
+
payload = json.loads(canonical_payload)
|
|
141
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
142
|
+
raise AssertionError("Catalog must contain canonical UTF-8 JSON payload") from None
|
|
143
|
+
return json.dumps(
|
|
144
|
+
{
|
|
145
|
+
"name": definition.name,
|
|
146
|
+
"version": definition.version,
|
|
147
|
+
"payload": payload,
|
|
148
|
+
},
|
|
149
|
+
sort_keys=True,
|
|
150
|
+
separators=(",", ":"),
|
|
151
|
+
ensure_ascii=False,
|
|
152
|
+
).encode()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _decode_envelope(payload: bytes | None) -> tuple[str, int, object, Mapping[str, str]]:
|
|
156
|
+
if payload is None:
|
|
157
|
+
raise BackgroundJobPayloadError(
|
|
158
|
+
job="<unknown>",
|
|
159
|
+
version=0,
|
|
160
|
+
reason="delivery envelope is missing",
|
|
161
|
+
)
|
|
162
|
+
try:
|
|
163
|
+
candidate: Any = json.loads(payload)
|
|
164
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
165
|
+
raise BackgroundJobPayloadError(
|
|
166
|
+
job="<unknown>",
|
|
167
|
+
version=0,
|
|
168
|
+
reason="delivery envelope must be UTF-8 JSON",
|
|
169
|
+
) from None
|
|
170
|
+
required_keys = {"name", "version", "payload"}
|
|
171
|
+
if not isinstance(candidate, dict) or not (
|
|
172
|
+
required_keys <= set(candidate) <= required_keys | {"trace_context"}
|
|
173
|
+
):
|
|
174
|
+
raise BackgroundJobPayloadError(
|
|
175
|
+
job="<unknown>",
|
|
176
|
+
version=0,
|
|
177
|
+
reason="delivery envelope shape is invalid",
|
|
178
|
+
)
|
|
179
|
+
name = candidate["name"]
|
|
180
|
+
version = candidate["version"]
|
|
181
|
+
if not isinstance(name, str) or not name or name != name.strip():
|
|
182
|
+
raise BackgroundJobPayloadError(
|
|
183
|
+
job="<unknown>",
|
|
184
|
+
version=0,
|
|
185
|
+
reason="delivery envelope identity is invalid",
|
|
186
|
+
)
|
|
187
|
+
if not isinstance(version, int) or isinstance(version, bool) or version < 1:
|
|
188
|
+
raise BackgroundJobPayloadError(
|
|
189
|
+
job=name,
|
|
190
|
+
version=0,
|
|
191
|
+
reason="delivery envelope version is invalid",
|
|
192
|
+
)
|
|
193
|
+
trace_context = candidate.get("trace_context", {})
|
|
194
|
+
if not isinstance(trace_context, dict) or any(
|
|
195
|
+
key != "traceparent" or not isinstance(value, str) or len(value) > 512
|
|
196
|
+
for key, value in trace_context.items()
|
|
197
|
+
):
|
|
198
|
+
raise BackgroundJobPayloadError(
|
|
199
|
+
job=name, version=version, reason="delivery trace context shape is invalid"
|
|
200
|
+
)
|
|
201
|
+
return name, version, candidate["payload"], trace_context
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 janbjorge
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Active SQLAlchemy ACTION 内的 transactional PgQueuer enqueue implementation。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import timedelta
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import asyncpg # type: ignore[import-untyped]
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
from ...application_services.invocation import _InvocationRuntime
|
|
12
|
+
from ...observability.context import capture_trace_context
|
|
13
|
+
from ...options import Options
|
|
14
|
+
from ...sqlalchemy import SqlAlchemySessionProvider
|
|
15
|
+
from ..catalog import BackgroundJobCatalog
|
|
16
|
+
from ..contracts import BackgroundJobDefinition
|
|
17
|
+
from ..errors import BackgroundJobDefinitionError
|
|
18
|
+
from ..execution import _encode_envelope
|
|
19
|
+
from .options import PgQueuerBackgroundJobOptions
|
|
20
|
+
from .runtime import _entrypoint_name, _queries_for_connection
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _PgQueuerBackgroundJobEnqueuer:
|
|
24
|
+
__slots__ = ("_catalog", "_options", "_provider", "_runtime")
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
provider: SqlAlchemySessionProvider,
|
|
29
|
+
catalog: BackgroundJobCatalog,
|
|
30
|
+
options: Options[PgQueuerBackgroundJobOptions],
|
|
31
|
+
runtime: _InvocationRuntime,
|
|
32
|
+
) -> None:
|
|
33
|
+
self._provider = provider
|
|
34
|
+
self._catalog = catalog
|
|
35
|
+
self._options = options.value
|
|
36
|
+
self._runtime = runtime
|
|
37
|
+
|
|
38
|
+
async def enqueue(
|
|
39
|
+
self,
|
|
40
|
+
definition: BackgroundJobDefinition[BaseModel],
|
|
41
|
+
payload: BaseModel,
|
|
42
|
+
*,
|
|
43
|
+
priority: int = 0,
|
|
44
|
+
delay: timedelta | None = None,
|
|
45
|
+
) -> str:
|
|
46
|
+
if not self._catalog.owns(definition):
|
|
47
|
+
raise BackgroundJobDefinitionError(
|
|
48
|
+
job=getattr(definition, "name", "<unknown>"),
|
|
49
|
+
reason="definition is not owned by this Application catalog",
|
|
50
|
+
)
|
|
51
|
+
if (
|
|
52
|
+
not isinstance(priority, int)
|
|
53
|
+
or isinstance(priority, bool)
|
|
54
|
+
or not -(2**31) <= priority < 2**31
|
|
55
|
+
):
|
|
56
|
+
raise ValueError("priority must be a PostgreSQL int32 value")
|
|
57
|
+
if delay is not None and (not isinstance(delay, timedelta) or delay.total_seconds() < 0):
|
|
58
|
+
raise ValueError("delay must be a non-negative timedelta or None")
|
|
59
|
+
work = self._provider._manager.require_current(transactional=True)
|
|
60
|
+
if work.connection_name != self._options.connection_name:
|
|
61
|
+
raise RuntimeError("transactional enqueue connection does not match current UnitOfWork")
|
|
62
|
+
session = await self._provider.get_session()
|
|
63
|
+
connection = await session.connection()
|
|
64
|
+
raw = await connection.get_raw_connection()
|
|
65
|
+
driver_connection: Any = raw.driver_connection
|
|
66
|
+
if not isinstance(driver_connection, asyncpg.Connection):
|
|
67
|
+
raise TypeError("SQLAlchemy connection does not expose an asyncpg connection")
|
|
68
|
+
queries = _queries_for_connection(
|
|
69
|
+
driver_connection,
|
|
70
|
+
self._options,
|
|
71
|
+
)
|
|
72
|
+
identifiers = await queries.enqueue(
|
|
73
|
+
_entrypoint_name(definition.name, definition.version),
|
|
74
|
+
_encode_envelope(
|
|
75
|
+
definition, payload, trace_context=capture_trace_context(self._runtime.tracer)
|
|
76
|
+
),
|
|
77
|
+
priority,
|
|
78
|
+
delay,
|
|
79
|
+
)
|
|
80
|
+
if len(identifiers) != 1:
|
|
81
|
+
raise RuntimeError("PgQueuer enqueue did not return one job identifier")
|
|
82
|
+
return str(identifiers[0])
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Install the reviewed PgQueuer 1.3.2 schema-aware SQL snapshot。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from importlib import resources
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from alembic import context, op
|
|
10
|
+
|
|
11
|
+
revision = "python_platform_pgqueuer_0001"
|
|
12
|
+
down_revision = None
|
|
13
|
+
branch_labels = ("python_platform_background_jobs_pgqueuer",)
|
|
14
|
+
depends_on = None
|
|
15
|
+
|
|
16
|
+
_EXPECTED_SHA256 = "2c937aa4c42f780ece67df2739760e10f3dbcc6e299862e1cbf1ccdbaa0baf06"
|
|
17
|
+
_SCHEMA = "python_platform_jobs"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def upgrade() -> None:
|
|
21
|
+
sql = _install_sql()
|
|
22
|
+
_execute_driver_sql(sql)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def downgrade() -> None:
|
|
26
|
+
config = context.get_context().config
|
|
27
|
+
if config is None:
|
|
28
|
+
raise RuntimeError("PgQueuer downgrade requires an Alembic Config")
|
|
29
|
+
if config.attributes.get("background_execution_disabled") is not True:
|
|
30
|
+
raise RuntimeError("PgQueuer downgrade requires disabled background execution")
|
|
31
|
+
active = (
|
|
32
|
+
op.get_bind()
|
|
33
|
+
.exec_driver_sql(
|
|
34
|
+
"SELECT count(*) FROM python_platform_jobs.pgqueuer "
|
|
35
|
+
"WHERE status IN ('queued', 'picked')"
|
|
36
|
+
)
|
|
37
|
+
.scalar_one()
|
|
38
|
+
)
|
|
39
|
+
if active:
|
|
40
|
+
raise RuntimeError("PgQueuer downgrade refuses queued or picked jobs")
|
|
41
|
+
op.get_bind().exec_driver_sql(f"DROP SCHEMA {_SCHEMA} CASCADE")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _install_sql() -> str:
|
|
45
|
+
sql = (
|
|
46
|
+
resources.files("python_platform.background_jobs.pgqueuer")
|
|
47
|
+
.joinpath("sql")
|
|
48
|
+
.joinpath("pgqueuer_1_3_2_install.sql")
|
|
49
|
+
.read_text(encoding="utf-8")
|
|
50
|
+
)
|
|
51
|
+
# Repository text files retain one POSIX terminator;fingerprint 仍绑定 upstream
|
|
52
|
+
# build_install_query() 的逐字输出,不能吞掉其有意义的尾随空格。
|
|
53
|
+
generated_sql = sql.removesuffix("\n")
|
|
54
|
+
if hashlib.sha256(generated_sql.encode()).hexdigest() != _EXPECTED_SHA256:
|
|
55
|
+
raise RuntimeError("PgQueuer install SQL fingerprint changed")
|
|
56
|
+
return generated_sql
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _execute_driver_sql(sql: str) -> None:
|
|
60
|
+
# asyncpg 不接受 prepared multi-statement;复用 Alembic 当前 transaction 的 raw driver。
|
|
61
|
+
proxied: Any = op.get_bind().connection
|
|
62
|
+
adapted: Any = proxied.dbapi_connection
|
|
63
|
+
adapted.await_(adapted.driver_connection.execute(sql))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""PgQueuer provider Alembic revision package。"""
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Host-selected PgQueuer durable job provider Module。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import asyncpg # type: ignore[import-untyped]
|
|
9
|
+
from dishka import Provider, Scope
|
|
10
|
+
|
|
11
|
+
from ...background_jobs import BackgroundJobCatalog
|
|
12
|
+
from ...background_workers.runtime import _BackgroundCoordinator
|
|
13
|
+
from ...lifecycle import ConfigureContext, InitializeContext, ShutdownContext
|
|
14
|
+
from ...modularity import AppModule
|
|
15
|
+
from ...options import Options
|
|
16
|
+
from ...sqlalchemy import (
|
|
17
|
+
SqlAlchemyMigrationRegistration,
|
|
18
|
+
SqlAlchemyPersistenceModule,
|
|
19
|
+
contributes_sqlalchemy_migrations,
|
|
20
|
+
)
|
|
21
|
+
from ...sqlalchemy.options import ConnectionStringsOptions
|
|
22
|
+
from ..contracts import BackgroundJobEnqueuer
|
|
23
|
+
from ..execution import _BackgroundJobExecutor
|
|
24
|
+
from .enqueue import _PgQueuerBackgroundJobEnqueuer
|
|
25
|
+
from .options import PgQueuerBackgroundJobOptions
|
|
26
|
+
from .runtime import _PgQueuerRuntime
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@contributes_sqlalchemy_migrations(
|
|
30
|
+
SqlAlchemyMigrationRegistration(
|
|
31
|
+
migrations_package="python_platform.background_jobs.pgqueuer.migrations",
|
|
32
|
+
branch_label="python_platform_background_jobs_pgqueuer",
|
|
33
|
+
owned_schemas=("python_platform_jobs",),
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
class PgQueuerBackgroundJobsModule(AppModule):
|
|
37
|
+
dependencies = (SqlAlchemyPersistenceModule,)
|
|
38
|
+
|
|
39
|
+
def __init__(self) -> None:
|
|
40
|
+
self._runtimes: list[_PgQueuerRuntime] = []
|
|
41
|
+
self._pool: Any | None = None
|
|
42
|
+
|
|
43
|
+
def configure(self, context: ConfigureContext) -> None:
|
|
44
|
+
context.configure(PgQueuerBackgroundJobOptions, section="pgqueuer_background_jobs")
|
|
45
|
+
provider = Provider()
|
|
46
|
+
provider.provide(
|
|
47
|
+
_BackgroundJobExecutor,
|
|
48
|
+
provides=_BackgroundJobExecutor,
|
|
49
|
+
scope=Scope.APP,
|
|
50
|
+
)
|
|
51
|
+
provider.provide(
|
|
52
|
+
_PgQueuerBackgroundJobEnqueuer,
|
|
53
|
+
provides=BackgroundJobEnqueuer,
|
|
54
|
+
scope=Scope.ACTION,
|
|
55
|
+
)
|
|
56
|
+
context.services.contribute(provider, reason="PgQueuer durable background job provider")
|
|
57
|
+
|
|
58
|
+
async def initialize(self, context: InitializeContext) -> None:
|
|
59
|
+
coordinator = await context.container.get(_BackgroundCoordinator)
|
|
60
|
+
names = coordinator.local_job_names
|
|
61
|
+
if not names:
|
|
62
|
+
return
|
|
63
|
+
connections = context.options.get(ConnectionStringsOptions)
|
|
64
|
+
options = await context.container.get(Options[PgQueuerBackgroundJobOptions])
|
|
65
|
+
catalog = await context.container.get(BackgroundJobCatalog)
|
|
66
|
+
executor = await context.container.get(_BackgroundJobExecutor)
|
|
67
|
+
if options.value.executor_enabled and options.value.pool_max_size <= len(names):
|
|
68
|
+
raise ValueError("PgQueuer pool must have room for each local job listener and queries")
|
|
69
|
+
self._pool = await asyncpg.create_pool(
|
|
70
|
+
dsn=connections._url_for(options.value.connection_name).replace(
|
|
71
|
+
"postgresql+asyncpg://", "postgresql://", 1
|
|
72
|
+
),
|
|
73
|
+
min_size=options.value.pool_min_size,
|
|
74
|
+
max_size=options.value.pool_max_size,
|
|
75
|
+
)
|
|
76
|
+
for name in names:
|
|
77
|
+
# 每个逻辑 Job 原生 manager 只注册其版本/Cron。独立 stop 不影响共享进程其他任务,
|
|
78
|
+
# 不动态改写原生 registry,不删除或抢领已暂停任务的队列。
|
|
79
|
+
runtime = _PgQueuerRuntime(
|
|
80
|
+
self._pool,
|
|
81
|
+
options,
|
|
82
|
+
catalog,
|
|
83
|
+
executor,
|
|
84
|
+
name,
|
|
85
|
+
)
|
|
86
|
+
self._runtimes.append(runtime)
|
|
87
|
+
coordinator.bind_durable(name, runtime.binding)
|
|
88
|
+
await runtime.initialize()
|
|
89
|
+
|
|
90
|
+
async def shutdown(self, context: ShutdownContext) -> None:
|
|
91
|
+
outcomes = await asyncio.gather(
|
|
92
|
+
*(runtime.close() for runtime in self._runtimes), return_exceptions=True
|
|
93
|
+
)
|
|
94
|
+
failures = [outcome for outcome in outcomes if isinstance(outcome, BaseException)]
|
|
95
|
+
if self._pool is not None:
|
|
96
|
+
try:
|
|
97
|
+
await self._pool.close()
|
|
98
|
+
except BaseException as error:
|
|
99
|
+
failures.append(error)
|
|
100
|
+
self._pool = None
|
|
101
|
+
if failures:
|
|
102
|
+
raise BaseExceptionGroup("PgQueuer cleanup failed", tuple(failures))
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""PgQueuer provider options;连接字符串仍由 consuming Host 的 SQLAlchemy options 拥有。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from datetime import timedelta
|
|
7
|
+
|
|
8
|
+
from pydantic import ConfigDict, Field, field_validator, model_validator
|
|
9
|
+
|
|
10
|
+
from ...options import BaseOptions
|
|
11
|
+
from ...sqlalchemy.options import _validate_connection_name
|
|
12
|
+
|
|
13
|
+
_POSTGRES_IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PgQueuerBackgroundJobOptions(BaseOptions):
|
|
17
|
+
model_config = ConfigDict(extra="forbid")
|
|
18
|
+
|
|
19
|
+
connection_name: str = "default"
|
|
20
|
+
schema_name: str = "python_platform_jobs"
|
|
21
|
+
prefix: str = ""
|
|
22
|
+
executor_enabled: bool = True
|
|
23
|
+
scheduler_enabled: bool = True
|
|
24
|
+
batch_size: int = Field(default=10, ge=1)
|
|
25
|
+
max_concurrent_jobs: int = Field(default=20, ge=2)
|
|
26
|
+
poll_interval: timedelta = Field(default=timedelta(seconds=1), gt=timedelta(0))
|
|
27
|
+
heartbeat_timeout: timedelta = Field(default=timedelta(seconds=30), gt=timedelta(0))
|
|
28
|
+
log_aggregation_interval: timedelta = Field(
|
|
29
|
+
default=timedelta(seconds=30),
|
|
30
|
+
ge=timedelta(0),
|
|
31
|
+
)
|
|
32
|
+
pool_min_size: int = Field(default=1, ge=0)
|
|
33
|
+
pool_max_size: int = Field(default=10, ge=2)
|
|
34
|
+
|
|
35
|
+
@field_validator("connection_name")
|
|
36
|
+
@classmethod
|
|
37
|
+
def _connection_name_is_valid(cls, value: str) -> str:
|
|
38
|
+
_validate_connection_name(value)
|
|
39
|
+
if value != "default":
|
|
40
|
+
raise ValueError("Phase 5 PgQueuer migration owns only the default connection")
|
|
41
|
+
return value
|
|
42
|
+
|
|
43
|
+
@field_validator("schema_name")
|
|
44
|
+
@classmethod
|
|
45
|
+
def _schema_is_valid(cls, value: str) -> str:
|
|
46
|
+
if not _POSTGRES_IDENTIFIER.fullmatch(value):
|
|
47
|
+
raise ValueError("schema_name must be one unquoted PostgreSQL identifier")
|
|
48
|
+
normalized = value.lower()
|
|
49
|
+
if normalized != "python_platform_jobs":
|
|
50
|
+
raise ValueError("Phase 5 PgQueuer migration owns only schema python_platform_jobs")
|
|
51
|
+
return normalized
|
|
52
|
+
|
|
53
|
+
@field_validator("prefix")
|
|
54
|
+
@classmethod
|
|
55
|
+
def _prefix_is_valid(cls, value: str) -> str:
|
|
56
|
+
if value and not _POSTGRES_IDENTIFIER.fullmatch(value):
|
|
57
|
+
raise ValueError("prefix must be an unquoted PostgreSQL identifier prefix")
|
|
58
|
+
if value:
|
|
59
|
+
raise ValueError("Phase 5 PgQueuer migration owns only the empty prefix")
|
|
60
|
+
return value
|
|
61
|
+
|
|
62
|
+
@model_validator(mode="after")
|
|
63
|
+
def _runtime_shape_is_valid(self) -> PgQueuerBackgroundJobOptions:
|
|
64
|
+
if not self.executor_enabled and not self.scheduler_enabled:
|
|
65
|
+
raise ValueError("executor_enabled or scheduler_enabled must be true")
|
|
66
|
+
if self.max_concurrent_jobs < self.batch_size * 2:
|
|
67
|
+
raise ValueError("max_concurrent_jobs must be at least twice batch_size")
|
|
68
|
+
if self.pool_min_size > self.pool_max_size:
|
|
69
|
+
raise ValueError("pool_min_size cannot exceed pool_max_size")
|
|
70
|
+
return self
|