Yoda and the iRODS Python Rule Engine Plugin
iRODS can be used with various rule engine plugins, so that rules can be written in multiple programming languages. Yoda uses both the builtin (legacy) iRODS rule language plugin and the Python Rule Engine Plugin (PREP).
This page contains basic information about working with the PREP in combination with Yoda. The PREP documentation has additional generic information and examples.
Configuring the Python rule engine plugin
The Python Rule Engine Plugin is configured in server_config.json
, in the rule_engines
array:
{
"instance_name": "irods_rule_engine_plugin-python-instance",
"plugin_name": "irods_rule_engine_plugin-python",
"plugin_specific_configuration": {}
}
The Python plugin is the second element in the rule engine list, after the iRODS rule language plugin, so that we can combine legacy rule language code with new Python code.
Python rule functions
There are three different types of functions, each with its own way of handling arguments and return values:
-
Rules called directly by iRODS have numbered parameters passed through
rule_args
:def acPythonPEP(rule_args, callback, rei): callback.writeLine("stdout", "arg = " + rule_args[0])
Such rules can also return values through numbered output parameters.
-
Rules called with irule or from the frontend have access to
global_vars
, in which named parameters are passed as strings including the quotes:def main(rule_args, callback, rei): arg = global_vars["*arg"][1:-1] # strip the quotes callback.writeLine("stdout", "arg = " + arg)
Output cannot be passed back through named parameters, but has to be handled with writeString/writeLine:
INPUT *arg="some argument" OUTPUT ruleExecOut
Note that
global_vars
is only available to Python rule functions, not to other functions called by rule functions. -
Ordinary Python functions which are not called as a rule by iRODS or externally, but only by other Python code. For example:
def concat(str1, str2): return str1 + str2
Explanation of parameters from the PREP documentation:
The first argument [...], rule_args, is a tuple containing the optional, positional parameters fed to
the rule function by its caller; these same parameters may be written to, with the effect that the
written values will be returned to the caller. The callback object is effectively a gateway through
which other rules (even those managed by other rule engine plugins) and microservices may be called.
Finally, rei (also known as "rule execution instance") is an object carrying iRODS-related context
for the current rule call, including any session variables.
Running Python rule code using irule
The example below shows a simple “Hello world” rule that can be executed using irule
. The rule takes
the name argument from global_vars, strips away the quotes and prints a greeting.
def main(rule_args, callback, rei):
name = global_vars["*name"][1:-1]
callback.writeLine("stdout", "Hello " + name + "!")
input *name="World"
output ruleExecOut
The code can be run by putting it in a .r file, e.g. hello.r
and then running irule on it:
$ irule -r irods_rule_engine_plugin-python-instance -F hello.r
Hello World!
You need to have a rodsadmin account in order to run Python code this way. The rule function needs
to be named main
.
Calling rules from Python rule code
You can also call other Python rules in the rulebase from a local rule. This example shows how to call a test rule that echoes back a number.
Local rule file, e.g. call-echo.r
:
def main(rule_args, callback, rei):
number = global_vars['*number']
ret = callback.rule_echo_number(number, "")
callback.writeLine("stdout", "Received return value " + str(ret['arguments'][1]))
input *number="4"
output ruleExecOut
Remote rule code:
@rule.make(inputs=[0], outputs=[1])
def rule_echo_number(ctx, number):
"""Test function that returns a number passed to it."""
return str(number)
The local rule can then be executed with irule
: irule -r irods_rule_engine_plugin-python-instance -F call-echo.r
Defining Python code in the ruleset
iRODS Python code in the ruleset needs to ultimately be imported from /etc/irods/core.py
. On Yoda environments,
core.py
has an import statement that imports all Python code in the Yoda ruleset directory:
from rules_uu import *
Yoda Python rule code is put in ruleset directory /etc/irods/yoda-ruleset
, which is linked from
/etc/irods/rules_uu
. Each rule is defined in a function.
This is a minimal example of a rule definition in Python:
def hello_world(rule_args, callback, rei):
callback.writeLine("stdout", "Hello world!")
If you add this function to a Python file in the ruleset directory, and ensure it is included in the file’s __all__
list (assuming it has one), you can call it using irule:
$ irule -r irods_rule_engine_plugin-python-instance hello_world null ruleExecOut
Hello world!
If the rule would have had a parameter, you could have passed it like this:
irule -r irods_rule_engine_plugin-python-instance hello_world '*arg="some argument"' ruleExecOut
Using Yoda rule decorators
In Yoda, the rule.make
decorator is commonly used to write rules in a more pythonic way and
to reduce boilerplate.
This example defines a rule with two input parameters and a single output parameter using rule.make
:
@rule.make(inputs=[0,1], outputs=[2])
def foo(ctx, x, y):
return int(x) + int(y)
This is the equivalent of the code above without the decorator:
def foo(rule_args, callback, rei):
x, y = rule_args[0:2]
rule_args[2] = int(x) + int(y)
For a complete overview of functionality of rule.make
, please consult the function documentation
There’s also an api.make
decorator available that creates an API function which can
receive and return data in JSON format. For additional information, please consult the function documentation
Simple code example roundtrip iRODS -> Python -> iRODS
Essence of the example is that rule_args serves as both input and as output parameters
iRODS rule language:
# \brief Front end rule to retrieve XSD location
#
# \param[in] folder Path of the folder
# \param[out] schemaLocation Location of XSD
# \param[out] status Status of the action
# \param[out] statusInfo Information message when action was not successful
iiFrontGetSchemaLocation(*folder, *schemaLocation, *status, *statusInfo)
{
*status = "Success";
*statusInfo = "";
*schema = '';
iiRuleGetLocation(*folder, *schema); # it is not possible to directly use *schemaLocation here
writeLine('serverLog', 'schema: ' ++ *schema);
*schemaLocation = *schema; # again, does not work when passing schemaLocation directly in iiRuleGetLocation
}
Python:
# \brief Nonsense function that returns 'enriched' text based on rule_args[0]
# rule_args serves both as input and output parameters
def iiRuleGetLocation(rule_args, callback, rei):
rule_args[1] = 'You passed the folder: ' + rule_args[0]
Calling microservices from Python code
Use irods_types
to create output parameters of the proper type, and obtain the output values from ret_val["arguments"][N]
.
Example code:
def uuGetGroups(rule_args, callback, rei):
import json
groups = []
ret_val = callback.msiMakeGenQuery("USER_GROUP_NAME", "USER_TYPE = 'rodsgroup'", irods_types.GenQueryInp())
query = ret_val["arguments"][2] # output parameter type GenQueryInp
ret_val = callback.msiExecGenQuery(query, irods_types.GenQueryOut())
while True:
result = ret_val["arguments"][1] # output parameter type GenQueryOut
for row in range(result.rowCnt):
name = result.sqlResult[0].row(row)
groups.append(name)
# continue with this query
if result.continueInx == 0:
break
ret_val = callback.msiGetMoreRows(query, result, 0)
callback.writeString("stdout", json.dumps(groups))
Setting AVUs from Python
Example code:
import irods_types
def uuMetaAdd(callback, objType, objName, attribute, value):
keyValPair = callback.msiString2KeyValPair(attribute + "=" + value, irods_types.KeyValPair())['arguments'][1]
retval = callback.msiAssociateKeyValuePairsToObj(keyValPair, objName, objType)
def addCollectionStatus(rule_args, callback, rei):
uuMetaAdd(callback, "-C", "/tempZone/home/research-initial", "status", "PUBLISHED")