A simple task runner that generates command line interfaces
from cleek import task
@task
def binary_op(x: int, y: int, op: Literal['add', 'sub'] = 'add') -> None: ...⬇️ Becomes ⬇️
$ clk binary-op -h
usage: clk binary-op [-h] [-o {add,sub}] x y
positional arguments:
x
y
options:
-h, --help show this help message and exit
-o, --op {add,sub} default: addSee how I cleek in another project
pip install cleekgit clone https://github.com/petersuttondev/cleek.git
pip install ./cleek- Create a
cleeks.pyfile in the root of your project and add tasks.
from cleek import task
@task
def greet(name: str) -> None:
print(f'Hello, {name}!')- Run
clkfrom anywhere inside your project to see your tasks.
$ clk
┏━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┓
┃ Task ┃ Usage ┃
┡━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━┩
│ greet │ clk greet [-h] name │
└───────┴─────────────────────┘- Run
clk <task> -hto print a task's help.
$ clk greet
usage: clk greet [-h] name
positional arguments:
name
options:
-h, --help show this help message and exit- Run a task.
$ clk greet Peter
Hello, Peter!Set a task's name:
from cleek import task
@task('bar')
def foo() -> None:
print('foo function, bar task')$ clk bar
foo function, bar taskSet a task's group:
from cleek import task
@task(group='foo')
def bar() -> None:
print('bar task in the foo group')$ clk foo.bar
bar task in the foo groupSet a task's style. Used when listing tasks. See Rich's Style documentation for supported styles.
from cleek import task
@task(style='red')
def foo() -> None:
print("I'll be red if you run clk")To apply the same customization to many tasks, use customize() to create a
pre-configured version of the task decorator.
from cleek import customize
foo_task = customize('foo', style='red')
@foo_task
def a() -> None: ...
@foo_task
def b() -> None: ...
bar_task = customize('bar', style='blue')
@bar_task
def c() -> None: ...
@bar_task
def d() -> None: ...$ clk
┏━━━━━━━┳━━━━━━━━━━━━━━━━┓
┃ Task ┃ Usage ┃
┡━━━━━━━╇━━━━━━━━━━━━━━━━┩
│ foo.a │ clk foo.a [-h] │
│ foo.b │ clk foo.b [-h] │
│ bar.c │ clk bar.c [-h] │
│ bar.d │ clk bar.d [-h] │
└───────┴────────────────┘Sometimes it's useful to the directory your cleeks live in to be on the Python
path. For example, if your tasks use multiprocessing. cleek can do that for
you:
from cleek import config, task
config(prepend_to_path=True)
@task
def foo() -> None: ...Shell completion is provided by argcomplete:
-
Setup
argcompletefollowing their installation instructions -
Add
eval "$(register-python-argcomplete clk)"to your shell configuration.
Your tasks can be async functions:
from cleek import task
import trio
@task
async def sleep(duration: float = 1.0) -> None:
print(f'Sleeping for {duration} seconds')
await trio.sleep(duration)At the moment, trio is the only supported event loop. If want to use another
event loop (I'm guessing asyncio), open an issue and I'll add it.
-
If the environmental variable
CLEEKS_PATHis set,clktreats the value as a path and attempts to load it. If the load fails,clkfails. -
clksearches upwards from the current working directory towards the root directory/, looking for acleeks.pyscript or acleekspackage. A script takes precedence over a package if both are found in the same directory.
If you get an error saying your task's parameters are not supported, open an issue containing the function signature and I'll add support.
Keyword bool with False or True default
def foo(a: bool = False): ...
def foo(a: bool = True): ...Keyword optional bool with False, True, or None default
def foo(a: bool | None = False): ...
def foo(a: bool | None = True): ...
def foo(a: bool | None = None): ...Positional str
def foo(a: str): ...Positional optional str
def foo(a: str | None): ...Keyword str with str default
def foo(a: str = 'a'): ...Keyword optional str with str or None default
def foo(a: str | None = 'a'): ...
def foo(a: str | None = None): ...Variadic positional str
def foo(*a: str): ...Positional int
def foo(a: int): ...Keyword int with int default
def foo(a: int = 1): ...Poitisonal optinal int
def foo(a: int | None): ...Keyword optional int with int or None default
def foo(a: int | None = 1): ...
def foo(a: int | None = None): ...Positional float
def foo(a: float): ...Keyword float with float default
def foo(a: float = 1.0): ...Keyword optional float with float or None default
def foo(a: float | None = 1.0): ...
def foo(a: float | None = None): ...Positional int literal
@task
def foo(a: Literal[1, 2, 3]): ...Positional str literal
@task
def foo(a: Literal['a', 'b', 'c']): ...Positional only str literal
@task
def foo(a: Literal['a', 'b', 'c'], /): ...Positional only str literal with str default
@task
def foo(a: Literal['a', 'b', 'c'] = 'a', /): ...Keyword int literal with int default
@task
def foo(a: Literal[1, 2, 3] = 1): ...Keyword str literal with str default
@task
def foo(a: Literal['a', 'b', 'c'] = 'a'): ...Positional pathlib.Path or pathlib.PurePosixPath
from pathlib import Path, PurePosixPath
@task
def foo(a: Path): ...
@task
def bar(b: PurePosixPath): ...Keyword optional pathlib.Path or pathlib.PurePosixPath with None default
from pathlib import Path
@task
def foo(a: Path | None = None): ...
@task
def bar(a: PurePosixPath | None = None): ...Variadic positional pathlib.Path
from pathlib import Path
@task
def foo(*a: Path): ...Variadic positional trio.Path
from trio import Path
@task
def foo(*a: Path): ...