diff --git a/lib/python/base_cli/README.md b/lib/python/base_cli/README.md index 57009de8..1913358d 100644 --- a/lib/python/base_cli/README.md +++ b/lib/python/base_cli/README.md @@ -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 diff --git a/lib/python/base_cli/__init__.py b/lib/python/base_cli/__init__.py index 65d02b42..e92e5406 100644 --- a/lib/python/base_cli/__init__.py +++ b/lib/python/base_cli/__init__.py @@ -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 @@ -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", @@ -34,6 +48,9 @@ "log_error", "log_info", "log_warning", + "loads_records", + "normalize_command_filter", + "normalize_command_filters", "OutputFormatError", "PUBLIC_OUTPUT_FORMATS", "is_terminal", diff --git a/lib/python/base_cli/app.py b/lib/python/base_cli/app.py index 9d2400e6..1d7e3791 100644 --- a/lib/python/base_cli/app.py +++ b/lib/python/base_cli/app.py @@ -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, @@ -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: diff --git a/lib/python/base_cli/command_filters.py b/lib/python/base_cli/command_filters.py index 44028a03..ad5a41bf 100644 --- a/lib/python/base_cli/command_filters.py +++ b/lib/python/base_cli/command_filters.py @@ -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.""" diff --git a/lib/python/base_cli/command_protocol.py b/lib/python/base_cli/command_protocol.py index c4809d20..988d0bde 100644 --- a/lib/python/base_cli/command_protocol.py +++ b/lib/python/base_cli/command_protocol.py @@ -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 diff --git a/lib/python/base_cli/config.py b/lib/python/base_cli/config.py index 40b28dfc..68aabe5f 100644 --- a/lib/python/base_cli/config.py +++ b/lib/python/base_cli/config.py @@ -12,6 +12,15 @@ from .paths import base_state_root +__all__ = [ + "UserConfig", + "UserGithubConfig", + "UserIdeConfig", + "UserIdePreference", + "UserWorkspaceConfig", +] + + @dataclass(frozen=True) class UserIdePreference: enabled: bool | None diff --git a/lib/python/base_cli/context.py b/lib/python/base_cli/context.py index 0b564588..123d8cc9 100644 --- a/lib/python/base_cli/context.py +++ b/lib/python/base_cli/context.py @@ -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 diff --git a/lib/python/base_cli/history.py b/lib/python/base_cli/history.py index 1e9e1847..baf0973c 100644 --- a/lib/python/base_cli/history.py +++ b/lib/python/base_cli/history.py @@ -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" diff --git a/lib/python/base_cli/tests/test_public_api.py b/lib/python/base_cli/tests/test_public_api.py new file mode 100644 index 00000000..750ca418 --- /dev/null +++ b/lib/python/base_cli/tests/test_public_api.py @@ -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()