99
1010import typer
1111from cwl_utils .pack import pack
12- from cwl_utils .parser import load_document , save
13- from cwl_utils .parser .cwl_v1_2 import File
12+ from cwl_utils .parser import File , load_document , save
13+ from cwl_utils .parser .cwl_v1_2 import File as CWLFile
1414from cwl_utils .parser .utils import load_inputfile
1515from rich import print_json
1616from rich .console import Console
3535# -----------------------------------------------------------------------------
3636
3737
38+ def _parse_chunk (chunk_str : str ) -> tuple [str , int ]:
39+ """Parse a --chunk value of the form PARAM=SIZE.
40+
41+ :param chunk_str: The chunk string to parse.
42+ :return: Tuple of (parameter name, chunk size).
43+ :raises typer.BadParameter: If the format is invalid or size is not a positive integer.
44+ """
45+ if "=" not in chunk_str :
46+ raise typer .BadParameter (f"Invalid --chunk format '{ chunk_str } '. Expected PARAM=SIZE (e.g., input-data=3)." )
47+ param , size_str = chunk_str .split ("=" , 1 )
48+ if not param :
49+ raise typer .BadParameter ("Parameter name cannot be empty in --chunk." )
50+ try :
51+ size = int (size_str )
52+ except ValueError as err :
53+ raise typer .BadParameter (f"Chunk size must be an integer, got '{ size_str } '." ) from err
54+ if size <= 0 :
55+ raise typer .BadParameter (f"Chunk size must be > 0, got { size } ." )
56+ return param , size
57+
58+
3859@app .command ("submit" )
3960def submit_transformation_client (
4061 task_path : str = typer .Argument (..., help = "Path to the CWL file" ),
4162 inputs_file : str | None = typer .Option (None , help = "Path to the CWL inputs file" ),
63+ chunk : str | None = typer .Option (None , help = "Split an array input into jobs: PARAM=SIZE (e.g., input-data=3)" ),
4264 # Specific parameter for the purpose of the prototype
4365 local : Optional [bool ] = typer .Option (True , help = "Run the jobs locally instead of submitting them to the router" ),
4466):
@@ -50,15 +72,55 @@ def submit_transformation_client(
5072 - Start the transformation
5173 """
5274 os .environ ["DIRAC_PROTO_LOCAL" ] = "0"
75+
76+ # --chunk and --inputs-file must be used together
77+ if chunk and not inputs_file :
78+ console .print ("[red]:heavy_multiplication_x:[/red] [bold]CLI:[/bold] --chunk requires --inputs-file." )
79+ return typer .Exit (code = 1 )
80+ if inputs_file and not chunk :
81+ console .print ("[red]:heavy_multiplication_x:[/red] [bold]CLI:[/bold] --inputs-file requires --chunk." )
82+ return typer .Exit (code = 1 )
83+
5384 # Validate the workflow
5485 console .print ("[blue]:information_source:[/blue] [bold]CLI:[/bold] Validating the transformation..." )
5586 try :
5687 task = load_document (pack (task_path ))
5788
58- # Load Transformation inputs if existing
59- input_data = None
60- if inputs_file :
61- input_data = load_inputfile (task .cwlVersion , inputs_file ).get ("input-data" )
89+ # Warn if the hint already has input_data — CLI overrides it
90+ existing_hint = TransformationExecutionHooksHint .from_cwl (task )
91+ if inputs_file and existing_hint .input_data :
92+ console .print (
93+ "[yellow]:warning:[/yellow] [bold]CLI:[/bold] "
94+ "The workflow hint already contains input_data. "
95+ "Overriding with --inputs-file/--chunk values."
96+ )
97+
98+ # Load and validate inputs, inject into hint
99+ if inputs_file and chunk :
100+ all_inputs = load_inputfile (task .cwlVersion , inputs_file )
101+ chunk_param , chunk_size = _parse_chunk (chunk )
102+
103+ # Validate the chunk parameter exists and is a list
104+ if chunk_param not in all_inputs :
105+ console .print (
106+ f"[red]:heavy_multiplication_x:[/red] [bold]CLI:[/bold] "
107+ f"Parameter '{ chunk_param } ' not found in inputs file. "
108+ f"Available parameters: { list (all_inputs .keys ())} "
109+ )
110+ return typer .Exit (code = 1 )
111+ if not isinstance (all_inputs [chunk_param ], list ):
112+ console .print (
113+ f"[red]:heavy_multiplication_x:[/red] [bold]CLI:[/bold] "
114+ f"Parameter '{ chunk_param } ' must be an array type for --chunk, "
115+ f"got { type (all_inputs [chunk_param ]).__name__ } ."
116+ )
117+ return typer .Exit (code = 1 )
118+
119+ input_data = {chunk_param : all_inputs [chunk_param ]}
120+
121+ # Inject input_data and group_size into the task's ExecutionHooks hint
122+ hint_update = TransformationExecutionHooksHint (group_size = chunk_size , input_data = input_data )
123+ TransformationExecutionHooksHint .update_cwl (task , hint_update )
62124
63125 except FileNotFoundError as ex :
64126 console .print (f"[red]:heavy_multiplication_x:[/red] [bold]CLI:[/bold] Failed to load the task:\n { ex } " )
@@ -68,7 +130,7 @@ def submit_transformation_client(
68130 return typer .Exit (code = 1 )
69131 console .print (f"\t [green]:heavy_check_mark:[/green] Task { task_path } " )
70132
71- transformation = TransformationSubmissionModel (task = task , input_data = input_data )
133+ transformation = TransformationSubmissionModel (task = task )
72134 console .print ("[green]:heavy_check_mark:[/green] [bold]CLI:[/bold] Transformation validated." )
73135
74136 # Submit the transformation
@@ -111,26 +173,40 @@ def submit_transformation_router(transformation: TransformationSubmissionModel)
111173 except Exception as exc :
112174 raise ValueError (f"Invalid DIRAC hints:\n { exc } " ) from exc
113175
114- # Inputs from Transformation inputs_file
115- if transformation .input_data :
116- nb_files = len (transformation .input_data )
117- group_size = transformation_execution_hooks .group_size or 1
118- nb_groups = (nb_files + group_size - 1 ) // group_size
119- logger .info ("Creating %s jobs for the transformation with group size of %s..." , nb_groups , group_size )
120-
121- input_data = transformation .input_data
122- for i , start in enumerate (range (0 , len (input_data ), group_size )):
123- files_chunk = input_data [start : start + group_size ]
176+ # Inputs from static input_data (populated by --chunk on the client)
177+ if transformation_execution_hooks .input_data :
178+ if transformation_execution_hooks .configuration :
179+ raise ValueError (
180+ "Cannot specify both static input_data and dynamic input query (configuration). "
181+ "Use --chunk/--inputs-file for standalone transformations with known files. "
182+ "Use configuration for transformations that discover inputs from upstream outputs."
183+ )
124184
185+ group_size = transformation_execution_hooks .group_size or 1
186+ for param_name , file_list in transformation_execution_hooks .input_data .items ():
187+ nb_files = len (file_list )
188+ nb_groups = (nb_files + group_size - 1 ) // group_size
125189 logger .info (
126- "Group %i files: %s" ,
127- i + 1 ,
128- [save (file ).get ("path" ) if isinstance (file , File ) else file for file in files_chunk ],
190+ "Chunking '%s': %s files into %s jobs (group_size=%s)" ,
191+ param_name ,
192+ nb_files ,
193+ nb_groups ,
194+ group_size ,
129195 )
130- job_model_params .append (JobInputModel (sandbox = None , cwl = {"input-data" : files_chunk }))
131196
132- # Inputs from DataCatalog/Bookkeeping service
133- if transformation_execution_hooks .configuration and transformation_execution_hooks .group_size :
197+ for i , start in enumerate (range (0 , nb_files , group_size )):
198+ files_chunk = file_list [start : start + group_size ]
199+ logger .info (
200+ "Group %i files: %s" ,
201+ i + 1 ,
202+ [save (file ).get ("path" ) if isinstance (file , File ) else file for file in files_chunk ],
203+ )
204+ job_model_params .append (JobInputModel (sandbox = None , cwl = {param_name : files_chunk }))
205+
206+ # Inputs from DataCatalog/Bookkeeping service (dynamic query)
207+ elif transformation_execution_hooks .configuration :
208+ group_size = transformation_execution_hooks .group_size or 1
209+
134210 # Get the metadata class
135211 transformation_metadata = transformation_execution_hooks .to_runtime (transformation )
136212
@@ -143,9 +219,9 @@ def submit_transformation_router(transformation: TransformationSubmissionModel)
143219 # Wait for the input to be available
144220 logger .info ("\t - Waiting for input data..." )
145221 logger .debug ("\t \t - Query: %s" , input_query )
146- logger .debug ("\t \t - Group Size: %s" , transformation_execution_hooks . group_size )
222+ logger .debug ("\t \t - Group Size: %s" , group_size )
147223
148- while not (inputs := _get_inputs (input_query , transformation_execution_hooks . group_size )):
224+ while not (inputs := _get_inputs (input_query , group_size )):
149225 logger .debug ("\t \t - Result: %s" , inputs )
150226 time .sleep (5 )
151227
@@ -215,7 +291,7 @@ def _generate_job_model_parameter(
215291 for group in grouped_input_data :
216292 cwl_inputs = {}
217293 for input_name , input_data in group .items ():
218- cwl_inputs [input_name ] = [File (location = str (Path ("lfn:" ) / path )) for path in input_data ]
294+ cwl_inputs [input_name ] = [CWLFile (location = str (Path ("lfn:" ) / path )) for path in input_data ]
219295
220296 job_model_params .append (JobInputModel (sandbox = None , cwl = cwl_inputs ))
221297
0 commit comments