diff --git a/docs/examples/workflows/coinflip.md b/docs/examples/workflows/upstream/coinflip.md similarity index 81% rename from docs/examples/workflows/coinflip.md rename to docs/examples/workflows/upstream/coinflip.md index 5f8362b74..1abe8d60e 100644 --- a/docs/examples/workflows/coinflip.md +++ b/docs/examples/workflows/upstream/coinflip.md @@ -1,6 +1,6 @@ # Coinflip - +> Note: This example is a replication of an Argo Workflow example in Hera. The upstream example can be [found here](https://github.com/argoproj/argo-workflows/blob/master/examples/coinflip.yaml). @@ -24,18 +24,19 @@ with Workflow( "This is an example of coin flip defined as a sequence of conditional steps." ), }, + entrypoint="coinflip", ) as w: heads = Container( name="heads", image="alpine:3.6", command=["sh", "-c"], - args=["echo 'it was heads'"], + args=['echo "it was heads"'], ) tails = Container( name="tails", image="alpine:3.6", command=["sh", "-c"], - args=["echo 'it was tails'"], + args=['echo "it was tails"'], ) flip_coin = Script( @@ -43,6 +44,7 @@ with Workflow( image="python:alpine3.6", command=["python"], source=flip_coin_func, + add_cwd_to_sys_path=False, ) with Steps(name="coinflip") as s: @@ -64,10 +66,11 @@ metadata: a sequence of conditional steps. generateName: coinflip- spec: + entrypoint: coinflip templates: - container: args: - - echo 'it was heads' + - echo "it was heads" command: - sh - -c @@ -75,7 +78,7 @@ spec: name: heads - container: args: - - echo 'it was tails' + - echo "it was tails" command: - sh - -c @@ -86,13 +89,7 @@ spec: command: - python image: python:alpine3.6 - source: 'import os - - import sys - - sys.path.append(os.getcwd()) - - import random + source: 'import random result = "heads" if random.randint(0, 1) == 0 else "tails" diff --git a/docs/examples/workflows/upstream/cron_workflow.md b/docs/examples/workflows/upstream/cron_workflow.md index a86de3766..5ac893e0d 100644 --- a/docs/examples/workflows/upstream/cron_workflow.md +++ b/docs/examples/workflows/upstream/cron_workflow.md @@ -12,10 +12,6 @@ from hera.workflows import Container, CronWorkflow with CronWorkflow( name="hello-world", entrypoint="whalesay", - annotations={ - "workflows.argoproj.io/description": ("This example demonstrates running a DAG with inline templates."), - "workflows.argoproj.io/version": ">= 3.2.0", - }, schedule="* * * * *", timezone="America/Los_Angeles", starting_deadline_seconds=0, @@ -38,10 +34,6 @@ with CronWorkflow( apiVersion: argoproj.io/v1alpha1 kind: CronWorkflow metadata: - annotations: - workflows.argoproj.io/description: This example demonstrates running a DAG with - inline templates. - workflows.argoproj.io/version: '>= 3.2.0' name: hello-world spec: concurrencyPolicy: Replace diff --git a/docs/examples/workflows/upstream/steps_inline_workflow.md b/docs/examples/workflows/upstream/steps_inline_workflow.md new file mode 100644 index 000000000..4dcb061b5 --- /dev/null +++ b/docs/examples/workflows/upstream/steps_inline_workflow.md @@ -0,0 +1,46 @@ +# Steps Inline Workflow + +> Note: This example is a replication of an Argo Workflow example in Hera. The upstream example can be [found here](https://github.com/argoproj/argo-workflows/blob/master/examples/steps-inline-workflow.yaml). + + + +## Hera + +```python +from hera.workflows import Container, Step, Steps, Workflow + +container = Container(image="argoproj/argosay:v2") + +with Workflow( + generate_name="steps-inline-", + entrypoint="main", + annotations={ + "workflows.argoproj.io/description": ("This workflow demonstrates running a steps with inline templates."), + "workflows.argoproj.io/version": ">= 3.2.0", + }, +) as w: + with Steps(name="main"): + Step(name="a", inline=container) +``` + +## YAML + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + annotations: + workflows.argoproj.io/description: This workflow demonstrates running a steps + with inline templates. + workflows.argoproj.io/version: '>= 3.2.0' + generateName: steps-inline- +spec: + entrypoint: main + templates: + - name: main + steps: + - - inline: + container: + image: argoproj/argosay:v2 + name: a +``` diff --git a/docs/examples/workflows/upstream/workflow_template__dag.md b/docs/examples/workflows/upstream/workflow_template__dag.md new file mode 100644 index 000000000..a05fb6f1a --- /dev/null +++ b/docs/examples/workflows/upstream/workflow_template__dag.md @@ -0,0 +1,72 @@ +# Workflow Template Dag + +> Note: This example is a replication of an Argo Workflow example in Hera. The upstream example can be [found here](https://github.com/argoproj/argo-workflows/blob/master/examples/workflow-template/dag.yaml). + + + +## Hera + +```python +from hera.workflows import DAG, Task, Workflow +from hera.workflows.models import TemplateRef + +with Workflow( + generate_name="workflow-template-dag-diamond-", + entrypoint="diamond", +) as w: + whalesay_template_ref = TemplateRef(name="workflow-template-whalesay-template", template="whalesay-template") + inner_template_ref = TemplateRef(name="workflow-template-inner-dag", template="inner-diamond") + with DAG(name="diamond"): + A = Task(name="A", template_ref=whalesay_template_ref, arguments={"message": "A"}) + B = Task(name="B", template_ref=whalesay_template_ref, arguments={"message": "B"}) + C = Task(name="C", template_ref=inner_template_ref) + D = Task(name="D", template_ref=whalesay_template_ref, arguments={"message": "D"}) + + A >> [B, C] >> D +``` + +## YAML + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: workflow-template-dag-diamond- +spec: + entrypoint: diamond + templates: + - dag: + tasks: + - arguments: + parameters: + - name: message + value: A + name: A + templateRef: + name: workflow-template-whalesay-template + template: whalesay-template + - arguments: + parameters: + - name: message + value: B + depends: A + name: B + templateRef: + name: workflow-template-whalesay-template + template: whalesay-template + - depends: A + name: C + templateRef: + name: workflow-template-inner-dag + template: inner-diamond + - arguments: + parameters: + - name: message + value: D + depends: B && C + name: D + templateRef: + name: workflow-template-whalesay-template + template: whalesay-template + name: diamond +``` diff --git a/docs/examples/workflows/upstream/workflow_template__hello_world.md b/docs/examples/workflows/upstream/workflow_template__hello_world.md new file mode 100644 index 000000000..40d8e7c69 --- /dev/null +++ b/docs/examples/workflows/upstream/workflow_template__hello_world.md @@ -0,0 +1,49 @@ +# Workflow Template Hello World + +> Note: This example is a replication of an Argo Workflow example in Hera. The upstream example can be [found here](https://github.com/argoproj/argo-workflows/blob/master/examples/workflow-template/hello-world.yaml). + + + +## Hera + +```python +from hera.workflows import Step, Steps, Workflow +from hera.workflows.models import TemplateRef + +with Workflow( + generate_name="workflow-template-hello-world-", + entrypoint="whalesay", +) as w: + whalesay_template_ref = TemplateRef( + name="workflow-template-whalesay-template", + template="whalesay-template", + ) + with Steps(name="whalesay"): + Step( + name="call-whalesay-template", + template_ref=whalesay_template_ref, + arguments={"message": "hello world"}, + ) +``` + +## YAML + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: workflow-template-hello-world- +spec: + entrypoint: whalesay + templates: + - name: whalesay + steps: + - - arguments: + parameters: + - name: message + value: hello world + name: call-whalesay-template + templateRef: + name: workflow-template-whalesay-template + template: whalesay-template +``` diff --git a/docs/examples/workflows/upstream/workflow_template__workflow_template_ref.md b/docs/examples/workflows/upstream/workflow_template__workflow_template_ref.md new file mode 100644 index 000000000..6ddcb7b42 --- /dev/null +++ b/docs/examples/workflows/upstream/workflow_template__workflow_template_ref.md @@ -0,0 +1,32 @@ +# Workflow Template Workflow Template Ref + +> Note: This example is a replication of an Argo Workflow example in Hera. The upstream example can be [found here](https://github.com/argoproj/argo-workflows/blob/master/examples/workflow-template/workflow-template-ref.yaml). + + + +## Hera + +```python +from hera.workflows import Workflow +from hera.workflows.models import WorkflowTemplateRef + +wt_ref = WorkflowTemplateRef(name="workflow-template-submittable") + +with Workflow( + generate_name="workflow-template-hello-world-", + workflow_template_ref=wt_ref, +) as w: + pass +``` + +## YAML + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: workflow-template-hello-world- +spec: + workflowTemplateRef: + name: workflow-template-submittable +``` diff --git a/examples/workflows/coinflip.py b/examples/workflows/upstream/coinflip.py similarity index 87% rename from examples/workflows/coinflip.py rename to examples/workflows/upstream/coinflip.py index 2be457918..90fea0a50 100644 --- a/examples/workflows/coinflip.py +++ b/examples/workflows/upstream/coinflip.py @@ -15,18 +15,19 @@ def flip_coin_func() -> None: "This is an example of coin flip defined as a sequence of conditional steps." ), }, + entrypoint="coinflip", ) as w: heads = Container( name="heads", image="alpine:3.6", command=["sh", "-c"], - args=["echo 'it was heads'"], + args=['echo "it was heads"'], ) tails = Container( name="tails", image="alpine:3.6", command=["sh", "-c"], - args=["echo 'it was tails'"], + args=['echo "it was tails"'], ) flip_coin = Script( @@ -34,6 +35,7 @@ def flip_coin_func() -> None: image="python:alpine3.6", command=["python"], source=flip_coin_func, + add_cwd_to_sys_path=False, ) with Steps(name="coinflip") as s: diff --git a/examples/workflows/upstream/coinflip.upstream.yaml b/examples/workflows/upstream/coinflip.upstream.yaml new file mode 100644 index 000000000..b97e6a011 --- /dev/null +++ b/examples/workflows/upstream/coinflip.upstream.yaml @@ -0,0 +1,47 @@ +# The coinflip example combines the use of a script result, +# along with conditionals, to take a dynamic path in the +# workflow. In this example, depending on the result of the +# first step, 'flip-coin', the template will either run the +# 'heads' step or the 'tails' step. +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: coinflip- + annotations: + workflows.argoproj.io/description: | + This is an example of coin flip defined as a sequence of conditional steps. + You can also run it in Python: https://couler-proj.github.io/couler/examples/#coin-flip +spec: + entrypoint: coinflip + templates: + - name: coinflip + steps: + - - name: flip-coin + template: flip-coin + - - name: heads + template: heads + when: "{{steps.flip-coin.outputs.result}} == heads" + - name: tails + template: tails + when: "{{steps.flip-coin.outputs.result}} == tails" + + - name: flip-coin + script: + image: python:alpine3.6 + command: [python] + source: | + import random + result = "heads" if random.randint(0,1) == 0 else "tails" + print(result) + + - name: heads + container: + image: alpine:3.6 + command: [sh, -c] + args: ["echo \"it was heads\""] + + - name: tails + container: + image: alpine:3.6 + command: [sh, -c] + args: ["echo \"it was tails\""] diff --git a/examples/workflows/coinflip.yaml b/examples/workflows/upstream/coinflip.yaml similarity index 85% rename from examples/workflows/coinflip.yaml rename to examples/workflows/upstream/coinflip.yaml index 2bc092352..4ff03f527 100644 --- a/examples/workflows/coinflip.yaml +++ b/examples/workflows/upstream/coinflip.yaml @@ -6,10 +6,11 @@ metadata: a sequence of conditional steps. generateName: coinflip- spec: + entrypoint: coinflip templates: - container: args: - - echo 'it was heads' + - echo "it was heads" command: - sh - -c @@ -17,7 +18,7 @@ spec: name: heads - container: args: - - echo 'it was tails' + - echo "it was tails" command: - sh - -c @@ -28,13 +29,7 @@ spec: command: - python image: python:alpine3.6 - source: 'import os - - import sys - - sys.path.append(os.getcwd()) - - import random + source: 'import random result = "heads" if random.randint(0, 1) == 0 else "tails" diff --git a/examples/workflows/upstream/cron-workflow.yaml b/examples/workflows/upstream/cron-workflow.yaml index fd1098529..fdca977ba 100644 --- a/examples/workflows/upstream/cron-workflow.yaml +++ b/examples/workflows/upstream/cron-workflow.yaml @@ -1,10 +1,6 @@ apiVersion: argoproj.io/v1alpha1 kind: CronWorkflow metadata: - annotations: - workflows.argoproj.io/description: This example demonstrates running a DAG with - inline templates. - workflows.argoproj.io/version: '>= 3.2.0' name: hello-world spec: concurrencyPolicy: Replace diff --git a/examples/workflows/upstream/cron_workflow.py b/examples/workflows/upstream/cron_workflow.py index 6548e3665..38a155809 100644 --- a/examples/workflows/upstream/cron_workflow.py +++ b/examples/workflows/upstream/cron_workflow.py @@ -3,10 +3,6 @@ with CronWorkflow( name="hello-world", entrypoint="whalesay", - annotations={ - "workflows.argoproj.io/description": ("This example demonstrates running a DAG with inline templates."), - "workflows.argoproj.io/version": ">= 3.2.0", - }, schedule="* * * * *", timezone="America/Los_Angeles", starting_deadline_seconds=0, diff --git a/examples/workflows/upstream/steps-inline-workflow.upstream.yaml b/examples/workflows/upstream/steps-inline-workflow.upstream.yaml new file mode 100644 index 000000000..647f2d53b --- /dev/null +++ b/examples/workflows/upstream/steps-inline-workflow.upstream.yaml @@ -0,0 +1,19 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: steps-inline- + labels: + workflows.argoproj.io/test: "true" + annotations: + workflows.argoproj.io/description: | + This workflow demonstrates running a steps with inline templates. + workflows.argoproj.io/version: ">= 3.2.0" +spec: + entrypoint: main + templates: + - name: main + steps: + - - name: a + inline: + container: + image: argoproj/argosay:v2 \ No newline at end of file diff --git a/examples/workflows/upstream/steps-inline-workflow.yaml b/examples/workflows/upstream/steps-inline-workflow.yaml new file mode 100644 index 000000000..d864201e2 --- /dev/null +++ b/examples/workflows/upstream/steps-inline-workflow.yaml @@ -0,0 +1,17 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + annotations: + workflows.argoproj.io/description: This workflow demonstrates running a steps + with inline templates. + workflows.argoproj.io/version: '>= 3.2.0' + generateName: steps-inline- +spec: + entrypoint: main + templates: + - name: main + steps: + - - inline: + container: + image: argoproj/argosay:v2 + name: a diff --git a/examples/workflows/upstream/steps_inline_workflow.py b/examples/workflows/upstream/steps_inline_workflow.py new file mode 100644 index 000000000..61ff150a6 --- /dev/null +++ b/examples/workflows/upstream/steps_inline_workflow.py @@ -0,0 +1,14 @@ +from hera.workflows import Container, Step, Steps, Workflow + +container = Container(image="argoproj/argosay:v2") + +with Workflow( + generate_name="steps-inline-", + entrypoint="main", + annotations={ + "workflows.argoproj.io/description": ("This workflow demonstrates running a steps with inline templates."), + "workflows.argoproj.io/version": ">= 3.2.0", + }, +) as w: + with Steps(name="main"): + Step(name="a", inline=container) diff --git a/examples/workflows/upstream/workflow-template--dag.upstream.yaml b/examples/workflows/upstream/workflow-template--dag.upstream.yaml new file mode 100644 index 000000000..d0ba34c88 --- /dev/null +++ b/examples/workflows/upstream/workflow-template--dag.upstream.yaml @@ -0,0 +1,48 @@ +# The following workflow executes a diamond workflow +# +# A +# / \ +# B C +# \ / +# D +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: workflow-template-dag-diamond- +spec: + entrypoint: diamond + templates: + - name: diamond + dag: + tasks: + - name: A + templateRef: + name: workflow-template-whalesay-template + template: whalesay-template + arguments: + parameters: + - name: message + value: A + - name: B + depends: "A" + templateRef: + name: workflow-template-whalesay-template + template: whalesay-template + arguments: + parameters: + - name: message + value: B + - name: C + depends: "A" + templateRef: + name: workflow-template-inner-dag + template: inner-diamond + - name: D + depends: "B && C" + templateRef: + name: workflow-template-whalesay-template + template: whalesay-template + arguments: + parameters: + - name: message + value: D diff --git a/examples/workflows/upstream/workflow-template--dag.yaml b/examples/workflows/upstream/workflow-template--dag.yaml new file mode 100644 index 000000000..9502d4255 --- /dev/null +++ b/examples/workflows/upstream/workflow-template--dag.yaml @@ -0,0 +1,41 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: workflow-template-dag-diamond- +spec: + entrypoint: diamond + templates: + - dag: + tasks: + - arguments: + parameters: + - name: message + value: A + name: A + templateRef: + name: workflow-template-whalesay-template + template: whalesay-template + - arguments: + parameters: + - name: message + value: B + depends: A + name: B + templateRef: + name: workflow-template-whalesay-template + template: whalesay-template + - depends: A + name: C + templateRef: + name: workflow-template-inner-dag + template: inner-diamond + - arguments: + parameters: + - name: message + value: D + depends: B && C + name: D + templateRef: + name: workflow-template-whalesay-template + template: whalesay-template + name: diamond diff --git a/examples/workflows/upstream/workflow-template--hello-world.upstream.yaml b/examples/workflows/upstream/workflow-template--hello-world.upstream.yaml new file mode 100644 index 000000000..4d68103b3 --- /dev/null +++ b/examples/workflows/upstream/workflow-template--hello-world.upstream.yaml @@ -0,0 +1,17 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: workflow-template-hello-world- +spec: + entrypoint: whalesay + templates: + - name: whalesay + steps: + - - name: call-whalesay-template + templateRef: + name: workflow-template-whalesay-template + template: whalesay-template + arguments: + parameters: + - name: message + value: "hello world" diff --git a/examples/workflows/upstream/workflow-template--hello-world.yaml b/examples/workflows/upstream/workflow-template--hello-world.yaml new file mode 100644 index 000000000..8a7cde5ff --- /dev/null +++ b/examples/workflows/upstream/workflow-template--hello-world.yaml @@ -0,0 +1,17 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: workflow-template-hello-world- +spec: + entrypoint: whalesay + templates: + - name: whalesay + steps: + - - arguments: + parameters: + - name: message + value: hello world + name: call-whalesay-template + templateRef: + name: workflow-template-whalesay-template + template: whalesay-template diff --git a/examples/workflows/upstream/workflow-template--workflow-template-ref.upstream.yaml b/examples/workflows/upstream/workflow-template--workflow-template-ref.upstream.yaml new file mode 100644 index 000000000..c33379dc1 --- /dev/null +++ b/examples/workflows/upstream/workflow-template--workflow-template-ref.upstream.yaml @@ -0,0 +1,8 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: workflow-template-hello-world- +spec: + workflowTemplateRef: + name: workflow-template-submittable + diff --git a/examples/workflows/upstream/workflow-template--workflow-template-ref.yaml b/examples/workflows/upstream/workflow-template--workflow-template-ref.yaml new file mode 100644 index 000000000..43853fe24 --- /dev/null +++ b/examples/workflows/upstream/workflow-template--workflow-template-ref.yaml @@ -0,0 +1,7 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: workflow-template-hello-world- +spec: + workflowTemplateRef: + name: workflow-template-submittable diff --git a/examples/workflows/upstream/workflow_template__dag.py b/examples/workflows/upstream/workflow_template__dag.py new file mode 100644 index 000000000..b1585bc04 --- /dev/null +++ b/examples/workflows/upstream/workflow_template__dag.py @@ -0,0 +1,16 @@ +from hera.workflows import DAG, Task, Workflow +from hera.workflows.models import TemplateRef + +with Workflow( + generate_name="workflow-template-dag-diamond-", + entrypoint="diamond", +) as w: + whalesay_template_ref = TemplateRef(name="workflow-template-whalesay-template", template="whalesay-template") + inner_template_ref = TemplateRef(name="workflow-template-inner-dag", template="inner-diamond") + with DAG(name="diamond"): + A = Task(name="A", template_ref=whalesay_template_ref, arguments={"message": "A"}) + B = Task(name="B", template_ref=whalesay_template_ref, arguments={"message": "B"}) + C = Task(name="C", template_ref=inner_template_ref) + D = Task(name="D", template_ref=whalesay_template_ref, arguments={"message": "D"}) + + A >> [B, C] >> D diff --git a/examples/workflows/upstream/workflow_template__hello_world.py b/examples/workflows/upstream/workflow_template__hello_world.py new file mode 100644 index 000000000..bc2e7721e --- /dev/null +++ b/examples/workflows/upstream/workflow_template__hello_world.py @@ -0,0 +1,17 @@ +from hera.workflows import Step, Steps, Workflow +from hera.workflows.models import TemplateRef + +with Workflow( + generate_name="workflow-template-hello-world-", + entrypoint="whalesay", +) as w: + whalesay_template_ref = TemplateRef( + name="workflow-template-whalesay-template", + template="whalesay-template", + ) + with Steps(name="whalesay"): + Step( + name="call-whalesay-template", + template_ref=whalesay_template_ref, + arguments={"message": "hello world"}, + ) diff --git a/examples/workflows/upstream/workflow_template__workflow_template_ref.py b/examples/workflows/upstream/workflow_template__workflow_template_ref.py new file mode 100644 index 000000000..a9fd57a4b --- /dev/null +++ b/examples/workflows/upstream/workflow_template__workflow_template_ref.py @@ -0,0 +1,10 @@ +from hera.workflows import Workflow +from hera.workflows.models import WorkflowTemplateRef + +wt_ref = WorkflowTemplateRef(name="workflow-template-submittable") + +with Workflow( + generate_name="workflow-template-hello-world-", + workflow_template_ref=wt_ref, +) as w: + pass diff --git a/src/hera/workflows/_mixins.py b/src/hera/workflows/_mixins.py index c6e50e939..fd9f61963 100644 --- a/src/hera/workflows/_mixins.py +++ b/src/hera/workflows/_mixins.py @@ -3,6 +3,8 @@ import inspect from typing import Any, Callable, Dict, List, Optional, TypeVar, Union, cast +from pydantic import root_validator + from hera.shared import global_config from hera.shared._base_model import BaseMixin from hera.shared.serialization import serialize @@ -18,6 +20,7 @@ Artifact as ModelArtifact, ArtifactLocation, ContainerPort, + ContinueOn, EnvFromSource, EnvVar, ExecutorConfig, @@ -26,6 +29,7 @@ Inputs as ModelInputs, IntOrString, Item, + LifecycleHook, Memoize, Metadata, Metrics, @@ -37,7 +41,10 @@ Probe, ResourceRequirements, RetryStrategy, + Sequence, Synchronization, + Template, + TemplateRef, TerminationMessagePolicy, Toleration, UserContainer as ModelUserContainer, @@ -486,6 +493,28 @@ def _build_inputs(self) -> Optional[ModelInputs]: return inputs +class TemplateInvocatorMixin(BaseMixin): + name: str + continue_on: Optional[ContinueOn] = None + hooks: Optional[Dict[str, LifecycleHook]] = None + on_exit: Optional[str] = None + template: Optional[Union[str, Template, TemplateMixin]] = None + template_ref: Optional[TemplateRef] = None + inline: Optional[Union[Template, TemplateMixin]] = None + when: Optional[str] = None + with_sequence: Optional[Sequence] = None + + @root_validator(pre=False) + def _check_values(cls, values): + def one(xs: List): + xs = list(map(bool, xs)) + return xs.count(True) == 1 + + if not one([values.get("template"), values.get("template_ref"), values.get("inline")]): + raise ValueError("exactly one of ['template', 'template_ref', 'inline'] must be present") + return values + + def _get_params_from_source(source: Callable) -> Optional[List[Parameter]]: source_signature: Dict[str, Optional[str]] = {} for p in inspect.signature(source).parameters.values(): diff --git a/src/hera/workflows/env.py b/src/hera/workflows/env.py index 66161ed0c..82608ed58 100644 --- a/src/hera/workflows/env.py +++ b/src/hera/workflows/env.py @@ -58,6 +58,8 @@ def _check_values(cls, values): if values.get("value") is not None and values.get("value_from_input") is not None: raise ValueError("cannot specify both `value` and `value_from_input`") + return values + @property def param_name(self) -> str: if not self.value_from_input: diff --git a/src/hera/workflows/steps.py b/src/hera/workflows/steps.py index 0bcb5be38..b742211bb 100644 --- a/src/hera/workflows/steps.py +++ b/src/hera/workflows/steps.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional, Union +from typing import Any, List, Optional, Union from hera.workflows._mixins import ( ArgumentsMixin, @@ -7,36 +7,24 @@ ItemMixin, ParameterMixin, SubNodeMixin, + TemplateInvocatorMixin, TemplateMixin, ) from hera.workflows.exceptions import InvalidType from hera.workflows.models import ( - ContinueOn as _ModelContinueOn, - LifecycleHook as _ModelLifecycleHook, - Sequence as _ModelSequence, Template as _ModelTemplate, - TemplateRef as _ModelTemplateRef, WorkflowStep as _ModelWorkflowStep, ) -from hera.workflows.protocol import Steppable +from hera.workflows.protocol import Steppable, Templatable class Step( + TemplateInvocatorMixin, ArgumentsMixin, SubNodeMixin, ParameterMixin, ItemMixin, ): - continue_on: Optional[_ModelContinueOn] - hooks: Optional[Dict[str, _ModelLifecycleHook]] - inline: Optional[_ModelTemplate] - name: Optional[str] - on_exit: Optional[str] - template: Union[str, _ModelTemplate, TemplateMixin] - template_ref: Optional[_ModelTemplateRef] - when: Optional[str] - with_sequence: Optional[_ModelSequence] - @property def id(self) -> str: return f"{{{{steps.{self.name}.id}}}}" @@ -66,14 +54,26 @@ def result(self) -> str: return f"{{{{steps.{self.name}.outputs.result}}}}" def _build_as_workflow_step(self) -> _ModelWorkflowStep: + _template = None + if isinstance(self.template, str): + _template = self.template + elif isinstance(self.template, (_ModelTemplate, TemplateMixin)): + _template = self.template.name + + _inline = None + if isinstance(self.inline, _ModelTemplate): + _inline = self.inline + elif isinstance(self.inline, Templatable): + _inline = self.inline._build_template() + return _ModelWorkflowStep( arguments=self._build_arguments(), continue_on=self.continue_on, hooks=self.hooks, - inline=self.inline, + inline=_inline, name=self.name, on_exit=self.on_exit, - template=self.template if isinstance(self.template, str) else self.template.name, + template=_template, template_ref=self.template_ref, when=self.when, with_items=self._build_with_items(), diff --git a/src/hera/workflows/task.py b/src/hera/workflows/task.py index 034e06649..d3bc6721e 100644 --- a/src/hera/workflows/task.py +++ b/src/hera/workflows/task.py @@ -1,16 +1,19 @@ from __future__ import annotations from enum import Enum -from typing import Dict, List, Optional, Union - -from hera.workflows._mixins import ArgumentsMixin, ItemMixin, ParameterMixin, SubNodeMixin, TemplateMixin +from typing import List, Optional, Union + +from hera.workflows._mixins import ( + ArgumentsMixin, + ItemMixin, + ParameterMixin, + SubNodeMixin, + TemplateInvocatorMixin, + TemplateMixin, +) from hera.workflows.models import ( - ContinueOn, DAGTask as _ModelDAGTask, - LifecycleHook, - Sequence, Template, - TemplateRef, ) from hera.workflows.operator import Operator from hera.workflows.protocol import Templatable @@ -28,18 +31,15 @@ class TaskResult(Enum): all_failed = "AllFailed" -class Task(ArgumentsMixin, SubNodeMixin, ParameterMixin, ItemMixin): - name: str - continue_on: Optional[ContinueOn] = None +class Task( + TemplateInvocatorMixin, + ArgumentsMixin, + SubNodeMixin, + ParameterMixin, + ItemMixin, +): dependencies: Optional[List[str]] = None depends: Optional[str] = None - hooks: Optional[Dict[str, LifecycleHook]] = None - on_exit: Optional[str] = None - template: Optional[Union[str, Template, TemplateMixin]] = None - template_ref: Optional[TemplateRef] = None - inline: Optional[Union[Template, TemplateMixin]] = None - when: Optional[str] = None - with_sequence: Optional[Sequence] = None def _get_dependency_tasks(self) -> List[str]: if self.depends is None: @@ -155,9 +155,6 @@ def when_all_failed(self, other: Task) -> Task: return self.next(other, on=TaskResult.all_failed) def _build_dag_task(self) -> _ModelDAGTask: - if self.template is None and self.inline is None: - raise ValueError("Exactly one of `template` or `inline` must be supplied") - _template = None if isinstance(self.template, str): _template = self.template diff --git a/tests/test_workflow.py b/tests/test_workflow.py index e426bf852..dc6c67942 100644 --- a/tests/test_workflow.py +++ b/tests/test_workflow.py @@ -34,12 +34,14 @@ def _generate_yaml(path: Path) -> bool: def _transform_workflow(obj): w = ModelWorkflow.parse_obj(obj) - w.spec.templates.sort(key=lambda t: t.name) w.metadata.annotations = {} w.metadata.labels = {} - for t in w.spec.templates: - if t.script: - t.script.source = ast.dump(ast.parse(t.script.source)) + + if w.spec.templates is not None: + w.spec.templates.sort(key=lambda t: t.name) + for t in w.spec.templates: + if t.script: + t.script.source = ast.dump(ast.parse(t.script.source)) return w.dict()