Files
govoplan-decisions/src/govoplan_decisions/backend/record_source.py
T

130 lines
4.5 KiB
Python

from __future__ import annotations
from collections.abc import Sequence
import hashlib
import json
from sqlalchemy.orm import Session
from govoplan_core.core.institutional import InstitutionalReference
from govoplan_core.core.records import (
RecordContractError,
RecordSourceLocator,
RecordSourceReference,
)
from govoplan_decisions.backend.db.models import FormalDecisionRevision
from govoplan_decisions.backend.service import SqlDecisionRegistry
CAPABILITY_RECORD_SOURCE_DECISIONS = "records.source.decisions"
class DecisionsRecordSource:
provider_id = "decisions"
def resource_types(self) -> Sequence[str]:
return ("decision_revision",)
def resolve(
self,
session: object,
principal: object,
*,
locator: RecordSourceLocator,
purpose: str,
) -> RecordSourceReference:
if not isinstance(session, Session):
raise RecordContractError(
"Decision record references require a database session."
)
tenant_id = str(getattr(principal, "tenant_id", "") or "").strip()
if not tenant_id or locator.tenant_id != tenant_id:
raise RecordContractError(
"Decision record references cannot cross tenants."
)
if (
locator.source_module != "decisions"
or locator.resource_type != "decision_revision"
):
raise RecordContractError("Unsupported Decisions record source type.")
if not str(purpose or "").strip():
raise RecordContractError("Decision record references require a purpose.")
if not _has(principal, "decisions:decision:read"):
raise RecordContractError("Current Decision read permission is required.")
if not (
_has(principal, "decisions:decision:read_sensitive")
or _has(principal, "decisions:decision:admin")
):
raise RecordContractError(
"Protected Decision read permission is required to file the complete formal outcome."
)
row = (
session.query(FormalDecisionRevision)
.filter(
FormalDecisionRevision.tenant_id == tenant_id,
FormalDecisionRevision.decision_id == locator.resource_id,
FormalDecisionRevision.revision == locator.source_revision,
)
.one_or_none()
)
if row is None:
raise RecordContractError("The exact Decision revision does not exist.")
decision = SqlDecisionRegistry().get_decision(
session,
principal,
reference=_decision_reference(row),
)
if decision is None:
raise RecordContractError("The exact Decision revision is unavailable.")
snapshot_json = json.dumps(
row.payload,
sort_keys=True,
separators=(",", ":"),
default=str,
).encode("utf-8")
return RecordSourceReference(
locator=locator,
label=f"Decision {decision.decision_type} - {decision.reference.object_id}",
authority_mode="external_authoritative",
content_sha256=hashlib.sha256(snapshot_json).hexdigest(),
content_type="application/vnd.govoplan.formal-decision-revision+json",
size_bytes=len(snapshot_json),
valid_from=decision.temporal.valid_from,
valid_to=decision.temporal.valid_to,
recorded_at=decision.temporal.recorded_at,
metadata={
"decision_type": decision.decision_type,
"state": decision.state,
"assurance_level": decision.assurance_level,
"subject_count": len(decision.subject_refs),
"evidence_count": len(decision.fact_evidence),
"protected_snapshot": True,
"snapshot_sha256": hashlib.sha256(snapshot_json).hexdigest(),
},
)
def create_decisions_record_source(_context: object) -> DecisionsRecordSource:
return DecisionsRecordSource()
def _decision_reference(row: FormalDecisionRevision) -> InstitutionalReference:
return InstitutionalReference(
kind="decision",
owner_module="decisions",
object_id=row.decision_id,
tenant_id=row.tenant_id,
version=row.revision,
)
def _has(principal: object, scope: str) -> bool:
return bool(hasattr(principal, "has") and principal.has(scope))
__all__ = [
"CAPABILITY_RECORD_SOURCE_DECISIONS",
"DecisionsRecordSource",
"create_decisions_record_source",
]