2
2
3
3
import enum
4
4
import itertools
5
+ import re
5
6
import time
6
7
import typing
7
8
from dataclasses import dataclass , field
@@ -160,7 +161,7 @@ class TargetNodesBaseModel(_NamedBaseModel):
160
161
161
162
162
163
class ConfigCycleTaskInput (TargetNodesBaseModel ):
163
- port : str | None = None
164
+ port : str
164
165
165
166
166
167
class ConfigCycleTaskWaitOn (TargetNodesBaseModel ):
@@ -273,58 +274,66 @@ class ConfigRootTask(ConfigBaseTask):
273
274
plugin : ClassVar [Literal ["_root" ]] = "_root"
274
275
275
276
276
- # By using a frozen class we only need to validate on initialization
277
- @dataclass (frozen = True )
278
- class ShellCliArgument :
279
- """A holder for a CLI argument to simplify access.
280
-
281
- Stores CLI arguments of the form "file", "--init", "{file}" or "{--init file}". These examples translate into
282
- ShellCliArguments ShellCliArgument(name="file", references_data_item=False, cli_option_of_data_item=None),
283
- ShellCliArgument(name="--init", references_data_item=False, cli_option_of_data_item=None),
284
- ShellCliArgument(name="file", references_data_item=True, cli_option_of_data_item=None),
285
- ShellCliArgument(name="file", references_data_item=True, cli_option_of_data_item="--init")
277
+ @dataclass (kw_only = True )
278
+ class ConfigShellTaskSpecs :
279
+ plugin : ClassVar [Literal ["shell" ]] = "shell"
280
+ port_pattern : ClassVar [re .Pattern ] = field (default = re .compile (r"{PORT(\[sep=.+\])?::(.+?)}" ), repr = False )
281
+ sep_pattern : ClassVar [re .Pattern ] = field (default = re .compile (r"\[sep=(.+)\]" ), repr = False )
282
+ src : str | None = None
283
+ command : str
284
+ env_source_files : list [str ] = field (default_factory = list )
286
285
287
- Attributes:
288
- name: Name of the argument. For the examples it is "file", "--init", "file" and "file"
289
- references_data_item: Specifies if the argument references a data item signified by enclosing it by curly
290
- brackets.
291
- cli_option_of_data_item: The CLI option associated to the data item.
292
- """
286
+ def resolve_ports (self , input_labels : dict [str , list [str ]]) -> str :
287
+ """Replace port placeholders in command string with provided input labels.
293
288
294
- name : str
295
- references_data_item : bool
296
- cli_option_of_data_item : str | None = None
289
+ Returns a string corresponding to self.command with "{PORT::port_name}"
290
+ placeholders replaced by the content provided in the input_labels dict.
291
+ When multiple input nodes are linked to a single port (e.g. with
292
+ parameterized data or if the `when` keyword specifies a list of lags or
293
+ dates), the provided input labels are inserted with a separator
294
+ defaulting to a " ". Specifying an alternative separator, e.g. a comma,
295
+ is done via "{PORT[sep=,]::port_name}"
297
296
298
- def __post_init__ (self ):
299
- if self .cli_option_of_data_item is not None and not self .references_data_item :
300
- msg = "data_item_option cannot be not None if cli_option_of_data_item is False"
301
- raise ValueError (msg )
297
+ Examples:
302
298
303
- @classmethod
304
- def from_cli_argument (cls , arg : str ) -> ShellCliArgument :
305
- len_arg_with_option = 2
306
- len_arg_no_option = 1
307
- references_data_item = arg .startswith ("{" ) and arg .endswith ("}" )
308
- # remove curly brackets "{--init file}" -> "--init file"
309
- arg_unwrapped = arg [1 :- 1 ] if arg .startswith ("{" ) and arg .endswith ("}" ) else arg
310
-
311
- # "--init file" -> ["--init", "file"]
312
- input_arg = arg_unwrapped .split ()
313
- if len (input_arg ) != len_arg_with_option and len (input_arg ) != len_arg_no_option :
314
- msg = f"Expected argument of format {{data}} or {{option data}} but found { arg } "
315
- raise ValueError (msg )
316
- name = input_arg [0 ] if len (input_arg ) == len_arg_no_option else input_arg [1 ]
317
- cli_option_of_data_item = input_arg [0 ] if len (input_arg ) == len_arg_with_option else None
318
- return cls (name , references_data_item , cli_option_of_data_item )
299
+ >>> task_specs = ConfigShellTaskSpecs(
300
+ ... command="./my_script {PORT::positionals} -l -c --verbose 2 --arg {PORT::my_arg}"
301
+ ... )
302
+ >>> task_specs.resolve_ports(
303
+ ... {"positionals": ["input_1", "input_2"], "my_arg": ["input_3"]}
304
+ ... )
305
+ './my_script input_1 input_2 -l -c --verbose 2 --arg input_3'
319
306
307
+ >>> task_specs = ConfigShellTaskSpecs(
308
+ ... command="./my_script {PORT::positionals} --multi_arg {PORT[sep=,]::multi_arg}"
309
+ ... )
310
+ >>> task_specs.resolve_ports(
311
+ ... {"positionals": ["input_1", "input_2"], "multi_arg": ["input_3", "input_4"]}
312
+ ... )
313
+ './my_script input_1 input_2 --multi_arg input_3,input_4'
320
314
321
- @dataclass (kw_only = True )
322
- class ConfigShellTaskSpecs :
323
- plugin : ClassVar [Literal ["shell" ]] = "shell"
324
- command : str = ""
325
- cli_arguments : list [ShellCliArgument ] = field (default_factory = list )
326
- env_source_files : list [str ] = field (default_factory = list )
327
- src : str | None = None
315
+ >>> task_specs = ConfigShellTaskSpecs(
316
+ ... command="./my_script --input {PORT[sep= --input ]::repeat_input}"
317
+ ... )
318
+ >>> task_specs.resolve_ports({"repeat_input": ["input_1", "input_2", "input_3"]})
319
+ './my_script --input input_1 --input input_2 --input input_3'
320
+ """
321
+ cmd = self .command
322
+ for port_match in self .port_pattern .finditer (cmd ):
323
+ if (port_name := port_match .group (2 )) is None :
324
+ msg = f"Wrong port specification: { port_match .group (0 )} "
325
+ raise ValueError (msg )
326
+ if (sep := port_match .group (1 )) is None :
327
+ arg_sep = " "
328
+ else :
329
+ if (sep_match := self .sep_pattern .match (sep )) is None :
330
+ msg = "Wrong separator specification: sep"
331
+ raise ValueError (msg )
332
+ if (arg_sep := sep_match .group (1 )) is None :
333
+ msg = "Wrong separator specification: sep"
334
+ raise ValueError (msg )
335
+ cmd = cmd .replace (port_match .group (0 ), arg_sep .join (input_labels [port_name ]))
336
+ return cmd
328
337
329
338
330
339
class ConfigShellTask (ConfigBaseTask , ConfigShellTaskSpecs ):
@@ -340,75 +349,26 @@ class ConfigShellTask(ConfigBaseTask, ConfigShellTaskSpecs):
340
349
... '''
341
350
... my_task:
342
351
... plugin: shell
343
- ... command: my_script.sh
344
- ... src: post_run_scripts
345
- ... cli_arguments: "-n 1024 {current_sim_output}"
352
+ ... command: "my_script.sh -n 1024 {PORT::current_sim_output}"
353
+ ... src: post_run_scripts/my_script.sh
346
354
... env_source_files: "env.sh"
347
355
... walltime: 00:01:00
348
356
... '''
349
357
... ),
350
358
... )
351
- >>> my_task.cli_arguments[0]
352
- ShellCliArgument(name='-n', references_data_item=False, cli_option_of_data_item=None)
353
- >>> my_task.cli_arguments[1]
354
- ShellCliArgument(name='1024', references_data_item=False, cli_option_of_data_item=None)
355
- >>> my_task.cli_arguments[2]
356
- ShellCliArgument(name='current_sim_output', references_data_item=True, cli_option_of_data_item=None)
357
359
>>> my_task.env_source_files
358
360
['env.sh']
359
361
>>> my_task.walltime.tm_min
360
362
1
361
363
"""
362
364
363
- command : str = ""
364
- cli_arguments : list [ShellCliArgument ] = Field (default_factory = list )
365
365
env_source_files : list [str ] = Field (default_factory = list )
366
366
367
- @field_validator ("cli_arguments" , mode = "before" )
368
- @classmethod
369
- def validate_cli_arguments (cls , value : str ) -> list [ShellCliArgument ]:
370
- return cls .parse_cli_arguments (value )
371
-
372
367
@field_validator ("env_source_files" , mode = "before" )
373
368
@classmethod
374
369
def validate_env_source_files (cls , value : str | list [str ]) -> list [str ]:
375
370
return [value ] if isinstance (value , str ) else value
376
371
377
- @staticmethod
378
- def split_cli_arguments (cli_arguments : str ) -> list [str ]:
379
- """Splits the CLI arguments into a list of separate entities.
380
-
381
- Splits the CLI arguments by whitespaces except if the whitespace is contained within curly brackets. For example
382
- the string
383
- "-D --CMAKE_CXX_COMPILER=${CXX_COMPILER} {--init file}"
384
- will be splitted into the list
385
- ["-D", "--CMAKE_CXX_COMPILER=${CXX_COMPILER}", "{--init file}"]
386
- """
387
-
388
- nb_open_curly_brackets = 0
389
- last_split_idx = 0
390
- splits = []
391
- for i , char in enumerate (cli_arguments ):
392
- if char == " " and not nb_open_curly_brackets :
393
- # we ommit the space in the splitting therefore we only store up to i but move the last_split_idx to i+1
394
- splits .append (cli_arguments [last_split_idx :i ])
395
- last_split_idx = i + 1
396
- elif char == "{" :
397
- nb_open_curly_brackets += 1
398
- elif char == "}" :
399
- if nb_open_curly_brackets == 0 :
400
- msg = f"Invalid input for cli_arguments. Found a closing curly bracket before an opening in { cli_arguments !r} "
401
- raise ValueError (msg )
402
- nb_open_curly_brackets -= 1
403
-
404
- if last_split_idx != len (cli_arguments ):
405
- splits .append (cli_arguments [last_split_idx : len (cli_arguments )])
406
- return splits
407
-
408
- @staticmethod
409
- def parse_cli_arguments (cli_arguments : str ) -> list [ShellCliArgument ]:
410
- return [ShellCliArgument .from_cli_argument (arg ) for arg in ConfigShellTask .split_cli_arguments (cli_arguments )]
411
-
412
372
413
373
@dataclass (kw_only = True )
414
374
class NamelistSpec :
@@ -662,6 +622,7 @@ class ConfigWorkflow(BaseModel):
662
622
... tasks:
663
623
... - task_a:
664
624
... plugin: shell
625
+ ... command: "some_command"
665
626
... data:
666
627
... available:
667
628
... - foo:
@@ -681,7 +642,9 @@ class ConfigWorkflow(BaseModel):
681
642
... name="minimal",
682
643
... rootdir=Path("/location/of/config/file"),
683
644
... cycles=[ConfigCycle(minimal_cycle={"tasks": [ConfigCycleTask(task_a={})]})],
684
- ... tasks=[ConfigShellTask(task_a={"plugin": "shell"})],
645
+ ... tasks=[
646
+ ... ConfigShellTask(task_a={"plugin": "shell", "command": "some_command"})
647
+ ... ],
685
648
... data=ConfigData(
686
649
... available=[
687
650
... ConfigAvailableData(name="foo", type=DataType.FILE, src="foo.txt")
0 commit comments