hpc-runner 0.3.0__py3-none-any.whl → 0.3.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 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.3.0'
32
- __version_tuple__ = version_tuple = (0, 3, 0)
31
+ __version__ = version = '0.3.1'
32
+ __version_tuple__ = version_tuple = (0, 3, 1)
33
33
 
34
34
  __commit_id__ = commit_id = None
hpc_runner/cli/config.py CHANGED
@@ -62,7 +62,7 @@ def init(ctx: Context, global_config: bool) -> None:
62
62
  return
63
63
 
64
64
  # Write default config
65
- default_config = '''# hpc-runner configuration
65
+ default_config = """# hpc-runner configuration
66
66
  #
67
67
  # This file is safe to commit to a project repo (for shared defaults).
68
68
  # For a per-user config, run: hpc config init --global
@@ -92,7 +92,7 @@ merge_output = true
92
92
  # [types.gpu]
93
93
  # queue = "gpu"
94
94
  # resources = [{name = "gpu", value = 1}]
95
- '''
95
+ """
96
96
 
97
97
  config_path.write_text(default_config)
98
98
  console.print(f"[green]Created {config_path}[/green]")
hpc_runner/cli/main.py CHANGED
@@ -1,6 +1,8 @@
1
1
  """Main CLI entry point using rich-click."""
2
2
 
3
+ from collections.abc import Callable
3
4
  from pathlib import Path
5
+ from typing import TypeVar
4
6
 
5
7
  import rich_click as click
6
8
  from rich.console import Console
@@ -11,6 +13,7 @@ click.rich_click.SHOW_ARGUMENTS = True
11
13
  # Global console for Rich output
12
14
  console = Console()
13
15
 
16
+
14
17
  # Context object to pass state between commands
15
18
  class Context:
16
19
  def __init__(self) -> None:
@@ -18,7 +21,9 @@ class Context:
18
21
  self.scheduler: str | None = None
19
22
  self.verbose: bool = False
20
23
 
21
- pass_context = click.make_pass_decorator(Context, ensure=True)
24
+
25
+ F = TypeVar("F", bound=Callable[..., object])
26
+ pass_context: Callable[[F], F] = click.make_pass_decorator(Context, ensure=True) # type: ignore[assignment]
22
27
 
23
28
 
24
29
  @click.group(context_settings={"help_option_names": ["-h", "--help"]})
@@ -68,8 +73,8 @@ from hpc_runner.cli.status import status # noqa: E402
68
73
 
69
74
  cli.add_command(run)
70
75
  cli.add_command(status)
71
- cli.add_command(cancel)
72
- cli.add_command(config_cmd, name="config")
76
+ cli.add_command(cancel) # type: ignore[has-type]
77
+ cli.add_command(config_cmd, name="config") # type: ignore[has-type]
73
78
  cli.add_command(monitor)
74
79
 
75
80
 
hpc_runner/cli/run.py CHANGED
@@ -11,6 +11,7 @@ from hpc_runner.cli.main import Context, pass_context
11
11
 
12
12
  if TYPE_CHECKING:
13
13
  from hpc_runner.core.job import Job
14
+ from hpc_runner.schedulers.base import BaseScheduler
14
15
 
15
16
  console = Console()
16
17
 
@@ -34,10 +35,15 @@ console = Console()
34
35
  @click.option("--job-type", "job_type", help="Job type from config")
35
36
  @click.option("--module", "modules", multiple=True, help="Modules to load (repeatable)")
36
37
  @click.option("--stderr", help="Separate stderr file (default: merged)")
37
- @click.option("--output", help="Stdout file path pattern")
38
+ @click.option("--stdout", "stdout", help="Stdout file path pattern")
38
39
  @click.option("--array", help="Array job specification (e.g., 1-100)")
39
40
  @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(
42
+ "--inherit-env/--no-inherit-env",
43
+ "inherit_env",
44
+ default=True,
45
+ help="Inherit environment variables",
46
+ )
41
47
  @click.option("--interactive", is_flag=True, help="Run interactively (srun/qrsh)")
42
48
  @click.option("--local", is_flag=True, help="Run locally (no scheduler)")
43
49
  @click.option("--dry-run", "dry_run", is_flag=True, help="Show what would be submitted")
@@ -58,7 +64,7 @@ def run(
58
64
  job_type: str | None,
59
65
  modules: tuple[str, ...],
60
66
  stderr: str | None,
61
- output: str | None,
67
+ stdout: str | None,
62
68
  array: str | None,
63
69
  depend: str | None,
64
70
  inherit_env: bool,
@@ -128,8 +134,8 @@ def run(
128
134
  job.modules = list(modules)
129
135
  if stderr:
130
136
  job.stderr = stderr
131
- if output:
132
- job.stdout = output
137
+ if stdout:
138
+ job.stdout = stdout
133
139
  if depend:
134
140
  job.dependency = depend
135
141
 
@@ -233,7 +239,10 @@ def _parse_args(args: tuple[str, ...]) -> tuple[list[str], list[str]]:
233
239
 
234
240
 
235
241
  def _show_dry_run(
236
- job: "Job", scheduler, scheduler_args: list[str], interactive: bool = False
242
+ job: "Job",
243
+ scheduler: "BaseScheduler",
244
+ scheduler_args: list[str],
245
+ interactive: bool = False,
237
246
  ) -> None:
238
247
  """Display what would be submitted."""
239
248
  mode = "interactive" if interactive else "batch"
@@ -252,15 +261,21 @@ def _show_dry_run(
252
261
  console.print(f"\n[bold]Scheduler passthrough args:[/bold] {' '.join(scheduler_args)}")
253
262
 
254
263
  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")
264
+ if interactive:
265
+ script = scheduler.generate_interactive_script(job, "/tmp/example_script.sh")
257
266
  else:
258
267
  script = scheduler.generate_script(job)
259
268
  syntax = Syntax(script, "bash", theme="monokai", line_numbers=True)
260
269
  console.print(syntax)
261
270
 
262
271
 
263
- def _handle_array_job(job, array_spec: str, scheduler, dry_run: bool, verbose: bool) -> None:
272
+ def _handle_array_job(
273
+ job: "Job",
274
+ array_spec: str,
275
+ scheduler: "BaseScheduler",
276
+ dry_run: bool,
277
+ verbose: bool,
278
+ ) -> None:
264
279
  """Handle array job submission."""
265
280
  from hpc_runner.core.job_array import JobArray
266
281
 
hpc_runner/cli/status.py CHANGED
@@ -1,6 +1,5 @@
1
1
  """Status command - check job status."""
2
2
 
3
-
4
3
  import rich_click as click
5
4
  from rich.console import Console
6
5
  from rich.table import Table
hpc_runner/cli/submit.py CHANGED
@@ -5,7 +5,6 @@ from __future__ import annotations
5
5
  import sys
6
6
  from typing import Final
7
7
 
8
-
9
8
  _GLOBAL_FLAGS: Final[set[str]] = {"--config", "--scheduler", "--verbose", "-h", "--help"}
10
9
 
11
10
 
@@ -69,4 +68,3 @@ def main() -> None:
69
68
 
70
69
  global_opts, rest = _split_global_flags(argv)
71
70
  cli.main(args=[*global_opts, "run", *rest], prog_name="submit")
72
-
hpc_runner/core/config.py CHANGED
@@ -10,7 +10,7 @@ from typing import Any
10
10
  if sys.version_info >= (3, 11):
11
11
  import tomllib
12
12
  else:
13
- import tomli as tomllib
13
+ import tomli as tomllib # type: ignore[import-not-found]
14
14
 
15
15
 
16
16
  @dataclass
@@ -68,7 +68,13 @@ def _merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
68
68
  if value and value[0] == "-":
69
69
  result[key] = value[1:]
70
70
  else:
71
- result[key] = list(set(result[key] + value))
71
+ seen: set[Any] = set()
72
+ merged: list[Any] = []
73
+ for item in result[key] + value:
74
+ if item not in seen:
75
+ seen.add(item)
76
+ merged.append(item)
77
+ result[key] = merged
72
78
  else:
73
79
  result[key] = value
74
80
  return result
@@ -1,7 +1,7 @@
1
1
  """Descriptor pattern for job attributes and scheduler arguments."""
2
2
 
3
3
  from abc import ABC, abstractmethod
4
- from typing import Any, Generic, TypeVar
4
+ from typing import Any, Generic, TypeVar, overload
5
5
 
6
6
  T = TypeVar("T")
7
7
 
@@ -34,12 +34,18 @@ class JobAttribute(Generic[T]):
34
34
  def __init__(self, name: str, *, default: T | None = None):
35
35
  self.public_name = name
36
36
  self.default = default
37
- self._private_name: str | None = None
37
+ self._private_name: str = f"_{name}"
38
38
 
39
39
  def __set_name__(self, owner: type, name: str) -> None:
40
40
  self._private_name = f"_{name}"
41
41
 
42
- def __get__(self, obj: Any, objtype: type | None = None) -> T | "JobAttribute[T]":
42
+ @overload
43
+ def __get__(self, obj: None, objtype: type) -> "JobAttribute[T]": ...
44
+
45
+ @overload
46
+ def __get__(self, obj: Any, objtype: type | None = None) -> T | None: ...
47
+
48
+ def __get__(self, obj: Any, objtype: type | None = None) -> "T | None | JobAttribute[T]":
43
49
  if obj is None:
44
50
  return self
45
51
  return getattr(obj, self._private_name, self.default)
hpc_runner/core/job.py CHANGED
@@ -3,7 +3,8 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
6
- from typing import TYPE_CHECKING, Any, Iterator
6
+ from collections.abc import Iterator
7
+ from typing import TYPE_CHECKING, Any
7
8
 
8
9
  from hpc_runner.core.descriptors import JobAttribute
9
10
  from hpc_runner.core.resources import ResourceSet
@@ -182,7 +183,7 @@ class Job:
182
183
  # Submission API
183
184
  # =========================================================================
184
185
 
185
- def submit(self, scheduler: "BaseScheduler | None" = None) -> "JobResult":
186
+ def submit(self, scheduler: BaseScheduler | None = None) -> JobResult:
186
187
  """Submit the job to a scheduler.
