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.
- dirac_cwl/__init__.py +28 -0
- dirac_cwl/commands/__init__.py +5 -0
- dirac_cwl/commands/core.py +37 -0
- dirac_cwl/commands/download_config.py +22 -0
- dirac_cwl/commands/group_outputs.py +32 -0
- dirac_cwl/core/__init__.py +1 -0
- dirac_cwl/core/exceptions.py +5 -0
- dirac_cwl/core/utility.py +41 -0
- dirac_cwl/data_management_mocks/data_manager.py +99 -0
- dirac_cwl/data_management_mocks/file_catalog.py +132 -0
- dirac_cwl/data_management_mocks/sandbox.py +89 -0
- dirac_cwl/execution_hooks/__init__.py +40 -0
- dirac_cwl/execution_hooks/core.py +342 -0
- dirac_cwl/execution_hooks/plugins/__init__.py +16 -0
- dirac_cwl/execution_hooks/plugins/core.py +58 -0
- dirac_cwl/execution_hooks/registry.py +209 -0
- dirac_cwl/job/__init__.py +249 -0
- dirac_cwl/job/job_wrapper.py +375 -0
- dirac_cwl/job/job_wrapper_template.py +56 -0
- dirac_cwl/job/submission_clients.py +166 -0
- dirac_cwl/modules/crypto.py +96 -0
- dirac_cwl/modules/pi_gather.py +41 -0
- dirac_cwl/modules/pi_simulate.py +33 -0
- dirac_cwl/production/__init__.py +200 -0
- dirac_cwl/submission_models.py +157 -0
- dirac_cwl/transformation/__init__.py +203 -0
- dirac_cwl-1.0.2.dist-info/METADATA +285 -0
- dirac_cwl-1.0.2.dist-info/RECORD +32 -0
- dirac_cwl-1.0.2.dist-info/WHEEL +5 -0
- dirac_cwl-1.0.2.dist-info/entry_points.txt +8 -0
- dirac_cwl-1.0.2.dist-info/licenses/LICENSE +674 -0
- dirac_cwl-1.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Submission client characteristics used in job client.
|
|
3
|
+
|
|
4
|
+
This module contains functions to manage job submission to the prototype, DIRAC, and DiracX backends.
|
|
5
|
+
It is not meant to be integrated to DiracX logic itself in the future.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from diracx.api.jobs import create_sandbox
|
|
12
|
+
from diracx.client.aio import AsyncDiracClient
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
|
|
15
|
+
from dirac_cwl.core.utility import get_lfns
|
|
16
|
+
from dirac_cwl.execution_hooks import SchedulingHint
|
|
17
|
+
from dirac_cwl.submission_models import JobModel, JobSubmissionModel
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SubmissionClient(ABC):
|
|
23
|
+
"""Abstract base class for job submission strategies."""
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
async def create_sandbox(self, isb_file_paths: list[Path]) -> str | None:
|
|
27
|
+
"""
|
|
28
|
+
Upload parameter files to the sandbox store.
|
|
29
|
+
|
|
30
|
+
:param isb_file_paths: List of input sandbox file paths
|
|
31
|
+
:param parameter_path: Path to the parameter file
|
|
32
|
+
:return: Sandbox PFN or None
|
|
33
|
+
"""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
async def submit_job(self, job_submission: JobSubmissionModel) -> bool:
|
|
38
|
+
"""
|
|
39
|
+
Submit a job to the backend.
|
|
40
|
+
|
|
41
|
+
:param job_submission: Job submission model
|
|
42
|
+
"""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PrototypeSubmissionClient(SubmissionClient):
|
|
47
|
+
"""Submission client for local/prototype execution."""
|
|
48
|
+
|
|
49
|
+
async def create_sandbox(self, isb_file_paths: list[Path]) -> str | None:
|
|
50
|
+
"""
|
|
51
|
+
Upload files to the local sandbox store.
|
|
52
|
+
|
|
53
|
+
:param isb_file_paths: List of input sandbox file paths
|
|
54
|
+
:param parameter_path: Path to the parameter file (not used in local mode)
|
|
55
|
+
:return: Sandbox PFN or None
|
|
56
|
+
"""
|
|
57
|
+
from dirac_cwl.data_management_mocks.sandbox import (
|
|
58
|
+
create_sandbox,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if not isb_file_paths:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
return create_sandbox(paths=isb_file_paths)
|
|
65
|
+
|
|
66
|
+
async def submit_job(self, job_submission: JobSubmissionModel) -> bool:
|
|
67
|
+
"""
|
|
68
|
+
Submit a job to the backend.
|
|
69
|
+
|
|
70
|
+
:param job_submission: Job submission model
|
|
71
|
+
"""
|
|
72
|
+
from dirac_cwl.job import submit_job_router
|
|
73
|
+
|
|
74
|
+
result = submit_job_router(job_submission)
|
|
75
|
+
if result:
|
|
76
|
+
console.print("[green]:heavy_check_mark:[/green] [bold]CLI:[/bold] Job(s) done.")
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class DIRACSubmissionClient(SubmissionClient):
|
|
81
|
+
"""Submission client for DIRAC/DiracX production execution."""
|
|
82
|
+
|
|
83
|
+
async def create_sandbox(
|
|
84
|
+
self,
|
|
85
|
+
isb_file_paths: list[Path],
|
|
86
|
+
) -> str | None:
|
|
87
|
+
"""
|
|
88
|
+
Upload parameter files to the sandbox store.
|
|
89
|
+
|
|
90
|
+
:param isb_file_paths: List of input sandbox file paths
|
|
91
|
+
:return: Sandbox PFN or None
|
|
92
|
+
"""
|
|
93
|
+
return await create_sandbox(isb_file_paths)
|
|
94
|
+
|
|
95
|
+
async def submit_job(self, job_submission: JobSubmissionModel) -> bool:
|
|
96
|
+
"""
|
|
97
|
+
Submit a job to the backend.
|
|
98
|
+
|
|
99
|
+
:param job_submission: Job submission model
|
|
100
|
+
"""
|
|
101
|
+
from dirac_cwl.job import validate_jobs
|
|
102
|
+
|
|
103
|
+
jdls = []
|
|
104
|
+
job_submission_path = Path("job.json")
|
|
105
|
+
for job in validate_jobs(job_submission):
|
|
106
|
+
# Dump the job model to a file
|
|
107
|
+
with open(job_submission_path, "w") as f:
|
|
108
|
+
f.write(job.model_dump_json())
|
|
109
|
+
|
|
110
|
+
# Convert job.json to jdl
|
|
111
|
+
console.print("\t\t[blue]:information_source:[/blue] [bold]CLI:[/bold] Converting job model to jdl...")
|
|
112
|
+
sandbox_id = await create_sandbox([job_submission_path])
|
|
113
|
+
job_submission_path.unlink()
|
|
114
|
+
|
|
115
|
+
jdl = self.convert_to_jdl(job, sandbox_id)
|
|
116
|
+
jdls.append(jdl)
|
|
117
|
+
|
|
118
|
+
console.print("\t\t[blue]:information_source:[/blue] [bold]CLI:[/bold] Call diracx: jobs/jdl router...")
|
|
119
|
+
|
|
120
|
+
async with AsyncDiracClient() as api:
|
|
121
|
+
jdl_jobs = await api.jobs.submit_jdl_jobs(jdls)
|
|
122
|
+
|
|
123
|
+
console.print(
|
|
124
|
+
f"\t\t[green]:information_source:[/green] [bold]CLI:[/bold] Inserted {len(jdl_jobs)} jobs with ids: \
|
|
125
|
+
{','.join(map(str, (jdl_job.job_id for jdl_job in jdl_jobs)))}"
|
|
126
|
+
)
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
def convert_to_jdl(self, job: JobModel, sandbox_pfn: str) -> str:
|
|
130
|
+
"""
|
|
131
|
+
Convert job model to jdl.
|
|
132
|
+
|
|
133
|
+
:param job: The task to execute
|
|
134
|
+
:param sandbox_pfn: The sandbox PFN
|
|
135
|
+
:return: JDL string
|
|
136
|
+
"""
|
|
137
|
+
jdl_lines = []
|
|
138
|
+
jdl_lines.append("Executable = dirac-cwl-exec;")
|
|
139
|
+
jdl_lines.append("Arguments = job.json;")
|
|
140
|
+
|
|
141
|
+
if job.task.requirements and job.task.requirements[0].coresMin:
|
|
142
|
+
jdl_lines.append(f"NumberOfProcessors = {job.task.requirements[0].coresMin};")
|
|
143
|
+
|
|
144
|
+
jdl_lines.append("JobName = test;")
|
|
145
|
+
jdl_lines.append("OutputSandbox = {std.out, std.err};")
|
|
146
|
+
|
|
147
|
+
job_scheduling = SchedulingHint.from_cwl(job.task)
|
|
148
|
+
if job_scheduling.priority:
|
|
149
|
+
jdl_lines.append(f"Priority = {job_scheduling.priority};")
|
|
150
|
+
|
|
151
|
+
if job_scheduling.sites:
|
|
152
|
+
jdl_lines.append(f"Site = {job_scheduling.sites};")
|
|
153
|
+
|
|
154
|
+
jdl_lines.append(f"InputSandbox = {sandbox_pfn};")
|
|
155
|
+
if job.input:
|
|
156
|
+
formatted_lfns = []
|
|
157
|
+
lfns_list = get_lfns(job.input.cwl).values()
|
|
158
|
+
for lfns in lfns_list:
|
|
159
|
+
for lfn in lfns:
|
|
160
|
+
formatted_lfns.append(str(lfn).replace("lfn:", "LFN:", 1))
|
|
161
|
+
|
|
162
|
+
lfns_str = ", ".join(formatted_lfns)
|
|
163
|
+
if lfns_str:
|
|
164
|
+
jdl_lines.append(f"InputData = {lfns_str};")
|
|
165
|
+
|
|
166
|
+
return "\n".join(jdl_lines)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Cryptographic utility functions for CWL workflows."""
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
import codecs
|
|
6
|
+
import hashlib
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
app = typer.Typer()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def caesar_cipher(text: str, shift: int) -> str:
|
|
15
|
+
"""Apply Caesar cipher encryption to text.
|
|
16
|
+
|
|
17
|
+
:param text: Text to encrypt.
|
|
18
|
+
:param shift: Number of positions to shift.
|
|
19
|
+
:return: Encrypted text.
|
|
20
|
+
"""
|
|
21
|
+
encrypted_text = []
|
|
22
|
+
for char in text:
|
|
23
|
+
if char.isalpha():
|
|
24
|
+
# Shift within alphabet bounds
|
|
25
|
+
shifted = chr((ord(char.lower()) - 97 + shift) % 26 + 97)
|
|
26
|
+
encrypted_text.append(shifted if char.islower() else shifted.upper())
|
|
27
|
+
else:
|
|
28
|
+
encrypted_text.append(char)
|
|
29
|
+
return "".join(encrypted_text)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@app.command("caesar")
|
|
33
|
+
def caesar_command(input_string: str, shift_value: int):
|
|
34
|
+
"""Apply Caesar cipher to the input string with a given shift."""
|
|
35
|
+
result = caesar_cipher(input_string, shift_value)
|
|
36
|
+
typer.echo(f"Caesar Cipher Result: {result}")
|
|
37
|
+
Path("caesar_result.txt").write_text(result)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def base64_encode(text: str) -> str:
|
|
41
|
+
"""Encode text using Base64 encoding.
|
|
42
|
+
|
|
43
|
+
:param text: Text to encode.
|
|
44
|
+
:return: Base64 encoded string.
|
|
45
|
+
"""
|
|
46
|
+
byte_data = text.encode("utf-8")
|
|
47
|
+
base64_encoded = base64.b64encode(byte_data).decode("utf-8")
|
|
48
|
+
return base64_encoded
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app.command("base64")
|
|
52
|
+
def base64_command(input_string: str):
|
|
53
|
+
"""Base64 encode the input string."""
|
|
54
|
+
result = base64_encode(input_string)
|
|
55
|
+
typer.echo(f"Base64 Encoded Result: {result}")
|
|
56
|
+
Path("base64_result.txt").write_text(result)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def md5_hash(text: str) -> str:
|
|
60
|
+
"""Compute MD5 hash of text.
|
|
61
|
+
|
|
62
|
+
:param text: Text to hash.
|
|
63
|
+
:return: MD5 hash string.
|
|
64
|
+
"""
|
|
65
|
+
md5_result = hashlib.md5(text.encode("utf-8")).hexdigest()
|
|
66
|
+
return md5_result
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@app.command("md5")
|
|
70
|
+
def md5_command(input_string: str):
|
|
71
|
+
"""Compute the MD5 hash of the input string."""
|
|
72
|
+
result = md5_hash(input_string)
|
|
73
|
+
typer.echo(f"MD5 Hash Result: {result}")
|
|
74
|
+
Path("md5_result.txt").write_text(result)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def rot13_encrypt(text: str) -> str:
|
|
78
|
+
"""Apply ROT13 encryption to text.
|
|
79
|
+
|
|
80
|
+
:param text: Text to encrypt.
|
|
81
|
+
:return: ROT13 encrypted string.
|
|
82
|
+
"""
|
|
83
|
+
rot13_result = codecs.encode(text, "rot_13")
|
|
84
|
+
return rot13_result
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@app.command("rot13")
|
|
88
|
+
def rot13_command(input_string: str):
|
|
89
|
+
"""Apply ROT13 encryption to the input string."""
|
|
90
|
+
result = rot13_encrypt(input_string)
|
|
91
|
+
typer.echo(f"ROT13 Result: {result}")
|
|
92
|
+
Path("rot13_result.txt").write_text(result)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
app()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Pi estimation gathering module using Monte Carlo results."""
|
|
3
|
+
|
|
4
|
+
import math
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
app = typer.Typer()
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command()
|
|
15
|
+
def process(files: List[str] = typer.Argument(..., help="Paths to the input files")):
|
|
16
|
+
"""Process the input points and estimate the value of Pi using the Monte Carlo method."""
|
|
17
|
+
inside_circle = 0
|
|
18
|
+
total_points = 0
|
|
19
|
+
|
|
20
|
+
# Read points from file and check if they fall within the unit circle
|
|
21
|
+
with open(files[0], "r") as f:
|
|
22
|
+
for line in f:
|
|
23
|
+
x, y = map(float, line.split())
|
|
24
|
+
if math.sqrt(x**2 + y**2) <= 1:
|
|
25
|
+
inside_circle += 1
|
|
26
|
+
total_points += 1
|
|
27
|
+
|
|
28
|
+
# Estimate Pi
|
|
29
|
+
pi_estimate = 4 * (inside_circle / total_points)
|
|
30
|
+
|
|
31
|
+
# Write the result to a file
|
|
32
|
+
output_name = "result_final.sim"
|
|
33
|
+
with open(output_name, "w") as f:
|
|
34
|
+
f.write(f"Approximation of Pi: {pi_estimate}\n")
|
|
35
|
+
|
|
36
|
+
console.print(f"Pi approximation: [bold yellow]{pi_estimate}[/bold yellow]")
|
|
37
|
+
return output_name
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if __name__ == "__main__":
|
|
41
|
+
app()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Monte Carlo simulation module for Pi estimation."""
|
|
3
|
+
|
|
4
|
+
import random
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
app = typer.Typer()
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command()
|
|
14
|
+
def simulate(num_points: int = typer.Argument(..., help="Number of random points to generate")):
|
|
15
|
+
"""Simulate random points inside a square (Monte Carlo method)."""
|
|
16
|
+
points = []
|
|
17
|
+
|
|
18
|
+
for _ in range(num_points):
|
|
19
|
+
x, y = random.uniform(-1, 1), random.uniform(-1, 1)
|
|
20
|
+
points.append((x, y))
|
|
21
|
+
|
|
22
|
+
# Save points to file
|
|
23
|
+
output_path = "result.sim"
|
|
24
|
+
with open(output_path, "w") as f:
|
|
25
|
+
for point in points:
|
|
26
|
+
f.write(f"{point[0]} {point[1]}\n")
|
|
27
|
+
|
|
28
|
+
console.print(f"Generated [bold green]{num_points}[/bold green] random points.")
|
|
29
|
+
return output_path
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if __name__ == "__main__":
|
|
33
|
+
app()
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""CLI interface to run a workflow as a production."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from cwl_utils.pack import pack
|
|
10
|
+
from cwl_utils.parser import load_document
|
|
11
|
+
from cwl_utils.parser.cwl_v1_2 import (
|
|
12
|
+
CommandLineTool,
|
|
13
|
+
ExpressionTool,
|
|
14
|
+
Workflow,
|
|
15
|
+
WorkflowInputParameter,
|
|
16
|
+
WorkflowStep,
|
|
17
|
+
)
|
|
18
|
+
from rich import print_json
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
from schema_salad.exceptions import ValidationException
|
|
21
|
+
|
|
22
|
+
from dirac_cwl.submission_models import (
|
|
23
|
+
ProductionSubmissionModel,
|
|
24
|
+
TransformationSubmissionModel,
|
|
25
|
+
)
|
|
26
|
+
from dirac_cwl.transformation import (
|
|
27
|
+
submit_transformation_router,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
app = typer.Typer()
|
|
31
|
+
console = Console()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# -----------------------------------------------------------------------------
|
|
35
|
+
# dirac-cli commands
|
|
36
|
+
# -----------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.command("submit")
|
|
40
|
+
def submit_production_client(
|
|
41
|
+
task_path: str = typer.Argument(..., help="Path to the CWL file"),
|
|
42
|
+
# Specific parameter for the purpose of the prototype
|
|
43
|
+
local: Optional[bool] = typer.Option(True, help="Run the job locally instead of submitting it to the router"),
|
|
44
|
+
):
|
|
45
|
+
"""
|
|
46
|
+
Correspond to the dirac-cli command to submit productions.
|
|
47
|
+
|
|
48
|
+
This command will:
|
|
49
|
+
- Validate the workflow
|
|
50
|
+
- Start the production
|
|
51
|
+
"""
|
|
52
|
+
os.environ["DIRAC_PROTO_LOCAL"] = "0"
|
|
53
|
+
|
|
54
|
+
# Validate the workflow
|
|
55
|
+
console.print("[blue]:information_source:[/blue] [bold]CLI:[/bold] Validating the production...")
|
|
56
|
+
try:
|
|
57
|
+
task = load_document(pack(task_path))
|
|
58
|
+
except FileNotFoundError as ex:
|
|
59
|
+
console.print(f"[red]:heavy_multiplication_x:[/red] [bold]CLI:[/bold] Failed to load the task:\n{ex}")
|
|
60
|
+
return typer.Exit(code=1)
|
|
61
|
+
except ValidationException as ex:
|
|
62
|
+
console.print(f"[red]:heavy_multiplication_x:[/red] [bold]CLI:[/bold] Failed to validate the task:\n{ex}")
|
|
63
|
+
return typer.Exit(code=1)
|
|
64
|
+
console.print(f"\t[green]:heavy_check_mark:[/green] Task {task_path}")
|
|
65
|
+
console.print("\t[green]:heavy_check_mark:[/green] Metadata")
|
|
66
|
+
|
|
67
|
+
# Create the production
|
|
68
|
+
production = ProductionSubmissionModel(task=task)
|
|
69
|
+
console.print("[green]:heavy_check_mark:[/green] [bold]CLI:[/bold] Production validated.")
|
|
70
|
+
|
|
71
|
+
# Submit the tranaformation
|
|
72
|
+
console.print("[blue]:information_source:[/blue] [bold]CLI:[/bold] Submitting the production...")
|
|
73
|
+
print_json(production.model_dump_json(indent=4))
|
|
74
|
+
if not submit_production_router(production):
|
|
75
|
+
console.print("[red]:heavy_multiplication_x:[/red] [bold]CLI:[/bold] Failed to run production.")
|
|
76
|
+
return typer.Exit(code=1)
|
|
77
|
+
console.print("[green]:heavy_check_mark:[/green] [bold]CLI:[/bold] Production done.")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# -----------------------------------------------------------------------------
|
|
81
|
+
# dirac-router commands
|
|
82
|
+
# -----------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def submit_production_router(production: ProductionSubmissionModel) -> bool:
|
|
86
|
+
"""Submit a production to the router.
|
|
87
|
+
|
|
88
|
+
:param production: The production to submit
|
|
89
|
+
|
|
90
|
+
:return: True if the production was submitted successfully, False otherwise
|
|
91
|
+
"""
|
|
92
|
+
logger = logging.getLogger("ProductionRouter")
|
|
93
|
+
|
|
94
|
+
# Validate the transformation
|
|
95
|
+
logger.info("Validating the production...")
|
|
96
|
+
# Already validated by the pydantic model
|
|
97
|
+
logger.info("Production validated!")
|
|
98
|
+
|
|
99
|
+
# Split the production into transformations
|
|
100
|
+
logger.info("Creating transformations from production...")
|
|
101
|
+
transformations = _get_transformations(production)
|
|
102
|
+
logger.info("%s transformations created!", len(transformations))
|
|
103
|
+
|
|
104
|
+
# Submit the transformations
|
|
105
|
+
logger.info("Submitting transformations...")
|
|
106
|
+
with ThreadPoolExecutor() as executor:
|
|
107
|
+
results = list(executor.map(submit_transformation_router, transformations))
|
|
108
|
+
|
|
109
|
+
return all(results)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# -----------------------------------------------------------------------------
|
|
113
|
+
# Production management
|
|
114
|
+
# -----------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _get_transformations(
|
|
118
|
+
production: ProductionSubmissionModel,
|
|
119
|
+
) -> List[TransformationSubmissionModel]:
|
|
120
|
+
"""Create transformations from a given production.
|
|
121
|
+
|
|
122
|
+
:param production: The production to create transformations from
|
|
123
|
+
"""
|
|
124
|
+
# Create a subworkflow and a transformation for each step
|
|
125
|
+
transformations = []
|
|
126
|
+
|
|
127
|
+
for step in production.task.steps:
|
|
128
|
+
step_task = _create_subworkflow(step, str(production.task.cwlVersion), production.task.inputs)
|
|
129
|
+
|
|
130
|
+
transformations.append(
|
|
131
|
+
TransformationSubmissionModel(
|
|
132
|
+
task=step_task,
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
return transformations
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _create_subworkflow(
|
|
139
|
+
wf_step: WorkflowStep, cwlVersion: str, inputs: List[WorkflowInputParameter]
|
|
140
|
+
) -> Workflow | CommandLineTool | ExpressionTool:
|
|
141
|
+
"""Create a CWL file for a given step.
|
|
142
|
+
|
|
143
|
+
If the step is a workflow, a new workflow is created.
|
|
144
|
+
If the step is a command line tool, a new command line tool is created.
|
|
145
|
+
|
|
146
|
+
:param wf_step: The step to create a CWL file for
|
|
147
|
+
:param cwlVersion: The CWL version to use
|
|
148
|
+
|
|
149
|
+
:return: The CWL subworkflow
|
|
150
|
+
"""
|
|
151
|
+
new_workflow: Workflow | CommandLineTool
|
|
152
|
+
if wf_step.run.class_ == "Workflow":
|
|
153
|
+
# Handle nested workflows
|
|
154
|
+
new_workflow = Workflow(
|
|
155
|
+
cwlVersion=cwlVersion,
|
|
156
|
+
inputs=wf_step.run.inputs,
|
|
157
|
+
outputs=wf_step.run.outputs,
|
|
158
|
+
steps=wf_step.run.steps,
|
|
159
|
+
requirements=wf_step.run.requirements,
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
# Handle command line tools
|
|
163
|
+
new_workflow = CommandLineTool(
|
|
164
|
+
cwlVersion=cwlVersion,
|
|
165
|
+
arguments=wf_step.run.arguments,
|
|
166
|
+
baseCommand=wf_step.run.baseCommand,
|
|
167
|
+
inputs=wf_step.run.inputs,
|
|
168
|
+
outputs=wf_step.run.outputs,
|
|
169
|
+
requirements=wf_step.run.requirements,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Add the default value to the inputs if any
|
|
173
|
+
for new_workflow_input in new_workflow.inputs:
|
|
174
|
+
found_default = False
|
|
175
|
+
|
|
176
|
+
if not new_workflow_input.id:
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
new_workflow_input_name = new_workflow_input.id.split("#")[-1].split("/")[-1]
|
|
180
|
+
for wf_step_in in wf_step.in_:
|
|
181
|
+
# Skip if the input is not set: this should never happen
|
|
182
|
+
if not wf_step_in.id:
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
if new_workflow_input_name == wf_step_in.id.split("#")[-1].split("/")[-1]:
|
|
186
|
+
# Find the source input from the original workflow
|
|
187
|
+
for input in inputs:
|
|
188
|
+
# Skip if the input is not set: this should never happen
|
|
189
|
+
if not input.id:
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
if input.id == wf_step_in.source:
|
|
193
|
+
new_workflow_input.default = input.default
|
|
194
|
+
found_default = True
|
|
195
|
+
break
|
|
196
|
+
|
|
197
|
+
if found_default:
|
|
198
|
+
break
|
|
199
|
+
|
|
200
|
+
return new_workflow
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enhanced submission models for DIRAC CWL integration.
|
|
3
|
+
|
|
4
|
+
This module provides improved submission models with proper separation of concerns,
|
|
5
|
+
modern Python typing, and comprehensive numpydoc documentation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
from cwl_utils.parser import save
|
|
13
|
+
from cwl_utils.parser.cwl_v1_2 import (
|
|
14
|
+
CommandLineTool,
|
|
15
|
+
ExpressionTool,
|
|
16
|
+
Workflow,
|
|
17
|
+
)
|
|
18
|
+
from pydantic import BaseModel, ConfigDict, field_serializer, model_validator
|
|
19
|
+
|
|
20
|
+
from dirac_cwl.execution_hooks import (
|
|
21
|
+
ExecutionHooksHint,
|
|
22
|
+
SchedulingHint,
|
|
23
|
+
TransformationExecutionHooksHint,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# -----------------------------------------------------------------------------
|
|
27
|
+
# Job models
|
|
28
|
+
# -----------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class JobInputModel(BaseModel):
|
|
32
|
+
"""Input data and sandbox files for a job execution."""
|
|
33
|
+
|
|
34
|
+
# Allow arbitrary types to be passed to the model
|
|
35
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
36
|
+
|
|
37
|
+
sandbox: list[str] | None
|
|
38
|
+
cwl: dict[str, Any]
|
|
39
|
+
|
|
40
|
+
@field_serializer("cwl")
|
|
41
|
+
def serialize_cwl(self, value):
|
|
42
|
+
"""Serialize CWL object to dictionary.
|
|
43
|
+
|
|
44
|
+
:param value: CWL object to serialize.
|
|
45
|
+
:return: Serialized CWL dictionary.
|
|
46
|
+
"""
|
|
47
|
+
return save(value)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class BaseJobModel(BaseModel):
|
|
51
|
+
"""Base class for Job definition."""
|
|
52
|
+
|
|
53
|
+
# Allow arbitrary types to be passed to the model
|
|
54
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
55
|
+
|
|
56
|
+
task: CommandLineTool | Workflow | ExpressionTool
|
|
57
|
+
|
|
58
|
+
@field_serializer("task")
|
|
59
|
+
def serialize_task(self, value):
|
|
60
|
+
"""Serialize CWL task object to dictionary.
|
|
61
|
+
|
|
62
|
+
:param value: CWL task object to serialize.
|
|
63
|
+
:return: Serialized task dictionary.
|
|
64
|
+
:raises TypeError: If value is not a valid CWL task type.
|
|
65
|
+
"""
|
|
66
|
+
if isinstance(value, (CommandLineTool, Workflow, ExpressionTool)):
|
|
67
|
+
return save(value)
|
|
68
|
+
else:
|
|
69
|
+
raise TypeError(f"Cannot serialize type {type(value)}")
|
|
70
|
+
|
|
71
|
+
@model_validator(mode="before")
|
|
72
|
+
def validate_hints(cls, values):
|
|
73
|
+
"""Validate execution hooks and scheduling hints in the task.
|
|
74
|
+
|
|
75
|
+
:param values: Model values dictionary.
|
|
76
|
+
:return: Validated values dictionary.
|
|
77
|
+
"""
|
|
78
|
+
task = values.get("task")
|
|
79
|
+
ExecutionHooksHint.from_cwl(task), SchedulingHint.from_cwl(task)
|
|
80
|
+
return values
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class JobSubmissionModel(BaseJobModel):
|
|
84
|
+
"""Job definition sent to the router."""
|
|
85
|
+
|
|
86
|
+
inputs: list[JobInputModel] | None = None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class JobModel(BaseJobModel):
|
|
90
|
+
"""Job definition sent to the job wrapper."""
|
|
91
|
+
|
|
92
|
+
input: Optional[JobInputModel] = None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# -----------------------------------------------------------------------------
|
|
96
|
+
# Transformation models
|
|
97
|
+
# -----------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TransformationSubmissionModel(BaseModel):
|
|
101
|
+
"""Transformation definition sent to the router."""
|
|
102
|
+
|
|
103
|
+
# Allow arbitrary types to be passed to the model
|
|
104
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
105
|
+
|
|
106
|
+
task: CommandLineTool | Workflow | ExpressionTool
|
|
107
|
+
|
|
108
|
+
@field_serializer("task")
|
|
109
|
+
def serialize_task(self, value):
|
|
110
|
+
"""Serialize CWL task object to dictionary.
|
|
111
|
+
|
|
112
|
+
:param value: CWL task object to serialize.
|
|
113
|
+
:return: Serialized task dictionary.
|
|
114
|
+
:raises TypeError: If value is not a valid CWL task type.
|
|
115
|
+
"""
|
|
116
|
+
if isinstance(value, (CommandLineTool, Workflow, ExpressionTool)):
|
|
117
|
+
return save(value)
|
|
118
|
+
else:
|
|
119
|
+
raise TypeError(f"Cannot serialize type {type(value)}")
|
|
120
|
+
|
|
121
|
+
@model_validator(mode="before")
|
|
122
|
+
def validate_hints(cls, values):
|
|
123
|
+
"""Validate transformation execution hooks and scheduling hints in the task.
|
|
124
|
+
|
|
125
|
+
:param values: Model values dictionary.
|
|
126
|
+
:return: Validated values dictionary.
|
|
127
|
+
"""
|
|
128
|
+
task = values.get("task")
|
|
129
|
+
TransformationExecutionHooksHint.from_cwl(task), SchedulingHint.from_cwl(task)
|
|
130
|
+
return values
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# -----------------------------------------------------------------------------
|
|
134
|
+
# Production models
|
|
135
|
+
# -----------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class ProductionSubmissionModel(BaseModel):
|
|
139
|
+
"""Production definition sent to the router."""
|
|
140
|
+
|
|
141
|
+
# Allow arbitrary types to be passed to the model
|
|
142
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
143
|
+
|
|
144
|
+
task: Workflow
|
|
145
|
+
|
|
146
|
+
@field_serializer("task")
|
|
147
|
+
def serialize_task(self, value):
|
|
148
|
+
"""Serialize CWL workflow object to dictionary.
|
|
149
|
+
|
|
150
|
+
:param value: CWL workflow object to serialize.
|
|
151
|
+
:return: Serialized workflow dictionary.
|
|
152
|
+
:raises TypeError: If value is not a valid CWL workflow type.
|
|
153
|
+
"""
|
|
154
|
+
if isinstance(value, (ExpressionTool, CommandLineTool, Workflow)):
|
|
155
|
+
return save(value)
|
|
156
|
+
else:
|
|
157
|
+
raise TypeError(f"Cannot serialize type {type(value)}")
|