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