187
188
 
188
189
  This is the primary programmatic API for job submission.
@@ -216,7 +217,7 @@ class Job:
216
217
  tool_or_type: str,
217
218
  command: str | None = None,
218
219
  **overrides: Any,
219
- ) -> "Job":
220
+ ) -> Job:
220
221
  """Create a job from configuration.
221
222
 
222
223
  Looks up job settings from the config file by tool name or job type,
@@ -309,9 +310,9 @@ class Job:
309
310
 
310
311
  def after(
311
312
  self,
312
- *jobs: "JobResult",
313
+ *jobs: JobResult,
313
314
  type: str = "afterok",
314
- ) -> "Job":
315
+ ) -> Job:
315
316
  """Add job dependencies.
316
317
 
317
318
  Args:
@@ -2,8 +2,9 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from collections.abc import Iterator
5
6
  from dataclasses import dataclass
6
- from typing import TYPE_CHECKING, Iterator
7
+ from typing import TYPE_CHECKING
7
8
 
8
9
  if TYPE_CHECKING:
9
10
  from hpc_runner.core.job import Job
@@ -1,5 +1,6 @@
1
1
  """Resource abstraction for job resource requests."""
2
2
 
3
+ from collections.abc import Iterator
3
4
  from dataclasses import dataclass, field
