Skip to content

Commit 593c0bf

Browse files
authored
Add a script constructor that allows for flexible script construction and add a robust runner mode (#543)
Signed-off-by: Sambhav Kothari <[email protected]> This PR introduces a new mode for script called callable mode, which runs the function using the hera runner. hera runner is a robust version of the current script mode which can accomodate large files, imports and calling functions with kwargs. It is highly compatible/tied to pydantic and makes writing argo commands a breeze using pydantic + hera. It automatically serializes/deserializes inputs/outputs using pydantic and also updates the script callable to only return a task when under an active hera context. This mode is only possible if the user source code is installed in the output image along with hera. --- After conversations with @flaviuvadan, updated the implementation to allow for the following - - Added a new ScriptConstructor class that allows users to define their own ScriptConstructor that can inspect the callable source and generate the output source. Provided two default implementations - inline and runner. The default value is `inline` right now, but there are placeholders in the code to allow for the default value to be detected automatically. Additionally, users can provide their own ScriptConstructor if they wish to and set it as a default. - Added the ability to set class defaults for any hera class using global_config. This allows users to introduce default values of hera classes without needing us to implement it for every single use case. This also removes any circular dependencies between hera global config and any hera specific classes. --------- Signed-off-by: Sambhav Kothari <[email protected]>
1 parent 6a65289 commit 593c0bf

36 files changed

+1008
-206
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Callable Coinflip
2+
3+
4+
5+
6+
7+
8+
=== "Hera"
9+
10+
```python linenums="1"
11+
import random
12+
13+
from hera.shared import global_config
14+
from hera.workflows import DAG, Script, Workflow, script
15+
16+
# Note, setting constructor to runner is only possible if the source code is available
17+
# along with dependencies include hera in the image.
18+
# Callable is a robust mode that allows you to run any python function
19+
# and is compatible with pydantic. It automatically parses the input
20+
# and serializes the output.
21+
global_config.image = "my-image-with-python-source-code-and-dependencies"
22+
global_config.set_class_defaults(Script, constructor="runner")
23+
# Runner script constructor is still and experimental feature and we need to explicitly opt in to it
24+
# Note that experimental features are subject to breaking changes in future releases of the same major version
25+
global_config.experimental_features["script_runner"] = True
26+
27+
28+
@script()
29+
def flip():
30+
return "heads" if random.randint(0, 1) == 0 else "tails"
31+
32+
33+
@script()
34+
def heads():
35+
return "it was heads"
36+
37+
38+
@script()
39+
def tails():
40+
return "it was tails"
41+
42+
43+
with Workflow(generate_name="coinflip-", entrypoint="d") as w:
44+
with DAG(name="d") as s:
45+
f = flip()
46+
heads().on_other_result(f, "heads")
47+
tails().on_other_result(f, "tails")
48+
```
49+
50+
=== "YAML"
51+
52+
```yaml linenums="1"
53+
apiVersion: argoproj.io/v1alpha1
54+
kind: Workflow
55+
metadata:
56+
generateName: coinflip-
57+
spec:
58+
entrypoint: d
59+
templates:
60+
- dag:
61+
tasks:
62+
- name: flip
63+
template: flip
64+
- depends: flip
65+
name: heads
66+
template: heads
67+
when: '{{tasks.flip.outputs.result}} == heads'
68+
- depends: flip
69+
name: tails
70+
template: tails
71+
when: '{{tasks.flip.outputs.result}} == tails'
72+
name: d
73+
- name: flip
74+
script:
75+
args:
76+
- -m
77+
- hera.workflows.runner
78+
- -e
79+
- examples.workflows.callable_coinflip:flip
80+
command:
81+
- python
82+
image: my-image-with-python-source-code-and-dependencies
83+
source: '{{inputs.parameters}}'
84+
- name: heads
85+
script:
86+
args:
87+
- -m
88+
- hera.workflows.runner
89+
- -e
90+
- examples.workflows.callable_coinflip:heads
91+
command:
92+
- python
93+
image: my-image-with-python-source-code-and-dependencies
94+
source: '{{inputs.parameters}}'
95+
- name: tails
96+
script:
97+
args:
98+
- -m
99+
- hera.workflows.runner
100+
- -e
101+
- examples.workflows.callable_coinflip:tails
102+
command:
103+
- python
104+
image: my-image-with-python-source-code-and-dependencies
105+
source: '{{inputs.parameters}}'
106+
```
107+
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# Callable Script
2+
3+
4+
5+
6+
7+
8+
=== "Hera"
9+
10+
```python linenums="1"
11+
from typing import List
12+
13+
from pydantic import BaseModel
14+
15+
from hera.shared import global_config
16+
from hera.workflows import Script, Steps, Workflow, script
17+
18+
# Note, setting constructor to runner is only possible if the source code is available
19+
# along with dependencies include hera in the image.
20+
# Callable is a robust mode that allows you to run any python function
21+
# and is compatible with pydantic. It automatically parses the input
22+
# and serializes the output.
23+
global_config.image = "my-image-with-python-source-code-and-dependencies"
24+
global_config.set_class_defaults(Script, constructor="runner")
25+
# Runner script constructor is still and experimental feature and we need to explicitly opt in to it
26+
# Note that experimental features are subject to breaking changes in future releases of the same major version
27+
global_config.experimental_features["script_runner"] = True
28+
29+
30+
# An optional pydantic input type
31+
# hera can automatically de-serialize argo
32+
# arguments into types denoted by your function's signature
33+
# as long as they are de-serializable by pydantic
34+
# This provides auto-magic input parsing with validation
35+
# provided by pydantic.
36+
class Input(BaseModel):
37+
a: int
38+
b: str = "foo"
39+
40+
41+
# An optional pydantic output type
42+
# hera can automatically serialize the output
43+
# of your function into a json string
44+
# as long as they are serializable by pydantic or json serializable
45+
# This provides auto-magic output serialization with validation
46+
# provided by pydantic.
47+
class Output(BaseModel):
48+
output: List[Input]
49+
50+
51+
@script()
52+
def my_function(input: Input) -> Output:
53+
return Output(output=[input])
54+
55+
56+
# Note that the input type is a list of Input
57+
# hera can also automatically de-serialize
58+
# composite types like lists and dicts
59+
@script()
60+
def another_function(inputs: List[Input]) -> Output:
61+
return Output(output=inputs)
62+
63+
64+
# it also works with raw json strings
65+
# but those must be explicitly marked as
66+
# a string type
67+
@script()
68+
def str_function(input: str) -> Output:
69+
# Example function to ensure we are not json parsing
70+
# string types before passing it to the function
71+
return Output(output=[Input.parse_raw(input)])
72+
73+
74+
with Workflow(name="my-workflow") as w:
75+
with Steps(name="my-steps") as s:
76+
my_function(arguments={"input": Input(a=2, b="bar")})
77+
str_function(arguments={"input": Input(a=2, b="bar").json()})
78+
another_function(arguments={"inputs": [Input(a=2, b="bar"), Input(a=2, b="bar")]})
79+
```
80+
81+
=== "YAML"
82+
83+
```yaml linenums="1"
84+
apiVersion: argoproj.io/v1alpha1
85+
kind: Workflow
86+
metadata:
87+
name: my-workflow
88+
spec:
89+
templates:
90+
- name: my-steps
91+
steps:
92+
- - arguments:
93+
parameters:
94+
- name: input
95+
value: '{"a": 2, "b": "bar"}'
96+
name: my-function
97+
template: my-function
98+
- - arguments:
99+
parameters:
100+
- name: input
101+
value: '{"a": 2, "b": "bar"}'
102+
name: str-function
103+
template: str-function
104+
- - arguments:
105+
parameters:
106+
- name: inputs
107+
value: '[{"a": 2, "b": "bar"}, {"a": 2, "b": "bar"}]'
108+
name: another-function
109+
template: another-function
110+
- inputs:
111+
parameters:
112+
- name: input
113+
name: my-function
114+
script:
115+
args:
116+
- -m
117+
- hera.workflows.runner
118+
- -e
119+
- examples.workflows.callable_script:my_function
120+
command:
121+
- python
122+
image: my-image-with-python-source-code-and-dependencies
123+
source: '{{inputs.parameters}}'
124+
- inputs:
125+
parameters:
126+
- name: input
127+
name: str-function
128+
script:
129+
args:
130+
- -m
131+
- hera.workflows.runner
132+
- -e
133+
- examples.workflows.callable_script:str_function
134+
command:
135+
- python
136+
image: my-image-with-python-source-code-and-dependencies
137+
source: '{{inputs.parameters}}'
138+
- inputs:
139+
parameters:
140+
- name: inputs
141+
name: another-function
142+
script:
143+
args:
144+
- -m
145+
- hera.workflows.runner
146+
- -e
147+
- examples.workflows.callable_script:another_function
148+
command:
149+
- python
150+
image: my-image-with-python-source-code-and-dependencies
151+
source: '{{inputs.parameters}}'
152+
```
153+

docs/examples/workflows/coinflip.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
script:
6464
command:
6565
- python
66-
image: python:3.7
66+
image: python:3.8
6767
source: 'import os
6868

6969
import sys
@@ -82,7 +82,7 @@
8282
script:
8383
command:
8484
- python
85-
image: python:3.7
85+
image: python:3.8
8686
source: 'import os
8787

8888
import sys
@@ -96,7 +96,7 @@
9696
script:
9797
command:
9898
- python
99-
image: python:3.7
99+
image: python:3.8
100100
source: 'import os
101101

102102
import sys

docs/examples/workflows/complex_deps.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
script:
7676
command:
7777
- python
78-
image: python:3.7
78+
image: python:3.8
7979
source: "import os\nimport sys\nsys.path.append(os.getcwd())\nimport json\n\
8080
try: p = json.loads(r'''{{inputs.parameters.p}}''')\nexcept: p = r'''{{inputs.parameters.p}}'''\n\
8181
\nif p < 0.5:\n raise Exception(p)\nprint(42)\n"

docs/examples/workflows/dag_with_script_param_passing.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
script:
5555
command:
5656
- python
57-
image: python:3.7
57+
image: python:3.8
5858
source: 'import os
5959

6060
import sys
@@ -71,7 +71,7 @@
7171
script:
7272
command:
7373
- python
74-
image: python:3.7
74+
image: python:3.8
7575
source: 'import os
7676

7777
import sys

docs/examples/workflows/dynamic_volumes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
script:
4343
command:
4444
- python
45-
image: python:3.7
45+
image: python:3.8
4646
source: 'import os
4747

4848
import sys

docs/examples/workflows/global_config.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
global_config.service_account_name = "argo-account"
1717
global_config.image = "image-say"
1818
global_config.script_command = ["python3"]
19+
global_config.set_class_defaults(Container, active_deadline_seconds=100, command=["cowsay"])
1920

2021

2122
@script()
@@ -24,7 +25,7 @@
2425

2526

2627
with Workflow(generate_name="global-config-", entrypoint="whalesay") as w:
27-
whalesay = Container(image="docker/whalesay:latest", command=["cowsay"])
28+
whalesay = Container(image="docker/whalesay:latest")
2829
say()
2930
```
3031

@@ -40,7 +41,8 @@
4041
entrypoint: whalesay
4142
serviceAccountName: argo-account
4243
templates:
43-
- container:
44+
- activeDeadlineSeconds: '100'
45+
container:
4446
command:
4547
- cowsay
4648
image: docker/whalesay:latest

docs/examples/workflows/multi_env.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
value: '2'
5353
- name: c
5454
value: '3'
55-
image: python:3.7
55+
image: python:3.8
5656
source: 'import os
5757

5858
import sys

docs/examples/workflows/script_auto_infer.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
script:
6767
command:
6868
- python
69-
image: python:3.7
69+
image: python:3.8
7070
source: "import os\nimport sys\nsys.path.append(os.getcwd())\nimport pickle\n\
7171
\nresult = \"foo testing\"\nwith open(\"/tmp/result\", \"wb\") as f:\n \
7272
\ pickle.dump(result, f)\n"
@@ -78,7 +78,7 @@
7878
script:
7979
command:
8080
- python
81-
image: python:3.7
81+
image: python:3.8
8282
source: "import os\nimport sys\nsys.path.append(os.getcwd())\nimport json\n\n\
8383
import pickle\n\nwith open(\"/tmp/i\", \"rb\") as f:\n i = pickle.load(f)\n\
8484
print(i)\n"

docs/examples/workflows/script_with_default_params.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
script:
8181
command:
8282
- python
83-
image: python:3.7
83+
image: python:3.8
8484
source: 'import os
8585

8686
import sys

0 commit comments

Comments
 (0)