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,287 @@
|
|
|
1
|
+
"""Durable job versions 与 Cron schedules 的 immutable Application catalog。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import Iterator, Mapping
|
|
8
|
+
from dataclasses import replace
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from types import MappingProxyType
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, ValidationError
|
|
13
|
+
|
|
14
|
+
from ..modularity import AppModule, ModuleDescriptor, ModuleKey, ModuleRegistry
|
|
15
|
+
from .contracts import BackgroundJobDefinition, BackgroundJobSchedule
|
|
16
|
+
from .errors import BackgroundJobDefinitionError, BackgroundJobPayloadError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BackgroundJobCatalog:
|
|
20
|
+
__slots__ = (
|
|
21
|
+
"_current",
|
|
22
|
+
"_declarations",
|
|
23
|
+
"_definitions",
|
|
24
|
+
"_fingerprint",
|
|
25
|
+
"_schedules",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
definitions: tuple[BackgroundJobDefinition[BaseModel], ...],
|
|
31
|
+
schedules: tuple[BackgroundJobSchedule, ...],
|
|
32
|
+
*,
|
|
33
|
+
declarations: tuple[tuple[type[AppModule], str, tuple[object, ...]], ...],
|
|
34
|
+
owners: Mapping[tuple[str, int], ModuleKey],
|
|
35
|
+
) -> None:
|
|
36
|
+
self._definitions = MappingProxyType(
|
|
37
|
+
{(item.name, item.version): item for item in definitions}
|
|
38
|
+
)
|
|
39
|
+
self._current = MappingProxyType({item.name: item for item in definitions if item.current})
|
|
40
|
+
self._schedules = MappingProxyType({item.name: item for item in schedules})
|
|
41
|
+
self._declarations = declarations
|
|
42
|
+
encoded = json.dumps(
|
|
43
|
+
{
|
|
44
|
+
"definitions": [
|
|
45
|
+
{
|
|
46
|
+
"name": item.name,
|
|
47
|
+
"version": item.version,
|
|
48
|
+
"current": item.current,
|
|
49
|
+
"owner": str(owners[(item.name, item.version)]),
|
|
50
|
+
"timeout_seconds": item.timeout.total_seconds(),
|
|
51
|
+
"max_attempts": item.retry_policy.max_attempts,
|
|
52
|
+
"initial_delay_seconds": item.retry_policy.initial_delay.total_seconds(),
|
|
53
|
+
"max_delay_seconds": item.retry_policy.max_delay.total_seconds(),
|
|
54
|
+
"backoff_multiplier": item.retry_policy.backoff_multiplier,
|
|
55
|
+
}
|
|
56
|
+
for item in definitions
|
|
57
|
+
],
|
|
58
|
+
"schedules": [
|
|
59
|
+
{
|
|
60
|
+
"name": item.name,
|
|
61
|
+
"cron": item.cron,
|
|
62
|
+
"job_name": item.job_name,
|
|
63
|
+
"job_version": item.job_version,
|
|
64
|
+
"payload": item.payload.decode(),
|
|
65
|
+
}
|
|
66
|
+
for item in schedules
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
sort_keys=True,
|
|
70
|
+
separators=(",", ":"),
|
|
71
|
+
).encode()
|
|
72
|
+
self._fingerprint = hashlib.sha256(encoded).hexdigest()
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def definitions(self) -> tuple[BackgroundJobDefinition[BaseModel], ...]:
|
|
76
|
+
return tuple(self._definitions.values())
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def schedules(self) -> tuple[BackgroundJobSchedule, ...]:
|
|
80
|
+
return tuple(self._schedules.values())
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def fingerprint(self) -> str:
|
|
84
|
+
return self._fingerprint
|
|
85
|
+
|
|
86
|
+
def __iter__(self) -> Iterator[tuple[str, int]]:
|
|
87
|
+
return iter(self._definitions)
|
|
88
|
+
|
|
89
|
+
def get(self, name: str, version: int) -> BackgroundJobDefinition[BaseModel] | None:
|
|
90
|
+
return self._definitions.get((name, version))
|
|
91
|
+
|
|
92
|
+
def current(self, name: str) -> BackgroundJobDefinition[BaseModel] | None:
|
|
93
|
+
return self._current.get(name)
|
|
94
|
+
|
|
95
|
+
def get_schedule(self, name: str) -> BackgroundJobSchedule | None:
|
|
96
|
+
return self._schedules.get(name)
|
|
97
|
+
|
|
98
|
+
def owns(self, definition: BackgroundJobDefinition[BaseModel]) -> bool:
|
|
99
|
+
return self._definitions.get((definition.name, definition.version)) is definition
|
|
100
|
+
|
|
101
|
+
def _assert_unchanged(self) -> None:
|
|
102
|
+
for module_type, attribute, declared in self._declarations:
|
|
103
|
+
if module_type.__dict__.get(attribute, ()) is not declared:
|
|
104
|
+
raise BackgroundJobDefinitionError(
|
|
105
|
+
job=f"{module_type.__module__}.{module_type.__qualname__}",
|
|
106
|
+
reason=f"Module {attribute} metadata changed after build",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _build_background_job_catalog(registry: ModuleRegistry) -> BackgroundJobCatalog:
|
|
111
|
+
builder = _BackgroundJobCatalogBuilder()
|
|
112
|
+
for descriptor in registry.dependency_order:
|
|
113
|
+
builder.add_module(descriptor)
|
|
114
|
+
return builder.build()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class _BackgroundJobCatalogBuilder:
|
|
118
|
+
"""拥有 catalog build 期间的 mutable arbitration state,产物仍为 immutable。"""
|
|
119
|
+
|
|
120
|
+
__slots__ = (
|
|
121
|
+
"_declarations",
|
|
122
|
+
"_definitions",
|
|
123
|
+
"_name_owners",
|
|
124
|
+
"_owners",
|
|
125
|
+
"_raw_schedules",
|
|
126
|
+
"_schedule_owners",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def __init__(self) -> None:
|
|
130
|
+
self._definitions: list[BackgroundJobDefinition[BaseModel]] = []
|
|
131
|
+
self._raw_schedules: list[BackgroundJobSchedule] = []
|
|
132
|
+
self._owners: dict[tuple[str, int], ModuleKey] = {}
|
|
133
|
+
self._name_owners: dict[str, ModuleKey] = {}
|
|
134
|
+
self._schedule_owners: dict[str, ModuleKey] = {}
|
|
135
|
+
self._declarations: list[tuple[type[AppModule], str, tuple[object, ...]]] = []
|
|
136
|
+
|
|
137
|
+
def add_module(self, descriptor: ModuleDescriptor) -> None:
|
|
138
|
+
declared_definitions = descriptor.module_type.__dict__.get("background_job_definitions", ())
|
|
139
|
+
declared_schedules = descriptor.module_type.__dict__.get("background_job_schedules", ())
|
|
140
|
+
_validate_metadata_tuple(
|
|
141
|
+
declared_definitions,
|
|
142
|
+
owner=str(descriptor.key),
|
|
143
|
+
attribute="background_job_definitions",
|
|
144
|
+
)
|
|
145
|
+
_validate_metadata_tuple(
|
|
146
|
+
declared_schedules,
|
|
147
|
+
owner=str(descriptor.key),
|
|
148
|
+
attribute="background_job_schedules",
|
|
149
|
+
)
|
|
150
|
+
self._declarations.extend(
|
|
151
|
+
(
|
|
152
|
+
(descriptor.module_type, "background_job_definitions", declared_definitions),
|
|
153
|
+
(descriptor.module_type, "background_job_schedules", declared_schedules),
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
for definition in declared_definitions:
|
|
157
|
+
self._add_definition(definition, descriptor.key)
|
|
158
|
+
for schedule in declared_schedules:
|
|
159
|
+
self._add_schedule(schedule, descriptor.key)
|
|
160
|
+
|
|
161
|
+
def build(self) -> BackgroundJobCatalog:
|
|
162
|
+
by_name: dict[str, list[BackgroundJobDefinition[BaseModel]]] = {}
|
|
163
|
+
by_identity = {(item.name, item.version): item for item in self._definitions}
|
|
164
|
+
for definition in self._definitions:
|
|
165
|
+
by_name.setdefault(definition.name, []).append(definition)
|
|
166
|
+
for name, versions in by_name.items():
|
|
167
|
+
if sum(item.current for item in versions) != 1:
|
|
168
|
+
raise BackgroundJobDefinitionError(
|
|
169
|
+
job=name,
|
|
170
|
+
reason="job versions must declare exactly one current definition",
|
|
171
|
+
)
|
|
172
|
+
schedules = tuple(_canonicalize_schedule(item, by_identity) for item in self._raw_schedules)
|
|
173
|
+
return BackgroundJobCatalog(
|
|
174
|
+
tuple(self._definitions),
|
|
175
|
+
schedules,
|
|
176
|
+
declarations=tuple(self._declarations),
|
|
177
|
+
owners=self._owners,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def _add_definition(self, candidate: object, owner: ModuleKey) -> None:
|
|
181
|
+
if not isinstance(candidate, BackgroundJobDefinition):
|
|
182
|
+
raise BackgroundJobDefinitionError(
|
|
183
|
+
job=str(owner),
|
|
184
|
+
reason="Module background_job_definitions contains an invalid item",
|
|
185
|
+
)
|
|
186
|
+
definition = candidate
|
|
187
|
+
identity = (definition.name, definition.version)
|
|
188
|
+
if identity in self._owners:
|
|
189
|
+
raise BackgroundJobDefinitionError(
|
|
190
|
+
job=definition.name,
|
|
191
|
+
reason=f"version {definition.version} is declared more than once",
|
|
192
|
+
)
|
|
193
|
+
previous_owner = self._name_owners.get(definition.name)
|
|
194
|
+
if previous_owner is not None and previous_owner != owner:
|
|
195
|
+
raise BackgroundJobDefinitionError(
|
|
196
|
+
job=definition.name,
|
|
197
|
+
reason=f"all versions must be owned by {previous_owner}",
|
|
198
|
+
)
|
|
199
|
+
self._owners[identity] = owner
|
|
200
|
+
self._name_owners[definition.name] = owner
|
|
201
|
+
self._definitions.append(definition)
|
|
202
|
+
|
|
203
|
+
def _add_schedule(self, candidate: object, owner: ModuleKey) -> None:
|
|
204
|
+
if not isinstance(candidate, BackgroundJobSchedule):
|
|
205
|
+
raise BackgroundJobDefinitionError(
|
|
206
|
+
job=str(owner),
|
|
207
|
+
reason="Module background_job_schedules contains an invalid item",
|
|
208
|
+
)
|
|
209
|
+
schedule = candidate
|
|
210
|
+
if schedule.name in self._schedule_owners:
|
|
211
|
+
raise BackgroundJobDefinitionError(
|
|
212
|
+
job=schedule.name,
|
|
213
|
+
reason=f"schedule is already owned by {self._schedule_owners[schedule.name]}",
|
|
214
|
+
)
|
|
215
|
+
self._schedule_owners[schedule.name] = owner
|
|
216
|
+
self._raw_schedules.append(schedule)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _validate_metadata_tuple(value: object, *, owner: str, attribute: str) -> None:
|
|
220
|
+
if not isinstance(value, tuple):
|
|
221
|
+
raise BackgroundJobDefinitionError(
|
|
222
|
+
job=owner,
|
|
223
|
+
reason=f"Module {attribute} must be an immutable tuple",
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _canonicalize_schedule(
|
|
228
|
+
schedule: BackgroundJobSchedule,
|
|
229
|
+
definitions: Mapping[tuple[str, int], BackgroundJobDefinition[BaseModel]],
|
|
230
|
+
) -> BackgroundJobSchedule:
|
|
231
|
+
definition = definitions.get((schedule.job_name, schedule.job_version))
|
|
232
|
+
if definition is None:
|
|
233
|
+
raise BackgroundJobDefinitionError(
|
|
234
|
+
job=schedule.name,
|
|
235
|
+
reason="schedule target does not exist in the Application catalog",
|
|
236
|
+
)
|
|
237
|
+
cron = _normalize_cron(schedule.name, schedule.cron)
|
|
238
|
+
try:
|
|
239
|
+
payload = definition.payload_type.model_validate_json(schedule.payload)
|
|
240
|
+
except ValidationError:
|
|
241
|
+
raise BackgroundJobPayloadError(
|
|
242
|
+
job=definition.name,
|
|
243
|
+
version=definition.version,
|
|
244
|
+
reason="scheduled payload failed schema validation",
|
|
245
|
+
) from None
|
|
246
|
+
canonical = json.dumps(
|
|
247
|
+
payload.model_dump(mode="json"),
|
|
248
|
+
sort_keys=True,
|
|
249
|
+
separators=(",", ":"),
|
|
250
|
+
ensure_ascii=False,
|
|
251
|
+
).encode()
|
|
252
|
+
return replace(schedule, cron=cron, payload=canonical)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _normalize_cron(schedule_name: str, expression: str) -> str:
|
|
256
|
+
normalized = " ".join(expression.split())
|
|
257
|
+
fields = normalized.split(" ")
|
|
258
|
+
if len(fields) not in (5, 6) or any(
|
|
259
|
+
not field or any(character not in "0123456789*/,-" for character in field)
|
|
260
|
+
for field in fields
|
|
261
|
+
):
|
|
262
|
+
raise BackgroundJobDefinitionError(
|
|
263
|
+
job=schedule_name,
|
|
264
|
+
reason="cron must use numeric five-field or trailing-second six-field grammar",
|
|
265
|
+
)
|
|
266
|
+
try:
|
|
267
|
+
from croniter import croniter # type: ignore[import-untyped]
|
|
268
|
+
except ImportError:
|
|
269
|
+
raise BackgroundJobDefinitionError(
|
|
270
|
+
job=schedule_name,
|
|
271
|
+
reason="Cron schedules require the background-jobs extra",
|
|
272
|
+
) from None
|
|
273
|
+
try:
|
|
274
|
+
valid = croniter.is_valid(normalized, strict=True)
|
|
275
|
+
if valid:
|
|
276
|
+
croniter(
|
|
277
|
+
normalized,
|
|
278
|
+
start_time=datetime.now(UTC),
|
|
279
|
+
).get_next(datetime)
|
|
280
|
+
except (TypeError, ValueError):
|
|
281
|
+
valid = False
|
|
282
|
+
if not valid:
|
|
283
|
+
raise BackgroundJobDefinitionError(
|
|
284
|
+
job=schedule_name,
|
|
285
|
+
reason="cron expression is outside the supported UTC grammar",
|
|
286
|
+
)
|
|
287
|
+
return normalized
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Provider-neutral durable job identity、payload 与 enqueue contracts。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import inspect
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from collections.abc import Awaitable, Callable
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from datetime import datetime, timedelta
|
|
11
|
+
from typing import Generic, Protocol, TypeVar, cast
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
from ..invocation import BackgroundSystemCaller
|
|
16
|
+
from .errors import BackgroundJobDefinitionError
|
|
17
|
+
|
|
18
|
+
_TPayload = TypeVar("_TPayload", bound=BaseModel)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BackgroundJobHandler(ABC, Generic[_TPayload]):
|
|
22
|
+
"""一次 durable delivery 的业务边界;queue loop/retry 由 provider 独占。"""
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
async def execute(self, payload: _TPayload, context: BackgroundJobContext) -> None:
|
|
26
|
+
raise NotImplementedError
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class BackgroundJobContext:
|
|
31
|
+
job_id: str
|
|
32
|
+
name: str
|
|
33
|
+
version: int
|
|
34
|
+
attempt: int
|
|
35
|
+
scheduled_at: datetime
|
|
36
|
+
system_caller: BackgroundSystemCaller
|
|
37
|
+
_cancellation_event: asyncio.Event = field(repr=False, compare=False)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def cancellation_requested(self) -> bool:
|
|
41
|
+
return self._cancellation_event.is_set()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True, slots=True)
|
|
45
|
+
class BackgroundJobRetryPolicy:
|
|
46
|
+
max_attempts: int = 5
|
|
47
|
+
initial_delay: timedelta = timedelta(seconds=1)
|
|
48
|
+
max_delay: timedelta = timedelta(minutes=5)
|
|
49
|
+
backoff_multiplier: float = 2.0
|
|
50
|
+
|
|
51
|
+
def __post_init__(self) -> None:
|
|
52
|
+
if (
|
|
53
|
+
not isinstance(self.max_attempts, int)
|
|
54
|
+
or isinstance(self.max_attempts, bool)
|
|
55
|
+
or self.max_attempts < 1
|
|
56
|
+
):
|
|
57
|
+
raise BackgroundJobDefinitionError(
|
|
58
|
+
job="<retry-policy>",
|
|
59
|
+
reason="max_attempts must be a positive integer",
|
|
60
|
+
)
|
|
61
|
+
if not isinstance(self.initial_delay, timedelta) or self.initial_delay.total_seconds() <= 0:
|
|
62
|
+
raise BackgroundJobDefinitionError(
|
|
63
|
+
job="<retry-policy>",
|
|
64
|
+
reason="initial_delay must be a positive timedelta",
|
|
65
|
+
)
|
|
66
|
+
if not isinstance(self.max_delay, timedelta) or self.max_delay < self.initial_delay:
|
|
67
|
+
raise BackgroundJobDefinitionError(
|
|
68
|
+
job="<retry-policy>",
|
|
69
|
+
reason="max_delay must be a timedelta no smaller than initial_delay",
|
|
70
|
+
)
|
|
71
|
+
if (
|
|
72
|
+
not isinstance(self.backoff_multiplier, (int, float))
|
|
73
|
+
or isinstance(self.backoff_multiplier, bool)
|
|
74
|
+
or self.backoff_multiplier < 1
|
|
75
|
+
):
|
|
76
|
+
raise BackgroundJobDefinitionError(
|
|
77
|
+
job="<retry-policy>",
|
|
78
|
+
reason="backoff_multiplier must be at least 1",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def delay_for_attempt(self, attempt: int) -> timedelta:
|
|
82
|
+
if not isinstance(attempt, int) or isinstance(attempt, bool) or attempt < 0:
|
|
83
|
+
raise ValueError("attempt must be a non-negative integer")
|
|
84
|
+
seconds = self.initial_delay.total_seconds() * (self.backoff_multiplier**attempt)
|
|
85
|
+
return timedelta(seconds=min(seconds, self.max_delay.total_seconds()))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True, slots=True)
|
|
89
|
+
class BackgroundJobDefinition(Generic[_TPayload]):
|
|
90
|
+
name: str
|
|
91
|
+
version: int
|
|
92
|
+
payload_type: type[_TPayload]
|
|
93
|
+
handler_type: type[BackgroundJobHandler[_TPayload]] | Callable[..., Awaitable[None]]
|
|
94
|
+
timeout: timedelta
|
|
95
|
+
current: bool = False
|
|
96
|
+
retry_policy: BackgroundJobRetryPolicy = BackgroundJobRetryPolicy()
|
|
97
|
+
|
|
98
|
+
def __post_init__(self) -> None:
|
|
99
|
+
_validate_name(self.name)
|
|
100
|
+
if not isinstance(self.version, int) or isinstance(self.version, bool) or self.version < 1:
|
|
101
|
+
raise BackgroundJobDefinitionError(
|
|
102
|
+
job=self.name,
|
|
103
|
+
reason="version must be a positive integer",
|
|
104
|
+
)
|
|
105
|
+
if not isinstance(self.payload_type, type) or not issubclass(self.payload_type, BaseModel):
|
|
106
|
+
raise BackgroundJobDefinitionError(
|
|
107
|
+
job=self.name,
|
|
108
|
+
reason="payload_type must be a Pydantic BaseModel class",
|
|
109
|
+
)
|
|
110
|
+
is_class = isinstance(self.handler_type, type) and issubclass(
|
|
111
|
+
self.handler_type, BackgroundJobHandler
|
|
112
|
+
)
|
|
113
|
+
if not is_class and not inspect.iscoroutinefunction(self.handler_type):
|
|
114
|
+
raise BackgroundJobDefinitionError(
|
|
115
|
+
job=self.name,
|
|
116
|
+
reason="handler_type must be a BackgroundJobHandler class or async function",
|
|
117
|
+
)
|
|
118
|
+
if isinstance(self.handler_type, type) and not inspect.iscoroutinefunction(
|
|
119
|
+
cast(type[BackgroundJobHandler[_TPayload]], self.handler_type).execute
|
|
120
|
+
):
|
|
121
|
+
raise BackgroundJobDefinitionError(
|
|
122
|
+
job=self.name,
|
|
123
|
+
reason="execute must be async",
|
|
124
|
+
)
|
|
125
|
+
if not isinstance(self.timeout, timedelta) or self.timeout.total_seconds() <= 0:
|
|
126
|
+
raise BackgroundJobDefinitionError(
|
|
127
|
+
job=self.name,
|
|
128
|
+
reason="timeout must be a positive timedelta",
|
|
129
|
+
)
|
|
130
|
+
if not isinstance(self.current, bool):
|
|
131
|
+
raise BackgroundJobDefinitionError(job=self.name, reason="current must be bool")
|
|
132
|
+
if not isinstance(self.retry_policy, BackgroundJobRetryPolicy):
|
|
133
|
+
raise BackgroundJobDefinitionError(
|
|
134
|
+
job=self.name,
|
|
135
|
+
reason="retry_policy must be BackgroundJobRetryPolicy",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass(frozen=True, slots=True)
|
|
140
|
+
class BackgroundJobSchedule:
|
|
141
|
+
name: str
|
|
142
|
+
cron: str
|
|
143
|
+
job_name: str
|
|
144
|
+
job_version: int
|
|
145
|
+
payload: bytes
|
|
146
|
+
|
|
147
|
+
def __post_init__(self) -> None:
|
|
148
|
+
_validate_name(self.name)
|
|
149
|
+
_validate_name(self.job_name)
|
|
150
|
+
if (
|
|
151
|
+
not isinstance(self.job_version, int)
|
|
152
|
+
or isinstance(self.job_version, bool)
|
|
153
|
+
or self.job_version < 1
|
|
154
|
+
):
|
|
155
|
+
raise BackgroundJobDefinitionError(
|
|
156
|
+
job=self.name,
|
|
157
|
+
reason="job_version must be a positive integer",
|
|
158
|
+
)
|
|
159
|
+
if not isinstance(self.cron, str) or not self.cron.strip():
|
|
160
|
+
raise BackgroundJobDefinitionError(
|
|
161
|
+
job=self.name,
|
|
162
|
+
reason="cron must be a non-empty string",
|
|
163
|
+
)
|
|
164
|
+
if not isinstance(self.payload, bytes):
|
|
165
|
+
raise BackgroundJobDefinitionError(
|
|
166
|
+
job=self.name,
|
|
167
|
+
reason="payload must be immutable UTF-8 JSON bytes",
|
|
168
|
+
)
|
|
169
|
+
try:
|
|
170
|
+
self.payload.decode("utf-8")
|
|
171
|
+
except UnicodeDecodeError:
|
|
172
|
+
raise BackgroundJobDefinitionError(
|
|
173
|
+
job=self.name,
|
|
174
|
+
reason="payload must be UTF-8 JSON bytes",
|
|
175
|
+
) from None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class BackgroundJobEnqueuer(Protocol):
|
|
179
|
+
async def enqueue(
|
|
180
|
+
self,
|
|
181
|
+
definition: BackgroundJobDefinition[_TPayload],
|
|
182
|
+
payload: _TPayload,
|
|
183
|
+
*,
|
|
184
|
+
priority: int = 0,
|
|
185
|
+
delay: timedelta | None = None,
|
|
186
|
+
) -> str: ...
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _validate_name(name: str) -> None:
|
|
190
|
+
if (
|
|
191
|
+
not isinstance(name, str)
|
|
192
|
+
or not name
|
|
193
|
+
or name != name.strip()
|
|
194
|
+
or name.startswith(".")
|
|
195
|
+
or name.endswith(".")
|
|
196
|
+
or ".." in name
|
|
197
|
+
or any(not (character.isalnum() or character in "._-") for character in name)
|
|
198
|
+
):
|
|
199
|
+
raise BackgroundJobDefinitionError(
|
|
200
|
+
job=name if isinstance(name, str) else repr(name),
|
|
201
|
+
reason="name must be a trimmed stable identifier",
|
|
202
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Durable background job definition、payload 与 provider-neutral failures。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..errors.base import FrameworkError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BackgroundJobDefinitionError(FrameworkError):
|
|
9
|
+
def __init__(self, *, job: str, reason: str) -> None:
|
|
10
|
+
self.job = job
|
|
11
|
+
self.reason = reason
|
|
12
|
+
super().__init__(f"Invalid background job {job!r}: {reason}")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BackgroundJobPayloadError(FrameworkError):
|
|
16
|
+
def __init__(self, *, job: str, version: int, reason: str) -> None:
|
|
17
|
+
self.job = job
|
|
18
|
+
self.version = version
|
|
19
|
+
self.reason = reason
|
|
20
|
+
super().__init__(f"Invalid payload for background job {job!r} v{version}: {reason}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BackgroundJobExecutionError(FrameworkError):
|
|
24
|
+
def __init__(self, *, job: str, version: int, error_type: str) -> None:
|
|
25
|
+
self.job = job
|
|
26
|
+
self.version = version
|
|
27
|
+
self.error_type = error_type
|
|
28
|
+
super().__init__(f"Background job {job!r} v{version} failed with {error_type}")
|