hpcflow-new2 0.2.0a179__py3-none-any.whl → 0.2.0a181__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 (70) hide show
  1. hpcflow/_version.py +1 -1
  2. hpcflow/data/demo_data_manifest/__init__.py +3 -0
  3. hpcflow/sdk/__init__.py +4 -1
  4. hpcflow/sdk/app.py +160 -15
  5. hpcflow/sdk/cli.py +14 -0
  6. hpcflow/sdk/cli_common.py +83 -0
  7. hpcflow/sdk/config/__init__.py +4 -0
  8. hpcflow/sdk/config/callbacks.py +25 -2
  9. hpcflow/sdk/config/cli.py +4 -1
  10. hpcflow/sdk/config/config.py +188 -14
  11. hpcflow/sdk/config/config_file.py +91 -3
  12. hpcflow/sdk/config/errors.py +33 -0
  13. hpcflow/sdk/core/__init__.py +2 -0
  14. hpcflow/sdk/core/actions.py +492 -35
  15. hpcflow/sdk/core/cache.py +22 -0
  16. hpcflow/sdk/core/command_files.py +221 -5
  17. hpcflow/sdk/core/commands.py +57 -0
  18. hpcflow/sdk/core/element.py +407 -8
  19. hpcflow/sdk/core/environment.py +92 -0
  20. hpcflow/sdk/core/errors.py +245 -61
  21. hpcflow/sdk/core/json_like.py +72 -14
  22. hpcflow/sdk/core/loop.py +122 -21
  23. hpcflow/sdk/core/loop_cache.py +34 -9
  24. hpcflow/sdk/core/object_list.py +172 -26
  25. hpcflow/sdk/core/parallel.py +14 -0
  26. hpcflow/sdk/core/parameters.py +478 -25
  27. hpcflow/sdk/core/rule.py +31 -1
  28. hpcflow/sdk/core/run_dir_files.py +12 -2
  29. hpcflow/sdk/core/task.py +407 -80
  30. hpcflow/sdk/core/task_schema.py +70 -9
  31. hpcflow/sdk/core/test_utils.py +35 -0
  32. hpcflow/sdk/core/utils.py +101 -4
  33. hpcflow/sdk/core/validation.py +13 -1
  34. hpcflow/sdk/core/workflow.py +316 -96
  35. hpcflow/sdk/core/zarr_io.py +23 -0
  36. hpcflow/sdk/data/__init__.py +13 -0
  37. hpcflow/sdk/demo/__init__.py +3 -0
  38. hpcflow/sdk/helper/__init__.py +3 -0
  39. hpcflow/sdk/helper/cli.py +9 -0
  40. hpcflow/sdk/helper/helper.py +28 -0
  41. hpcflow/sdk/helper/watcher.py +33 -0
  42. hpcflow/sdk/log.py +40 -0
  43. hpcflow/sdk/persistence/__init__.py +14 -4
  44. hpcflow/sdk/persistence/base.py +289 -23
  45. hpcflow/sdk/persistence/json.py +29 -0
  46. hpcflow/sdk/persistence/pending.py +217 -107
  47. hpcflow/sdk/persistence/store_resource.py +58 -2
  48. hpcflow/sdk/persistence/utils.py +8 -0
  49. hpcflow/sdk/persistence/zarr.py +68 -1
  50. hpcflow/sdk/runtime.py +52 -10
  51. hpcflow/sdk/submission/__init__.py +3 -0
  52. hpcflow/sdk/submission/jobscript.py +198 -9
  53. hpcflow/sdk/submission/jobscript_info.py +13 -0
  54. hpcflow/sdk/submission/schedulers/__init__.py +60 -0
  55. hpcflow/sdk/submission/schedulers/direct.py +53 -0
  56. hpcflow/sdk/submission/schedulers/sge.py +45 -7
  57. hpcflow/sdk/submission/schedulers/slurm.py +45 -8
  58. hpcflow/sdk/submission/schedulers/utils.py +4 -0
  59. hpcflow/sdk/submission/shells/__init__.py +11 -1
  60. hpcflow/sdk/submission/shells/base.py +32 -1
  61. hpcflow/sdk/submission/shells/bash.py +36 -1
  62. hpcflow/sdk/submission/shells/os_version.py +18 -6
  63. hpcflow/sdk/submission/shells/powershell.py +22 -0
  64. hpcflow/sdk/submission/submission.py +88 -3
  65. hpcflow/sdk/typing.py +10 -1
  66. {hpcflow_new2-0.2.0a179.dist-info → hpcflow_new2-0.2.0a181.dist-info}/METADATA +3 -3
  67. {hpcflow_new2-0.2.0a179.dist-info → hpcflow_new2-0.2.0a181.dist-info}/RECORD +70 -70
  68. {hpcflow_new2-0.2.0a179.dist-info → hpcflow_new2-0.2.0a181.dist-info}/LICENSE +0 -0
  69. {hpcflow_new2-0.2.0a179.dist-info → hpcflow_new2-0.2.0a181.dist-info}/WHEEL +0 -0
  70. {hpcflow_new2-0.2.0a179.dist-info → hpcflow_new2-0.2.0a181.dist-info}/entry_points.txt +0 -0
