Python → expr transpiler
Expr is a Go-centric expression language used by Argo Workflows.
Hera provides an easy way to construct expr
expressions in Python. It supports the full language definition of expr
including the enhancements added by Argo.
Usage
The recommended way of using the hera.expr
module is to construct the expression in Python. Once your expressions is
ready to be used, you may call str(<expression>)
to convert it to an appropriate expr
expression. Hera also supports
formatting expressions such that they are surrounded by braces which is useful in Argo when substituting variables. You
can do this via Python string format literals and by adding $
as a format string.
Example:
f"{g.input.parameters.value:$}" == "{{input.parameters.value}}"
: the$
format string tellshera
to insert the braces around the output as a simple variable.f"{g.workflow.parameters.config.jsonpath('$.a'):=}" == "{{=jsonpath(workflow.parameters.config, '$.a')}}"
: the=
format string tellshera
to insert the braces around the output and insert and equals sign and output a complex expression.f"{g.input.parameters.value}" == "input.parameters.value"
: without any extra format strings, the output is the transpiledexpr
expression.str(g.input.parameters.value) == "input.parameters.value"
: callingstr
on ahera.expr
expression also triggers the transpilation.
Supported Literals
The transpiler supports constant literals via the class C
:
- strings - single and double quotes (e.g.
"hello"
,'hello'
) can be represented asC("hello")
,C('hello')
- numbers - e.g.
103
,2.5
,.5
can be represented asC(103)
,C(2.5)
,C(.5)
- arrays - e.g.
[1, 2, 3]
can be represented asC([1, 2, 3])
- maps - e.g.
{foo: "bar"}
can be represented asC({'foo': 'bar'})
- booleans -
true
andfalse
can be represented asC(True)
andC(False)
- nil -
nil
can be represented asC(None)
Note:
hera
is smart enough to transpile python constants used in conjunction with a literal directly to a literal. This helps with brevity. For eg:C(1) + C(2)
is the same as writingC(1) + 2
. This however only works if the left operand is a literal or an identifier. If in doubt, always define literals viaC(...)
Reference
Python | expr transpilation |
---|---|
C(1) | 1 |
C(None) | nil |
C(True) | true |
C(False) | false |
C([1, 2, 3]) | [1, 2, 3] |
C({“hello”: False, “world”: None}) | {“hello”: false, “world”: nil} |
Identifiers/Variables
Global variables/identifiers can be accessed through attributes of the global context variable g
:
g.var
transpiles tovar
Fields
Struct fields can be accessed by using the .
syntax.
g.var.attribute
transpiles tovar.attribute
There maybe times that you want to get a struct field whose name conflicts with a hera.expr
transpiler function. In this case you can use the get
method.
g.get("var").get("attribute")
transpiles tovar.attribute
Map elements
Map elements can be accessed used the []
syntax.
g.var['element']
transpiles tovar['element']
Reference
Python | expr transpilation |
---|---|
g.test[2] | test[2] |
g.test[‘as’] | test[‘as’] |
g.test.attr | test.attr |
g.get(“test”).get(“attr”).another_attr | test.attr.another_attr |
Slices
array[:]
(slice)
Slices can work with arrays or strings. Python slices are transpiled appropriately to expr slices.
Note: Only slices with step-size of 1 are supported.
Example:
Variable array
is [1,2,3,4,5]
.
transpiles to
Reference
Python | expr transpilation |
---|---|
g.test[1:9] | test[1:9] |
g.test[1:] | test[1:] |
g.test[:] | test[:] |
g.test[:9] | test[:9] |
g.test[0:9] | test[0:9] |
Operators
All the operators transpile appropriately and provide a native python look and feel.
Arithmetic Operators
+
(addition)-
(subtraction)*
(multiplication)/
(division)%
(modulus)**
(pow)
Example:
g.x ** 2 + g.y
transpiles tox**2 + y
Comparison Operators
==
(equal)!=
(not equal)<
(less than)>
(greater than)<=
(less than or equal to)>=
(greater than or equal to)
Example:
g.x >= C(1)
transpiles tox >= 1
Logical Operators
not
or!
and
or&&
or
or||
Example:
~g.y
transpiles to!y
g.y & g.x
transpiles toy && x
g.y | g.x
transpiles toy || x
String Operators
+
(concatenation)matches
(regex match)contains
(string contains)starts_with
(has prefix)ends_with
(has suffix)
Example:
C("test") + C("test2")
transpiles to'test' + 'test2'
C("test").matches(".*")
transpiles to'test' matches '.*'
g.var.contains("substring")
transpiles tovar contains 'substring'
g.var.starts_with("substring")
transpiles tovar startsWith 'substring'
g.var.ends_with("substring")
transpiles tovar endsWith 'substring'
Membership Operators
in
(contain)not in
(does not contain)
Example:
g.user.Group.in([["human_resources", "marketing"]])
transpiles touser.Group in ["human_resources", "marketing"]
C("baz").not_in({"foo": 1, "bar": 2})
transpiles to'baz' in {'foo': 1, 'bar': 2}
Numeric Operators
1..9
(range) can be represented asC(range(1, 10))
Note:
range
inexpr
is inclusive i.e.C(range(1, 3)) == C([1, 2]) == 1..2
Ternary Operators
foo ? 'yes' : 'no'
can be represented asg.foo.check('yes', 'no')
Parantheses
In case users want to add parantheses around an expression, you can use the class P
:
P(C(1) * 4) / 2
transpiles to (1 * 4) / 2
Reference
Python | expr transpilation |
---|---|
g.x**2 + g.y | x ** 2 + y |
C(1) + C(2) * C(3) / C(4) | 1 + 2 * 3 / 4 |
C(1) >= C(2) | 1 >= 2 |
C(range(1, 10)) | 1..9 |
-g.y | -y |
+g.y | +y |
~g.y | !y |
C(False) & C(True) | true && false |
C(False) | C(True) | true || false |
C(1).not_in([1, 2, 3]) | 1 not in [1, 2, 3] |
C(1).in_([1, 2, 3]) | 1 in [1, 2, 3] |
C(“has”) + “as” | ‘has’ + ‘as’ |
C(“has”).contains(“as”) | ‘has’ contains ‘as’ |
C(“has”).matches(“.*”) | ‘has’ matches ‘.*’ |
C(“has”).starts_with(“h”) | ‘has’ startsWith ‘h’ |
C(“has”).ends_with(“s”) | ‘has’ endsWith ‘s’ |
C(range(1, 10)) | 1..10 |
g.test.check(g.test1, g.test2) | test ? test1 : test2 |
P(C(1) >= C(2)) & P(C(2) < C(3)) | (1 >= 2) and (2 < 3) |
Builtin functions
len(foo)
(length of array, map or string) can be represented asg.foo.length()
asInt('1')
(convert the string to integer) can be represented asC("1").as_int()
asFloat('1.2')
(convert the string to integer) can be represented asC("1.2").as_float()
asFloat('1.2')
(convert the string to integer) can be represented asC("1.2").as_float()
toJson([1, 2])
(convert to a JSON string) can be represented asC([1, 2]).to_json()
jsonpath(test, 'test')
(extract the element from JSON using JSON Path) can be represented asg.test.jsonpath("test")
Note:
jsonpath(path)
thepath
variable in thejsonpath
method may either be a string or a programmatic JSONPath expression built using thejsonpath-ng
library.
There are also some functional programming related functions which accept a list of items and a predicate. The iterable inside the predicate can be accessed via a special variable it
. The following functions are supported
all
(will returntrue
if all element satisfies the predicate)none
(will returntrue
if all element does NOT satisfy the predicate)any
(will returntrue
if any element satisfies the predicate)one
(will returntrue
if exactly ONE element satisfies the predicate)filter
(filter array by the predicate)map
(map all items with the closure)count
(returns number of elements what satisfies the predicate)
Closures
The closure is an expression that accepts a single argument. To access
the argument use the it
variable.
Examples:
transpiles to
Examples:
Ensure all tweets are less than 280 chars.
transpiles to
Ensure there is exactly one winner.
transpiles to
Note:
expr
allows you to omit#
when accessing attributes in the predicate. In order to ensure consistency,hera
always includes#
in the output closure, even though it can be omitted.
Reference
Python | expr transpilation |
---|---|
g.test.length() | len(test) |
g.test.length() > 2 | len(test) > 2 |
g.test[g.test.length() - 1] | test[len(test) - 1] |
g.test.string() | string(test) |
g.test.to_json() | toJson(test) |
g.test.jsonpath(“test”).test.length() | len(jsonpath(test, ‘test’).test) |
g.test.jsonpath(“test”).test.as_float() | asFloat(jsonpath(test, ‘test’).test) |
g.test.jsonpath(“test”).test.as_int() | asInt(jsonpath(test, ‘test’).test) |
g.test.jsonpath(Fields(“foo”).child(“test”).child(Slice(“*”))) | jsonpath(test, ‘foo.test.[*]’) |
g.test.map(it + 2) | map(test, {# + 2}) |
g.test.map(it.Size + 2) | map(test, {#.Size + 2}) |
g.test.filter(P(it[“items”].length() + 1) > 0) | filter(test, {(len(#[‘items’]) + 1) > 0}) |
Sprig functions
Spring functions may be called using the sprig.<function>(*args)
syntax. For a complete list of functions you may visit the sprig function documentation
sprig.trim(g.test)
transpiles tosprig.trim(test)
Reference
Python | expr transpilation |
---|---|
sprig.trim(“c”) | sprig.trim(‘c’) |
sprig.add(g.test.length(), 1) | sprig.add(len(test), 1) |