hpc-runner 0.1.1__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hpc_runner/_version.py +2 -2
- hpc_runner/cli/cancel.py +1 -1
- hpc_runner/cli/config.py +2 -2
- hpc_runner/cli/main.py +17 -13
- hpc_runner/cli/monitor.py +30 -0
- hpc_runner/cli/run.py +223 -67
- hpc_runner/cli/status.py +6 -5
- hpc_runner/core/__init__.py +30 -0
- hpc_runner/core/descriptors.py +87 -33
- hpc_runner/core/exceptions.py +9 -0
- hpc_runner/core/job.py +272 -93
- hpc_runner/core/job_info.py +104 -0
- hpc_runner/core/result.py +4 -0
- hpc_runner/schedulers/base.py +148 -30
- hpc_runner/schedulers/detection.py +22 -4
- hpc_runner/schedulers/local/scheduler.py +119 -2
- hpc_runner/schedulers/sge/args.py +161 -94
- hpc_runner/schedulers/sge/parser.py +106 -13
- hpc_runner/schedulers/sge/scheduler.py +727 -171
- hpc_runner/schedulers/sge/templates/batch.sh.j2 +82 -0
- hpc_runner/schedulers/sge/templates/interactive.sh.j2 +78 -0
- hpc_runner/tui/__init__.py +5 -0
- hpc_runner/tui/app.py +436 -0
- hpc_runner/tui/components/__init__.py +17 -0
- hpc_runner/tui/components/detail_panel.py +187 -0
- hpc_runner/tui/components/filter_bar.py +174 -0
- hpc_runner/tui/components/filter_popup.py +345 -0
- hpc_runner/tui/components/job_table.py +260 -0
- hpc_runner/tui/providers/__init__.py +5 -0
- hpc_runner/tui/providers/jobs.py +197 -0
- hpc_runner/tui/screens/__init__.py +7 -0
- hpc_runner/tui/screens/confirm.py +67 -0
- hpc_runner/tui/screens/job_details.py +210 -0
- hpc_runner/tui/screens/log_viewer.py +170 -0
- hpc_runner/tui/snapshot.py +153 -0
- hpc_runner/tui/styles/monitor.tcss +567 -0
- hpc_runner-0.2.1.dist-info/METADATA +285 -0
- hpc_runner-0.2.1.dist-info/RECORD +56 -0
- hpc_runner/schedulers/sge/templates/job.sh.j2 +0 -39
- hpc_runner-0.1.1.dist-info/METADATA +0 -46
- hpc_runner-0.1.1.dist-info/RECORD +0 -38
- {hpc_runner-0.1.1.dist-info → hpc_runner-0.2.1.dist-info}/WHEEL +0 -0
- {hpc_runner-0.1.1.dist-info → hpc_runner-0.2.1.dist-info}/entry_points.txt +0 -0
hpc_runner/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.2.1'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 2, 1)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
hpc_runner/cli/cancel.py
CHANGED
|
@@ -10,7 +10,7 @@ console = Console()
|
|
|
10
10
|
|
|
11
11
|
@click.command()
|
|
12
12
|
@click.argument("job_id")
|
|
13
|
-
@click.option("--force",
|
|
13
|
+
@click.option("--force", is_flag=True, help="Cancel without confirmation")
|
|
14
14
|
@pass_context
|
|
15
15
|
def cancel(
|
|
16
16
|
ctx: Context,
|
hpc_runner/cli/config.py
CHANGED
|
@@ -21,7 +21,7 @@ def config_cmd() -> None:
|
|
|
21
21
|
@pass_context
|
|
22
22
|
def show(ctx: Context) -> None:
|
|
23
23
|
"""Show current configuration."""
|
|
24
|
-
from hpc_runner.core.config import find_config_file
|
|
24
|
+
from hpc_runner.core.config import find_config_file
|
|
25
25
|
|
|
26
26
|
config_path = ctx.config_path or find_config_file()
|
|
27
27
|
|
|
@@ -45,7 +45,7 @@ def show(ctx: Context) -> None:
|
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
@config_cmd.command("init")
|
|
48
|
-
@click.option("--global", "
|
|
48
|
+
@click.option("--global", "global_config", is_flag=True, help="Create in ~/.config/hpc-tools/")
|
|
49
49
|
@pass_context
|
|
50
50
|
def init(ctx: Context, global_config: bool) -> None:
|
|
51
51
|
"""Create a new configuration file."""
|
hpc_runner/cli/main.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Main CLI entry point using rich-click."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Optional
|
|
5
4
|
|
|
6
5
|
import rich_click as click
|
|
7
6
|
from rich.console import Console
|
|
@@ -15,52 +14,57 @@ console = Console()
|
|
|
15
14
|
# Context object to pass state between commands
|
|
16
15
|
class Context:
|
|
17
16
|
def __init__(self) -> None:
|
|
18
|
-
self.config_path:
|
|
19
|
-
self.scheduler:
|
|
17
|
+
self.config_path: Path | None = None
|
|
18
|
+
self.scheduler: str | None = None
|
|
20
19
|
self.verbose: bool = False
|
|
21
20
|
|
|
22
21
|
pass_context = click.make_pass_decorator(Context, ensure=True)
|
|
23
22
|
|
|
24
23
|
|
|
25
|
-
@click.group()
|
|
24
|
+
@click.group(context_settings={"help_option_names": ["-h", "--help"]})
|
|
26
25
|
@click.option(
|
|
27
|
-
"--config",
|
|
26
|
+
"--config",
|
|
28
27
|
type=click.Path(exists=True, path_type=Path),
|
|
29
28
|
help="Path to configuration file",
|
|
30
29
|
)
|
|
31
30
|
@click.option(
|
|
32
|
-
"--scheduler",
|
|
31
|
+
"--scheduler",
|
|
33
32
|
type=str,
|
|
34
33
|
help="Force scheduler (sge, slurm, pbs, local)",
|
|
35
34
|
)
|
|
36
35
|
@click.option(
|
|
37
|
-
"--verbose",
|
|
36
|
+
"--verbose",
|
|
38
37
|
is_flag=True,
|
|
39
38
|
help="Enable verbose output",
|
|
40
39
|
)
|
|
41
40
|
@click.version_option(package_name="hpc-runner")
|
|
42
41
|
@pass_context
|
|
43
|
-
def cli(ctx: Context, config:
|
|
42
|
+
def cli(ctx: Context, config: Path | None, scheduler: str | None, verbose: bool) -> None:
|
|
44
43
|
"""HPC job submission tool.
|
|
45
44
|
|
|
46
45
|
Submit and manage jobs across different HPC schedulers (SGE, Slurm, PBS)
|
|
47
46
|
with a unified interface.
|
|
47
|
+
|
|
48
|
+
Any unrecognized short options are passed directly to the underlying
|
|
49
|
+
scheduler, allowing use of native flags like -N, -n, -q, etc.
|
|
48
50
|
"""
|
|
49
51
|
ctx.config_path = config
|
|
50
52
|
ctx.scheduler = scheduler
|
|
51
53
|
ctx.verbose = verbose
|
|
52
54
|
|
|
53
55
|
|
|
54
|
-
# Import and register subcommands
|
|
55
|
-
from hpc_runner.cli.
|
|
56
|
-
from hpc_runner.cli.
|
|
57
|
-
from hpc_runner.cli.
|
|
58
|
-
from hpc_runner.cli.
|
|
56
|
+
# Import and register subcommands (must be after cli is defined to avoid circular imports)
|
|
57
|
+
from hpc_runner.cli.cancel import cancel # noqa: E402
|
|
58
|
+
from hpc_runner.cli.config import config_cmd # noqa: E402
|
|
59
|
+
from hpc_runner.cli.monitor import monitor # noqa: E402
|
|
60
|
+
from hpc_runner.cli.run import run # noqa: E402
|
|
61
|
+
from hpc_runner.cli.status import status # noqa: E402
|
|
59
62
|
|
|
60
63
|
cli.add_command(run)
|
|
61
64
|
cli.add_command(status)
|
|
62
65
|
cli.add_command(cancel)
|
|
63
66
|
cli.add_command(config_cmd, name="config")
|
|
67
|
+
cli.add_command(monitor)
|
|
64
68
|
|
|
65
69
|
|
|
66
70
|
def main() -> None:
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""CLI command for launching the interactive job monitor."""
|
|
2
|
+
|
|
3
|
+
import rich_click as click
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@click.command()
|
|
7
|
+
@click.option(
|
|
8
|
+
"--refresh",
|
|
9
|
+
"-r",
|
|
10
|
+
default=10,
|
|
11
|
+
type=int,
|
|
12
|
+
help="Auto-refresh interval in seconds",
|
|
13
|
+
)
|
|
14
|
+
def monitor(refresh: int) -> None:
|
|
15
|
+
"""Launch interactive job monitor TUI.
|
|
16
|
+
|
|
17
|
+
Opens a terminal UI for monitoring HPC jobs across schedulers.
|
|
18
|
+
Shows active and completed jobs with filtering and search.
|
|
19
|
+
|
|
20
|
+
\b
|
|
21
|
+
Keyboard shortcuts:
|
|
22
|
+
q Quit
|
|
23
|
+
r Manual refresh
|
|
24
|
+
u Toggle user filter (me/all)
|
|
25
|
+
Tab Switch tabs
|
|
26
|
+
"""
|
|
27
|
+
from hpc_runner.tui import HpcMonitorApp
|
|
28
|
+
|
|
29
|
+
app = HpcMonitorApp(refresh_interval=refresh)
|
|
30
|
+
app.run()
|
hpc_runner/cli/run.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Run command - submit jobs to the scheduler."""
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
import rich_click as click
|
|
6
6
|
from rich.console import Console
|
|
@@ -9,128 +9,284 @@ from rich.syntax import Syntax
|
|
|
9
9
|
|
|
10
10
|
from hpc_runner.cli.main import Context, pass_context
|
|
11
11
|
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from hpc_runner.core.job import Job
|
|
14
|
+
|
|
12
15
|
console = Console()
|
|
13
16
|
|
|
14
17
|
|
|
15
|
-
@click.command(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@click.
|
|
22
|
-
|
|
23
|
-
@click.option("--
|
|
24
|
-
@click.option("--
|
|
25
|
-
@click.option("--
|
|
26
|
-
@click.option("--
|
|
27
|
-
@click.option("--
|
|
28
|
-
@click.option("--
|
|
18
|
+
@click.command(
|
|
19
|
+
context_settings={
|
|
20
|
+
"ignore_unknown_options": True,
|
|
21
|
+
"allow_interspersed_args": True,
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
@click.argument("args", nargs=-1, type=click.UNPROCESSED)
|
|
25
|
+
# All hpc-runner options are long-form only
|
|
26
|
+
@click.option("--job-name", "job_name", help="Job name")
|
|
27
|
+
@click.option("--cpu", type=int, help="Number of CPUs")
|
|
28
|
+
@click.option("--mem", help="Memory requirement (e.g., 16G)")
|
|
29
|
+
@click.option("--time", "time_limit", help="Time limit (e.g., 4:00:00)")
|
|
30
|
+
@click.option("--queue", help="Queue/partition name")
|
|
31
|
+
@click.option("--nodes", type=int, help="Number of nodes (MPI jobs)")
|
|
32
|
+
@click.option("--ntasks", type=int, help="Number of tasks (MPI jobs)")
|
|
33
|
+
@click.option("--directory", type=click.Path(exists=True), help="Working directory")
|
|
34
|
+
@click.option("--job-type", "job_type", help="Job type from config")
|
|
35
|
+
@click.option("--module", "modules", multiple=True, help="Modules to load (repeatable)")
|
|
36
|
+
@click.option("--stderr", help="Separate stderr file (default: merged)")
|
|
37
|
+
@click.option("--output", help="Stdout file path pattern")
|
|
38
|
+
@click.option("--array", help="Array job specification (e.g., 1-100)")
|
|
39
|
+
@click.option("--depend", help="Job dependency specification")
|
|
40
|
+
@click.option("--inherit-env/--no-inherit-env", "inherit_env", default=True, help="Inherit environment variables")
|
|
41
|
+
@click.option("--interactive", is_flag=True, help="Run interactively (srun/qrsh)")
|
|
42
|
+
@click.option("--local", is_flag=True, help="Run locally (no scheduler)")
|
|
43
|
+
@click.option("--dry-run", "dry_run", is_flag=True, help="Show what would be submitted")
|
|
44
|
+
@click.option("--wait", is_flag=True, help="Wait for job completion")
|
|
45
|
+
@click.option("--keep-script", "keep_script", is_flag=True, help="Keep job script for debugging")
|
|
29
46
|
@pass_context
|
|
30
47
|
def run(
|
|
31
48
|
ctx: Context,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
cpu:
|
|
35
|
-
mem:
|
|
36
|
-
|
|
37
|
-
queue:
|
|
49
|
+
args: tuple[str, ...],
|
|
50
|
+
job_name: str | None,
|
|
51
|
+
cpu: int | None,
|
|
52
|
+
mem: str | None,
|
|
53
|
+
time_limit: str | None,
|
|
54
|
+
queue: str | None,
|
|
55
|
+
nodes: int | None,
|
|
56
|
+
ntasks: int | None,
|
|
57
|
+
directory: str | None,
|
|
58
|
+
job_type: str | None,
|
|
59
|
+
modules: tuple[str, ...],
|
|
60
|
+
stderr: str | None,
|
|
61
|
+
output: str | None,
|
|
62
|
+
array: str | None,
|
|
63
|
+
depend: str | None,
|
|
64
|
+
inherit_env: bool,
|
|
38
65
|
interactive: bool,
|
|
39
66
|
local: bool,
|
|
40
|
-
job_type: Optional[str],
|
|
41
|
-
module: Tuple[str, ...],
|
|
42
|
-
raw: Tuple[str, ...],
|
|
43
67
|
dry_run: bool,
|
|
44
|
-
|
|
68
|
+
wait: bool,
|
|
69
|
+
keep_script: bool,
|
|
45
70
|
) -> None:
|
|
46
71
|
"""Submit a job to the scheduler.
|
|
47
72
|
|
|
48
73
|
COMMAND is the command to execute. Use quotes for complex commands:
|
|
49
74
|
|
|
75
|
+
\b
|
|
50
76
|
hpc run "make -j8 all"
|
|
51
|
-
|
|
52
77
|
hpc run python script.py --arg value
|
|
78
|
+
|
|
79
|
+
Any unrecognized options starting with '-' are passed directly to the
|
|
80
|
+
underlying scheduler. This allows using native flags:
|
|
81
|
+
|
|
82
|
+
\b
|
|
83
|
+
hpc run -N 4 -n 16 "mpirun ./sim" # Slurm nodes/tasks
|
|
84
|
+
hpc run -q batch.q -l gpu=2 "train" # SGE queue/resources
|
|
53
85
|
"""
|
|
86
|
+
import shlex
|
|
87
|
+
|
|
54
88
|
from hpc_runner.core.job import Job
|
|
55
89
|
from hpc_runner.schedulers import get_scheduler
|
|
56
90
|
|
|
91
|
+
# Parse args into command and scheduler passthrough args
|
|
92
|
+
command_parts, scheduler_args = _parse_args(args)
|
|
93
|
+
|
|
94
|
+
if not command_parts:
|
|
95
|
+
raise click.UsageError("Command is required")
|
|
96
|
+
|
|
97
|
+
# Use shlex.join to preserve quoting for args with spaces/special chars
|
|
98
|
+
cmd_str = shlex.join(command_parts)
|
|
99
|
+
|
|
57
100
|
# Get scheduler
|
|
58
101
|
scheduler_name = "local" if local else ctx.scheduler
|
|
59
102
|
scheduler = get_scheduler(scheduler_name)
|
|
60
103
|
|
|
61
|
-
# Build command string
|
|
62
|
-
cmd_str = " ".join(command)
|
|
63
|
-
|
|
64
104
|
# Create job from config or parameters
|
|
65
105
|
if job_type:
|
|
66
106
|
job = Job.from_config(job_type, command=cmd_str)
|
|
67
107
|
else:
|
|
68
108
|
job = Job(command=cmd_str)
|
|
69
109
|
|
|
70
|
-
#
|
|
71
|
-
if
|
|
72
|
-
job.name =
|
|
110
|
+
# Apply CLI overrides
|
|
111
|
+
if job_name:
|
|
112
|
+
job.name = job_name
|
|
73
113
|
if cpu:
|
|
74
114
|
job.cpu = cpu
|
|
75
115
|
if mem:
|
|
76
116
|
job.mem = mem
|
|
77
|
-
if
|
|
78
|
-
job.time =
|
|
117
|
+
if time_limit:
|
|
118
|
+
job.time = time_limit
|
|
79
119
|
if queue:
|
|
80
120
|
job.queue = queue
|
|
81
|
-
if
|
|
82
|
-
job.
|
|
83
|
-
if
|
|
84
|
-
job.
|
|
121
|
+
if nodes:
|
|
122
|
+
job.nodes = nodes
|
|
123
|
+
if ntasks:
|
|
124
|
+
job.tasks = ntasks
|
|
125
|
+
if directory:
|
|
126
|
+
job.workdir = directory
|
|
127
|
+
if modules:
|
|
128
|
+
job.modules = list(modules)
|
|
85
129
|
if stderr:
|
|
86
130
|
job.stderr = stderr
|
|
131
|
+
if output:
|
|
132
|
+
job.stdout = output
|
|
133
|
+
if depend:
|
|
134
|
+
job.dependency = depend
|
|
135
|
+
|
|
136
|
+
# inherit_env is always set (has a default), so always apply it
|
|
137
|
+
job.inherit_env = inherit_env
|
|
138
|
+
|
|
139
|
+
# Add scheduler passthrough args
|
|
140
|
+
if scheduler_args:
|
|
141
|
+
job.raw_args = scheduler_args
|
|
142
|
+
if ctx.verbose:
|
|
143
|
+
console.print(f"[dim]Scheduler passthrough: {' '.join(scheduler_args)}[/dim]")
|
|
144
|
+
|
|
145
|
+
# Handle array jobs
|
|
146
|
+
if array:
|
|
147
|
+
_handle_array_job(job, array, scheduler, dry_run, ctx.verbose)
|
|
148
|
+
return
|
|
87
149
|
|
|
88
150
|
if dry_run:
|
|
89
|
-
_show_dry_run(job, scheduler)
|
|
151
|
+
_show_dry_run(job, scheduler, scheduler_args, interactive=interactive)
|
|
90
152
|
return
|
|
91
153
|
|
|
92
|
-
# Submit
|
|
93
|
-
result = scheduler.submit(job, interactive=interactive)
|
|
154
|
+
# Submit the job
|
|
155
|
+
result = scheduler.submit(job, interactive=interactive, keep_script=keep_script)
|
|
94
156
|
|
|
95
157
|
if interactive:
|
|
96
158
|
if result.returncode == 0:
|
|
97
|
-
console.print(
|
|
159
|
+
console.print("[green]Job completed successfully[/green]")
|
|
98
160
|
else:
|
|
99
161
|
console.print(f"[red]Job failed with exit code: {result.returncode}[/red]")
|
|
100
162
|
else:
|
|
101
163
|
console.print(f"Submitted job [bold cyan]{result.job_id}[/bold cyan]")
|
|
164
|
+
|
|
102
165
|
if ctx.verbose:
|
|
103
166
|
console.print(f" Scheduler: {scheduler.name}")
|
|
104
167
|
console.print(f" Job name: {job.name}")
|
|
105
168
|
console.print(f" Command: {job.command}")
|
|
106
169
|
|
|
170
|
+
if wait:
|
|
171
|
+
console.print("[dim]Waiting for job completion...[/dim]")
|
|
172
|
+
final_status = result.wait()
|
|
173
|
+
console.print(f"Job completed with status: [bold]{final_status.name}[/bold]")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _parse_args(args: tuple[str, ...]) -> tuple[list[str], list[str]]:
|
|
177
|
+
"""Parse args into command parts and scheduler passthrough args.
|
|
178
|
+
|
|
179
|
+
Scheduler args are any args that:
|
|
180
|
+
- Start with '-' and are not recognized hpc-runner options
|
|
181
|
+
- Include their values (e.g., "-N 4" becomes ["-N", "4"])
|
|
182
|
+
|
|
183
|
+
The command is everything after the first non-option arg or after '--'.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
args: Raw arguments from click
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Tuple of (command_parts, scheduler_args)
|
|
190
|
+
"""
|
|
191
|
+
command_parts: list[str] = []
|
|
192
|
+
scheduler_args: list[str] = []
|
|
107
193
|
|
|
108
|
-
|
|
194
|
+
args_list = list(args)
|
|
195
|
+
i = 0
|
|
196
|
+
in_command = False
|
|
197
|
+
|
|
198
|
+
while i < len(args_list):
|
|
199
|
+
arg = args_list[i]
|
|
200
|
+
|
|
201
|
+
# '--' signals end of options
|
|
202
|
+
if arg == "--":
|
|
203
|
+
in_command = True
|
|
204
|
+
i += 1
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
if in_command:
|
|
208
|
+
command_parts.append(arg)
|
|
209
|
+
i += 1
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
# Check if this looks like an option
|
|
213
|
+
if arg.startswith("-"):
|
|
214
|
+
# This is a scheduler passthrough option
|
|
215
|
+
scheduler_args.append(arg)
|
|
216
|
+
|
|
217
|
+
# Check if next arg is the value (not another option)
|
|
218
|
+
if i + 1 < len(args_list) and not args_list[i + 1].startswith("-"):
|
|
219
|
+
# Handle special case: is this a flag or does it take a value?
|
|
220
|
+
# Heuristic: if next arg doesn't start with '-', treat as value
|
|
221
|
+
# unless the current arg uses '=' syntax
|
|
222
|
+
if "=" not in arg:
|
|
223
|
+
i += 1
|
|
224
|
+
scheduler_args.append(args_list[i])
|
|
225
|
+
i += 1
|
|
226
|
+
else:
|
|
227
|
+
# First non-option arg starts the command
|
|
228
|
+
in_command = True
|
|
229
|
+
command_parts.append(arg)
|
|
230
|
+
i += 1
|
|
231
|
+
|
|
232
|
+
return command_parts, scheduler_args
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _show_dry_run(
|
|
236
|
+
job: "Job", scheduler, scheduler_args: list[str], interactive: bool = False
|
|
237
|
+
) -> None:
|
|
109
238
|
"""Display what would be submitted."""
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if job.time:
|
|
122
|
-
console.print(f"[bold]Time:[/bold] {job.time}")
|
|
123
|
-
if job.queue:
|
|
124
|
-
console.print(f"[bold]Queue:[/bold] {job.queue}")
|
|
125
|
-
if job.modules:
|
|
126
|
-
console.print(f"[bold]Modules:[/bold] {', '.join(job.modules)}")
|
|
127
|
-
if job.merge_output:
|
|
128
|
-
console.print(f"[bold]Output:[/bold] merged (stdout only)")
|
|
129
|
-
else:
|
|
130
|
-
console.print(f"[bold]Stderr:[/bold] {job.stderr}")
|
|
239
|
+
mode = "interactive" if interactive else "batch"
|
|
240
|
+
console.print(
|
|
241
|
+
Panel.fit(
|
|
242
|
+
f"[bold]Scheduler:[/bold] {scheduler.name}\n"
|
|
243
|
+
f"[bold]Mode:[/bold] {mode}\n"
|
|
244
|
+
f"[bold]Job name:[/bold] {job.name}\n"
|
|
245
|
+
f"[bold]Command:[/bold] {job.command}",
|
|
246
|
+
title="Dry Run",
|
|
247
|
+
border_style="blue",
|
|
248
|
+
)
|
|
249
|
+
)
|
|
131
250
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
251
|
+
if scheduler_args:
|
|
252
|
+
console.print(f"\n[bold]Scheduler passthrough args:[/bold] {' '.join(scheduler_args)}")
|
|
253
|
+
|
|
254
|
+
console.print("\n[bold]Generated script:[/bold]")
|
|
255
|
+
if interactive and hasattr(scheduler, "_generate_interactive_script"):
|
|
256
|
+
script = scheduler._generate_interactive_script(job, "/tmp/example_script.sh")
|
|
257
|
+
else:
|
|
258
|
+
script = scheduler.generate_script(job)
|
|
135
259
|
syntax = Syntax(script, "bash", theme="monokai", line_numbers=True)
|
|
136
260
|
console.print(syntax)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _handle_array_job(job, array_spec: str, scheduler, dry_run: bool, verbose: bool) -> None:
|
|
264
|
+
"""Handle array job submission."""
|
|
265
|
+
from hpc_runner.core.job_array import JobArray
|
|
266
|
+
|
|
267
|
+
# Parse array spec (e.g., "1-100", "1-100:10", "1-100%5")
|
|
268
|
+
# Basic parsing - could be enhanced
|
|
269
|
+
parts = array_spec.replace("%", ":").split(":")
|
|
270
|
+
range_parts = parts[0].split("-")
|
|
271
|
+
|
|
272
|
+
start = int(range_parts[0])
|
|
273
|
+
end = int(range_parts[1]) if len(range_parts) > 1 else start
|
|
274
|
+
step = int(parts[1]) if len(parts) > 1 else 1
|
|
275
|
+
max_concurrent = int(parts[2]) if len(parts) > 2 else None
|
|
276
|
+
|
|
277
|
+
array_job = JobArray(
|
|
278
|
+
job=job,
|
|
279
|
+
start=start,
|
|
280
|
+
end=end,
|
|
281
|
+
step=step,
|
|
282
|
+
max_concurrent=max_concurrent,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
if dry_run:
|
|
286
|
+
console.print(f"[bold]Array job:[/bold] {array_job.range_str} ({array_job.count} tasks)")
|
|
287
|
+
_show_dry_run(job, scheduler, [])
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
result = array_job.submit(scheduler)
|
|
291
|
+
console.print(f"Submitted array job [bold cyan]{result.base_job_id}[/bold cyan]")
|
|
292
|
+
console.print(f" Tasks: {array_job.count} ({array_job.range_str})")
|
hpc_runner/cli/status.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Status command - check job status."""
|
|
2
2
|
|
|
3
|
-
from typing import Optional
|
|
4
3
|
|
|
5
4
|
import rich_click as click
|
|
6
5
|
from rich.console import Console
|
|
@@ -13,12 +12,12 @@ console = Console()
|
|
|
13
12
|
|
|
14
13
|
@click.command()
|
|
15
14
|
@click.argument("job_id", required=False)
|
|
16
|
-
@click.option("--all", "
|
|
17
|
-
@click.option("--watch",
|
|
15
|
+
@click.option("--all", "all_users", is_flag=True, help="Show all users' jobs")
|
|
16
|
+
@click.option("--watch", is_flag=True, help="Watch mode (refresh periodically)")
|
|
18
17
|
@pass_context
|
|
19
18
|
def status(
|
|
20
19
|
ctx: Context,
|
|
21
|
-
job_id:
|
|
20
|
+
job_id: str | None,
|
|
22
21
|
all_users: bool,
|
|
23
22
|
watch: bool,
|
|
24
23
|
) -> None:
|
|
@@ -47,7 +46,9 @@ def status(
|
|
|
47
46
|
console.print(table)
|
|
48
47
|
else:
|
|
49
48
|
# List all jobs (not implemented for all schedulers)
|
|
50
|
-
console.print(
|
|
49
|
+
console.print(
|
|
50
|
+
"[yellow]Listing all jobs requires scheduler-specific implementation[/yellow]"
|
|
51
|
+
)
|
|
51
52
|
console.print("Use 'hpc status <job_id>' to check a specific job")
|
|
52
53
|
|
|
53
54
|
|
hpc_runner/core/__init__.py
CHANGED
|
@@ -1 +1,31 @@
|
|
|
1
1
|
"""Core models and abstractions for hpc-tools."""
|
|
2
|
+
|
|
3
|
+
from .exceptions import (
|
|
4
|
+
AccountingNotAvailable,
|
|
5
|
+
ConfigError,
|
|
6
|
+
ConfigNotFoundError,
|
|
7
|
+
HPCToolsError,
|
|
8
|
+
JobNotFoundError,
|
|
9
|
+
SchedulerError,
|
|
10
|
+
SubmissionError,
|
|
11
|
+
ValidationError,
|
|
12
|
+
)
|
|
13
|
+
from .job_info import JobInfo
|
|
14
|
+
from .result import ArrayJobResult, JobResult, JobStatus
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
# Exceptions
|
|
18
|
+
"AccountingNotAvailable",
|
|
19
|
+
"ConfigError",
|
|
20
|
+
"ConfigNotFoundError",
|
|
21
|
+
"HPCToolsError",
|
|
22
|
+
"JobNotFoundError",
|
|
23
|
+
"SchedulerError",
|
|
24
|
+
"SubmissionError",
|
|
25
|
+
"ValidationError",
|
|
26
|
+
# Types
|
|
27
|
+
"JobInfo",
|
|
28
|
+
"JobResult",
|
|
29
|
+
"ArrayJobResult",
|
|
30
|
+
"JobStatus",
|
|
31
|
+
]
|