Files
lars 3b9d1ef1a8 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>
2026-07-22 15:29:55 +02:00

187 lines
5.7 KiB
Python

"""Gallery building and rendering logic."""
import shutil
from pathlib import Path
from typing import Any, Dict, Optional, Union
from jinja2 import Environment, FileSystemLoader, Template
from gallery.config import GalleryConfig
from gallery.utils.datetime_utils import (
datetime_from_timestamp,
strftime_filter,
)
from gallery.utils.metadata import (
load_folder_metadata,
merge_metadata,
save_metadata_cache,
)
from gallery.utils.processing import (
process_plot_files,
render_gallery_page,
)
def get_template(template_dir: Optional[Union[Path, str]] = None):
"""
Get the Jinja2 template for gallery rendering.
Args:
template_dir: Path to template directory. If None,
uses package default.
Returns:
Jinja2 Template object
"""
if isinstance(template_dir, str):
template_dir = Path(template_dir)
if template_dir is None:
# Use package-included template
import gallery
gallery_module_path = Path(gallery.__file__).parent
template_dir = gallery_module_path / "templates"
env = Environment(loader=FileSystemLoader(str(template_dir)))
env.filters["datetime_from_timestamp"] = datetime_from_timestamp
env.filters["strftime"] = strftime_filter
return env.get_template("gallery.html")
def build_gallery(
config: GalleryConfig,
source_dir: Path,
web_dir: Path,
template: Optional[Template] = None,
relative_path: Optional[Path] = None,
inherited_metadata: Optional[Dict[str, Any]] = None,
) -> None:
"""
Recursively build gallery structure from source directory.
Processes all PDF files in the source directory, converts them to PNG,
copies both to the web directory, and generates index.html files with
navigation and thumbnails. Includes metadata support.
Args:
config: Gallery configuration object
template: Jinja2 template for rendering
source_dir: Source directory containing PDF files
web_dir: Target web directory for gallery output
relative_path: Relative path from gallery root (for navigation)
inherited_metadata: Metadata inherited from parent directories
"""
if not template:
template = Template("./templates/gallery.html")
if relative_path is None:
relative_path = Path(".")
if inherited_metadata is None:
inherited_metadata = {}
folder_metadata = load_folder_metadata(source_dir)
current_metadata = merge_metadata(inherited_metadata, folder_metadata)
# Find all plot files (both PDF and HTML)
pdf_files = list(source_dir.glob("*.pdf"))
html_files = list(source_dir.glob("*.html"))
plot_files = pdf_files + html_files
items = []
plot_metadata_cache = {}
# Process all plot files (PDFs and HTMLs)
for plot_file in plot_files:
item = process_plot_files(
config=config,
plot_file=plot_file,
web_dir=web_dir,
current_metadata=current_metadata,
)
items.append(item)
plot_metadata_cache[plot_file.stem] = item["metadata"]
if config.cache_enabled:
save_metadata_cache(web_dir, plot_metadata_cache)
# Process subdirectories
subdirs = [d for d in source_dir.iterdir() if d.is_dir()]
subdir_names = []
for subdir in subdirs:
subdir_web = web_dir / subdir.name
subdir_web.mkdir(exist_ok=True)
subdir_relative = relative_path / subdir.name
build_gallery(
config,
subdir,
subdir_web,
template,
subdir_relative,
current_metadata if config.inherit_from_parent else {},
)
subdir_names.append(subdir.name)
render_gallery_page(
config=config,
template=template,
web_dir=web_dir,
items=items,
subdirs=subdir_names,
relative_path=relative_path,
metadata=current_metadata,
)
def copy_assets(config: GalleryConfig, assets_src: Optional[Path] = None, verbose: bool = False) -> bool:
"""
Copy assets to the web directory.
Args:
config: Gallery configuration object
assets_src: Path to assets source. If None, uses package default.
verbose: Whether to print status messages
Returns:
True if successful, False otherwise
"""
try:
if assets_src is None:
# Use package-included assets
import gallery
gallery_module_path = Path(gallery.__file__).parent
assets_src = gallery_module_path / "assets"
if not assets_src.exists():
if verbose:
print(f"Warning: Assets directory {assets_src} not found")
return False
gallery_root = Path(config.web_folder) / config.plot_root
assets_dst = gallery_root.parent / "assets"
# Use the newest mtime across all source asset files as the staleness check,
# so any change to any JS/CSS file triggers a redeploy.
sentinel_dst = assets_dst / "css" / "main.css"
newest_src_mtime = max(
(f.stat().st_mtime for f in assets_src.rglob("*") if f.is_file()),
default=0,
)
dst_mtime = sentinel_dst.stat().st_mtime if sentinel_dst.exists() else 0
if not assets_dst.exists() or newest_src_mtime > (dst_mtime + 30):
if assets_dst.exists():
shutil.rmtree(assets_dst)
shutil.copytree(assets_src, assets_dst)
if verbose:
print(f"Updated assets from {assets_src} to {assets_dst}")
return True
except Exception as e:
if verbose:
print(f"Warning: Could not copy assets: {e}")
return False