implemented a batch of tickets from devops. Not deployed, not tested. Could be broken. Will commit again if fixes are required.

This commit is contained in:
michael 2025-09-19 07:57:57 -05:00
parent d7049b5de2
commit 94f0c1d60d
9 changed files with 922 additions and 173 deletions

View file

@ -0,0 +1,213 @@
import os
import json
import logging
import re
from typing import Dict, Any, Set, Tuple, Optional, List
from utils.check_helpers import record_skipped_type, prepare_skipped_result
def run_check(config):
"""
Comprehensive Beltline Validation Check
Validates beltline images with the new structure:
- Desktop: imagetype="desktop", variant="desktop", frames 0-71 (72 total)
- Mobile: imagetype="mobile", variant="mobile", frames 49-71 (23 total)
Validation Requirements:
1. Frame Count: Exactly 72 desktop frames (0-71) and 23 mobile frames (49-71)
2. Frame Range Warnings: Mobile frames 0-48 should not exist
3. Angle/Filename Consistency: JSON angle field must match filename frame number
4. Variant Consistency: imagetype must equal variant field
Expected config:
- working_dir (str): Directory where linkingrecord.json and images reside
- linkingrecord_filename (str): Name of the linking record file, default: linkingrecord.json
Returns:
- "passed" if all validations pass
- "failed" if required frames missing or critical issues found
- Warnings for non-critical issues (unexpected frames, mismatches)
"""
working_dir = config.get("working_dir", "working")
linkingrecord_filename = config.get("linkingrecord_filename", "linkingrecord.json")
linkingrecord_path = os.path.join(working_dir, linkingrecord_filename)
if not os.path.exists(linkingrecord_path):
return {
"status": "error",
"error_message": f"Linking record '{linkingrecord_filename}' not found in {working_dir}."
}
try:
with open(linkingrecord_path, 'r', encoding='utf-8') as f:
linkingrecord = json.load(f)
except json.JSONDecodeError as e:
return {
"status": "error",
"error_message": f"Invalid JSON in {linkingrecord_filename}: {str(e)}"
}
except Exception as e:
return {
"status": "error",
"error_message": f"Error reading {linkingrecord_filename}: {str(e)}"
}
if "items" not in linkingrecord or not isinstance(linkingrecord["items"], list):
return {
"status": "error",
"error_message": "Invalid linkingrecord.json structure: 'items' missing or not a list."
}
# Track skipped viewtype/imagetype combinations
skipped_types: Dict[str, Set[Tuple[str, Optional[str]]]] = {"beltline_validation_check": set()}
# Find all beltline items (vehicleSelector with desktop/mobile imagetype)
desktop_items = []
mobile_items = []
warnings = []
for item in linkingrecord["items"]:
conditions = item.get("conditions", {})
viewtype = conditions.get("viewtype")
imagetype = conditions.get("imagetype")
variant = conditions.get("variant")
# Skip items with missing viewtype
if not viewtype:
logging.debug(f"Skipping item with missing viewtype: {conditions}")
continue
# Only process vehicleSelector items
if viewtype != "vehicleSelector":
continue
# Check for beltline items (desktop/mobile imagetype)
if imagetype in ["desktop", "mobile"]:
# Validate variant consistency
if variant != imagetype:
warnings.append(f"Inconsistent variant field: imagetype='{imagetype}' but variant='{variant}' (should match)")
if imagetype == "desktop":
desktop_items.append(item)
elif imagetype == "mobile":
mobile_items.append(item)
else:
# Record unknown imagetype combinations for vehicleSelector
if imagetype is not None:
record_skipped_type(skipped_types, "beltline_validation_check", viewtype, imagetype)
# If no beltline items found, this is likely not a beltline pack
if not desktop_items and not mobile_items:
if skipped_types["beltline_validation_check"]:
return prepare_skipped_result(skipped_types, "beltline_validation_check")
return {
"status": "passed",
"details": {
"message": "No beltline items found in this pack.",
"pack_type": "Non-beltline pack"
}
}
# Validate desktop frames (0-71, total 72)
desktop_validation = validate_frame_set(desktop_items, "desktop", expected_frames=set(range(72)), working_dir=working_dir)
# Validate mobile frames (49-71, total 23)
mobile_validation = validate_frame_set(mobile_items, "mobile", expected_frames=set(range(49, 72)), working_dir=working_dir)
# Combine all validation results
all_missing_frames = desktop_validation["missing_frames"] + mobile_validation["missing_frames"]
all_warnings = warnings + desktop_validation["warnings"] + mobile_validation["warnings"]
# Determine overall status
if all_missing_frames:
result = {
"status": "failed",
"details": {
"message": f"Beltline validation failed - {len(all_missing_frames)} required frames missing.",
"missing_desktop_frames": desktop_validation["missing_frames"],
"missing_mobile_frames": mobile_validation["missing_frames"],
"desktop_frame_summary": f"{desktop_validation['found_count']}/72 desktop frames found",
"mobile_frame_summary": f"{mobile_validation['found_count']}/23 mobile frames found"
}
}
else:
result = {
"status": "passed",
"details": {
"message": "All required beltline frames found and validated successfully.",
"desktop_frame_summary": f"{desktop_validation['found_count']}/72 desktop frames found",
"mobile_frame_summary": f"{mobile_validation['found_count']}/23 mobile frames found"
}
}
# Add warnings if any exist
if all_warnings:
result["details"]["warnings"] = all_warnings
result["details"]["warning_count"] = len(all_warnings)
# Add skipped types if any exist
if skipped_types["beltline_validation_check"]:
result["details"]["skipped_types_message"] = f"{len(skipped_types['beltline_validation_check'])} unknown viewtype/imagetype combinations were skipped."
result["details"]["skipped_types"] = [
{"viewtype": vt, "imagetype": it if it is not None else "None"}
for vt, it in skipped_types["beltline_validation_check"]
]
return result
def validate_frame_set(items: List[Dict], variant_type: str, expected_frames: Set[int], working_dir: str) -> Dict[str, Any]:
"""
Validate a set of beltline frames (desktop or mobile).
Args:
items: List of linkingrecord items for this variant
variant_type: "desktop" or "mobile" for logging
expected_frames: Set of expected frame numbers (0-71 for desktop, 49-71 for mobile)
working_dir: Directory containing the images
Returns:
Dict with validation results
"""
found_frames = set()
warnings = []
filename_pattern = r'(vsd|vsm)_(\d+)_\d+\.(avif|AVIF)'
for item in items:
records = item.get("records", [])
for record in records:
angle = record.get("angle")
assets = record.get("assets", [])
for asset in assets:
filename = asset.get("filename", "")
if filename:
# Extract frame number from filename
match = re.search(filename_pattern, filename)
if match:
filename_frame = int(match.group(2))
found_frames.add(filename_frame)
# Validate angle/filename consistency
if angle is not None and angle != filename_frame:
warnings.append(f"Angle/filename mismatch: angle={angle} but filename='{filename}' suggests frame {filename_frame}")
# Check for unexpected mobile frames 0-48
if variant_type == "mobile" and filename_frame < 49:
warnings.append(f"Unexpected mobile frame {filename_frame} found (mobile frames should only be 49-71)")
else:
warnings.append(f"Could not parse frame number from filename: '{filename}'")
# Calculate missing frames
missing_frames = sorted(list(expected_frames - found_frames))
found_count = len(found_frames)
return {
"missing_frames": missing_frames,
"found_count": found_count,
"warnings": warnings,
"found_frames": sorted(list(found_frames))
}

