471 lines
15 KiB
Python
471 lines
15 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import Any, Literal
|
|
|
|
from pydantic import BaseModel, Field, model_validator
|
|
|
|
|
|
PostboxClassification = Literal[
|
|
"public",
|
|
"internal",
|
|
"confidential",
|
|
"restricted",
|
|
]
|
|
|
|
|
|
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
|
|
classification: str = "internal"
|
|
classification_allowed: bool = True
|
|
binding_status: str = "active"
|
|
|
|
|
|
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
|
|
resource_revision: int = Field(default=1, ge=1)
|
|
etag: str | 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
|
|
availability: Literal["available", "withdrawn", "expired"]
|
|
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
|
|
in_reply_to_message_id: str | None = None
|
|
replaces_message_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 PostboxMessageAuthoringPayload(BaseModel):
|
|
idempotency_key: str = Field(min_length=1, max_length=255)
|
|
subject: str = Field(min_length=1, max_length=1000)
|
|
body_text: str | None = None
|
|
classification: PostboxClassification = "internal"
|
|
participants: list[PostboxParticipantPayload] = Field(default_factory=list)
|
|
attachments: list[PostboxAttachmentPayload] = Field(default_factory=list)
|
|
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
|
|
class PostboxMessageCreateRequest(PostboxMessageAuthoringPayload):
|
|
postbox_id: str = Field(min_length=1, max_length=36)
|
|
|
|
|
|
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: PostboxClassification = "internal"
|
|
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[PostboxClassification] = 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: PostboxClassification = "internal"
|
|
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: PostboxClassification = "internal"
|
|
|
|
|
|
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: PostboxClassification = "internal"
|
|
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 PostboxTemplateReviseRequest(PostboxTemplateRevisionPayload):
|
|
base_revision: int = Field(ge=1)
|
|
|
|
|
|
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
|
|
resource_revision: int = Field(ge=1)
|
|
etag: str
|
|
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)
|
|
base_revision: int = Field(ge=1)
|
|
|
|
|
|
class PostboxMutationRequest(BaseModel):
|
|
base_revision: int = Field(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 PostboxGroupingUpdateRequest(PostboxGroupingPayload):
|
|
base_revision: int = Field(ge=1)
|
|
|
|
|
|
class PostboxGroupingItem(PostboxGroupingPayload):
|
|
id: str
|
|
resource_revision: int = Field(ge=1)
|
|
etag: str
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
|
|
class PostboxGroupingListResponse(BaseModel):
|
|
groupings: list[PostboxGroupingItem]
|