Rewrite CI to lint/typecheck/audit/test only; add HPC deployment path
- Replace the Docker build/publish CI stages with ruff (lint + format check), ty (type check), pip-audit, and pytest run directly against python:3.11-slim; Docker remains for manual/server deployment only. - Swap black/pylint/mypy for ruff/ty across pyproject.toml, and fix every resulting lint, format, and type diagnostic in gallery/ and plotstyle/. - Fix tests broken/stale from before the package restructuring: wrong `utils.*` import paths, mock patch targets pointed at the wrong module, and PDF-conversion tests still assuming ImageMagick instead of the current PyMuPDF-first path. Drop test_container.py (obsolete Docker-container smoke tests, fully superseded elsewhere). - Add a plain-venv + systemd --user timer deployment path (deploy/systemd/) for HPC login nodes without a Docker daemon, where public_html is already served by existing infrastructure. - Document both in CLAUDE.md, including running CI's checks locally before committing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+38
-37
@@ -6,86 +6,87 @@ 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
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List
|
||||
from typing import List
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
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:
|
||||
with file_path.open("r", encoding="utf-8") as f:
|
||||
suffix_lower = file_path.suffix.lower()
|
||||
if suffix_lower in ['.yaml', '.yml']:
|
||||
if suffix_lower in [".yaml", ".yml"]:
|
||||
data = yaml.safe_load(f)
|
||||
elif suffix_lower == '.json':
|
||||
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):
|
||||
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):
|
||||
|
||||
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):
|
||||
|
||||
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 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:
|
||||
if file_path.name.startswith("meta.") or file_path.stem != file_path.name:
|
||||
metadata_files.append(file_path)
|
||||
|
||||
|
||||
return metadata_files
|
||||
|
||||
|
||||
@@ -94,32 +95,32 @@ def main():
|
||||
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
|
||||
@@ -127,10 +128,10 @@ def main():
|
||||
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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user