View file

@ -0,0 +1,235 @@
import os
import json
import logging
from typing import Dict, Any, List, Tuple
def run_check(config):
"""
Enhanced Extra Carousel Images Validation Check
Validates that carousel/extra images match their WERS codes and meet coverage requirements:
- BAU: 0-49% fail, 50-99% warning, 100% pass
- MEC: 0-99% fail, 100% pass
Expected config:
- working_dir (str): Directory where linkingrecord.json and images reside
- linkingrecord_filename (str): Name of the linking record file, default: linkingrecord.json
Returns:
- "passed" if coverage meets requirements
- "warning" for BAU packs with 50-99% coverage
- "failed" if coverage below requirements or missing features
- "error" for structural issues
"""
working_dir = config.get("working_dir", "working")
linkingrecord_filename = config.get("linkingrecord_filename", "linkingrecord.json")
linkingrecord_path = os.path.join(working_dir, linkingrecord_filename)
if not os.path.exists(linkingrecord_path):
return {
"status": "error",
"error_message": f"Linking record '{linkingrecord_filename}' not found in {working_dir}."
}
try:
with open(linkingrecord_path, 'r', encoding='utf-8') as f:
linkingrecord = json.load(f)
except json.JSONDecodeError as e:
return {
"status": "error",
"error_message": f"Invalid JSON in {linkingrecord_filename}: {str(e)}"
}
except Exception as e:
return {
"status": "error",
"error_message": f"Error reading {linkingrecord_filename}: {str(e)}"
}
if "items" not in linkingrecord or not isinstance(linkingrecord["items"], list):
return {
"status": "error",
"error_message": "Invalid linkingrecord.json structure: 'items' missing or not a list."
}
# Detect pack type (MEC vs BAU)
is_mec = any(
item.get("conditions", {}).get("experienceCondition") == "2d-background"
for item in linkingrecord["items"]
)
pack_type = "MEC" if is_mec else "BAU"
# Find all carousel/extra items
carousel_extra_items = []
for item_index, item in enumerate(linkingrecord["items"]):
conditions = item.get("conditions", {})
viewtype = conditions.get("viewtype")
imagetype = conditions.get("imagetype")
if viewtype == "carousel" and imagetype == "extra":
carousel_extra_items.append((item_index, item))
# If no carousel/extra items found, this pack doesn't use extra carousel images
if not carousel_extra_items:
return {
"status": "passed",
"details": {
"message": "No carousel/extra items found - validation not applicable.",
"pack_type": pack_type,
"validation_type": "not_applicable"
}
}
# Validate each carousel/extra item
record_results = []
all_features = []
all_matched_features = []
for item_index, item in carousel_extra_items:
records = item.get("records", [])
for record_index, record in enumerate(records):
result = validate_record_features(record, working_dir, item_index, record_index)
if result["status"] == "error":
return {
"status": "error",
"error_message": result["error_message"]
}
record_results.append(result)
all_features.extend(result["features"])
all_matched_features.extend(result["matched_features"])
# Calculate overall coverage
if not all_features:
return {
"status": "error",
"error_message": "No features found in any carousel/extra records. Features array cannot be empty."
}
total_features = len(all_features)
matched_count = len(all_matched_features)
overall_coverage = (matched_count / total_features) * 100
# Apply validation rules based on pack type
if pack_type == "MEC":
# MEC: Must have 100% coverage
if overall_coverage < 100:
status = "failed"
message = f"MEC pack requires 100% extra carousel coverage, found {overall_coverage:.1f}%"
else:
status = "passed"
message = f"MEC pack extra carousel validation passed - 100% coverage achieved"
else:
# BAU: 0-49% fail, 50-99% warning, 100% pass
if overall_coverage < 50:
status = "failed"
message = f"BAU pack extra carousel coverage too low: {overall_coverage:.1f}% (minimum 50% required)"
elif overall_coverage < 100:
status = "warning"
message = f"BAU pack extra carousel coverage at {overall_coverage:.1f}% - consider improving to 100%"
else:
status = "passed"
message = f"BAU pack extra carousel validation passed - 100% coverage achieved"
# Compile missing features summary
missing_features = []
for result in record_results:
missing_features.extend(result["missing_features"])
# Build comprehensive result
result = {
"status": status,
"details": {
"message": message,
"pack_type": pack_type,
"overall_coverage": round(overall_coverage, 1),
"total_features_across_all_records": total_features,
"matched_features_count": matched_count,
"validation_summary": f"{matched_count}/{total_features} features matched ({overall_coverage:.1f}% coverage)",
"record_results": record_results
}
}
# Add missing features summary if any
if missing_features:
result["details"]["missing_features_summary"] = sorted(list(set(missing_features)))
return result
def validate_record_features(record: Dict[str, Any], working_dir: str, item_index: int, record_index: int) -> Dict[str, Any]:
"""
Validate features in a single record against available image files.
Args:
record: Single record from carousel/extra item
working_dir: Directory containing image files
item_index: Index of the parent item
record_index: Index of this record within the item
Returns:
Dict with validation results for this record
"""
features = record.get("features", [])
# Check for empty or missing features
if not features:
return {
"status": "error",
"error_message": f"Empty or missing 'features' array in carousel/extra record (item {item_index}, record {record_index})"
}
# Get all image files in working directory
available_files = []
try:
for root, dirs, files in os.walk(working_dir):
for file in files:
if file.lower().endswith(('.jpg', '.jpeg', '.png', '.avif')):
available_files.append(file)
except Exception as e:
return {
"status": "error",
"error_message": f"Error scanning for image files: {str(e)}"
}
# Check each feature for matching images using substring search
matched_features = []
found_filenames = []
missing_features = []
for feature in features:
# Case-sensitive substring search in filenames
matching_files = [f for f in available_files if feature in f]
if matching_files:
matched_features.append(feature)
found_filenames.extend(matching_files)
else:
missing_features.append(feature)
# Calculate coverage for this record
coverage = (len(matched_features) / len(features)) * 100 if features else 0
return {
"status": "success",
"record_index": record_index,
"item_index": item_index,
"features": features,
"coverage": round(coverage, 1),
"matched_features": matched_features,
"missing_features": missing_features,
"found_filenames": sorted(list(set(found_filenames))) # Remove duplicates and sort
}
def check_feature_in_filename(feature: str, filename: str) -> bool:
"""
Simple case-sensitive substring search.
Args:
feature: WERS code to search for
filename: Filename to search in
Returns:
bool: True if feature appears anywhere in filename
"""
return feature in filename

