Release v0.1.4
This commit is contained in:
@@ -489,6 +489,35 @@ class EntryConfig(StrictModel):
|
||||
last_sent: str | None = None
|
||||
|
||||
|
||||
class ImportColumnProvenance(StrictModel):
|
||||
column_index: int = Field(ge=0)
|
||||
header: str
|
||||
kind: str
|
||||
field_name: str | None = None
|
||||
new_field_name: str | None = None
|
||||
|
||||
|
||||
class ImportProvenance(StrictModel):
|
||||
id: str
|
||||
imported_at: str
|
||||
mode: Literal["append", "replace"]
|
||||
source_type: Literal["csv", "xlsx", "text"]
|
||||
filename: str | None = None
|
||||
sheet_name: str | None = None
|
||||
encoding: str | None = None
|
||||
delimiter: str | None = None
|
||||
header_rows: int = Field(default=0, ge=0)
|
||||
quoted: bool | None = None
|
||||
value_separators: str | None = None
|
||||
rows_total: int = Field(default=0, ge=0)
|
||||
valid_rows: int = Field(default=0, ge=0)
|
||||
invalid_rows: int = Field(default=0, ge=0)
|
||||
imported_rows: int = Field(default=0, ge=0)
|
||||
field_names_created: list[str] = Field(default_factory=list)
|
||||
attachment_patterns: int = Field(default=0, ge=0)
|
||||
mapping: list[ImportColumnProvenance] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SourceConfig(StrictModel):
|
||||
type: SourceType
|
||||
path: str
|
||||
@@ -502,6 +531,7 @@ class EntriesConfig(StrictModel):
|
||||
source: SourceConfig | None = None
|
||||
mapping: dict[str, str] | None = None
|
||||
defaults: EntryConfig | None = None
|
||||
imports: list[ImportProvenance] = Field(default_factory=list)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def inline_or_external(self) -> "EntriesConfig":
|
||||
|
||||
@@ -144,6 +144,33 @@ class CampaignShare(Base, TimestampMixin):
|
||||
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
|
||||
|
||||
|
||||
class RecipientImportMappingProfile(Base, TimestampMixin):
|
||||
__tablename__ = "campaign_recipient_import_mapping_profiles"
|
||||
__table_args__ = (
|
||||
Index("ix_recipient_import_profiles_owner", "tenant_id", "owner_user_id"),
|
||||
Index("ix_recipient_import_profiles_ordered_fp", "tenant_id", "owner_user_id", "ordered_header_fingerprint"),
|
||||
Index("ix_recipient_import_profiles_unordered_fp", "tenant_id", "owner_user_id", "unordered_header_fingerprint"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
owner_user_id: Mapped[str] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
column_count: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
headers: Mapped[list[str]] = mapped_column(JSON, default=list, nullable=False)
|
||||
normalized_headers: Mapped[list[str]] = mapped_column(JSON, default=list, nullable=False)
|
||||
ordered_header_fingerprint: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
||||
unordered_header_fingerprint: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
||||
delimiter: Mapped[str] = mapped_column(String(8), nullable=False)
|
||||
header_rows: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
|
||||
quoted: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
value_separators: Mapped[str] = mapped_column(String(50), default=",;|", nullable=False)
|
||||
mappings: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list, nullable=False)
|
||||
|
||||
tenant: Mapped[Tenant] = relationship()
|
||||
owner: Mapped[User] = relationship()
|
||||
|
||||
|
||||
class CampaignVersion(Base, TimestampMixin):
|
||||
__tablename__ = "campaign_versions"
|
||||
__table_args__ = (UniqueConstraint("campaign_id", "version_number", name="uq_campaign_versions_campaign_number"),)
|
||||
|
||||
@@ -109,7 +109,7 @@ def _campaigns_router(context: ModuleContext):
|
||||
manifest = ModuleManifest(
|
||||
id="campaigns",
|
||||
name="Campaigns",
|
||||
version="0.1.2",
|
||||
version="0.1.4",
|
||||
dependencies=("access",),
|
||||
optional_dependencies=("files", "mail"),
|
||||
permissions=PERMISSIONS,
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""recipient import mapping profiles
|
||||
|
||||
Revision ID: 2c3d4e5f7081
|
||||
Revises: 1b2c3d4e5f70
|
||||
Create Date: 2026-06-26 00:00:00.000000
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "2c3d4e5f7081"
|
||||
down_revision = "1b2c3d4e5f70"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
if "campaign_recipient_import_mapping_profiles" in inspector.get_table_names():
|
||||
return
|
||||
op.create_table(
|
||||
"campaign_recipient_import_mapping_profiles",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("tenant_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("owner_user_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("name", sa.String(length=255), nullable=False),
|
||||
sa.Column("column_count", sa.Integer(), nullable=False),
|
||||
sa.Column("headers", sa.JSON(), nullable=False),
|
||||
sa.Column("normalized_headers", sa.JSON(), nullable=False),
|
||||
sa.Column("ordered_header_fingerprint", sa.String(length=64), nullable=False),
|
||||
sa.Column("unordered_header_fingerprint", sa.String(length=64), nullable=False),
|
||||
sa.Column("delimiter", sa.String(length=8), nullable=False),
|
||||
sa.Column("header_rows", sa.Integer(), nullable=False),
|
||||
sa.Column("quoted", sa.Boolean(), nullable=False),
|
||||
sa.Column("value_separators", sa.String(length=50), nullable=False),
|
||||
sa.Column("mappings", sa.JSON(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["owner_user_id"], ["users.id"], name=op.f("fk_campaign_recipient_import_mapping_profiles_owner_user_id_users"), ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["tenant_id"], ["tenants.id"], name=op.f("fk_campaign_recipient_import_mapping_profiles_tenant_id_tenants"), ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_campaign_recipient_import_mapping_profiles")),
|
||||
)
|
||||
op.create_index(op.f("ix_campaign_recipient_import_mapping_profiles_tenant_id"), "campaign_recipient_import_mapping_profiles", ["tenant_id"], unique=False)
|
||||
op.create_index(op.f("ix_campaign_recipient_import_mapping_profiles_owner_user_id"), "campaign_recipient_import_mapping_profiles", ["owner_user_id"], unique=False)
|
||||
op.create_index(op.f("ix_campaign_recipient_import_mapping_profiles_ordered_header_fingerprint"), "campaign_recipient_import_mapping_profiles", ["ordered_header_fingerprint"], unique=False)
|
||||
op.create_index(op.f("ix_campaign_recipient_import_mapping_profiles_unordered_header_fingerprint"), "campaign_recipient_import_mapping_profiles", ["unordered_header_fingerprint"], unique=False)
|
||||
op.create_index("ix_recipient_import_profiles_owner", "campaign_recipient_import_mapping_profiles", ["tenant_id", "owner_user_id"], unique=False)
|
||||
op.create_index("ix_recipient_import_profiles_ordered_fp", "campaign_recipient_import_mapping_profiles", ["tenant_id", "owner_user_id", "ordered_header_fingerprint"], unique=False)
|
||||
op.create_index("ix_recipient_import_profiles_unordered_fp", "campaign_recipient_import_mapping_profiles", ["tenant_id", "owner_user_id", "unordered_header_fingerprint"], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
if "campaign_recipient_import_mapping_profiles" not in inspector.get_table_names():
|
||||
return
|
||||
op.drop_index("ix_recipient_import_profiles_unordered_fp", table_name="campaign_recipient_import_mapping_profiles")
|
||||
op.drop_index("ix_recipient_import_profiles_ordered_fp", table_name="campaign_recipient_import_mapping_profiles")
|
||||
op.drop_index("ix_recipient_import_profiles_owner", table_name="campaign_recipient_import_mapping_profiles")
|
||||
op.drop_index(op.f("ix_campaign_recipient_import_mapping_profiles_unordered_header_fingerprint"), table_name="campaign_recipient_import_mapping_profiles")
|
||||
op.drop_index(op.f("ix_campaign_recipient_import_mapping_profiles_ordered_header_fingerprint"), table_name="campaign_recipient_import_mapping_profiles")
|
||||
op.drop_index(op.f("ix_campaign_recipient_import_mapping_profiles_owner_user_id"), table_name="campaign_recipient_import_mapping_profiles")
|
||||
op.drop_index(op.f("ix_campaign_recipient_import_mapping_profiles_tenant_id"), table_name="campaign_recipient_import_mapping_profiles")
|
||||
op.drop_table("campaign_recipient_import_mapping_profiles")
|
||||
@@ -19,6 +19,9 @@ from govoplan_campaign.backend.schemas import (
|
||||
CampaignShareTargetsResponse,
|
||||
CampaignShareUpsertRequest,
|
||||
CampaignOwnerUpdateRequest,
|
||||
RecipientImportMappingProfileListResponse,
|
||||
RecipientImportMappingProfilePayload,
|
||||
RecipientImportMappingProfileResponse,
|
||||
CampaignJobsResponse,
|
||||
CampaignJobDetailResponse,
|
||||
CampaignRetryJobsRequest,
|
||||
@@ -40,7 +43,7 @@ from govoplan_campaign.backend.schemas import (
|
||||
)
|
||||
from govoplan_core.auth.dependencies import ApiPrincipal, has_scope, require_scope
|
||||
from govoplan_core.audit.logging import audit_from_principal
|
||||
from govoplan_campaign.backend.db.models import Campaign, CampaignJob, CampaignShare, CampaignVersion, Group, ImapAppendAttempt, SendAttempt, User, UserGroupMembership
|
||||
from govoplan_campaign.backend.db.models import Campaign, CampaignJob, CampaignShare, CampaignVersion, Group, ImapAppendAttempt, RecipientImportMappingProfile, SendAttempt, User, UserGroupMembership
|
||||
from govoplan_core.db.session import get_session
|
||||
from govoplan_campaign.backend.reports.campaigns import CampaignReportError, generate_campaign_report, generate_jobs_csv
|
||||
from govoplan_campaign.backend.reports.emailing import CampaignReportEmailError, send_campaign_report_email
|
||||
@@ -181,6 +184,27 @@ def _require_permission(principal: ApiPrincipal, scope: str) -> None:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"Missing scope: {scope}")
|
||||
|
||||
|
||||
def _get_recipient_import_profile_for_principal(session: Session, profile_id: str, principal: ApiPrincipal) -> RecipientImportMappingProfile:
|
||||
profile = session.get(RecipientImportMappingProfile, profile_id)
|
||||
if not profile or profile.tenant_id != principal.tenant_id or profile.owner_user_id != principal.user.id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Recipient import mapping profile not found")
|
||||
return profile
|
||||
|
||||
|
||||
def _apply_recipient_import_profile_payload(profile: RecipientImportMappingProfile, payload: RecipientImportMappingProfilePayload) -> None:
|
||||
profile.name = payload.name.strip()
|
||||
profile.column_count = payload.column_count
|
||||
profile.headers = list(payload.headers)
|
||||
profile.normalized_headers = list(payload.normalized_headers)
|
||||
profile.ordered_header_fingerprint = payload.ordered_header_fingerprint
|
||||
profile.unordered_header_fingerprint = payload.unordered_header_fingerprint
|
||||
profile.delimiter = payload.delimiter
|
||||
profile.header_rows = payload.header_rows
|
||||
profile.quoted = payload.quoted
|
||||
profile.value_separators = payload.value_separators
|
||||
profile.mappings = [mapping.model_dump(mode="json") for mapping in payload.mappings]
|
||||
|
||||
|
||||
def _recipient_sections_changed(current: dict[str, object] | None, proposed: dict[str, object] | None) -> bool:
|
||||
if proposed is None:
|
||||
return False
|
||||
@@ -346,6 +370,109 @@ def list_campaigns(
|
||||
return CampaignListResponse(campaigns=[CampaignResponse.model_validate(item) for item in campaigns])
|
||||
|
||||
|
||||
@router.get("/recipient-import/mapping-profiles", response_model=RecipientImportMappingProfileListResponse)
|
||||
def list_recipient_import_mapping_profiles(
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_scope("campaigns:recipient:import")),
|
||||
):
|
||||
profiles = (
|
||||
session.query(RecipientImportMappingProfile)
|
||||
.filter(
|
||||
RecipientImportMappingProfile.tenant_id == principal.tenant_id,
|
||||
RecipientImportMappingProfile.owner_user_id == principal.user.id,
|
||||
)
|
||||
.order_by(RecipientImportMappingProfile.updated_at.desc(), RecipientImportMappingProfile.name.asc())
|
||||
.all()
|
||||
)
|
||||
return RecipientImportMappingProfileListResponse(
|
||||
profiles=[RecipientImportMappingProfileResponse.model_validate(profile) for profile in profiles]
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/recipient-import/mapping-profiles",
|
||||
response_model=RecipientImportMappingProfileResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def create_recipient_import_mapping_profile(
|
||||
payload: RecipientImportMappingProfilePayload,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_scope("campaigns:recipient:import")),
|
||||
):
|
||||
profile = RecipientImportMappingProfile(
|
||||
tenant_id=principal.tenant_id,
|
||||
owner_user_id=principal.user.id,
|
||||
name=payload.name.strip(),
|
||||
column_count=payload.column_count,
|
||||
headers=list(payload.headers),
|
||||
normalized_headers=list(payload.normalized_headers),
|
||||
ordered_header_fingerprint=payload.ordered_header_fingerprint,
|
||||
unordered_header_fingerprint=payload.unordered_header_fingerprint,
|
||||
delimiter=payload.delimiter,
|
||||
header_rows=payload.header_rows,
|
||||
quoted=payload.quoted,
|
||||
value_separators=payload.value_separators,
|
||||
mappings=[mapping.model_dump(mode="json") for mapping in payload.mappings],
|
||||
)
|
||||
session.add(profile)
|
||||
session.flush()
|
||||
audit_from_principal(
|
||||
session,
|
||||
principal,
|
||||
action="campaign.recipient_import_mapping_profile_created",
|
||||
object_type="recipient_import_mapping_profile",
|
||||
object_id=profile.id,
|
||||
details={"name": profile.name, "ordered_header_fingerprint": profile.ordered_header_fingerprint},
|
||||
commit=True,
|
||||
)
|
||||
session.refresh(profile)
|
||||
return RecipientImportMappingProfileResponse.model_validate(profile)
|
||||
|
||||
|
||||
@router.put("/recipient-import/mapping-profiles/{profile_id}", response_model=RecipientImportMappingProfileResponse)
|
||||
def update_recipient_import_mapping_profile(
|
||||
profile_id: str,
|
||||
payload: RecipientImportMappingProfilePayload,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_scope("campaigns:recipient:import")),
|
||||
):
|
||||
profile = _get_recipient_import_profile_for_principal(session, profile_id, principal)
|
||||
_apply_recipient_import_profile_payload(profile, payload)
|
||||
session.add(profile)
|
||||
audit_from_principal(
|
||||
session,
|
||||
principal,
|
||||
action="campaign.recipient_import_mapping_profile_updated",
|
||||
object_type="recipient_import_mapping_profile",
|
||||
object_id=profile.id,
|
||||
details={"name": profile.name, "ordered_header_fingerprint": profile.ordered_header_fingerprint},
|
||||
commit=True,
|
||||
)
|
||||
session.refresh(profile)
|
||||
return RecipientImportMappingProfileResponse.model_validate(profile)
|
||||
|
||||
|
||||
@router.delete("/recipient-import/mapping-profiles/{profile_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_recipient_import_mapping_profile(
|
||||
profile_id: str,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_scope("campaigns:recipient:import")),
|
||||
):
|
||||
profile = _get_recipient_import_profile_for_principal(session, profile_id, principal)
|
||||
details = {"name": profile.name, "ordered_header_fingerprint": profile.ordered_header_fingerprint}
|
||||
session.delete(profile)
|
||||
audit_from_principal(
|
||||
session,
|
||||
principal,
|
||||
action="campaign.recipient_import_mapping_profile_deleted",
|
||||
object_type="recipient_import_mapping_profile",
|
||||
object_id=profile_id,
|
||||
details=details,
|
||||
commit=True,
|
||||
)
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@router.get("/{campaign_id}", response_model=CampaignResponse)
|
||||
def get_campaign(
|
||||
campaign_id: str,
|
||||
@@ -2008,4 +2135,3 @@ def preview_campaign_attachments(
|
||||
rules=rules,
|
||||
unused_shared_files=[_file_preview(session, asset) for asset in unused] if payload.include_unmatched else [],
|
||||
)
|
||||
|
||||
|
||||
@@ -482,6 +482,13 @@
|
||||
},
|
||||
"defaults": {
|
||||
"$ref": "#/$defs/entry"
|
||||
},
|
||||
"imports": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/import_provenance"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -505,6 +512,13 @@
|
||||
},
|
||||
"defaults": {
|
||||
"$ref": "#/$defs/entry"
|
||||
},
|
||||
"imports": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/import_provenance"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -1027,6 +1041,151 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"import_provenance": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"imported_at",
|
||||
"mode",
|
||||
"source_type",
|
||||
"header_rows",
|
||||
"rows_total",
|
||||
"valid_rows",
|
||||
"invalid_rows",
|
||||
"imported_rows"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"imported_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"append",
|
||||
"replace"
|
||||
]
|
||||
},
|
||||
"source_type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"csv",
|
||||
"xlsx",
|
||||
"text"
|
||||
]
|
||||
},
|
||||
"filename": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sheet_name": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"encoding": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"delimiter": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"header_rows": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"default": 0
|
||||
},
|
||||
"quoted": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"value_separators": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"rows_total": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"valid_rows": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"invalid_rows": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"imported_rows": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"field_names_created": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
},
|
||||
"attachment_patterns": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"default": 0
|
||||
},
|
||||
"mapping": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"column_index",
|
||||
"header",
|
||||
"kind"
|
||||
],
|
||||
"properties": {
|
||||
"column_index": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"header": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"field_name": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"new_field_name": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"attachment_base_path": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
from govoplan_core.mail.config import ImapConfig, SmtpConfig
|
||||
|
||||
@@ -186,6 +186,70 @@ class CampaignOwnerUpdateRequest(BaseModel):
|
||||
owner_group_id: str | None = None
|
||||
|
||||
|
||||
RecipientImportColumnKind = Literal[
|
||||
"ignore",
|
||||
"id",
|
||||
"active",
|
||||
"name",
|
||||
"from",
|
||||
"to",
|
||||
"cc",
|
||||
"bcc",
|
||||
"reply_to",
|
||||
"field",
|
||||
"new_field",
|
||||
"attachment_pattern",
|
||||
]
|
||||
|
||||
|
||||
class RecipientImportColumnMappingPayload(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
||||
|
||||
column_index: int = Field(ge=0, alias="columnIndex")
|
||||
kind: RecipientImportColumnKind
|
||||
field_name: str | None = Field(default=None, max_length=255, alias="fieldName")
|
||||
new_field_name: str | None = Field(default=None, max_length=255, alias="newFieldName")
|
||||
|
||||
|
||||
class RecipientImportMappingProfilePayload(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
||||
|
||||
name: str = Field(min_length=1, max_length=255)
|
||||
column_count: int = Field(ge=0, le=500, alias="columnCount")
|
||||
headers: list[str] = Field(default_factory=list, max_length=500)
|
||||
normalized_headers: list[str] = Field(default_factory=list, max_length=500, alias="normalizedHeaders")
|
||||
ordered_header_fingerprint: str = Field(min_length=1, max_length=64, alias="orderedHeaderFingerprint")
|
||||
unordered_header_fingerprint: str = Field(min_length=1, max_length=64, alias="unorderedHeaderFingerprint")
|
||||
delimiter: Literal[",", ";", "\t"]
|
||||
header_rows: int = Field(ge=0, le=10, alias="headerRows")
|
||||
quoted: bool = True
|
||||
value_separators: str = Field(default=",;|", max_length=50, alias="valueSeparators")
|
||||
mappings: list[RecipientImportColumnMappingPayload] = Field(default_factory=list, max_length=500)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_column_shape(self) -> "RecipientImportMappingProfilePayload":
|
||||
if len(self.headers) != self.column_count:
|
||||
raise ValueError("headers length must match columnCount")
|
||||
if len(self.normalized_headers) != self.column_count:
|
||||
raise ValueError("normalizedHeaders length must match columnCount")
|
||||
for mapping in self.mappings:
|
||||
if mapping.column_index >= self.column_count:
|
||||
raise ValueError("mapping columnIndex exceeds columnCount")
|
||||
return self
|
||||
|
||||
|
||||
class RecipientImportMappingProfileResponse(RecipientImportMappingProfilePayload):
|
||||
model_config = ConfigDict(from_attributes=True, populate_by_name=True)
|
||||
|
||||
id: str
|
||||
created_at: datetime = Field(alias="createdAt")
|
||||
updated_at: datetime = Field(alias="updatedAt")
|
||||
|
||||
|
||||
class RecipientImportMappingProfileListResponse(BaseModel):
|
||||
profiles: list[RecipientImportMappingProfileResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class CampaignJobsResponse(BaseModel):
|
||||
jobs: list[dict[str, Any]]
|
||||
page: int = 1
|
||||
|
||||
Reference in New Issue
Block a user