looper 1.7.0a1__py3-none-any.whl → 2.0.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- looper/__main__.py +1 -1
- looper/_version.py +2 -1
- looper/cli_divvy.py +10 -6
- looper/cli_pydantic.py +413 -0
- looper/command_models/DEVELOPER.md +85 -0
- looper/command_models/README.md +4 -0
- looper/command_models/__init__.py +6 -0
- looper/command_models/arguments.py +293 -0
- looper/command_models/commands.py +335 -0
- looper/conductor.py +161 -28
- looper/const.py +9 -0
- looper/divvy.py +56 -47
- looper/exceptions.py +9 -1
- looper/looper.py +196 -168
- looper/pipeline_interface.py +2 -12
- looper/project.py +154 -176
- looper/schemas/pipeline_interface_schema_generic.yaml +14 -6
- looper/utils.py +450 -78
- {looper-1.7.0a1.dist-info → looper-2.0.0.dist-info}/METADATA +24 -14
- {looper-1.7.0a1.dist-info → looper-2.0.0.dist-info}/RECORD +24 -19
- {looper-1.7.0a1.dist-info → looper-2.0.0.dist-info}/WHEEL +1 -1
- {looper-1.7.0a1.dist-info → looper-2.0.0.dist-info}/entry_points.txt +1 -1
- looper/cli_looper.py +0 -788
- {looper-1.7.0a1.dist-info → looper-2.0.0.dist-info}/LICENSE.txt +0 -0
- {looper-1.7.0a1.dist-info → looper-2.0.0.dist-info}/top_level.txt +0 -0
looper/conductor.py
CHANGED
@@ -4,9 +4,12 @@ import importlib
|
|
4
4
|
import logging
|
5
5
|
import os
|
6
6
|
import subprocess
|
7
|
+
import signal
|
8
|
+
import psutil
|
9
|
+
import sys
|
7
10
|
import time
|
8
11
|
import yaml
|
9
|
-
from
|
12
|
+
from math import ceil
|
10
13
|
from json import loads
|
11
14
|
from subprocess import check_output
|
12
15
|
from typing import *
|
@@ -18,14 +21,19 @@ from jinja2.exceptions import UndefinedError
|
|
18
21
|
from peppy.const import CONFIG_KEY, SAMPLE_NAME_ATTR, SAMPLE_YAML_EXT
|
19
22
|
from peppy.exceptions import RemoteYAMLError
|
20
23
|
from pipestat import PipestatError
|
21
|
-
from ubiquerg import expandpath
|
24
|
+
from ubiquerg import expandpath
|
22
25
|
from yaml import dump
|
23
|
-
from yacman import YAMLConfigManager
|
26
|
+
from yacman import FutureYAMLConfigManager as YAMLConfigManager
|
24
27
|
|
25
28
|
from .const import *
|
26
|
-
from .exceptions import JobSubmissionException
|
29
|
+
from .exceptions import JobSubmissionException
|
27
30
|
from .processed_project import populate_sample_paths
|
28
|
-
from .utils import
|
31
|
+
from .utils import (
|
32
|
+
fetch_sample_flags,
|
33
|
+
jinja_render_template_strictly,
|
34
|
+
expand_nested_var_templates,
|
35
|
+
)
|
36
|
+
from .const import PipelineLevel
|
29
37
|
|
30
38
|
|
31
39
|
_LOGGER = logging.getLogger(__name__)
|
@@ -84,11 +92,23 @@ def _get_yaml_path(namespaces, template_key, default_name_appendix="", filename=
|
|
84
92
|
|
85
93
|
def write_pipestat_config(looper_pipestat_config_path, pipestat_config_dict):
|
86
94
|
"""
|
87
|
-
This
|
95
|
+
This writes a combined configuration file to be passed to a PipestatManager.
|
96
|
+
:param str looper_pipestat_config_path: path to the created pipestat configuration file
|
97
|
+
:param dict pipestat_config_dict: the dict containing key value pairs to be written to the pipestat configutation
|
98
|
+
return bool
|
88
99
|
"""
|
100
|
+
|
101
|
+
if not os.path.exists(os.path.dirname(looper_pipestat_config_path)):
|
102
|
+
try:
|
103
|
+
os.makedirs(os.path.dirname(looper_pipestat_config_path))
|
104
|
+
except FileExistsError:
|
105
|
+
pass
|
106
|
+
|
89
107
|
with open(looper_pipestat_config_path, "w") as f:
|
90
108
|
yaml.dump(pipestat_config_dict, f)
|
91
|
-
|
109
|
+
_LOGGER.debug(
|
110
|
+
msg=f"Initialized pipestat config file: {looper_pipestat_config_path}"
|
111
|
+
)
|
92
112
|
|
93
113
|
return True
|
94
114
|
|
@@ -132,6 +152,7 @@ class SubmissionConductor(object):
|
|
132
152
|
compute_variables=None,
|
133
153
|
max_cmds=None,
|
134
154
|
max_size=None,
|
155
|
+
max_jobs=None,
|
135
156
|
automatic=True,
|
136
157
|
collate=False,
|
137
158
|
):
|
@@ -166,14 +187,20 @@ class SubmissionConductor(object):
|
|
166
187
|
include in a single job script.
|
167
188
|
:param int | float | NoneType max_size: Upper bound on total file
|
168
189
|
size of inputs used by the commands lumped into single job script.
|
190
|
+
:param int | float | NoneType max_jobs: Upper bound on total number of jobs to
|
191
|
+
group samples for submission.
|
169
192
|
:param bool automatic: Whether the submission should be automatic once
|
170
193
|
the pool reaches capacity.
|
171
194
|
:param bool collate: Whether a collate job is to be submitted (runs on
|
172
195
|
the project level, rather that on the sample level)
|
173
196
|
"""
|
174
197
|
super(SubmissionConductor, self).__init__()
|
198
|
+
|
175
199
|
self.collate = collate
|
176
200
|
self.section_key = PROJECT_PL_KEY if self.collate else SAMPLE_PL_KEY
|
201
|
+
self.pipeline_interface_type = (
|
202
|
+
"project_interface" if self.collate else "sample_interface"
|
203
|
+
)
|
177
204
|
self.pl_iface = pipeline_interface
|
178
205
|
self.pl_name = self.pl_iface.pipeline_name
|
179
206
|
self.prj = prj
|
@@ -193,6 +220,7 @@ class SubmissionConductor(object):
|
|
193
220
|
self._curr_size = 0
|
194
221
|
self._failed_sample_names = []
|
195
222
|
self._curr_skip_pool = []
|
223
|
+
self.process_id = None # this is used for currently submitted subprocess
|
196
224
|
|
197
225
|
if self.extra_pipe_args:
|
198
226
|
_LOGGER.debug(
|
@@ -200,6 +228,16 @@ class SubmissionConductor(object):
|
|
200
228
|
"{}".format(self.extra_pipe_args)
|
201
229
|
)
|
202
230
|
|
231
|
+
if max_jobs:
|
232
|
+
if max_jobs == 0 or max_jobs < 0:
|
233
|
+
raise ValueError(
|
234
|
+
"If specified, max job command count must be a positive integer, greater than zero."
|
235
|
+
)
|
236
|
+
|
237
|
+
num_samples = len(self.prj.samples)
|
238
|
+
samples_per_job = num_samples / max_jobs
|
239
|
+
max_cmds = ceil(samples_per_job)
|
240
|
+
|
203
241
|
if not self.collate:
|
204
242
|
self.automatic = automatic
|
205
243
|
if max_cmds is None and max_size is None:
|
@@ -247,8 +285,12 @@ class SubmissionConductor(object):
|
|
247
285
|
|
248
286
|
:param bool frorce: whether to force the project submission (ignore status/flags)
|
249
287
|
"""
|
288
|
+
psms = {}
|
250
289
|
if self.prj.pipestat_configured_project:
|
251
|
-
|
290
|
+
for piface in self.prj.project_pipeline_interfaces:
|
291
|
+
if piface.psm.pipeline_type == PipelineLevel.PROJECT.value:
|
292
|
+
psms[piface.psm.pipeline_name] = piface.psm
|
293
|
+
psm = psms[self.pl_name]
|
252
294
|
status = psm.get_status()
|
253
295
|
if not force and status is not None:
|
254
296
|
_LOGGER.info(f"> Skipping project. Determined status: {status}")
|
@@ -274,12 +316,11 @@ class SubmissionConductor(object):
|
|
274
316
|
)
|
275
317
|
)
|
276
318
|
if self.prj.pipestat_configured:
|
277
|
-
|
278
|
-
sample_statuses = psms[self.pl_name].get_status(
|
319
|
+
sample_statuses = self.pl_iface.psm.get_status(
|
279
320
|
record_identifier=sample.sample_name
|
280
321
|
)
|
281
322
|
if sample_statuses == "failed" and rerun is True:
|
282
|
-
|
323
|
+
self.pl_iface.psm.set_status(
|
283
324
|
record_identifier=sample.sample_name, status_identifier="waiting"
|
284
325
|
)
|
285
326
|
sample_statuses = "waiting"
|
@@ -289,23 +330,27 @@ class SubmissionConductor(object):
|
|
289
330
|
|
290
331
|
use_this_sample = True # default to running this sample
|
291
332
|
msg = None
|
333
|
+
if rerun and sample_statuses == []:
|
334
|
+
msg = f"> Skipping sample because rerun requested, but no failed or waiting flag found."
|
335
|
+
use_this_sample = False
|
292
336
|
if sample_statuses:
|
293
337
|
status_str = ", ".join(sample_statuses)
|
294
338
|
failed_flag = any("failed" in x for x in sample_statuses)
|
339
|
+
waiting_flag = any("waiting" in x for x in sample_statuses)
|
295
340
|
if self.ignore_flags:
|
296
341
|
msg = f"> Found existing status: {status_str}. Ignoring."
|
297
342
|
else: # this pipeline already has a status
|
298
343
|
msg = f"> Found existing status: {status_str}. Skipping sample."
|
299
|
-
if failed_flag:
|
344
|
+
if failed_flag and not rerun:
|
300
345
|
msg += " Use rerun to ignore failed status." # help guidance
|
301
346
|
use_this_sample = False
|
302
347
|
if rerun:
|
303
348
|
# Rescue the sample if rerun requested, and failed flag is found
|
304
|
-
if failed_flag:
|
305
|
-
msg = f"> Re-running
|
349
|
+
if failed_flag or waiting_flag:
|
350
|
+
msg = f"> Re-running sample. Status: {status_str}"
|
306
351
|
use_this_sample = True
|
307
352
|
else:
|
308
|
-
msg = f"> Skipping sample because rerun requested, but no failed flag found. Status: {status_str}"
|
353
|
+
msg = f"> Skipping sample because rerun requested, but no failed or waiting flag found. Status: {status_str}"
|
309
354
|
use_this_sample = False
|
310
355
|
if msg:
|
311
356
|
_LOGGER.info(msg)
|
@@ -358,6 +403,10 @@ class SubmissionConductor(object):
|
|
358
403
|
not for dry run)
|
359
404
|
"""
|
360
405
|
submitted = False
|
406
|
+
|
407
|
+
# Override signal handler so that Ctrl+C can be used to gracefully terminate child process
|
408
|
+
signal.signal(signal.SIGINT, self._signal_int_handler)
|
409
|
+
|
361
410
|
if not self._pool:
|
362
411
|
_LOGGER.debug("No submission (no pooled samples): %s", self.pl_name)
|
363
412
|
# submitted = False
|
@@ -386,9 +435,10 @@ class SubmissionConductor(object):
|
|
386
435
|
submission_command = "{} {}".format(sub_cmd, script)
|
387
436
|
# Capture submission command return value so that we can
|
388
437
|
# intercept and report basic submission failures; #167
|
389
|
-
|
390
|
-
|
391
|
-
|
438
|
+
process = subprocess.Popen(submission_command, shell=True)
|
439
|
+
self.process_id = process.pid
|
440
|
+
process.wait()
|
441
|
+
if process.returncode != 0:
|
392
442
|
fails = (
|
393
443
|
"" if self.collate else [s.sample_name for s in self._samples]
|
394
444
|
)
|
@@ -455,6 +505,87 @@ class SubmissionConductor(object):
|
|
455
505
|
# name concordant with 1-based, not 0-based indexing.
|
456
506
|
return "lump{}".format(self._num_total_job_submissions + 1)
|
457
507
|
|
508
|
+
def _signal_int_handler(self, signal, frame):
|
509
|
+
"""
|
510
|
+
For catching interrupt (Ctrl +C) signals. Fails gracefully.
|
511
|
+
"""
|
512
|
+
signal_type = "SIGINT"
|
513
|
+
self._generic_signal_handler(signal_type)
|
514
|
+
|
515
|
+
def _generic_signal_handler(self, signal_type):
|
516
|
+
"""
|
517
|
+
Function for handling both SIGTERM and SIGINT
|
518
|
+
"""
|
519
|
+
message = "Received " + signal_type + ". Failing gracefully..."
|
520
|
+
_LOGGER.warning(msg=message)
|
521
|
+
|
522
|
+
self._terminate_current_subprocess()
|
523
|
+
|
524
|
+
sys.exit(1)
|
525
|
+
|
526
|
+
def _terminate_current_subprocess(self):
|
527
|
+
"""This terminates the current sub process associated with self.process_id"""
|
528
|
+
|
529
|
+
def pskill(proc_pid, sig=signal.SIGINT):
|
530
|
+
parent_process = psutil.Process(proc_pid)
|
531
|
+
for child_proc in parent_process.children(recursive=True):
|
532
|
+
child_proc.send_signal(sig)
|
533
|
+
parent_process.send_signal(sig)
|
534
|
+
|
535
|
+
if self.process_id is None:
|
536
|
+
return
|
537
|
+
|
538
|
+
# Gently wait for the subprocess before attempting to kill it
|
539
|
+
sys.stdout.flush()
|
540
|
+
still_running = self._attend_process(psutil.Process(self.process_id), 0)
|
541
|
+
sleeptime = 0.25
|
542
|
+
time_waiting = 0
|
543
|
+
|
544
|
+
while still_running and time_waiting < 3:
|
545
|
+
try:
|
546
|
+
if time_waiting > 2:
|
547
|
+
pskill(self.process_id, signal.SIGKILL)
|
548
|
+
elif time_waiting > 1:
|
549
|
+
pskill(self.process_id, signal.SIGTERM)
|
550
|
+
else:
|
551
|
+
pskill(self.process_id, signal.SIGINT)
|
552
|
+
|
553
|
+
except OSError:
|
554
|
+
# This would happen if the child process ended between the check
|
555
|
+
# and the next kill step
|
556
|
+
still_running = False
|
557
|
+
time_waiting = time_waiting + sleeptime
|
558
|
+
|
559
|
+
# Now see if it's still running
|
560
|
+
time_waiting = time_waiting + sleeptime
|
561
|
+
if not self._attend_process(psutil.Process(self.process_id), sleeptime):
|
562
|
+
still_running = False
|
563
|
+
|
564
|
+
if still_running:
|
565
|
+
_LOGGER.warning(f"Unable to halt child process: {self.process_id}")
|
566
|
+
else:
|
567
|
+
if time_waiting > 0:
|
568
|
+
note = f"terminated after {time_waiting} sec"
|
569
|
+
else:
|
570
|
+
note = "was already terminated"
|
571
|
+
_LOGGER.warning(msg=f"Child process {self.process_id} {note}.")
|
572
|
+
|
573
|
+
def _attend_process(self, proc, sleeptime):
|
574
|
+
"""
|
575
|
+
Waits on a process for a given time to see if it finishes, returns True
|
576
|
+
if it's still running after the given time or False as soon as it
|
577
|
+
returns.
|
578
|
+
|
579
|
+
:param psutil.Process proc: Process object opened by psutil.Popen()
|
580
|
+
:param float sleeptime: Time to wait
|
581
|
+
:return bool: True if process is still running; otherwise false
|
582
|
+
"""
|
583
|
+
try:
|
584
|
+
proc.wait(timeout=int(sleeptime))
|
585
|
+
except psutil.TimeoutExpired:
|
586
|
+
return True
|
587
|
+
return False
|
588
|
+
|
458
589
|
def _jobname(self, pool):
|
459
590
|
"""Create the name for a job submission."""
|
460
591
|
return "{}_{}".format(self.pl_iface.pipeline_name, self._sample_lump_name(pool))
|
@@ -514,12 +645,7 @@ class SubmissionConductor(object):
|
|
514
645
|
:return yacman.YAMLConfigManager: pipestat namespace
|
515
646
|
"""
|
516
647
|
try:
|
517
|
-
|
518
|
-
self.prj.get_pipestat_managers(sample_name)
|
519
|
-
if sample_name
|
520
|
-
else self.prj.get_pipestat_managers(project_level=True)
|
521
|
-
)
|
522
|
-
psm = psms[self.pl_iface.pipeline_name]
|
648
|
+
psm = self.pl_iface.psm
|
523
649
|
except (PipestatError, AttributeError) as e:
|
524
650
|
# pipestat section faulty or not found in project.looper or sample
|
525
651
|
# or project is missing required pipestat attributes
|
@@ -534,6 +660,8 @@ class SubmissionConductor(object):
|
|
534
660
|
"results_file": psm.file,
|
535
661
|
"record_identifier": psm.record_identifier,
|
536
662
|
"config_file": psm.config_path,
|
663
|
+
"output_schema": psm.cfg["_schema_path"],
|
664
|
+
"pephub_path": psm.cfg["pephub_path"],
|
537
665
|
}
|
538
666
|
filtered_namespace = {k: v for k, v in full_namespace.items() if v}
|
539
667
|
return YAMLConfigManager(filtered_namespace)
|
@@ -557,7 +685,11 @@ class SubmissionConductor(object):
|
|
557
685
|
pipeline=self.pl_iface,
|
558
686
|
compute=self.prj.dcc.compute,
|
559
687
|
)
|
560
|
-
|
688
|
+
|
689
|
+
if self.pipeline_interface_type is None:
|
690
|
+
templ = self.pl_iface["command_template"]
|
691
|
+
else:
|
692
|
+
templ = self.pl_iface[self.pipeline_interface_type]["command_template"]
|
561
693
|
if not self.override_extra:
|
562
694
|
extras_template = (
|
563
695
|
EXTRA_PROJECT_CMD_TEMPLATE
|
@@ -597,8 +729,10 @@ class SubmissionConductor(object):
|
|
597
729
|
_LOGGER.debug(f"namespace pipelines: { pl_iface }")
|
598
730
|
|
599
731
|
namespaces["pipeline"]["var_templates"] = pl_iface[VAR_TEMPL_KEY] or {}
|
600
|
-
|
601
|
-
|
732
|
+
|
733
|
+
namespaces["pipeline"]["var_templates"] = expand_nested_var_templates(
|
734
|
+
namespaces["pipeline"]["var_templates"], namespaces
|
735
|
+
)
|
602
736
|
|
603
737
|
# pre_submit hook namespace updates
|
604
738
|
namespaces = _exec_pre_submit(pl_iface, namespaces)
|
@@ -607,7 +741,6 @@ class SubmissionConductor(object):
|
|
607
741
|
argstring = jinja_render_template_strictly(
|
608
742
|
template=templ, namespaces=namespaces
|
609
743
|
)
|
610
|
-
print(argstring)
|
611
744
|
except UndefinedError as jinja_exception:
|
612
745
|
_LOGGER.warning(NOT_SUB_MSG.format(str(jinja_exception)))
|
613
746
|
except KeyError as e:
|
looper/const.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
""" Shared project constants """
|
2
2
|
|
3
3
|
import os
|
4
|
+
from enum import Enum
|
4
5
|
|
5
6
|
__author__ = "Databio lab"
|
6
7
|
__email__ = "nathan@code.databio.org"
|
@@ -92,6 +93,7 @@ __all__ = [
|
|
92
93
|
"DEBUG_EIDO_VALIDATION",
|
93
94
|
"LOOPER_GENERIC_OUTPUT_SCHEMA",
|
94
95
|
"LOOPER_GENERIC_COUNT_LINES",
|
96
|
+
"PipelineLevel",
|
95
97
|
]
|
96
98
|
|
97
99
|
FLAGS = ["completed", "running", "failed", "waiting", "partial"]
|
@@ -268,3 +270,10 @@ MESSAGE_BY_SUBCOMMAND = {
|
|
268
270
|
"init-piface": "Initialize generic pipeline interface.",
|
269
271
|
"link": "Create directory of symlinks for reported results.",
|
270
272
|
}
|
273
|
+
|
274
|
+
# Add project/sample enum
|
275
|
+
|
276
|
+
|
277
|
+
class PipelineLevel(Enum):
|
278
|
+
SAMPLE = "sample"
|
279
|
+
PROJECT = "project"
|
looper/divvy.py
CHANGED
@@ -1,16 +1,14 @@
|
|
1
1
|
""" Computing configuration representation """
|
2
2
|
|
3
3
|
import logging
|
4
|
-
import logmuse
|
5
4
|
import os
|
6
|
-
import sys
|
7
5
|
import shutil
|
8
|
-
|
9
|
-
|
6
|
+
|
7
|
+
|
10
8
|
from shutil import copytree
|
9
|
+
from yacman import FutureYAMLConfigManager as YAMLConfigManager
|
10
|
+
from yacman import write_lock, FILEPATH_KEY, load_yaml, select_config
|
11
11
|
|
12
|
-
from ubiquerg import is_writable, VersionInHelpParser
|
13
|
-
import yacman
|
14
12
|
|
15
13
|
from .const import (
|
16
14
|
COMPUTE_SETTINGS_VARNAME,
|
@@ -21,14 +19,13 @@ from .const import (
|
|
21
19
|
)
|
22
20
|
from .utils import write_submit_script
|
23
21
|
|
24
|
-
# from . import __version__
|
25
22
|
|
26
23
|
_LOGGER = logging.getLogger(__name__)
|
27
24
|
|
28
25
|
# This is the divvy.py submodule from divvy
|
29
26
|
|
30
27
|
|
31
|
-
class ComputingConfiguration(
|
28
|
+
class ComputingConfiguration(YAMLConfigManager):
|
32
29
|
"""
|
33
30
|
Represents computing configuration objects.
|
34
31
|
|
@@ -44,36 +41,31 @@ class ComputingConfiguration(yacman.YAMLConfigManager):
|
|
44
41
|
`DIVCFG` file)
|
45
42
|
"""
|
46
43
|
|
47
|
-
def __init__(
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
validate_on_write
|
44
|
+
def __init__(
|
45
|
+
self,
|
46
|
+
entries=None,
|
47
|
+
wait_max=None,
|
48
|
+
strict_ro_locks=False,
|
49
|
+
schema_source=None,
|
50
|
+
validate_on_write=False,
|
51
|
+
):
|
52
|
+
super().__init__(
|
53
|
+
entries, wait_max, strict_ro_locks, schema_source, validate_on_write
|
57
54
|
)
|
58
55
|
|
59
|
-
if
|
60
|
-
raise Exception(
|
61
|
-
"Your divvy config file is not in divvy config format "
|
62
|
-
"(it lacks a compute_packages section): '{}'".format(filepath)
|
63
|
-
)
|
64
|
-
# We require that compute_packages be present, even if empty
|
56
|
+
if "compute_packages" not in self:
|
65
57
|
self["compute_packages"] = {}
|
66
|
-
|
67
58
|
# Initialize default compute settings.
|
68
59
|
_LOGGER.debug("Establishing project compute settings")
|
69
60
|
self.compute = None
|
70
61
|
self.setdefault("adapters", None)
|
71
62
|
self.activate_package(DEFAULT_COMPUTE_RESOURCES_NAME)
|
72
|
-
self.config_file = self.filepath
|
73
63
|
|
74
64
|
def write(self, filename=None):
|
75
|
-
|
76
|
-
|
65
|
+
with write_lock(self) as locked_ym:
|
66
|
+
locked_ym.rebase()
|
67
|
+
locked_ym.write()
|
68
|
+
filename = filename or getattr(self, FILEPATH_KEY)
|
77
69
|
filedir = os.path.dirname(filename)
|
78
70
|
# For this object, we *also* have to write the template files
|
79
71
|
for pkg_name, pkg in self["compute_packages"].items():
|
@@ -119,9 +111,12 @@ class ComputingConfiguration(yacman.YAMLConfigManager):
|
|
119
111
|
|
120
112
|
:return str: path to folder with default submission templates
|
121
113
|
"""
|
122
|
-
|
123
|
-
os.path.dirname(
|
124
|
-
|
114
|
+
if self.filepath:
|
115
|
+
return os.path.join(os.path.dirname(self.filepath), "divvy_templates")
|
116
|
+
else:
|
117
|
+
return os.path.join(
|
118
|
+
os.path.dirname(__file__), "default_config", "divvy_templates"
|
119
|
+
)
|
125
120
|
|
126
121
|
def activate_package(self, package_name):
|
127
122
|
"""
|
@@ -151,23 +146,30 @@ class ComputingConfiguration(yacman.YAMLConfigManager):
|
|
151
146
|
# Augment compute, creating it if needed.
|
152
147
|
if self.compute is None:
|
153
148
|
_LOGGER.debug("Creating Project compute")
|
154
|
-
self.compute =
|
149
|
+
self.compute = YAMLConfigManager()
|
155
150
|
_LOGGER.debug(
|
156
151
|
"Adding entries for package_name '{}'".format(package_name)
|
157
152
|
)
|
158
153
|
|
159
|
-
self.compute.
|
154
|
+
self.compute.update_from_obj(self["compute_packages"][package_name])
|
160
155
|
|
161
156
|
# Ensure submission template is absolute. This *used to be* handled
|
162
157
|
# at update (so the paths were stored as absolutes in the packages),
|
163
158
|
# but now, it makes more sense to do it here so we can piggyback on
|
164
159
|
# the default update() method and not even have to do that.
|
165
160
|
if not os.path.isabs(self.compute["submission_template"]):
|
161
|
+
|
166
162
|
try:
|
167
|
-
self.
|
168
|
-
os.path.
|
169
|
-
|
170
|
-
|
163
|
+
if self.filepath:
|
164
|
+
self.compute["submission_template"] = os.path.join(
|
165
|
+
os.path.dirname(self.filepath),
|
166
|
+
self.compute["submission_template"],
|
167
|
+
)
|
168
|
+
else:
|
169
|
+
self.compute["submission_template"] = os.path.join(
|
170
|
+
os.path.dirname(self.default_config_file),
|
171
|
+
self.compute["submission_template"],
|
172
|
+
)
|
171
173
|
except AttributeError as e:
|
172
174
|
# Environment and environment compute should at least have been
|
173
175
|
# set as null-valued attributes, so execution here is an error.
|
@@ -200,14 +202,19 @@ class ComputingConfiguration(yacman.YAMLConfigManager):
|
|
200
202
|
self.reset_active_settings()
|
201
203
|
return self.activate_package(package_name)
|
202
204
|
|
203
|
-
def get_active_package(self):
|
205
|
+
def get_active_package(self) -> YAMLConfigManager:
|
204
206
|
"""
|
205
207
|
Returns settings for the currently active compute package
|
206
208
|
|
207
|
-
:return
|
209
|
+
:return YAMLConfigManager: data defining the active compute package
|
208
210
|
"""
|
209
211
|
return self.compute
|
210
212
|
|
213
|
+
@property
|
214
|
+
def compute_packages(self):
|
215
|
+
|
216
|
+
return self["compute_packages"]
|
217
|
+
|
211
218
|
def list_compute_packages(self):
|
212
219
|
"""
|
213
220
|
Returns a list of available compute packages.
|
@@ -222,7 +229,7 @@ class ComputingConfiguration(yacman.YAMLConfigManager):
|
|
222
229
|
|
223
230
|
:return bool: success flag
|
224
231
|
"""
|
225
|
-
self.compute =
|
232
|
+
self.compute = YAMLConfigManager()
|
226
233
|
return True
|
227
234
|
|
228
235
|
def update_packages(self, config_file):
|
@@ -235,11 +242,11 @@ class ComputingConfiguration(yacman.YAMLConfigManager):
|
|
235
242
|
|
236
243
|
:param str config_file: path to file with new divvy configuration data
|
237
244
|
"""
|
238
|
-
entries =
|
245
|
+
entries = load_yaml(config_file)
|
239
246
|
self.update(entries)
|
240
247
|
return True
|
241
248
|
|
242
|
-
def get_adapters(self):
|
249
|
+
def get_adapters(self) -> YAMLConfigManager:
|
243
250
|
"""
|
244
251
|
Get current adapters, if defined.
|
245
252
|
|
@@ -248,9 +255,9 @@ class ComputingConfiguration(yacman.YAMLConfigManager):
|
|
248
255
|
package-specific set of adapters, if any defined in 'adapters' section
|
249
256
|
under currently active compute package.
|
250
257
|
|
251
|
-
:return
|
258
|
+
:return YAMLConfigManager: current adapters mapping
|
252
259
|
"""
|
253
|
-
adapters =
|
260
|
+
adapters = YAMLConfigManager()
|
254
261
|
if "adapters" in self and self["adapters"] is not None:
|
255
262
|
adapters.update(self["adapters"])
|
256
263
|
if "compute" in self and "adapters" in self.compute:
|
@@ -376,7 +383,7 @@ def select_divvy_config(filepath):
|
|
376
383
|
:param str | NoneType filepath: direct file path specification
|
377
384
|
:return str: path to the config file to read
|
378
385
|
"""
|
379
|
-
divcfg =
|
386
|
+
divcfg = select_config(
|
380
387
|
config_filepath=filepath,
|
381
388
|
config_env_vars=COMPUTE_SETTINGS_VARNAME,
|
382
389
|
default_config_filepath=DEFAULT_CONFIG_FILEPATH,
|
@@ -404,11 +411,13 @@ def divvy_init(config_path, template_config_path):
|
|
404
411
|
_LOGGER.error("You must specify a template config file path.")
|
405
412
|
return
|
406
413
|
|
414
|
+
if not os.path.isabs(config_path):
|
415
|
+
config_path = os.path.abspath(config_path)
|
416
|
+
|
407
417
|
if config_path and not os.path.exists(config_path):
|
408
|
-
# dcc.write(config_path)
|
409
418
|
# Init should *also* write the templates.
|
410
419
|
dest_folder = os.path.dirname(config_path)
|
411
|
-
copytree(os.path.dirname(template_config_path), dest_folder)
|
420
|
+
copytree(os.path.dirname(template_config_path), dest_folder, dirs_exist_ok=True)
|
412
421
|
template_subfolder = os.path.join(dest_folder, "divvy_templates")
|
413
422
|
_LOGGER.info("Wrote divvy templates to folder: {}".format(template_subfolder))
|
414
423
|
new_template = os.path.join(
|
looper/exceptions.py
CHANGED
@@ -15,6 +15,7 @@ _all__ = [
|
|
15
15
|
"PipelineInterfaceConfigError",
|
16
16
|
"PipelineInterfaceRequirementsError",
|
17
17
|
"MisconfigurationException",
|
18
|
+
"LooperReportError",
|
18
19
|
]
|
19
20
|
|
20
21
|
|
@@ -31,7 +32,7 @@ class SampleFailedException(LooperError):
|
|
31
32
|
|
32
33
|
|
33
34
|
class MisconfigurationException(LooperError):
|
34
|
-
"""
|
35
|
+
"""Looper not properly configured"""
|
35
36
|
|
36
37
|
def __init__(self, key):
|
37
38
|
super(MisconfigurationException, self).__init__(key)
|
@@ -109,3 +110,10 @@ class PipelineInterfaceRequirementsError(LooperError):
|
|
109
110
|
)
|
110
111
|
)
|
111
112
|
self.error_specs = typename_by_requirement
|
113
|
+
|
114
|
+
|
115
|
+
class LooperReportError(LooperError):
|
116
|
+
"""Looper reporting errors"""
|
117
|
+
|
118
|
+
def __init__(self, reason):
|
119
|
+
super(LooperReportError, self).__init__(reason)
|