Add governed tabular and LDAP address sources
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
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"]
|
||||
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"
|
||||
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
|
||||
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):
|
||||
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",
|
||||
]
|
||||
Reference in New Issue
Block a user