Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions lib/python/base_cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ The package follows these rules:
- **Base-aware, Click-compatible**: command authors keep using familiar Click
concepts such as options and arguments.

## Public API

The supported facade is `import base_cli`. It exports the command lifecycle
(`App`, `Context`, `run_app`, decorators, and logging helpers), command filters,
the structured command protocol helpers, and the user configuration types used
by `Context.user_config`. The corresponding modules are also available as
`base_cli.command_filters`, `base_cli.command_protocol`, and
`base_cli.history`.

Low-level implementation helpers are intentionally not included in the
module `__all__` surfaces. Downstream code should use the documented facade or
the explicitly supported symbols from those modules.

## Minimal Command

```python
Expand Down
19 changes: 18 additions & 1 deletion lib/python/base_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from __future__ import annotations

from . import history, testing
from . import command_filters, command_protocol, history, testing
from .app import App, argument, command, delegated_display_command, option, run_app
from .command_filters import command_matches, normalize_command_filter, normalize_command_filters
from .command_protocol import CommandProtocolError, dumps_record, dumps_records, loads_records
from .config import UserConfig, UserGithubConfig, UserIdeConfig, UserIdePreference, UserWorkspaceConfig
from .context import Context, get_current_context
from .exit_codes import ExitCode
from .inspection import inspection_envelope, render_inspection_json
Expand All @@ -18,8 +21,19 @@

__all__ = [
"App",
"CommandProtocolError",
"Context",
"ExitCode",
"UserConfig",
"UserGithubConfig",
"UserIdeConfig",
"UserIdePreference",
"UserWorkspaceConfig",
"command_filters",
"command_matches",
"command_protocol",
"dumps_record",
"dumps_records",
"history",
"inspection_envelope",
"render_inspection_json",
Expand All @@ -34,6 +48,9 @@
"log_error",
"log_info",
"log_warning",
"loads_records",
"normalize_command_filter",
"normalize_command_filters",
"OutputFormatError",
"PUBLIC_OUTPUT_FORMATS",
"is_terminal",
Expand Down
4 changes: 4 additions & 0 deletions lib/python/base_cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ def _require_click():

# pylint: disable=too-many-statements
class App:
"""Define a Click-backed command with Base's shared runtime lifecycle."""

# pylint: disable=too-many-arguments,too-many-positional-arguments
def __init__(
self,
Expand Down Expand Up @@ -314,6 +316,8 @@ def _create_context(self, standard: dict[str, Any], sensitive_options: set[str],


def run_app(app: App, argv: list[str] | None = None) -> int:
"""Run an :class:`App` and return its normalized process exit code."""

try:
click = _require_click()
except RuntimeError as exc:
Expand Down
7 changes: 7 additions & 0 deletions lib/python/base_cli/command_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
from __future__ import annotations


__all__ = [
"command_matches",
"normalize_command_filter",
"normalize_command_filters",
]


def normalize_command_filter(value: str) -> str:
"""Normalize one public or internal command name for matching."""

Expand Down
8 changes: 8 additions & 0 deletions lib/python/base_cli/command_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
from dataclasses import dataclass


__all__ = [
"CommandProtocolError",
"dumps_record",
"dumps_records",
"loads_records",
]


PROTOCOL_HEADER = "BASE_COMMAND_PROTOCOL_V1"
MAX_RECORD_COUNT = 1_000_000

Expand Down
9 changes: 9 additions & 0 deletions lib/python/base_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@
from .paths import base_state_root


__all__ = [
"UserConfig",
"UserGithubConfig",
"UserIdeConfig",
"UserIdePreference",
"UserWorkspaceConfig",
]


@dataclass(frozen=True)
class UserIdePreference:
enabled: bool | None
Expand Down
2 changes: 2 additions & 0 deletions lib/python/base_cli/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ def _default_user_config() -> UserConfig:

@dataclass
class Context:
"""Runtime state and cleanup hooks available to an active Base CLI command."""

cli_name: str
run_id: str
state_dir: Path
Expand Down
29 changes: 29 additions & 0 deletions lib/python/base_cli/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,35 @@
from .redaction import REDACTED, is_secret_key, option_name_to_parameter, redact_argv, redact_text_value


__all__ = [
"HISTORY_PATH",
"HISTORY_SCOPE_INTERNAL",
"HISTORY_SCOPE_PRIMARY",
"SCHEMA_VERSION",
"base_setup_action",
"base_version",
"build_finished_record",
"compact_home_text",
"compact_optional_path",
"compact_path",
"display_command",
"duration_ms",
"format_timestamp",
"optional_int",
"optional_string",
"parse_finished_history_record_line",
"parse_positive_int",
"project_name",
"redact_history_argv",
"redact_history_text",
"runtime_bundle_path",
"utc_now",
"write_finished_record",
"write_history_record",
"write_primary_record",
]


SCHEMA_VERSION = 1
HISTORY_PATH = Path("base") / "history" / "runs.jsonl"
HISTORY_SCOPE_PRIMARY = "primary"
Expand Down
59 changes: 59 additions & 0 deletions lib/python/base_cli/tests/test_public_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from __future__ import annotations

import unittest

import base_cli
from base_cli import command_filters, command_protocol, history
from base_cli.config import UserConfig, UserGithubConfig, UserIdeConfig, UserIdePreference, UserWorkspaceConfig


class PublicApiTests(unittest.TestCase):
def test_facade_exports_supported_modules_functions_and_types(self) -> None:
expected = {
"CommandProtocolError",
"UserConfig",
"UserGithubConfig",
"UserIdeConfig",
"UserIdePreference",
"UserWorkspaceConfig",
"command_filters",
"command_matches",
"command_protocol",
"dumps_record",
"dumps_records",
"history",
"loads_records",
"normalize_command_filter",
"normalize_command_filters",
}

self.assertTrue(expected.issubset(base_cli.__all__))
self.assertIs(base_cli.UserConfig, UserConfig)
self.assertIs(base_cli.UserGithubConfig, UserGithubConfig)
self.assertIs(base_cli.UserIdeConfig, UserIdeConfig)
self.assertIs(base_cli.UserIdePreference, UserIdePreference)
self.assertIs(base_cli.UserWorkspaceConfig, UserWorkspaceConfig)
self.assertIs(base_cli.command_filters, command_filters)
self.assertIs(base_cli.command_protocol, command_protocol)

def test_module_all_surfaces_are_explicit(self) -> None:
self.assertEqual(
set(command_filters.__all__),
{"command_matches", "normalize_command_filter", "normalize_command_filters"},
)
self.assertEqual(
set(command_protocol.__all__),
{"CommandProtocolError", "dumps_record", "dumps_records", "loads_records"},
)
self.assertIn("write_primary_record", history.__all__)
self.assertNotIn("lock_history_file", history.__all__)
self.assertNotIn("write_all", history.__all__)

def test_entry_points_have_docstrings(self) -> None:
self.assertTrue(base_cli.App.__doc__)
self.assertTrue(base_cli.Context.__doc__)
self.assertTrue(base_cli.run_app.__doc__)


if __name__ == "__main__":
unittest.main()
Loading