Release v0.1.2
This commit is contained in:
@@ -2,6 +2,8 @@ from __future__ import annotations
|
||||
|
||||
import fnmatch
|
||||
import re
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from enum import StrEnum
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
@@ -69,6 +71,10 @@ class ResolvedAttachment(BaseModel):
|
||||
zip_mode: ZipRuleMode = ZipRuleMode.INHERIT
|
||||
zip_archive_id: str | None = None
|
||||
zip_filename: str | None = None
|
||||
message_filename_template: str | None = None
|
||||
zip_entry_name_template: str | None = None
|
||||
message_filenames: list[str] = Field(default_factory=list)
|
||||
zip_entry_names: list[str] = Field(default_factory=list)
|
||||
status: AttachmentMatchStatus
|
||||
behavior: Behavior | None = None
|
||||
matches: list[str] = Field(default_factory=list)
|
||||
@@ -99,6 +105,7 @@ class AttachmentResolutionReport(BaseModel):
|
||||
attachments_base_path: str
|
||||
entries_count: int
|
||||
entries: list[EntryAttachmentResolution] = Field(default_factory=list)
|
||||
profile: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def ready_count(self) -> int:
|
||||
@@ -304,7 +311,55 @@ def _resolve_attachment_directory(
|
||||
return (legacy_root / rendered_base_dir).resolve(), None
|
||||
|
||||
|
||||
def _match_files(directory: Path, file_filter: str, include_subdirs: bool) -> list[Path]:
|
||||
@dataclass(slots=True)
|
||||
class AttachmentMatchIndex:
|
||||
_files_by_directory: dict[tuple[str, bool], list[Path]] = field(default_factory=dict)
|
||||
filesystem_scans: int = 0
|
||||
indexed_files: int = 0
|
||||
fallback_globs: int = 0
|
||||
|
||||
def _directory_key(self, directory: Path, *, recursive: bool) -> tuple[str, bool]:
|
||||
return str(directory.resolve()), recursive
|
||||
|
||||
def _indexed_files(self, directory: Path, *, recursive: bool) -> list[Path]:
|
||||
key = self._directory_key(directory, recursive=recursive)
|
||||
if key not in self._files_by_directory:
|
||||
iterator = directory.rglob("*") if recursive else directory.iterdir()
|
||||
files = sorted(path for path in iterator if path.is_file())
|
||||
self._files_by_directory[key] = files
|
||||
self.filesystem_scans += 1
|
||||
self.indexed_files += len(files)
|
||||
return self._files_by_directory[key]
|
||||
|
||||
def iter_files(self, directory: Path, *, recursive: bool = True) -> list[Path]:
|
||||
if not directory.exists() or not directory.is_dir():
|
||||
return []
|
||||
return list(self._indexed_files(directory, recursive=recursive))
|
||||
|
||||
def match_files(self, directory: Path, file_filter: str, include_subdirs: bool) -> list[Path]:
|
||||
if not directory.exists() or not directory.is_dir():
|
||||
return []
|
||||
if not include_subdirs and ("/" in file_filter or "\\" in file_filter):
|
||||
# Preserve pathlib.glob semantics for legacy filters that include a
|
||||
# relative path segment instead of only a filename pattern.
|
||||
self.fallback_globs += 1
|
||||
return sorted(path for path in directory.glob(file_filter) if path.is_file())
|
||||
return sorted(path for path in self._indexed_files(directory, recursive=include_subdirs) if fnmatch.fnmatch(path.name, file_filter))
|
||||
|
||||
def stats(self, *, duration_ms: float, rules_resolved: int) -> dict[str, Any]:
|
||||
return {
|
||||
"duration_ms": round(duration_ms, 2),
|
||||
"rules_resolved": rules_resolved,
|
||||
"indexed_directories": len(self._files_by_directory),
|
||||
"filesystem_scans": self.filesystem_scans,
|
||||
"indexed_files": self.indexed_files,
|
||||
"fallback_globs": self.fallback_globs,
|
||||
}
|
||||
|
||||
|
||||
def _match_files(directory: Path, file_filter: str, include_subdirs: bool, match_index: AttachmentMatchIndex | None = None) -> list[Path]:
|
||||
if match_index is not None:
|
||||
return match_index.match_files(directory, file_filter, include_subdirs)
|
||||
if not directory.exists() or not directory.is_dir():
|
||||
return []
|
||||
if include_subdirs:
|
||||
@@ -343,6 +398,7 @@ def _resolve_one_config(
|
||||
scope: AttachmentScope,
|
||||
index: int,
|
||||
config: AttachmentConfig,
|
||||
match_index: AttachmentMatchIndex | None = None,
|
||||
) -> ResolvedAttachment:
|
||||
rendered_base_dir = _rendered_base_dir(config, values)
|
||||
rendered_file_filter = _render_template(config.file_filter, values)
|
||||
@@ -352,7 +408,7 @@ def _resolve_one_config(
|
||||
attachment_config=config,
|
||||
rendered_base_dir=rendered_base_dir,
|
||||
)
|
||||
matches = _match_files(directory, rendered_file_filter, config.include_subdirs)
|
||||
matches = _match_files(directory, rendered_file_filter, config.include_subdirs, match_index)
|
||||
allow_multiple = _rule_allows_multiple(config, rendered_file_filter)
|
||||
|
||||
issues: list[AttachmentIssue] = []
|
||||
@@ -389,6 +445,8 @@ def _resolve_one_config(
|
||||
zip_mode=config.zip.mode,
|
||||
zip_archive_id=archive.id if archive else None,
|
||||
zip_filename=_render_zip_filename(archive, values),
|
||||
message_filename_template=config.message_filename_template,
|
||||
zip_entry_name_template=config.zip_entry_name_template,
|
||||
status=status,
|
||||
behavior=behavior,
|
||||
matches=[str(path) for path in matches],
|
||||
@@ -417,6 +475,7 @@ def resolve_entry_attachments(
|
||||
campaign_file: str | Path,
|
||||
entry: EntryConfig,
|
||||
entry_index: int,
|
||||
match_index: AttachmentMatchIndex | None = None,
|
||||
) -> EntryAttachmentResolution:
|
||||
values = build_template_values(config, entry)
|
||||
resolved: list[ResolvedAttachment] = []
|
||||
@@ -431,6 +490,7 @@ def resolve_entry_attachments(
|
||||
scope=scope,
|
||||
index=index,
|
||||
config=attachment_config,
|
||||
match_index=match_index,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -448,10 +508,13 @@ def resolve_entry_attachments(
|
||||
def resolve_campaign_attachments(config: CampaignConfig, *, campaign_file: str | Path) -> AttachmentResolutionReport:
|
||||
entries = load_campaign_entries(config, campaign_file=campaign_file)
|
||||
base_path = _resolve_path(campaign_file, config.attachments.base_paths[0].path if config.attachments.base_paths else config.attachments.base_path)
|
||||
started = time.perf_counter()
|
||||
match_index = AttachmentMatchIndex()
|
||||
resolved_entries = [
|
||||
resolve_entry_attachments(config=config, campaign_file=campaign_file, entry=entry, entry_index=index)
|
||||
resolve_entry_attachments(config=config, campaign_file=campaign_file, entry=entry, entry_index=index, match_index=match_index)
|
||||
for index, entry in enumerate(entries, start=1)
|
||||
]
|
||||
rules_resolved = sum(len(entry.attachments) for entry in resolved_entries)
|
||||
return AttachmentResolutionReport(
|
||||
campaign_id=config.campaign.id,
|
||||
campaign_name=config.campaign.name,
|
||||
@@ -459,4 +522,5 @@ def resolve_campaign_attachments(config: CampaignConfig, *, campaign_file: str |
|
||||
attachments_base_path=str(base_path),
|
||||
entries_count=len(entries),
|
||||
entries=resolved_entries,
|
||||
profile=match_index.stats(duration_ms=(time.perf_counter() - started) * 1000, rules_resolved=rules_resolved),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user