Files
govoplan-addresses/src/govoplan_addresses/backend/import_schemas.py
T

175 lines
5.8 KiB
Python

from __future__ import annotations
from datetime import datetime
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, model_validator
AddressImportFormat = Literal["csv", "xlsx", "ldif"]
AddressImportScope = Literal["user", "group", "tenant", "system"]
IMPORT_TARGET_FIELDS = frozenset(
{
"source_key",
"display_name",
"given_name",
"family_name",
"organization",
"role_title",
"note",
"email",
"phone",
"street",
"postal_code",
"locality",
"region",
"country",
"tags",
"visibility",
}
)
class AddressImportConfiguration(BaseModel):
field_mappings: dict[str, str] = Field(default_factory=dict, max_length=40)
delimiter: Literal[",", ";", "\t", "|"] = ","
encoding: Literal["utf-8", "utf-8-sig", "cp1252", "latin-1"] = "utf-8-sig"
header_row: int = Field(default=1, ge=1, le=100)
sheet_name: str | None = Field(default=None, max_length=255)
source_key_column: str | None = Field(default=None, max_length=255)
duplicate_source_key_policy: Literal["reject", "first", "last"] = "reject"
existing_contact_policy: Literal["update", "ignore", "reject"] = "update"
blank_value_policy: Literal["ignore", "clear", "reject"] = "ignore"
ldif_change_record_policy: Literal["reject", "ignore", "treat_add_as_entry"] = "reject"
locale: str | None = Field(default=None, max_length=35)
default_tags: list[str] = Field(default_factory=list, max_length=100)
max_rows: int = Field(default=10_000, ge=1, le=10_000)
@model_validator(mode="after")
def validate_mappings(self) -> "AddressImportConfiguration":
invalid = sorted(set(self.field_mappings).difference(IMPORT_TARGET_FIELDS))
if invalid:
raise ValueError(f"Unsupported address import target fields: {', '.join(invalid)}")
for target, column in self.field_mappings.items():
if not target.strip() or not column.strip():
raise ValueError("Import mapping targets and source columns cannot be blank.")
if "source_key" not in self.field_mappings and not self.source_key_column:
raise ValueError("Address import profiles require a stable source-key column.")
return self
class AddressImportProfileCreateRequest(BaseModel):
scope_type: AddressImportScope = "tenant"
scope_id: str | None = Field(default=None, max_length=36)
name: str = Field(min_length=1, max_length=255)
description: str | None = Field(default=None, max_length=4000)
source_format: AddressImportFormat
configuration: AddressImportConfiguration
class AddressImportProfileUpdateRequest(BaseModel):
name: str | None = Field(default=None, min_length=1, max_length=255)
description: str | None = Field(default=None, max_length=4000)
configuration: AddressImportConfiguration | None = None
class AddressImportProfileResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str
profile_key: str
version: int
tenant_id: str | None = None
scope_type: str
scope_id: str | None = None
name: str
description: str | None = None
source_format: str
configuration: AddressImportConfiguration
is_current: bool
created_by_account_id: str | None = None
superseded_at: datetime | None = None
created_at: datetime
updated_at: datetime
class AddressImportProfileListResponse(BaseModel):
profiles: list[AddressImportProfileResponse] = Field(default_factory=list)
class AddressImportFilePayload(BaseModel):
filename: str = Field(min_length=1, max_length=500)
content_base64: str = Field(min_length=1, max_length=14_000_000)
class AddressImportPreviewRequest(AddressImportFilePayload):
profile_id: str = Field(min_length=1, max_length=36)
class AddressImportEffectResponse(BaseModel):
row_number: int
action: Literal["create", "update", "conflict", "unchanged", "ignored"]
source_key: str | None = None
contact_id: str | None = None
display_name: str | None = None
changed_fields: list[str] = Field(default_factory=list)
message: str | None = None
class AddressImportDiagnosticResponse(BaseModel):
severity: Literal["info", "warning", "error"]
code: str
message: str
row_number: int | None = None
field: str | None = None
details: dict[str, Any] = Field(default_factory=dict)
class AddressImportRunResponse(BaseModel):
id: str
address_book_id: str
profile_id: str | None
source_filename: str
source_format: str
input_hash: str
plan_hash: str
status: str
row_count: int
statistics: dict[str, int] = Field(default_factory=dict)
diagnostics: list[AddressImportDiagnosticResponse] = Field(default_factory=list)
effects: list[AddressImportEffectResponse] = Field(default_factory=list)
can_apply: bool
result_evidence: dict[str, Any] = Field(default_factory=dict)
created_at: datetime
updated_at: datetime
applied_at: datetime | None = None
rolled_back_at: datetime | None = None
class AddressImportCommitRequest(BaseModel):
expected_plan_hash: str = Field(min_length=64, max_length=64)
class AddressImportRollbackRequest(BaseModel):
expected_plan_hash: str = Field(min_length=64, max_length=64)
reason: str = Field(min_length=3, max_length=2000)
__all__ = [
"AddressImportCommitRequest",
"AddressImportConfiguration",
"AddressImportDiagnosticResponse",
"AddressImportEffectResponse",
"AddressImportFilePayload",
"AddressImportFormat",
"AddressImportPreviewRequest",
"AddressImportProfileCreateRequest",
"AddressImportProfileListResponse",
"AddressImportProfileResponse",
"AddressImportProfileUpdateRequest",
"AddressImportRollbackRequest",
"AddressImportRunResponse",
"IMPORT_TARGET_FIELDS",
]