Skip to content

Interfaces

BaseInterface

Base class for the pyclif service layer. Subclass it to group all data-access and business-logic operations for a resource. Declare a renderers dict to associate each method with its renderer, then call respond() from commands — it handles list vs generator detection, renderer selection, and Response construction automatically.

pyclif.BaseInterface

Base class for pyclif service-layer interfaces.

Subclass and declare a renderers dict to associate each method with its renderer. Call respond() from commands — it handles list vs generator detection, renderer selection, and Response construction automatically.

Class attributes

renderers: Maps method names to renderer classes. Missing keys fall back to renderer_class. renderer_class: Default renderer is used when a method has no entry in renderers.

Example

class ArticleInterface(BaseInterface): renderers = { "list": ArticleListRenderer, "create": ArticleCreateRenderer, }

def list(self) -> list[OperationResult]:
    ...

def create(self, title: str) -> list[OperationResult]:
    ...
Source code in src/pyclif/core/interfaces/base.py
class BaseInterface:
    """Base class for pyclif service-layer interfaces.

    Subclass and declare a renderers dict to associate each method with its
    renderer. Call respond() from commands — it handles list vs generator
    detection, renderer selection, and Response construction automatically.

    Class attributes:
        renderers: Maps method names to renderer classes. Missing keys fall
            back to renderer_class.
        renderer_class: Default renderer is used when a method has no entry in
            renderers.

    Example:
        class ArticleInterface(BaseInterface):
            renderers = {
                "list": ArticleListRenderer,
                "create": ArticleCreateRenderer,
            }

            def list(self) -> list[OperationResult]:
                ...

            def create(self, title: str) -> list[OperationResult]:
                ...
    """

    renderers: ClassVar[dict[str, type[BaseRenderer]]] = {}
    renderer_class: ClassVar[type[BaseRenderer]] = BaseRenderer

    def __init__(self, ctx: object) -> None:
        """Store the CLI context for use in interface methods.

        Args:
            ctx: The pyclif CLI context passed from the command.
        """
        self.ctx = ctx

    def respond(self, method_name: str, *args: object, **kwargs: object) -> Response:
        """Call a method and wrap its output in a Response with the right renderer.

        Auto-detects whether the method returns a list or a generator and
        picks from_results() vs from_stream() accordingly.

        An AttributeError raised by getattr is intentionally not caught —
        a wrong method_name is a programming error, not a business failure.

        Args:
            method_name: Name of an interface method to call.
            *args: Positional arguments forwarded to the method.
            **kwargs: Keyword arguments forwarded to the method.

        Returns:
            A Response ready for the framework to dispatch to the renderer.
        """
        # Lazy import — breaks the circular dependency between interfaces/base.py
        # and output/responses.py (Response imports nothing from interfaces, but
        # keeping this at module level would require responses.py to import
        # BaseInterface which would close the cycle).
        from pyclif.core.output.responses import Response  # noqa: PLC0415

        method = getattr(self, method_name)
        renderer_cls = self.renderers.get(method_name, self.renderer_class)
        renderer = renderer_cls()
        result = method(*args, **kwargs)

        if inspect.isgenerator(result):
            return Response.from_stream(result, renderer=renderer)

        return Response.from_results(
            result,
            success_message=renderer.get_success_message(result),
            failure_message=renderer.get_failure_message(result),
            renderer=renderer,
        )

__init__(ctx)

Store the CLI context for use in interface methods.

Parameters:

Name Type Description Default
ctx object

The pyclif CLI context passed from the command.

required
Source code in src/pyclif/core/interfaces/base.py
def __init__(self, ctx: object) -> None:
    """Store the CLI context for use in interface methods.

    Args:
        ctx: The pyclif CLI context passed from the command.
    """
    self.ctx = ctx

respond(method_name, *args, **kwargs)

Call a method and wrap its output in a Response with the right renderer.

Auto-detects whether the method returns a list or a generator and picks from_results() vs from_stream() accordingly.

An AttributeError raised by getattr is intentionally not caught — a wrong method_name is a programming error, not a business failure.

Parameters:

Name Type Description Default
method_name str

Name of an interface method to call.

required
*args object

Positional arguments forwarded to the method.

()
**kwargs object

Keyword arguments forwarded to the method.

{}

Returns:

Type Description
Response

A Response ready for the framework to dispatch to the renderer.

