Add governed form evidence contract

This commit is contained in:
2026-08-06 12:42:20 +02:00
parent f5949427cc
commit b5f5be15f6
3 changed files with 359 additions and 0 deletions
+227
View File
@@ -0,0 +1,227 @@
from __future__ import annotations
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from datetime import datetime
from typing import Literal, Protocol, runtime_checkable
from govoplan_core.core.institutional import EvidenceReference, InstitutionalReference
CAPABILITY_FORM_EVIDENCE_PREFIX = "forms_runtime.evidence."
FormEvidenceState = Literal[
"accepted",
"pending",
"rejected",
"expired",
"revoked",
"unavailable",
]
_FORM_EVIDENCE_STATES = {
"accepted",
"pending",
"rejected",
"expired",
"revoked",
"unavailable",
}
class FormEvidenceContractError(ValueError):
"""Stable error for provider-neutral Form evidence operations."""
@dataclass(frozen=True, slots=True)
class FormEvidenceGrantRequest:
"""Request a short-lived, purpose-bound grant from an evidence owner."""
tenant_id: str
instance_id: str
definition_ref: InstitutionalReference
evidence_kind: str
purpose: str
idempotency_key: str
expires_at: datetime
custodian_ref: str | None = None
max_size_bytes: int | None = None
allowed_content_types: tuple[str, ...] = ()
metadata: Mapping[str, object] = field(default_factory=dict)
def __post_init__(self) -> None:
_require_text(self.tenant_id, "Form evidence tenant")
_require_text(self.instance_id, "Form evidence instance")
_require_text(self.evidence_kind, "Form evidence kind")
_require_text(self.purpose, "Form evidence purpose")
_require_text(self.idempotency_key, "Form evidence idempotency key")
if (
self.definition_ref.kind != "form"
or self.definition_ref.tenant_id != self.tenant_id
or not self.definition_ref.version
):
raise FormEvidenceContractError(
"Form evidence grants require an exact same-tenant Form definition."
)
if self.expires_at.tzinfo is None or self.expires_at.utcoffset() is None:
raise FormEvidenceContractError(
"Form evidence grant expiry must include a timezone."
)
if self.max_size_bytes is not None and self.max_size_bytes <= 0:
raise FormEvidenceContractError(
"Form evidence grant size limits must be positive."
)
if any(not item.strip() for item in self.allowed_content_types):
raise FormEvidenceContractError(
"Form evidence content types cannot be empty."
)
@dataclass(frozen=True, slots=True)
class FormEvidenceGrant:
provider_id: str
grant_id: str
upload_token: str | None
upload_url: str
expires_at: datetime
max_size_bytes: int
allowed_content_types: tuple[str, ...] = ()
replayed: bool = False
def __post_init__(self) -> None:
for value, label in (
(self.provider_id, "Form evidence provider"),
(self.grant_id, "Form evidence grant"),
(self.upload_url, "Form evidence upload URL"),
):
_require_text(value, label)
if self.upload_token is not None:
_require_text(self.upload_token, "Form evidence upload token")
if self.max_size_bytes <= 0:
raise FormEvidenceContractError(
"Form evidence grant size limits must be positive."
)
if self.expires_at.tzinfo is None or self.expires_at.utcoffset() is None:
raise FormEvidenceContractError(
"Form evidence grant expiry must include a timezone."
)
@dataclass(frozen=True, slots=True)
class FormEvidenceInspectionRequest:
tenant_id: str
instance_id: str
definition_ref: InstitutionalReference
evidence: EvidenceReference
purpose: str
final: bool
def __post_init__(self) -> None:
_require_text(self.tenant_id, "Form evidence tenant")
_require_text(self.instance_id, "Form evidence instance")
_require_text(self.purpose, "Form evidence purpose")
if self.definition_ref.tenant_id != self.tenant_id:
raise FormEvidenceContractError(
"Form evidence inspection cannot cross tenants."
)
if self.evidence.tenant_id != self.tenant_id:
raise FormEvidenceContractError(
"Form evidence inspection cannot cross tenants."
)
@dataclass(frozen=True, slots=True)
class FormEvidenceInspection:
provider_id: str
reference: EvidenceReference
state: FormEvidenceState
observed_at: datetime
retryable: bool = False
reason: str | None = None
metadata: Mapping[str, object] = field(default_factory=dict)
def __post_init__(self) -> None:
_require_text(self.provider_id, "Form evidence provider")
if self.state not in _FORM_EVIDENCE_STATES:
raise FormEvidenceContractError(
f"Unsupported Form evidence state: {self.state!r}."
)
if self.observed_at.tzinfo is None or self.observed_at.utcoffset() is None:
raise FormEvidenceContractError(
"Form evidence inspection time must include a timezone."
)
if self.state == "accepted" and self.retryable:
raise FormEvidenceContractError(
"Accepted Form evidence cannot require a retry."
)
@property
def accepted(self) -> bool:
return self.state == "accepted"
@runtime_checkable
class FormEvidenceProvider(Protocol):
provider_id: str
def supported_kinds(self) -> Sequence[str]: ...
def create_upload_grant(
self,
session: object,
principal: object,
*,
request: FormEvidenceGrantRequest,
) -> FormEvidenceGrant: ...
def inspect_evidence(
self,
session: object,
principal: object,
*,
request: FormEvidenceInspectionRequest,
) -> FormEvidenceInspection: ...
def form_evidence_capability(provider_id: str) -> str:
normalized = str(provider_id or "").strip().lower().replace("-", "_")
if not normalized or not normalized.replace("_", "").isalnum():
raise FormEvidenceContractError("Invalid Form evidence provider id.")
return f"{CAPABILITY_FORM_EVIDENCE_PREFIX}{normalized}"
def form_evidence_provider(
registry: object | None,
provider_id: str,
) -> FormEvidenceProvider | None:
capability_name = form_evidence_capability(provider_id)
if registry is None or not hasattr(registry, "has_capability"):
return None
if not registry.has_capability(capability_name):
return None
if hasattr(registry, "require_capability"):
provider = registry.require_capability(capability_name)
elif hasattr(registry, "capability"):
provider = registry.capability(capability_name)
else:
return None
return provider if isinstance(provider, FormEvidenceProvider) else None
def _require_text(value: str, label: str) -> None:
if not isinstance(value, str) or not value.strip():
raise FormEvidenceContractError(f"{label} is required.")
__all__ = [
"CAPABILITY_FORM_EVIDENCE_PREFIX",
"FormEvidenceContractError",
"FormEvidenceGrant",
"FormEvidenceGrantRequest",
"FormEvidenceInspection",
"FormEvidenceInspectionRequest",
"FormEvidenceProvider",
"FormEvidenceState",
"form_evidence_capability",
"form_evidence_provider",
]