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
+49 -52
View File
@@ -1,79 +1,81 @@
import json
import pytest
from utils import metadata
import yaml
from gallery.utils import metadata
def test_load_metadata_file_yaml(tmp_path):
data = {'a': 1, 'b': 'test'}
yaml_path = tmp_path / 'meta.yaml'
data = {"a": 1, "b": "test"}
yaml_path = tmp_path / "meta.yaml"
yaml_path.write_text(yaml.dump(data))
result = metadata.load_metadata_file(yaml_path)
assert result == data
def test_load_metadata_file_json(tmp_path):
data = {'x': 42, 'y': 'hello'}
json_path = tmp_path / 'meta.json'
data = {"x": 42, "y": "hello"}
json_path = tmp_path / "meta.json"
json_path.write_text(json.dumps(data))
result = metadata.load_metadata_file(json_path)
assert result == data
def test_load_metadata_file_missing(tmp_path):
missing_path = tmp_path / 'nope.yaml'
missing_path = tmp_path / "nope.yaml"
result = metadata.load_metadata_file(missing_path)
assert result == {}
def test_load_metadata_file_yml_extension(tmp_path):
data = {'test': 'yml_format'}
yml_path = tmp_path / 'meta.yml'
data = {"test": "yml_format"}
yml_path = tmp_path / "meta.yml"
yml_path.write_text(yaml.dump(data))
result = metadata.load_metadata_file(yml_path)
assert result == data
def test_load_metadata_file_unknown_format(tmp_path):
txt_path = tmp_path / 'meta.txt'
txt_path.write_text('some text')
txt_path = tmp_path / "meta.txt"
txt_path.write_text("some text")
result = metadata.load_metadata_file(txt_path)
assert result == {}
def test_load_metadata_file_malformed_yaml(tmp_path):
yaml_path = tmp_path / 'bad.yaml'
yaml_path.write_text('invalid: yaml: content: [')
yaml_path = tmp_path / "bad.yaml"
yaml_path.write_text("invalid: yaml: content: [")
with pytest.raises(yaml.YAMLError):
metadata.load_metadata_file(yaml_path)
def test_load_metadata_file_malformed_json(tmp_path):
json_path = tmp_path / 'bad.json'
json_path = tmp_path / "bad.json"
json_path.write_text('{"invalid": json}')
with pytest.raises(json.JSONDecodeError):
metadata.load_metadata_file(json_path)
def test_load_folder_metadata_yaml(tmp_path):
data = {'folder': 'metadata'}
metadata_path = tmp_path / 'metadata.yaml'
data = {"folder": "metadata"}
metadata_path = tmp_path / "metadata.yaml"
metadata_path.write_text(yaml.dump(data))
result = metadata.load_folder_metadata(tmp_path)
assert result == data
def test_load_folder_metadata_yml(tmp_path):
data = {'folder': 'metadata_yml'}
metadata_path = tmp_path / 'metadata.yml'
data = {"folder": "metadata_yml"}
metadata_path = tmp_path / "metadata.yml"
metadata_path.write_text(yaml.dump(data))
result = metadata.load_folder_metadata(tmp_path)
assert result == data
def test_load_folder_metadata_json(tmp_path):
data = {'folder': 'metadata_json'}
metadata_path = tmp_path / 'metadata.json'
data = {"folder": "metadata_json"}
metadata_path = tmp_path / "metadata.json"
metadata_path.write_text(json.dumps(data))
result = metadata.load_folder_metadata(tmp_path)
assert result == data
@@ -85,21 +87,21 @@ def test_load_folder_metadata_missing(tmp_path):
def test_get_metadata_file_path_existing_yaml(tmp_path):
metadata_path = tmp_path / 'metadata.yaml'
metadata_path.write_text('test: data')
metadata_path = tmp_path / "metadata.yaml"
metadata_path.write_text("test: data")
result = metadata.get_metadata_file_path(tmp_path)
assert result == str(metadata_path)
def test_get_metadata_file_path_existing_yml(tmp_path):
metadata_path = tmp_path / 'metadata.yml'
metadata_path.write_text('test: data')
metadata_path = tmp_path / "metadata.yml"
metadata_path.write_text("test: data")
result = metadata.get_metadata_file_path(tmp_path)
assert result == str(metadata_path)
def test_get_metadata_file_path_existing_json(tmp_path):
metadata_path = tmp_path / 'metadata.json'
metadata_path = tmp_path / "metadata.json"
metadata_path.write_text('{"test": "data"}')
result = metadata.get_metadata_file_path(tmp_path)
assert result == str(metadata_path)
@@ -107,28 +109,27 @@ def test_get_metadata_file_path_existing_json(tmp_path):
def test_get_metadata_file_path_none_existing(tmp_path):
result = metadata.get_metadata_file_path(tmp_path)
expected = str(tmp_path / 'metadata.yaml')
expected = str(tmp_path / "metadata.yaml")
assert result == expected
def test_merge_metadata():
parent = {'project': 'Test', 'version': '1.0', 'author': 'Parent'}
child = {'experiment': 'A', 'version': '1.1'}
parent = {"project": "Test", "version": "1.0", "author": "Parent"}
child = {"experiment": "A", "version": "1.1"}
merged = metadata.merge_metadata(parent, child)
expected = {'project': 'Test', 'version': '1.1',
'author': 'Parent', 'experiment': 'A'}
expected = {"project": "Test", "version": "1.1", "author": "Parent", "experiment": "A"}
assert merged == expected
def test_merge_metadata_empty_parent():
parent = {}
child = {'experiment': 'A', 'version': '1.1'}
child = {"experiment": "A", "version": "1.1"}
merged = metadata.merge_metadata(parent, child)
assert merged == child
def test_merge_metadata_empty_child():
parent = {'project': 'Test', 'version': '1.0'}
parent = {"project": "Test", "version": "1.0"}
child = {}
merged = metadata.merge_metadata(parent, child)
assert merged == parent
@@ -136,35 +137,34 @@ def test_merge_metadata_empty_child():
def test_resolve_metadata_for_plot_with_specific_yaml(tmp_path):
# Create plot-specific metadata file
plot_path = tmp_path / 'test_plot.pdf'
plot_metadata_path = tmp_path / 'test_plot.yaml'
plot_metadata = {'specific': 'plot_data', 'override': 'plot_value'}
plot_path = tmp_path / "test_plot.pdf"
plot_metadata_path = tmp_path / "test_plot.yaml"
plot_metadata = {"specific": "plot_data", "override": "plot_value"}
plot_metadata_path.write_text(yaml.dump(plot_metadata))
inherited = {'general': 'data', 'override': 'inherited_value'}
inherited = {"general": "data", "override": "inherited_value"}
result = metadata.resolve_metadata_for_plot(plot_path, inherited)
expected = {'general': 'data',
'override': 'plot_value', 'specific': 'plot_data'}
expected = {"general": "data", "override": "plot_value", "specific": "plot_data"}
assert result == expected
def test_resolve_metadata_for_plot_with_specific_json(tmp_path):
plot_path = tmp_path / 'test_plot.pdf'
plot_metadata_path = tmp_path / 'test_plot.json'
plot_metadata = {'specific': 'plot_data_json'}
plot_path = tmp_path / "test_plot.pdf"
plot_metadata_path = tmp_path / "test_plot.json"
plot_metadata = {"specific": "plot_data_json"}
plot_metadata_path.write_text(json.dumps(plot_metadata))
inherited = {'general': 'data'}
inherited = {"general": "data"}
result = metadata.resolve_metadata_for_plot(plot_path, inherited)
expected = {'general': 'data', 'specific': 'plot_data_json'}
expected = {"general": "data", "specific": "plot_data_json"}
assert result == expected
def test_resolve_metadata_for_plot_no_specific(tmp_path):
plot_path = tmp_path / 'test_plot.pdf'
inherited = {'general': 'data', 'inherited': 'value'}
plot_path = tmp_path / "test_plot.pdf"
inherited = {"general": "data", "inherited": "value"}
result = metadata.resolve_metadata_for_plot(plot_path, inherited)
# Should return copy of inherited metadata
@@ -173,17 +173,14 @@ def test_resolve_metadata_for_plot_no_specific(tmp_path):
def test_save_metadata_cache(tmp_path):
cache_data = {
'plot1': {'title': 'Plot 1', 'author': 'Test'},
'plot2': {'title': 'Plot 2', 'experiment': 'B'}
}
cache_data = {"plot1": {"title": "Plot 1", "author": "Test"}, "plot2": {"title": "Plot 2", "experiment": "B"}}
metadata.save_metadata_cache(tmp_path, cache_data)
cache_file = tmp_path / 'meta_cache.json'
cache_file = tmp_path / "meta_cache.json"
assert cache_file.exists()
with cache_file.open('r') as f:
with cache_file.open("r") as f:
loaded_data = json.load(f)
assert loaded_data == cache_data
@@ -193,10 +190,10 @@ def test_save_metadata_cache_empty(tmp_path):
cache_data = {}
metadata.save_metadata_cache(tmp_path, cache_data)
cache_file = tmp_path / 'meta_cache.json'
cache_file = tmp_path / "meta_cache.json"
assert cache_file.exists()
with cache_file.open('r') as f:
with cache_file.open("r") as f:
loaded_data = json.load(f)
assert loaded_data == {}