4
5
 
5
6
 
@@ -39,7 +40,7 @@ class ResourceSet:
39
40
  return r
40
41
  return None
41
42
 
42
- def __iter__(self):
43
+ def __iter__(self) -> Iterator[Resource]:
43
44
  return iter(self.resources)
44
45
 
45
46
  def __len__(self) -> int:
@@ -18,7 +18,7 @@ _SCHEDULERS: dict[str, str] = {
18
18
  }
19
19
 
20
20
 
21
- def get_scheduler(name: str | None = None) -> "BaseScheduler":
21
+ def get_scheduler(name: str | None = None) -> BaseScheduler:
22
22
  """Get scheduler instance.
23
23
 
24
24
  Args:
@@ -39,7 +39,7 @@ def get_scheduler(name: str | None = None) -> "BaseScheduler":
39
39
  module = importlib.import_module(module_path)
40
40
  scheduler_class = getattr(module, class_name)
41
41
 
42
- return scheduler_class()
42
+ return scheduler_class() # type: ignore[no-any-return]
43
43
 
44
44
 
45
45
  def register_scheduler(name: str, import_path: str) -> None:
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from abc import ABC, abstractmethod
6
6
  from datetime import datetime
7
7
  from pathlib import Path
8
- from typing import TYPE_CHECKING
8
+ from typing import TYPE_CHECKING, Any
9
9
 
10
10
  from hpc_runner.core.descriptors import SchedulerArg
11
11
 
@@ -35,13 +35,13 @@ class BaseScheduler(ABC):
35
35
  name: str = ""
36
36
 
37
37
  # Subclasses populate this in __init__ with config-driven values
38
- ARG_RENDERERS: dict[str, SchedulerArg] = {}
38
+ ARG_RENDERERS: dict[str, SchedulerArg[Any]] = {}
39
39
 
40
40
  # =========================================================================
41
41
  # Rendering Protocol
42
42
  # =========================================================================
43
43
 
44
- def render_directives(self, job: "Job") -> list[str]:
44
+ def render_directives(self, job: Job) -> list[str]:
45
45
  """Render job attributes as script directives.
46
46
 
47
47
  Iterates over job's renderable attributes and uses ARG_RENDERERS
@@ -66,7 +66,7 @@ class BaseScheduler(ABC):
66
66
 
67
67
  return directives
68
68
 
69
- def render_args(self, job: "Job") -> list[str]:
69
+ def render_args(self, job: Job) -> list[str]:
70
70
  """Render job attributes as command-line arguments.
71
71
 
72
72
  Iterates over job's renderable attributes and uses ARG_RENDERERS
@@ -94,9 +94,7 @@ class BaseScheduler(ABC):
94
94
  # =========================================================================
95
95
 
96
96
  @abstractmethod
97
- def submit(
98
- self, job: "Job", interactive: bool = False, keep_script: bool = False
99
- ) -> "JobResult":
97
+ def submit(self, job: Job, interactive: bool = False, keep_script: bool = False) -> JobResult:
100
98
  """Submit a job to the scheduler.
101
99
 
102
100
  Args:
@@ -106,7 +104,7 @@ class BaseScheduler(ABC):
106
104
  """
107
105
 
108
106
  @abstractmethod
109
- def submit_array(self, array: "JobArray") -> "ArrayJobResult":
107
+ def submit_array(self, array: JobArray) -> ArrayJobResult:
110
108
  """Submit an array job."""
111
109
 
112
110
  @abstractmethod
@@ -114,7 +112,7 @@ class BaseScheduler(ABC):
114
112
  """Cancel a job."""
115
113
 
116
114
  @abstractmethod
117
- def get_status(self, job_id: str) -> "JobStatus":
115
+ def get_status(self, job_id: str) -> JobStatus:
118
116
  """Get job status."""
119
117
 
120
118
  @abstractmethod
@@ -122,21 +120,37 @@ class BaseScheduler(ABC):
122
120
  """Get job exit code."""
123
121
 
124
122
  @abstractmethod
125
- def generate_script(self, job: "Job", array_range: str | None = None) -> str:
123
+ def generate_script(self, job: Job, array_range: str | None = None) -> str:
126
124
  """Generate submission script."""
127
125
 
128
126
  @abstractmethod
129
- def build_submit_command(self, job: "Job") -> list[str]:
127
+ def build_submit_command(self, job: Job) -> list[str]:
130
128
  """Build submission command line."""
131
129
 
132
130
  @abstractmethod
133
- def build_interactive_command(self, job: "Job") -> list[str]:
131
+ def build_interactive_command(self, job: Job) -> list[str]:
134
132
  """Build interactive execution command."""
135
133
 
136
134
  # =========================================================================
137
135
  # Optional Methods - Override if scheduler supports these
138
136
  # =========================================================================
139
137
 
138
+ def generate_interactive_script(self, job: Job, script_path: str) -> str:
139
+ """Generate wrapper script for interactive jobs.
140
+
141
+ By default, falls back to the standard batch script.
142
+ Override in subclasses that need a different template for
143
+ interactive sessions (e.g. SGE uses qrsh with no #$ directives).
144
+
145
+ Args:
146
+ job: Job to generate script for.
147
+ script_path: Path where the script will be written.
148
+
149
+ Returns:
150
+ Script content as a string.
151
+ """
152
+ return self.generate_script(job)
153
+
140
154
  def get_output_path(self, job_id: str, stream: str) -> Path | None:
141
155
  """Get path to output file.
142
156
 
@@ -149,16 +163,16 @@ class BaseScheduler(ABC):
149
163
  """
150
164
  return None
151
165
 
152
- def get_scheduler_args(self, job: "Job") -> list[str]:
166
+ def get_scheduler_args(self, job: Job) -> list[str]:
153
167
  """Get scheduler-specific raw args from job."""
154
168
  return getattr(job, f"{self.name}_args", [])
155
169
 
156
170
  def list_active_jobs(
157
171
  self,
158
172
  user: str | None = None,
159
- status: set["JobStatus"] | None = None,
173
+ status: set[JobStatus] | None = None,
160
174
  queue: str | None = None,
161
- ) -> list["JobInfo"]:
175
+ ) -> list[JobInfo]:
162
176
  """List active jobs. Override in subclass."""
163
177
  return []
164
178
 
@@ -170,7 +184,7 @@ class BaseScheduler(ABC):
170
184
  exit_code: int | None = None,
171
185
  queue: str | None = None,
172
186
  limit: int = 100,
173
- ) -> list["JobInfo"]:
187
+ ) -> list[JobInfo]:
174
188
  """List completed jobs from accounting. Override in subclass."""
175
189
  return []
176
190
 
@@ -178,7 +192,7 @@ class BaseScheduler(ABC):
178
192
  """Check if job accounting/history is available."""
179
193
  return False
180
194
 
181
- def get_job_details(self, job_id: str) -> tuple["JobInfo", dict[str, object]]:
195
+ def get_job_details(self, job_id: str) -> tuple[JobInfo, dict[str, object]]:
182
196
  """Get detailed information for a single job.
183
197
 
184
198
  Args: