175 lines
5.8 KiB
Python
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",
|
|
]
|