3b9d1ef1a8
- 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>
187 lines
5.7 KiB
Python
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
|