snakemake-executor-plugin-slurm 1.8.0__py3-none-any.whl → 1.9.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.

Potentially problematic release.


This version of snakemake-executor-plugin-slurm might be problematic. Click here for more details.

@@ -18,6 +18,7 @@ import uuid
18
18
 
19
19
  from snakemake_interface_executor_plugins.executors.base import SubmittedJobInfo
20
20
  from snakemake_interface_executor_plugins.executors.remote import RemoteExecutor
21
+
21
22
  from snakemake_interface_executor_plugins.settings import (
22
23
  ExecutorSettingsBase,
23
24
  CommonSettings,
@@ -34,6 +35,7 @@ from .utils import (
34
35
  )
35
36
  from .efficiency_report import create_efficiency_report
36
37
  from .submit_string import get_submit_command
38
+ from .validation import validate_slurm_extra
37
39
 
38
40
 
39
41
  @dataclass
@@ -339,17 +341,6 @@ class Executor(RemoteExecutor):
339
341
  "- submitting without. This might or might not work on your cluster."
340
342
  )
341
343
 
342
- # MPI job
343
- if job.resources.get("mpi", False):
344
- if not job.resources.get("tasks_per_node") and not job.resources.get(
345
- "nodes"
346
- ):
347
- self.logger.warning(
348
- "MPI job detected, but no 'tasks_per_node' or 'nodes' "
349
- "specified. Assuming 'tasks_per_node=1'."
350
- "Probably not what you want."
351
- )
352
-
353
344
  exec_job = self.format_job_exec(job)
354
345
 
355
346
  # and finally the job to execute with all the snakemake parameters
@@ -682,6 +673,7 @@ We leave it to SLURM to resume your job(s)"""
682
673
  # we have to quote the account, because it might
683
674
  # contain build-in shell commands - see issue #354
684
675
  for account in accounts:
676
+ self.test_account(account)
685
677
  yield f" -A {shlex.quote(account)}"
686
678
  else:
687
679
  if self._fallback_account_arg is None:
@@ -820,13 +812,5 @@ We leave it to SLURM to resume your job(s)"""
820
812
  return ""
821
813
 
822
814
  def check_slurm_extra(self, job):
823
- jobname = re.compile(r"--job-name[=?|\s+]|-J\s?")
824
- if re.search(jobname, job.resources.slurm_extra):
825
- raise WorkflowError(
826
- "The --job-name option is not allowed in the 'slurm_extra' parameter. "
827
- "The job name is set by snakemake and must not be overwritten. "
828
- "It is internally used to check the stati of the all submitted jobs "
829
- "by this workflow."
830
- "Please consult the documentation if you are unsure how to "
831
- "query the status of your jobs."
832
- )
815
+ """Validate that slurm_extra doesn't contain executor-managed options."""
816
+ validate_slurm_extra(job)
@@ -3,24 +3,67 @@ import pandas as pd
3
3
  from pathlib import Path
4
4
  import subprocess
5
5
  import shlex
6
-
7
- import os # only temporarily needed for printf debugging
6
+ from datetime import datetime
8
7
  import numpy as np
8
+ import os
9
9
 
10
10
 
11
11
  def time_to_seconds(time_str):
12
- """Convert SLURM time format to seconds."""
13
- if pd.isna(time_str) or time_str.strip() == "":
12
+ """
13
+ Convert SLURM sacct time format to seconds.
14
+
15
+ Handles sacct output formats:
16
+ - Elapsed: [D-]HH:MM:SS or [DD-]HH:MM:SS (no fractional seconds)
17
+ - TotalCPU: [D-][HH:]MM:SS or [DD-][HH:]MM:SS (with fractional seconds)
18
+
19
+ Examples:
20
+ - "1-12:30:45" -> 131445 seconds (1 day + 12h 30m 45s)
21
+ - "23:59:59" -> 86399 seconds
22
+ - "45:30" -> 2730 seconds (45 minutes 30 seconds)
23
+ - "30.5" -> 30.5 seconds (fractional seconds for TotalCPU)
24
+ """
25
+ if (
26
+ pd.isna(time_str)
27
+ or str(time_str).strip() == ""
28
+ or str(time_str).strip() == "invalid"
29
+ ):
14
30
  return 0
