feat: add Campaign work orchestration contract
Module Package Release / publish-packages (push) Failing after 6s

This commit is contained in:
2026-08-22 02:15:32 +02:00
parent 137c7c005f
commit a9035c4c3b
5 changed files with 221 additions and 5 deletions
+139 -1
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
from collections.abc import Callable, Iterable, Mapping
from dataclasses import dataclass, field
from datetime import datetime
from typing import Protocol, runtime_checkable
from typing import Literal, Protocol, runtime_checkable
CAPABILITY_CAMPAIGNS_MAIL_POLICY_CONTEXT = "campaigns.mailPolicyContext"
@@ -12,6 +12,20 @@ CAPABILITY_CAMPAIGNS_POLICY_CONTEXT = "campaigns.policyContext"
CAPABILITY_CAMPAIGNS_DELIVERY_TASKS = "campaigns.deliveryTasks"
CAPABILITY_CAMPAIGNS_SCHEDULES = "campaigns.schedules"
CAPABILITY_CAMPAIGNS_RETENTION = "campaigns.retention"
CAPABILITY_CAMPAIGNS_WORK_ORCHESTRATION = "campaigns.workOrchestration"
CampaignWorkAssigneeKind = Literal[
"account",
"group",
"organization_function",
]
CampaignWorkHandoffStatus = Literal[
"open",
"in_progress",
"completed",
"rejected",
"cancelled",
]
@dataclass(frozen=True, slots=True)
@@ -32,6 +46,88 @@ class CampaignPolicyContext:
settings: Mapping[str, object] = field(default_factory=dict)
@dataclass(frozen=True, slots=True)
class CampaignWorkHandoffRequest:
"""Typed request used by Workflow to open accountable Campaign work."""
tenant_id: str
idempotency_key: str
purpose: str
assignee_kind: CampaignWorkAssigneeKind
assignee_id: str
campaign_id: str | None = None
create_external_id: str | None = None
create_name: str | None = None
create_description: str | None = None
expected_campaign_revision: int | None = None
due_at: datetime | None = None
mirror_to_tasks: bool = True
correlation_id: str | None = None
workflow_instance_id: str | None = None
workflow_step_id: str | None = None
def __post_init__(self) -> None:
for value, label in (
(self.tenant_id, "Campaign hand-off tenant"),
(self.idempotency_key, "Campaign hand-off idempotency key"),
(self.purpose, "Campaign hand-off purpose"),
(self.assignee_id, "Campaign hand-off assignee"),
):
if not value.strip():
raise ValueError(f"{label} is required")
references_existing = bool(self.campaign_id and self.campaign_id.strip())
creates_new = bool(
self.create_external_id
and self.create_external_id.strip()
and self.create_name
and self.create_name.strip()
)
if references_existing == creates_new:
raise ValueError(
"Campaign hand-offs must either reference one campaign or "
"declare one new campaign."
)
if self.expected_campaign_revision is not None and (
self.expected_campaign_revision < 1
):
raise ValueError("Expected Campaign revisions start at one")
if self.due_at is not None and self.due_at.tzinfo is None:
raise ValueError("Campaign hand-off due dates require a timezone")
@dataclass(frozen=True, slots=True)
class CampaignWorkHandoffRef:
"""Stable, revision-bearing reference returned to the Workflow instance."""
tenant_id: str
campaign_id: str
campaign_version_id: str
campaign_revision: int
assignment_id: str
assignment_revision: int
status: CampaignWorkHandoffStatus
action_url: str
campaign_ref: str
assignment_ref: str
event_type: str = "campaign.work.changed"
replayed: bool = False
optional_capabilities: Mapping[str, bool] = field(default_factory=dict)
provenance: Mapping[str, object] = field(default_factory=dict)
@dataclass(frozen=True, slots=True)
class CampaignWorkHandoffInspection:
"""Current authorization and revision check before Workflow continuation."""
allowed: bool
status: CampaignWorkHandoffStatus | None = None
assignment_revision: int | None = None
action_url: str | None = None
assignment_ref: str | None = None
reason: str | None = None
provenance: Mapping[str, object] = field(default_factory=dict)
@runtime_checkable
class CampaignMailPolicyContextProvider(Protocol):
def get_campaign_mail_policy_context(
@@ -132,3 +228,45 @@ class CampaignRetentionProvider(Protocol):
policy_for_campaign_id: Callable[[str | None], object],
) -> Mapping[str, Mapping[str, int]]:
...
@runtime_checkable
class CampaignWorkOrchestrationProvider(Protocol):
"""Optional Campaign boundary for durable Workflow-owned hand-offs."""
def prepare_handoff(
self,
session: object,
principal: object,
*,
request: CampaignWorkHandoffRequest,
) -> CampaignWorkHandoffRef:
...
def inspect_handoff(
self,
session: object,
principal: object,
*,
tenant_id: str,
assignment_id: str,
expected_revision: int | None = None,
) -> CampaignWorkHandoffInspection:
...
def campaign_work_orchestration_provider(
registry: object | None,
) -> CampaignWorkOrchestrationProvider | None:
if (
registry is None
or not hasattr(registry, "has_capability")
or not registry.has_capability(CAPABILITY_CAMPAIGNS_WORK_ORCHESTRATION)
):
return None
capability = registry.capability(CAPABILITY_CAMPAIGNS_WORK_ORCHESTRATION)
return (
capability
if isinstance(capability, CampaignWorkOrchestrationProvider)
else None
)