Skip to content

Multi-integration Commands

Some commands coordinate two or more interfaces — for example, validating a user exists before creating a task assigned to them, or provisioning a resource and immediately registering it elsewhere.

The key is knowing when to call respond() and when to call an interface method directly.

respond() vs direct method calls

respond() is a one-shot helper: one method, one renderer, one Response. When you need results from several interfaces, call their methods directly — each returns a plain list[OperationResult] — then aggregate with Response.from_results().

respond("method")          →  calls method, wraps in Response, attaches renderer
interface.method()         →  just calls the method, returns list[OperationResult]
Response.from_results([])  →  builds a Response from a combined list

Pattern 1 — Sequential with guard

Validate a precondition with the first interface. If it fails, return early without calling the second interface.

Scenario: add a task and assign it to a user, but reject the command if the assignee does not exist.

# tasks/commands/add_assigned.py
from pyclifer import Response, command, option
from ..context import pass_my_context
from ..interfaces import TaskInterface
from ...users.interfaces import UserInterface
from ...users.renderers import UserNotFoundRenderer


@command()
@option("--title", required=True, help="Task title.")
@option("--assignee", required=True, help="Username to assign the task to.")
@pass_my_context
def add_assigned(ctx, title: str, assignee: str) -> Response:
    """Add a task and assign it to an existing user."""
    # Step 1 — validate the assignee exists
    user_results = UserInterface(ctx).list_users()
    known = {r.item for r in user_results if r.success}
    if assignee not in known:
        return Response.from_results(
            [next(r for r in user_results if r.item == assignee or not r.success)],
            failure_message=f"User '{assignee}' not found.",
            renderer=UserNotFoundRenderer(),
        )

    # Step 2 — create the task (user is confirmed to exist)
    return TaskInterface(ctx).respond("add_task", title=title, assignee=assignee)

Call interface.method() directly to get the raw list[OperationResult] without building a Response. Inspect the results, then decide whether to proceed or return early.

Pattern 2 — Sequential with aggregation

Run both interfaces and combine their results into a single Response that reflects the overall outcome.

Scenario: provision a new user — create the user record and immediately create a welcome task assigned to them. Both operations appear in the final output.

# users/commands/provision.py
from pyclifer import Response, command, option
from ..context import pass_my_context
from ..interfaces import UserInterface
from ...tasks.interfaces import TaskInterface
from .renderers import ProvisionRenderer


@command()
@option("--username", required=True, help="New username.")
@option("--email", required=True, help="User e-mail address.")
@pass_my_context
def provision(ctx, username: str, email: str) -> Response:
    """Create a user and assign them a welcome task."""
    user_results = UserInterface(ctx).create_user(username=username, email=email)

    task_results = []
    if all(r.success for r in user_results):
        task_results = TaskInterface(ctx).add_task(
            title=f"Welcome, {username}!",
            assignee=username,
        )

    return Response.from_results(
        user_results + task_results,
        success_message=f"User '{username}' provisioned.",
        failure_message=f"Provisioning '{username}' failed.",
        renderer=ProvisionRenderer(),
    )

Response.from_results() sets success=True only if every result in the combined list succeeded. error_code is taken from the first failure.

Handling partial failures

When some operations succeed and others fail, from_results() marks the overall response as failed but preserves all results in data["results"]. The renderer and output format both receive the full list — the table will show successes and failures side by side.

user_results = UserInterface(ctx).create_user(username=username, email=email)
task_results = TaskInterface(ctx).add_task(title="Welcome!", assignee=username)

all_results = user_results + task_results
# success=False if any result failed; error_code from first failure
return Response.from_results(all_results, renderer=ProvisionRenderer())

To stop on the first failure instead of continuing:

user_results = UserInterface(ctx).create_user(username=username, email=email)
if not all(r.success for r in user_results):
    return Response.from_results(user_results, renderer=ProvisionRenderer())

task_results = TaskInterface(ctx).add_task(title="Welcome!", assignee=username)
return Response.from_results(user_results + task_results, renderer=ProvisionRenderer())

Keeping the renderer separate

The renderer passed to Response.from_results() controls how the combined results are displayed. Define one renderer per command rather than reusing a domain renderer:

# users/renderers.py
from pyclifer import BaseRenderer


class ProvisionRenderer(BaseRenderer):
    fields = ["item", "success", "message"]
    columns = ["item", "success", "message"]
    success_message = "Provisioning complete."
    failure_message = "Provisioning failed."

The item field on each OperationResult carries a human-readable identifier (username, task ID, file path, …). Using ["item", "success", "message"] as columns gives a readable summary across heterogeneous results from different interfaces.

Interfaces used in the examples

The demo app ships TaskInterface and UserInterface as independent building blocks:

See also