hpcflow-new2 0.2.0a189__py3-none-any.whl → 0.2.0a190__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.
Files changed (115) hide show
  1. hpcflow/__pyinstaller/hook-hpcflow.py +8 -6
  2. hpcflow/_version.py +1 -1
  3. hpcflow/app.py +1 -0
  4. hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +1 -1
  5. hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +1 -1
  6. hpcflow/sdk/__init__.py +21 -15
  7. hpcflow/sdk/app.py +2133 -770
  8. hpcflow/sdk/cli.py +281 -250
  9. hpcflow/sdk/cli_common.py +6 -2
  10. hpcflow/sdk/config/__init__.py +1 -1
  11. hpcflow/sdk/config/callbacks.py +77 -42
  12. hpcflow/sdk/config/cli.py +126 -103
  13. hpcflow/sdk/config/config.py +578 -311
  14. hpcflow/sdk/config/config_file.py +131 -95
  15. hpcflow/sdk/config/errors.py +112 -85
  16. hpcflow/sdk/config/types.py +145 -0
  17. hpcflow/sdk/core/actions.py +1054 -994
  18. hpcflow/sdk/core/app_aware.py +24 -0
  19. hpcflow/sdk/core/cache.py +81 -63
  20. hpcflow/sdk/core/command_files.py +275 -185
  21. hpcflow/sdk/core/commands.py +111 -107
  22. hpcflow/sdk/core/element.py +724 -503
  23. hpcflow/sdk/core/enums.py +192 -0
  24. hpcflow/sdk/core/environment.py +74 -93
  25. hpcflow/sdk/core/errors.py +398 -51
  26. hpcflow/sdk/core/json_like.py +540 -272
  27. hpcflow/sdk/core/loop.py +380 -334
  28. hpcflow/sdk/core/loop_cache.py +160 -43
  29. hpcflow/sdk/core/object_list.py +370 -207
  30. hpcflow/sdk/core/parameters.py +728 -600
  31. hpcflow/sdk/core/rule.py +59 -41
  32. hpcflow/sdk/core/run_dir_files.py +33 -22
  33. hpcflow/sdk/core/task.py +1546 -1325
  34. hpcflow/sdk/core/task_schema.py +240 -196
  35. hpcflow/sdk/core/test_utils.py +126 -88
  36. hpcflow/sdk/core/types.py +387 -0
  37. hpcflow/sdk/core/utils.py +410 -305
  38. hpcflow/sdk/core/validation.py +82 -9
  39. hpcflow/sdk/core/workflow.py +1192 -1028
  40. hpcflow/sdk/core/zarr_io.py +98 -137
  41. hpcflow/sdk/demo/cli.py +46 -33
  42. hpcflow/sdk/helper/cli.py +18 -16
  43. hpcflow/sdk/helper/helper.py +75 -63
  44. hpcflow/sdk/helper/watcher.py +61 -28
  45. hpcflow/sdk/log.py +83 -59
  46. hpcflow/sdk/persistence/__init__.py +8 -31
  47. hpcflow/sdk/persistence/base.py +988 -586
  48. hpcflow/sdk/persistence/defaults.py +6 -0
  49. hpcflow/sdk/persistence/discovery.py +38 -0
  50. hpcflow/sdk/persistence/json.py +408 -153
  51. hpcflow/sdk/persistence/pending.py +158 -123
  52. hpcflow/sdk/persistence/store_resource.py +37 -22
  53. hpcflow/sdk/persistence/types.py +307 -0
  54. hpcflow/sdk/persistence/utils.py +14 -11
  55. hpcflow/sdk/persistence/zarr.py +477 -420
  56. hpcflow/sdk/runtime.py +44 -41
  57. hpcflow/sdk/submission/{jobscript_info.py → enums.py} +39 -12
  58. hpcflow/sdk/submission/jobscript.py +444 -404
  59. hpcflow/sdk/submission/schedulers/__init__.py +133 -40
  60. hpcflow/sdk/submission/schedulers/direct.py +97 -71
  61. hpcflow/sdk/submission/schedulers/sge.py +132 -126
  62. hpcflow/sdk/submission/schedulers/slurm.py +263 -268
  63. hpcflow/sdk/submission/schedulers/utils.py +7 -2
  64. hpcflow/sdk/submission/shells/__init__.py +14 -15
  65. hpcflow/sdk/submission/shells/base.py +102 -29
  66. hpcflow/sdk/submission/shells/bash.py +72 -55
  67. hpcflow/sdk/submission/shells/os_version.py +31 -30
  68. hpcflow/sdk/submission/shells/powershell.py +37 -29
  69. hpcflow/sdk/submission/submission.py +203 -257
  70. hpcflow/sdk/submission/types.py +143 -0
  71. hpcflow/sdk/typing.py +163 -12
  72. hpcflow/tests/conftest.py +8 -6
  73. hpcflow/tests/schedulers/slurm/test_slurm_submission.py +5 -2
  74. hpcflow/tests/scripts/test_main_scripts.py +60 -30
  75. hpcflow/tests/shells/wsl/test_wsl_submission.py +6 -4
  76. hpcflow/tests/unit/test_action.py +86 -75
  77. hpcflow/tests/unit/test_action_rule.py +9 -4
  78. hpcflow/tests/unit/test_app.py +13 -6
  79. hpcflow/tests/unit/test_cli.py +1 -1
  80. hpcflow/tests/unit/test_command.py +71 -54
  81. hpcflow/tests/unit/test_config.py +20 -15
  82. hpcflow/tests/unit/test_config_file.py +21 -18
  83. hpcflow/tests/unit/test_element.py +58 -62
  84. hpcflow/tests/unit/test_element_iteration.py +3 -1
  85. hpcflow/tests/unit/test_element_set.py +29 -19
  86. hpcflow/tests/unit/test_group.py +4 -2
  87. hpcflow/tests/unit/test_input_source.py +116 -93
  88. hpcflow/tests/unit/test_input_value.py +29 -24
  89. hpcflow/tests/unit/test_json_like.py +44 -35
  90. hpcflow/tests/unit/test_loop.py +65 -58
  91. hpcflow/tests/unit/test_object_list.py +17 -12
  92. hpcflow/tests/unit/test_parameter.py +16 -7
  93. hpcflow/tests/unit/test_persistence.py +48 -35
  94. hpcflow/tests/unit/test_resources.py +20 -18
  95. hpcflow/tests/unit/test_run.py +8 -3
  96. hpcflow/tests/unit/test_runtime.py +2 -1
  97. hpcflow/tests/unit/test_schema_input.py +23 -15
  98. hpcflow/tests/unit/test_shell.py +3 -2
  99. hpcflow/tests/unit/test_slurm.py +8 -7
  100. hpcflow/tests/unit/test_submission.py +39 -19
  101. hpcflow/tests/unit/test_task.py +352 -247
  102. hpcflow/tests/unit/test_task_schema.py +33 -20
  103. hpcflow/tests/unit/test_utils.py +9 -11
  104. hpcflow/tests/unit/test_value_sequence.py +15 -12
  105. hpcflow/tests/unit/test_workflow.py +114 -83
  106. hpcflow/tests/unit/test_workflow_template.py +0 -1
  107. hpcflow/tests/workflows/test_jobscript.py +2 -1
  108. hpcflow/tests/workflows/test_workflows.py +18 -13
  109. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/METADATA +2 -1
  110. hpcflow_new2-0.2.0a190.dist-info/RECORD +165 -0
  111. hpcflow/sdk/core/parallel.py +0 -21
  112. hpcflow_new2-0.2.0a189.dist-info/RECORD +0 -158
  113. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/LICENSE +0 -0
  114. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/WHEEL +0 -0
  115. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/entry_points.txt +0 -0
@@ -2,22 +2,34 @@
2
2
  An interface to SGE.
3
3
  """
4
4
 
5
- from pathlib import Path
5
+ from __future__ import annotations
6
+ from collections.abc import Sequence
6
7
  import re
7
- from typing import Dict, List, Tuple
8
+ from typing import TYPE_CHECKING
9
+ from typing_extensions import override
10
+ from hpcflow.sdk.typing import hydrate
8
11
  from hpcflow.sdk.core.errors import (
9
12
  IncompatibleSGEPEError,
10
13
  NoCompatibleSGEPEError,
11
14
  UnknownSGEPEError,
12
15
  )
13
16
  from hpcflow.sdk.log import TimeIt
14
- from hpcflow.sdk.submission.jobscript_info import JobscriptElementState
15
- from hpcflow.sdk.submission.schedulers import Scheduler
17
+ from hpcflow.sdk.submission.enums import JobscriptElementState
18
+ from hpcflow.sdk.submission.schedulers import QueuedScheduler
16
19
  from hpcflow.sdk.submission.schedulers.utils import run_cmd
17
- from hpcflow.sdk.submission.shells.base import Shell
18
20
 
21
+ if TYPE_CHECKING:
22
+ from collections.abc import Iterator, Mapping
23
+ from typing import Any, ClassVar
24
+ from ...config.types import SchedulerConfigDescriptor
25
+ from ...core.element import ElementResources
26
+ from ..jobscript import Jobscript
27
+ from ..types import VersionInfo
28
+ from ..shells.base import Shell
19
29
 
20
- class SGEPosix(Scheduler):
30
+
31
+ @hydrate
32
+ class SGEPosix(QueuedScheduler):
21
33
  """
22
34
  A scheduler that uses SGE.
23
35
 
@@ -43,36 +55,34 @@ class SGEPosix(Scheduler):
43
55
 
44
56
  """
45
57
 
46
- _app_attr = "app"
47
-
48
58
  #: Default args for shebang line.
49
- DEFAULT_SHEBANG_ARGS = ""
59
+ DEFAULT_SHEBANG_ARGS: ClassVar[str] = ""
50
60
  #: Default submission command.
51
- DEFAULT_SUBMIT_CMD = "qsub"
61
+ DEFAULT_SUBMIT_CMD: ClassVar[str] = "qsub"
52
62
  #: Default command to show the queue state.
53
- DEFAULT_SHOW_CMD = ["qstat"]
63
+ DEFAULT_SHOW_CMD: ClassVar[Sequence[str]] = ("qstat",)
54
64
  #: Default cancel command.
55
- DEFAULT_DEL_CMD = "qdel"
65
+ DEFAULT_DEL_CMD: ClassVar[str] = "qdel"
56
66
  #: Default job control directive prefix.
57
- DEFAULT_JS_CMD = "#$"
67
+ DEFAULT_JS_CMD: ClassVar[str] = "#$"
58
68
  #: Default prefix to enable array processing.
59
- DEFAULT_ARRAY_SWITCH = "-t"
69
+ DEFAULT_ARRAY_SWITCH: ClassVar[str] = "-t"
60
70
  #: Default shell variable with array ID.
61
- DEFAULT_ARRAY_ITEM_VAR = "SGE_TASK_ID"
71
+ DEFAULT_ARRAY_ITEM_VAR: ClassVar[str] = "SGE_TASK_ID"
62
72
  #: Default switch to control CWD.
63
- DEFAULT_CWD_SWITCH = "-cwd"
73
+ DEFAULT_CWD_SWITCH: ClassVar[str] = "-cwd"
64
74
  #: Default command to get the login nodes.
65
- DEFAULT_LOGIN_NODES_CMD = ["qconf", "-sh"]
75
+ DEFAULT_LOGIN_NODES_CMD: ClassVar[Sequence[str]] = ("qconf", "-sh")
66
76
 
67
77
  #: Maps scheduler state codes to :py:class:`JobscriptElementState` values.
68
- state_lookup = {
78
+ state_lookup: ClassVar[Mapping[str, JobscriptElementState]] = {
69
79
  "qw": JobscriptElementState.pending,
70
80
  "hq": JobscriptElementState.waiting,
71
81
  "hR": JobscriptElementState.waiting,
72
82
  "r": JobscriptElementState.running,
73
83
  "t": JobscriptElementState.running,
74
84
  "Rr": JobscriptElementState.running,
75
- "Rt": JobscriptElementState.running,
85
+ # "Rt": JobscriptElementState.running,
76
86
  "s": JobscriptElementState.errored,
77
87
  "ts": JobscriptElementState.errored,
78
88
  "S": JobscriptElementState.errored,
@@ -93,17 +103,22 @@ class SGEPosix(Scheduler):
93
103
  "dT": JobscriptElementState.cancelled,
94
104
  }
95
105
 
96
- def __init__(self, cwd_switch=None, *args, **kwargs):
106
+ def __init__(self, cwd_switch: str | None = None, *args, **kwargs):
97
107
  super().__init__(*args, **kwargs)
98
108
  self.cwd_switch = cwd_switch or self.DEFAULT_CWD_SWITCH
99
109
 
100
110
  @classmethod
111
+ @override
101
112
  @TimeIt.decorator
102
- def process_resources(cls, resources, scheduler_config: Dict) -> None:
103
- """Perform scheduler-specific processing to the element resources.
104
-
105
- Note: this mutates `resources`.
113
+ def process_resources(
114
+ cls, resources: ElementResources, scheduler_config: SchedulerConfigDescriptor
115
+ ) -> None:
116
+ """
117
+ Perform scheduler-specific processing to the element resources.
106
118
 
119
+ Note
120
+ ----
121
+ This mutates `resources`.
107
122
  """
