feat: Implement metadata system and reorganize project structure
✨ Features: - Add comprehensive metadata system with YAML/JSON support - Implement hierarchical metadata inheritance from parent folders - Support plot-specific metadata overrides - Add metadata caching for performance optimization 🗂️ Code Organization: - Move Python orchestration code to orchestration/ folder - Move validation utilities to tests/ folder - Move documentation to docs/ folder - Separate metadata functionality into dedicated module 🔧 Infrastructure: - Add automatic asset copying to web directory - Fix asset path resolution for nested directories - Update template to use dynamic asset paths - Add MetadataConfig class with inheritance options 📚 Documentation: - Add comprehensive metadata usage guide (METADATA_USAGE.md) - Add implementation documentation (METADATA_IMPLEMENTATION.md) - Include example metadata files in examples/ - Add metadata validation utility script 🐛 Bug Fixes: - Fix breadcrumb navigation and JavaScript functionality - Resolve asset path issues in nested directories - Update template imports for modular CSS/JS structure This commit introduces a flexible metadata system that allows users to add rich metadata to plots and folders using YAML or JSON files, with full hierarchical inheritance and plot-specific overrides. The project structure is now better organized with clear separation of concerns.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Metadata Validation Utility
|
||||
|
||||
This script validates metadata files in the gallery source directories,
|
||||
checking for proper YAML/JSON syntax and common field validation.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List
|
||||
|
||||
|
||||
def validate_metadata_file(file_path: Path) -> tuple[bool, List[str]]:
|
||||
"""
|
||||
Validate a single metadata file.
|
||||
|
||||
Args:
|
||||
file_path: Path to the metadata file
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, error_messages)
|
||||
"""
|
||||
errors = []
|
||||
|
||||
if not file_path.exists():
|
||||
errors.append(f"File does not exist: {file_path}")
|
||||
return False, errors
|
||||
|
||||
try:
|
||||
with file_path.open('r', encoding='utf-8') as f:
|
||||
suffix_lower = file_path.suffix.lower()
|
||||
if suffix_lower in ['.yaml', '.yml']:
|
||||
data = yaml.safe_load(f)
|
||||
elif suffix_lower == '.json':
|
||||
data = json.load(f)
|
||||
else:
|
||||
errors.append(f"Unsupported file format: {file_path}")
|
||||
return False, errors
|
||||
|
||||
if data is None:
|
||||
errors.append(f"Empty metadata file: {file_path}")
|
||||
return False, errors
|
||||
|
||||
# Basic validation
|
||||
if not isinstance(data, dict):
|
||||
errors.append(f"Metadata must be a dictionary: {file_path}")
|
||||
return False, errors
|
||||
|
||||
# Check for common issues
|
||||
if 'title' in data and not isinstance(data['title'], str):
|
||||
errors.append(f"Title must be a string: {file_path}")
|
||||
|
||||
if 'tags' in data and not isinstance(data['tags'], list):
|
||||
errors.append(f"Tags must be a list: {file_path}")
|
||||
|
||||
if 'author' in data and not isinstance(data['author'], dict):
|
||||
errors.append(f"Author must be a dictionary: {file_path}")
|
||||
|
||||
except (yaml.YAMLError, json.JSONDecodeError) as e:
|
||||
errors.append(f"Parse error in {file_path}: {e}")
|
||||
return False, errors
|
||||
except Exception as e:
|
||||
errors.append(f"Unexpected error reading {file_path}: {e}")
|
||||
return False, errors
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
|
||||
def find_metadata_files(root_dir: Path) -> List[Path]:
|
||||
"""
|
||||
Find all metadata files in a directory tree.
|
||||
|
||||
Args:
|
||||
root_dir: Root directory to search
|
||||
|
||||
Returns:
|
||||
List of metadata file paths
|
||||
"""
|
||||
metadata_files = []
|
||||
|
||||
for pattern in ['**/*.yaml', '**/*.yml', '**/*.json']:
|
||||
for file_path in root_dir.glob(pattern):
|
||||
if file_path.name.startswith('meta.') or file_path.stem != file_path.name:
|
||||
metadata_files.append(file_path)
|
||||
|
||||
return metadata_files
|
||||
|
||||
|
||||
def main():
|
||||
"""Main validation function."""
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python validate_metadata.py <directory>")
|
||||
sys.exit(1)
|
||||
|
||||
root_dir = Path(sys.argv[1])
|
||||
|
||||
if not root_dir.exists():
|
||||
print(f"Error: Directory does not exist: {root_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
if not root_dir.is_dir():
|
||||
print(f"Error: Not a directory: {root_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Validating metadata files in: {root_dir}")
|
||||
print("-" * 50)
|
||||
|
||||
metadata_files = find_metadata_files(root_dir)
|
||||
|
||||
if not metadata_files:
|
||||
print("No metadata files found.")
|
||||
return
|
||||
|
||||
total_files = len(metadata_files)
|
||||
valid_files = 0
|
||||
|
||||
for file_path in metadata_files:
|
||||
is_valid, errors = validate_metadata_file(file_path)
|
||||
|
||||
if is_valid:
|
||||
print(f"✓ {file_path.relative_to(root_dir)}")
|
||||
valid_files += 1
|
||||
else:
|
||||
print(f"✗ {file_path.relative_to(root_dir)}")
|
||||
for error in errors:
|
||||
print(f" - {error}")
|
||||
|
||||
print("-" * 50)
|
||||
print(f"Summary: {valid_files}/{total_files} files valid")
|
||||
|
||||
if valid_files != total_files:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user