View file

@ -93,23 +93,23 @@ def run_check(config):
)
pack_type = "MEC" if is_mec else "BAU"
# Max sizes in KB
# Max sizes in KB (updated limits with 25% advisory buffer system)
max_sizes_kb = {
"Base Exterior Images": 750,
"Base Interior Images": 1200,
"Engine and Transmission Images": 1024,
"Interior Layered Option Images": 1024,
"Exterior Layered Option Images": 600,
"Option Carousel Images": 500,
"Powertrain Image": 400,
"Showroom Images": 300,
"Colour Chips": 50,
"Bodystyle Images": 200,
"Series Images": 300,
"Trim Images": 700,
"Lifestyle Images": 500, # 500KB for lifestyle
"Inventory Images": 1024, # 1MB for inventory
"Beltline Images": 800 # 800KB for beltline AVIF images
"Base Exterior Images": 750, # No change
"Base Interior Images": 1024, # 1MB binary (was 1200)
"Engine and Transmission Images": 1024, # No change
"Interior Layered Option Images": 1024, # No change
"Exterior Layered Option Images": 750, # Increased from 600
"Option Carousel Images": 500, # No change
"Powertrain Images": 600, # Increased from 400, renamed from singular
"Showroom Images": 200, # Decreased from 300
"Colour Chips": 30, # Decreased from 50
"Bodystyle Images": 50, # Decreased from 200
"Series Images": 50, # Decreased from 300
"Trim Images": 800, # Increased from 700
"Lifestyle Images": 400, # Decreased from 500
"Inventory Images": 800, # Decreased from 1024
"Beltline Images": 800 # No change (typo corrected)
}
def get_asset_type(conditions):
@ -122,8 +122,13 @@ def run_check(config):
return "Interior Layered Option Images"
if viewtype == "exterior" and imagetype == "layeroptext":
return "Exterior Layered Option Images"
if experience == "2d-background" and viewtype in ["exterior", "interior"] and imagetype is None:
return "Engine and Transmission Images"
# Check for MEC powertrain images (2d-background + powertrain in filename)
if experience == "2d-background":
if viewtype in ["exterior", "interior"] and imagetype is None:
return "Engine and Transmission Images"
# MEC carousel powertrain should also use Powertrain Images limit
if viewtype == "carousel" and imagetype == "powertrain":
return "Powertrain Images"
if viewtype == "exterior" and imagetype is None:
return "Base Exterior Images"
if viewtype == "interior" and imagetype is None:
@ -132,7 +137,7 @@ def run_check(config):
if imagetype == "extra":
return "Option Carousel Images"
if imagetype == "powertrain":
return "Powertrain Image"
return "Powertrain Images" # Updated to plural to match new limit key
if imagetype == "colour":
return "Colour Chips"
if imagetype == "bodystyle":
@ -147,14 +152,16 @@ def run_check(config):
return "Lifestyle Images"
if viewtype == "inventory":
return "Inventory Images"
if viewtype == "vehicleSelector" and imagetype == "beltline":
if viewtype == "vehicleSelector" and imagetype in ["desktop", "mobile"]:
return "Beltline Images"
# No match
return None
# Use a dictionary to avoid duplicates (key by filename)
failed_images_dict = {}
# Three-tier validation system: compliant, advisory warnings, size violations
compliant_images = {}
advisory_warnings = {}
size_violations = {}
# Track skipped viewtype/imagetype combinations
skipped_types: Dict[str, Set[Tuple[str, Optional[str]]]] = {"file_size_check": set()}
@ -179,6 +186,7 @@ def run_check(config):
max_size_kb = max_sizes_kb[asset_type]
max_size_bytes = max_size_kb * 1024
advisory_threshold_bytes = int(max_size_bytes * 1.25) # 25% buffer zone
records = item.get("records", [])
for record_index, record in enumerate(records):
@ -200,53 +208,113 @@ def run_check(config):
logging.warning(f"Could not get size of file {file_path}: {str(e)}")
continue
if file_size > max_size_bytes:
if filename not in failed_images_dict:
failed_images_dict[filename] = {
# Calculate percentages and categorize
file_size_kb = round(file_size / 1024, 2)
percentage_of_limit = round((file_size / max_size_bytes) * 100, 2)
# Three-tier validation
if file_size <= max_size_bytes:
# Compliant: 0-100% of limit
if filename not in compliant_images:
compliant_images[filename] = {
"filename": filename,
"asset_type": asset_type,
"file_size_bytes": file_size,
"max_size_bytes": max_size_bytes,
"file_size_kb": round(file_size / 1024, 2),
"max_size_kb": max_size_kb,
"over_limit_kb": round((file_size - max_size_bytes) / 1024, 2),
"over_limit_percentage": round(((file_size - max_size_bytes) / max_size_bytes) * 100, 2)
"file_size_kb": file_size_kb,
"limit_kb": max_size_kb,
"percentage_of_limit": percentage_of_limit,
"status": "compliant"
}
elif file_size <= advisory_threshold_bytes:
# Advisory Warning: 101-125% of limit
if filename not in advisory_warnings:
advisory_warnings[filename] = {
"filename": filename,
"asset_type": asset_type,
"file_size_kb": file_size_kb,
"limit_kb": max_size_kb,
"percentage_over": round(percentage_of_limit - 100, 1),
"percentage_of_limit": percentage_of_limit,
"status": "advisory",
"message": "File size slightly exceeds recommended limit but is within acceptable tolerance. Consider optimizing for better performance."
}
else:
# Size Violation: 126%+ of limit
if filename not in size_violations:
size_violations[filename] = {
"filename": filename,
"asset_type": asset_type,
"file_size_kb": file_size_kb,
"limit_kb": max_size_kb,
"percentage_over": round(percentage_of_limit - 100, 1),
"percentage_of_limit": percentage_of_limit,
"status": "violation",
"message": "File size significantly exceeds maximum limit and requires optimization before final delivery."
}
if failed_images_dict:
result = {
"status": "failed",
"details": {
"message": f"{len(failed_images_dict)} files exceed the maximum allowed size.",
"failed_images": list(failed_images_dict.values()),
"pack_type": pack_type
}
}
# Add skipped types to the result if any were found
if skipped_types["file_size_check"]:
result["details"]["skipped_types_message"] = f"{len(skipped_types['file_size_check'])} unknown viewtype/imagetype combinations were skipped."
result["details"]["skipped_types"] = [
{"viewtype": vt, "imagetype": it if it is not None else "None"}
for vt, it in skipped_types["file_size_check"]
]
return result
# Generate counts for summary
compliant_count = len(compliant_images)
advisory_count = len(advisory_warnings)
violation_count = len(size_violations)
total_files = compliant_count + advisory_count + violation_count
# If we skipped any types, add that to the successful result
if skipped_types["file_size_check"]:
result = prepare_skipped_result(skipped_types, "file_size_check")
result["details"]["pack_type"] = pack_type
return result
# Determine overall status (fail only if there are violations)
if violation_count > 0:
status = "failed"
status_message = f"File size validation failed - {violation_count} files significantly exceed size limits"
elif advisory_count > 0:
status = "passed"
status_message = f"File size validation passed with {advisory_count} advisory warnings"
else:
status = "passed"
status_message = "All present images are within recommended file size limits"
return {
"status": "passed",
# Build comprehensive result
result = {
"status": status,
"details": {
"message": "All present images are within the allowed file size.",
"message": status_message,
"validation_summary": f"{compliant_count} compliant, {advisory_count} advisory warnings, {violation_count} violations",
"pack_type": pack_type
}
}
# Add compliant images if any
if compliant_images:
result["details"]["compliant_images"] = list(compliant_images.values())
# Add advisory warnings if any (these don't cause failure)
if advisory_warnings:
result["details"]["advisory_warnings"] = list(advisory_warnings.values())
# Add size violations if any (these cause failure)
if size_violations:
result["details"]["size_violations"] = list(size_violations.values())
# Add detailed statistics
result["details"]["statistics"] = {
"total_files_checked": total_files,
"compliant_files": compliant_count,
"advisory_warnings": advisory_count,
"size_violations": violation_count,
"compliance_rate": round((compliant_count / total_files) * 100, 1) if total_files > 0 else 100
}
# Add skipped types if any exist
if skipped_types["file_size_check"]:
result["details"]["skipped_types_message"] = f"{len(skipped_types['file_size_check'])} unknown viewtype/imagetype combinations were skipped."
result["details"]["skipped_types"] = [
{"viewtype": vt, "imagetype": it if it is not None else "None"}
for vt, it in skipped_types["file_size_check"]
]
# Handle case where no files were processed
if total_files == 0:
if skipped_types["file_size_check"]:
return prepare_skipped_result(skipped_types, "file_size_check")
result["details"]["message"] = "No files were found to validate for file size"
return result
except Exception as e:
# Catch any unexpected exceptions and return detailed error information
error_msg = f"Unexpected error in file size check: {str(e)}"

View file

@ -40,13 +40,18 @@ def run_check(config):
"status": "error",
"error_message": "Invalid linkingrecord.json structure: 'items' missing or not a list."
}
# Define format requirements
# Detect if this is a MEC pack (contains experienceCondition="2d-background")
is_mec = any(
item.get("conditions", {}).get("experienceCondition") == "2d-background"
for item in linkingrecord["items"]
)
pack_type = "MEC" if is_mec else "BAU"
# Define format requirements (base assets handled separately)
# Format: (viewtype, imagetype): expected_format
format_requirements = {
# For BAU
('exterior', None): 'PNG',
('interior', None): 'PNG',
# Base assets ('exterior', None) and ('interior', None) are handled with flexible logic below
('exterior', 'layeroptext'): 'PNG',
('interior', 'layeroptint'): 'PNG',
('carousel', 'extra'): 'JPEG',
@ -59,40 +64,72 @@ def run_check(config):
# Adding lifestyle and inventory requirements
('lifestyle', None): 'JPEG',
('inventory', None): 'JPEG',
# Adding beltline requirements
('vehicleSelector', 'beltline'): 'AVIF',
# Adding beltline requirements (desktop and mobile variants)
('vehicleSelector', 'desktop'): 'AVIF',
('vehicleSelector', 'mobile'): 'AVIF',
}
def is_base_asset(viewtype, imagetype):
"""Check if this is a base exterior or interior asset."""
return (viewtype == 'exterior' and imagetype is None) or (viewtype == 'interior' and imagetype is None)
def validate_base_asset_format(filename, actual_format, pack_type, viewtype):
"""
Validate base asset format with flexible rules:
- MEC base assets: Strict JPG only (fail if PNG)
- BAU base assets: JPG preferred, PNG acceptable with warning
Returns: (is_valid, warning_message)
"""
if pack_type == "MEC":
# MEC base assets must be JPG only
if actual_format != "JPEG":
return False, None
return True, None
else: # BAU pack
# BAU base assets: JPG preferred, PNG acceptable with warning
if actual_format == "JPEG":
return True, None
elif actual_format == "PNG":
return True, f"PNG format found but JPG format is preferred for {viewtype} base assets. Please consider changing this to JPG to match business requirements."
else:
return False, None
# Use a dictionary to avoid duplicates (keyed by filename)
failed_images_dict = {}
warnings_list = []
# Track skipped viewtype/imagetype combinations
skipped_types: Dict[str, Set[Tuple[str, Optional[str]]]] = {"image_format_check": set()}
for item in linkingrecord["items"]:
conditions = item.get("conditions", {})
viewtype = conditions.get("viewtype")
imagetype = conditions.get("imagetype")
# Skip items with missing viewtype
if not viewtype:
logging.debug(f"Skipping item with missing viewtype: {conditions}")
continue
# Find the applicable format requirement
key = (viewtype, imagetype)
expected_format = None
if key in format_requirements:
expected_format = format_requirements[key]
# Try with None imagetype as fallback
elif (viewtype, None) in format_requirements:
expected_format = format_requirements[(viewtype, None)]
# If no requirement is found, record it and skip this item
if expected_format is None:
record_skipped_type(skipped_types, "image_format_check", viewtype, imagetype)
continue
# Check if this is a base asset that needs special handling
if is_base_asset(viewtype, imagetype):
# Base assets use flexible validation logic
expected_format = None # Will be handled in the validation function
else:
# Non-base assets use the standard format requirements
key = (viewtype, imagetype)
expected_format = None
if key in format_requirements:
expected_format = format_requirements[key]
# Try with None imagetype as fallback
elif (viewtype, None) in format_requirements:
expected_format = format_requirements[(viewtype, None)]
# If no requirement is found, record it and skip this item
if expected_format is None:
record_skipped_type(skipped_types, "image_format_check", viewtype, imagetype)
continue
records = item.get("records", [])
for record in records:
@ -111,26 +148,52 @@ def run_check(config):
try:
with Image.open(image_path) as img:
actual_format = img.format
# PIL reports "JPEG" for JPG files
if actual_format != expected_format:
# Only add an entry if this filename not yet in the dict
if filename not in failed_images_dict:
failed_images_dict[filename] = {
if is_base_asset(viewtype, imagetype):
# Use flexible validation for base assets
is_valid, warning_message = validate_base_asset_format(filename, actual_format, pack_type, viewtype)
if not is_valid:
# Only add an entry if this filename not yet in the dict
if filename not in failed_images_dict:
expected_desc = "JPEG" if pack_type == "MEC" else "JPEG (preferred) or PNG (acceptable)"
failed_images_dict[filename] = {
"filename": filename,
"viewtype": viewtype,
"imagetype": imagetype,
"expected_format": expected_desc,
"actual_format": actual_format,
"pack_type": pack_type
}
elif warning_message:
# Add warning but don't fail
warnings_list.append({
"filename": filename,
"viewtype": viewtype,
"imagetype": imagetype,
"expected_format": expected_format,
"actual_format": actual_format
}
"message": warning_message
})
else:
# Standard validation for non-base assets
# PIL reports "JPEG" for JPG files
if actual_format != expected_format:
# Only add an entry if this filename not yet in the dict
if filename not in failed_images_dict:
failed_images_dict[filename] = {
"filename": filename,
"viewtype": viewtype,
"imagetype": imagetype,
"expected_format": expected_format,
"actual_format": actual_format
}
except Exception as e:
# If we can't open the image, mark it as failed
if filename not in failed_images_dict:
expected_desc = expected_format if expected_format else ("JPEG" if pack_type == "MEC" and is_base_asset(viewtype, imagetype) else "Unknown")
failed_images_dict[filename] = {
"filename": filename,
"viewtype": viewtype,
"imagetype": imagetype,
"expected_format": expected_format,
"expected_format": expected_desc,
"actual_format": f"Error: {str(e)}"
}
@ -140,10 +203,15 @@ def run_check(config):
"status": "failed",
"details": {
"message": "Some images are not in the expected file format.",
"failed_images": list(failed_images_dict.values())
"failed_images": list(failed_images_dict.values()),
"pack_type": pack_type
}
}
# Add warnings to failed result if any exist
if warnings_list:
result["details"]["warnings"] = warnings_list
# Add skipped types to the result if any were found
if skipped_types["image_format_check"]:
result["details"]["skipped_types_message"] = f"{len(skipped_types['image_format_check'])} unknown viewtype/imagetype combinations were skipped."
@ -151,16 +219,32 @@ def run_check(config):
{"viewtype": vt, "imagetype": it if it is not None else "None"}
for vt, it in skipped_types["image_format_check"]
]
return result
# If we skipped any types, add that to the successful result
if skipped_types["image_format_check"]:
return prepare_skipped_result(skipped_types, "image_format_check")
return {
# No failures - construct success result
base_message = "All present images match their required file format."
# If we have warnings, it's still a pass but with warnings
result = {
"status": "passed",
"details": {
"message": "All present images match their required file format."
"message": base_message,
"pack_type": pack_type
}
}
}
# Add warnings to successful result if any exist
if warnings_list:
result["details"]["warnings"] = warnings_list
result["details"]["message"] = f"{base_message} Some recommendations noted below."
# If we skipped any types, add that to the result
if skipped_types["image_format_check"]:
result["details"]["skipped_types_message"] = f"{len(skipped_types['image_format_check'])} unknown viewtype/imagetype combinations were skipped."
result["details"]["skipped_types"] = [
{"viewtype": vt, "imagetype": it if it is not None else "None"}
for vt, it in skipped_types["image_format_check"]
]
return result

