Hera Python -> expr transpiler
Expr is an expression evaluation language used by Argo.
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 tellsherato 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 tellsherato 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 transpiledexprexpression.str(g.input.parameters.value) == "input.parameters.value": callingstron ahera.exprexpression 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,.5can 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 -
trueandfalsecan be represented asC(True)andC(False) - nil -
nilcan 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 writing C(1) + 2. This however only works if the left operand is a literal or an identifier. If in doubt, always define literals via C(...)
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.vartranspiles tovar
Fields
Struct fields can be accessed by using the . syntax.
g.var.attributetranspiles 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.ytranspiles 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
notor!andor&&oror||
Example:
~g.ytranspiles to!yg.y & g.xtranspiles toy && xg.y | g.xtranspiles 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 in expr 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) the path variable in the jsonpath method may either be a string or a programmatic JSONPath expression built using the jsonpath-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 returntrueif all element satisfies the predicate)none(will returntrueif all element does NOT satisfy the predicate)any(will returntrueif any element satisfies the predicate)one(will returntrueif 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) |