@@ -1,3 +1,7 @@
1
+ """
2
+ Model of an execution environment.
3
+ """
4
+
1
5
  from __future__ import annotations
2
6
 
3
7
  from dataclasses import dataclass
@@ -14,8 +18,24 @@ from hpcflow.sdk.core.utils import check_valid_py_identifier, get_duplicate_item
14
18
 
15
19
  @dataclass
16
20
  class NumCores(JSONLike):
21
+ """
22
+ A range of cores supported by an executable instance.
23
+
24
+ Parameters
25
+ ----------
26
+ start:
27
+ The minimum number of cores supported.
28
+ stop:
29
+ The maximum number of cores supported.
30
+ step: int
31
+ The step in the number of cores supported. Defaults to 1.
32
+ """
33
+
34
+ #: The minimum number of cores supported.
17
35
  start: int
36
+ #: The maximum number of cores supported.
18
37
  stop: int
38
+ #: The step in the number of cores supported. Normally 1.
19
39
  step: int = None
20
40
 
21
41
  def __post_init__(self):
@@ -41,8 +61,24 @@ class NumCores(JSONLike):
41
61
 
42
62
  @dataclass
43
63
  class ExecutableInstance(JSONLike):
64
+ """
65
+ A particular instance of an executable that can support some mode of operation.
66
+
67
+ Parameters
68
+ ----------
69
+ parallel_mode:
70
+ What parallel mode is supported by this executable instance.
71
+ num_cores: NumCores | int | dict[str, int]
72
+ The number of cores supported by this executable instance.
73
+ command:
74
+ The actual command to use for this executable instance.
75
+ """
76
+
77
+ #: What parallel mode is supported by this executable instance.
44
78
  parallel_mode: str
79
+ #: The number of cores supported by this executable instance.
45
80
  num_cores: Any
81
+ #: The actual command to use for this executable instance.
46
82
  command: str
47
83
 
48
84
  def __post_init__(self):
@@ -63,10 +99,24 @@ class ExecutableInstance(JSONLike):
63
99
 
64
100
  @classmethod
65
101
  def from_spec(cls, spec):
102
+ """
103
+ Construct an instance from a specification dictionary.
104
+ """
66
105
  return cls(**spec)
67
106
 
68
107
 
69
108
  class Executable(JSONLike):