15
- parts = time_str.split(":")
16
-
17
- if len(parts) == 3: # H:M:S
18
- return int(parts[0]) * 3600 + int(parts[1]) * 60 + float(parts[2])
19
- elif len(parts) == 2: # M:S
20
- return int(parts[0]) * 60 + float(parts[1])
21
- elif len(parts) == 1: # S
22
- return float(parts[0])
23
- return 0
31
+
32
+ time_str = str(time_str).strip()
33
+
34
+ # Try different SLURM time formats with datetime parsing
35
+ time_formats = [
36
+ "%d-%H:%M:%S.%f", # D-HH:MM:SS.ffffff (with fractional seconds)
37
+ "%d-%H:%M:%S", # D-HH:MM:SS
38
+ "%d-%M:%S", # D-MM:SS
39
+ "%d-%M:%S.%f", # D-MM:SS.ffffff (with fractional seconds)
40
+ "%H:%M:%S.%f", # HH:MM:SS.ffffff (with fractional seconds)
41
+ "%H:%M:%S", # HH:MM:SS
42
+ "%M:%S.%f", # MM:SS.ffffff (with fractional seconds)
43
+ "%M:%S", # MM:SS
44
+ "%S.%f", # SS.ffffff (with fractional seconds)
45
+ "%S", # SS
46
+ ]
47
+
48
+ for fmt in time_formats:
49
+ try:
50
+ time_obj = datetime.strptime(time_str, fmt)
51
+
52
+ total_seconds = (
53
+ time_obj.hour * 3600
54
+ + time_obj.minute * 60
55
+ + time_obj.second
56
+ + time_obj.microsecond / 1000000
57
+ )
58
+
59
+ # Add days if present (datetime treats day 1 as the first day)
60
+ if fmt.startswith("%d-"):
61
+ total_seconds += time_obj.day * 86400
62
+
63
+ return total_seconds
64
+ except ValueError:
65
+ continue
66
+ return 0 # If all parsing attempts fail, return 0
24
67
 
25
68
 
26
69
  def parse_maxrss(maxrss):
@@ -1,3 +1,4 @@
1
+ from snakemake_interface_common.exceptions import WorkflowError
1
2
  from snakemake_executor_plugin_slurm_jobstep import get_cpu_setting
2
3
  from types import SimpleNamespace
3
4
  import shlex
@@ -83,7 +84,36 @@ def get_submit_command(job, params):
83
84
  else:
84
85
  # fixes #40 - set ntasks regardless of mpi, because
85
86
  # SLURM v22.05 will require it for all jobs
86
- call += f" --ntasks={job.resources.get('tasks') or 1}"
87
+ # if the job is a MPI job, ntasks will be set later
88
+ if not job.resources.get("mpi", False):
89
+ call += f" --ntasks={job.resources.get('tasks') or 1}"
90
+
91
+ # if the job is an MPI job, we need to have some task setting:
92
+ if job.resources.get("mpi", False):
93
+ if not job.resources.get("tasks_per_node") and not job.resources.get("tasks"):
94
+ raise WorkflowError(
95
+ "For MPI jobs, please specify either "
96
+ "'tasks_per_node' or 'tasks' (at least one is required)."
97
+ )
98
+ # raise an error if both task settings are used
99
+ if job.resources.get("tasks_per_node") and job.resources.get("tasks"):
100
+ raise WorkflowError(
101
+ "For MPI jobs, please specify either 'tasks_per_node' or 'tasks', "
102
+ "but not both."
103
+ )
104
+ if job.resources.get("tasks_per_node"):
105
+ if job.resources.get("tasks_per_node") <= 1:
106
+ raise WorkflowError(
107
+ "For MPI jobs, 'tasks_per_node' must be greater than 1."
108
+ )
109
+ call += f" --ntasks-per-node={job.resources.tasks_per_node}"
110
+ elif job.resources.get("tasks"):
111
+ if job.resources.get("tasks") == 1:
112
+ raise WorkflowError("For MPI jobs, 'tasks' must be greater than 1.")
113
+ call += f" --ntasks={job.resources.tasks}"
114
+ # nodes CAN be set independently of tasks or tasks_per_node
115
+ # this is at a user's discretion. The nodes flag might already
116
+ # be set above, if the user specified it.
87
117
 
88
118
  # we need to set cpus-per-task OR cpus-per-gpu, the function
89
119
  # will return a string with the corresponding value
