snakemake-executor-plugin-sge 0.6.0__tar.gz → 0.6.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: snakemake-executor-plugin-sge
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: A Snakemake executor plugin for submitting jobs to SGE/UGE/OGS clusters.
5
5
  License: MIT
6
6
  Keywords: snakemake,plugin,executor,cluster,sge,uge,gridengine
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "snakemake-executor-plugin-sge"
3
- version = "0.6.0"
3
+ version = "0.6.2"
4
4
  description = "A Snakemake executor plugin for submitting jobs to SGE/UGE/OGS clusters."
5
5
  authors = ["Stylianos Charalampous <stylianosc@github.com>"]
6
6
  readme = "README.md"
@@ -153,7 +153,7 @@ class ExecutorSettings(ExecutorSettingsBase):
153
153
  metadata={
154
154
  "help": (
155
155
  "Directory for SGE log files. Defaults to "
156
- "'.sge_logs' relative to the working directory. "
156
+ "'.snakemake/sge_logs' relative to the working directory. "
157
157
  "Absolute paths are used as-is; relative paths are resolved "
158
158
  "against the workflow working directory."
159
159
  ),
@@ -306,7 +306,7 @@ def _resolve_logdir(workflow) -> Path:
306
306
  elif logdir:
307
307
  return Path(workflow.workdir_init) / logdir
308
308
  else:
309
- return (Path(workflow.workdir_init) / ".sge_logs").resolve()
309
+ return (Path(workflow.workdir_init) / ".snakemake" / "sge_logs").resolve()
310
310
 
311
311
 
312
312
  def _wildcard_sort_key(job: JobExecutorInterface):
@@ -351,8 +351,8 @@ class Executor(RemoteExecutor):
351
351
  )
352
352
  self.logger.info(f"SGE run ID: {self.run_uuid}")
353
353
 
354
- self.sge_logdir = _resolve_logdir(self.workflow)
355
- self.sge_logdir.mkdir(parents=True, exist_ok=True)
354
+ self.sge_logdir_default = _resolve_logdir(self.workflow)
355
+ self.sge_logdir_default.mkdir(parents=True, exist_ok=True)
356
356
 