108
123
  if resources.num_nodes is not None:
109
124
  raise ValueError(
@@ -116,7 +131,7 @@ class SGEPosix(Scheduler):
116
131
  if resources.SGE_parallel_env is not None:
117
132
  # check user-specified `parallel_env` is valid and compatible with
118
133
  # `num_cores`:
119
- if resources.num_cores > 1:
134
+ if resources.num_cores and resources.num_cores > 1:
120
135
  raise ValueError(
121
136
  f"An SGE parallel environment should not be specified if `num_cores` "
122
137
  f"is 1 (`SGE_parallel_env` was specified as "
@@ -126,105 +141,91 @@ class SGEPosix(Scheduler):
126
141
  try:
127
142
  env = para_envs[resources.SGE_parallel_env]
128
143
  except KeyError:
129
- raise UnknownSGEPEError(
130
- f"The SGE parallel environment {resources.SGE_parallel_env!r} is not "
131
- f"specified in the configuration. Specified parallel environments "
132
- f"are {list(para_envs.keys())!r}."
133
- )
144
+ raise UnknownSGEPEError(resources.SGE_parallel_env, para_envs)
134
145
  if not cls.is_num_cores_supported(resources.num_cores, env["num_cores"]):
135
146
  raise IncompatibleSGEPEError(
136
- f"The SGE parallel environment {resources.SGE_parallel_env!r} is not "
137
- f"compatible with the number of cores requested: "
138
- f"{resources.num_cores!r}."
147
+ resources.SGE_parallel_env, resources.num_cores
139
148
  )
140
149
  else:
141
150
  # find the first compatible PE:
142
- pe_match = -1 # pe_name might be `None`
143
151
  for pe_name, pe_info in para_envs.items():
144
152
  if cls.is_num_cores_supported(resources.num_cores, pe_info["num_cores"]):
145
- pe_match = pe_name
153
+ resources.SGE_parallel_env = pe_name
146
154
  break
147
- if pe_match != -1:
148
- resources.SGE_parallel_env = pe_name
149
155
  else:
150
- raise NoCompatibleSGEPEError(
151
- f"No compatible SGE parallel environment could be found for the "
152
- f"specified `num_cores` ({resources.num_cores!r})."
153
- )
156
+ raise NoCompatibleSGEPEError(resources.num_cores)
154
157
 
155
- def get_login_nodes(self):
158
+ def get_login_nodes(self) -> list[str]:
156
159
  """Return a list of hostnames of login/administrative nodes as reported by the
157
160
  scheduler."""
158
- stdout, stderr = run_cmd(self.login_nodes_cmd)
161
+ get_login = self.login_nodes_cmd
162
+ assert get_login is not None and len(get_login) >= 1
163
+ stdout, stderr = run_cmd(get_login)
159
164
  if stderr:
160
165
  print(stderr)
161
- nodes = stdout.strip().split("\n")
162
- return nodes
163
-
164
- def _format_core_request_lines(self, resources):
165
- lns = []
166
- if resources.num_cores > 1:
167
- lns.append(
168
- f"{self.js_cmd} -pe {resources.SGE_parallel_env} {resources.num_cores}"
169
- )
166
+ return stdout.strip().split("\n")
167
+
168
+ def __format_core_request_lines(self, resources: ElementResources) -> Iterator[str]:
169
+ if resources.num_cores and resources.num_cores > 1:
170
+ yield f"{self.js_cmd} -pe {resources.SGE_parallel_env} {resources.num_cores}"
170
171
  if resources.max_array_items:
171
- lns.append(f"{self.js_cmd} -tc {resources.max_array_items}")
172
- return lns
172
+ yield f"{self.js_cmd} -tc {resources.max_array_items}"
173
173
 
174
- def _format_array_request(self, num_elements):
174
+ def __format_array_request(self, num_elements: int) -> str:
175
175
  return f"{self.js_cmd} {self.array_switch} 1-{num_elements}"
176
176
 
177
- def _format_std_stream_file_option_lines(self, is_array, sub_idx):
177
+ def __format_std_stream_file_option_lines(
178
+ self, is_array: bool, sub_idx: int
179
+ ) -> Iterator[str]:
178
180
  # note: we can't modify the file names
179
- base = f"./artifacts/submissions/{sub_idx}"
180
- return [
181
- f"{self.js_cmd} -o {base}",
182
- f"{self.js_cmd} -e {base}",
183
- ]
181
+ yield f"{self.js_cmd} -o ./artifacts/submissions/{sub_idx}"
182
+ yield f"{self.js_cmd} -e ./artifacts/submissions/{sub_idx}"
184
183
 
185
- def format_options(self, resources, num_elements, is_array, sub_idx):
184
+ @override
185
+ def format_options(
186
+ self, resources: ElementResources, num_elements: int, is_array: bool, sub_idx: int
187
+ ) -> str:
186
188
  """
187
189
  Format the options to the jobscript command.
188
190
  """
189
- opts = []
191
+ opts: list[str] = []
190
192
  opts.append(self.format_switch(self.cwd_switch))
191
- opts.extend(self._format_core_request_lines(resources))
193
+ opts.extend(self.__format_core_request_lines(resources))
192
194
  if is_array:
193
- opts.append(self._format_array_request(num_elements))
195
+ opts.append(self.__format_array_request(num_elements))
194
196
 
195
- opts.extend(self._format_std_stream_file_option_lines(is_array, sub_idx))
197
+ opts.extend(self.__format_std_stream_file_option_lines(is_array, sub_idx))
196
198
 
197
199
  for opt_k, opt_v in self.options.items():
198
- if isinstance(opt_v, list):
199
- for i in opt_v:
200
- opts.append(f"{self.js_cmd} {opt_k} {i}")
200
+ if opt_v is None:
201
+ opts.append(f"{self.js_cmd} {opt_k}")
202
+ elif isinstance(opt_v, list):
203
+ opts.extend(f"{self.js_cmd} {opt_k} {i}" for i in opt_v)
201
204
  elif opt_v:
202
205
  opts.append(f"{self.js_cmd} {opt_k} {opt_v}")
203
- elif opt_v is None:
204
- opts.append(f"{self.js_cmd} {opt_k}")
205
206
 
206
207
  return "\n".join(opts) + "\n"
207
208
 
209
+ @override
208
210
  @TimeIt.decorator
209
- def get_version_info(self):
210
- vers_cmd = self.show_cmd + ["-help"]
211
- stdout, stderr = run_cmd(vers_cmd)
211
+ def get_version_info(self) -> VersionInfo:
212
+ stdout, stderr = run_cmd([*self.show_cmd, "-help"])
212
213
  if stderr:
213
214
  print(stderr)
214
- version_str = stdout.split("\n")[0].strip()
215
- name, version = version_str.split()
216
- out = {
215
+ first_line, *_ = stdout.split("\n")
216
+ name, version, *_ = first_line.strip().split()
217
+ return {
217
218
  "scheduler_name": name,
218
219
  "scheduler_version": version,
219
220
  }
220
- return out
221
221
 
222
+ @override
222
223
  def get_submit_command(
223
224
  self,
224
225
  shell: Shell,
225
226
  js_path: str,
226
- deps: List[Tuple],
227
- ) -> List[str]:
227
+ deps: dict[Any, tuple[Any, ...]],
228
+ ) -> list[str]:
228
229
  """
229
230
  Get the command to use to submit a job to the scheduler.
230
231
 
@@ -234,8 +235,8 @@ class SGEPosix(Scheduler):
234
235
  """
235
236
  cmd = [self.submit_cmd, "-terse"]
236
237
 
237
- dep_job_IDs = []
238
- dep_job_IDs_arr = []
238
+ dep_job_IDs: list[str] = []
239
+ dep_job_IDs_arr: list[str] = []
239
240
  for job_ID, is_array_dep in deps.values():
240
241
  if is_array_dep: # array dependency
241
242
  dep_job_IDs_arr.append(str(job_ID))
@@ -253,60 +254,59 @@ class SGEPosix(Scheduler):
253
254
  cmd.append(js_path)
254
255
  return cmd
255
256
 
257
+ __SGE_JOB_ID_RE: ClassVar[re.Pattern] = re.compile(r"^\d+")
258
+
256
259
  def parse_submission_output(self, stdout: str) -> str:
257
260
  """Extract scheduler reference for a newly submitted jobscript"""
258
- match = re.search(r"^\d+", stdout)
259
- if match:
260
- job_ID = match.group()
261
- else:
261
+ if not (match := self.__SGE_JOB_ID_RE.search(stdout)):
262
262
  raise RuntimeError(f"Could not parse Job ID from scheduler output {stdout!r}")
263
- return job_ID
263
+ return match.group()
264
264
 
265
- def get_job_statuses(self):
265
+ def get_job_statuses(
266
+ self,
267
+ ) -> Mapping[str, Mapping[int | None, JobscriptElementState]]:
266
268
  """Get information about all of this user's jobscripts that currently listed by
267
269
  the scheduler."""
268
- cmd = self.show_cmd + ["-u", "$USER", "-g", "d"] # "-g d": separate arrays items
269
- stdout, stderr = run_cmd(cmd, logger=self.app.submission_logger)
270
+ cmd = [*self.show_cmd, "-u", "$USER", "-g", "d"] # "-g d": separate arrays items
271
+ stdout, stderr = run_cmd(cmd, logger=self._app.submission_logger)
270
272
  if stderr:
271
273
  raise ValueError(
272
274
  f"Could not get query SGE jobs. Command was: {cmd!r}; stderr was: "
273
275
  f"{stderr}"
274
276
  )
275
277
  elif not stdout:
276
- info = {}
277
- else:
278
- info = {}
279
- lines = stdout.split("\n")
280
- # assuming a job name with spaces means we can't split on spaces to get
281
- # anywhere beyond the job name, so get the column index of the state heading
282
- # and assume the state is always left-aligned with the heading:
283
- state_idx = lines[0].index("state")
284
- task_id_idx = lines[0].index("ja-task-ID")
285
- for ln in lines[2:]:
286
- if not ln:
287
- continue
288
- ln_s = ln.split()
289
- base_job_ID = ln_s[0]
290
-
291
- # states can be one or two chars (for our limited purposes):
292
- state_str = ln[state_idx : state_idx + 2].strip()
293
- state = self.state_lookup[state_str]
294
-
295
- arr_idx = ln[task_id_idx:].strip()
296
- if arr_idx:
297
- arr_idx = int(arr_idx) - 1 # zero-index
298
- else:
299
- arr_idx = None
300
-
301
- if base_job_ID not in info:
302
- info[base_job_ID] = {}
303
-
304
- info[base_job_ID][arr_idx] = state
278
+ return {}
279
+
280
+ info: dict[str, dict[int | None, JobscriptElementState]] = {}
281
+ lines = stdout.split("\n")
282
+ # assuming a job name with spaces means we can't split on spaces to get
283
+ # anywhere beyond the job name, so get the column index of the state heading
284
+ # and assume the state is always left-aligned with the heading:
285
+ state_idx = lines[0].index("state")
286
+ task_id_idx = lines[0].index("ja-task-ID")
287
+ for ln in lines[2:]:
288
+ if not ln:
289
+ continue
290
+ base_job_ID, *_ = ln.split()
291
+
292
+ # states can be one or two chars (for our limited purposes):
293
+ state_str = ln[state_idx : state_idx + 2].strip()
294
+ state = self.state_lookup[state_str]
295
+
296
+ arr_idx_s = ln[task_id_idx:].strip()
297
+ arr_idx = (
298
+ int(arr_idx_s) - 1 # We are using zero-indexed info
299
+ if arr_idx_s
300
+ else None
301
+ )
302
+
303
+ info.setdefault(base_job_ID, {})[arr_idx] = state
305
304
  return info
306
305
 
306
+ @override
307
307
  def get_job_state_info(
308
- self, js_refs: List[str] = None
309
- ) -> Dict[str, Dict[int, JobscriptElementState]]:
308
+ self, *, js_refs: Sequence[str] | None = None, num_js_elements: int = 0
309
+ ) -> Mapping[str, Mapping[int | None, JobscriptElementState]]:
310
310
  """Query the scheduler to get the states of all of this user's jobs, optionally
311
311
  filtering by specified job IDs.
312
312
 
@@ -316,23 +316,29 @@ class SGEPosix(Scheduler):
316
316
  """
317
317
  info = self.get_job_statuses()
318
318
  if js_refs:
319
- info = {k: v for k, v in info.items() if k in js_refs}
319
+ return {k: v for k, v in info.items() if k in js_refs}
320
320
  return info
321
321
 
322
- def cancel_jobs(self, js_refs: List[str], jobscripts: List = None):
322
+ @override
323
+ def cancel_jobs(
324
+ self,
325
+ js_refs: list[str],
326
+ jobscripts: list[Jobscript] | None = None,
327
+ num_js_elements: int = 0, # Ignored!
328
+ ):
323
329
  """
324
330
  Cancel submitted jobs.
325
331
  """
326
332
  cmd = [self.del_cmd] + js_refs
327
- self.app.submission_logger.info(
333
+ self._app.submission_logger.info(
328
334
  f"cancelling {self.__class__.__name__} jobscripts with command: {cmd}."
329
335
  )
330
- stdout, stderr = run_cmd(cmd, logger=self.app.submission_logger)
336
+ stdout, stderr = run_cmd(cmd, logger=self._app.submission_logger)
331
337
  if stderr:
332
338
  raise ValueError(
333
339
  f"Could not get query SGE {self.__class__.__name__}. Command was: "
334
340
  f"{cmd!r}; stderr was: {stderr}"
335
341
  )
336
- self.app.submission_logger.info(
342
+ self._app.submission_logger.info(
337
343
  f"jobscripts cancel command executed; stdout was: {stdout}."
338
344
  )