900 lines
29 KiB
Python
900 lines
29 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import shutil
|
|
import tempfile
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
from typing import Any, Iterator
|
|
|
|
from govoplan_core.core.approvals import (
|
|
ApprovalCheck,
|
|
ApprovalRequestCreateCommand,
|
|
ApprovalRequestProvider,
|
|
ApprovalRequestRef,
|
|
CAPABILITY_APPROVAL_REQUESTS,
|
|
)
|
|
from govoplan_core.core.calendar import (
|
|
CAPABILITY_CALENDAR_INVITATIONS,
|
|
CalendarInvitationAttendeeRequest,
|
|
CalendarInvitationCalendarRef,
|
|
CalendarInvitationProvider,
|
|
CalendarInvitationRef,
|
|
CalendarInvitationRequest,
|
|
)
|
|
from govoplan_core.core.postbox import (
|
|
CAPABILITY_POSTBOX_DIRECTORY,
|
|
CAPABILITY_POSTBOX_DELIVERY,
|
|
CAPABILITY_POSTBOX_EVIDENCE,
|
|
PostboxDeliveryCatalogRef,
|
|
PostboxDeliveryProvider,
|
|
PostboxDeliveryReceiptSummaryRef,
|
|
PostboxDeliveryRequest,
|
|
PostboxDeliveryResult,
|
|
PostboxDirectoryEntryRef,
|
|
PostboxDirectoryProvider,
|
|
PostboxEvidenceProvider,
|
|
PostboxTargetRef,
|
|
)
|
|
from govoplan_core.core.templates import (
|
|
CAPABILITY_TEMPLATE_CATALOG,
|
|
CAPABILITY_TEMPLATE_CONTENT_LIBRARY,
|
|
CAPABILITY_TEMPLATE_RENDERER,
|
|
TemplateCatalogProvider,
|
|
TemplateCompatibility,
|
|
TemplateContentDraftRequest,
|
|
TemplateContentLibraryProvider,
|
|
TemplateRef,
|
|
TemplateRenderRequest,
|
|
TemplateRenderResult,
|
|
TemplateRendererProvider,
|
|
)
|
|
from govoplan_campaign.backend.runtime import capability
|
|
|
|
|
|
FILES_CAPABILITY = "files.campaign_attachments"
|
|
MAIL_CAPABILITY = "mail.campaign_delivery"
|
|
POSTBOX_CAPABILITY = CAPABILITY_POSTBOX_DELIVERY
|
|
POSTBOX_DIRECTORY_CAPABILITY = CAPABILITY_POSTBOX_DIRECTORY
|
|
POSTBOX_EVIDENCE_CAPABILITY = CAPABILITY_POSTBOX_EVIDENCE
|
|
APPROVALS_CAPABILITY = CAPABILITY_APPROVAL_REQUESTS
|
|
TEMPLATE_CATALOG_CAPABILITY = CAPABILITY_TEMPLATE_CATALOG
|
|
TEMPLATE_CONTENT_LIBRARY_CAPABILITY = CAPABILITY_TEMPLATE_CONTENT_LIBRARY
|
|
TEMPLATE_RENDERER_CAPABILITY = CAPABILITY_TEMPLATE_RENDERER
|
|
CALENDAR_INVITATIONS_CAPABILITY = CAPABILITY_CALENDAR_INVITATIONS
|
|
|
|
|
|
class OptionalModuleUnavailable(RuntimeError):
|
|
pass
|
|
|
|
|
|
class SmtpConfigurationError(RuntimeError):
|
|
pass
|
|
|
|
|
|
class SmtpSendError(RuntimeError):
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
*,
|
|
temporary: bool = False,
|
|
outcome_unknown: bool = False,
|
|
systemic: bool = False,
|
|
reason_code: str | None = None,
|
|
phase: str = "send",
|
|
) -> None:
|
|
super().__init__(message)
|
|
self.temporary = temporary
|
|
self.outcome_unknown = outcome_unknown
|
|
self.systemic = systemic
|
|
self.reason_code = reason_code
|
|
self.phase = phase
|
|
|
|
|
|
class ImapConfigurationError(RuntimeError):
|
|
pass
|
|
|
|
|
|
class ImapAppendError(RuntimeError):
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
*,
|
|
temporary: bool | None = None,
|
|
outcome_unknown: bool = False,
|
|
) -> None:
|
|
super().__init__(message)
|
|
self.temporary = temporary
|
|
self.outcome_unknown = outcome_unknown
|
|
|
|
|
|
class MailProfileError(OptionalModuleUnavailable):
|
|
pass
|
|
|
|
|
|
class MailDeliveryCommandError(RuntimeError):
|
|
pass
|
|
|
|
|
|
class PostboxDeliveryUnavailable(OptionalModuleUnavailable):
|
|
pass
|
|
|
|
|
|
class ApprovalGateUnavailable(OptionalModuleUnavailable):
|
|
pass
|
|
|
|
|
|
class TemplateOutputUnavailable(OptionalModuleUnavailable):
|
|
pass
|
|
|
|
|
|
class CalendarInvitationUnavailable(OptionalModuleUnavailable):
|
|
pass
|
|
|
|
|
|
class _PreparedCampaignSnapshot:
|
|
def __init__(self, directory: Path, path: Path, raw_json: dict[str, Any]) -> None:
|
|
self._directory = directory
|
|
self.path = path
|
|
self.raw_json = raw_json
|
|
self.managed_files_by_local_path: dict[str, Any] = {}
|
|
self.shared_assets: list[Any] = []
|
|
self.candidate_assets: list[Any] = []
|
|
|
|
def cleanup(self) -> None:
|
|
shutil.rmtree(self._directory, ignore_errors=True)
|
|
|
|
|
|
class FilesCampaignIntegration:
|
|
def __init__(self, delegate: Any | None = None) -> None:
|
|
self._delegate = delegate
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
return self._delegate is not None
|
|
|
|
@contextmanager
|
|
def prepared_campaign_snapshot(self, *args: Any, **kwargs: Any) -> Iterator[Any]:
|
|
if self._delegate is not None:
|
|
with self._delegate.prepared_campaign_snapshot(*args, **kwargs) as prepared:
|
|
yield prepared
|
|
return
|
|
|
|
raw_json = (
|
|
kwargs.get("raw_json") if isinstance(kwargs.get("raw_json"), dict) else {}
|
|
)
|
|
prefix = str(kwargs.get("prefix") or "govoplan-campaign-")
|
|
directory = Path(tempfile.mkdtemp(prefix=prefix))
|
|
snapshot = _PreparedCampaignSnapshot(
|
|
directory, directory / "campaign.json", raw_json
|
|
)
|
|
snapshot.path.write_text(
|
|
json.dumps(raw_json, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
)
|
|
try:
|
|
yield snapshot
|
|
finally:
|
|
snapshot.cleanup()
|
|
|
|
def managed_match_payloads(
|
|
self, matches: Any, managed_files_by_local_path: dict[str, Any]
|
|
) -> list[dict[str, Any]]:
|
|
if self._delegate is None:
|
|
return []
|
|
return self._delegate.managed_match_payloads(
|
|
matches, managed_files_by_local_path
|
|
)
|
|
|
|
def public_attachment_summary_payload(self, attachment: Any) -> dict[str, Any]:
|
|
if self._delegate is not None:
|
|
return self._delegate.public_attachment_summary_payload(attachment)
|
|
if hasattr(attachment, "model_dump"):
|
|
return attachment.model_dump(mode="json")
|
|
if isinstance(attachment, dict):
|
|
return dict(attachment)
|
|
return {"path": str(attachment)}
|
|
|
|
def annotate_built_messages_with_managed_files(
|
|
self, built_messages: Any, managed_files_by_local_path: dict[str, Any]
|
|
) -> None:
|
|
if self._delegate is not None:
|
|
self._delegate.annotate_built_messages_with_managed_files(
|
|
built_messages, managed_files_by_local_path
|
|
)
|
|
|
|
def record_campaign_attachment_uses_for_jobs(
|
|
self, session: Any, jobs: Any, *, stage: str
|
|
) -> None:
|
|
if self._delegate is not None:
|
|
self._delegate.record_campaign_attachment_uses_for_jobs(
|
|
session, jobs, stage=stage
|
|
)
|
|
|
|
def current_version_and_blob(self, session: Any, asset: Any) -> tuple[Any, Any]:
|
|
if self._delegate is None:
|
|
raise OptionalModuleUnavailable("Files module is not available")
|
|
return self._delegate.current_version_and_blob(session, asset)
|
|
|
|
def share_assets_with_campaign(
|
|
self, session: Any, **kwargs: Any
|
|
) -> list[dict[str, Any]]:
|
|
if self._delegate is None:
|
|
raise OptionalModuleUnavailable("Files module is not available")
|
|
return self._delegate.share_assets_with_campaign(session, **kwargs)
|
|
|
|
def mark_job_attachment_uses_sent(self, session: Any, job: Any) -> None:
|
|
if self._delegate is not None:
|
|
self._delegate.mark_job_attachment_uses_sent(session, job)
|
|
|
|
|
|
class MailCampaignIntegration:
|
|
def __init__(self, delegate: Any | None = None) -> None:
|
|
self._delegate = delegate
|
|
if delegate is not None:
|
|
self.MailProfileError = getattr(
|
|
delegate, "MailProfileError", MailProfileError
|
|
)
|
|
|
|
MailProfileError = MailProfileError
|
|
SmtpConfigurationError = SmtpConfigurationError
|
|
SmtpSendError = SmtpSendError
|
|
ImapConfigurationError = ImapConfigurationError
|
|
ImapAppendError = ImapAppendError
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
return self._delegate is not None
|
|
|
|
@property
|
|
def durable_delivery_available(self) -> bool:
|
|
return self._delegate is not None and callable(
|
|
getattr(self._delegate, "submit_delivery_command", None)
|
|
)
|
|
|
|
def _require(self) -> Any:
|
|
if self._delegate is None:
|
|
raise MailProfileError("Mail module is not available")
|
|
return self._delegate
|
|
|
|
def assert_campaign_mail_policy_allows_json(
|
|
self, session: Any, **kwargs: Any
|
|
) -> None:
|
|
if self._delegate is None:
|
|
raw_json = kwargs.get("raw_json")
|
|
profile_id = self.mail_profile_id_from_campaign_json(
|
|
raw_json if isinstance(raw_json, dict) else {}
|
|
)
|
|
if profile_id:
|
|
raise MailProfileError(
|
|
"Campaign mail-server profiles require the mail module"
|
|
)
|
|
return None
|
|
try:
|
|
return self._delegate.assert_campaign_mail_policy_allows_json(
|
|
session, **kwargs
|
|
)
|
|
except getattr(self._delegate, "MailProfileError", MailProfileError) as exc:
|
|
raise MailProfileError(str(exc)) from exc
|
|
|
|
def mail_profile_id_from_campaign_json(
|
|
self, raw_json: dict[str, Any]
|
|
) -> str | None:
|
|
if self._delegate is not None:
|
|
return self._delegate.mail_profile_id_from_campaign_json(raw_json)
|
|
server = raw_json.get("server") if isinstance(raw_json, dict) else None
|
|
profile_id = server.get("mail_profile_id") if isinstance(server, dict) else None
|
|
return str(profile_id).strip() if profile_id else None
|
|
|
|
def campaign_profile_delivery_summary(
|
|
self, session: Any, **kwargs: Any
|
|
) -> dict[str, Any]:
|
|
delegate = self._require()
|
|
try:
|
|
return delegate.campaign_profile_delivery_summary(session, **kwargs)
|
|
except getattr(delegate, "MailProfileError", MailProfileError) as exc:
|
|
raise MailProfileError(str(exc)) from exc
|
|
|
|
def wait_for_rate_limit(self, **kwargs: Any) -> None:
|
|
if self._delegate is None:
|
|
return None
|
|
return self._delegate.wait_for_rate_limit(**kwargs)
|
|
|
|
def send_campaign_email_bytes(self, *args: Any, **kwargs: Any) -> Any:
|
|
delegate = self._require()
|
|
try:
|
|
return delegate.send_campaign_email_bytes(*args, **kwargs)
|
|
except getattr(delegate, "SmtpSendError", SmtpSendError) as exc:
|
|
raise SmtpSendError(
|
|
str(exc),
|
|
temporary=bool(getattr(exc, "temporary", False)),
|
|
outcome_unknown=bool(getattr(exc, "outcome_unknown", False)),
|
|
systemic=bool(getattr(exc, "systemic", False)),
|
|
reason_code=str(getattr(exc, "reason_code", "") or "") or None,
|
|
phase=str(getattr(exc, "phase", "send") or "send"),
|
|
) from exc
|
|
except getattr(
|
|
delegate, "SmtpConfigurationError", SmtpConfigurationError
|
|
) as exc:
|
|
raise SmtpConfigurationError(str(exc)) from exc
|
|
except getattr(delegate, "MailProfileError", MailProfileError) as exc:
|
|
raise MailProfileError(str(exc)) from exc
|
|
|
|
@contextmanager
|
|
def campaign_smtp_batch(self, *args: Any, **kwargs: Any) -> Iterator[Any]:
|
|
delegate = self._require()
|
|
method = getattr(delegate, "campaign_smtp_batch", None)
|
|
if not callable(method):
|
|
yield None
|
|
return
|
|
try:
|
|
with method(*args, **kwargs) as state:
|
|
yield state
|
|
except getattr(delegate, "SmtpSendError", SmtpSendError) as exc:
|
|
raise SmtpSendError(
|
|
str(exc),
|
|
temporary=bool(getattr(exc, "temporary", False)),
|
|
outcome_unknown=bool(getattr(exc, "outcome_unknown", False)),
|
|
systemic=bool(getattr(exc, "systemic", False)),
|
|
reason_code=str(getattr(exc, "reason_code", "") or "") or None,
|
|
phase=str(getattr(exc, "phase", "preflight") or "preflight"),
|
|
) from exc
|
|
except getattr(delegate, "SmtpConfigurationError", SmtpConfigurationError) as exc:
|
|
raise SmtpConfigurationError(str(exc)) from exc
|
|
except getattr(delegate, "MailProfileError", MailProfileError) as exc:
|
|
raise MailProfileError(str(exc)) from exc
|
|
|
|
def append_campaign_message_to_sent(self, *args: Any, **kwargs: Any) -> Any:
|
|
delegate = self._require()
|
|
try:
|
|
return delegate.append_campaign_message_to_sent(*args, **kwargs)
|
|
except getattr(delegate, "ImapAppendError", ImapAppendError) as exc:
|
|
raise ImapAppendError(
|
|
str(exc),
|
|
temporary=getattr(exc, "temporary", None),
|
|
outcome_unknown=bool(getattr(exc, "outcome_unknown", False)),
|
|
) from exc
|
|
except getattr(
|
|
delegate, "ImapConfigurationError", ImapConfigurationError
|
|
) as exc:
|
|
raise ImapConfigurationError(str(exc)) from exc
|
|
except getattr(delegate, "MailProfileError", MailProfileError) as exc:
|
|
raise MailProfileError(str(exc)) from exc
|
|
|
|
def submit_delivery_command(self, session: Any, **kwargs: Any) -> dict[str, Any]:
|
|
delegate = self._require()
|
|
method = getattr(delegate, "submit_delivery_command", None)
|
|
if not callable(method):
|
|
raise MailDeliveryCommandError(
|
|
"The installed Mail module does not provide durable delivery commands"
|
|
)
|
|
try:
|
|
return dict(method(session, **kwargs))
|
|
except Exception as exc:
|
|
raise MailDeliveryCommandError(str(exc)) from exc
|
|
|
|
def delivery_command_summary(
|
|
self,
|
|
session: Any,
|
|
*,
|
|
tenant_id: str,
|
|
command_id: str,
|
|
) -> dict[str, Any]:
|
|
delegate = self._require()
|
|
method = getattr(delegate, "delivery_command_summary", None)
|
|
if not callable(method):
|
|
raise MailDeliveryCommandError(
|
|
"The installed Mail module does not provide durable delivery status"
|
|
)
|
|
try:
|
|
return dict(
|
|
method(
|
|
session,
|
|
tenant_id=tenant_id,
|
|
command_id=command_id,
|
|
)
|
|
)
|
|
except Exception as exc:
|
|
raise MailDeliveryCommandError(str(exc)) from exc
|
|
|
|
def mock_mailbox(self) -> Any | None:
|
|
if self._delegate is None or not hasattr(self._delegate, "mock_mailbox"):
|
|
return None
|
|
return self._delegate.mock_mailbox()
|
|
|
|
|
|
class PostboxCampaignIntegration:
|
|
def __init__(
|
|
self,
|
|
delivery_delegate: object | None = None,
|
|
directory_delegate: object | None = None,
|
|
evidence_delegate: object | None = None,
|
|
) -> None:
|
|
self._delivery_delegate = (
|
|
delivery_delegate
|
|
if isinstance(delivery_delegate, PostboxDeliveryProvider)
|
|
else None
|
|
)
|
|
self._directory_delegate = (
|
|
directory_delegate
|
|
if isinstance(directory_delegate, PostboxDirectoryProvider)
|
|
else None
|
|
)
|
|
self._evidence_delegate = (
|
|
evidence_delegate
|
|
if isinstance(evidence_delegate, PostboxEvidenceProvider)
|
|
else None
|
|
)
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
return (
|
|
self._delivery_delegate is not None and self._directory_delegate is not None
|
|
)
|
|
|
|
@property
|
|
def receipt_evidence_available(self) -> bool:
|
|
return self._evidence_delegate is not None
|
|
|
|
def delivery_catalog(
|
|
self,
|
|
session: object,
|
|
*,
|
|
tenant_id: str,
|
|
) -> PostboxDeliveryCatalogRef:
|
|
if self._directory_delegate is None:
|
|
raise PostboxDeliveryUnavailable(
|
|
"Postbox targets are unavailable because the Postbox module "
|
|
"is not active."
|
|
)
|
|
return self._directory_delegate.delivery_catalog(
|
|
session,
|
|
tenant_id=tenant_id,
|
|
)
|
|
|
|
def resolve_postbox(
|
|
self,
|
|
session: object,
|
|
*,
|
|
tenant_id: str,
|
|
target: PostboxTargetRef,
|
|
materialize: bool = False,
|
|
) -> PostboxDirectoryEntryRef | None:
|
|
if self._directory_delegate is None:
|
|
raise PostboxDeliveryUnavailable(
|
|
"Postbox targets are unavailable because the Postbox module "
|
|
"is not active."
|
|
)
|
|
return self._directory_delegate.resolve_postbox(
|
|
session,
|
|
tenant_id=tenant_id,
|
|
target=target,
|
|
materialize=materialize,
|
|
)
|
|
|
|
def deliver(
|
|
self,
|
|
session: object,
|
|
request: PostboxDeliveryRequest,
|
|
) -> PostboxDeliveryResult:
|
|
if self._delivery_delegate is None:
|
|
raise PostboxDeliveryUnavailable(
|
|
"Postbox delivery is unavailable because the Postbox module "
|
|
"is not active."
|
|
)
|
|
return self._delivery_delegate.deliver(session, request)
|
|
|
|
def delivery_receipt_summaries(
|
|
self,
|
|
session: object,
|
|
*,
|
|
tenant_id: str,
|
|
delivery_ids: list[str] | tuple[str, ...],
|
|
) -> dict[str, PostboxDeliveryReceiptSummaryRef]:
|
|
if self._evidence_delegate is None:
|
|
return {}
|
|
unique_ids = tuple(dict.fromkeys(delivery_ids))
|
|
summaries: dict[str, PostboxDeliveryReceiptSummaryRef] = {}
|
|
for offset in range(0, len(unique_ids), 500):
|
|
summaries.update(
|
|
self._evidence_delegate.delivery_receipt_summaries(
|
|
session,
|
|
tenant_id=tenant_id,
|
|
producer_module="campaigns",
|
|
delivery_ids=unique_ids[offset : offset + 500],
|
|
)
|
|
)
|
|
return summaries
|
|
|
|
|
|
class ApprovalCampaignIntegration:
|
|
def __init__(self, delegate: object | None = None) -> None:
|
|
self._delegate = (
|
|
delegate if isinstance(delegate, ApprovalRequestProvider) else None
|
|
)
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
return self._delegate is not None
|
|
|
|
def create_request(
|
|
self,
|
|
session: object,
|
|
principal: object,
|
|
*,
|
|
command: ApprovalRequestCreateCommand,
|
|
idempotency_key: str,
|
|
) -> ApprovalRequestRef:
|
|
if self._delegate is None:
|
|
raise ApprovalGateUnavailable(
|
|
"Campaign approval gates require the Approvals module."
|
|
)
|
|
return self._delegate.create_request(
|
|
session,
|
|
principal,
|
|
command=command,
|
|
idempotency_key=idempotency_key,
|
|
)
|
|
|
|
def check_approved(
|
|
self,
|
|
session: object,
|
|
principal: object,
|
|
*,
|
|
request_id: str,
|
|
subject_module: str,
|
|
subject_type: str,
|
|
subject_id: str,
|
|
subject_version: str | None,
|
|
subject_digest: str,
|
|
) -> ApprovalCheck:
|
|
if self._delegate is None:
|
|
raise ApprovalGateUnavailable(
|
|
"Campaign delivery is approval-gated, but the Approvals module is unavailable."
|
|
)
|
|
return self._delegate.check_approved(
|
|
session,
|
|
principal,
|
|
request_id=request_id,
|
|
subject_module=subject_module,
|
|
subject_type=subject_type,
|
|
subject_id=subject_id,
|
|
subject_version=subject_version,
|
|
subject_digest=subject_digest,
|
|
)
|
|
|
|
|
|
class TemplatesCampaignIntegration:
|
|
def __init__(
|
|
self,
|
|
catalog_delegate: object | None = None,
|
|
renderer_delegate: object | None = None,
|
|
content_library_delegate: object | None = None,
|
|
) -> None:
|
|
self._catalog = (
|
|
catalog_delegate
|
|
if isinstance(catalog_delegate, TemplateCatalogProvider)
|
|
else None
|
|
)
|
|
self._renderer = (
|
|
renderer_delegate
|
|
if isinstance(renderer_delegate, TemplateRendererProvider)
|
|
else None
|
|
)
|
|
self._content_library = (
|
|
content_library_delegate
|
|
if isinstance(content_library_delegate, TemplateContentLibraryProvider)
|
|
else None
|
|
)
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
return self._catalog is not None and self._renderer is not None
|
|
|
|
@property
|
|
def content_available(self) -> bool:
|
|
return self._catalog is not None
|
|
|
|
@property
|
|
def content_writable(self) -> bool:
|
|
return self._content_library is not None
|
|
|
|
def list_templates(
|
|
self,
|
|
session: object,
|
|
principal: object,
|
|
*,
|
|
query: str = "",
|
|
limit: int = 100,
|
|
) -> tuple[TemplateRef, ...]:
|
|
if self._catalog is None:
|
|
return ()
|
|
return tuple(
|
|
self._catalog.list_templates(
|
|
session,
|
|
principal,
|
|
query=query,
|
|
usage="campaign_print",
|
|
limit=limit,
|
|
)
|
|
)
|
|
|
|
def list_content_templates(
|
|
self,
|
|
session: object,
|
|
principal: object,
|
|
*,
|
|
query: str = "",
|
|
limit: int = 100,
|
|
) -> tuple[TemplateRef, ...]:
|
|
if self._catalog is None:
|
|
return ()
|
|
return tuple(
|
|
self._catalog.list_templates(
|
|
session,
|
|
principal,
|
|
query=query,
|
|
usage="campaign.content",
|
|
limit=limit,
|
|
)
|
|
)
|
|
|
|
def create_content_draft(
|
|
self,
|
|
session: object,
|
|
principal: object,
|
|
*,
|
|
request: TemplateContentDraftRequest,
|
|
) -> TemplateRef:
|
|
if self._content_library is None:
|
|
raise TemplateOutputUnavailable(
|
|
"Saving reusable Campaign content requires the Templates content-library capability."
|
|
)
|
|
return self._content_library.create_content_draft(
|
|
session,
|
|
principal,
|
|
request=request,
|
|
)
|
|
|
|
def check_compatibility(
|
|
self,
|
|
session: object,
|
|
principal: object,
|
|
*,
|
|
template_id: str,
|
|
revision: int | None,
|
|
output_format: str,
|
|
available_fields: dict[str, str] | tuple[str, ...],
|
|
) -> TemplateCompatibility:
|
|
if self._catalog is None:
|
|
raise TemplateOutputUnavailable(
|
|
"Printable output is unavailable because Templates is not active."
|
|
)
|
|
return self._catalog.check_compatibility(
|
|
session,
|
|
principal,
|
|
template_id=template_id,
|
|
revision=revision,
|
|
usage="campaign_print",
|
|
output_format=output_format,
|
|
available_fields=available_fields,
|
|
)
|
|
|
|
def get_template(
|
|
self,
|
|
session: object,
|
|
principal: object,
|
|
*,
|
|
template_id: str,
|
|
revision: int,
|
|
) -> TemplateRef | None:
|
|
if self._catalog is None:
|
|
return None
|
|
return self._catalog.get_template(
|
|
session,
|
|
principal,
|
|
template_id=template_id,
|
|
revision=revision,
|
|
)
|
|
|
|
def render(
|
|
self,
|
|
session: object,
|
|
principal: object,
|
|
*,
|
|
request: TemplateRenderRequest,
|
|
) -> TemplateRenderResult:
|
|
if self._renderer is None:
|
|
raise TemplateOutputUnavailable(
|
|
"Printable output is unavailable because Templates is not active."
|
|
)
|
|
return self._renderer.render(session, principal, request=request)
|
|
|
|
|
|
class CalendarCampaignIntegration:
|
|
def __init__(self, delegate: object | None = None) -> None:
|
|
self._delegate = (
|
|
delegate if isinstance(delegate, CalendarInvitationProvider) else None
|
|
)
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
return self._delegate is not None
|
|
|
|
def list_calendars(
|
|
self,
|
|
session: object,
|
|
*,
|
|
tenant_id: str,
|
|
user_id: str | None,
|
|
group_ids: tuple[str, ...] = (),
|
|
can_admin: bool = False,
|
|
) -> tuple[CalendarInvitationCalendarRef, ...]:
|
|
if self._delegate is None:
|
|
return ()
|
|
return tuple(
|
|
self._delegate.list_calendars(
|
|
session,
|
|
tenant_id=tenant_id,
|
|
user_id=user_id,
|
|
group_ids=group_ids,
|
|
can_admin=can_admin,
|
|
)
|
|
)
|
|
|
|
def render_invitation(self, request: CalendarInvitationRequest) -> str:
|
|
if self._delegate is None:
|
|
raise CalendarInvitationUnavailable(
|
|
"Calendar invitations require the optional Calendar module."
|
|
)
|
|
return self._delegate.render_invitation(request)
|
|
|
|
def upsert_invitation(
|
|
self,
|
|
session: object,
|
|
*,
|
|
tenant_id: str,
|
|
user_id: str | None,
|
|
request: CalendarInvitationRequest,
|
|
) -> CalendarInvitationRef:
|
|
if self._delegate is None:
|
|
raise CalendarInvitationUnavailable(
|
|
"Calendar invitations require the optional Calendar module."
|
|
)
|
|
return self._delegate.upsert_invitation(
|
|
session,
|
|
tenant_id=tenant_id,
|
|
user_id=user_id,
|
|
request=request,
|
|
)
|
|
|
|
def get_invitations(
|
|
self,
|
|
session: object,
|
|
*,
|
|
tenant_id: str,
|
|
correlation_ids: tuple[str, ...],
|
|
) -> dict[str, CalendarInvitationRef]:
|
|
if self._delegate is None or not correlation_ids:
|
|
return {}
|
|
result: dict[str, CalendarInvitationRef] = {}
|
|
unique_ids = tuple(dict.fromkeys(correlation_ids))
|
|
for offset in range(0, len(unique_ids), 500):
|
|
result.update(
|
|
self._delegate.get_invitations(
|
|
session,
|
|
tenant_id=tenant_id,
|
|
correlation_ids=unique_ids[offset : offset + 500],
|
|
)
|
|
)
|
|
return result
|
|
|
|
def summarize_invitations(
|
|
self,
|
|
session: object,
|
|
*,
|
|
tenant_id: str,
|
|
source_resource_id: str | None,
|
|
) -> dict[str, object]:
|
|
if self._delegate is None:
|
|
return {
|
|
"available": False,
|
|
"reason": "The Calendar invitation capability is not active.",
|
|
}
|
|
return dict(
|
|
self._delegate.summarize_invitations(
|
|
session,
|
|
tenant_id=tenant_id,
|
|
source_module="campaigns",
|
|
source_resource_type="campaign_version",
|
|
source_resource_id=source_resource_id,
|
|
)
|
|
)
|
|
|
|
@staticmethod
|
|
def request_from_payload(payload: dict[str, Any]) -> CalendarInvitationRequest:
|
|
from datetime import datetime
|
|
|
|
attendees = tuple(
|
|
CalendarInvitationAttendeeRequest(
|
|
address=str(item.get("address") or ""),
|
|
name=str(item["name"]) if item.get("name") else None,
|
|
role=str(item.get("role") or "REQ-PARTICIPANT"),
|
|
participation_status=str(
|
|
item.get("participation_status") or "NEEDS-ACTION"
|
|
),
|
|
rsvp=bool(item.get("rsvp", True)),
|
|
)
|
|
for item in payload.get("attendees") or []
|
|
if isinstance(item, dict)
|
|
)
|
|
return CalendarInvitationRequest(
|
|
correlation_id=str(payload.get("correlation_id") or ""),
|
|
source_module="campaigns",
|
|
source_resource_type="campaign_version",
|
|
source_resource_id=(
|
|
str(payload["source_resource_id"])
|
|
if payload.get("source_resource_id")
|
|
else None
|
|
),
|
|
calendar_id=(
|
|
str(payload["calendar_id"]) if payload.get("calendar_id") else None
|
|
),
|
|
summary=str(payload.get("summary") or ""),
|
|
description=(
|
|
str(payload["description"]) if payload.get("description") else None
|
|
),
|
|
location=str(payload["location"]) if payload.get("location") else None,
|
|
start_at=datetime.fromisoformat(str(payload.get("start_at") or "")),
|
|
end_at=(
|
|
datetime.fromisoformat(str(payload["end_at"]))
|
|
if payload.get("end_at")
|
|
else None
|
|
),
|
|
timezone=str(payload["timezone"]) if payload.get("timezone") else None,
|
|
organizer=(
|
|
dict(payload["organizer"])
|
|
if isinstance(payload.get("organizer"), dict)
|
|
else None
|
|
),
|
|
attendees=attendees,
|
|
classification=str(payload.get("classification") or "PUBLIC"),
|
|
categories=tuple(str(value) for value in payload.get("categories") or []),
|
|
metadata=(
|
|
dict(payload["metadata"])
|
|
if isinstance(payload.get("metadata"), dict)
|
|
else {}
|
|
),
|
|
)
|
|
|
|
|
|
def files_integration() -> FilesCampaignIntegration:
|
|
return FilesCampaignIntegration(capability(FILES_CAPABILITY))
|
|
|
|
|
|
def mail_integration() -> MailCampaignIntegration:
|
|
return MailCampaignIntegration(capability(MAIL_CAPABILITY))
|
|
|
|
|
|
def postbox_integration() -> PostboxCampaignIntegration:
|
|
return PostboxCampaignIntegration(
|
|
capability(POSTBOX_CAPABILITY),
|
|
capability(POSTBOX_DIRECTORY_CAPABILITY),
|
|
capability(POSTBOX_EVIDENCE_CAPABILITY),
|
|
)
|
|
|
|
|
|
def approvals_integration() -> ApprovalCampaignIntegration:
|
|
return ApprovalCampaignIntegration(capability(APPROVALS_CAPABILITY))
|
|
|
|
|
|
def templates_integration() -> TemplatesCampaignIntegration:
|
|
return TemplatesCampaignIntegration(
|
|
capability(TEMPLATE_CATALOG_CAPABILITY),
|
|
capability(TEMPLATE_RENDERER_CAPABILITY),
|
|
capability(TEMPLATE_CONTENT_LIBRARY_CAPABILITY),
|
|
)
|
|
|
|
|
|
def calendar_integration() -> CalendarCampaignIntegration:
|
|
return CalendarCampaignIntegration(capability(CALENDAR_INVITATIONS_CAPABILITY))
|