Tasks development - more examples ¶
RKD has multiple approaches to define a task. The first one is simpler - in makefile in YAML or in Python. The second one is a set of tasks as a Python package.
Option 1) Simplest - in YAML syntax ¶
Definitely the simplest way to define a task is to use YAML syntax, it is recommended for beginning users.
Example 1:
version: org.riotkit.rkd/yaml/v1
imports:
- rkd.standardlib.jinja.RenderDirectoryTask
tasks:
# see this task in "rkd :tasks"
# run with "rkd :examples:bash-test"
:examples:bash-test:
description: Execute an example command in bash - show only python related tasks
steps: |
echo "RKD_DEPTH: ${RKD_DEPTH} # >= 2 means we are running rkd-in-rkd"
echo "RKD_PATH: ${RKD_PATH}"
rkd --silent :tasks | grep ":py"
# try "rkd :examples:arguments-test --text=Hello --test-boolean"
:examples:arguments-test:
description: Show example usage of arguments in Bash
arguments:
"--text":
help: "Adds text message"
required: True
"--test-boolean":
help: "Example of a boolean flag"
action: store_true # or store_false
steps:
- |
#!bash
echo " ==> In Bash"
echo " Text: ${ARG_TEXT}"
echo " Boolean test: ${ARG_TEST_BOOLEAN}"
- |
#!python
print(' ==> In Python')
print(' Text: %s ' % ctx.args['text'])
print(' Text: %s ' % str(ctx.args['test_boolean']))
return True
# run with "rkd :examples:list-standardlib-modules"
:examples:list-standardlib-modules:
description: List all modules in the standardlib
steps:
- |
#!python
ctx: ExecutionContext
this: TaskInterface
import os
print('Hello world')
print(os)
print(ctx)
print(this)
return True
:examples:with-other-workdir:
description: "This task runs in /tmp"
workdir: "/tmp"
steps: |
echo "I run in"
pwd
Example 2:
version: org.riotkit.rkd/yaml/v1
environment:
GLOBALLY_DEFINED: "16 May 1966, seamen across the UK walked out on a nationwide strike for the first time in half a century. Holding solid for seven weeks, they won a reduction in working hours from 56 to 48 per week "
env_files:
- env/global.env
tasks:
:hello:
description: |
#1 line: 11 June 1888 Bartolomeo Vanzetti, Italian-American anarchist who was framed & executed alongside Nicola Sacco, was born.
#2 line: This is his short autobiography:
#3 line: https://libcom.org/library/story-proletarian-life
environment:
INLINE_PER_TASK: "17 May 1972 10,000 schoolchildren in the UK walked out on strike in protest against corporal punishment. Within two years, London state schools banned corporal punishment. The rest of the country followed in 1987."
env_files: ['env/per-task.env']
steps: |
echo " >> ENVIRONMENT VARIABLES DEMO"
echo "Inline defined in this task: ${INLINE_PER_TASK}\n\n"
echo "Inline defined globally: ${GLOBALLY_DEFINED}\n\n"
echo "Included globally - global.env: ${TEXT_FROM_GLOBAL_ENV}\n\n"
echo "Included in task - per-task.env: ${TEXT_PER_TASK_FROM_FILE}\n\n"
Explanation of examples:
-
“arguments” is an optional dict of arguments, key is the argument name, subkeys are passed directly to argparse
-
“steps” is a mandatory list or text with step definition in Bash or Python language
-
“description” is an optional text field that puts a description visible in “:tasks” task
-
“workdir” allows to optionally specify a working directory for a task
-
“environment” is a dict of environment variables that can be defined
-
“env_files” is a list of paths to .env files that should be included
-
“imports” imports a Python package that contains tasks to be used in the makefile and in shell usage
Option 2) For Python developers - task as a class ¶
This way allows to create tasks in a structure of a Python module. Such task can be packaged, then published to eg. PyPI (or other private repository) and used in multiple projects.
Each task should implement methods of rkd.core.api.contract.TaskInterface interface, that’s the basic rule.
Following example task could be imported with path rkd.standardlib.ShellCommandTask , in your own task you would have a different package name instead of rkd.standardlib .
Example task from RKD standardlib:
class ShellCommandTask(ExtendableTaskInterface, MultiStepLanguageExtensionInterface):
"""
Executes shell commands and scripts
Extendable in two ways:
- overwrite stdin()/input to execute a script
- overwrite execute() to execute a Python code that could contain calls to self.sh()
"""
# to be overridden in compile()
is_cmd_required: bool # Is --cmd switch required to be set?
code: Optional[str] # (Optional) Execute script from a variable value
name: Optional[str] # (Optional) Task name
step_num: int
def __init__(self):
self.is_cmd_required = True
self.code = None
self.name = None
self.step_num = 0
def get_name(self) -> str:
return ':sh' if not self.name else self.name
def get_group_name(self) -> str:
return ''
def get_configuration_attributes(self) -> List[str]:
return ['is_cmd_required']
def configure_argparse(self, parser: ArgumentParser):
parser.add_argument('--cmd', '-c', help='Shell command', required=self.is_cmd_required)
def with_predefined_details(self, code: str, name: str, step_num: int) -> 'ShellCommandTask':
clone = copy(self)
clone.code = code
clone.name = name
clone.step_num = step_num
clone.is_cmd_required = False
return clone
def execute(self, context: ExecutionContext) -> bool:
cmd = ''
if context.get_input():
cmd = context.get_input().read()
if context.get_arg('cmd'):
cmd = context.get_arg('cmd')
if self.code:
cmd = self.code
try:
# self.sh() and self.io() are part of the base class
if cmd:
self.sh(cmd, capture=False)
self.inner_execute(context)
except CalledProcessError as e:
self.io().error_msg(str(e))
return False
return True
Explanation of example:
-
The docstring in Python class is what will be shown in :tasks as description . You can also define your description by implementing def get_description() -> str
-
Name and group name defines a full name eg. :your-project:build
-
def configure_argparse() allows to inject arguments, and –help description for a task - it’s a standard Python’s argparse object to use
-
def execute() provides a context of execution, please read Tasks API chapter about it. In short words you can get commandline arguments, environment variables there.
-
self.io() is providing input-output interaction, please use it instead of print, please read Tasks API chapter about it.
Option 3) Quick and elastic way in Python code of Makefile.py ¶
Multiple Makefile files can be used at one time, you don’t have to choose between YAML and Python. This opens a possibility to define more advanced tasks in pure Python, while you have most of the tasks in YAML. It’s elastic - use YAML, or Python or both.
Let’s define then a task in Python in a simplest method.
Makefile.py
from argparse import ArgumentParser
from rkd.core.api.contract import ExecutionContext
from rkd.core.api.decorators import extends
from rkd.core.api.syntax import ExtendedTaskDeclaration
from rkd.core.standardlib.syntax import PythonSyntaxTask
@extends(PythonSyntaxTask)
def hello_task():
"""
Prints your name
"""
def configure_argparse(task: PythonSyntaxTask, parser: ArgumentParser):
parser.add_argument('--name', required=True, help='Allows to specify a name')
def execute(task: PythonSyntaxTask, ctx: ExecutionContext):
task.io().info_msg(f'Hello {ctx.get_arg("--name")}, I\'m talking in Python, and you?')
return True
return [configure_argparse, execute]
IMPORTS = [
ExtendedTaskDeclaration(hello_task, name=':hello2')
]