357
357
  self._job_submission_executor = ThreadPoolExecutor(
358
358
  max_workers=4, thread_name_prefix="sge_job_submit"
@@ -616,18 +616,37 @@ class Executor(RemoteExecutor):
616
616
  dep_ids.append(base_id)
617
617
  return dep_ids
618
618
 
619
+ def _get_job_logdir(self, job: JobExecutorInterface) -> Path:
620
+ """Get the SGE log directory for a job.
621
+
622
+ If the job specifies a workdir resource, place logs in
623
+ {workdir}/.snakemake/sge_logs. Otherwise use the default logdir.
624
+ """
625
+ workdir = job.resources.get("workdir") if hasattr(job, "resources") else None
626
+ if workdir:
627
+ return (Path(workdir) / ".snakemake" / "sge_logs").resolve()
628
+ return self.sge_logdir_default
629
+
619
630
  def run_job(self, job: JobExecutorInterface) -> None:
620
631
  """Submit a single job via qsub."""
621
- # Log files are stored directly in sge_logdir with job ID
632
+ # Determine job-specific log directory (uses workdir resource if available)
633
+ job_logdir = self._get_job_logdir(job)
634
+ job_logdir.mkdir(parents=True, exist_ok=True)
635
+
636
+ # Log files are stored directly in job_logdir with job ID
622
637
  # stdout: $JOB_ID.log, stderr: $JOB_ID.error
623
- log_stdout = self.sge_logdir / "$JOB_ID.log"
624
- log_stderr = self.sge_logdir / "$JOB_ID.error"
638
+ log_stdout = job_logdir / "$JOB_ID.log"
639
+ log_stderr = job_logdir / "$JOB_ID.error"
640
+
641
+ # Use job's workdir resource if available, else fall back to workflow workdir
642
+ workdir = job.resources.get("workdir") if hasattr(job, "resources") else None
643
+ workdir = workdir or self.workflow.workdir_init
625
644
 
626
645
  job_params = {
627
646
  "run_uuid": self.run_uuid,
628
647
  "log_stdout": log_stdout,
629
648
  "log_stderr": log_stderr,
630
- "workdir": self.workflow.workdir_init,
649
+ "workdir": workdir,
631
650
  }
632
651
 
633
652
  # Resolve upstream SGE job IDs for -hold_jid (needed for --immediate-submit)
@@ -674,15 +693,15 @@ class Executor(RemoteExecutor):
674
693
 
675
694
  self.logger.info(
676
695
  f"Job {job.jobid} submitted as SGE job {sge_jobid} "
677
- f"(log: {self.sge_logdir})"
696
+ f"(log: {job_logdir})"
678
697
  )
679
698
  self._submitted_job_ids.append(sge_jobid)
680
699
  # Record the job→SGE-id mapping BEFORE notifying Snakemake so any
681
700
  # downstream submission triggered by the report sees it.
682
701
  self._job_to_sge[job] = (sge_jobid, None)
683
702
  # Resolve the actual log path now that we have the job ID
684
- log_stdout_resolved = self.sge_logdir / f"{sge_jobid}.log"
685
- log_stderr_resolved = self.sge_logdir / f"{sge_jobid}.error"
703
+ log_stdout_resolved = job_logdir / f"{sge_jobid}.log"
704
+ log_stderr_resolved = job_logdir / f"{sge_jobid}.error"
686
705
  self._report_submission_threadsafe(
687
706
  SubmittedJobInfo(
688
707
  job,
@@ -724,8 +743,12 @@ class Executor(RemoteExecutor):
724
743
  else f"rule_{jobs[0].name}"
725
744
  )
726
745
 
746
+ # Determine array-specific log directory from first job's workdir resource
747
+ first_job_logdir = self._get_job_logdir(jobs[0])
748
+ first_job_logdir.mkdir(parents=True, exist_ok=True)
749
+
727
750
  # Helper files (manifests, scripts, task maps) are stored in .meta subdirectory
728
- meta_dir = self.sge_logdir / ".meta" / group_or_rule
751
+ meta_dir = first_job_logdir / ".meta" / group_or_rule
729
752
  meta_dir.mkdir(parents=True, exist_ok=True)
730
753
 
731
754
  # Determine the global starting task index for this batch.
@@ -828,11 +851,15 @@ class Executor(RemoteExecutor):
828
851
  script_path.write_text(script_content)
829
852
  script_path.chmod(0o755)
830
853
 
854
+ # Use first job's workdir resource if available, else fall back to workflow workdir
855
+ workdir = jobs[0].resources.get("workdir") if hasattr(jobs[0], "resources") else None
856
+ workdir = workdir or self.workflow.workdir_init
857
+
831
858
  job_params = {
832
859
  "run_uuid": self.run_uuid,
833
- "log_stdout": self.sge_logdir / "$JOB_ID.$TASK_ID.log",
834
- "log_stderr": self.sge_logdir / "$JOB_ID.$TASK_ID.error",
835
- "workdir": self.workflow.workdir_init,
860
+ "log_stdout": first_job_logdir / "$JOB_ID.$TASK_ID.log",
861
+ "log_stderr": first_job_logdir / "$JOB_ID.$TASK_ID.error",
862
+ "workdir": workdir,
836
863
  "array_range": f"{chunk_start}-{chunk_end}",
837
864
  }
838
865
 
@@ -921,8 +948,9 @@ class Executor(RemoteExecutor):
921
948
  # Register each task with Snakemake
922
949
  for task_idx, job in enumerate(chunk_jobs, start=chunk_start):
923
950
  external_id = f"{sge_jobid}.{task_idx}"
924
- log_o = self.sge_logdir / f"{sge_jobid}.{task_idx}.log"
925
- log_e = self.sge_logdir / f"{sge_jobid}.{task_idx}.error"
951
+ # Each job in the array may have its own logdir; use the first job's for the array
952
+ log_o = first_job_logdir / f"{sge_jobid}.{task_idx}.log"
953
+ log_e = first_job_logdir / f"{sge_jobid}.{task_idx}.error"
926
954
  self._report_submission_threadsafe(
927
955
  SubmittedJobInfo(
928
956
  job,
@@ -1052,7 +1080,7 @@ class Executor(RemoteExecutor):
1052
1080
  self.logger.debug(
1053
1081
  f"Cleaning SGE log files older than {age_cutoff} day(s)."
1054
1082
  )
1055
- for path in self.sge_logdir.rglob("*"):
1083
+ for path in self.sge_logdir_default.rglob("*"):
1056
1084
  if path.is_file():
1057
1085
  try:
1058
1086
  if now - path.stat().st_mtime > cutoff_secs:
@@ -1060,7 +1088,7 @@ class Executor(RemoteExecutor):
1060
1088
  except OSError as exc:
1061
1089
  self.logger.warning(f"Could not delete log {path}: {exc}")
1062
1090
  # Clean up empty directories
1063
- for path in sorted(self.sge_logdir.rglob("*"), reverse=True):
1091
+ for path in sorted(self.sge_logdir_default.rglob("*"), reverse=True):
1064
1092
  if path.is_dir():
1065
1093
  try:
1066
1094
  path.rmdir() # Only removes if empty