109
+ """
110
+ A program managed by the environment.
111
+
112
+ Parameters
113
+ ----------
114
+ label:
115
+ The abstract name of the program.
116
+ instances: list[ExecutableInstance]
117
+ The concrete instances of the application that may be present.
118
+ """
119
+
70
120
  _child_objects = (
71
121
  ChildObjectSpec(
72
122
  name="instances",
@@ -76,7 +126,9 @@ class Executable(JSONLike):
76
126
  )
77
127
 
78
128
  def __init__(self, label: str, instances: List[app.ExecutableInstance]):
129
+ #: The abstract name of the program.
79
130
  self.label = check_valid_py_identifier(label)
131
+ #: The concrete instances of the application that may be present.
80
132
  self.instances = instances
81
133
 
82
134
  self._executables_list = None # assigned by parent
@@ -101,9 +153,28 @@ class Executable(JSONLike):
101
153
 
102
154
  @property
103
155
  def environment(self):
156
+ """
157
+ The environment that the executable is going to run in.
158
+ """
104
159
  return self._executables_list.environment
105
160
 
106
161
  def filter_instances(self, parallel_mode=None, num_cores=None):
162
+ """
163
+ Select the instances of the executable that are compatible with the given
164
+ requirements.
165
+
166
+ Parameters
167
+ ----------
168
+ parallel_mode: str
169
+ If given, the parallel mode to require.
170
+ num_cores: int
171
+ If given, the number of cores desired.
172
+
173
+ Returns
174
+ -------
175
+ list[ExecutableInstance]:
176
+ The known executable instances that match the requirements.
177
+ """
107
178
  out = []
108
179
  for i in self.instances:
109
180
  if parallel_mode is None or i.parallel_mode == parallel_mode:
@@ -113,6 +184,22 @@ class Executable(JSONLike):
113
184
 
114
185
 
115
186
  class Environment(JSONLike):
187
+ """
188
+ An execution environment that contains a number of executables.
189
+
190
+ Parameters
191
+ ----------
192
+ name: str
193
+ The name of the environment.
194
+ setup: list[str]
195
+ Commands to run to enter the environment.
196
+ specifiers: dict[str, str]
197
+ Dictionary of attributes that may be used to supply addional key/value pairs to
198
+ look up an environment by.
199
+ executables: list[Executable]
200
+ List of abstract executables in the environment.
201
+ """
202
+
116
203
  _hash_value = None
117
204
  _validation_schema = "environments_spec_schema.yaml"
118
205
  _child_objects = (
@@ -126,9 +213,14 @@ class Environment(JSONLike):
126
213
  def __init__(
127
214
  self, name, setup=None, specifiers=None, executables=None, _hash_value=None
128
215
  ):
216
+ #: The name of the environment.
129
217
  self.name = name
218
+ #: Commands to run to enter the environment.
130
219
  self.setup = setup
220
+ #: Dictionary of attributes that may be used to supply addional key/value pairs
221
+ #: to look up an environment by.
131
222
  self.specifiers = specifiers or {}
223
+ #: List of abstract executables in the environment.
132
224
  self.executables = (
133
225
  executables
134
226
  if isinstance(executables, ExecutablesList)
@@ -1,32 +1,59 @@
1
+ """
2
+ Errors from the workflow system.
3
+ """
4
+
1
5
  import os
2
6
  from typing import Iterable, List
3
7
 
4
8
 
5
9
  class InputValueDuplicateSequenceAddress(ValueError):
6
- pass
10
+ """
11
+ An InputValue has the same sequence address twice.
12
+ """
7
13
 
8
14
 
9
15
  class TaskTemplateMultipleSchemaObjectives(ValueError):
10
- pass
16
+ """
17
+ A TaskTemplate has multiple objectives.
18
+ """
11
19
 
12
20
 
13
21
  class TaskTemplateUnexpectedInput(ValueError):
14
- pass
22
+ """
23
+ A TaskTemplate was given unexpected input.
24
+ """
15
25
 
16
26
 
17
27
  class TaskTemplateUnexpectedSequenceInput(ValueError):
18
- pass
28
+ """
29
+ A TaskTemplate was given an unexpected sequence.
30
+ """
19
31
 
20
32
 
21
33
  class TaskTemplateMultipleInputValues(ValueError):
22
- pass
34
+ """
35
+ A TaskTemplate had multiple input values bound over each other.
36
+ """
23
37
 
24
38
 
25
39
  class InvalidIdentifier(ValueError):
26
- pass
40
+ """
41
+ A bad identifier name was given.
42
+ """
27
43
 
28
44
 
29
45
  class MissingInputs(Exception):
46
+ """
47
+ Inputs were missing.
48
+
49
+ Parameters
50
+ ----------
51
+ message: str
52
+ The message of the exception.
53
+ missing_inputs: list[str]
54
+ The missing inputs.
55
+ """
56
+
30
57
  # TODO: add links to doc pages for common user-exceptions?
31
58
 
32
59
  def __init__(self, message, missing_inputs) -> None:
@@ -35,6 +62,17 @@ class MissingInputs(Exception):
35
62
 
36
63
 
37
64
  class UnrequiredInputSources(ValueError):
65
+ """
66
+ Input sources were provided that were not required.
67
+
68
+ Parameters
69
+ ----------
70
+ message: str
71
+ The message of the exception.
72
+ unrequired_sources: list
73
+ The input sources that were not required.
74
+ """
75
+
38
76
  def __init__(self, message, unrequired_sources) -> None:
39
77
  self.unrequired_sources = unrequired_sources
40
78
  for src in unrequired_sources:
@@ -50,129 +88,199 @@ class UnrequiredInputSources(ValueError):
50
88
 
51
89
 
52
90
  class ExtraInputs(Exception):
91
+ """
92
+ Extra inputs were provided.
93
+
94
+ Parameters
95
+ ----------
96
+ message: str
97
+ The message of the exception.
98
+ extra_inputs: list
99
+ The extra inputs.
100
+ """
101
+
53
102
  def __init__(self, message, extra_inputs) -> None:
54
103
  self.extra_inputs = extra_inputs
55
104
  super().__init__(message)
56
105
 
57
106
 
58
107
  class UnavailableInputSource(ValueError):
59
- pass
108
+ """
109
+ An input source was not available.
110
+ """
60
111
 
61
112
 
62
113
  class InapplicableInputSourceElementIters(ValueError):
63
- pass
114
+ """
115
+ An input source element iteration was inapplicable."""
64
116
 
65
117
 
66
118
  class NoCoincidentInputSources(ValueError):
67
- pass
119
+ """
120
+ Could not line up input sources to make an actual valid execution.
121
+ """
68
122
 
69
123
 
70
124
  class TaskTemplateInvalidNesting(ValueError):
71
- pass
125
+ """
126
+ Invalid nesting in a task template.
127
+ """
72
128
 
73
129
 
74
130
  class TaskSchemaSpecValidationError(Exception):
75
- pass
131
+ """
132
+ A task schema failed to validate.
133
+ """
76
134
 
77
135
 
78
136
  class WorkflowSpecValidationError(Exception):
79
- pass
137
+ """
138
+ A workflow failed to validate.
139
+ """
80
140
 
81
141
 
82
142
  class InputSourceValidationError(Exception):
83
- pass
143
+ """
144
+ An input source failed to validate.
145
+ """
84
146
 
85
147
 
86
148
  class EnvironmentSpecValidationError(Exception):
87
- pass
149
+ """
150
+ An environment specification failed to validate.
151
+ """
88
152
 
89
153
 
90
154
  class ParameterSpecValidationError(Exception):
91
- pass
155
+ """
156
+ A parameter specification failed to validate.
157
+ """
92
158
 
93
159
 
94
160
  class FileSpecValidationError(Exception):
95
- pass
161
+ """
162
+ A file specification failed to validate.
163
+ """
96
164
 
97
165
 
98
166
  class DuplicateExecutableError(ValueError):
99
- pass
167
+ """
168
+ The same executable was present twice in an executable environment.
169
+ """
100
170
 
101
171
 
102
172
  class MissingCompatibleActionEnvironment(Exception):
103
- pass
173
+ """
174
+ Could not find a compatible action environment.
175
+ """
104
176
 
105
177
 
106
178
  class MissingActionEnvironment(Exception):
107
- pass
179
+ """
180
+ Could not find an action environment.
181
+ """
108
182
 
109
183
 
110
184
  class ActionEnvironmentMissingNameError(Exception):
111
- pass
185
+ """
186
+ An action environment was missing its name.
187
+ """
112
188
 
113
189
 
114
190
  class FromSpecMissingObjectError(Exception):
115
- pass
191
+ """
192
+ Missing object when deserialising from specification.
193
+ """
116
194
 
117
195
 
118
196
  class TaskSchemaMissingParameterError(Exception):
119
- pass
197
+ """
198
+ Parameter was missing from task schema.
199
+ """
120
200
 
121
201
 
122
202
  class ToJSONLikeChildReferenceError(Exception):
123
- pass
203
+ """
204
+ Failed to generate or reference a child object when converting to JSON.
205
+ """
124
206
 
125
207
 
126
208
  class InvalidInputSourceTaskReference(Exception):
127
- pass
209
+ """
210
+ Invalid input source in task reference.
211
+ """
128
212
 
129
213
 
130
214
  class WorkflowNotFoundError(Exception):
131
- pass
215
+ """
216
+ Could not find the workflow.
217
+ """
132
218
 
133
219
 
134
220
  class MalformedWorkflowError(Exception):
135
- pass
221
+ """
222
+ Workflow was a malformed document.
223
+ """
136
224
 
137
225
 
138
226
  class ValuesAlreadyPersistentError(Exception):
139
- pass
227
+ """
228
+ Trying to make a value persistent that already is so.
229
+ """
140
230
 
141
231
 
142
232
  class MalformedParameterPathError(ValueError):
143
- pass
233
+ """
234
+ The path to a parameter was ill-formed.
235
+ """
144
236
 
145
237
 
146
238
  class MalformedNestingOrderPath(ValueError):
147
- pass
239
+ """
240
+ A nesting order path was ill-formed.
241
+ """
148
242
 
149
243
 
150
244
  class UnknownResourceSpecItemError(ValueError):
151
- pass
245
+ """
246
+ A resource specification item was not found.
247
+ """
152
248
 
153
249
 
154
250
  class WorkflowParameterMissingError(AttributeError):
155
- pass
251
+ """
252
+ A parameter to a workflow was missing.
253
+ """
156
254
 
157
255
 
158
256
  class WorkflowBatchUpdateFailedError(Exception):
159
- pass
257
+ """
258
+ An update to a workflow failed.
259
+ """
160
260
 
161
261
 
162
262
  class WorkflowLimitsError(ValueError):
163
- pass
263
+ """
264
+ Workflow hit limits.
265
+ """
164
266
 
165
267
 
166
268
  class UnsetParameterDataError(Exception):
167
- pass
269
+ """
270
+ Tried to read from an unset parameter.
271
+ """
168
272
 
169
273
 
170
274
  class LoopAlreadyExistsError(Exception):
171
- pass
275
+ """
276
+ A particular loop (or its name) already exists.
277
+ """
172
278
 
173
279
 
174
280
  class LoopTaskSubsetError(ValueError):
175
- pass
281
+ """
282
+ Problem constructing a subset of a task for a loop.
283
+ """
176
284
 
177
285
 
178
286
  class SchedulerVersionsFailure(RuntimeError):
@@ -184,6 +292,10 @@ class SchedulerVersionsFailure(RuntimeError):
184
292
 
185
293
 
186
294
  class JobscriptSubmissionFailure(RuntimeError):
295
+ """
296
+ A job script could not be submitted to the scheduler.
297
+ """
298
+
187
299
  def __init__(
188
300
  self,
189
301
  message,
@@ -207,13 +319,19 @@ class JobscriptSubmissionFailure(RuntimeError):
207
319
 
208
320
 
209
321
  class SubmissionFailure(RuntimeError):
322
+ """
323
+ A job submission failed.
324
+ """
325
+
210
326
  def __init__(self, message) -> None:
211
327
  self.message = message
212
328
  super().__init__(message)
213
329
 
214
330
 
215
331
  class WorkflowSubmissionFailure(RuntimeError):
216
- pass
332
+ """
333
+ A workflow submission failed.
334
+ """
217
335
 
218
336
 
219
337
  class ResourceValidationError(ValueError):
@@ -272,31 +390,45 @@ class UnsupportedSchedulerError(ResourceValidationError):
272
390
 
273
391
 
274
392
  class UnknownSGEPEError(ResourceValidationError):
275
- pass
393
+ """
394
+ Miscellaneous error from SGE parallel environment.
395
+ """
276
396
 
277
397
 
278
398
  class IncompatibleSGEPEError(ResourceValidationError):
279
- pass
399
+ """
400
+ The SGE parallel environment selected is incompatible.
401
+ """
280
402
 
281
403
 
282
404
  class NoCompatibleSGEPEError(ResourceValidationError):
283
- pass
405
+ """
406
+ No SGE parallel environment is compatible with request.
407
+ """
284
408
 
285
409
 
286
410
  class IncompatibleParallelModeError(ResourceValidationError):
287
- pass
411
+ """
412
+ The parallel mode is incompatible.
413
+ """
288
414
 
289
415
 
290
416
  class UnknownSLURMPartitionError(ResourceValidationError):
291
- pass
417
+ """
418
+ The requested SLURM partition isn't known.
419
+ """
292
420
 
293
421
 
294
422
  class IncompatibleSLURMPartitionError(ResourceValidationError):
295
- pass
423
+ """
424
+ The requested SLURM partition is incompatible.
425
+ """
296
426
 
297
427
 
298
428
  class IncompatibleSLURMArgumentsError(ResourceValidationError):
299
- pass
429
+ """
430
+ The SLURM arguments are incompatible with each other.
431
+ """
300
432
 
301
433
 
302
434
  class _MissingStoreItemError(ValueError):
@@ -354,80 +486,132 @@ class MissingParameterData(_MissingStoreItemError):
354
486
 
355
487
 
356
488
  class NotSubmitMachineError(RuntimeError):
357
- pass
489
+ """
490
+ The requested machine can't be submitted to.
491
+ """
358
492
 
359
493
 
360
494
  class RunNotAbortableError(ValueError):
361
- pass
495
+ """
496
+ Cannot abort the run.
497
+ """
362
498
 
363
499
 
364
500
  class NoCLIFormatMethodError(AttributeError):
365
- pass
501
+ """
502
+ Some CLI class lacks a format method
503
+ """
366
504
 
367
505
 
368
506
  class ContainerKeyError(KeyError):
507
+ """
508
+ A key could not be mapped in a container.
509
+
510
+ Parameters
511
+ ----------
512
+ path: list[str]
513
+ The path whose resolution failed.
514
+ """
515
+
369
516
  def __init__(self, path: List[str]) -> None:
370
517
  self.path = path
371
518
  super().__init__()
372
519
 
373
520
 
374
521
  class MayNeedObjectError(Exception):
522
+ """
523
+ An object is needed but not present.
524
+
525
+ Parameters
526
+ ----------
527
+ path: list[str]
528
+ The path whose resolution failed.
529
+ """
530
+
375
531
  def __init__(self, path):
376
532
  self.path = path
377
533
  super().__init__()
378
534
 
379
535
 
380
536
  class NoAvailableElementSetsError(Exception):
381
- pass
537
+ """
538
+ No element set is available.
539
+ """
382
540
 
383
541
 
384
542
  class OutputFileParserNoOutputError(ValueError):
385
- pass
543
+ """
544
+ There was no output for the output file parser to parse.
545
+ """
386
546
 
387
547
 
388
548
  class SubmissionEnvironmentError(ValueError):
389
- """Raised when submitting a workflow on a machine without a compatible environment."""
549
+ """
550
+ Raised when submitting a workflow on a machine without a compatible environment.
551
+ """
390
552
 
391
553
 
392
554
  class MissingEnvironmentExecutableError(SubmissionEnvironmentError):
393
- pass
555
+ """
556
+ The environment does not have the requested executable at all.
557
+ """
394
558
 
395
559
 
396
560
  class MissingEnvironmentExecutableInstanceError(SubmissionEnvironmentError):
397
- pass
561
+ """
562
+ The environment does not have a suitable instance of the requested executable.
563
+ """
398
564
 
399
565
 
400
566
  class MissingEnvironmentError(SubmissionEnvironmentError):
401
- pass
567
+ """
568
+ There is no environment with that name.
569
+ """
402
570
 
403
571
 
404
572
  class UnsupportedScriptDataFormat(ValueError):
405
- pass
573
+ """
574
+ That format of script data is not supported.
575
+ """
406
576
 
407
577
 
408
578
  class UnknownScriptDataParameter(ValueError):
409
- pass
579
+ """
580
+ Unknown parameter in script data.
581
+ """
410
582
 
411
583
 
412
584
  class UnknownScriptDataKey(ValueError):
413
- pass
585
+ """
586
+ Unknown key in script data.
587
+ """
414
588
 
415
589
 
416
590
  class MissingVariableSubstitutionError(KeyError):
417
- pass
591
+ """
592
+ No definition available of a variable being substituted.
593
+ """
418
594
 
419
595
 
420
596
  class EnvironmentPresetUnknownEnvironmentError(ValueError):
421
- pass
597
+ """
598
+ An environment preset could not be resolved to an execution environment.
599
+ """
422
600
 
423
601
 
424
602
  class UnknownEnvironmentPresetError(ValueError):
425
- pass
603
+ """
604
+ An execution environment was unknown.
605
+ """
426
606
 
427
607
 
428
608
  class MultipleEnvironmentsError(ValueError):
429
- pass
609
+ """
610
+ Multiple applicable execution environments exist.
611
+ """
430
612
 
431
613
 
432
614
  class MissingElementGroup(ValueError):
433
- pass
615
+ """
616
+ An element group should exist but doesn't.
617
+ """