feat(poll): define the scheduling capability contract
This commit is contained in:
215
src/govoplan_core/core/poll.py
Normal file
215
src/govoplan_core/core/poll.py
Normal file
@@ -0,0 +1,215 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
|
||||
CAPABILITY_POLL_SCHEDULING = "poll.scheduling"
|
||||
|
||||
|
||||
class PollCapabilityError(ValueError):
|
||||
"""Stable error raised by Poll capability implementations."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PollOptionRequest:
|
||||
label: str
|
||||
key: str | None = None
|
||||
description: str | None = None
|
||||
value: Mapping[str, object] | None = None
|
||||
metadata: Mapping[str, object] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PollCreateCommand:
|
||||
title: str
|
||||
kind: str
|
||||
status: str
|
||||
visibility: str
|
||||
result_visibility: str
|
||||
description: str | None = None
|
||||
context_module: str | None = None
|
||||
context_resource_type: str | None = None
|
||||
context_resource_id: str | None = None
|
||||
workflow_state: str | None = None
|
||||
workflow_steps: tuple[Mapping[str, object], ...] = ()
|
||||
allow_anonymous: bool = False
|
||||
allow_response_update: bool = True
|
||||
min_choices: int = 1
|
||||
max_choices: int | None = None
|
||||
opens_at: datetime | None = None
|
||||
closes_at: datetime | None = None
|
||||
options: tuple[PollOptionRequest, ...] = ()
|
||||
metadata: Mapping[str, object] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PollUpdateCommand:
|
||||
"""Scheduling-owned Poll fields synchronized as one policy snapshot."""
|
||||
|
||||
title: str
|
||||
description: str | None
|
||||
visibility: str
|
||||
result_visibility: str
|
||||
allow_anonymous: bool
|
||||
allow_response_update: bool
|
||||
closes_at: datetime | None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PollInvitationCommand:
|
||||
respondent_id: str | None = None
|
||||
respondent_label: str | None = None
|
||||
email: str | None = None
|
||||
expires_at: datetime | None = None
|
||||
metadata: Mapping[str, object] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PollAnswerRequest:
|
||||
option_id: str | None = None
|
||||
option_key: str | None = None
|
||||
value: object = None
|
||||
rank: int | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PollSubmitResponseCommand:
|
||||
respondent_id: str
|
||||
respondent_label: str | None = None
|
||||
answers: tuple[PollAnswerRequest, ...] = ()
|
||||
metadata: Mapping[str, object] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PollOptionRef:
|
||||
id: str
|
||||
position: int
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PollRef:
|
||||
id: str
|
||||
status: str
|
||||
options: tuple[PollOptionRef, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PollInvitationRef:
|
||||
id: str
|
||||
token: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PollResponseRef:
|
||||
invitation_id: str | None
|
||||
submitted_at: datetime
|
||||
# Optional for backwards compatibility with existing providers. A
|
||||
# consumer can use it to reconcile an authenticated response without
|
||||
# trusting client-supplied invitation metadata.
|
||||
respondent_id: str | None = None
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class PollSchedulingProvider(Protocol):
|
||||
def create_poll(
|
||||
self,
|
||||
session: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
user_id: str | None,
|
||||
command: PollCreateCommand,
|
||||
) -> PollRef:
|
||||
...
|
||||
|
||||
def create_invitation(
|
||||
self,
|
||||
session: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
poll_id: str,
|
||||
command: PollInvitationCommand,
|
||||
) -> PollInvitationRef:
|
||||
...
|
||||
|
||||
def update_poll(
|
||||
self,
|
||||
session: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
poll_id: str,
|
||||
command: PollUpdateCommand,
|
||||
) -> PollRef:
|
||||
...
|
||||
|
||||
def open_poll(self, session: object, *, tenant_id: str, poll_id: str) -> PollRef:
|
||||
...
|
||||
|
||||
def close_poll(self, session: object, *, tenant_id: str, poll_id: str) -> PollRef:
|
||||
...
|
||||
|
||||
def decide_poll(
|
||||
self,
|
||||
session: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
poll_id: str,
|
||||
option_id: str | None,
|
||||
) -> PollRef:
|
||||
...
|
||||
|
||||
def get_poll(self, session: object, *, tenant_id: str, poll_id: str) -> PollRef:
|
||||
...
|
||||
|
||||
def set_workflow_context(
|
||||
self,
|
||||
session: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
poll_id: str,
|
||||
workflow_state: str,
|
||||
workflow_steps: Sequence[Mapping[str, object]],
|
||||
context_module: str,
|
||||
context_resource_type: str,
|
||||
context_resource_id: str,
|
||||
) -> PollRef:
|
||||
...
|
||||
|
||||
def result_summary(self, session: object, *, tenant_id: str, poll_id: str) -> Mapping[str, object]:
|
||||
...
|
||||
|
||||
def list_responses(self, session: object, *, tenant_id: str, poll_id: str) -> Sequence[PollResponseRef]:
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class PollResponseSubmissionProvider(Protocol):
|
||||
"""Optional extension for modules that collect responses through Poll."""
|
||||
|
||||
def submit_response(
|
||||
self,
|
||||
session: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
poll_id: str,
|
||||
command: PollSubmitResponseCommand,
|
||||
) -> PollResponseRef:
|
||||
...
|
||||
|
||||
|
||||
def poll_scheduling_provider(registry: object | None) -> PollSchedulingProvider | None:
|
||||
if registry is None or not hasattr(registry, "has_capability"):
|
||||
return None
|
||||
if not registry.has_capability(CAPABILITY_POLL_SCHEDULING):
|
||||
return None
|
||||
capability = registry.capability(CAPABILITY_POLL_SCHEDULING)
|
||||
return capability if isinstance(capability, PollSchedulingProvider) else None
|
||||
|
||||
|
||||
def poll_response_submission_provider(
|
||||
registry: object | None,
|
||||
) -> PollResponseSubmissionProvider | None:
|
||||
provider = poll_scheduling_provider(registry)
|
||||
return provider if isinstance(provider, PollResponseSubmissionProvider) else None
|
||||
Reference in New Issue
Block a user