Describing a Program in Python
This section explains how to describe a program in Python with the SDK
of DeBasher, for whoever prefers code to a canvas and Python to Bash.
The SDK is a library with which processes are defined, with their
options and their connections, and a program that adds them; the tool
debasher_build turns the description into the same saved program
and the same module that the web interface saves, so that the program
opens in the editor and runs with debasher_exec like any other. The
SDK executes nothing itself and generates no Bash of its own: what it
writes is what the editor would write for the same program, and the
code of each process stays in a file of its own language beside the
description. Read the Programs, Processes and Options Section first for what a program
is made of; this section says how each piece is written in Python.
Installing and importing the SDK
The SDK is the Python package debasher, installed with the rest of
DeBasher (see the Installation Section). It needs the Python
packages of the web interface (pydantic among them), installed into
a virtual environment as the DeBasher Web Interface Section explains, and it is
found in two ways:
debasher_buildfinds it on its own: it runs with the Python thatDEBASHER_WEBUI_PYTHONnames, else withpython3on thePATH(that of an activated virtual environment), asdebasher_webuidoes, and puts the directory of the package on its path, so that the description it loads importsdebasherwith nothing more. This is all that is needed to build a program.To use the library by hand, in a Python session or a test,
import debasherworks as it is when DeBasher was configured with the prefix of the Python itself (a virtual environment, or a conda environment with its own Python). The conda package of DeBasher keeps the package in a directory of its own and puts it inPYTHONPATHwhen the environment is activated. With another prefix, put the Python directory of the installation inPYTHONPATH(<prefix>/lib/python3.X/site-packages).
A first program
The hello world of the Quickstart Section, with its process in
Python, is shipped as sdk_hello_world_py among the examples of the
SDK (the data/sdk_programs/python folder of the repository,
installed under <prefix>/share/debasher/sdk_programs/python), a
directory with the description and the code of the process:
from debasher import Process, Program
hello = Process("hello_world", code="hello_world.py", description="Prints a greeting.")
hello.cmdline_opt("-s", "string", "string to print ('Hello World!' by default)", mandatory=False)
prg = Program("sdk_hello_world_py")
prg.add(hello, cpus=1, mem=256, time="01:00:00")
The description reads like a module. Process defines a process with
its name, its description and the file of its code, hello_world.py,
whose extension tells its language (.sh, .py, .pl, .r,
.groovy); the file holds what the code box of the editor holds, the
whole script of a process in Python, or, for a process in Bash, the
function named as the process with its helper functions. The methods of
the process declare its options: here one command line option, -s,
with its data type, its description and whether it is mandatory.
Program defines the program, named as the file, and add adds the
process with the specifications of its use, as add_debasher_process
does in a module. The file calls nothing: it defines, as a module
defines, and the tool does the rest.
Building and running
$ debasher_build sdk_hello_world_py.py
/path/to/sdk_hello_world_py/sdk_hello_world_py.sh
debasher_build loads the file, finds the program it defines and
writes it beside the file: the program as the web interface saves it,
the topology file program.yaml (see the The program.yaml Format
Section), which names the file of each process where the description has
it, and the module sdk_hello_world_py.sh, exactly what the “Save”
button of the editor writes (-o <dir> writes them elsewhere, with a
copy of each process file). The file is named after its program, as a
module is, and defines one program; anything else is an error that says
so. From here the program is one more: it runs with debasher_exec,
its state is followed with debasher_status, and its directory opens
in the web interface with “Load program”, where it shows on the canvas
with its processes laid out as those of an imported module are.
$ debasher_exec --pfile sdk_hello_world_py.sh --outdir out -s "Hello from Python" --wait
$ debasher_get_stdout -d out -p hello_world
Hello from Python
The Python file is never run with python and never given to
debasher_exec; after a change to it, debasher_build is run
again. Given the directory of a saved program instead, or its
program.yaml, or nothing inside it, debasher_build writes the
module again from program.yaml and the files of the processes,
whoever saved them (see the DeBasher Tools Section); -o goes with a
Python file only.
Processes, options and connections
The fan-out to a pool of workers of the Design Patterns Section,
as webui_dynamic_fanout_fifos builds it in the web interface, is
shipped as sdk_dynamic_fanout_fifos and shows the rest: a generated
file is counted and cut into fragments, which a dispatcher streams
through FIFOs to a pool of workers, and a last process merges their
counts.
from debasher import Array, Process, Program, in_outdir
SPECS = dict(cpus=1, mem=32, time="00:01:00")
count = Process("count", code="count.sh",
description="Counts characters of an input file in a conventional way. "
"This process is added for debugging purposes.")
count.input("-inf", "file", "input file", source=("generate", "-outf"))
count.output("-outf", "file", "output file", in_outdir("counts.txt"))
generate = Process("generate", code="generate.sh",
description="Generates a text file of a given size and random content.")
generate.output("-outf", "file", "output file", in_outdir("file.txt"))
generate.cmdline_opt("-l", "int", "Length of file in lines")
generate.cmdline_opt("-c", "int", "Length of each line in characters")
fragment = Process("fragment", code="fragment.sh",
description="Fragments an input file into a given number of equally sized "
"blocks, inverting the file lines.")
fragment.output("-outd", "dir", "name of output directory", in_outdir())
fragment.fifo_output("-outf", "file", "output fifo", "fragment_fifo")
fragment.input("-inf", "file", "input file", source=generate["-outf"])
fragment.cmdline_opt("-b", "int", "Number of blocks")
aggregate = Process("aggregate", code="aggregate.sh", description="Aggregates worker results.")
aggregate.output("-outf", "file", "output file", in_outdir("result.txt"))
workers = aggregate.cmdline_opt("-w", "int", "Number of workers.")
aggregate.input("-ind-ith", "dir", "i'th input directory", source=("worker", "-outd"),
count_source=workers)
dispatch = Process("dispatch", script="./dynamic_fanout_dispatcher.py",
description="Dispatches file blocks to workers.")
dispatch.fifo_output("-outf-ith", "file", "i'th output fifo", "dispatch_fifo_${i}", count_source="-w")
dispatch.cmdline_opt("-w", "int", "Number of workers.")
dispatch.input("-inf", "file", "input fifo", source=fragment["-outf"])
dispatch.cmdline_opt("--log-level", "string",
"Logging verbosity. Choices: DEBUG, INFO, WARNING, ERROR, CRITICAL (default: INFO).",
mandatory=False)
worker = Array("worker", code="worker.sh", tasks="worker_tasks.py",
description="Executes an array of w tasks. Each task takes a list of text files "
"and generates another file for each one.")
worker.outdir_opt("-outd", "directory of the task, below the output directory of the process",
subpath="${task_idx}")
worker.input("-inf", "file", "input fifo", source=dispatch["-outf-ith"])
worker.cmdline_opt("-w", "int", "Number of workers.", shaping=True)
prg = Program(
"sdk_dynamic_fanout_fifos",
description="Example described with the SDK: a generated file is counted and cut into "
"fragments, which a dispatcher streams through FIFOs to a pool of workers, and a last "
"process merges their counts.",
program_options={"-l": "200", "-c": "20", "-b": "20", "-w": "4"},
)
prg.add(count, **SPECS)
prg.add(generate, **SPECS)
prg.add(fragment, **SPECS, processdeps="afterok:count")
prg.add(aggregate, **SPECS)
prg.add(dispatch, **SPECS)
prg.add(worker, **SPECS)
The pieces, in the order a process is written (the Python SDK Section lists every class and method):
Where the code is.
code=names the file of the process, which the topology file names in turn, where the description has it: the module embeds it when it is Bash, and runs it as it is, as an external alias of the file, in any other language.script=names a file that the module runs as it is too, an executable with the interpreter of its extension, but that is not the code of the process: the editor shows acode=file as the code of the process and edits it, and leaves ascript=file alone (the dispatcher); the external alias of the Writing a Module in Bash Section.alias=runs the code of another process under this one’s name and options (alias_opt_map={"-x": "-y"}renames the options it passes on, as the alias option map of a module does), and a process with none of them takes its code from a module that the preamble of the program loads (Program(..., preamble="load_debasher_module ...")).Options. Every option is declared by a method that says where its value comes from and how it is delivered, with the data types of the editor (
int,float,string,file,dir):cmdline_optfor a value of the command line of the run (mandatory=Falsefor an optional one,shaping=Truefor a task shaping option),cmdline_flagfor a flag,inputfor an input with a literal value or connected to an output,outputfor an output, whose value names what the process produces (in_outdir("counts.txt")is the file of that name in the process output directory),fifo_outputandfifo_inputfor an option delivered through a FIFO of a given name (mirror=Truekeeps the copy of what is written thatdebasher_get_fifo_mirrorreads),outdir_optfor an output that names the process output directory or a task subdirectory of it (subpath="${task_idx}"),shared_dir_opt,procspec_optandvalue_desc_outputfor the other option channels of the editor, andoptfor any option with its fields given by name. The direction of an option comes from its label, as the engine reads it: an output starts with-out. An option is reached again by its label,generate["-outf"].Connections. An input names the output it reads, as the module does in
define_opt_from_proc_out:source=takes the option of another process, or the name of the process and the label of the option,("generate", "-outf"), which lets a process be defined before the one it reads; a list of sources is a fan-in. The program derives its connections from the inputs of the processes it adds, and an input that reads a process the program does not add is an error.Families. An option whose label ends in
-ithstands for one option per task of the array or generator at the other end (-outf-ithfor-outf0,-outf1, …), as in the editor;count_source=names the command line option of the same process that counts them, and without it the family is counted by the tasks of the process it is connected to.Arrays and generators.
ArrayandGeneratorare processes with several tasks, the array and generator modes of the options handler of the editor, andtasks=names the file that shapes them. A Python file (.py) defines a functiontasks(run), which returns the elements of the array, or the number of tasks of a generator, from the command line of the run:worker_tasks.pygives one element per worker,# One task per worker: the elements of the array are 0, 1, ..., w-1 def tasks(run): return range(int(run.value("-w")))
and any other file is Bash, the code that the editor accepts in that mode (see The Tasks in Python for what
runholds and how the module calls the function). Either way the code runs whendebasher_execprepares a run, as it does for a program of the editor, since the tasks depend on the command line of each run, and only the shape of the tasks comes from it: the values of the options of each task are still the Bash words of their declarations, such assubpath="${task_idx}".The program.
Programtakes the description, the program options that a run is given (what “Set program options” of the editor holds), the preamble, the environment variables, the shared directories and the execution options of the editor by name (scheduler,conda_support, …);addtakes the process and the specifications of its use there:cpus,mem,time(1 CPU, 256 megabytes and01:00:00for those not given, as the editor gives a new process), the explicit dependencies (processdeps="afterok:count"),force, andpositionon the canvas when the layout of the editor is not wanted.
Skipping tasks, and the environments of a process
The additional methods of a process (skip, post,
reset_outfiles, outdir_basename) are given as files, each with
the Bash body that the editor takes. The skip method may be a Python
file instead, with a function skip(task) that returns True to
skip the task and False to run it. The skip example of the
Writing a Module in Bash Section, shipped as sdk_skip_py, skips the
process that reads a sum when the sum is odd:
from debasher import Process, Program, in_outdir
SPECS = dict(cpus=1, mem=32, time="00:01:00")
value_writer = Process("value_writer", code="value_writer.sh",
description="Takes two numbers and produces their sum as output.")
value_writer.cmdline_opt("-num-a", "int", "First number to be summed")
value_writer.cmdline_opt("-num-b", "int", "Second number to be summed")
value_writer.value_desc_output("-outv", "int", "output value descriptor")
value_reader = Process("value_reader", code="value_reader.sh", skip="value_reader_skip.py",
description="Reads the sum, increases it by one and writes it into a file; "
"skipped when the sum is odd.")
value_reader.input("-val-desc", "int", "value descriptor", source=value_writer["-outv"])
value_reader.output("-outf", "file", "output file", in_outdir("value_reader.out"))
prg = Program(
"sdk_skip_py",
description="Example described with the SDK: a process sums two numbers into a value "
"descriptor, and another writes the sum plus one, skipped by a rule in Python when the "
"sum is odd.",
program_options={"-num-a": "1", "-num-b": "3"},
)
prg.add(value_writer, **SPECS)
prg.add(value_reader, **SPECS)
# The task is skipped when the value it reads is odd: task.value reads
# the value that the value descriptor of value_writer holds
def skip(task):
return int(task.value("-val-desc")) % 2 == 1
task.value (task.value(label, default) for a default) reads an
option of the task as read_opt_value_from_func_args does, so the
value that a value descriptor holds comes back as it is. The function
runs inside the task, with the options of the task, before anything of
its process output directory is touched; an error in it (an exception, a
value that is not True or False) fails the task instead of
running it. Only skip takes a Python file: any other method given
one is an error.
The Conda environments and the Docker images of a process are data:
conda_envs= takes the name of each environment and its yml file,
and docker_imgs= a list of images, from which the module writes the
lines that prepare them when the run has conda or docker support
(conda_support=True or docker_support=True in Program, or
--conda-support or --docker-support given to debasher_exec).
The yml file is found in the directories that DEBASHER_YML_DIR
lists, else among those installed with DeBasher, where py27.yml is,
and not beside the description. sdk_conda_envs activates the
environment py27, which its runs create if it does not exist:
python_version = Process("python_version", code="python_version.sh",
conda_envs={"py27": "py27.yml"},
description="Activates the conda environment py27 and writes the version "
"of its Python into a file.")
A path given to conda_envs= or docker_imgs= is a method file
with the Bash body instead, as for the other methods.
A tasks file and a skip file in Python run with the Python of DeBasher, not inside the Conda environment or the Docker image of the process, so the packages they import are those of that Python.
A resident program
A resident program (see the Resident DeBasher Programs Section) is a Program
with resident=True that adds nodes instead of processes. The running
sum of that Section, shipped as sdk_running_sum, has one node and a
Supervisor:
from debasher import Node, Program, Supervisor
acc = Node("Accumulate", code="accumulate.py", initiator=True,
description="Keeps the running sum of the numbers it receives, and sends each new sum out.")
acc.fifo_input("-numbers", "string", "numbers written from outside the program, one per message",
"numbers", external=True)
acc.fifo_output("-outsum", "string", "the running sum after each number, read outside the program", "sum")
sup = Supervisor("Sup", description="Watches Accumulate and relaunches it if it goes down.")
prg = Program(
"sdk_running_sum",
resident=True,
description="Example described with the SDK: a resident program whose node keeps the running "
"sum of the numbers written into its external input, and writes each new sum out.",
)
prg.add(acc, cpus=1, mem=256, time="01:00:00")
prg.add(sup, cpus=1, mem=256, time="01:00:00")
A Node is given as a Python file with the class of the node,
accumulate.py, written as the node code editor of the web interface
shows it: the import of its node kind, the class derived from
FBPProcess, ProgramLauncher or DirectoryWatcher with its
hooks, and the line that runs it. initiator=True makes the node an
initiator, the node where a round starts (see the Resident DeBasher Programs
Section). The build splits the file into the parts that the editor
keeps, so a file that the editor could not hold is refused with what to
change. Its ports are its options: an external
input, written from outside the program, is fifo_input(...,
external=True), and an output is fifo_output. The Supervisor
is added without a file, since the module writes its whole class and
the channels between it and the nodes. The specifications of a node
(input_log_max_mb, heartbeat_timeout_s, …) go to add with
the others.
What the SDK does not do
It does not run a program:
debasher_execdoes, from the command line or from the editor, so that building a program never executes it.It does not read a program: the way from a module or from the editor into Python does not exist. A program edited and saved in the editor keeps its Python file as it was; the code of a process edited there goes into the file that the description names, since the topology file names the same file, and survives the next build, while a change to the options, the connections or the specifications goes into
program.yamlonly and is written over by the next build.It shapes no tasks itself: a tasks file in Python is run by the engine when it prepares a run, and only shapes the tasks; the values of the options, the preamble and the bodies of
post,reset_outfilesandoutdir_basenamestay Bash, as the engine reads them. The manual mode of the editor, a wholedefine_optswritten by hand, is not offered: a program that needs it is written as a module (see the Writing a Module in Bash Section).