Skip to content

About

A set of API extension to Invoke API to enhance CLI writing

Resources

Contributing

Stars

3 stars

Watchers

2 watching

Forks

Repository files navigation

invoke-toolkit

A set of extensions for rich output, more options in collection/config discovery through entry-points.

This extends the Collection from Invoke so it can create automatically collections.

PyPI - Version PyPI - Python Version


Table of Contents

Features

  • Task discovery by namespace for extendable/composable CLIs
  • Discovery to plain old tasks.py (or any other name)
  • Local tasks discovery from local_tasks.py in the current directory
  • Integration with stand alone binaries for specific tasks
  • Task result caching with TTL support via diskcache (optional)
  • Future Download binaries

Do I need this package

If you have...

  • Used invoke for a while and...
  • Have a large tasks.py that needs to be modularized
  • Have a lot of copy/pasted code in multiple tasks.py across multiple repos.
  • Have exceeded the approach of a repository cloned as ~/tasks/ with more .py files that you want to manage.
  • Or you want to combine various tasks defined in multiple directories
  • You want to create a zipped (shiv) redistribute script for container environments like Kubernetes based CI environments with only requiring the Python interpreter.

Installation

pip install invoke-toolkit

Quick Start

Using Local Tasks

Create a local_tasks.py file in your project directory with your tasks:

from invoke_toolkit import task

@task()
def my_task(ctx):
    """Do something useful"""
    print("Hello from local tasks!")

Then run it with:

intk local.my-task

Local tasks are automatically discovered and added to the local namespace, allowing you to keep project-specific tasks separate from your main task collection.

Using Task Caching

Cache expensive task results with the cache parameter:

from invoke_toolkit import task

# Simple caching (no expiration)
@task(cache=True)
def expensive_task(ctx, param: str) -> str:
    """Results are cached across invocations."""
    return do_expensive_computation(param)

# Caching with TTL (1 hour)
@task(cache={"ttl": 3600})
def cached_task(ctx, name: str) -> dict:
    """Results cached for 1 hour."""
    return fetch_data(name)

# Caching with ignored arguments
@task(cache={"ttl": 600, "ignore_args": ["verbose"]})
def cached_with_options(ctx, query: str, verbose: bool = False) -> list:
    """Cache key ignores verbose flag."""
    return search(query, verbose=verbose)

Cache features:

  • Cache location is computed from git repository root + platformdirs
  • Debug logging (-d flag) shows cache hits/misses
  • Graceful degradation when diskcache is not installed

To enable caching, install with the cache extra:

pip install invoke-toolkit[cache]

Dynamic task defaults

Use Field when an argument default must be computed from the final task context or resolved from a URI. Explicit command-line values always take precedence.

Bind a resolver once, then reuse the resulting callable for scalar and file defaults. A local resolver always returns one string value per request. The resolver receives every URI using that callback and scheme in one batch.

from invoke_toolkit import Context, Field, FilePath, task


def resolve_bw(ctx: Context, requests: list) -> dict[str, str]:
    return {
        request.parameter: ctx.run("bw get password ...", hide=True).stdout.strip()
        for request in requests
    }


BitwardenField = Field(resolver=resolve_bw)
ExistingFile = FilePath(exists=True, dir_okay=False)


@task
def deploy(
    ctx: Context,
    password: str = BitwardenField(default="bw://PASSWORD_ID"),
    config: ExistingFile = BitwardenField(
        default="bw://CONFIG_ID", cleanup="task"
    ),
) -> None:
    ...

For str fields, the resolver string reaches the task unchanged. For Path or FilePath fields, Field.create_temporary_file() writes that string to a managed temporary file and passes its Path to the task. Override that method in a Field subclass to control file creation; resolvers do not return paths.

For omitted arguments, lookup precedence is explicit CLI/Python/call value, then INVOKE_<PARAMETER> environment value, then the resolved ctx.config value, then the declared Field default or factory. URI values from config use the same local or entry-point resolver dispatch as declared URI defaults.

Field(default_factory=callback) runs only if no higher-precedence value is available. Factories and resolvers never run during help, listing, or shell completion; Path/FilePath completion continues to work normally for explicit values.

Temporary Path files from resolver-bound Field instances are owned by invoke-toolkit. cleanup="pipeline" is the default and keeps the file through expanded pre/main/post execution; cleanup="task" removes it when that task returns. Cleanup runs after failures and cancellation.

Installed invoke_toolkit.field_resolver entry points remain supported for compatibility but issue a warning recommending a task-local resolver. Generate a resolver-only provider package with:

intk -x create.package --provider op

Provider packages expose no task collection. Providers return text only; for Path fields invoke-toolkit materializes and cleans the resulting temporary file according to that Field's cleanup lifetime.

uv tool plugins

Plugin management requires a persistent uv tool install invoke-toolkit environment. Plugins are distributions that expose an invoke_toolkit.collection entry point; their package names do not need an invoke-toolkit- prefix.

intk -x plugin.list
intk -x plugin.add 'invoke-toolkit-example>=1'
intk -x plugin.add git+https://github.com/example/invoke-toolkit-example.git
intk -x plugin.add ./invoke-toolkit-example
intk -x plugin.link ./invoke-toolkit-example
intk -x plugin.update --name invoke-toolkit-example
intk -x plugin.update
intk -x plugin.remove invoke-toolkit-example

plugin.add uses uv's --with mode. A local directory is built as a static installation. plugin.link uses --with-editable, so source changes are live. A named update refreshes one non-editable plugin; an update without a name refreshes all non-editable plugins and reports linked plugins as skipped. Registry constraints and pinned Git references remain in effect. Plugin updates do not update invoke-toolkit itself. Shell completion suggests directly installed plugin names for plugin.remove and named plugin.update operations.

To reduce shell-completion latency in environments with expensive collection plugins, set completion.disable_plugins: true in invoke.yaml:

completion:
  disable_plugins: true

INVOKE_COMPLETION_DISABLE_PLUGINS=1 enables the same behavior without changing the configuration file. Completion then skips installed invoke_toolkit.collection entry points while retaining project, local, and built-in -x task suggestions. These settings affect completion only; normal task execution and listing still load plugins.

These commands reject uvx, uv run, project environments, and installations managed by another tool. pipx offers a similar manual workflow:

pipx inject invoke-toolkit PLUGIN
pipx inject invoke-toolkit --editable ./PLUGIN
pipx uninject invoke-toolkit PLUGIN
pipx list --include-injected

pipx accepts registry, local, and Git sources supported by pip. invoke-toolkit does not detect or execute pipx lifecycle operations.

Development

This project utilizes the pre-commit framework, make sure you run:

pre-commit install

With uvx:

uvx --with pre-commit-uv pre-commit install

License

invoke-toolkit is distributed under the terms of the MIT license.

About

A set of API extension to Invoke API to enhance CLI writing

Resources

Contributing

Stars

3 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages