Files
govoplan-postbox/src/govoplan_postbox/backend/schemas.py
T

424 lines
14 KiB
Python

from __future__ import annotations
from datetime import datetime
from typing import Any, Literal
from pydantic import BaseModel, Field, model_validator
class PostboxAccessDecisionResponse(BaseModel):
allowed: bool
action: str
postbox_id: str
reason_code: str
explanation: str
organization_unit_id: str | None = None
function_id: str | None = None
assignment_ids: list[str] = Field(default_factory=list)
assignment_sources: list[str] = Field(default_factory=list)
selected_assignment_id: str | None = None
holder_count: int = 0
vacant: bool = True
class PostboxDirectoryItem(BaseModel):
id: str
tenant_id: str
address: str
address_key: str
name: str
status: str
classification: str
organization_unit_id: str | None = None
organization_unit_name: str | None = None
function_id: str | None = None
function_name: str | None = None
context_key: str | None = None
template_revision_id: str | None = None
holder_count: int = 0
vacant: bool = True
access: PostboxAccessDecisionResponse | None = None
class PostboxDirectoryResponse(BaseModel):
postboxes: list[PostboxDirectoryItem]
class PostboxParticipantPayload(BaseModel):
kind: str = Field(min_length=1, max_length=30)
reference_type: str = Field(min_length=1, max_length=50)
reference_id: str | None = Field(default=None, max_length=255)
label: str | None = Field(default=None, max_length=500)
address: str | None = Field(default=None, max_length=500)
class PostboxAttachmentPayload(BaseModel):
reference_type: str = Field(min_length=1, max_length=50)
reference_id: str = Field(min_length=1, max_length=255)
name: str | None = Field(default=None, max_length=1000)
media_type: str | None = Field(default=None, max_length=255)
size_bytes: int | None = Field(default=None, ge=0)
digest: str | None = Field(default=None, max_length=255)
metadata: dict[str, Any] = Field(default_factory=dict)
class PostboxMessageItem(BaseModel):
id: str
tenant_id: str
postbox_id: str
subject: str
body_text: str | None = None
status: str
classification: str
sender_label: str | None = None
delivered_at: datetime
read_at: datetime | None = None
acknowledged_at: datetime | None = None
expires_at: datetime | None = None
withdrawn_at: datetime | None = None
producer_module: str | None = None
producer_resource_type: str | None = None
producer_resource_id: str | None = None
encryption_profile: str
key_epoch: int
ciphertext_ref: str | None = None
signed_manifest_ref: str | None = None
participants: list[PostboxParticipantPayload] = Field(default_factory=list)
attachments: list[PostboxAttachmentPayload] = Field(default_factory=list)
metadata: dict[str, Any] = Field(default_factory=dict)
class PostboxMessageListResponse(BaseModel):
messages: list[PostboxMessageItem]
total: int
limit: int
offset: int
class PostboxMessageStateRequest(BaseModel):
state: Literal["read", "acknowledged"]
class PostboxTargetPayload(BaseModel):
postbox_id: str | None = Field(default=None, max_length=36)
address_key: str | None = Field(default=None, max_length=500)
template_id: str | None = Field(default=None, max_length=36)
organization_unit_id: str | None = Field(default=None, max_length=36)
function_id: str | None = Field(default=None, max_length=36)
context_key: str | None = Field(default=None, max_length=255)
@model_validator(mode="after")
def validate_target(self) -> "PostboxTargetPayload":
direct = bool(self.postbox_id or self.address_key)
templated = bool(
self.template_id
and self.organization_unit_id
and self.function_id
)
if direct == templated:
raise ValueError(
"Specify one direct Postbox target or one complete template target."
)
return self
class PostboxDeliveryCreateRequest(BaseModel):
target: PostboxTargetPayload
producer_module: str = Field(min_length=1, max_length=100)
producer_resource_type: str = Field(min_length=1, max_length=100)
producer_resource_id: str | None = Field(default=None, max_length=255)
idempotency_key: str = Field(min_length=1, max_length=255)
subject: str = Field(min_length=1, max_length=1000)
body_text: str | None = None
sender_label: str | None = Field(default=None, max_length=500)
classification: str = Field(default="internal", min_length=1, max_length=50)
participants: list[PostboxParticipantPayload] = Field(default_factory=list)
attachments: list[PostboxAttachmentPayload] = Field(default_factory=list)
expires_at: datetime | None = None
metadata: dict[str, Any] = Field(default_factory=dict)
class PostboxDeliveryResponse(BaseModel):
delivery_id: str
postbox_id: str
message_id: str
address: str
status: str
vacant: bool
holder_count: int
duplicate: bool = False
evidence: dict[str, Any] = Field(default_factory=dict)
class PostboxLinkedCopyPolicyPayload(BaseModel):
enabled: bool = False
structure_id: str | None = Field(default=None, max_length=36)
relation_type_ids: list[str] = Field(default_factory=list, max_length=20)
max_depth: int = Field(default=1, ge=1, le=20)
stop_unit_id: str | None = Field(default=None, max_length=36)
stop_unit_type_id: str | None = Field(default=None, max_length=36)
target_function_type_id: str | None = Field(default=None, max_length=36)
target_template_id: str | None = Field(default=None, max_length=36)
fanout: Literal["nearest", "all"] = "nearest"
allowed_classifications: list[str] = Field(
default_factory=lambda: ["internal"],
max_length=20,
)
allowed_producer_modules: list[str] = Field(
default_factory=list,
max_length=50,
)
require_expiry: bool = False
max_retention_days: int | None = Field(default=None, ge=1, le=36500)
@model_validator(mode="after")
def validate_enabled_policy(self) -> "PostboxLinkedCopyPolicyPayload":
self.relation_type_ids = list(dict.fromkeys(self.relation_type_ids))
self.allowed_classifications = list(
dict.fromkeys(
value.strip() for value in self.allowed_classifications
)
)
self.allowed_producer_modules = list(
dict.fromkeys(
value.strip() for value in self.allowed_producer_modules
)
)
if any(not value for value in self.relation_type_ids):
raise ValueError("Relation type IDs must not be empty.")
if any(not value for value in self.allowed_classifications):
raise ValueError("Allowed classifications must not be empty.")
if any(not value for value in self.allowed_producer_modules):
raise ValueError("Allowed producer modules must not be empty.")
if self.enabled and not all(
(
self.structure_id,
self.target_function_type_id,
self.target_template_id,
self.allowed_classifications,
self.allowed_producer_modules,
)
):
raise ValueError(
"Enabled hierarchy copy requires a structure, target function "
"type, target template, classification gate, and producer allowlist."
)
return self
class PostboxAttentionPolicyPayload(BaseModel):
mode: Literal["none", "vacancy_escalation"] = "none"
delay_minutes: int | None = Field(default=None, ge=1, le=43200)
@model_validator(mode="after")
def validate_delay(self) -> "PostboxAttentionPolicyPayload":
if self.mode == "vacancy_escalation" and self.delay_minutes is None:
raise ValueError("Vacancy escalation requires a delay.")
if self.mode == "none":
self.delay_minutes = None
return self
class PostboxSharedVisibilityPolicyPayload(BaseModel):
mode: Literal["none"] = "none"
class PostboxRoutingPolicyPayload(BaseModel):
linked_copy: PostboxLinkedCopyPolicyPayload = Field(
default_factory=PostboxLinkedCopyPolicyPayload
)
attention: PostboxAttentionPolicyPayload = Field(
default_factory=PostboxAttentionPolicyPayload
)
shared_visibility: PostboxSharedVisibilityPolicyPayload = Field(
default_factory=PostboxSharedVisibilityPolicyPayload
)
@model_validator(mode="before")
@classmethod
def normalize_legacy_policy(cls, value: Any) -> Any:
if value in (None, {}, {"mode": "none"}):
return {}
return value
@model_validator(mode="after")
def validate_semantics(self) -> "PostboxRoutingPolicyPayload":
if (
self.attention.mode == "vacancy_escalation"
and (
not self.linked_copy.enabled
or self.linked_copy.fanout != "nearest"
)
):
raise ValueError(
"Vacancy escalation requires nearest linked-copy routing."
)
return self
class PostboxRoutePreviewTarget(BaseModel):
depth: int
organization_unit_id: str
organization_unit_name: str
function_id: str | None = None
function_name: str | None = None
target_postbox_id: str | None = None
target_address: str | None = None
status: str
vacant: bool = True
holder_count: int = 0
path: list[dict[str, Any]] = Field(default_factory=list)
diagnostics: list[str] = Field(default_factory=list)
class PostboxRouteDryRunRequest(BaseModel):
target: PostboxTargetPayload
producer_module: str = Field(min_length=1, max_length=100)
classification: str = Field(default="internal", min_length=1, max_length=50)
expires_at: datetime | None = None
class PostboxRouteDryRunResponse(BaseModel):
status: str
source_postbox_id: str | None = None
policy: PostboxRoutingPolicyPayload = Field(
default_factory=PostboxRoutingPolicyPayload
)
routes: list[PostboxRoutePreviewTarget] = Field(default_factory=list)
diagnostics: list[str] = Field(default_factory=list)
class PostboxExactCreateRequest(BaseModel):
name: str = Field(min_length=1, max_length=500)
description: str | None = None
organization_unit_id: str = Field(min_length=1, max_length=36)
function_id: str = Field(min_length=1, max_length=36)
address_key: str | None = Field(default=None, max_length=120)
classification: str = Field(default="internal", min_length=1, max_length=50)
class PostboxTemplateRevisionPayload(BaseModel):
function_type_id: str | None = Field(default=None, max_length=36)
scope_kind: Literal["tenant", "unit", "subtree", "unit_type"] = "tenant"
scope_id: str | None = Field(default=None, max_length=255)
name_pattern: str = Field(
default="{unit_name} / {function_name}",
min_length=1,
max_length=500,
)
address_pattern: str = Field(
default="{template_slug}.{unit_slug}.{function_slug}",
min_length=1,
max_length=500,
)
classification: str = Field(default="internal", min_length=1, max_length=50)
allow_vacant_delivery: bool = True
routing_policy: PostboxRoutingPolicyPayload = Field(
default_factory=PostboxRoutingPolicyPayload
)
class PostboxTemplateCreateRequest(PostboxTemplateRevisionPayload):
slug: str = Field(min_length=1, max_length=120)
name: str = Field(min_length=1, max_length=250)
description: str | None = None
class PostboxTemplateRevisionItem(PostboxTemplateRevisionPayload):
id: str
revision: int
encryption_profile: str
history_policy: dict[str, Any] = Field(default_factory=dict)
retention_policy: dict[str, Any] = Field(default_factory=dict)
published_at: datetime | None = None
created_at: datetime
class PostboxTemplateItem(BaseModel):
id: str
tenant_id: str
slug: str
name: str
description: str | None = None
status: str
current_revision: int
published_revision_id: str | None = None
revisions: list[PostboxTemplateRevisionItem] = Field(default_factory=list)
created_at: datetime
updated_at: datetime
class PostboxTemplateListResponse(BaseModel):
templates: list[PostboxTemplateItem]
class PostboxTemplatePublishRequest(BaseModel):
revision: int | None = Field(default=None, ge=1)
class PostboxMaterializeRequest(BaseModel):
organization_unit_id: str = Field(min_length=1, max_length=36)
function_id: str = Field(min_length=1, max_length=36)
context_key: str | None = Field(default=None, max_length=255)
class PostboxOrganizationFunctionItem(BaseModel):
id: str
slug: str
name: str
function_type_id: str | None = None
delegable: bool = False
act_in_place_allowed: bool = False
class PostboxOrganizationUnitItem(BaseModel):
id: str
slug: str
name: str
unit_type_id: str | None = None
parent_id: str | None = None
functions: list[PostboxOrganizationFunctionItem] = Field(default_factory=list)
class PostboxOrganizationRelationTypeItem(BaseModel):
id: str
slug: str
name: str
structure_id: str | None = None
is_hierarchical: bool = True
status: str = "active"
class PostboxOrganizationStructureItem(BaseModel):
id: str
slug: str
name: str
structure_kind: str
status: str = "active"
relation_types: list[PostboxOrganizationRelationTypeItem] = Field(
default_factory=list
)
class PostboxOrganizationTargetsResponse(BaseModel):
units: list[PostboxOrganizationUnitItem]
structures: list[PostboxOrganizationStructureItem] = Field(
default_factory=list
)
class PostboxGroupingPayload(BaseModel):
name: str = Field(min_length=1, max_length=250)
is_default: bool = False
postbox_ids: list[str] = Field(default_factory=list, max_length=250)
class PostboxGroupingItem(PostboxGroupingPayload):
id: str
created_at: datetime
updated_at: datetime
class PostboxGroupingListResponse(BaseModel):
groupings: list[PostboxGroupingItem]