diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d83fcdde2..4439d3e59 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,7 +45,7 @@ respected. To solve this, add the following as a config in your `.vscode/launch. ```json { "name": "Debug Tests", - "type": "python", + "type": "debugpy", // "python" is now deprecated "request": "launch", "purpose": ["debug-test"], "console": "integratedTerminal", @@ -69,7 +69,7 @@ Please keep in mind the following guidelines and practices when contributing to 1. Add unit tests for any new code you write. 1. Add an example, or extend an existing example, with any new features you may add. Use `make examples` to ensure that the documentation and examples are in sync. -## Adding new Workflow tests +## Adding new Workflow YAML generation tests Hera has an automated-test harness that is coupled with our documentation. In order to add new tests, please follow these steps - @@ -88,7 +88,10 @@ In order to add a new workflow test to test Hera functionality, do the following ### Upstream Hera examples -Tests that correspond to any [upstream Argo Workflow examples](https://github.com/argoproj/argo-workflows/tree/main/examples) should live in `examples/workflows/upstream/*.py`. These tests exist to ensure that Hera has complete parity with Argo Workflows and also to catch any regressions that might happen. +Tests that correspond to any +[upstream Argo Workflow examples](https://github.com/argoproj/argo-workflows/tree/main/examples) should live in +`examples/workflows/upstream/*.py`. These tests exist to ensure that Hera has complete parity with Argo Workflows and +also to catch any regressions that might happen. In order to add a new workflow test to test Hera functionality, do the following - @@ -104,6 +107,52 @@ In order to add a new workflow test to test Hera functionality, do the following * If you would like to update the golden copy of the test files, you can run `make regenerate-test-data` * The golden copies must be checked in to ensure that regressions may be caught in the future +## Adding new Workflow on-cluster tests + +Hera's CICD spins up Argo Workflows on a local Kubernetes cluster, which runs tests decorated with +`@pytest.mark.on_cluster`. If you want to add more on-cluster tests, the easiest way is through a GitHub Codespace. You +can then run the same `make` commands that run in CICD: + +``` +make install-k3d +``` + +This will install the k3d CLI. + +``` +make run-argo +``` + +This will create a cluster using k3d called `test-cluster`, then create a namespace called `argo` on it, applying the +argo configuration, and patching the deployment to use `server` as the `auth-mode`, meaning the connection to submit the +workflow doesn't require an authentication mechanism. + +You can then run existing on-cluster tests to ensure everything is set up correctly. This command also ports-forward the +server's port. + +``` +make test-on-cluster +``` + +### Viewing the Argo UI from a Codespace + +> Before doing this, note that **anyone** will be able to connect using the Argo UI URL! + +Ensure Argo Workflows is running using the `make` command: + +``` +make run-argo +``` + +Forward the Server's port using kubectl: + +``` +kubectl -n argo port-forward deployment/argo-server 2746:2746 +``` + +Then, go to the `PORTS` panel in VSCode, and add the `2746` port. You should see a green circle to the left of the port. +Then right click on the `2746` row and set `Port Visibility` to `public`. You can then open the URL in your browser to view the Argo UI. + ## Code of Conduct Please be mindful of, and adhere to, the CNCF's diff --git a/Makefile b/Makefile index e66a8285b..717af1bb6 100644 --- a/Makefile +++ b/Makefile @@ -155,6 +155,7 @@ run-argo: ## Start the argo server kubectl get namespace argo || kubectl create namespace argo kubectl apply -n argo -f https://github.com/argoproj/argo-workflows/releases/download/v$(ARGO_WORKFLOWS_VERSION)/install.yaml kubectl patch deployment argo-server --namespace argo --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/args", "value": ["server", "--auth-mode=server"]}]' + kubectl create rolebinding default-admin --clusterrole=admin --serviceaccount=argo:default --namespace=argo kubectl rollout status -n argo deployment/argo-server --timeout=120s --watch=true .PHONY: stop-argo diff --git a/docs/examples/workflows/dags/dag_diamond_with_callable_decorators.md b/docs/examples/workflows/dags/dag_diamond_with_callable_decorators.md index 9652696c2..9207c5d8d 100644 --- a/docs/examples/workflows/dags/dag_diamond_with_callable_decorators.md +++ b/docs/examples/workflows/dags/dag_diamond_with_callable_decorators.md @@ -8,14 +8,10 @@ === "Hera" ```python linenums="1" - from hera.workflows import ( - DAG, - Workflow, - script, - ) + from hera.workflows import DAG, Workflow, script - @script(add_cwd_to_sys_path=False, image="python:alpine3.6") + @script(image="python:3.12") def echo(message): print(message) @@ -76,8 +72,11 @@ script: command: - python - image: python:alpine3.6 + image: python:3.12 source: |- + import os + import sys + sys.path.append(os.getcwd()) import json try: message = json.loads(r'''{{inputs.parameters.message}}''') except: message = r'''{{inputs.parameters.message}}''' diff --git a/docs/examples/workflows/upstream/dag_diamond.md b/docs/examples/workflows/upstream/dag_diamond.md index eb731acb0..167875310 100644 --- a/docs/examples/workflows/upstream/dag_diamond.md +++ b/docs/examples/workflows/upstream/dag_diamond.md @@ -13,10 +13,7 @@ The upstream example can be [found here](https://github.com/argoproj/argo-workfl ```python linenums="1" from hera.workflows import DAG, Container, Parameter, Workflow - with Workflow( - generate_name="dag-diamond-", - entrypoint="diamond", - ) as w: + with Workflow(generate_name="dag-diamond-", entrypoint="diamond") as w: echo = Container( name="echo", image="alpine:3.7", diff --git a/docs/examples/workflows/use-cases/testing_templates_and_workflows.md b/docs/examples/workflows/use-cases/testing_templates_and_workflows.md new file mode 100644 index 000000000..bf8434b27 --- /dev/null +++ b/docs/examples/workflows/use-cases/testing_templates_and_workflows.md @@ -0,0 +1,111 @@ +# Testing Templates And Workflows + + + + + + +=== "Hera" + + ```python linenums="1" + from hera.shared import global_config + from hera.workflows import DAG, RunnerScriptConstructor, Script, Workflow, WorkflowsService, script + + try: + from pydantic.v1 import BaseModel + except ImportError: + from pydantic import BaseModel + + global_config.set_class_defaults(Script, constructor=RunnerScriptConstructor()) + + + class Rectangle(BaseModel): + length: float + width: float + + def area(self) -> float: + return self.length * self.width + + + @script(constructor="runner", image="my-built-python-image") + def calculate_area_of_rectangle(rectangle: Rectangle) -> float: + return rectangle.area() + + + with Workflow( + generate_name="dag-", + entrypoint="dag", + namespace="argo", + workflows_service=WorkflowsService(host="https://localhost:2746"), + ) as w: + with DAG(name="dag"): + A = calculate_area_of_rectangle( + name="rectangle-1", arguments={"rectangle": Rectangle(length=1.2, width=3.4).json()} + ) + B = calculate_area_of_rectangle( + name="rectangle-2", arguments={"rectangle": Rectangle(length=4.3, width=2.1).json()} + ) + A >> B + + + def test_calculate_area_of_rectangle(): + r = Rectangle(length=2.0, width=3.0) + assert calculate_area_of_rectangle(r) == 6.0 + + + def test_create_workflow(): + model_workflow = w.create(wait=True) + assert model_workflow.status and model_workflow.status.phase == "Succeeded" + + echo_node = next( + filter( + lambda n: n.display_name == "echo", + model_workflow.status.nodes.values(), + ) + ) + assert echo_node.outputs.parameters[0].value == "my value" + ``` + +=== "YAML" + + ```yaml linenums="1" + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: dag- + namespace: argo + spec: + entrypoint: dag + templates: + - dag: + tasks: + - arguments: + parameters: + - name: rectangle + value: '{"length": 1.2, "width": 3.4}' + name: rectangle-1 + template: calculate-area-of-rectangle + - arguments: + parameters: + - name: rectangle + value: '{"length": 4.3, "width": 2.1}' + depends: rectangle-1 + name: rectangle-2 + template: calculate-area-of-rectangle + name: dag + - inputs: + parameters: + - name: rectangle + name: calculate-area-of-rectangle + script: + args: + - -m + - hera.workflows.runner + - -e + - examples.workflows.use_cases.testing_templates_and_workflows:calculate_area_of_rectangle + command: + - python + image: my-built-python-image + source: '{{inputs.parameters}}' + ``` + diff --git a/docs/walk-through/about-hera.md b/docs/walk-through/about-hera.md index 50a4383b4..9db54c41d 100644 --- a/docs/walk-through/about-hera.md +++ b/docs/walk-through/about-hera.md @@ -48,3 +48,5 @@ functionality in Hera. Read more in the [user guides](../user-guides/core-concep A natural extension of a Python DSL for Argo is tighter integration with Python scripts. This is where Hera improves the developer experience through its tailored classes and syntactic sugar to enable developers to easily orchestrate Python functions. Check out [Hello World](hello-world.md) to get started! + +If you want an introductory tour of Hera, check out the [Hera Tour](hera-tour.md)! diff --git a/docs/walk-through/hera-tour.md b/docs/walk-through/hera-tour.md new file mode 100644 index 000000000..0880c0392 --- /dev/null +++ b/docs/walk-through/hera-tour.md @@ -0,0 +1,331 @@ +# Hera Tour-torial + +## Building the DAG diamond from scratch + +> This tutorial is a written, in-depth version of the ArgoCon EU 2024 talk - "Orchestrating Python Functions Natively +> with Hera", link coming soon! + +Let's go through some features of Hera by building a Workflow that will initially implement the DAG diamond example. + +First, with your Python environment set up, we need to install `hera`: + +``` +pip install hera +``` + +Then, open a new file for our Workflow. Let's make a function that echoes the input message: + +```py +def echo(message): + print(message) +``` + +In Hera, we can declare a function as a "Script" template using the `@script` decorator: + +```py +from hera.workflows import script + +@script() +def echo(message): + print(message) +``` + +This makes `echo` an inline script function, which means when submitted to Argo Workflows as part of a Workflow, you +will see the function body dumped into the `source` of the Script template, which is written in YAML (a superset of JSON): + +```yaml + - name: echo + inputs: + parameters: + - name: message + script: + command: + - python + image: python:3.8 + source: |- + import json + try: message = json.loads(r'''{{inputs.parameters.message}}''') + except: message = r'''{{inputs.parameters.message}}''' + + print(message) +``` + +But wait, where did `image: python:3.8` come from? It's a default value for the `image` of a script-decorated function +in Hera. Let's update that to a more recent version of Python in the decorator: + +```py +from hera.workflows import script + +@script(image="python:3.12") +def echo(message): + print(message) +``` + +You'll also notice the use of the `json` built-in module to load the parameter in the `source`. This is so Python can +try to load the string value from the YAML to a valid JSON type (otherwise using the raw string). This is possible +because YAML is a superset of JSON, so for a single value, it should be valid JSON. + + +Next, we need a Workflow to hold the `DAG` that we want to create. We do this with the `Workflow` class from +`hera.workflows`. We'll use `generate_name` to tell Argo to create a unique suffix appended to the given string each +time we submit the Workflow. + +```py +from hera.workflows import Workflow, script + +@script(image="python:3.12") +def echo(message): + print(message) + +with Workflow(generate_name="dag-diamond-") as w: + ... +``` + +We've created the `Workflow` as a context manager, which means any templates we reference or create within the context +will be added automatically to the Workflow. + +In Argo Workflows, a DAG is itself a template that is made up of Tasks that run other templates. In Hera, we create a +DAG using the `DAG` class from `hera.workflows`, using it as a context manager. We can also specify the DAG as the +`entrypoint` of the workflow: + +```py +from hera.workflows import DAG, Workflow, script + +@script(image="python:3.12") +def echo(message): + print(message) + +with Workflow(generate_name="dag-diamond-", entrypoint="diamond") as w: + with DAG(name="diamond"): + ... +``` + +The DAG context manager will add any `Task` created within its context to the graph. The `Task` class, imported from +`hera.workflows` requires a `name`, and one of a `template` or a `template_ref` (more on that later). Let's create some +tasks! We can pass arguments to the echo template used by each `Task` in a dictionary to the `arguments` kwarg of the +`Task`. + +```py +from hera.workflows import DAG, Task, Workflow, script + +@script(image="python:3.12") +def echo(message): + print(message) + +with Workflow(generate_name="dag-diamond-", entrypoint="diamond") as w: + with DAG(name="diamond"): + A = Task(name="A", template=echo, arguments={"message": "A"}) + B = Task(name="B", template=echo, arguments={"message": "B"}) + C = Task(name="C", template=echo, arguments={"message": "C"}) + D = Task(name="D", template=echo, arguments={"message": "D"}) +``` + +We now have 4 `Tasks`, but we have not declared any dependencies between the tasks, which means they will all run in +parallel! Remember, we are trying to create a diamond. To declare dependencies, we use the rshift syntax, which also +allows you to use Python lists of Tasks in places. Let's create the diamond! + +```py +from hera.workflows import DAG, Task, Workflow, script + +@script(image="python:3.12") +def echo(message): + print(message) + +with Workflow(generate_name="dag-diamond-", entrypoint="diamond") as w: + with DAG(name="diamond"): + A = Task(name="A", template=echo, arguments={"message": "A"}) + B = Task(name="B", template=echo, arguments={"message": "B"}) + C = Task(name="C", template=echo, arguments={"message": "C"}) + D = Task(name="D", template=echo, arguments={"message": "D"}) + A >> [B, C] >> D +``` + +This means A will run first as it has no dependencies, then B and C will run in parallel once A completes, and finally +once both B and C complete, D will run. + +Now, we want to actually submit this workflow! You'll need to use the right authorization for your cluster, but for now, +assuming a `localhost`, we can submit the workflow by passing a `WorkflowsService` to the `Workflow` and calling +`w.submit()`. + +```py +from hera.workflows import DAG, Task, Workflow, WorkflowsService, script + +@script(image="python:3.12") +def echo(message): + print(message) + +with Workflow( + generate_name="dag-diamond-", + entrypoint="diamond", + namespace="argo", + workflows_service=WorkflowsService(host="https://localhost:2746"), +) as w: + with DAG(name="diamond"): + A = Task(name="A", template=echo, arguments={"message": "A"}) + B = Task(name="B", template=echo, arguments={"message": "B"}) + C = Task(name="C", template=echo, arguments={"message": "C"}) + D = Task(name="D", template=echo, arguments={"message": "D"}) + A >> [B, C] >> D + +w.submit() +``` + +Now, you can write and submit Workflows that use DAG and Script templates! + +## Syntactic Sugar + +If you want to save on some typing, you can use the name of the function in place of `Task`, which looks like: + +```py +from hera.workflows import DAG, Workflow, WorkflowsService, script + +@script(image="python:3.12") +def echo(message): + print(message) + +with Workflow( + generate_name="dag-diamond-", + entrypoint="diamond", + namespace="argo", + workflows_service=WorkflowsService(host="https://localhost:2746"), +) as w: + with DAG(name="diamond"): + A = echo(name="A", arguments={"message": "A"}) + B = echo(name="B", arguments={"message": "B"}) + C = echo(name="C", arguments={"message": "C"}) + D = echo(name="D", arguments={"message": "D"}) + A >> [B, C] >> D + +w.submit() +``` + +This syntax means we don't need to specify the template being used anymore, or use the `Task` class directly. The return +values from the function calls are still `Tasks` though! + +> **Note** Currently, this syntax is not understood by Mypy, so you may see linting errors, in which case you can use +> `cast` from `typing`, or tell your Mypy linter to ignore those lines. + +## Type Safe Functions + +So far, the `echo` function doesn't make use of type annotations or return values, which makes it much harder to test. +So, if you want to write type-safe functions and return values, you'll need the Hera Runner! This is a feature that can +be enabled by setting the script's `constructor` field, or setting the `RunnerScriptConstructor` as the default in the +`global_config` to use it for all your scripts: + +```py +from hera.shared import global_config +from hera.workflows import Script, RunnerScriptConstructor + +global_config.set_class_defaults(Script, constructor=RunnerScriptConstructor()) +``` + +> When you use the Hera Runner, you need to build your code into a hosted image that your Argo instance can pull! You can then set the `global_config.image`: +> ```py +> global_config.image = "my-built-python-image" +> ``` + +Now we can write a function with parameters that use type hints! Let's calculate the area of a given rectangle's length +and width: + +```py +from hera.workflows import script + +@script(constructor="runner", image="my-built-python-image") +def calculate_area_of_rectangle(length: float, width: float): + print(length * width) +``` + +Notice how we now use `float` as the type hint of the inputs. The Hera Runner will raise a Pydantic validation error if +passed a string that cannot be deserialized into a float - e.g. `"hello"` will raise an error, but not `"1"`. This is +because Hera uses `validate_arguments` (for Pydantic v1 installations) or `validate_call` (for Pydantic v2 +installations) when calling the function. + +What if we wanted to use classes in the types in the inputs? Before we jump into adding classes, we must keep in mind +we'll be running this function on Argo, where the inputs are coming from the YAML definition. So we need a way to +deserialize inputs strings into the classes. Lucky for us, Pydantic is able to deserialize JSON-serialized strings into +Python objects, and as we've mentioned, the Hera Runner integrates with Pydantic! + +## Pydantic-Powered Functions + +The Hera Runner inspects the types of your function's parameters, and for any type that inherits from Pydantic's +`BaseModel`, the Hera Runner will try to deserialize the JSON input string into an object of that type. Therefore we can +create a class inheriting from `BaseModel`, which lets Hera deserialize the input into an object, and lets us use +convenience functions on that object, such as an `area()` function. Let's try it out! + +```py +from pydantic import BaseModel + +class Rectangle(BaseModel): + length: float + width: float + + def area(self) -> float: + return self.length * self.width + +@script(constructor="runner", image="my-built-python-image") +def calculate_area_of_rectangle(rectangle: Rectangle): + print(rectangle.area()) +``` + +But we've still got that pesky `print` statement. We should return the value of the area instead! When running on Argo, +the Hera Runner will print the return value to stdout, so you can still use the `.result` of the previous step or task +as before. + +```py +@script(constructor="runner", image="my-built-python-image") +def calculate_area_of_rectangle(rectangle: Rectangle) -> float: + return rectangle.area() + +``` + +Now we can test this function - acting as a script template - in isolation outside of Argo! + +```py +def test_calculate_area_of_rectangle(): + r = Rectangle(length=2.0, width=3.0) + assert calculate_area_of_rectangle(r) == 6.0 +``` + +Awesome! + +## Workflow Testing + +So, we can test individual templates, what about the whole workflow? Well, good news! Using Hera's YAML to Python +deserializing, we can inspect the `status` of the `Workflow` returned from the workflows API. The `status` in particular +contains the `phase` which can be "Succeeded", "Failed" etc, along with the `nodes` of the workflow. + +Taking an example workflow definition that we can retrieve from a function such as: + +```py +@script(outputs=Parameter(name="message-out", value_from={"path": "/tmp/message-out"})) +def echo_to_param(message: str): + with open("/tmp/message-out", "w") as f: + f.write(message) + +def get_workflow_definition() -> Workflow: + with Workflow(generate_name="hello-world-", entrypoint="steps") as w: + with Steps(name="steps"): + echo_to_param(arguments={"message": "Hello world!"}) + return w +``` + +We can write a test like so: + +```py +def test_create_workflow(): + workflow = get_workflow_definition() + model_workflow = workflow.create(wait=True) + assert model_workflow.status and model_workflow.status.phase == "Succeeded" + + echo_node = next( + filter( + lambda n: n.display_name == "echo-to-param", # use display_name to get the human-readable name of the nodes + model_workflow.status.nodes.values(), + ) + ) + + message_out = next(filter(lambda n: n.name == "message-out", echo_node.outputs.parameters)) + assert message_out.value == "Hello world!" +``` + +Good luck, and happy Hera-ing! diff --git a/examples/workflows/dags/dag-diamond-with-callable-decorators.yaml b/examples/workflows/dags/dag-diamond-with-callable-decorators.yaml index 7d4afb3bb..53f610bc2 100644 --- a/examples/workflows/dags/dag-diamond-with-callable-decorators.yaml +++ b/examples/workflows/dags/dag-diamond-with-callable-decorators.yaml @@ -42,8 +42,11 @@ spec: script: command: - python - image: python:alpine3.6 + image: python:3.12 source: |- + import os + import sys + sys.path.append(os.getcwd()) import json try: message = json.loads(r'''{{inputs.parameters.message}}''') except: message = r'''{{inputs.parameters.message}}''' diff --git a/examples/workflows/dags/dag_diamond_with_callable_decorators.py b/examples/workflows/dags/dag_diamond_with_callable_decorators.py index 1cd9c711e..d4571ceaa 100644 --- a/examples/workflows/dags/dag_diamond_with_callable_decorators.py +++ b/examples/workflows/dags/dag_diamond_with_callable_decorators.py @@ -1,11 +1,7 @@ -from hera.workflows import ( - DAG, - Workflow, - script, -) +from hera.workflows import DAG, Workflow, script -@script(add_cwd_to_sys_path=False, image="python:alpine3.6") +@script(image="python:3.12") def echo(message): print(message) diff --git a/examples/workflows/upstream/dag_diamond.py b/examples/workflows/upstream/dag_diamond.py index d56b6403d..0c6d21940 100644 --- a/examples/workflows/upstream/dag_diamond.py +++ b/examples/workflows/upstream/dag_diamond.py @@ -1,9 +1,6 @@ from hera.workflows import DAG, Container, Parameter, Workflow -with Workflow( - generate_name="dag-diamond-", - entrypoint="diamond", -) as w: +with Workflow(generate_name="dag-diamond-", entrypoint="diamond") as w: echo = Container( name="echo", image="alpine:3.7", diff --git a/examples/workflows/use_cases/testing-templates-and-workflows.yaml b/examples/workflows/use_cases/testing-templates-and-workflows.yaml new file mode 100644 index 000000000..61eba0545 --- /dev/null +++ b/examples/workflows/use_cases/testing-templates-and-workflows.yaml @@ -0,0 +1,38 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: dag- + namespace: argo +spec: + entrypoint: dag + templates: + - dag: + tasks: + - arguments: + parameters: + - name: rectangle + value: '{"length": 1.2, "width": 3.4}' + name: rectangle-1 + template: calculate-area-of-rectangle + - arguments: + parameters: + - name: rectangle + value: '{"length": 4.3, "width": 2.1}' + depends: rectangle-1 + name: rectangle-2 + template: calculate-area-of-rectangle + name: dag + - inputs: + parameters: + - name: rectangle + name: calculate-area-of-rectangle + script: + args: + - -m + - hera.workflows.runner + - -e + - examples.workflows.use_cases.testing_templates_and_workflows:calculate_area_of_rectangle + command: + - python + image: my-built-python-image + source: '{{inputs.parameters}}' diff --git a/examples/workflows/use_cases/testing_templates_and_workflows.py b/examples/workflows/use_cases/testing_templates_and_workflows.py new file mode 100644 index 000000000..fd8e64575 --- /dev/null +++ b/examples/workflows/use_cases/testing_templates_and_workflows.py @@ -0,0 +1,56 @@ +from hera.shared import global_config +from hera.workflows import DAG, RunnerScriptConstructor, Script, Workflow, WorkflowsService, script + +try: + from pydantic.v1 import BaseModel +except ImportError: + from pydantic import BaseModel + +global_config.set_class_defaults(Script, constructor=RunnerScriptConstructor()) + + +class Rectangle(BaseModel): + length: float + width: float + + def area(self) -> float: + return self.length * self.width + + +@script(constructor="runner", image="my-built-python-image") +def calculate_area_of_rectangle(rectangle: Rectangle) -> float: + return rectangle.area() + + +with Workflow( + generate_name="dag-", + entrypoint="dag", + namespace="argo", + workflows_service=WorkflowsService(host="https://localhost:2746"), +) as w: + with DAG(name="dag"): + A = calculate_area_of_rectangle( + name="rectangle-1", arguments={"rectangle": Rectangle(length=1.2, width=3.4).json()} + ) + B = calculate_area_of_rectangle( + name="rectangle-2", arguments={"rectangle": Rectangle(length=4.3, width=2.1).json()} + ) + A >> B + + +def test_calculate_area_of_rectangle(): + r = Rectangle(length=2.0, width=3.0) + assert calculate_area_of_rectangle(r) == 6.0 + + +def test_create_workflow(): + model_workflow = w.create(wait=True) + assert model_workflow.status and model_workflow.status.phase == "Succeeded" + + echo_node = next( + filter( + lambda n: n.display_name == "echo", + model_workflow.status.nodes.values(), + ) + ) + assert echo_node.outputs.parameters[0].value == "my value" diff --git a/mkdocs.yml b/mkdocs.yml index 164012dc9..ff358d11e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,6 +8,7 @@ nav: - Walk Through: - walk-through/quick-start.md - walk-through/about-hera.md + - walk-through/hera-tour.md - walk-through/hello-world.md - walk-through/parameters.md - walk-through/steps.md diff --git a/tests/test_submission.py b/tests/test_submission.py index cc4a3e45f..300023a5e 100644 --- a/tests/test_submission.py +++ b/tests/test_submission.py @@ -1,11 +1,16 @@ import pytest -from hera.workflows import Steps, Workflow, WorkflowsService, script +from hera.workflows import Parameter, Steps, Workflow, WorkflowsService, script +from hera.workflows.models import ( + NodeStatus, + Parameter as ModelParameter, +) -@script() -def echo(message: str): - print(message) +@script(outputs=Parameter(name="message-out", value_from={"path": "/tmp/message-out"})) +def echo_to_param(message: str): + with open("/tmp/message-out", "w") as f: + f.write(message) def get_workflow() -> Workflow: @@ -20,7 +25,7 @@ def get_workflow() -> Workflow: ), ) as w: with Steps(name="steps"): - echo(arguments={"message": "Hello world!"}) + echo_to_param(arguments={"message": "Hello world!"}) return w @@ -28,3 +33,9 @@ def get_workflow() -> Workflow: def test_create_hello_world(): model_workflow = get_workflow().create(wait=True) assert model_workflow.status and model_workflow.status.phase == "Succeeded" + + echo_node: NodeStatus = next( + filter(lambda n: n.display_name == "echo-to-param", model_workflow.status.nodes.values()) + ) + message_out: ModelParameter = next(filter(lambda n: n.name == "message-out", echo_node.outputs.parameters)) + assert message_out.value == "Hello world!"