Skip to content

CLI

provide.foundation.cli

TODO: Add module docstring.

Classes

CLIAdapter

Bases: Protocol

Protocol for CLI framework adapters.

Adapters convert framework-agnostic CommandInfo objects into framework-specific CLI commands and groups. This allows the hub to work with any CLI framework without tight coupling.

Currently provides ClickAdapter. Custom adapters for other frameworks (Typer, argparse) can be implemented by following the CLIAdapter protocol.

See: docs/guide/advanced/integration-patterns.md#custom-cli-adapters

Examples:

>>> from provide.foundation.cli import get_cli_adapter
>>> adapter = get_cli_adapter('click')
>>> command = adapter.build_command(command_info)
Functions
build_command
build_command(info: CommandInfo) -> Any

Build framework-specific command from CommandInfo.

Parameters:

Name Type Description Default
info CommandInfo

Framework-agnostic command information

required

Returns:

Type Description
Any

Framework-specific command object (e.g., click.Command)

Raises:

Type Description
CLIBuildError

If command building fails

Source code in provide/foundation/cli/base.py
def build_command(self, info: CommandInfo) -> Any:
    """Build framework-specific command from CommandInfo.

    Args:
        info: Framework-agnostic command information

    Returns:
        Framework-specific command object (e.g., click.Command)

    Raises:
        CLIBuildError: If command building fails

    """
    ...
build_group
build_group(
    name: str,
    commands: list[CommandInfo] | None = None,
    registry: Registry | None = None,
    **kwargs: Any
) -> Any

Build framework-specific command group.

Parameters:

Name Type Description Default
name str

Group name

required
commands list[CommandInfo] | None

List of command names to include (None = all from registry)

None
registry Registry | None

Command registry (defaults to global)

None
**kwargs Any

Framework-specific options

{}

Returns:

Type Description
Any

Framework-specific group object (e.g., click.Group)

Raises:

Type Description
CLIBuildError

If group building fails

Source code in provide/foundation/cli/base.py
def build_group(
    self,
    name: str,
    commands: list[CommandInfo] | None = None,
    registry: Registry | None = None,
    **kwargs: Any,
) -> Any:
    """Build framework-specific command group.

    Args:
        name: Group name
        commands: List of command names to include (None = all from registry)
        registry: Command registry (defaults to global)
        **kwargs: Framework-specific options

    Returns:
        Framework-specific group object (e.g., click.Group)

    Raises:
        CLIBuildError: If group building fails

    """
    ...
ensure_parent_groups
ensure_parent_groups(
    parent_path: str, registry: Registry
) -> None

Ensure all parent groups in path exist.

Creates missing parent groups in the registry. For example, if parent_path is "db.migrate", ensures both "db" and "db.migrate" groups exist.

Parameters:

Name Type Description Default
parent_path str

Dot-notation path to parent (e.g., "db.migrate")

required
registry Registry

Command registry to update

required
Source code in provide/foundation/cli/base.py
def ensure_parent_groups(self, parent_path: str, registry: Registry) -> None:
    """Ensure all parent groups in path exist.

    Creates missing parent groups in the registry. For example, if
    parent_path is "db.migrate", ensures both "db" and "db.migrate"
    groups exist.

    Args:
        parent_path: Dot-notation path to parent (e.g., "db.migrate")
        registry: Command registry to update

    """
    ...

CLIAdapterNotFoundError

CLIAdapterNotFoundError(
    framework: str, package: str | None = None
)

Bases: CLIError

Raised when CLI adapter dependencies are missing.

This error occurs when attempting to use a CLI framework adapter but the required framework package is not installed.

Examples:

>>> # Raises if Click not installed
>>> adapter = get_cli_adapter('click')

Initialize with framework details.

Parameters:

Name Type Description Default
framework str

Name of the CLI framework (e.g., 'click')

required
package str | None

Optional package name to install