Source code in src/pyclif/core/interfaces/base.py
def respond(self, method_name: str, *args: object, **kwargs: object) -> Response:
    """Call a method and wrap its output in a Response with the right renderer.

    Auto-detects whether the method returns a list or a generator and
    picks from_results() vs from_stream() accordingly.

    An AttributeError raised by getattr is intentionally not caught —
    a wrong method_name is a programming error, not a business failure.

    Args:
        method_name: Name of an interface method to call.
        *args: Positional arguments forwarded to the method.
        **kwargs: Keyword arguments forwarded to the method.

    Returns:
        A Response ready for the framework to dispatch to the renderer.
    """
    # Lazy import — breaks the circular dependency between interfaces/base.py
    # and output/responses.py (Response imports nothing from interfaces, but
    # keeping this at module level would require responses.py to import
    # BaseInterface which would close the cycle).
    from pyclif.core.output.responses import Response  # noqa: PLC0415

    method = getattr(self, method_name)
    renderer_cls = self.renderers.get(method_name, self.renderer_class)
    renderer = renderer_cls()
    result = method(*args, **kwargs)

    if inspect.isgenerator(result):
        return Response.from_stream(result, renderer=renderer)

    return Response.from_results(
        result,
        success_message=renderer.get_success_message(result),
        failure_message=renderer.get_failure_message(result),
        renderer=renderer,
    )

Full example

1. Declare the renderer

from pyclif import BaseRenderer


class ArticleRenderer(BaseRenderer):
    fields = ["id", "title", "author", "published"]  # JSON / YAML / raw
    columns = ["id", "title", "author"]               # table columns
    rich_title = "Articles"
    success_message = "Articles retrieved."
    failure_message = "Failed to retrieve articles."


class ArticleCreateRenderer(BaseRenderer):
    fields = ["item", "action", "success"]
    columns = ["item", "action"]
    rich_title = "Create Article"
    success_message = "Article created."
    failure_message = "Article creation failed."

2. Implement the interface

Interface methods return list[OperationResult] for batch operations, or Iterator[OperationResult] for generators (streaming). They never raise for expected business failures — return OperationResult.error() instead.

from collections.abc import Iterator
from pyclif import BaseInterface, OperationResult


class ArticleInterface(BaseInterface):
    renderers = {
        "list_articles": ArticleRenderer,
        "create_article": ArticleCreateRenderer,
        "import_articles": ArticleCreateRenderer,
    }

    def list_articles(self) -> list[OperationResult]:
        """Return all articles as a list of results."""
        articles = self._db.all()
        return [
            OperationResult.ok(a.id, data={"id": a.id, "title": a.title, "author": a.author})
            for a in articles
        ]

    def create_article(self, title: str, author: str) -> list[OperationResult]:
        """Create a single article."""
        if self._db.exists(title=title):
            return [OperationResult.error(title, f"'{title}' already exists.", error_code=2)]
        article = self._db.create(title=title, author=author)
        return [OperationResult.ok(article.id, data={"id": article.id, "action": "created"})]

    def import_articles(self, paths: list[str]) -> Iterator[OperationResult]:
        """Import articles from files — yields one result per file for live output."""
        for path in paths:
            try:
                article = self._import_file(path)
                yield OperationResult.ok(path, data={"id": article.id, "action": "created"})
            except ValueError as exc:
                yield OperationResult.error(path, str(exc))

3. Write the commands

Commands are thin views. Call respond() and return the result — no try/except, no renderer wiring.

from pyclif import Response, argument, command, option, pass_context

from .interfaces import ArticleInterface


@command()
@pass_context
def list_articles(ctx) -> Response:
    """List all articles."""
    return ArticleInterface(ctx).respond("list_articles")


@command()
@argument("title")
@option("--author", required=True)
@pass_context
def create_article(ctx, title: str, author: str) -> Response:
    """Create a new article."""
    return ArticleInterface(ctx).respond("create_article", title, author)

For streaming commands (generator methods), respond() automatically wraps the generator in Response.from_stream():

@command()
@argument("paths", nargs=-1)
@pass_context
def import_articles(ctx, paths: tuple[str, ...]) -> Response:
    """Import articles from files."""
    return ArticleInterface(ctx).respond("import_articles", list(paths))

With --output-format rich, the framework drives a Live context and calls renderer.rich_on_item() after each yielded result. With all other formats the stream is materialized first, then dispatched normally.

4. Default renderer fallback

When a method has no entry in renderers, renderer_class is used:

class ArticleInterface(BaseInterface):
    renderer_class = ArticleRenderer   # fallback for methods not in renderers
    renderers = {
        "create_article": ArticleCreateRenderer,
    }
    # list_articles, import_articles, etc. will use ArticleRenderer automatically