Refactor campaign validation to return optional broken directory path on failure

This commit is contained in:
Joakim Hellsén 2026-01-05 20:29:01 +01:00
commit bd73f8d250
No known key found for this signature in database

View file

@ -336,7 +336,7 @@ class Command(BaseCommand):
campaigns_found: list[dict[str, Any]], campaigns_found: list[dict[str, Any]],
file_path: Path, file_path: Path,
options: dict[str, Any], options: dict[str, Any],
) -> list[GraphQLResponse]: ) -> tuple[list[GraphQLResponse], Path | None]:
"""Validate campaign data using Pydantic schema. """Validate campaign data using Pydantic schema.
Args: Args:
@ -345,13 +345,15 @@ class Command(BaseCommand):
options: Command options. options: Command options.
Returns: Returns:
List of validated Pydantic GraphQLResponse models. Tuple of validated Pydantic GraphQLResponse models and an optional
broken directory path when the file was moved during validation.
Raises: Raises:
ValidationError: If campaign data fails Pydantic validation ValidationError: If campaign data fails Pydantic validation
and crash-on-error is enabled. and crash-on-error is enabled.
""" """
valid_campaigns: list[GraphQLResponse] = [] valid_campaigns: list[GraphQLResponse] = []
broken_dir: Path | None = None
if isinstance(campaigns_found, list): if isinstance(campaigns_found, list):
for campaign in campaigns_found: for campaign in campaigns_found:
@ -369,7 +371,10 @@ class Command(BaseCommand):
# Move invalid inputs out of the hot path so future runs can progress. # Move invalid inputs out of the hot path so future runs can progress.
if not options.get("skip_broken_moves"): if not options.get("skip_broken_moves"):
op_name: str | None = extract_operation_name_from_parsed(campaign) op_name: str | None = extract_operation_name_from_parsed(campaign)
move_failed_validation_file(file_path, operation_name=op_name) broken_dir = move_failed_validation_file(file_path, operation_name=op_name)
# Once the file has been moved, bail out so we don't try to move it again later.
return [], broken_dir
# optionally crash early to surface schema issues. # optionally crash early to surface schema issues.
if options.get("crash_on_error"): if options.get("crash_on_error"):
@ -377,7 +382,7 @@ class Command(BaseCommand):
continue continue
return valid_campaigns return valid_campaigns, broken_dir
def _get_or_create_organization( def _get_or_create_organization(
self, self,
@ -493,7 +498,7 @@ class Command(BaseCommand):
campaigns_found: list[dict[str, Any]], campaigns_found: list[dict[str, Any]],
file_path: Path, file_path: Path,
options: dict[str, Any], options: dict[str, Any],
) -> None: ) -> tuple[bool, Path | None]:
"""Process, validate, and import campaign data. """Process, validate, and import campaign data.
With dependency resolution and caching. With dependency resolution and caching.
@ -506,13 +511,20 @@ class Command(BaseCommand):
Raises: Raises:
ValueError: If datetime parsing fails for campaign dates and ValueError: If datetime parsing fails for campaign dates and
crash-on-error is enabled. crash-on-error is enabled.
Returns:
Tuple of (success flag, broken directory path if moved).
""" """
valid_campaigns: list[GraphQLResponse] = self._validate_campaigns( valid_campaigns, broken_dir = self._validate_campaigns(
campaigns_found=campaigns_found, campaigns_found=campaigns_found,
file_path=file_path, file_path=file_path,
options=options, options=options,
) )
if broken_dir:
# File already moved due to validation failure; signal caller to skip further handling.
return False, broken_dir
for response in valid_campaigns: for response in valid_campaigns:
if not response.data.current_user: if not response.data.current_user:
continue continue
@ -592,6 +604,8 @@ class Command(BaseCommand):
campaign_obj=campaign_obj, campaign_obj=campaign_obj,
) )
return True, None
def _process_time_based_drops( def _process_time_based_drops(
self, self,
time_based_drops_schema: list[TimeBasedDropSchema], time_based_drops_schema: list[TimeBasedDropSchema],
@ -875,12 +889,20 @@ class Command(BaseCommand):
return {"success": False, "broken_dir": str(broken_dir), "reason": "no dropCampaign present"} return {"success": False, "broken_dir": str(broken_dir), "reason": "no dropCampaign present"}
return {"success": False, "broken_dir": "(skipped)", "reason": "no dropCampaign present"} return {"success": False, "broken_dir": "(skipped)", "reason": "no dropCampaign present"}
campaigns_found: list[dict[str, Any]] = [parsed_json] campaigns_found: list[dict[str, Any]] = [parsed_json]
self.process_campaigns( processed, broken_dir = self.process_campaigns(
campaigns_found=campaigns_found, campaigns_found=campaigns_found,
file_path=file_path, file_path=file_path,
options=options, options=options,
) )
if not processed:
# File was already moved to broken during validation
return {
"success": False,
"broken_dir": str(broken_dir) if broken_dir else "(unknown)",
"reason": "validation failed",
}
move_completed_file(file_path=file_path, operation_name=operation_name) move_completed_file(file_path=file_path, operation_name=operation_name)
except (ValidationError, json.JSONDecodeError): except (ValidationError, json.JSONDecodeError):
@ -964,7 +986,19 @@ class Command(BaseCommand):
campaigns_found: list[dict[str, Any]] = [parsed_json] campaigns_found: list[dict[str, Any]] = [parsed_json]
self.process_campaigns(campaigns_found=campaigns_found, file_path=file_path, options=options) processed, broken_dir = self.process_campaigns(
campaigns_found=campaigns_found,
file_path=file_path,
options=options,
)
if not processed:
# File already moved during validation; nothing more to do here.
progress_bar.write(
f"{Fore.RED}{Style.RESET_ALL} {file_path.name}"
f"{broken_dir}/{file_path.name} (validation failed)",
)
return
move_completed_file(file_path=file_path, operation_name=operation_name) move_completed_file(file_path=file_path, operation_name=operation_name)