None
Source code in provide/foundation/cli/errors.py
def __init__(self, framework: str, package: str | None = None) -> None:
    """Initialize with framework details.

    Args:
        framework: Name of the CLI framework (e.g., 'click')
        package: Optional package name to install

    """
    pkg = package or framework
    super().__init__(
        f"CLI adapter for '{framework}' requires: pip install 'provide-foundation[{pkg}]'",
        code="CLI_ADAPTER_NOT_FOUND",
        framework=framework,
        package=pkg,
    )
    self.framework = framework
    self.package = pkg
Functions

CLIBuildError

CLIBuildError(
    message: str,
    *,
    code: str | None = None,
    context: dict[str, Any] | None = None,
    cause: Exception | None = None,
    **extra_context: Any
)

Bases: CLIError

Raised when CLI command/group building fails.

This error occurs during the conversion of framework-agnostic CommandInfo to framework-specific CLI objects.

Source code in provide/foundation/errors/base.py
def __init__(
    self,
    message: str,
    *,
    code: str | None = None,
    context: dict[str, Any] | None = None,
    cause: Exception | None = None,
    **extra_context: Any,
) -> None:
    self.message = message
    self.code = code or self._default_code()
    self.context = context or {}
    self.context.update(extra_context)
    self.cause = cause
    if cause:
        self.__cause__ = cause
    super().__init__(message)

CLIError

CLIError(
    message: str,
    *,
    code: str | None = None,
    context: dict[str, Any] | None = None,
    cause: Exception | None = None,
    **extra_context: Any
)

Bases: FoundationError

Base error for CLI adapter operations.

Raised when CLI adapter operations fail.

Source code in provide/foundation/errors/base.py
def __init__(
    self,
    message: str,
    *,
    code: str | None = None,
    context: dict[str, Any] | None = None,
    cause: Exception | None = None,
    **extra_context: Any,
) -> None:
    self.message = message
    self.code = code or self._default_code()
    self.context = context or {}
    self.context.update(extra_context)
    self.cause = cause
    if cause:
        self.__cause__ = cause
    super().__init__(message)

CliTestRunner

CliTestRunner()

Test runner for CLI commands using Click's testing facilities.

Source code in provide/foundation/cli/utils.py
def __init__(self) -> None:
    self.runner = CliRunner()
Functions
invoke
invoke(
    cli: Command | Group,
    args: list[str] | None = None,
    input: str | None = None,
    env: dict[str, str] | None = None,
    catch_exceptions: bool = True,
    **kwargs: Any
) -> Result

Invoke a CLI command for testing.

Source code in provide/foundation/cli/utils.py
def invoke(
    self,
    cli: click_types.Command | click_types.Group,
    args: list[str] | None = None,
    input: str | None = None,
    env: dict[str, str] | None = None,
    catch_exceptions: bool = True,
    **kwargs: Any,
) -> Result:
    """Invoke a CLI command for testing."""
    return self.runner.invoke(
        cli,
        args=args,
        input=input,
        env=env,
        catch_exceptions=catch_exceptions,
        **kwargs,
    )
isolated_filesystem
isolated_filesystem() -> object

Context manager for isolated filesystem.

Source code in provide/foundation/cli/utils.py
def isolated_filesystem(self) -> object:
    """Context manager for isolated filesystem."""
    return self.runner.isolated_filesystem()

InvalidCLIHintError

InvalidCLIHintError(hint: str, param_name: str)

Bases: CLIError

Raised when an invalid CLI hint is provided in Annotated.

This error occurs when a parameter uses typing.Annotated with an invalid CLI rendering hint. Valid hints are 'option' and 'argument'.

Examples:

>>> # Valid usage
>>> def cmd(user: Annotated[str, 'option']): ...
>>> # Invalid - will raise InvalidCLIHintError
>>> def cmd(user: Annotated[str, 'invalid']): ...

Initialize with hint and parameter details.

Parameters:

Name Type Description Default
hint str

The invalid hint that was provided

required
param_name str

