"""Campaigns: 12-step wizard (draft CRUD), preview with real rendered messages, start/pause/resume/cancel, SSE live progress stream, per-recipient monitor, CSV export.""" import asyncio import csv import io import json import re from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query, Request from fastapi.responses import StreamingResponse from pydantic import BaseModel, Field from sqlalchemy import delete as sa_delete, func, select from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_current_user, get_db, require_csrf from app.core.config import settings as cfg from app.core.security import log_audit from app.models import (Attachment, Campaign, CampaignRecipient, CompanyPhoneNumber, EmailTemplate, GmailAccount, Recipient, RecipientList, SendJob, SubjectLine, SuppressionEntry, User) from app.services.campaign_engine import get_setting, materialize_campaign from app.services.campaign_service import (cancel_campaign, pause_campaign, resume_campaign, start_campaign) from app.services.message_builder import sanitize_preview from app.services.variable_engine import GeneratedValues, VariableEngine router = APIRouter(prefix="/campaigns", tags=["campaigns"]) MSG_TYPES = {"html", "plain", "both"} class CampaignIn(BaseModel): name: str = Field(min_length=1, max_length=250) recipient_list_id: Optional[int] = None subject_set_ids: list = [] template_ids: list = [] message_type: str = "html" # html|plain|both attach_mode: str = "none" # none|pdf|file|image attach_pdf: bool = False 19:from app.models import (Attachment, Campaign, CampaignRecipient, CompanyPhoneNumber,