dirac-cwl 1.0.2__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.
@@ -0,0 +1,249 @@
1
+ """CLI interface to run a workflow as a job."""
2
+
3
+ import logging
4
+ import os
5
+ import random
6
+ import subprocess
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import typer
11
+ from cwl_utils.pack import pack
12
+ from cwl_utils.parser import load_document
13
+ from cwl_utils.parser.cwl_v1_2 import (
14
+ File,
15
+ )
16
+ from cwl_utils.parser.cwl_v1_2_utils import load_inputfile
17
+ from diracx.cli.utils import AsyncTyper
18
+ from rich import print_json
19
+ from rich.console import Console
20
+ from schema_salad.exceptions import ValidationException
21
+
22
+ from dirac_cwl.job.submission_clients import (
23
+ DIRACSubmissionClient,
24
+ PrototypeSubmissionClient,
25
+ SubmissionClient,
26
+ )
27
+ from dirac_cwl.submission_models import (
28
+ JobInputModel,
29
+ JobModel,
30
+ JobSubmissionModel,
31
+ )
32
+
33
+ app = AsyncTyper()
34
+ console = Console()
35
+
36
+
37
+ # -----------------------------------------------------------------------------
38
+ # dirac-cli commands
39
+ # -----------------------------------------------------------------------------
40
+ @app.async_command("submit")
41
+ async def submit_job_client(
42
+ task_path: str = typer.Argument(..., help="Path to the CWL file"),
43
+ parameter_path: list[str] | None = typer.Option(None, help="Path to the files containing the metadata"),
44
+ # Specific parameter for the purpose of the prototype
45
+ local: bool | None = typer.Option(True, help="Run the job locally instead of submitting it to the router"),
46
+ ):
47
+ """
48
+ Correspond to the dirac-cli command to submit jobs.
49
+
50
+ This command will:
51
+ - Validate the workflow
52
+ - Start the jobs
53
+ """
54
+ # Select submission strategy based on local flag
55
+ submission_client: SubmissionClient = PrototypeSubmissionClient() if local else DIRACSubmissionClient()
56
+
57
+ os.environ["DIRAC_PROTO_LOCAL"] = "0"
58
+
59
+ # Validate the workflow
60
+ console.print("[blue]:information_source:[/blue] [bold]CLI:[/bold] Validating the job(s)...")
61
+ try:
62
+ task = load_document(pack(task_path), baseuri=".")
63
+ except FileNotFoundError as ex:
64
+ console.print(f"[red]:heavy_multiplication_x:[/red] [bold]CLI:[/bold] Failed to load the task:\n{ex}")
65
+ return typer.Exit(code=1)
66
+ except ValidationException as ex:
67
+ console.print(f"[red]:heavy_multiplication_x:[/red] [bold]CLI:[/bold] Failed to validate the task:\n{ex}")
68
+ return typer.Exit(code=1)
69
+
70
+ console.print(f"\t[green]:heavy_check_mark:[/green] Task {task_path}")
71
+ console.print("\t[green]:heavy_check_mark:[/green] Hints")
72
+
73
+ # Extract parameters if any
74
+ parameters = []
75
+ if parameter_path:
76
+ for parameter_p in parameter_path:
77
+ try:
78
+ parameter = load_inputfile(parameter_p)
79
+ except Exception as ex:
80
+ console.print(
81
+ f"[red]:heavy_multiplication_x:[/red] [bold]CLI:[/bold] Failed to validate the parameter:\n{ex}"
82
+ )
83
+ return typer.Exit(code=1)
84
+
85
+ # Prepare files for the ISB
86
+ isb_file_paths = prepare_input_sandbox(parameter)
87
+
88
+ # Upload parameter sandbox
89
+ sandbox_id = await submission_client.create_sandbox(isb_file_paths)
90
+
91
+ parameters.append(
92
+ JobInputModel(
93
+ sandbox=[sandbox_id] if sandbox_id else None,
94
+ cwl=parameter,
95
+ )
96
+ )
97
+ console.print(f"\t[green]:heavy_check_mark:[/green] Parameter {parameter_p}")
98
+
99
+ job = JobSubmissionModel(
100
+ task=task,
101
+ inputs=parameters,
102
+ )
103
+ console.print("[green]:heavy_check_mark:[/green] [bold]CLI:[/bold] Job(s) validated.")
104
+
105
+ # Submit the job
106
+ console.print("[blue]:information_source:[/blue] [bold]CLI:[/bold] Submitting the job(s)...")
107
+ print_json(job.model_dump_json(indent=4))
108
+
109
+ if not await submission_client.submit_job(job):
110
+ console.print("[red]:heavy_multiplication_x:[/red] [bold]CLI:[/bold] Failed to submit job(s).")
111
+ return typer.Exit(code=1)
112
+
113
+
114
+ def validate_jobs(job: JobSubmissionModel) -> list[JobModel]:
115
+ """
116
+ Validate jobs.
117
+
118
+ :param job: The task to execute
119
+
120
+ :return: The list of jobs to execute
121
+ """
122
+ console.print("[blue]:information_source:[/blue] [bold]CLI:[/bold] Validating the job(s)...")
123
+ # Initiate 1 job per parameter
124
+ jobs = []
125
+ if not job.inputs:
126
+ jobs.append(
127
+ JobModel(
128
+ task=job.task,
129
+ )
130
+ )
131
+ else:
132
+ for parameter in job.inputs:
133
+ jobs.append(
134
+ JobModel(
135
+ task=job.task,
136
+ input=parameter,
137
+ )
138
+ )
139
+ console.print("[green]:information_source:[/green] [bold]CLI:[/bold] Job(s) validated!")
140
+ return jobs
141
+
142
+
143
+ def prepare_input_sandbox(input_data: dict[str, Any]) -> list[Path]:
144
+ """
145
+ Extract the files from the parameters.
146
+
147
+ :param parameters: The parameters of the job
148
+
149
+ :return: The list of files
150
+ """
151
+ # Get the files from the input data
152
+ files = []
153
+ for _, input_value in input_data.items():
154
+ if isinstance(input_value, list):
155
+ for item in input_value:
156
+ if isinstance(item, File):
157
+ files.append(item)
158
+ elif isinstance(input_value, File):
159
+ files.append(input_value)
160
+
161
+ files_path = []
162
+ for file in files:
163
+ # TODO: path is not the only attribute to consider, but so far it is the only one used
164
+ if not file.location and not file.path:
165
+ raise NotImplementedError("File path is not defined.")
166
+
167
+ if file.path:
168
+ file_path = Path(file.path.replace("file://", ""))
169
+ files_path.append(file_path)
170
+
171
+ return files_path
172
+
173
+
174
+ # -----------------------------------------------------------------------------
175
+ # dirac-router commands
176
+ # -----------------------------------------------------------------------------
177
+
178
+
179
+ def submit_job_router(job: JobSubmissionModel) -> bool:
180
+ """
181
+ Execute a job using the router.
182
+
183
+ :param job: The task to execute
184
+
185
+ :return: True if the job executed successfully, False otherwise
186
+ """
187
+ logger = logging.getLogger("JobRouter")
188
+
189
+ os.environ["DIRAC_PROTO_LOCAL"] = "1"
190
+
191
+ # Validate the jobs
192
+ jobs = validate_jobs(job)
193
+
194
+ # Execute the job locally
195
+ logger.info("Executing jobs locally...")
196
+ results = []
197
+
198
+ for job in jobs:
199
+ job_id = random.randint(1000, 9999)
200
+ results.append(run_job(job_id, job, logger.getChild(f"job-{job_id}")))
201
+
202
+ return all(results)
203
+
204
+
205
+ # -----------------------------------------------------------------------------
206
+ # Worker node execution
207
+ # -----------------------------------------------------------------------------
208
+
209
+
210
+ def run_job(job_id: int, job: JobModel, logger: logging.Logger) -> bool:
211
+ """
212
+ Run a single job by dumping it to JSON and executing the job_wrapper_template.py script.
213
+
214
+ :param job: The job to execute
215
+ :param logger: Logger instance for output
216
+
217
+ :return: True if the job executed successfully, False otherwise
218
+ """
219
+ logger.info("Executing job locally:\n")
220
+ print_json(job.model_dump_json(indent=4))
221
+
222
+ # Dump job to a JSON file
223
+ job_json_path = Path(f"job_{job_id}.json")
224
+ with open(job_json_path, "w") as f:
225
+ f.write(job.model_dump_json())
226
+
227
+ # Run the job_wrapper_template.py script via bash command
228
+ result = subprocess.run(
229
+ [
230
+ "python",
231
+ "-m",
232
+ "dirac_cwl.job.job_wrapper_template",
233
+ str(job_json_path),
234
+ ],
235
+ capture_output=True,
236
+ text=True,
237
+ )
238
+
239
+ # Clean up the job JSON file
240
+ job_json_path.unlink()
241
+
242
+ # Log output
243
+ if result.stdout:
244
+ logger.info("STDOUT %s:\n%s", job_id, result.stdout)
245
+ if result.stderr:
246
+ logger.error("STDERR %s:\n%s", job_id, result.stderr)
247
+
248
+ logger.info("Job execution completed.")
249
+ return result.returncode == 0
@@ -0,0 +1,375 @@
1
+ #!/usr/bin/env python
2
+ """Job wrapper for executing CWL workflows with DIRAC."""
3
+
4
+ import json
5
+ import logging
6
+ import os
7
+ import random
8
+ import shutil
9
+ import subprocess
10
+ from pathlib import Path
11
+ from typing import Any, List, Sequence, cast
12
+
13
+ from cwl_utils.parser import (
14
+ save,
15
+ )
16
+ from cwl_utils.parser.cwl_v1_2 import (
17
+ CommandLineTool,
18
+ ExpressionTool,
19
+ File,
20
+ Saveable,
21
+ Workflow,
22
+ )
23
+ from DIRACCommon.Core.Utilities.ReturnValues import ( # type: ignore[import-untyped]
24
+ returnValueOrRaise,
25
+ )
26
+ from rich.text import Text
27
+ from ruamel.yaml import YAML
28
+
29
+ from dirac_cwl.commands import PostProcessCommand, PreProcessCommand
30
+ from dirac_cwl.core.exceptions import WorkflowProcessingException
31
+ from dirac_cwl.core.utility import get_lfns
32
+ from dirac_cwl.execution_hooks import ExecutionHooksHint
33
+ from dirac_cwl.execution_hooks.core import ExecutionHooksBasePlugin
34
+ from dirac_cwl.submission_models import (
35
+ JobInputModel,
36
+ JobModel,
37
+ )
38
+
39
+ if os.getenv("DIRAC_PROTO_LOCAL") == "1":
40
+ from dirac_cwl.data_management_mocks.sandbox import create_sandbox, download_sandbox # type: ignore[no-redef]
41
+ else:
42
+ from diracx.api.jobs import create_sandbox, download_sandbox # type: ignore[no-redef]
43
+
44
+ # -----------------------------------------------------------------------------
45
+ # JobWrapper
46
+ # -----------------------------------------------------------------------------
47
+
48
+ logger = logging.getLogger(__name__)
49
+
50
+
51
+ class JobWrapper:
52
+ """Job Wrapper for the execution hook."""
53
+
54
+ def __init__(self) -> None:
55
+ """Initialize the job wrapper."""
56
+ self.execution_hooks_plugin: ExecutionHooksBasePlugin | None = None
57
+ self.job_path: Path = Path()
58
+
59
+ def __download_input_sandbox(self, arguments: JobInputModel, job_path: Path) -> None:
60
+ """Download the files from the sandbox store.
61
+
62
+ :param arguments: Job input model containing sandbox information.
63
+ :param job_path: Path to the job working directory.
64
+ """
65
+ assert arguments.sandbox is not None
66
+ if not self.execution_hooks_plugin:
67
+ raise RuntimeError("Could not download sandboxes")
68
+ for sandbox in arguments.sandbox:
69
+ download_sandbox(sandbox, job_path)
70
+
71
+ def __upload_output_sandbox(
72
+ self,
73
+ outputs: dict[str, str | Path | Sequence[str | Path]],
74
+ ):
75
+ if not self.execution_hooks_plugin:
76
+ raise RuntimeError("Could not upload sandbox : Execution hook is not defined.")
77
+
78
+ outputs_to_sandbox = []
79
+ for output_name, src_path in outputs.items():
80
+ if self.execution_hooks_plugin.output_sandbox and output_name in self.execution_hooks_plugin.output_sandbox:
81
+ if isinstance(src_path, Path) or isinstance(src_path, str):
82
+ src_path = [src_path]
83
+ for path in src_path:
84
+ outputs_to_sandbox.append(path)
85
+
86
+ sb_path = Path(create_sandbox(outputs_to_sandbox))
87
+ logger.info("Successfully stored output %s in Sandbox %s", self.execution_hooks_plugin.output_sandbox, sb_path)
88
+
89
+ def __download_input_data(self, inputs: JobInputModel, job_path: Path) -> dict[str, Path | list[Path]]:
90
+ """Download LFNs into the job working directory.
91
+
92
+ :param JobInputModel inputs:
93
+ The job input model containing ``lfns_input``, a mapping from input names to one or more LFN paths.
94
+ :param Path job_path:
95
+ Path to the job working directory where files will be copied.
96
+
97
+ :return dict[str, Path | list[Path]]:
98
+ A dictionary mapping each input name to the corresponding downloaded
99
+ file path(s) located in the working directory.
100
+ """
101
+ new_paths: dict[str, Path | list[Path]] = {}
102
+ if not self.execution_hooks_plugin:
103
+ raise RuntimeWarning("Could not download input data: Execution hook is not defined.")
104
+
105
+ lfns_inputs = get_lfns(inputs.cwl)
106
+
107
+ if lfns_inputs:
108
+ for input_name, lfns in lfns_inputs.items():
109
+ res = returnValueOrRaise(self.execution_hooks_plugin._datamanager.getFile(lfns, str(job_path)))
110
+ if res["Failed"]:
111
+ raise RuntimeError(f"Could not get files : {res['Failed']}")
112
+ paths = res["Successful"]
113
+ if paths and isinstance(lfns, list):
114
+ new_paths[input_name] = [Path(paths[lfn]).relative_to(job_path.resolve()) for lfn in paths]
115
+ elif paths and isinstance(lfns, str):
116
+ new_paths[input_name] = Path(paths[lfns]).relative_to(job_path.resolve())
117
+ return new_paths
118
+
119
+ def __update_inputs(self, inputs: JobInputModel, updates: dict[str, Path | list[Path]]):
120
+ """Update CWL job inputs with new file paths.
121
+
122
+ This method updates the `inputs.cwl` object by replacing or adding
123
+ file paths for each input specified in `updates`. It supports both
124
+ single files and lists of files.
125
+
126
+ :param inputs: The job input model whose `cwl` dictionary will be updated.
127
+ :type inputs: JobInputModel
128
+ :param updates: Dictionary mapping input names to their corresponding local file
129
+ paths. Each value can be a single `Path` or a list of `Path` objects.
130
+ :type updates: dict[str, Path | list[Path]]
131
+
132
+ .. note::
133
+ This method is typically called after downloading LFNs
134
+ using `download_lfns` to ensure that the CWL job inputs reference
135
+ the correct local files.
136
+ """
137
+ for _, value in inputs.cwl.items():
138
+ files = value if isinstance(value, list) else [value]
139
+ for file in files:
140
+ if isinstance(file, File) and file.path:
141
+ file.path = Path(file.path).name
142
+ for input_name, path in updates.items():
143
+ if isinstance(path, Path):
144
+ inputs.cwl[input_name] = File(path=str(path))
145
+ else:
146
+ inputs.cwl[input_name] = []
147
+ for p in path:
148
+ inputs.cwl[input_name].append(File(path=str(p)))
149
+
150
+ def __parse_output_filepaths(self, stdout: str) -> dict[str, str | Path | Sequence[str | Path]]:
151
+ """Get the outputted filepaths per output.
152
+
153
+ :param str stdout:
154
+ The console output of the the job
155
+
156
+ :return dict[str, list[str]]:
157
+ The dict of the list of filepaths for each output
158
+ """
159
+ outputted_files: dict[str, str | Path | Sequence[str | Path]] = {}
160
+ outputs = json.loads(stdout)
161
+ for output, files in outputs.items():
162
+ if not files:
163
+ continue
164
+ if not isinstance(files, list):
165
+ files = [files]
166
+ file_paths = []
167
+ for file in files:
168
+ if file:
169
+ file_paths.append(str(file["path"]))
170
+ outputted_files[output] = file_paths
171
+ return outputted_files
172
+
173
+ def pre_process(
174
+ self,
175
+ executable: CommandLineTool | Workflow | ExpressionTool,
176
+ arguments: JobInputModel | None,
177
+ ) -> list[str]:
178
+ """
179
+ Pre-process the job before execution.
180
+
181
+ :return: True if the job is pre-processed successfully, False otherwise
182
+ """
183
+ logger = logging.getLogger("JobWrapper - Pre-process")
184
+
185
+ # Prepare the task for cwltool
186
+ logger.info("Preparing the task for cwltool...")
187
+ command = ["cwltool", "--parallel"]
188
+
189
+ task_dict = save(executable)
190
+ task_path = self.job_path / "task.cwl"
191
+ with open(task_path, "w") as task_file:
192
+ YAML().dump(task_dict, task_file)
193
+ command.append(str(task_path.name))
194
+
195
+ if arguments:
196
+ if arguments.sandbox:
197
+ # Download the files from the sandbox store
198
+ logger.info("Downloading the files from the sandbox store...")
199
+ self.__download_input_sandbox(arguments, self.job_path)
200
+ logger.info("Files downloaded successfully!")
201
+
202
+ updates = self.__download_input_data(arguments, self.job_path)
203
+ self.__update_inputs(arguments, updates)
204
+
205
+ logger.info("Preparing the parameters for cwltool...")
206
+ parameter_dict = save(cast(Saveable, arguments.cwl))
207
+ parameter_path = self.job_path / "parameter.cwl"
208
+ with open(parameter_path, "w") as parameter_file:
209
+ YAML().dump(parameter_dict, parameter_file)
210
+ command.append(str(parameter_path.name))
211
+
212
+ if self.execution_hooks_plugin:
213
+ return self.__pre_process_hooks(executable, arguments, self.job_path, command)
214
+
215
+ return command
216
+
217
+ def post_process(
218
+ self,
219
+ status: int,
220
+ stdout: str,
221
+ stderr: str,
222
+ ):
223
+ """
224
+ Post-process the job after execution.
225
+
226
+ :return: True if the job is post-processed successfully, False otherwise
227
+ """
228
+ logger = logging.getLogger("JobWrapper - Post-process")
229
+ if status != 0:
230
+ raise RuntimeError(f"Error {status} during the task execution.")
231
+
232
+ logger.info(stdout)
233
+ logger.info(stderr)
234
+
235
+ outputs = self.__parse_output_filepaths(stdout)
236
+
237
+ success = True
238
+
239
+ if self.execution_hooks_plugin:
240
+ success = self.__post_process_hooks(self.job_path, outputs=outputs)
241
+
242
+ self.__upload_output_sandbox(outputs=outputs)
243
+
244
+ return success
245
+
246
+ def __pre_process_hooks(
247
+ self,
248
+ executable: CommandLineTool | Workflow | ExpressionTool,
249
+ arguments: Any | None,
250
+ job_path: Path,
251
+ command: List[str],
252
+ **kwargs: Any,
253
+ ) -> List[str]:
254
+ """Pre-process job inputs and command before execution.
255
+
256
+ :param CommandLineTool | Workflow | ExpressionTool executable:
257
+ The CWL tool, workflow, or expression to be executed.
258
+ :param JobInputModel arguments:
259
+ The job inputs, including CWL and LFN data.
260
+ :param Path job_path:
261
+ Path to the job working directory.
262
+ :param list[str] command:
263
+ The command to be executed, which will be modified.
264
+ :param Any **kwargs:
265
+ Additional parameters, allowing extensions to pass extra context
266
+ or configuration options.
267
+
268
+ :return list[str]:
269
+ The modified command, typically including the serialized CWL
270
+ input file path.
271
+ """
272
+ if not self.execution_hooks_plugin:
273
+ raise RuntimeWarning("Could not run pre_process_hooks: Execution hook is not defined.")
274
+
275
+ for preprocess_command in self.execution_hooks_plugin.preprocess_commands:
276
+ if not issubclass(preprocess_command, PreProcessCommand):
277
+ msg = f"The command {preprocess_command} is not a {PreProcessCommand.__name__}"
278
+ logger.error(msg)
279
+ raise TypeError(msg)
280
+
281
+ try:
282
+ preprocess_command().execute(job_path, **kwargs)
283
+ except Exception as e:
284
+ msg = f"Command '{preprocess_command.__name__}' failed during the pre-process stage: {e}"
285
+ logger.exception(msg)
286
+ raise WorkflowProcessingException(msg) from e
287
+
288
+ return command
289
+
290
+ def __post_process_hooks(
291
+ self,
292
+ job_path: Path,
293
+ outputs: dict[str, str | Path | Sequence[str | Path]] = {},
294
+ **kwargs: Any,
295
+ ) -> bool:
296
+ """Post-process job outputs.
297
+
298
+ :param Path job_path:
299
+ Path to the job working directory.
300
+ :param str|None stdout:
301
+ cwltool standard output.
302
+ :param Any **kwargs:
303
+ Additional keyword arguments for extensibility.
304
+ """
305
+ if not self.execution_hooks_plugin:
306
+ raise RuntimeWarning("Could not run post_process_hooks: Execution hook is not defined.")
307
+
308
+ for postprocess_command in self.execution_hooks_plugin.postprocess_commands:
309
+ if not issubclass(postprocess_command, PostProcessCommand):
310
+ msg = f"The command {postprocess_command} is not a {PostProcessCommand.__name__}"
311
+ logger.error(msg)
312
+ raise TypeError(msg)
313
+
314
+ try:
315
+ postprocess_command().execute(job_path, **kwargs)
316
+ except Exception as e:
317
+ msg = f"Command '{postprocess_command.__name__}' failed during the post-process stage: {e}"
318
+ logger.exception(msg)
319
+ raise WorkflowProcessingException(msg) from e
320
+
321
+ self.execution_hooks_plugin.store_output(outputs)
322
+ return True
323
+
324
+ def run_job(self, job: JobModel) -> bool:
325
+ """Execute a given CWL workflow using cwltool.
326
+
327
+ This is the equivalent of the DIRAC JobWrapper.
328
+
329
+ :param job: The job model containing workflow and inputs.
330
+ :return: True if the job is executed successfully, False otherwise.
331
+ """
332
+ logger = logging.getLogger("JobWrapper")
333
+ # Instantiate runtime metadata from the serializable descriptor and
334
+ # the job context so implementations can access task inputs/overrides.
335
+ job_execution_hooks = ExecutionHooksHint.from_cwl(job.task)
336
+ self.execution_hooks_plugin = job_execution_hooks.to_runtime(job) if job_execution_hooks else None
337
+
338
+ # Isolate the job in a specific directory
339
+ self.job_path = Path(".") / "workernode" / f"{random.randint(1000, 9999)}"
340
+ self.job_path.mkdir(parents=True, exist_ok=True)
341
+
342
+ try:
343
+ # Pre-process the job
344
+ logger.info("Pre-processing Task...")
345
+ command = self.pre_process(job.task, job.input)
346
+ logger.info("Task pre-processed successfully!")
347
+
348
+ # Execute the task
349
+ logger.info("Executing Task: %s", command)
350
+ result = subprocess.run(command, capture_output=True, text=True, cwd=self.job_path)
351
+
352
+ if result.returncode != 0:
353
+ logger.error("Error in executing workflow:\n%s", Text.from_ansi(result.stderr))
354
+ return False
355
+ logger.info("Task executed successfully!")
356
+
357
+ # Post-process the job
358
+ logger.info("Post-processing Task...")
359
+ if self.post_process(
360
+ result.returncode,
361
+ result.stdout,
362
+ result.stderr,
363
+ ):
364
+ logger.info("Task post-processed successfully!")
365
+ return True
366
+ logger.error("Failed to post-process Task")
367
+ return False
368
+
369
+ except Exception:
370
+ logger.exception("JobWrapper: Failed to execute workflow")
371
+ return False
372
+ finally:
373
+ # Clean up
374
+ if self.job_path.exists():
375
+ shutil.rmtree(self.job_path)
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env python
2
+ """Job wrapper template for executing CWL jobs."""
3
+
4
+ import json
5
+ import logging
6
+ import os
7
+ import sys
8
+ import tempfile
9
+
10
+ import DIRAC # type: ignore[import-untyped]
11
+ from cwl_utils.parser import load_document_by_uri
12
+ from cwl_utils.parser.cwl_v1_2_utils import load_inputfile
13
+ from ruamel.yaml import YAML
14
+
15
+ if os.getenv("DIRAC_PROTO_LOCAL") != "1":
16
+ DIRAC.initialize()
17
+
18
+ from dirac_cwl.job.job_wrapper import JobWrapper
19
+ from dirac_cwl.submission_models import JobModel
20
+
21
+
22
+ def main():
23
+ """Execute the job wrapper for a given job model."""
24
+ if len(sys.argv) != 2:
25
+ logging.error("1 argument is required")
26
+ sys.exit(1)
27
+
28
+ job_json_file = sys.argv[1]
29
+ job_wrapper = JobWrapper()
30
+ with open(job_json_file, "r") as file:
31
+ job_model_dict = json.load(file)
32
+
33
+ task_dict = job_model_dict["task"]
34
+
35
+ with tempfile.NamedTemporaryFile("w+", suffix=".cwl", delete=False) as f:
36
+ YAML().dump(task_dict, f)
37
+ f.flush()
38
+ task_obj = load_document_by_uri(f.name)
39
+
40
+ if job_model_dict["input"]:
41
+ cwl_inputs_obj = load_inputfile(job_model_dict["input"]["cwl"])
42
+ job_model_dict["input"]["cwl"] = cwl_inputs_obj
43
+ job_model_dict["task"] = task_obj
44
+
45
+ job = JobModel.model_validate(job_model_dict)
46
+
47
+ res = job_wrapper.run_job(job)
48
+ if res:
49
+ logging.info("Job done.")
50
+ else:
51
+ logging.info("Job failed.")
52
+ sys.exit(1)
53
+
54
+
55
+ if __name__ == "__main__":
56
+ main()