Name of the parameter with invalid hint

required
Source code in provide/foundation/cli/errors.py
def __init__(self, hint: str, param_name: str) -> None:
    """Initialize with hint and parameter details.

    Args:
        hint: The invalid hint that was provided
        param_name: Name of the parameter with invalid hint

    """
    super().__init__(
        f"Invalid CLI hint '{hint}' for parameter '{param_name}'. Must be 'option' or 'argument'.",
        code="CLI_INVALID_HINT",
        hint=hint,
        param_name=param_name,
    )
    self.hint = hint
    self.param_name = param_name
Functions

Functions

assert_cli_error

assert_cli_error(
    result: Result,
    expected_error: str | None = None,
    exit_code: int | None = None,
) -> None

Assert that a CLI command failed.

Source code in provide/foundation/cli/utils.py
def assert_cli_error(
    result: Result,
    expected_error: str | None = None,
    exit_code: int | None = None,
) -> None:
    """Assert that a CLI command failed."""
    if result.exit_code == 0:
        raise AssertionError(f"Command succeeded unexpectedly\nOutput: {result.output}")

    if exit_code is not None and result.exit_code != exit_code:
        raise AssertionError(f"Wrong exit code.\nExpected: {exit_code}\nActual: {result.exit_code}")

    if expected_error and expected_error not in result.output:
        raise AssertionError(f"Expected error not found.\nExpected: {expected_error}\nActual: {result.output}")

assert_cli_success

assert_cli_success(
    result: Result, expected_output: str | None = None
) -> None

Assert that a CLI command succeeded.

Source code in provide/foundation/cli/utils.py
def assert_cli_success(result: Result, expected_output: str | None = None) -> None:
    """Assert that a CLI command succeeded."""
    if result.exit_code != 0:
        raise AssertionError(
            f"Command failed with exit code {result.exit_code}\n"
            f"Output: {result.output}\n"
            f"Exception: {result.exception}",
        )

    if expected_output and expected_output not in result.output:
        raise AssertionError(
            f"Expected output not found.\nExpected: {expected_output}\nActual: {result.output}",
        )

config_options

config_options(f: F) -> F

Add configuration file options to a Click command.

Adds: - --config/-c: Path to configuration file - --profile/-p: Configuration profile to use

Source code in provide/foundation/cli/decorators.py
def config_options(f: F) -> F:
    """Add configuration file options to a Click command.

    Adds:
    - --config/-c: Path to configuration file
    - --profile/-p: Configuration profile to use
    """
    f = click.option(
        "--config",
        "-c",
        type=click.Path(exists=True, dir_okay=False, path_type=Path),
        default=None,
        envvar="PROVIDE_CONFIG_FILE",
        help="Path to configuration file",
    )(f)
    f = click.option(
        "--profile",
        "-p",
        default=None,
        envvar="PROVIDE_PROFILE",
        help="Configuration profile to use",
    )(f)
    return f

create_cli_context

create_cli_context(**kwargs: Any) -> CLIContext

Create a CLIContext for CLI usage.

Loads from environment, then overlays any provided kwargs.

Parameters:

Name Type Description Default
**kwargs Any

Override values for the context

{}

Returns:

Type Description
CLIContext

Configured CLIContext instance

Source code in provide/foundation/cli/utils.py
def create_cli_context(**kwargs: Any) -> CLIContext:
    """Create a CLIContext for CLI usage.

    Loads from environment, then overlays any provided kwargs.

    Args:
        **kwargs: Override values for the context

    Returns:
        Configured CLIContext instance

    """
    ctx = CLIContext.from_env()
    for key, value in kwargs.items():
        if value is not None and hasattr(ctx, key):
            setattr(ctx, key, value)
    return ctx

echo_error

echo_error(message: str, json_output: bool = False) -> None

Output an error message.

Parameters:

Name Type Description Default
message str

Error message to output

required
json_output bool

Whether to output as JSON

False
Source code in provide/foundation/cli/utils.py
def echo_error(message: str, json_output: bool = False) -> None:
    """Output an error message.

    Args:
        message: Error message to output
        json_output: Whether to output as JSON

    """
    if json_output:
        perr(message, json_key="error")
    else:
        perr(f"✗ {message}", color="red")

echo_info

echo_info(message: str, json_output: bool = False) -> None

Output an informational message.

Parameters:

Name Type Description Default
message str

Info message to output

required
json_output bool

Whether to output as JSON

False
Source code in provide/foundation/cli/utils.py
def echo_info(message: str, json_output: bool = False) -> None:
    """Output an informational message.

    Args:
        message: Info message to output
        json_output: Whether to output as JSON

    """
    if json_output:
        pout(message, json_key="info")
    else:
        pout(f"i {message}")

echo_json

echo_json(data: Any, err: bool = False) -> None

Output data as JSON.

Parameters:

Name Type Description Default
data Any

Data to output as JSON

required
err bool

Whether to output to stderr

False
Source code in provide/foundation/cli/utils.py
def echo_json(data: Any, err: bool = False) -> None:
    """Output data as JSON.

    Args:
        data: Data to output as JSON
        err: Whether to output to stderr

    """
    if err:
        perr(data)
    else:
        pout(data)

echo_success

echo_success(
    message: str, json_output: bool = False
) -> None

Output a success message.

Parameters:

Name Type Description Default
message str

Success message to output

required
json_output bool

Whether to output as JSON

False
Source code in provide/foundation/cli/utils.py
def echo_success(message: str, json_output: bool = False) -> None:
    """Output a success message.

    Args:
        message: Success message to output
        json_output: Whether to output as JSON

    """
    if json_output:
        pout(message, json_key="success")
    else:
        pout(f"✓ {message}", color="green")

echo_warning

echo_warning(
    message: str, json_output: bool = False
) -> None

Output a warning message.

Parameters:

Name Type Description Default
message str

Warning message to output

required
json_output bool

Whether to output as JSON

False
Source code in provide/foundation/cli/utils.py
def echo_warning(message: str, json_output: bool = False) -> None:
    """Output a warning message.

    Args:
        message: Warning message to output
        json_output: Whether to output as JSON

    """
    if json_output:
        perr(message, json_key="warning")
    else:
        perr(f"âš  {message}", color="yellow")

error_handler

error_handler(f: F) -> F

Decorator to handle errors consistently in CLI commands.

Catches exceptions and formats them appropriately based on debug mode and output format.

Source code in provide/foundation/cli/decorators.py
def error_handler(f: F) -> F:
    """Decorator to handle errors consistently in CLI commands.

    Catches exceptions and formats them appropriately based on
    debug mode and output format.
    """

    @functools.wraps(f)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        click.get_current_context()
        debug = kwargs.get("debug", False)
        json_output = kwargs.get("json_output", False)

        try:
            return f(*args, **kwargs)
        except click.ClickException:
            # Let Click handle its own exceptions
            raise
        except KeyboardInterrupt:
            if not json_output:
                click.secho("\nInterrupted by user", fg="yellow", err=True)
            exit_interrupted()
        except Exception as e:
            if debug:
                # In debug mode, show full traceback
                raise

            if json_output:
                error_data = {
                    "error": str(e),
                    "type": type(e).__name__,
                }
                click.echo(json_dumps(error_data), err=True)
            else:
                click.secho(f"Error: {e}", fg="red", err=True)

            exit_error(f"Command failed: {e!s}")

    return wrapper  # type: ignore[return-value]

flexible_options

flexible_options(f: F) -> F

Apply flexible CLI options that can be used at any command level.

Combines logging_options and config_options for consistent control at both group and command levels.

Source code in provide/foundation/cli/decorators.py
def flexible_options(f: F) -> F:
    """Apply flexible CLI options that can be used at any command level.

    Combines logging_options and config_options for consistent
    control at both group and command levels.
    """
    f = logging_options(f)
    f = config_options(f)
    return f