View file

@ -74,8 +74,9 @@ def run_check(config):
("carousel", "bodystyle"): (678, 381),
("carousel", "series"): (678, 381),
("carousel", "trim"): (678, 381),
# Beltline resolution requirements (desktop and mobile)
("vehicleSelector", "beltline"): (1920, 842), # Default to desktop
# Beltline resolution requirements (desktop and mobile variants)
("vehicleSelector", "desktop"): (1920, 842),
("vehicleSelector", "mobile"): (640, 428),
}
mec_map = {
@ -92,8 +93,9 @@ def run_check(config):
("carousel", "bodystyle"): (678, 381),
("carousel", "series"): (678, 381),
("carousel", "trim"): (678, 381),
# Beltline resolution requirements (desktop and mobile)
("vehicleSelector", "beltline"): (1920, 842), # Default to desktop
# Beltline resolution requirements (desktop and mobile variants)
("vehicleSelector", "desktop"): (1920, 842),
("vehicleSelector", "mobile"): (640, 428),
}
# -------------------------------------------------------------------------
@ -141,15 +143,7 @@ def run_check(config):
)
key = (viewtype, imagetype)
# Special handling for beltline images with mobile variant
if viewtype == "vehicleSelector" and imagetype == "beltline":
if variant == "mobile":
return (640, 428), used_experience_type
else:
# Default to desktop resolution for beltline
return (1920, 842), used_experience_type
# If the exact key isn't in the map, try a fallback without imagetype
if key not in resolution_map:
fallback_key = (viewtype, None)

