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,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)}")