get_cli_adapter

get_cli_adapter(framework: str = 'click') -> CLIAdapter

Get CLI adapter for specified framework.

Parameters:

Name Type Description Default
framework str

CLI framework name ('click', 'typer', etc.)

'click'

Returns:

Type Description
CLIAdapter

CLIAdapter instance for the framework

Raises:

Type Description
CLIAdapterNotFoundError

If framework adapter is not available

ValueError

If framework name is unknown

Examples:

>>> adapter = get_cli_adapter('click')
>>> command = adapter.build_command(command_info)
Source code in provide/foundation/cli/__init__.py
def get_cli_adapter(framework: str = "click") -> CLIAdapter:
    """Get CLI adapter for specified framework.

    Args:
        framework: CLI framework name ('click', 'typer', etc.)

    Returns:
        CLIAdapter instance for the framework

    Raises:
        CLIAdapterNotFoundError: If framework adapter is not available
        ValueError: If framework name is unknown

    Examples:
        >>> adapter = get_cli_adapter('click')
        >>> command = adapter.build_command(command_info)

    """
    if framework == "click":
        try:
            from provide.foundation.cli.click import ClickAdapter

            return ClickAdapter()
        except ImportError as e:
            if "click" in str(e).lower():
                raise CLIAdapterNotFoundError(
                    framework="click",
                    package="cli",
                ) from e
            raise

    raise ValueError(f"Unknown CLI framework: {framework}. Supported frameworks: click")

logging_options

logging_options(f: F) -> F

Add standard logging options to a Click command.

Adds: - --log-level/-l: Set logging verbosity (TRACE, DEBUG, INFO, WARNING, ERROR, CRITICAL) - --log-file: Write logs to file - --log-format: Choose log output format (json, text, key_value)

Source code in provide/foundation/cli/decorators.py
def logging_options(f: F) -> F:
    """Add standard logging options to a Click command.

    Adds:
    - --log-level/-l: Set logging verbosity (TRACE, DEBUG, INFO, WARNING, ERROR, CRITICAL)
    - --log-file: Write logs to file
    - --log-format: Choose log output format (json, text, key_value)
    """
    f = click.option(
        "--log-level",
        "-l",
        type=click.Choice(LOG_LEVELS, case_sensitive=False),
        default=None,
        envvar="PROVIDE_LOG_LEVEL",
        help="Set the logging level",
    )(f)
    f = click.option(
        "--log-file",
        type=click.Path(dir_okay=False, writable=True, path_type=Path),
        default=None,
        envvar="PROVIDE_LOG_FILE",
        help="Write logs to file",
    )(f)
    f = click.option(
        "--log-format",
        type=click.Choice(["json", "text", "key_value"], case_sensitive=False),
        default="key_value",
        envvar="PROVIDE_LOG_FORMAT",
        help="Log output format",
    )(f)
    return f

output_options

output_options(f: F) -> F

Add output formatting options to a Click command.

Adds: - --json: Output in JSON format - --no-color: Disable colored output - --no-emoji: Disable emoji in output

Source code in provide/foundation/cli/decorators.py
def output_options(f: F) -> F:
    """Add output formatting options to a Click command.

    Adds:
    - --json: Output in JSON format
    - --no-color: Disable colored output
    - --no-emoji: Disable emoji in output
    """
    f = click.option(
        "--json",
        "json_output",
        is_flag=True,
        default=None,
        envvar="PROVIDE_JSON_OUTPUT",
        help="Output in JSON format",
    )(f)
    f = click.option(
        "--no-color",
        is_flag=True,
        default=False,
        envvar="PROVIDE_NO_COLOR",
        help="Disable colored output",
    )(f)
    f = click.option(
        "--no-emoji",
        is_flag=True,
        default=False,
        envvar="PROVIDE_NO_EMOJI",
        help="Disable emoji in output",
    )(f)
    return f

