[ENH]: Implement junifer reset to delete job/results file. #240
7 changed files with 286 additions and 4 deletions
1
docs/changes/newsfragments/240.feature
Normal file
1
docs/changes/newsfragments/240.feature
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Add ``junifer reset`` to reset storage and jobs directory by `Synchon Mandal`_
|
||||||
|
|
@ -21,6 +21,7 @@ from ..utils.logging import (
|
||||||
)
|
)
|
||||||
from .functions import collect as api_collect
|
from .functions import collect as api_collect
|
||||||
from .functions import queue as api_queue
|
from .functions import queue as api_queue
|
||||||
|
from .functions import reset as api_reset
|
||||||
from .functions import run as api_run
|
from .functions import run as api_run
|
||||||
from .parser import parse_yaml
|
from .parser import parse_yaml
|
||||||
from .utils import (
|
from .utils import (
|
||||||
|
|
@ -414,6 +415,43 @@ def selftest(subpkg: str) -> None:
|
||||||
click.secho("Failure.", fg="red")
|
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()
|
@cli.group()
|
||||||
def setup() -> None: # pragma: no cover
|
def setup() -> None: # pragma: no cover
|
||||||
"""Configure commands for Junifer."""
|
"""Configure commands for Junifer."""
|
||||||
|
|
|
||||||
|
|
@ -709,3 +709,40 @@ def _queue_slurm(
|
||||||
# logger.info(
|
# logger.info(
|
||||||
# f"SLURM job files created, to submit the job, run `{cmd}`"
|
# 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()
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,21 @@
|
||||||
# License: AGPL
|
# License: AGPL
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Tuple
|
from typing import Callable, List, Tuple
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
from ruamel.yaml import YAML
|
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
|
# Configure YAML class
|
||||||
|
|
@ -181,6 +189,105 @@ def test_multi_element_access(
|
||||||
assert read_elements == expected_list
|
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:
|
def test_wtf_short() -> None:
|
||||||
"""Test short version of wtf command."""
|
"""Test short version of wtf command."""
|
||||||
# Invoke wtf command
|
# Invoke wtf command
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import pytest
|
||||||
from ruamel.yaml import YAML
|
from ruamel.yaml import YAML
|
||||||
|
|
||||||
import junifer.testing.registry # noqa: F401
|
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.datagrabber.base import BaseDataGrabber
|
||||||
from junifer.pipeline.registry import build
|
from junifer.pipeline.registry import build
|
||||||
|
|
||||||
|
|
@ -867,3 +867,86 @@ def test_queue_condor_submission_fail(
|
||||||
def test_queue_slurm() -> None:
|
def test_queue_slurm() -> None:
|
||||||
"""Test job queueing in SLURM."""
|
"""Test job queueing in SLURM."""
|
||||||
pass
|
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()
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,6 @@ def test_DMCC13Benchmark(
|
||||||
dg.uri = URI
|
dg.uri = URI
|
||||||
|
|
||||||
with dg:
|
with dg:
|
||||||
# breakpoint()
|
|
||||||
# Get all elements
|
# Get all elements
|
||||||
all_elements = dg.get_elements()
|
all_elements = dg.get_elements()
|
||||||
# Get test element
|
# Get test element
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,11 @@ def register(step: str, name: str, klass: type) -> None:
|
||||||
klass : class
|
klass : class
|
||||||
Class to be registered.
|
Class to be registered.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValueError
|
||||||
|
If the ``step`` is invalid.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Verify step
|
# Verify step
|
||||||
if step not in _VALID_STEPS:
|
if step not in _VALID_STEPS:
|
||||||
|
|
@ -63,6 +68,11 @@ def get_step_names(step: str) -> List[str]:
|
||||||
list
|
list
|
||||||
List of registered function names.
|
List of registered function names.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValueError
|
||||||
|
If the ``step`` is invalid.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Verify step
|
# Verify step
|
||||||
if step not in _VALID_STEPS:
|
if step not in _VALID_STEPS:
|
||||||
|
|
@ -86,6 +96,11 @@ def get_class(step: str, name: str) -> type:
|
||||||
class
|
class
|
||||||
Registered function class.
|
Registered function class.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValueError
|
||||||
|
If the ``step`` or ``name`` is invalid.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Verify step
|
# Verify step
|
||||||
if step not in _VALID_STEPS:
|
if step not in _VALID_STEPS:
|
||||||
|
|
@ -123,6 +138,8 @@ def build(
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
------
|
------
|
||||||
|
RuntimeError
|
||||||
|
If there is a problem creating the instance.
|
||||||
ValueError
|
ValueError
|
||||||
If the created object with the given name is not an instance of the
|
If the created object with the given name is not an instance of the
|
||||||
base class.
|
base class.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue