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:
parent
d7049b5de2
commit
94f0c1d60d
9 changed files with 922 additions and 173 deletions
213
checks/beltline_validation_check.py
Normal file
213
checks/beltline_validation_check.py
Normal 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))
|
||||
}
|
||||
235
checks/extra_carousel_validation_check.py
Normal file
235
checks/extra_carousel_validation_check.py
Normal 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
|
||||
|
|
@ -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)}"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
Loading…
Add table
Reference in a new issue