View file

@ -119,7 +119,7 @@ def run_check(config):
elif viewtype == "carousel" and imagetype not in ["powertrain", "extra", "colour", "bodystyle", "series", "trim"]:
record_skipped_type(skipped_types, "layer_depth_check", viewtype, imagetype)
continue
elif viewtype == "vehicleSelector" and imagetype not in ["beltline"]:
elif viewtype == "vehicleSelector" and imagetype not in ["desktop", "mobile"]:
record_skipped_type(skipped_types, "layer_depth_check", viewtype, imagetype)
continue

View file

@ -15,9 +15,8 @@ def run_check(config):
- In its records, look for angle=30 and an asset filename containing "powertrain" and "mec_30_0".
- If not found, fail.
* If BAU (no "2d-background"):
- Find carousel/powertrain item.
- Ensure it has at least one asset in its records.
- If not found, fail.
- Validate _mec filename placement rules (no carousel/powertrain requirements).
- Pass if _mec assets are properly placed or not present.
* Unknown viewtype/imagetype combinations are gracefully skipped and recorded.
"""
@ -63,7 +62,7 @@ def run_check(config):
record_skipped_type(skipped_types, "special_requirements_mec_bau", viewtype, imagetype)
elif viewtype in ["exterior", "interior"] and imagetype not in [None, "layeroptint", "layeroptext", "showroom"]:
record_skipped_type(skipped_types, "special_requirements_mec_bau", viewtype, imagetype)
elif viewtype == "vehicleSelector" and imagetype not in ["beltline"]:
elif viewtype == "vehicleSelector" and imagetype not in ["desktop", "mobile"]:
record_skipped_type(skipped_types, "special_requirements_mec_bau", viewtype, imagetype)
# Detect if MEC scenario:
@ -200,7 +199,29 @@ def run_check(config):
break
if not has_mec_asset:
section_validation_warnings.append(f"2d-background {description} section exists but no assets with '_mec' found in filenames")
# Universal _mec filename validation: check ALL items to ensure _mec assets are only in 2d-background sections
for item in items:
conditions = item.get("conditions", {})
viewtype = conditions.get("viewtype")
imagetype = conditions.get("imagetype")
experience_condition = conditions.get("experienceCondition")
# Skip items without viewtype
if not viewtype:
continue
# Check all assets in this item
for record in item.get("records", []):
for asset in record.get("assets", []):
filename = asset.get("filename", "")
if "_mec" in filename:
# Asset has _mec in filename - it should ONLY be in 2d-background sections
if experience_condition != "2d-background":
imagetype_display = imagetype if imagetype is not None else "None"
experience_display = experience_condition if experience_condition is not None else "None"
section_validation_errors.append(f"Asset '{filename}' found in regular viewtype='{viewtype}', imagetype='{imagetype_display}' section (should only be in 2d-background sections)")
# If there are section validation errors, fail the check
if section_validation_errors:
result = {
@ -249,71 +270,60 @@ def run_check(config):
else:
# BAU scenario:
# Need to find a carousel/powertrain item with at least one asset
carousel_powertrain_items = [
item for item in items
if item.get("conditions", {}).get("viewtype") == "carousel"
and item.get("conditions", {}).get("imagetype") == "powertrain"
]
# No carousel/powertrain requirements - only validate _mec filename placement rules
if not carousel_powertrain_items:
# Universal _mec filename validation for BAU: check ALL items to ensure _mec assets are only in 2d-background sections
bau_validation_errors = []
for item in items:
conditions = item.get("conditions", {})
viewtype = conditions.get("viewtype")
imagetype = conditions.get("imagetype")
experience_condition = conditions.get("experienceCondition")
# Skip items without viewtype
if not viewtype:
continue
# Check all assets in this item
for record in item.get("records", []):
for asset in record.get("assets", []):
filename = asset.get("filename", "")
if "_mec" in filename:
# Asset has _mec in filename - it should ONLY be in 2d-background sections
if experience_condition != "2d-background":
imagetype_display = imagetype if imagetype is not None else "None"
experience_display = experience_condition if experience_condition is not None else "None"
bau_validation_errors.append(f"Asset '{filename}' found in regular viewtype='{viewtype}', imagetype='{imagetype_display}' section (should only be in 2d-background sections)")
# If there are BAU validation errors, fail the check
if bau_validation_errors:
result = {
"status": "failed",
"details": {
"message": "BAU scenario: No carousel/powertrain section found.",
"pack_type": pack_type
"message": "BAU scenario: _mec filename validation failed.",
"pack_type": pack_type,
"bau_validation_errors": bau_validation_errors
}
}
# Add skipped types to the result if any were found
if skipped_types["special_requirements_mec_bau"]:
result["details"]["skipped_types_message"] = f"{len(skipped_types['special_requirements_mec_bau'])} unknown viewtype/imagetype combinations were skipped."
result["details"]["skipped_types"] = [
{"viewtype": vt, "imagetype": it if it is not None else "None"}
for vt, it in skipped_types["special_requirements_mec_bau"]
]
return result
# Check for at least one asset
found_asset = False
for item in carousel_powertrain_items:
records = item.get("records", [])
for record in records:
assets = record.get("assets", [])
if assets:
found_asset = True
break
if found_asset:
break
if not found_asset:
result = {
"status": "failed",
"details": {
"message": "BAU scenario: carousel/powertrain found but no assets present.",
"pack_type": pack_type
}
}
# Add skipped types to the result if any were found
if skipped_types["special_requirements_mec_bau"]:
result["details"]["skipped_types_message"] = f"{len(skipped_types['special_requirements_mec_bau'])} unknown viewtype/imagetype combinations were skipped."
result["details"]["skipped_types"] = [
{"viewtype": vt, "imagetype": it if it is not None else "None"}
for vt, it in skipped_types["special_requirements_mec_bau"]
]
return result
# BAU pack passes - no carousel/powertrain requirements, _mec validation passed
result = {
"status": "passed",
"details": {
"message": "BAU scenario: carousel/powertrain section with assets found.",
"message": "BAU scenario: _mec filename validation passed. No carousel/powertrain requirements.",
"pack_type": pack_type
}
}
# If we skipped any types, add that to the successful result
if skipped_types["special_requirements_mec_bau"]:
result["details"]["skipped_types_message"] = f"{len(skipped_types['special_requirements_mec_bau'])} unknown viewtype/imagetype combinations were skipped."
@ -321,5 +331,5 @@ def run_check(config):
{"viewtype": vt, "imagetype": it if it is not None else "None"}
for vt, it in skipped_types["special_requirements_mec_bau"]
]
return result

View file

@ -2,6 +2,112 @@ import os
import zipfile
import shutil
import json
import re
def scan_for_extraneous_files(working_dir):
"""
Recursively scan the working directory for files that are not in the allowed whitelist.
Args:
working_dir (str): Directory to scan
Returns:
list: List of dictionaries containing information about extraneous files
"""
# Define allowed file extensions (case-insensitive)
allowed_extensions = {'.jpg', '.jpeg', '.png', '.avif', '.json'}
extraneous_files = []
# Walk through all directories and files recursively
for root, dirs, files in os.walk(working_dir):
for file in files:
file_path = os.path.join(root, file)
# Get the file extension (case-insensitive)
_, ext = os.path.splitext(file.lower())
# Check if the extension is in the allowed list
if ext not in allowed_extensions:
# Calculate relative path from working_dir for cleaner reporting
relative_path = os.path.relpath(file_path, working_dir)
# Special handling for files that start with . but have no extension (like .DS_Store)
if not ext and file.startswith('.'):
display_extension = file.lower() # Show full filename for hidden files
else:
display_extension = ext if ext else "(no extension)"
extraneous_files.append({
"path": relative_path,
"filename": file,
"extension": display_extension,
"full_path": file_path,
"size_bytes": os.path.getsize(file_path) if os.path.exists(file_path) else 0
})
return extraneous_files
def validate_linkingrecord_header(linkingrecord_data):
"""
Validate the header section of linkingrecord.json for required field formats.
Args:
linkingrecord_data (dict): The parsed linkingrecord JSON data
Returns:
tuple: (is_valid: bool, error_details: list)
"""
errors = []
# Extract header fields
market = linkingrecord_data.get("market", "")
model = linkingrecord_data.get("model", "")
year = linkingrecord_data.get("year", "")
# Get numericalMmy from header sub-object
header = linkingrecord_data.get("header", {})
numerical_mmy = header.get("numericalMmy", "")
# Validate market (exactly 3 alphabetic characters)
if not isinstance(market, str) or not re.match(r'^[A-Z]{3}$', market):
errors.append({
"field": "market",
"value": market,
"error": "Must be exactly 3 alphabetic characters (A-Z)"
})
# Validate model (alphanumeric capitals only)
if not isinstance(model, str) or not re.match(r'^[A-Z0-9]+$', model):
errors.append({
"field": "model",
"value": model,
"error": "Must contain only capital letters and numbers"
})
# Validate year (exactly 3 digits)
if not isinstance(year, str) or not re.match(r'^[0-9]{3}$', year):
errors.append({
"field": "year",
"value": year,
"error": "Must be exactly 3 digits representing the year"
})
# Validate numericalMmy (no dots allowed)
if isinstance(numerical_mmy, str) and '.' in numerical_mmy:
errors.append({
"field": "numericalMmy",
"value": numerical_mmy,
"error": "Must not contain dot characters"
})
elif not isinstance(numerical_mmy, str):
errors.append({
"field": "numericalMmy",
"value": numerical_mmy,
"error": "Must be a string value"
})
return len(errors) == 0, errors
def run_check(config):
# We expect config to contain:
@ -74,7 +180,7 @@ def run_check(config):
if expected_file.lower().endswith('.json'):
try:
with open(expected_path, 'r', encoding='utf-8') as f:
json.load(f)
linkingrecord_data = json.load(f)
except json.JSONDecodeError as e:
return {
"status": "failed",
@ -113,10 +219,35 @@ def run_check(config):
}
}
# Validate header fields if this is a linkingrecord.json file
if expected_file.lower() == 'linkingrecord.json':
is_valid, header_errors = validate_linkingrecord_header(linkingrecord_data)
if not is_valid:
return {
"status": "failed",
"details": {
"message": f"Linkingrecord header validation failed - {len(header_errors)} header field errors found",
"header_validation_errors": header_errors
}
}
# Scan for extraneous files (files not in the allowed whitelist)
extraneous_files = scan_for_extraneous_files(working_dir)
if extraneous_files:
return {
"status": "failed",
"details": {
"message": "Extraneous files found that are not allowed in asset packs.",
"extraneous_files": extraneous_files,
"allowed_types": [".jpg", ".jpeg", ".png", ".avif", ".json"],
"total_extraneous_count": len(extraneous_files)
}
}
return {
"status": "passed",
"details": {
"message": f"{expected_file} found in {working_dir} and validated successfully.",
"message": f"{expected_file} found in {working_dir} and validated successfully. No extraneous files detected.",
"extracted_dir": working_dir
}
}

View file

@ -83,5 +83,19 @@
"working_dir": "__WORKING_DIR__",
"linkingrecord_filename": "linkingrecord.json"
}
},
{
"script": "checks.beltline_validation_check",
"config": {
"working_dir": "__WORKING_DIR__",
"linkingrecord_filename": "linkingrecord.json"
}
},
{
"script": "checks.extra_carousel_validation_check",
"config": {
"working_dir": "__WORKING_DIR__",
"linkingrecord_filename": "linkingrecord.json"
}
}
]