intermittent commit
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
@@ -8,7 +9,7 @@ from typing import Iterable
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from .field_values import ignored_entry_field_overrides
|
||||
from .models import AttachmentConfig, CampaignConfig, EntryConfig, FieldType, SourceType, ZipPasswordMode, ZipPasswordScope, ZipRuleMode
|
||||
from .models import AttachmentConfig, CampaignConfig, EntryConfig, FieldType, SourceType, ZipArchiveConfig, ZipPasswordMode, ZipPasswordScope, ZipRuleMode
|
||||
|
||||
|
||||
class Severity(StrEnum):
|
||||
@@ -51,6 +52,13 @@ class SemanticReport(BaseModel):
|
||||
return self.error_count == 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class _EntriesValidation:
|
||||
mode: str
|
||||
count: int | None
|
||||
issues: list[SemanticIssue]
|
||||
|
||||
|
||||
def _issue(severity: Severity, code: str, message: str, path: str | None = None) -> SemanticIssue:
|
||||
return SemanticIssue(severity=severity, code=code, message=message, path=path)
|
||||
|
||||
@@ -200,57 +208,95 @@ def _attachment_path_issues(config: CampaignConfig) -> list[SemanticIssue]:
|
||||
|
||||
def _zip_configuration_issues(config: CampaignConfig) -> list[SemanticIssue]:
|
||||
collection = config.attachments.zip
|
||||
issues: list[SemanticIssue] = []
|
||||
if not collection.enabled:
|
||||
return issues
|
||||
return []
|
||||
if not collection.archives:
|
||||
return [_issue(Severity.ERROR, "zip_archive_missing", "Attachment zipping is enabled, but no ZIP archive is configured", "/attachments/zip/archives")]
|
||||
|
||||
issues, archive_ids, standard_count = _zip_archive_catalog_issues(config)
|
||||
if standard_count != 1:
|
||||
issues.append(_issue(Severity.ERROR, "zip_standard_archive_invalid", "Exactly one ZIP archive must be selected as the campaign standard", "/attachments/zip/archives"))
|
||||
issues.extend(_zip_rule_selection_issues(config, archive_ids))
|
||||
return issues
|
||||
|
||||
|
||||
def _zip_archive_catalog_issues(config: CampaignConfig) -> tuple[list[SemanticIssue], set[str], int]:
|
||||
issues: list[SemanticIssue] = []
|
||||
archive_ids: set[str] = set()
|
||||
archive_names: set[str] = set()
|
||||
standard_count = 0
|
||||
field_definitions = {field.name: field for field in config.fields}
|
||||
for index, archive in enumerate(collection.archives):
|
||||
for index, archive in enumerate(config.attachments.zip.archives):
|
||||
path = f"/attachments/zip/archives/{index}"
|
||||
if not archive.id.strip():
|
||||
issues.append(_issue(Severity.ERROR, "zip_archive_id_missing", "ZIP archive has no identifier", f"{path}/id"))
|
||||
elif archive.id in archive_ids:
|
||||
issues.append(_issue(Severity.ERROR, "zip_archive_id_duplicate", f"ZIP archive id {archive.id!r} is used more than once", f"{path}/id"))
|
||||
archive_ids.add(archive.id)
|
||||
normalized_archive_name = _normalized_zip_archive_name(archive.name)
|
||||
if not normalized_archive_name:
|
||||
issues.append(_issue(Severity.ERROR, "zip_archive_name_missing", "ZIP archive has no filename", f"{path}/name"))
|
||||
elif normalized_archive_name in archive_names:
|
||||
issues.append(_issue(Severity.ERROR, "zip_archive_name_duplicate", f"ZIP archive filename {archive.name!r} is used more than once; archive filenames must be unique", f"{path}/name"))
|
||||
archive_names.add(normalized_archive_name)
|
||||
issues.extend(_zip_archive_identity_issues(archive, path, archive_ids, archive_names))
|
||||
if archive.standard:
|
||||
standard_count += 1
|
||||
issues.extend(_zip_password_issues(config, archive, path, field_definitions))
|
||||
return issues, archive_ids, standard_count
|
||||
|
||||
if not archive.password_enabled:
|
||||
continue
|
||||
if archive.password_mode == ZipPasswordMode.DIRECT:
|
||||
if not (archive.password or ""):
|
||||
issues.append(_issue(Severity.ERROR, "zip_password_missing", "A legacy fixed ZIP password is enabled, but no password is configured", f"{path}/password"))
|
||||
continue
|
||||
if archive.password_mode == ZipPasswordMode.TEMPLATE:
|
||||
if not (archive.password_template or ""):
|
||||
issues.append(_issue(Severity.ERROR, "zip_password_template_missing", "A legacy ZIP password template is enabled, but no template is configured", f"{path}/password_template"))
|
||||
continue
|
||||
|
||||
field_name = (archive.password_field or "").strip()
|
||||
field = field_definitions.get(field_name)
|
||||
if not field_name:
|
||||
issues.append(_issue(Severity.ERROR, "zip_password_field_missing", f"ZIP archive {archive.name!r} has password protection enabled, but no field is selected", f"{path}/password_field"))
|
||||
elif field is None:
|
||||
issues.append(_issue(Severity.ERROR, "zip_password_field_unknown", f"ZIP password field {field_name!r} is not declared in campaign fields", f"{path}/password_field"))
|
||||
elif field.type != FieldType.PASSWORD:
|
||||
issues.append(_issue(Severity.WARNING, "zip_password_field_not_password_type", f"ZIP password field {field_name!r} is not configured with field type 'password'", f"{path}/password_field"))
|
||||
elif archive.password_scope == ZipPasswordScope.GLOBAL and config.global_values.get(field_name) in (None, ""):
|
||||
issues.append(_issue(Severity.ERROR, "zip_global_password_value_missing", f"Global ZIP password field {field_name!r} has no campaign-wide value", f"/global_values/{field_name}"))
|
||||
def _zip_archive_identity_issues(
|
||||
archive: ZipArchiveConfig,
|
||||
path: str,
|
||||
archive_ids: set[str],
|
||||
archive_names: set[str],
|
||||
) -> list[SemanticIssue]:
|
||||
issues: list[SemanticIssue] = []
|
||||
if not archive.id.strip():
|
||||
issues.append(_issue(Severity.ERROR, "zip_archive_id_missing", "ZIP archive has no identifier", f"{path}/id"))
|
||||
elif archive.id in archive_ids:
|
||||
issues.append(_issue(Severity.ERROR, "zip_archive_id_duplicate", f"ZIP archive id {archive.id!r} is used more than once", f"{path}/id"))
|
||||
archive_ids.add(archive.id)
|
||||
|
||||
if standard_count != 1:
|
||||
issues.append(_issue(Severity.ERROR, "zip_standard_archive_invalid", "Exactly one ZIP archive must be selected as the campaign standard", "/attachments/zip/archives"))
|
||||
normalized_archive_name = _normalized_zip_archive_name(archive.name)
|
||||
if not normalized_archive_name:
|
||||
issues.append(_issue(Severity.ERROR, "zip_archive_name_missing", "ZIP archive has no filename", f"{path}/name"))
|
||||
elif normalized_archive_name in archive_names:
|
||||
issues.append(_issue(Severity.ERROR, "zip_archive_name_duplicate", f"ZIP archive filename {archive.name!r} is used more than once; archive filenames must be unique", f"{path}/name"))
|
||||
archive_names.add(normalized_archive_name)
|
||||
return issues
|
||||
|
||||
|
||||
def _zip_password_issues(
|
||||
config: CampaignConfig,
|
||||
archive: ZipArchiveConfig,
|
||||
path: str,
|
||||
field_definitions: dict[str, object],
|
||||
) -> list[SemanticIssue]:
|
||||
if not archive.password_enabled:
|
||||
return []
|
||||
if archive.password_mode == ZipPasswordMode.DIRECT:
|
||||
if not (archive.password or ""):
|
||||
return [_issue(Severity.ERROR, "zip_password_missing", "A legacy fixed ZIP password is enabled, but no password is configured", f"{path}/password")]
|
||||
return []
|
||||
if archive.password_mode == ZipPasswordMode.TEMPLATE:
|
||||
if not (archive.password_template or ""):
|
||||
return [_issue(Severity.ERROR, "zip_password_template_missing", "A legacy ZIP password template is enabled, but no template is configured", f"{path}/password_template")]
|
||||
return []
|
||||
return _zip_password_field_issues(config, archive, path, field_definitions)
|
||||
|
||||
|
||||
def _zip_password_field_issues(
|
||||
config: CampaignConfig,
|
||||
archive: ZipArchiveConfig,
|
||||
path: str,
|
||||
field_definitions: dict[str, object],
|
||||
) -> list[SemanticIssue]:
|
||||
field_name = (archive.password_field or "").strip()
|
||||
field = field_definitions.get(field_name)
|
||||
if not field_name:
|
||||
return [_issue(Severity.ERROR, "zip_password_field_missing", f"ZIP archive {archive.name!r} has password protection enabled, but no field is selected", f"{path}/password_field")]
|
||||
if field is None:
|
||||
return [_issue(Severity.ERROR, "zip_password_field_unknown", f"ZIP password field {field_name!r} is not declared in campaign fields", f"{path}/password_field")]
|
||||
if getattr(field, "type", None) != FieldType.PASSWORD:
|
||||
return [_issue(Severity.WARNING, "zip_password_field_not_password_type", f"ZIP password field {field_name!r} is not configured with field type 'password'", f"{path}/password_field")]
|
||||
if archive.password_scope == ZipPasswordScope.GLOBAL and config.global_values.get(field_name) in (None, ""):
|
||||
return [_issue(Severity.ERROR, "zip_global_password_value_missing", f"Global ZIP password field {field_name!r} has no campaign-wide value", f"/global_values/{field_name}")]
|
||||
return []
|
||||
|
||||
|
||||
def _zip_rule_selection_issues(config: CampaignConfig, archive_ids: set[str]) -> list[SemanticIssue]:
|
||||
issues: list[SemanticIssue] = []
|
||||
for path, rule, _is_individual in _iter_attachment_rules(config):
|
||||
selection = (rule.zip.archive_id or ZipRuleMode.INHERIT.value).strip()
|
||||
if selection not in {"", ZipRuleMode.INHERIT.value, ZipRuleMode.INCLUDE.value, ZipRuleMode.EXCLUDE.value} and selection not in archive_ids:
|
||||
@@ -276,6 +322,236 @@ def _ignored_override_issues(config: CampaignConfig, entry: EntryConfig, path_pr
|
||||
]
|
||||
|
||||
|
||||
def _global_value_issues(config: CampaignConfig, declared_names: set[str]) -> list[SemanticIssue]:
|
||||
return [
|
||||
_issue(
|
||||
Severity.WARNING,
|
||||
"unknown_global_value",
|
||||
f"global_values contains {key!r}, but it is not declared in fields",
|
||||
f"/global_values/{key}",
|
||||
)
|
||||
for key in config.global_values
|
||||
if declared_names and key not in declared_names
|
||||
]
|
||||
|
||||
|
||||
def _delivery_issues(config: CampaignConfig) -> list[SemanticIssue]:
|
||||
issues: list[SemanticIssue] = []
|
||||
runtime_imap = config.server.runtime_imap_config()
|
||||
if config.delivery.imap_append_sent.enabled:
|
||||
issues.extend(_imap_delivery_issues(runtime_imap))
|
||||
runtime_smtp = config.server.runtime_smtp_config()
|
||||
if config.campaign.mode == "send" and not runtime_smtp:
|
||||
issues.append(
|
||||
_issue(
|
||||
Severity.ERROR,
|
||||
"missing_smtp_config",
|
||||
"campaign mode is 'send', but no server.smtp configuration is present",
|
||||
"/server/smtp",
|
||||
)
|
||||
)
|
||||
if runtime_smtp:
|
||||
missing = [name for name in ["host", "port"] if getattr(runtime_smtp, name) in (None, "")]
|
||||
if missing:
|
||||
issues.append(
|
||||
_issue(
|
||||
Severity.WARNING,
|
||||
"incomplete_smtp_config",
|
||||
"SMTP settings are present, but these settings are missing: " + ", ".join(missing),
|
||||
"/server/smtp",
|
||||
)
|
||||
)
|
||||
return issues
|
||||
|
||||
|
||||
def _imap_delivery_issues(runtime_imap: object | None) -> list[SemanticIssue]:
|
||||
if runtime_imap is None:
|
||||
return [
|
||||
_issue(
|
||||
Severity.WARNING,
|
||||
"delivery_imap_enabled_without_server_imap",
|
||||
"delivery.imap_append_sent is enabled, but no server.imap configuration is present",
|
||||
"/delivery/imap_append_sent/enabled",
|
||||
)
|
||||
]
|
||||
missing = [name for name in ["host", "port", "username", "password"] if getattr(runtime_imap, name) in (None, "")]
|
||||
if not missing:
|
||||
return []
|
||||
return [
|
||||
_issue(
|
||||
Severity.ERROR,
|
||||
"incomplete_imap_config",
|
||||
"IMAP append is enabled, but these IMAP settings are missing: " + ", ".join(missing),
|
||||
"/server/imap",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def _entries_validation(
|
||||
config: CampaignConfig,
|
||||
*,
|
||||
campaign_path: Path,
|
||||
check_files: bool,
|
||||
field_names: set[str],
|
||||
field_definitions: dict[str, object],
|
||||
) -> _EntriesValidation:
|
||||
if config.entries.is_inline:
|
||||
inline_entries = config.entries.inline or []
|
||||
issues = _inline_entries_issues(config, inline_entries)
|
||||
return _EntriesValidation(mode="inline", count=len(inline_entries), issues=issues)
|
||||
issues = _external_entries_issues(
|
||||
config,
|
||||
campaign_path=campaign_path,
|
||||
check_files=check_files,
|
||||
field_names=field_names,
|
||||
field_definitions=field_definitions,
|
||||
)
|
||||
source_type = config.entries.source.type.value if config.entries.source else "unknown"
|
||||
return _EntriesValidation(mode=f"external:{source_type}", count=None, issues=issues)
|
||||
|
||||
|
||||
def _inline_entries_issues(config: CampaignConfig, inline_entries: list[EntryConfig]) -> list[SemanticIssue]:
|
||||
issues: list[SemanticIssue] = []
|
||||
if not inline_entries:
|
||||
issues.append(_issue(Severity.WARNING, "no_inline_entries", "entries.inline is empty", "/entries/inline"))
|
||||
for index, entry in enumerate(inline_entries):
|
||||
if entry.active:
|
||||
issues.extend(_ignored_override_issues(config, entry, f"/entries/inline/{index}"))
|
||||
return issues
|
||||
|
||||
|
||||
def _external_entries_issues(
|
||||
config: CampaignConfig,
|
||||
*,
|
||||
campaign_path: Path,
|
||||
check_files: bool,
|
||||
field_names: set[str],
|
||||
field_definitions: dict[str, object],
|
||||
) -> list[SemanticIssue]:
|
||||
issues: list[SemanticIssue] = []
|
||||
mapping = config.entries.mapping or {}
|
||||
if not mapping:
|
||||
issues.append(_issue(Severity.ERROR, "empty_mapping", "external entries require a non-empty mapping", "/entries/mapping"))
|
||||
issues.extend(_mapping_issues(mapping, field_names, field_definitions))
|
||||
if config.entries.defaults:
|
||||
issues.extend(_ignored_override_issues(config, config.entries.defaults, "/entries/defaults"))
|
||||
if check_files and config.entries.source:
|
||||
issues.extend(_entries_source_file_issues(config, campaign_path, mapping))
|
||||
return issues
|
||||
|
||||
|
||||
def _mapping_issues(
|
||||
mapping: dict[str, str],
|
||||
field_names: set[str],
|
||||
field_definitions: dict[str, object],
|
||||
) -> list[SemanticIssue]:
|
||||
issues: list[SemanticIssue] = []
|
||||
for target in mapping:
|
||||
if not _mapping_target_known(target, field_names):
|
||||
issues.append(
|
||||
_issue(
|
||||
Severity.WARNING,
|
||||
"unknown_mapping_target",
|
||||
f"mapping target {target!r} is not recognized by the current campaign model",
|
||||
f"/entries/mapping/{target}",
|
||||
)
|
||||
)
|
||||
field_name = _mapping_target_field_name(target)
|
||||
field = field_definitions.get(field_name or "")
|
||||
if field_name and field is not None and not getattr(field, "can_override", True):
|
||||
issues.append(
|
||||
_issue(
|
||||
Severity.WARNING,
|
||||
"mapping_target_not_overridable",
|
||||
f"mapping target {target!r} points to a field that does not allow recipient overrides; mapped values will be ignored",
|
||||
f"/entries/mapping/{target}",
|
||||
)
|
||||
)
|
||||
return issues
|
||||
|
||||
|
||||
def _entries_source_file_issues(config: CampaignConfig, campaign_path: Path, mapping: dict[str, str]) -> list[SemanticIssue]:
|
||||
source = config.entries.source
|
||||
if source is None:
|
||||
return []
|
||||
source_path = _resolve(campaign_path, source.path)
|
||||
if not source_path.exists():
|
||||
return [
|
||||
_issue(
|
||||
Severity.ERROR,
|
||||
"entries_source_not_found",
|
||||
f"entries source file does not exist: {source_path}",
|
||||
"/entries/source/path",
|
||||
)
|
||||
]
|
||||
if source.type != SourceType.CSV or not source.has_header:
|
||||
return []
|
||||
try:
|
||||
header = _csv_header(source_path, source.delimiter, source.encoding)
|
||||
except OSError as exc:
|
||||
return [_issue(Severity.ERROR, "entries_source_read_error", str(exc), "/entries/source/path")]
|
||||
header_set = set(header or [])
|
||||
missing_columns = sorted({source_name for source_name in mapping.values() if source_name not in header_set})
|
||||
if not missing_columns:
|
||||
return []
|
||||
return [
|
||||
_issue(
|
||||
Severity.ERROR,
|
||||
"mapping_columns_missing",
|
||||
"CSV mapping refers to missing columns: " + ", ".join(missing_columns),
|
||||
"/entries/mapping",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def _file_check_issues(config: CampaignConfig, campaign_path: Path) -> list[SemanticIssue]:
|
||||
issues: list[SemanticIssue] = []
|
||||
issues.extend(_attachment_file_check_issues(config, campaign_path))
|
||||
issues.extend(_template_source_file_issues(config, campaign_path))
|
||||
return issues
|
||||
|
||||
|
||||
def _attachment_file_check_issues(config: CampaignConfig, campaign_path: Path) -> list[SemanticIssue]:
|
||||
if config.attachments.base_paths:
|
||||
return [
|
||||
_issue(
|
||||
Severity.WARNING,
|
||||
"attachments_base_path_not_found",
|
||||
f"attachment base path {base_path_config.name!r} does not exist: {_resolve(campaign_path, base_path_config.path)}",
|
||||
f"/attachments/base_paths/{index}/path",
|
||||
)
|
||||
for index, base_path_config in enumerate(config.attachments.base_paths)
|
||||
if not _resolve(campaign_path, base_path_config.path).exists()
|
||||
]
|
||||
attachments_base_path = _resolve(campaign_path, config.attachments.base_path)
|
||||
if attachments_base_path.exists():
|
||||
return []
|
||||
return [
|
||||
_issue(
|
||||
Severity.WARNING,
|
||||
"attachments_base_path_not_found",
|
||||
f"attachments.base_path does not exist: {attachments_base_path}",
|
||||
"/attachments/base_path",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def _template_source_file_issues(config: CampaignConfig, campaign_path: Path) -> list[SemanticIssue]:
|
||||
issues: list[SemanticIssue] = []
|
||||
for schema_path, raw_path in _iter_template_source_paths(config):
|
||||
path = _resolve(campaign_path, raw_path)
|
||||
if not path.exists():
|
||||
issues.append(
|
||||
_issue(
|
||||
Severity.ERROR,
|
||||
"template_source_not_found",
|
||||
f"template source file does not exist: {path}",
|
||||
schema_path,
|
||||
)
|
||||
)
|
||||
return issues
|
||||
|
||||
|
||||
def validate_campaign_config(
|
||||
config: CampaignConfig,
|
||||
*,
|
||||
@@ -289,149 +565,29 @@ def validate_campaign_config(
|
||||
field_definitions = {field.name: field for field in config.fields}
|
||||
declared_names = set(field_definitions)
|
||||
|
||||
for key in config.global_values:
|
||||
if declared_names and key not in declared_names:
|
||||
issues.append(_issue(
|
||||
Severity.WARNING,
|
||||
"unknown_global_value",
|
||||
f"global_values contains {key!r}, but it is not declared in fields",
|
||||
f"/global_values/{key}",
|
||||
))
|
||||
|
||||
issues.extend(_global_value_issues(config, declared_names))
|
||||
issues.extend(_attachment_path_issues(config))
|
||||
issues.extend(_zip_configuration_issues(config))
|
||||
issues.extend(_delivery_issues(config))
|
||||
|
||||
runtime_imap = config.server.runtime_imap_config()
|
||||
if config.delivery.imap_append_sent.enabled:
|
||||
if runtime_imap is None:
|
||||
issues.append(_issue(
|
||||
Severity.WARNING,
|
||||
"delivery_imap_enabled_without_server_imap",
|
||||
"delivery.imap_append_sent is enabled, but no server.imap configuration is present",
|
||||
"/delivery/imap_append_sent/enabled",
|
||||
))
|
||||
else:
|
||||
missing = [name for name in ["host", "port", "username", "password"] if getattr(runtime_imap, name) in (None, "")]
|
||||
if missing:
|
||||
issues.append(_issue(
|
||||
Severity.ERROR,
|
||||
"incomplete_imap_config",
|
||||
"IMAP append is enabled, but these IMAP settings are missing: " + ", ".join(missing),
|
||||
"/server/imap",
|
||||
))
|
||||
|
||||
runtime_smtp = config.server.runtime_smtp_config()
|
||||
if config.campaign.mode == "send" and not runtime_smtp:
|
||||
issues.append(_issue(
|
||||
Severity.ERROR,
|
||||
"missing_smtp_config",
|
||||
"campaign mode is 'send', but no server.smtp configuration is present",
|
||||
"/server/smtp",
|
||||
))
|
||||
|
||||
if runtime_smtp:
|
||||
missing = [name for name in ["host", "port"] if getattr(runtime_smtp, name) in (None, "")]
|
||||
if missing:
|
||||
issues.append(_issue(
|
||||
Severity.WARNING,
|
||||
"incomplete_smtp_config",
|
||||
"SMTP settings are present, but these settings are missing: " + ", ".join(missing),
|
||||
"/server/smtp",
|
||||
))
|
||||
|
||||
if config.entries.is_inline:
|
||||
inline_entries = config.entries.inline or []
|
||||
entries_count = len(inline_entries)
|
||||
entries_mode = "inline"
|
||||
if entries_count == 0:
|
||||
issues.append(_issue(Severity.WARNING, "no_inline_entries", "entries.inline is empty", "/entries/inline"))
|
||||
for index, entry in enumerate(inline_entries):
|
||||
if entry.active:
|
||||
issues.extend(_ignored_override_issues(config, entry, f"/entries/inline/{index}"))
|
||||
else:
|
||||
entries_count = None
|
||||
entries_mode = f"external:{config.entries.source.type.value if config.entries.source else 'unknown'}"
|
||||
mapping = config.entries.mapping or {}
|
||||
if not mapping:
|
||||
issues.append(_issue(Severity.ERROR, "empty_mapping", "external entries require a non-empty mapping", "/entries/mapping"))
|
||||
for target in mapping:
|
||||
if not _mapping_target_known(target, field_names):
|
||||
issues.append(_issue(
|
||||
Severity.WARNING,
|
||||
"unknown_mapping_target",
|
||||
f"mapping target {target!r} is not recognized by the current campaign model",
|
||||
f"/entries/mapping/{target}",
|
||||
))
|
||||
field_name = _mapping_target_field_name(target)
|
||||
if field_name and field_name in field_definitions and not field_definitions[field_name].can_override:
|
||||
issues.append(_issue(
|
||||
Severity.WARNING,
|
||||
"mapping_target_not_overridable",
|
||||
f"mapping target {target!r} points to a field that does not allow recipient overrides; mapped values will be ignored",
|
||||
f"/entries/mapping/{target}",
|
||||
))
|
||||
if config.entries.defaults:
|
||||
issues.extend(_ignored_override_issues(config, config.entries.defaults, "/entries/defaults"))
|
||||
if check_files and config.entries.source:
|
||||
source_path = _resolve(campaign_path, config.entries.source.path)
|
||||
if not source_path.exists():
|
||||
issues.append(_issue(
|
||||
Severity.ERROR,
|
||||
"entries_source_not_found",
|
||||
f"entries source file does not exist: {source_path}",
|
||||
"/entries/source/path",
|
||||
))
|
||||
elif config.entries.source.type == SourceType.CSV and config.entries.source.has_header:
|
||||
try:
|
||||
header = _csv_header(source_path, config.entries.source.delimiter, config.entries.source.encoding)
|
||||
header_set = set(header or [])
|
||||
missing_columns = sorted({source_name for source_name in mapping.values() if source_name not in header_set})
|
||||
if missing_columns:
|
||||
issues.append(_issue(
|
||||
Severity.ERROR,
|
||||
"mapping_columns_missing",
|
||||
"CSV mapping refers to missing columns: " + ", ".join(missing_columns),
|
||||
"/entries/mapping",
|
||||
))
|
||||
except OSError as exc:
|
||||
issues.append(_issue(Severity.ERROR, "entries_source_read_error", str(exc), "/entries/source/path"))
|
||||
entries = _entries_validation(
|
||||
config,
|
||||
campaign_path=campaign_path,
|
||||
check_files=check_files,
|
||||
field_names=field_names,
|
||||
field_definitions=field_definitions,
|
||||
)
|
||||
issues.extend(entries.issues)
|
||||
|
||||
if check_files:
|
||||
if config.attachments.base_paths:
|
||||
for index, base_path_config in enumerate(config.attachments.base_paths):
|
||||
attachments_base_path = _resolve(campaign_path, base_path_config.path)
|
||||
if not attachments_base_path.exists():
|
||||
issues.append(_issue(
|
||||
Severity.WARNING,
|
||||
"attachments_base_path_not_found",
|
||||
f"attachment base path {base_path_config.name!r} does not exist: {attachments_base_path}",
|
||||
f"/attachments/base_paths/{index}/path",
|
||||
))
|
||||
else:
|
||||
attachments_base_path = _resolve(campaign_path, config.attachments.base_path)
|
||||
if not attachments_base_path.exists():
|
||||
issues.append(_issue(
|
||||
Severity.WARNING,
|
||||
"attachments_base_path_not_found",
|
||||
f"attachments.base_path does not exist: {attachments_base_path}",
|
||||
"/attachments/base_path",
|
||||
))
|
||||
for schema_path, raw_path in _iter_template_source_paths(config):
|
||||
path = _resolve(campaign_path, raw_path)
|
||||
if not path.exists():
|
||||
issues.append(_issue(
|
||||
Severity.ERROR,
|
||||
"template_source_not_found",
|
||||
f"template source file does not exist: {path}",
|
||||
schema_path,
|
||||
))
|
||||
issues.extend(_file_check_issues(config, campaign_path))
|
||||
|
||||
report = SemanticReport(
|
||||
campaign_id=config.campaign.id,
|
||||
campaign_name=config.campaign.name,
|
||||
issues=issues,
|
||||
entries_mode=entries_mode,
|
||||
entries_count=entries_count,
|
||||
entries_mode=entries.mode,
|
||||
entries_count=entries.count,
|
||||
attachments_base_path=_attachment_base_path_report_value(config),
|
||||
rate_limit=f"{config.delivery.rate_limit.messages_per_minute}/min, concurrency {config.delivery.rate_limit.concurrency}",
|
||||
imap_append_enabled=config.delivery.imap_append_sent.enabled,
|
||||
|
||||
Reference in New Issue
Block a user