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:
2026-07-22 15:29:55 +02:00
parent 4ba321b327
commit 3b9d1ef1a8
27 changed files with 752 additions and 915 deletions
+27 -57
View File
@@ -6,19 +6,19 @@ Provides the primary entry point for programmatic gallery generation.
import shutil
from pathlib import Path
from typing import Union, List, Dict, Any
from typing import Any, Dict, List, Optional, Union, cast
from gallery.builder import build_gallery, copy_assets, get_template
from gallery.config import GalleryConfig, GallerySource
from gallery.builder import get_template, build_gallery, copy_assets
def generate(
config: Union[GalleryConfig, str, Path] = None,
web_folder: Union[str, Path] = None,
sources: List[Union[GallerySource, Dict[str, Any]]] = None,
config: Optional[Union[GalleryConfig, str, Path]] = None,
web_folder: Optional[Union[str, Path]] = None,
sources: Optional[List[Union[GallerySource, Dict[str, Any]]]] = None,
clean_first: bool = False,
verbose: bool = False,
source_to_update: GallerySource = None,
source_to_update: Optional[GallerySource] = None,
) -> bool:
"""
Generate a scientific gallery from plot sources.
@@ -79,20 +79,11 @@ def generate(
if isinstance(config, (str, Path)):
config = GalleryConfig.from_yaml(config)
elif not isinstance(config, GalleryConfig):
raise TypeError(
f"config must be GalleryConfig, str, or Path, "
f"got {type(config)}"
)
raise TypeError(f"config must be GalleryConfig, str, or Path, got {type(config)}")
else:
if web_folder is None or sources is None:
raise ValueError(
"Either config or both web_folder and sources "
"must be provided"
)
config = GalleryConfig(
web_folder=web_folder,
sources=sources or []
)
raise ValueError("Either config or both web_folder and sources must be provided")
config = GalleryConfig(web_folder=web_folder, sources=sources or [])
# Validate configuration
if not config.sources:
@@ -104,10 +95,7 @@ def generate(
web_folder_path = Path(config.web_folder)
if not _is_writable(web_folder_path):
if verbose:
print(
f"Error: Cannot write to web_folder: "
f"{config.web_folder}"
)
print(f"Error: Cannot write to web_folder: {config.web_folder}")
return False
# Create gallery root directory
@@ -127,17 +115,12 @@ def generate(
source_subdir = gallery_root / source_to_update.name
if source_subdir.exists():
if verbose:
print(
f"Updating source directory {source_to_update.name}..."
)
print(f"Updating source directory {source_to_update.name}...")
try:
shutil.rmtree(source_subdir)
except Exception as e:
if verbose:
print(
f"Warning: Could not clean source subdirectory "
f"{source_subdir}: {e}"
)
print(f"Warning: Could not clean source subdirectory {source_subdir}: {e}")
return False
try:
@@ -162,8 +145,10 @@ def generate(
return False
# Process sources
# config.sources is always List[GallerySource] after GalleryConfig.__post_init__ normalizes it.
source_subdirs = []
for source in config.sources:
source = cast(GallerySource, source)
# Skip sources not matching the update target (if specified)
if source_to_update and source.name != source_to_update.name:
# Still include them in the index if they exist
@@ -178,10 +163,7 @@ def generate(
# Validate source exists
if not source_path.exists():
if verbose:
print(
f"Warning: Source {source.path} does not exist. "
f"Skipping."
)
print(f"Warning: Source {source.path} does not exist. Skipping.")
continue
source_web_dir = gallery_root / source.name
@@ -189,47 +171,37 @@ def generate(
source_web_dir.mkdir(parents=True, exist_ok=True)
except Exception as e:
if verbose:
print(
f"Warning: Could not create directory "
f"{source_web_dir}: {e}"
)
print(f"Warning: Could not create directory {source_web_dir}: {e}")
continue
source_subdirs.append(source.name)
# Process source
if source_path.is_file() and source_path.suffix == '.pdf':
if source_path.is_file() and source_path.suffix == ".pdf":
# Single PDF file
from gallery.utils.processing import process_plot_files
item = process_plot_files(
config=config,
plot_file=source_path,
web_dir=source_web_dir,
)
from gallery.utils.processing import render_gallery_page
render_gallery_page(
config=config,
template=template,
web_dir=source_web_dir,
items=[item],
subdirs=[],
relative_path=Path(source.name)
relative_path=Path(source.name),
)
elif source_path.is_dir():
# Directory of plots
build_gallery(
config,
source_path,
source_web_dir,
template,
Path(source.name)
)
build_gallery(config, source_path, source_web_dir, template, Path(source.name))
else:
if verbose:
print(
f"Warning: Source {source.path} is neither a "
f"directory nor a PDF file. Skipping."
)
print(f"Warning: Source {source.path} is neither a directory nor a PDF file. Skipping.")
continue
if verbose:
@@ -237,15 +209,13 @@ def generate(
except Exception as e:
if verbose:
print(
f"Warning: Error processing source "
f"{source.name}: {e}"
)
print(f"Warning: Error processing source {source.name}: {e}")
continue
# Render gallery root index
try:
from gallery.utils.processing import render_gallery_page
render_gallery_page(
config=config,
template=template,
@@ -253,7 +223,7 @@ def generate(
items=[],
subdirs=source_subdirs,
relative_path=Path("."),
title="Gallery Root"
title="Gallery Root",
)
except Exception as e:
if verbose:
@@ -277,8 +247,8 @@ def generate(
def _count_gallery_plots(gallery_root: Path) -> int:
"""Recursively count plot files (PDFs and HTMLs, excluding index.html) in the gallery output."""
count = 0
for f in gallery_root.rglob('*'):
if f.is_file() and f.suffix.lower() in ('.pdf', '.html') and f.name != 'index.html':
for f in gallery_root.rglob("*"):
if f.is_file() and f.suffix.lower() in (".pdf", ".html") and f.name != "index.html":
count += 1
return count