[ENH]: Implement junifer reset to delete job/results file. #240

Merged
synchon merged 6 commits from feat/junifer-reset into main 2024-03-07 09:49:50 +00:00
7 changed files with 286 additions and 4 deletions

View file

@ -0,0 +1 @@
Add ``junifer reset`` to reset storage and jobs directory by `Synchon Mandal`_

View file

@ -21,6 +21,7 @@ from ..utils.logging import (
)
from .functions import collect as api_collect
from .functions import queue as api_queue
from .functions import reset as api_reset
from .functions import run as api_run
from .parser import parse_yaml
from .utils import (
@ -414,6 +415,43 @@ def selftest(subpkg: str) -> None:
click.secho("Failure.", fg="red")
@cli.command()
@click.argument(
"filepath",
type=click.Path(
exists=True, readable=True, dir_okay=False, path_type=pathlib.Path
),
)
@click.option(
"-v",
"--verbose",
type=click.UNPROCESSED,
callback=_validate_verbose,
default="info",
)
def reset(
filepath: click.Path,
verbose: Union[str, int],
) -> None:
"""Reset command for CLI.
\f
Parameters
----------
filepath : click.Path
The filepath to the configuration file.
verbose : click.Choice
The verbosity level: warning, info or debug (default "info").
"""
configure_logging(level=verbose)
# Parse YAML
config = parse_yaml(filepath)
# Perform operation
api_reset(config)
@cli.group()
def setup() -> None: # pragma: no cover
"""Configure commands for Junifer."""

View file

@ -709,3 +709,40 @@ def _queue_slurm(
# logger.info(
# f"SLURM job files created, to submit the job, run `{cmd}`"
# )
def reset(config: Dict) -> None:
"""Reset the storage and jobs directory.
Parameters
----------
config : dict
The configuration to be used for resetting.
"""
# Fetch storage
storage = config["storage"]
storage_uri = Path(storage["uri"])
logger.info(f"Deleting {storage_uri.resolve()!s}")
# Delete storage; will be str
if storage_uri.exists():
# Delete files in the directory
for file in storage_uri.iterdir():
file.unlink(missing_ok=True)
# Remove directory
storage_uri.parent.rmdir()
# Fetch job name (if present)
if config.get("queue") is not None:
queue = config["queue"]
job_dir = (
Path.cwd()
/ "junifer_jobs"
/ (queue.get("jobname") or "junifer_job")
)
logger.info(f"Deleting job directory at {job_dir.resolve()!s}")
if job_dir.exists():
# Remove files and directories
shutil.rmtree(job_dir)
# Remove directory
job_dir.parent.rmdir()

View file

@ -5,13 +5,21 @@
# License: AGPL
from pathlib import Path
from typing import List, Tuple
from typing import Callable, List, Tuple
import pytest
from click.testing import CliRunner
from ruamel.yaml import YAML
from junifer.api.cli import _parse_elements_file, collect, run, selftest, wtf
from junifer.api.cli import (
_parse_elements_file,
collect,
queue,
reset,
run,
selftest,
wtf,
)
# Configure YAML class
@ -181,6 +189,105 @@ def test_multi_element_access(
assert read_elements == expected_list
def test_queue(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test queue command.
Parameters
----------
tmp_path : pathlib.Path
The path to the test directory.
monkeypatch : pytest.MonkeyPatch
The pytest.MonkeyPatch object.
"""
with monkeypatch.context() as m:
m.chdir(tmp_path)
# Get test config
infile = Path(__file__).parent / "data" / "gmd_mean_htcondor.yaml"
# Read test config
contents = yaml.load(infile)
# Working directory
contents["workdir"] = str(tmp_path.resolve())
# Storage
contents["storage"]["uri"] = str((tmp_path / "out.sqlite").resolve())
# Write new test config
outfile = tmp_path / "in.yaml"
yaml.dump(contents, stream=outfile)
# Queue command arguments
queue_args = [
str(outfile.resolve()),
"--verbose",
"debug",
]
# Invoke queue command
queue_result = runner.invoke(queue, queue_args)
# Check
assert queue_result.exit_code == 0
@pytest.mark.parametrize(
"action, action_file",
[
(run, "gmd_mean.yaml"),
(queue, "gmd_mean_htcondor.yaml"),
],
)
def test_reset(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
action: Callable,
action_file: str,
) -> None:
"""Test reset command.
Parameters
----------
tmp_path : pathlib.Path
The path to the test directory.
monkeypatch : pytest.MonkeyPatch
The pytest.MonkeyPatch object.
action : callable
The parametrized action to perform.
action_file : str
The parametrized file for the action.
"""
with monkeypatch.context() as m:
m.chdir(tmp_path)
# Get test config
infile = Path(__file__).parent / "data" / action_file
# Read test config
contents = yaml.load(infile)
# Working directory
contents["workdir"] = str(tmp_path.resolve())
# Storage
contents["storage"]["uri"] = str((tmp_path / "out.sqlite").resolve())
# Write new test config
outfile = tmp_path / "in.yaml"
yaml.dump(contents, stream=outfile)
# Command arguments
action_args = [
str(outfile.resolve()),
"--verbose",
"debug",
]
# Invoke command
result = runner.invoke(action, action_args)
# Check
assert result.exit_code == 0
# Reset arguments
reset_args = [
str(outfile.resolve()),
"--verbose",
"debug",
]
# Run reset
reset_result = runner.invoke(reset, reset_args)
# Check
assert reset_result.exit_code == 0
def test_wtf_short() -> None:
"""Test short version of wtf command."""
# Invoke wtf command

View file

@ -14,7 +14,7 @@ import pytest
from ruamel.yaml import YAML
import junifer.testing.registry # noqa: F401
from junifer.api.functions import collect, queue, run
from junifer.api.functions import collect, queue, reset, run
from junifer.datagrabber.base import BaseDataGrabber
from junifer.pipeline.registry import build
@ -867,3 +867,86 @@ def test_queue_condor_submission_fail(
def test_queue_slurm() -> None:
"""Test job queueing in SLURM."""
pass
def test_reset_run(tmp_path: Path) -> None:
"""Test reset function for run.
Parameters
----------
tmp_path : pathlib.Path
The path to the test directory.
"""
# Create storage
storage["uri"] = tmp_path / "test_reset_run.sqlite" # type: ignore
# Run operation to generate files
run(
workdir=tmp_path,
datagrabber=datagrabber,
markers=markers,
storage=storage,
elements=["sub-01"],
)
# Reset operation
reset(config={"storage": storage})
assert not Path(storage["uri"]).exists()
@pytest.mark.parametrize(
"job_name",
(
"job",
None,
),
)
def test_reset_queue(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, job_name: str
) -> None:
"""Test reset function for queue.
Parameters
----------
tmp_path : pathlib.Path
The path to the test directory.
monkeypatch : pytest.MonkeyPatch
The pytest.MonkeyPatch object.
job_name : str
The parametrized job name.
"""
with monkeypatch.context() as m:
m.chdir(tmp_path)
# Create storage
storage["uri"] = "test_reset_queue.sqlite"
# Set job name
if job_name is None:
job_name = "junifer_job"
# Queue operation to generate files
queue(
config={
"with": "junifer.testing.registry",
"workdir": str(tmp_path.resolve()),
"datagrabber": datagrabber,
"markers": markers,
"storage": storage,
"env": {
"kind": "conda",
"name": "junifer",
},
"mem": "8G",
},
kind="HTCondor",
jobname=job_name,
)
# Reset operation
reset(
config={
"storage": storage,
"queue": {"jobname": job_name},
}
)
assert not Path(storage["uri"]).exists()
assert not (tmp_path / "junifer_jobs" / job_name).exists()

View file

@ -81,7 +81,6 @@ def test_DMCC13Benchmark(
dg.uri = URI
with dg:
# breakpoint()
# Get all elements
all_elements = dg.get_elements()
# Get test element

View file

@ -41,6 +41,11 @@ def register(step: str, name: str, klass: type) -> None:
klass : class
Class to be registered.
Raises
------
ValueError
If the ``step`` is invalid.
"""
# Verify step
if step not in _VALID_STEPS:
@ -63,6 +68,11 @@ def get_step_names(step: str) -> List[str]:
list
List of registered function names.
Raises
------
ValueError
If the ``step`` is invalid.
"""
# Verify step
if step not in _VALID_STEPS:
@ -86,6 +96,11 @@ def get_class(step: str, name: str) -> type:
class
Registered function class.
Raises
------
ValueError
If the ``step`` or ``name`` is invalid.
"""
# Verify step
if step not in _VALID_STEPS:
@ -123,6 +138,8 @@ def build(
Raises
------
RuntimeError
If there is a problem creating the instance.
ValueError
If the created object with the given name is not an instance of the
base class.