Command
Command tasks run subprocesses. The simplest way is to use quickie.command() decorator to define a task.
The decorated function should return either a string of command and arguments in POSIX shell format, or a list of strings.
from quickie import command
@command
def command_from_string():
return "my_command arg1 arg2"
@command
def command_from_list():
return ["my_command", "arg1", "arg2"]
To create a command from a class, inherit from quickie.tasks.Command and replace the quickie.tasks.Command.get_cmd() method.
Or, replace the quickie.tasks.Command.get_binary() and quickie.tasks.Command.get_cmd_args() methods.
For example the followings are all equivalent:
from quickie import Command
@task
class SomeCommand(Command):
def get_cmd(self):
return "my_command arg1 arg2" # or ["my_command", "arg1", "arg2"]
@task
class SomeCommand(Command):
def get_binary(self):
return "my_command"
def get_cmd_args(self):
return "arg1 arg2" # or ["arg1", "arg2"]
Environment variables
Pass per-task environment variables with env:
from quickie import command
@command(env={"API_KEY": "secret"})
def deploy():
return ["my_deploy_tool"]
To load variables from a .env file, use env_file:
from quickie import command
@command(env_file=".env")
def deploy():
return ["my_deploy_tool"]
Relative paths are resolved from the quickie tasks root directory (the folder
that contains _qk/). Absolute paths are used as-is.
When both env and env_file are supplied, values in env take
precedence over values loaded from the file:
@command(env_file=".env", env={"API_KEY": "override"})
def deploy():
return ["my_deploy_tool"]
The same attributes are available on subclasses:
class Deploy(Command):
env_file = ".env"
env = {"API_KEY": "override"}
You can also import a .env file as a plain dict independently of any task,
for example to inspect values or set a global context from your _qk/__init__.py:
from quickie import load_env_file, Context, app
env = load_env_file(".env") # returns dict[str, str]
# Or build a Context from a .env file and apply it globally:
app.set_context(Context.from_env_file(".env"))
Exit codes
Command tasks validate subprocess exit codes strictly by default. Exit code 0 is accepted, and any other exit code raises an error that exits the CLI with the same code.
If a command is expected to return a non-zero code, allow it explicitly with expected_exit_codes:
from quickie import command
@command(expected_exit_codes=(0, 2))
def grep_with_no_match_allowed():
return ["grep", "-q", "pattern", "file.txt"]
The same option is available on subclasses by setting the expected_exit_codes attribute.
To disable exit-code validation completely, set expected_exit_codes to None or ().
Timeout
To limit how long a single subprocess attempt may run, set timeout (in seconds):
from quickie import command
@command(timeout=30)
def long_running():
return ["my_tool", "--process"]
If the subprocess does not finish within the allotted time, a
SubprocessTimeoutError is raised (exit code 124).
The same attribute is available on subclasses.
Retry
To automatically retry a command after a transient failure, set retries to the
number of additional attempts:
from quickie import command
@command(retries=3)
def flaky_network_call():
return ["curl", "https://example.com/api"]
An optional retry_delay (in seconds) can be added to wait between attempts:
@command(retries=3, retry_delay=2.0)
def flaky_network_call():
return ["curl", "https://example.com/api"]
Retries are triggered by both SubprocessExitCodeError and
SubprocessTimeoutError. If all attempts fail, the last
exception is re-raised. A warning is logged for each failed attempt and each retry.
Both attributes are available on subclasses.
Capturing output
By default, command output streams directly to the terminal. Use
output_mode to control this behaviour:
"stream"(default) — output goes to the terminal; nothing is captured."capture"— output is captured as bytes; nothing is printed."tee"— output streams to the terminal and is captured.
Both OutputMode enum values and plain string literals
are accepted interchangeably:
from quickie import command, task, OutputMode
# Using the enum
@command(output_mode=OutputMode.CAPTURE)
def get_version_enum():
return ["my_tool", "--version"]
# Using a string literal — equivalent to the above
@command(output_mode="capture")
def get_version_str():
return ["my_tool", "--version"]
# Tee — terminal sees output and it is also available afterwards
@command(output_mode="tee")
def verbose_build():
return ["make", "all"]
@task
def print_version():
result = get_version_str() # CompletedProcess.stdout is bytes
print(result.stdout.decode())
When subclassing, set output_mode as a class attribute using either form:
class MyCommand(Command):
output_mode = OutputMode.CAPTURE # or output_mode = "capture"
See also Calling tasks from within run() for the task composition pattern.