@@ -0,0 +1,75 @@
1
+ """
2
+ SLURM parameter validation functions for the Snakemake executor plugin.
3
+ """
4
+
5
+ import re
6
+ from snakemake_interface_common.exceptions import WorkflowError
7
+
8
+
9
+ def get_forbidden_slurm_options():
10
+ """
11
+ Return a dictionary of forbidden SLURM options that the executor manages.
12
+
13
+ Returns:
14
+ dict: Mapping of regex patterns to human-readable option names
15
+ """
16
+ return {
17
+ # Job identification and output
18
+ r"--job-name[=\s]|-J\s?": "job name",
19
+ r"--output[=\s]|-o\s": "output file",
20
+ r"--error[=\s]|-e\s": "error file",
21
+ r"--parsable": "parsable output",
22
+ r"--export[=\s]": "environment export",
23
+ r"--comment[=\s]": "job comment",
24
+ r"--workdir[=\s]|-D\s": "working directory",
25
+ # Account and partition
26
+ r"--account[=\s]|-A\s": "account",
27
+ r"--partition[=\s]|-p\s": "partition",
28
+ # Memory options
29
+ r"--mem[=\s]": "memory",
30
+ r"--mem-per-cpu[=\s]": "memory per CPU",
31
+ # CPU and task options
32
+ r"--ntasks[=\s]|-n\s": "number of tasks",
33
+ r"--ntasks-per-gpu[=\s]": "tasks per GPU",
34
+ r"--cpus-per-task[=\s]|-c\s": "CPUs per task",
35
+ r"--cpus-per-gpu[=\s]": "CPUs per GPU",
36
+ # Time and resource constraints
37
+ r"--time[=\s]|-t\s": "runtime/time limit",
38
+ r"--constraint[=\s]|-C\s": "node constraints",
39
+ r"--qos[=\s]": "quality of service",
40
+ r"--nodes[=\s]|-N\s": "number of nodes",
41
+ r"--clusters[=\s]": "cluster specification",
42
+ # GPU options
43
+ r"--gres[=\s]": "generic resources (GRES)",
44
+ r"--gpus[=\s]": "GPU allocation",
45
+ }
46
+
47
+
48
+ def validate_slurm_extra(job):
49
+ """
50
+ Validate that slurm_extra doesn't contain executor-managed options.
51
+
52
+ Args:
53
+ job: Snakemake job object with resources attribute
54
+
55
+ Raises:
56
+ WorkflowError: If forbidden SLURM options are found in slurm_extra
57
+ """
58
+ # skip testing if no slurm_extra is set
59
+ slurm_extra = getattr(job.resources, "slurm_extra", None)
60
+ if not slurm_extra:
61
+ return
62
+
63
+ forbidden_options = get_forbidden_slurm_options()
64
+
65
+ for pattern, option_name in forbidden_options.items():
66
+ if re.search(pattern, slurm_extra):
67
+ raise WorkflowError(
68
+ f"The --{option_name.replace(' ', '-')} option is not "
69
+ f"allowed in the 'slurm_extra' parameter. "
70
+ f"The {option_name} is set by the snakemake executor plugin "
71
+ f"and must not be overwritten. "
72
+ f"Please use the appropriate snakemake resource "
73
+ f"specification instead. "
74
+ f"Consult the documentation for proper resource configuration."
75
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: snakemake-executor-plugin-slurm
3
- Version: 1.8.0
3
+ Version: 1.9.1
4
4
  Summary: A Snakemake executor plugin for submitting jobs to a SLURM cluster.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -0,0 +1,9 @@
1
+ snakemake_executor_plugin_slurm/__init__.py,sha256=YUFJMWKlrbId4VgRreaea3B0JXJ_xTB1zNBJgAZ59Vw,33953
2
+ snakemake_executor_plugin_slurm/efficiency_report.py,sha256=3F6AqkKOZBn9MxI7T27u3FF3-OAxY_ee_toHXnTGirs,8812
3
+ snakemake_executor_plugin_slurm/submit_string.py,sha256=HL2QyT7-_HRu4yg_7tJSs1VtxGebNODOV8OQd2hYirg,5021
4
+ snakemake_executor_plugin_slurm/utils.py,sha256=7XVXtzu7bg_89wWZisW-Zk7TNQyEgK4v_y4Y3F9uOwc,4491
5
+ snakemake_executor_plugin_slurm/validation.py,sha256=hyWQWDjqHDLhO7caksYILTKjA2Ak91CMJeFwEcltYFo,2702
6
+ snakemake_executor_plugin_slurm-1.9.1.dist-info/METADATA,sha256=kbo2DqmPPULY7OAXxivzfXA4nFk-KJoRP7iyyLx7Gow,1507
7
+ snakemake_executor_plugin_slurm-1.9.1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
8
+ snakemake_executor_plugin_slurm-1.9.1.dist-info/licenses/LICENSE,sha256=YVc4xTLWMqGfFL36120k7rzXtsT6e4RkJsh68VVn12s,1076
9
+ snakemake_executor_plugin_slurm-1.9.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.2.0
2
+ Generator: poetry-core 2.2.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,8 +0,0 @@
1
- snakemake_executor_plugin_slurm/__init__.py,sha256=G40Rbm62GFQF7bo38OJBFEFAxa4BLYeNO66wPQuki-c,34755
2
- snakemake_executor_plugin_slurm/efficiency_report.py,sha256=crPfJDK4NojfRbu_wEw3ZmC3suMRABr5r-1rO5q3WEo,7429
3
- snakemake_executor_plugin_slurm/submit_string.py,sha256=12DaJ9BIqEMC19dKuViPU8W5mZRjBRtNDQQu7M-iEjM,3448
4
- snakemake_executor_plugin_slurm/utils.py,sha256=7XVXtzu7bg_89wWZisW-Zk7TNQyEgK4v_y4Y3F9uOwc,4491
5
- snakemake_executor_plugin_slurm-1.8.0.dist-info/METADATA,sha256=8-ktC0sGhHDPOAyxpFl1LwWc7wPueZ2PxREKrQguhME,1507
6
- snakemake_executor_plugin_slurm-1.8.0.dist-info/WHEEL,sha256=M5asmiAlL6HEcOq52Yi5mmk9KmTVjY2RDPtO4p9DMrc,88
7
- snakemake_executor_plugin_slurm-1.8.0.dist-info/licenses/LICENSE,sha256=YVc4xTLWMqGfFL36120k7rzXtsT6e4RkJsh68VVn12s,1076
8
- snakemake_executor_plugin_slurm-1.8.0.dist-info/RECORD,,