pass_context

pass_context(f: F) -> F

Decorator to pass the foundation CLIContext to a command.

Creates or retrieves a CLIContext from Click's context object and passes it as the first argument to the decorated function.

Source code in provide/foundation/cli/decorators.py
def pass_context(f: F) -> F:
    """Decorator to pass the foundation CLIContext to a command.

    Creates or retrieves a CLIContext from Click's context object
    and passes it as the first argument to the decorated function.
    """

    @functools.wraps(f)
    @click.pass_context
    def wrapper(ctx: click_types.Context, *args: Any, **kwargs: Any) -> Any:
        # Get or create foundation context
        _ensure_cli_context(ctx)

        # Update context from command options
        _update_context_from_kwargs(ctx.obj, kwargs)

        # Remove CLI options from kwargs to avoid duplicate arguments
        _remove_cli_options_from_kwargs(kwargs)

        return f(ctx.obj, *args, **kwargs)

    return wrapper  # type: ignore[return-value]

setup_cli_logging

setup_cli_logging(
    ctx: CLIContext, reinit_logging: bool = True
) -> None

Setup logging for CLI applications using a CLIContext object.

This function is the designated way to configure logging within a CLI application built with foundation. It uses the provided context object to construct a full TelemetryConfig and initializes the system.

Parameters:

Name Type Description Default
ctx CLIContext

The foundation CLIContext, populated by CLI decorators.

required
reinit_logging bool

Whether to force re-initialization of logging (default: True). Set to False when embedding Foundation in a host application to avoid clobbering the host's logging configuration.

True
Source code in provide/foundation/cli/utils.py
def setup_cli_logging(
    ctx: CLIContext,
    reinit_logging: bool = True,
) -> None:
    """Setup logging for CLI applications using a CLIContext object.

    This function is the designated way to configure logging within a CLI
    application built with foundation. It uses the provided context object
    to construct a full TelemetryConfig and initializes the system.

    Args:
        ctx: The foundation CLIContext, populated by CLI decorators.
        reinit_logging: Whether to force re-initialization of logging (default: True).
            Set to False when embedding Foundation in a host application to avoid
            clobbering the host's logging configuration.

    """
    console_formatter = "json" if ctx.json_output else ctx.log_format

    logging_config = LoggingConfig(
        default_level=ctx.log_level,  # type: ignore[arg-type]
        console_formatter=console_formatter,  # type: ignore[arg-type]
        omit_timestamp=False,
        logger_name_emoji_prefix_enabled=not ctx.no_emoji,
        das_emoji_prefix_enabled=not ctx.no_emoji,
        log_file=ctx.log_file,
    )

    telemetry_config = TelemetryConfig(
        service_name=ctx.profile,
        logging=logging_config,
    )

    hub = get_hub()
    hub.initialize_foundation(config=telemetry_config, force=reinit_logging)

standard_options

standard_options(f: F) -> F

Apply all standard CLI options.

Combines logging_options, config_options, and output_options.

Source code in provide/foundation/cli/decorators.py
def standard_options(f: F) -> F:
    """Apply all standard CLI options.

    Combines logging_options, config_options, and output_options.
    """
    f = logging_options(f)
    f = config_options(f)
    f = output_options(f)
    return f

version_option

version_option(
    version: str | None = None, prog_name: str | None = None
) -> Callable[[F], F]

Add a --version option to display version information.

Parameters:

Name Type Description Default
version str | None

Version string to display

None
prog_name str | None

Program name to display

None
Source code in provide/foundation/cli/decorators.py
def version_option(version: str | None = None, prog_name: str | None = None) -> Callable[[F], F]:
    """Add a --version option to display version information.

    Args:
        version: Version string to display
        prog_name: Program name to display

    """

    def decorator(f: F) -> F:
        return click.version_option(
            version=version,
            prog_name=prog_name,
            message="%(prog)s version %(version)s",
        )(f)

    return decorator