Quickstart

This page will guide you through the process of creating a new Quickie project.

Where to define tasks

Tasks can be defined in a __quickie Python module, be it a single file or a package. It does not need to be defined in the current working directory, Quickie will also look across parent directories until it finds it or reaches the root directory.

For example:

# MyProject/__quickie.py
from quickie import arg, task, script, command

@task
def hello():
    print("Hello, World!")

@script
@arg("--name", help="Your name")
def hello_script(name):
    return f"echo 'Hello, {name}!'"

@command(extra_args=True)
def some_command(*args):
    return ["my_command", *args]

Now you can run the tasks from anywhere in the project, even from a subdirectory.

$ qck hello
Hello, World!

$ qck hello_script --name Alice
Hello, Alice!

$ qck some_command arg1 arg2
my_command arg1 arg2

Defining tasks in a package

For more complex projects, or teams, it is recommended to define tasks in a package. This allows to better organize the tasks and to have private tasks that are not committed to the repository.

For example:

MyProject/
├── __quickie
│   ├── __init__.py
│   ├── public.py
│   ├── private.py  # might not exist   └── ...        # more files
└── ...

Then in the __init__.py file you can import the tasks from the other files.

# MyProject/__quickie/__init__.py
from . import public
try:
    from . import private
except ImportError:
    private = None

NAMESPACES = {  # Namespaces tasks
    "": [public, private],  # private tasks will take precedence
    "private": ,  # private tasks also available under `private` namespace
}

Function based vs Class based tasks

Tasks can be defined as functions, by using one of the task decorators, or as classes by subclassing quickie.tasks.Task or one of its subclasses.

Functions decorated with quickie.task(), quickie.script(), and quickie.command() are equivalent to subclasses of quickie.tasks.Task, quickie.tasks.Script, and quickie.tasks.Command respectively. However, functions are simpler to define and are recommended for most tasks.

Python Tasks

You can define tasks that simply run Python code. The simplest way is to use quickie.task() decorator to define a task:

from quickie import task

@task
def hello():
    print("Hello, World!")

This will return a quickie.tasks.Task instance, equivalent to:

from quickie import Task

class Task(Task):
    def run(self):
        print("Hello, World!")

Script Tasks

Specialized subclass of tasks that run shell scripts. The simplest way is to use quickie.script() decorator to define a task:

from quickie import script

@script
def hello_script():
    return "echo 'Hello, World!'"

This will return a quickie.tasks.Script instance, equivalent to:

from quickie import Script

class HelloScript(Script):
    def get_script(self):
        return "echo 'Hello, World!'"

Command Tasks

Command tasks run subprocesses. The simplest way is to use quickie.command() decorator to define a task:

from quickie import command

@command
def some_command():
    return ["my_command", "arg1", "arg2"]

This will return a quickie.tasks.Command instance, equivalent to:

from quickie import Command

class SomeCommand(Command):
    def get_cmd(self):
        return ["my_command", "arg1", "arg2"]

GroupTasks

Group tasks are used to run multiple tasks in order.

The simplest way is to use the quickie.group() decorator to define a group task:

from quickie import group

@task
def task1():
    print("Task 1")

@task
def task2():
    print("Task 2")

@group
def my_group():
    return [
        task1,
        task2,
    ]

This will return a quickie.tasks.Group instance, equivalent to:

from quickie import Group

class MyGroup(Group):
    def get_tasks(self):
        return [
            task1,
            task2,
        ]

ThreadGroupTasks

Thread group tasks are used to run multiple tasks in parallel.

The simplest way is to use the quickie.thread_group() decorator to define a thread group task:

from quickie import thread_group

@task
def task1():
    print("Task 1")

@task
def task2():
    print("Task 2")

@thread_group
def my_thread_group():
    return [
        task1,
        task2,
    ]

This will return a quickie.tasks.ThreadGroup instance, equivalent to:

from quickie import ThreadGroup

class MyThreadGroup(ThreadGroup):
    def get_tasks(self):
        return [
            task1,
            task2,
        ]

Changing the task name

By default the task name is the function/class name. You can change the task name, or add aliases, by passing the name argument to the task decorator.

from quickie import task

@task(name="my_task")
def task1():
    print("Task 1")

@task(name=["task2", "t2"])
def task2():
    print("Task 2")

# This will run task1
qck my_task

# These will run task2
qck task2
qck t2

Equivalent to:

from quickie import Task

class MyTask(Task, name="my_task"):
    pass

class Task2(Task, name=["task2", "t2"]):
    pass

Arguments

You can define arguments for your tasks using the quickie.arg() decorator. This will add the argument to the task’s signature and make it available as a keyword argument. In addition, you can pass extra_args=True to the task decorator to allow unknown arguments to be passed to the task.

from quickie import arg, task

@task
@arg("--name", help="Your name")
def hello(name):
    print(f"Hello, {name}!")

@task(extra_args=True)
@arg("--flag", help="A flag", action="store_true")
def hello_extra(*args, flag=False):
    print(f"{args=}, {flag=}")

Under the hood each Task defines an argparse.ArgumentParser instance. By using the arg decorator we call the argparse.ArgumentParser.add_argument method with the provided arguments. The exception is the completer argument, which is used for auto completion.

Please refer to argparse.ArgumentParser.add_argument for more information on the available arguments.

Conditions

Conditions are used to determine if a task should run or not. They can be chained together with logical operators to create complex conditions.

Logical operators:

  • & for AND

  • | for OR

  • ~ for NOT

  • ^ for XOR

from quickie import task, conditions

@task(condition[conditions.FirstRun() & conditions.PathsExist("file1", "file2"))
def some_task():
    print("This task will run only the first time and if both files exist.")

You can use built-in conditions from quickie.conditions or create your own by subclassing quickie.conditions.base.BaseCondition and implementing the quickie.conditions.base.BaseCondition.__call__() method, which should return True if the condition passes, False otherwise. Additionally the quickie.conditions.base.BaseCondition.__call__() must accept the task as an argument, and the arguments passed to the task.

Additionally you can use the quickie.conditions.condition() decorator to create a condition from a function.

Private tasks

You can define private tasks by prefixing the task name with an underscore.

from quickie import task

@task
def _private_task():
    print("Private task")

Roughly equivalent to:

from quickie import Task

class _PrivateTask(Task):
    def run(self):
        print("Private task")

Sometimes however, it is useful to create base task classes that are not meant to be run directly. This can be achieved by setting private=True when defining the class.

from quickie import Task

class BaseTask(Task, private=True):
    pass