diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 45538ba..4b542cf 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -17,6 +17,7 @@ on: - ".github/workflows/dependency-matrix.yml" - ".github/workflows/compatibility.yml" - "scripts/generate_api_reference.py" + - "scripts/generate_core_surface.py" - "scripts/generate_compatibility_dashboard.py" - ".github/workflows/tests.yml" - ".github/workflows/dependency-matrix.yml" @@ -74,6 +75,8 @@ jobs: - name: Validate generated public API reference run: python scripts/generate_api_reference.py --check + - name: Validate generated core surface + run: python scripts/generate_core_surface.py --check - name: Build documentation with strict warnings run: python -m mkdocs build --strict --site-dir site diff --git a/CHANGELOG.md b/CHANGELOG.md index d3482a1..8b320cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and versions are tracked in the repo-root `VERSION` file. ### Changed - Include `uv.lock` freshness in the authoritative local validation aggregate (#424). +- Classify the compatibility facade in a reviewed API manifest and add the + smaller `base_cli.core` starting surface for new applications (#242). ## [0.5.1] - 2026-10-10 diff --git a/README.md b/README.md index 617b52c..e4eed8e 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,11 @@ and independently released. an application's project model. Base is one consumer of the library, not part of its generic contract. +New applications can start with the smaller `base_cli.core` surface. The +compatibility facade remains available for existing 0.x consumers; the +[API manifest](https://basefoundry.github.io/base-cli/api-stability/#public-surface) +classifies advanced and optional integration APIs. + It is intentionally thin. Click still owns argument parsing and command execution, while `base_cli` provides reusable lifecycle behavior: diff --git a/docs/api-manifest.json b/docs/api-manifest.json new file mode 100644 index 0000000..2224afc --- /dev/null +++ b/docs/api-manifest.json @@ -0,0 +1,138 @@ +{ + "schema_version": 1, + "facade": "base_cli", + "tiers": { + "core": [ + "App", + "ApplicationStateT", + "BaseCliDeprecationWarning", + "ConfigT", + "ConfigurationError", + "Context", + "ExitCode", + "LIFECYCLE_META_KEY", + "LifecycleOption", + "LifecycleOptions", + "LifecycleValues", + "ServicesT", + "__version__", + "argument", + "attach", + "command", + "configure_logger", + "deprecated", + "get_current_context", + "get_lifecycle_values", + "log_critical", + "log_debug", + "log_error", + "log_info", + "log_warning", + "option", + "run_app", + "testing" + ], + "integration": [ + "TelemetryOptions", + "TelemetrySession", + "TyperAdapter", + "asyncio_adapter", + "attach_typer", + "get_typer_command", + "integrations", + "run_async", + "try_render_rich_table" + ], + "advanced": [ + "AttachmentAdapter", + "AttachmentContextFactory", + "AttachmentContract", + "AttachmentServiceFactory", + "BOOLEAN", + "BatteriesIncludedConfigLoader", + "COMMAND_ENTRY_POINT_GROUP", + "CliProfile", + "CommandCodec", + "CommandExtension", + "CommandFilterNormalizer", + "CommandProtocolError", + "CommandSchemaRegistry", + "ConfigLoader", + "ConfigSnapshot", + "DEFAULT_SCHEMA_REGISTRY", + "DisplayCommandResolver", + "ENTRY_POINT_GROUPS", + "EXTENSION_API_VERSION", + "EnvironmentConfigLoader", + "ExtensionCollisionError", + "ExtensionCompatibilityError", + "ExtensionDescriptor", + "ExtensionDiscovery", + "ExtensionDiscoveryError", + "ExtensionLoadError", + "ExtensionLoadResult", + "ExtensionsDisabledError", + "FieldSpec", + "FrameworkConfig", + "HistoryDisplayResolver", + "HistoryWriter", + "JSON_CONTRACT_VERSION", + "JSON_ERROR_SCHEMA", + "JSON_LOG_SCHEMA", + "JSON_OUTPUT_SCHEMA", + "JsonLogFormatter", + "MAX_JSON_LOG_MESSAGE_LENGTH", + "NDJSON_SCHEMA", + "NDJSON_SCHEMA_VERSION", + "NULLABLE_STRING", + "NdjsonWriter", + "OutputFormatError", + "PLUGIN_ENTRY_POINT_GROUP", + "PROFILE_ENTRY_POINT_GROUP", + "PUBLIC_OUTPUT_FORMATS", + "PluginExtension", + "ProfileExtension", + "ProjectDiscovery", + "ProjectInfo", + "RECORD_SCHEMAS", + "RetentionPolicy", + "RuntimeBinding", + "RuntimeLayout", + "RuntimeResolver", + "STRING", + "StructuredRecord", + "StructuredResultWriter", + "UserConfigLoader", + "WorkspaceRootResolver", + "command_filters", + "command_matches", + "command_protocol", + "delegated_display_command", + "deprecations", + "dumps_envelope", + "dumps_record", + "dumps_records", + "error_envelope", + "extensions", + "get_command_app", + "history", + "is_terminal", + "json_contracts", + "loads_records", + "normalize_command_filter", + "normalize_command_filters", + "output_format_choices", + "redact_json_value", + "register_record_schema", + "render_document", + "render_records", + "resolve_output_format", + "success_envelope" + ], + "provisional": [ + "experimental", + "inspection_envelope", + "render_inspection_json" + ] + } +} diff --git a/docs/api-reference.md b/docs/api-reference.md index e1390ec..62dd2bc 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -1,7 +1,9 @@ # Public API reference -This page is generated from `base_cli.__all__`. It is the complete stable -facade: private modules and names are deliberately excluded. Each entry +This page is generated from `base_cli.__all__` and checked against +`api-manifest.json`. It is the compatibility facade: private modules and +names are deliberately excluded. New applications should start with the +smaller `base_cli.core` surface described in `api-stability.md`. Each entry includes the runtime signature when Python can inspect one, the public docstring summary, compatibility guidance, and a minimal usage shape. For behavior that is shared by several symbols, follow the linked @@ -19,7 +21,7 @@ The facade currently documents **124** public symbols. ## Classes and protocols ### `App` -**Kind:** class +**Kind:** class
**Signature:** `App(name: 'str | None' = None, version: 'str | None' = None, help: 'str | None' = None, log_to_file: 'bool' = True, max_log_files: 'int | None' = None, profile: 'CliProfile | None' = None, lifecycle_options: 'LifecycleOptions | None' = None, retention: 'RetentionPolicy | None' = None, max_run_bundles: 'int | None' = None, max_run_age_seconds: 'float | None' = None, max_run_total_bytes: 'int | None' = None, rich: 'bool' = False, telemetry: 'TelemetryOptions | None' = None) -> 'None'` **Behavior:** Define a Click-backed command with a shared runtime lifecycle. @@ -35,7 +37,7 @@ base_cli.App(...) ``` ### `AttachmentAdapter` -**Kind:** class +**Kind:** class
**Signature:** `AttachmentAdapter(*args, **kwargs)` **Behavior:** Lifecycle adapter contract implemented by :class:`base_cli.App`. @@ -51,7 +53,7 @@ base_cli.AttachmentAdapter(...) ``` ### `AttachmentContextFactory` -**Kind:** class +**Kind:** class
**Signature:** `AttachmentContextFactory(*args, **kwargs)` **Behavior:** Create consumer-owned application state for an active Context. @@ -67,7 +69,7 @@ base_cli.AttachmentContextFactory(...) ``` ### `AttachmentContract` -**Kind:** class +**Kind:** class
**Signature:** `AttachmentContract(app: 'Any', command: 'CommandT', context_factory: 'Callable[[Context[Any, Any, Any]], Any] | None', service_factory: 'Callable[[Context[Any, Any, Any]], Any] | None', sensitive_parameters: 'frozenset[str]', lifecycle_options: 'Any', standard_bindings: 'dict[str, Any]') -> None` **Behavior:** Immutable attachment state shared by the private Click adapter. @@ -83,7 +85,7 @@ base_cli.AttachmentContract(...) ``` ### `AttachmentServiceFactory` -**Kind:** class +**Kind:** class
**Signature:** `AttachmentServiceFactory(*args, **kwargs)` **Behavior:** Create consumer-owned services for an active Context. @@ -99,7 +101,7 @@ base_cli.AttachmentServiceFactory(...) ``` ### `TyperAdapter` -**Kind:** class +**Kind:** class
**Signature:** `TyperAdapter(typer_app: '_TyperApp') -> 'None'` **Behavior:** Attach one Typer application to a base-cli lifecycle. @@ -115,7 +117,7 @@ base_cli.TyperAdapter(...) ``` ### `BatteriesIncludedConfigLoader` -**Kind:** class +**Kind:** class
**Signature:** `BatteriesIncludedConfigLoader(cli_name: 'str | None' = None, *, user_config_dir: 'Path | None' = None, user_config_name: 'str' = 'config.yaml', project_config_name: 'str' = '.base-cli.yaml', environment_dir_name: 'str' = 'environments', verify_project_config: 'bool' = False) -> 'None'` **Behavior:** Load conventional user, project, environment, and explicit layers. @@ -131,7 +133,7 @@ base_cli.BatteriesIncludedConfigLoader(...) ``` ### `BaseCliDeprecationWarning` -**Kind:** class +**Kind:** class
**Signature:** (value is not callable) **Behavior:** Warning emitted when a supported base-cli API is being retired. @@ -147,7 +149,7 @@ base_cli.BaseCliDeprecationWarning(...) ``` ### `CliProfile` -**Kind:** class +**Kind:** class
**Signature:** `CliProfile(discover_project: 'ProjectDiscovery', load_user_config: 'UserConfigLoader', load_config: 'ConfigLoader', resolve_runtime: 'RuntimeResolver', history_writer: 'HistoryWriter | None' = None, display_command: 'DisplayCommandResolver' = , history_display_command: 'HistoryDisplayResolver' = , resolve_workspace_root: 'WorkspaceRootResolver' = , load_config_for_environment: 'EnvironmentConfigLoader | None' = None) -> None` **Behavior:** Policy boundary between the generic CLI lifecycle and its consumer. @@ -163,7 +165,7 @@ base_cli.CliProfile(...) ``` ### `ConfigLoader` -**Kind:** class +**Kind:** class
**Signature:** `ConfigLoader(*args, **kwargs)` **Behavior:** Load validated framework configuration and opaque consumer settings. @@ -179,7 +181,7 @@ base_cli.ConfigLoader(...) ``` ### `ConfigSnapshot` -**Kind:** class +**Kind:** class
**Signature:** `ConfigSnapshot(config: 'dict[str, Any]', framework: 'FrameworkConfig', provenance: 'Mapping[str, str]') -> None` **Behavior:** One deterministic layered configuration result. @@ -195,7 +197,7 @@ base_cli.ConfigSnapshot(...) ``` ### `CommandCodec` -**Kind:** class +**Kind:** class
**Signature:** `CommandCodec(registry: 'CommandSchemaRegistry | None' = None) -> 'None'` **Behavior:** Encode and decode records using one isolated schema registry. @@ -211,7 +213,7 @@ base_cli.CommandCodec(...) ``` ### `CommandProtocolError` -**Kind:** class +**Kind:** class
**Signature:** (value is not callable) **Behavior:** Raised when a command-protocol schema or payload violates its contract. @@ -227,7 +229,7 @@ base_cli.CommandProtocolError(...) ``` ### `CommandSchemaRegistry` -**Kind:** class +**Kind:** class
**Signature:** `CommandSchemaRegistry() -> 'None'` **Behavior:** Own an isolated set of command record schemas. @@ -243,7 +245,7 @@ base_cli.CommandSchemaRegistry(...) ``` ### `ConfigurationError` -**Kind:** class +**Kind:** class
**Signature:** (value is not callable) **Behavior:** A user-correctable configuration error that is safe to show. @@ -259,7 +261,7 @@ base_cli.ConfigurationError(...) ``` ### `CommandExtension` -**Kind:** class +**Kind:** class
**Signature:** `CommandExtension(*args, **kwargs)` **Behavior:** Callable contract for ``base_cli.commands`` entry points. @@ -275,7 +277,7 @@ base_cli.CommandExtension(...) ``` ### `Context` -**Kind:** class +**Kind:** class
**Signature:** `Context(cli_name: 'str', run_id: 'str', state_dir: 'Path', log_dir: 'Path', cache_dir: 'Path', temp_dir: 'Path', log_file: 'Path | None', config: 'ConfigT', environment: 'str', debug: 'bool', keep_temp: 'bool', log: 'logging.Logger', dry_run: 'bool' = False, application_home: 'Path | None' = None, project_root: 'Path | None' = None, manifest_path: 'Path | None' = None, project_name: 'str | None' = None, history_scope: 'str' = 'primary', history_parent_run_id: 'str | None' = None, user_config: 'object | None' = None, history_display_command: 'Callable[[str, list[str]], str]' = , cleanup_hooks: 'list[Callable[[], None]]' = , workspace_root: 'Path | None' = None, quiet: 'bool' = False, runtime_owner: 'str' = 'default', owner_root: 'Path | None' = None, run_root: 'Path | None' = None, application_context: 'ApplicationStateT | None' = None, services: 'ServicesT | None' = None, framework_config: 'FrameworkConfig | None' = None, config_provenance: 'Mapping[str, str]' = , json_output: 'bool' = False, rich: 'bool' = False) -> None` **Behavior:** Runtime state and cleanup hooks available to an active CLI command. @@ -291,7 +293,7 @@ base_cli.Context(...) ``` ### `ExtensionCompatibilityError` -**Kind:** class +**Kind:** class
**Signature:** `ExtensionCompatibilityError(descriptor: 'ExtensionDescriptor', supported: 'Sequence[str]') -> 'None'` **Behavior:** Raised when an extension declares an unsupported SDK version. @@ -307,7 +309,7 @@ base_cli.ExtensionCompatibilityError(...) ``` ### `ExtensionCollisionError` -**Kind:** class +**Kind:** class
**Signature:** `ExtensionCollisionError(group: 'str', name: 'str', descriptors: 'Sequence[ExtensionDescriptor]') -> 'None'` **Behavior:** Raised when more than one distribution claims the same extension name. @@ -323,7 +325,7 @@ base_cli.ExtensionCollisionError(...) ``` ### `ExtensionDescriptor` -**Kind:** class +**Kind:** class
**Signature:** `ExtensionDescriptor(group: 'str', name: 'str', value: 'str', distribution: 'str | None', version: 'str | None', extras: 'tuple[str, ...]' = (), api_version: 'str' = '1', capabilities: 'tuple[str, ...]' = ()) -> None` **Behavior:** Stable metadata for one entry point, before its object is loaded. @@ -339,7 +341,7 @@ base_cli.ExtensionDescriptor(...) ``` ### `ExtensionDiscovery` -**Kind:** class +**Kind:** class
**Signature:** `ExtensionDiscovery(*, disabled: 'bool' = False, allowlist: 'Iterable[str] | None' = None, entry_points: 'Iterable[Any] | EntryPointProvider | None' = None, paths: 'Iterable[Path] | None' = None, supported_api_versions: 'Iterable[str]' = ('1',)) -> 'None'` **Behavior:** Discover and lazily load command, profile, and plugin entry points. @@ -355,7 +357,7 @@ base_cli.ExtensionDiscovery(...) ``` ### `ExtensionDiscoveryError` -**Kind:** class +**Kind:** class
**Signature:** (value is not callable) **Behavior:** Base class for actionable extension discovery failures. @@ -371,7 +373,7 @@ base_cli.ExtensionDiscoveryError(...) ``` ### `ExtensionLoadError` -**Kind:** class +**Kind:** class
**Signature:** `ExtensionLoadError(descriptor: 'ExtensionDescriptor', cause: 'BaseException') -> 'None'` **Behavior:** Wrap an extension import failure without hiding its source metadata. @@ -387,7 +389,7 @@ base_cli.ExtensionLoadError(...) ``` ### `ExtensionLoadResult` -**Kind:** class +**Kind:** class
**Signature:** `ExtensionLoadResult(descriptor: 'ExtensionDescriptor', value: 'Any | None' = None, error: 'ExtensionLoadError | None' = None) -> None` **Behavior:** Result of an isolated bulk extension load. @@ -403,7 +405,7 @@ base_cli.ExtensionLoadResult(...) ``` ### `ExtensionsDisabledError` -**Kind:** class +**Kind:** class
**Signature:** (value is not callable) **Behavior:** Raised when a caller attempts to load an extension while disabled. @@ -419,7 +421,7 @@ base_cli.ExtensionsDisabledError(...) ``` ### `DisplayCommandResolver` -**Kind:** class +**Kind:** class
**Signature:** `DisplayCommandResolver(*args, **kwargs)` **Behavior:** Resolve the process-facing command label used in diagnostics. @@ -435,7 +437,7 @@ base_cli.DisplayCommandResolver(...) ``` ### `EnvironmentConfigLoader` -**Kind:** class +**Kind:** class
**Signature:** `EnvironmentConfigLoader(*args, **kwargs)` **Behavior:** Load configuration with an explicitly selected environment. @@ -451,7 +453,7 @@ base_cli.EnvironmentConfigLoader(...) ``` ### `ExitCode` -**Kind:** class +**Kind:** class
**Signature:** `ExitCode()` **Behavior:** Standard command exit code constants. @@ -467,7 +469,7 @@ base_cli.ExitCode(...) ``` ### `FieldSpec` -**Kind:** class +**Kind:** class
**Signature:** `FieldSpec(value_type: 'str', nullable: 'bool' = False) -> None` **Behavior:** Describe the wire type and nullability of one command-record field. @@ -483,7 +485,7 @@ base_cli.FieldSpec(...) ``` ### `FrameworkConfig` -**Kind:** class +**Kind:** class
**Signature:** `FrameworkConfig(environment: 'str' = 'dev', log_level: 'str | None' = None, keep_temp: 'bool' = False) -> None` **Behavior:** Validated lifecycle settings separated from consumer configuration. @@ -499,7 +501,7 @@ base_cli.FrameworkConfig(...) ``` ### `LifecycleOption` -**Kind:** class +**Kind:** class
**Signature:** `LifecycleOption(*param_decls: 'str', name: 'str | None' = None, help: 'str | None' = None, metavar: 'str | None' = None, envvar: 'str | tuple[str, ...] | list[str] | None' = None, show_envvar: 'bool' = False, show_default: 'bool | str | None' = None, hidden: 'bool' = False, default: 'Any' = None) -> 'None'` **Behavior:** Immutable public configuration for one lifecycle-owned Click option. @@ -515,7 +517,7 @@ base_cli.LifecycleOption(...) ``` ### `LifecycleOptions` -**Kind:** class +**Kind:** class
**Signature:** `LifecycleOptions(debug: 'LifecycleOption | None' = , quiet: 'LifecycleOption | None' = , environment: 'LifecycleOption | None' = , config: 'LifecycleOption | None' = , keep_temp: 'LifecycleOption | None' = , log_file: 'LifecycleOption | None' = , version: 'LifecycleOption | None' = , dry_run: 'LifecycleOption | None' = None, json: 'LifecycleOption | None' = None) -> None` **Behavior:** Composable option policy used by native and attached applications. @@ -531,7 +533,7 @@ base_cli.LifecycleOptions(...) ``` ### `LifecycleValues` -**Kind:** class +**Kind:** class
**Signature:** `LifecycleValues(debug: 'bool' = False, quiet: 'bool' = False, environment: 'str | None' = None, config: 'Path | None' = None, keep_temp: 'bool' = False, log_file: 'Path | None' = None, dry_run: 'bool' = False, json: 'bool' = False) -> None` **Behavior:** Normalized lifecycle values resolved for the active Click context. @@ -547,7 +549,7 @@ base_cli.LifecycleValues(...) ``` ### `JsonLogFormatter` -**Kind:** class +**Kind:** class
**Signature:** `JsonLogFormatter(run_id: 'str | None' = None) -> 'None'` **Behavior:** Format one ``LogRecord`` as a bounded, redacted JSON object. @@ -563,7 +565,7 @@ base_cli.JsonLogFormatter(...) ``` ### `NdjsonWriter` -**Kind:** class +**Kind:** class
**Signature:** `NdjsonWriter(stream: 'TextIO', schema: 'str' = 'base-cli.record', schema_version: 'int' = 1) -> None` **Behavior:** Write versioned structured records as newline-delimited JSON. @@ -579,7 +581,7 @@ base_cli.NdjsonWriter(...) ``` ### `OutputFormatError` -**Kind:** class +**Kind:** class
**Signature:** (value is not callable) **Behavior:** Raised when a public output format is not supported. @@ -595,7 +597,7 @@ base_cli.OutputFormatError(...) ``` ### `StructuredResultWriter` -**Kind:** class +**Kind:** class
**Signature:** `StructuredResultWriter(*args, **kwargs)` **Behavior:** Typed sink for one structured result at a time. @@ -611,7 +613,7 @@ base_cli.StructuredResultWriter(...) ``` ### `ProjectInfo` -**Kind:** class +**Kind:** class
**Signature:** `ProjectInfo(root: 'Path | None' = None, manifest: 'Path | None' = None, name: 'str | None' = None) -> None` **Behavior:** Consumer-neutral project information discovered for an invocation. @@ -627,7 +629,7 @@ base_cli.ProjectInfo(...) ``` ### `ProjectDiscovery` -**Kind:** class +**Kind:** class
**Signature:** `ProjectDiscovery(*args, **kwargs)` **Behavior:** Discover consumer-owned project information for the current directory. @@ -643,7 +645,7 @@ base_cli.ProjectDiscovery(...) ``` ### `RuntimeLayout` -**Kind:** class +**Kind:** class
**Signature:** `RuntimeLayout(owner_root: 'Path', run_root: 'Path', state_dir: 'Path', log_dir: 'Path', cache_dir: 'Path', temp_dir: 'Path') -> None` **Behavior:** Filesystem locations owned by one base-cli runtime binding. @@ -659,7 +661,7 @@ base_cli.RuntimeLayout(...) ``` ### `PluginExtension` -**Kind:** class +**Kind:** class
**Signature:** `PluginExtension(*args, **kwargs)` **Behavior:** Callable contract for ``base_cli.plugins`` entry points. @@ -675,7 +677,7 @@ base_cli.PluginExtension(...) ``` ### `ProfileExtension` -**Kind:** class +**Kind:** class
**Signature:** `ProfileExtension(*args, **kwargs)` **Behavior:** Callable contract for ``base_cli.profiles`` entry points. @@ -691,7 +693,7 @@ base_cli.ProfileExtension(...) ``` ### `RetentionPolicy` -**Kind:** class +**Kind:** class
**Signature:** `RetentionPolicy(max_bundles: 'int | None' = None, max_age_seconds: 'float | None' = None, max_total_bytes: 'int | None' = None) -> None` **Behavior:** Bounds for complete invocation bundles. @@ -707,7 +709,7 @@ base_cli.RetentionPolicy(...) ``` ### `RuntimeResolver` -**Kind:** class +**Kind:** class
**Signature:** `RuntimeResolver(*args, **kwargs)` **Behavior:** Resolve the runtime directories and ownership for one invocation. @@ -723,7 +725,7 @@ base_cli.RuntimeResolver(...) ``` ### `RuntimeBinding` -**Kind:** class +**Kind:** class
**Signature:** `RuntimeBinding(cache_root: 'Path', layout: 'RuntimeLayout', application_home: 'Path | None', runtime_owner: 'str', project_root: 'Path | None', project_name: 'str | None', inherited_path: 'Path | None', history_parent_run_id: 'str | None', run_id: 'str', primary_log_file: 'Path | None' = None, history_scope: 'str' = 'primary', write_identity: 'bool' = False) -> None` **Behavior:** Runtime decisions supplied by a consumer profile. @@ -739,7 +741,7 @@ base_cli.RuntimeBinding(...) ``` ### `TelemetryOptions` -**Kind:** class +**Kind:** class
**Signature:** `TelemetryOptions(enabled: 'bool' = True, tracer: 'Any | None' = None, tracer_provider: 'Any | None' = None, tracer_name: 'str' = 'base_cli') -> None` **Behavior:** Opt-in OpenTelemetry configuration for one :class:`base_cli.App`. @@ -755,7 +757,7 @@ base_cli.TelemetryOptions(...) ``` ### `TelemetrySession` -**Kind:** class +**Kind:** class
**Signature:** `TelemetrySession(span: 'Any', started_monotonic_ns: 'int') -> None` **Behavior:** Best-effort state for a single lifecycle span. @@ -771,7 +773,7 @@ base_cli.TelemetrySession(...) ``` ### `HistoryWriter` -**Kind:** class +**Kind:** class
**Signature:** `HistoryWriter(*args, **kwargs)` **Behavior:** Persist one completed invocation using the active typed Context. @@ -787,7 +789,7 @@ base_cli.HistoryWriter(...) ``` ### `HistoryDisplayResolver` -**Kind:** class +**Kind:** class
**Signature:** `HistoryDisplayResolver(*args, **kwargs)` **Behavior:** Resolve the command label persisted in consumer history. @@ -803,7 +805,7 @@ base_cli.HistoryDisplayResolver(...) ``` ### `UserConfigLoader` -**Kind:** class +**Kind:** class
**Signature:** `UserConfigLoader(*args, **kwargs)` **Behavior:** Load opaque consumer-owned user configuration. @@ -819,7 +821,7 @@ base_cli.UserConfigLoader(...) ``` ### `WorkspaceRootResolver` -**Kind:** class +**Kind:** class
**Signature:** `WorkspaceRootResolver(*args, **kwargs)` **Behavior:** Project a consumer-owned user configuration into a workspace root. @@ -837,7 +839,7 @@ base_cli.WorkspaceRootResolver(...) ## Functions ### `command_matches` -**Kind:** function +**Kind:** function
**Signature:** `command_matches(value: 'str', command_filters: 'tuple[str, ...]', *, normalizer: 'CommandFilterNormalizer | None' = None) -> 'bool'` **Behavior:** Return whether a command value matches one of the normalized filters. @@ -853,7 +855,7 @@ base_cli.command_matches(...) ``` ### `dumps_envelope` -**Kind:** function +**Kind:** function
**Signature:** `dumps_envelope(envelope: 'Mapping[str, Any]') -> 'str'` **Behavior:** Serialize an envelope as one compact, newline-terminated JSON record. @@ -869,7 +871,7 @@ base_cli.dumps_envelope(...) ``` ### `dumps_record` -**Kind:** function +**Kind:** function
**Signature:** `dumps_record(record_type: 'str', record: 'Record', *, protocol_header: 'str' = 'COMMAND_PROTOCOL_V1', registry: 'CommandSchemaRegistry | None' = None) -> 'str'` **Behavior:** Serialize one typed command record using the protocol framing. @@ -885,7 +887,7 @@ base_cli.dumps_record(...) ``` ### `dumps_records` -**Kind:** function +**Kind:** function
**Signature:** `dumps_records(record_type: 'str', records: 'tuple[Record, ...] | list[Record]', *, protocol_header: 'str' = 'COMMAND_PROTOCOL_V1', registry: 'CommandSchemaRegistry | None' = None) -> 'str'` **Behavior:** Serialize a sequence of typed command records using the protocol framing. @@ -901,7 +903,7 @@ base_cli.dumps_records(...) ``` ### `error_envelope` -**Kind:** function +**Kind:** function
**Signature:** `error_envelope(*, run_id: 'str | None', code: 'str', message: 'str', details: 'Mapping[str, Any] | None' = None) -> 'dict[str, Any]'` **Behavior:** Return the stable v1 machine-readable error envelope. @@ -917,7 +919,7 @@ base_cli.error_envelope(...) ``` ### `inspection_envelope` -**Kind:** function +**Kind:** function
**Signature:** `inspection_envelope(*, command: 'str', status: 'InspectionStatus', data: 'Mapping[str, Any]', error: 'Mapping[str, Any] | None' = None) -> 'dict[str, Any]'` **Behavior:** Build the stable v1 envelope for read-only inspection commands. @@ -933,7 +935,7 @@ base_cli.inspection_envelope(...) ``` ### `render_inspection_json` -**Kind:** function +**Kind:** function
**Signature:** `render_inspection_json(*, command: 'str', status: 'InspectionStatus', data: 'Mapping[str, Any]', error: 'Mapping[str, Any] | None' = None) -> 'str'` **Behavior:** Serialize the stable inspection envelope with Python's JSON encoder. @@ -949,7 +951,7 @@ base_cli.render_inspection_json(...) ``` ### `argument` -**Kind:** function +**Kind:** function
**Signature:** `argument(*param_decls: 'str', sensitive: 'bool' = False, **attrs: 'Any') -> 'Callable[[Callable[_P, _R]], Callable[_P, _R]]'` **Behavior:** Public facade symbol; see the linked contract and source annotations for details. @@ -965,7 +967,7 @@ base_cli.argument(...) ``` ### `run_async` -**Kind:** function +**Kind:** function
**Signature:** `run_async(awaitable: 'Awaitable[_T]') -> '_T'` **Behavior:** Run one awaitable with an adapter-owned event loop. @@ -981,7 +983,7 @@ base_cli.run_async(...) ``` ### `attach` -**Kind:** function +**Kind:** function
**Signature:** `attach(command: '_ClickCommandT', *, app: 'App | None' = None, context_factory: 'Callable[[Context[Any, Any, Any]], Any] | None' = None, service_factory: 'Callable[[Context[Any, Any, Any]], Any] | None' = None, sensitive_parameters: 'Iterable[str]' = (), **app_kwargs: 'Any') -> '_ClickCommandT'` **Behavior:** Attach lifecycle middleware and return the same Click command object. @@ -997,7 +999,7 @@ base_cli.attach(...) ``` ### `attach_typer` -**Kind:** function +**Kind:** function
**Signature:** `attach_typer(typer_app: '_TyperApp', *, app: 'App | None' = None, context_factory: 'Callable[[Context[Any, Any, Any]], Any] | None' = None, service_factory: 'Callable[[Context[Any, Any, Any]], Any] | None' = None, sensitive_parameters: 'Iterable[str]' = (), name: 'str | None' = None, **app_kwargs: 'Any') -> '_ClickCommand'` **Behavior:** Attach a Typer application and return its native Click command. @@ -1013,7 +1015,7 @@ base_cli.attach_typer(...) ``` ### `command` -**Kind:** function +**Kind:** function
**Signature:** `command(*args: 'Any', **kwargs: 'Any') -> 'Callable[[Callable[_P, _R]], Callable[_P, _R]]'` **Behavior:** Public facade symbol; see the linked contract and source annotations for details. @@ -1029,7 +1031,7 @@ base_cli.command(...) ``` ### `configure_logger` -**Kind:** function +**Kind:** function
**Signature:** `configure_logger(cli_name: 'str', log_file: 'Path | None', debug: 'bool', *, quiet: 'bool' = False, stream: 'TextIO | None' = None, formatter: 'logging.Formatter | None' = None, json_logs: 'bool' = False, run_id: 'str | None' = None, log_level: 'str | None' = None, propagate: 'bool | None' = None) -> 'logging.Logger'` **Behavior:** Configure user-facing and persistent handlers for a CLI logger. @@ -1045,7 +1047,7 @@ base_cli.configure_logger(...) ``` ### `delegated_display_command` -**Kind:** function +**Kind:** function
**Signature:** `delegated_display_command(default: 'str | None' = None) -> 'str | None'` **Behavior:** Return the wrapper display label or ``default`` when none is set. @@ -1061,7 +1063,7 @@ base_cli.delegated_display_command(...) ``` ### `deprecated` -**Kind:** function +**Kind:** function
**Signature:** `deprecated(since: 'str', *, remove: 'str', alternative: 'str | None' = None) -> 'Callable[[Callable[P, R]], Callable[P, R]]'` **Behavior:** Mark a callable as deprecated while preserving its normal behavior. @@ -1077,7 +1079,7 @@ base_cli.deprecated(...) ``` ### `get_command_app` -**Kind:** function +**Kind:** function
**Signature:** `get_command_app(command_func: 'Any') -> 'App'` **Behavior:** Return the :class:`App` owning a registered function or attached tree. @@ -1093,7 +1095,7 @@ base_cli.get_command_app(...) ``` ### `get_current_context` -**Kind:** function +**Kind:** function
**Signature:** `get_current_context() -> 'Context[Any, Any, Any]'` **Behavior:** Return the active command context or raise when called outside a command. @@ -1109,7 +1111,7 @@ base_cli.get_current_context(...) ``` ### `get_typer_command` -**Kind:** function +**Kind:** function
**Signature:** `get_typer_command(typer_app: '_TyperApp') -> '_ClickCommand'` **Behavior:** Materialize Typer's native Click command tree. @@ -1125,7 +1127,7 @@ base_cli.get_typer_command(...) ``` ### `get_lifecycle_values` -**Kind:** function +**Kind:** function
**Signature:** `get_lifecycle_values(click_context: 'Any | None' = None) -> 'LifecycleValues'` **Behavior:** Return normalized lifecycle values stored on an active Click context. @@ -1141,7 +1143,7 @@ base_cli.get_lifecycle_values(...) ``` ### `log_critical` -**Kind:** function +**Kind:** function
**Signature:** `log_critical(message: 'str', *args: 'object') -> 'None'` **Behavior:** Public facade symbol; see the linked contract and source annotations for details. @@ -1157,7 +1159,7 @@ base_cli.log_critical(...) ``` ### `log_debug` -**Kind:** function +**Kind:** function
**Signature:** `log_debug(message: 'str', *args: 'object') -> 'None'` **Behavior:** Public facade symbol; see the linked contract and source annotations for details. @@ -1173,7 +1175,7 @@ base_cli.log_debug(...) ``` ### `log_error` -**Kind:** function +**Kind:** function
**Signature:** `log_error(message: 'str', *args: 'object') -> 'None'` **Behavior:** Public facade symbol; see the linked contract and source annotations for details. @@ -1189,7 +1191,7 @@ base_cli.log_error(...) ``` ### `log_info` -**Kind:** function +**Kind:** function
**Signature:** `log_info(message: 'str', *args: 'object') -> 'None'` **Behavior:** Public facade symbol; see the linked contract and source annotations for details. @@ -1205,7 +1207,7 @@ base_cli.log_info(...) ``` ### `log_warning` -**Kind:** function +**Kind:** function
**Signature:** `log_warning(message: 'str', *args: 'object') -> 'None'` **Behavior:** Public facade symbol; see the linked contract and source annotations for details. @@ -1221,7 +1223,7 @@ base_cli.log_warning(...) ``` ### `loads_records` -**Kind:** function +**Kind:** function
**Signature:** `loads_records(payload: 'str', expected_record_type: 'str | None' = None, *, protocol_header: 'str' = 'COMMAND_PROTOCOL_V1', registry: 'CommandSchemaRegistry | None' = None) -> 'tuple[str, tuple[dict[str, RecordValue], ...]]'` **Behavior:** Validate and decode protocol-framed command records. @@ -1237,7 +1239,7 @@ base_cli.loads_records(...) ``` ### `normalize_command_filter` -**Kind:** function +**Kind:** function
**Signature:** `normalize_command_filter(value: 'str', *, normalizer: 'CommandFilterNormalizer | None' = None) -> 'str'` **Behavior:** Normalize one command name for matching. @@ -1253,7 +1255,7 @@ base_cli.normalize_command_filter(...) ``` ### `normalize_command_filters` -**Kind:** function +**Kind:** function
**Signature:** `normalize_command_filters(value: 'str | None', *, normalizer: 'CommandFilterNormalizer | None' = None) -> 'tuple[str, ...]'` **Behavior:** Normalize a comma-separated command filter and reject empty entries. @@ -1269,7 +1271,7 @@ base_cli.normalize_command_filters(...) ``` ### `is_terminal` -**Kind:** function +**Kind:** function
**Signature:** `is_terminal(stream: 'TextIO | None' = None) -> 'bool'` **Behavior:** Return whether *stream* is an interactive terminal. @@ -1285,7 +1287,7 @@ base_cli.is_terminal(...) ``` ### `output_format_choices` -**Kind:** function +**Kind:** function
**Signature:** `output_format_choices() -> 'str'` **Behavior:** Return the public choices in help/error-message order. @@ -1301,7 +1303,7 @@ base_cli.output_format_choices(...) ``` ### `option` -**Kind:** function +**Kind:** function
**Signature:** `option(*param_decls: 'str', sensitive: 'bool' = False, dry_run: 'bool' = False, **attrs: 'Any') -> 'Callable[[Callable[_P, _R]], Callable[_P, _R]]'` **Behavior:** Public facade symbol; see the linked contract and source annotations for details. @@ -1317,7 +1319,7 @@ base_cli.option(...) ``` ### `render_document` -**Kind:** function +**Kind:** function
**Signature:** `render_document(document: 'Mapping[str, Any]', *, requested_format: 'str | None', records_key: 'str | None' = None, columns: 'Sequence[tuple[str, str]] | None' = None, stream: 'TextIO | None' = None, formula_guard: 'bool' = True) -> 'str'` **Behavior:** Render a structured report or leave terminal text to its existing renderer. @@ -1333,7 +1335,7 @@ base_cli.render_document(...) ``` ### `render_records` -**Kind:** function +**Kind:** function
**Signature:** `render_records(records: 'Iterable[Mapping[str, Any]]', *, requested_format: 'str | None', columns: 'Sequence[tuple[str, str]]', stream: 'TextIO | None' = None, footer: 'str | None' = None, minimum_widths: 'Sequence[int] | None' = None, terminal_width: 'int | None' = None, max_cell_width: 'int | None' = 80, rich: 'bool' = False, formula_guard: 'bool' = True) -> 'str'` **Behavior:** Render records according to the shared public output contract. @@ -1349,7 +1351,7 @@ base_cli.render_records(...) ``` ### `register_record_schema` -**Kind:** function +**Kind:** function
**Signature:** `register_record_schema(record_type: 'str', fields: 'Mapping[str, FieldSpec]') -> 'None'` **Behavior:** Register an application-specific record schema for the wire protocol. @@ -1365,7 +1367,7 @@ base_cli.register_record_schema(...) ``` ### `redact_json_value` -**Kind:** function +**Kind:** function
**Signature:** `redact_json_value(value: 'Any', *, _key: 'str | None' = None, _depth: 'int' = 0, _seen: 'set[int] | None' = None) -> 'Any'` **Behavior:** Recursively redact JSON values with bounded depth and cycle handling. @@ -1381,7 +1383,7 @@ base_cli.redact_json_value(...) ``` ### `resolve_output_format` -**Kind:** function +**Kind:** function
**Signature:** `resolve_output_format(requested: 'str | None', *, stream: 'TextIO | None' = None) -> 'str'` **Behavior:** Resolve a requested format, making text TTY-aware. @@ -1397,7 +1399,7 @@ base_cli.resolve_output_format(...) ``` ### `run_app` -**Kind:** function +**Kind:** function
**Signature:** `run_app(app: 'App | Callable[..., Any]', argv: 'list[str] | None' = None, *, reraise_unexpected: 'bool' = False) -> 'int'` **Behavior:** Run an App, registered command, or attached Click tree and return its status. @@ -1413,7 +1415,7 @@ base_cli.run_app(...) ``` ### `success_envelope` -**Kind:** function +**Kind:** function
**Signature:** `success_envelope(*, run_id: 'str | None', details: 'Mapping[str, Any] | None' = None, message: 'str' = 'Success', code: 'str' = 'ok') -> 'dict[str, Any]'` **Behavior:** Return the stable v1 machine-readable success envelope. @@ -1429,7 +1431,7 @@ base_cli.success_envelope(...) ``` ### `try_render_rich_table` -**Kind:** function +**Kind:** function
**Signature:** `try_render_rich_table(stream: 'TextIO', headers: 'Sequence[str]', rows: 'Sequence[Sequence[str]]', footer: 'str | None', *, terminal_width: 'int | None' = None) -> 'bool'` **Behavior:** Render a human table with Rich when it is installed and healthy. @@ -1447,7 +1449,7 @@ base_cli.try_render_rich_table(...) ## Modules ### `asyncio_adapter` -**Kind:** module +**Kind:** module
**Signature:** (value is not callable) **Behavior:** Explicit asyncio support for commands that opt into an async boundary. @@ -1462,7 +1464,7 @@ base_cli.asyncio_adapter ``` ### `command_filters` -**Kind:** module +**Kind:** module
**Signature:** (value is not callable) **Behavior:** Shared command-name filter normalization for CLI reports. @@ -1477,7 +1479,7 @@ base_cli.command_filters ``` ### `command_protocol` -**Kind:** module +**Kind:** module
**Signature:** (value is not callable) **Behavior:** Public facade symbol; see the linked contract and source annotations for details. @@ -1492,7 +1494,7 @@ base_cli.command_protocol ``` ### `deprecations` -**Kind:** module +**Kind:** module
**Signature:** (value is not callable) **Behavior:** Small, consistent deprecation primitives for public base-cli APIs. @@ -1507,7 +1509,7 @@ base_cli.deprecations ``` ### `experimental` -**Kind:** module +**Kind:** module
**Signature:** (value is not callable) **Behavior:** Preview APIs under active development. @@ -1522,7 +1524,7 @@ base_cli.experimental ``` ### `json_contracts` -**Kind:** module +**Kind:** module
**Signature:** (value is not callable) **Behavior:** Versioned JSON contracts for machine-facing CLI consumers. @@ -1537,7 +1539,7 @@ base_cli.json_contracts ``` ### `extensions` -**Kind:** module +**Kind:** module
**Signature:** (value is not callable) **Behavior:** Lazy, deterministic discovery of optional Python entry-point extensions. @@ -1552,7 +1554,7 @@ base_cli.extensions ``` ### `history` -**Kind:** module +**Kind:** module
**Signature:** (value is not callable) **Behavior:** Public facade symbol; see the linked contract and source annotations for details. @@ -1567,7 +1569,7 @@ base_cli.history ``` ### `integrations` -**Kind:** module +**Kind:** module
**Signature:** (value is not callable) **Behavior:** Optional Rich and OpenTelemetry integrations. @@ -1582,7 +1584,7 @@ base_cli.integrations ``` ### `testing` -**Kind:** module +**Kind:** module
**Signature:** (value is not callable) **Behavior:** Public facade symbol; see the linked contract and source annotations for details. @@ -1599,7 +1601,7 @@ base_cli.testing ## Constants and typing helpers ### `__version__` -**Kind:** constant/type +**Kind:** constant/type
**Signature:** (value is not callable) **Behavior:** Installed base-cli distribution version exposed by the public facade. @@ -1614,7 +1616,7 @@ value = base_cli.__version__ ``` ### `BOOLEAN` -**Kind:** constant/type +**Kind:** constant/type
**Signature:** (value is not callable) **Behavior:** Schema field descriptor for a required boolean command-record value. @@ -1629,7 +1631,7 @@ value = base_cli.BOOLEAN ``` ### `ApplicationStateT` -**Kind:** constant/type +**Kind:** constant/type
**Signature:** (value is not callable) **Behavior:** Consumer-owned application-state type parameter carried by Context. @@ -1644,7 +1646,7 @@ value = base_cli.ApplicationStateT ``` ### `CommandFilterNormalizer` -**Kind:** constant/type +**Kind:** constant/type
**Signature:** (value is not callable) **Behavior:** Callable type for canonicalizing command filter names. @@ -1659,7 +1661,7 @@ value = base_cli.CommandFilterNormalizer ``` ### `COMMAND_ENTRY_POINT_GROUP` -**Kind:** constant/type +**Kind:** constant/type
**Signature:** (value is not callable) **Behavior:** Entry-point group name used to discover command extensions. @@ -1674,7 +1676,7 @@ value = base_cli.COMMAND_ENTRY_POINT_GROUP ``` ### `ConfigT` -**Kind:** constant/type +**Kind:** constant/type
**Signature:** (value is not callable) **Behavior:** Consumer-owned configuration type parameter carried by Context and App. @@ -1689,7 +1691,7 @@ value = base_cli.ConfigT ``` ### `ENTRY_POINT_GROUPS` -**Kind:** constant/type +**Kind:** constant/type
**Signature:** (value is not callable) **Behavior:** Supported base-cli extension entry-point groups in discovery order. @@ -1704,7 +1706,7 @@ value = base_cli.ENTRY_POINT_GROUPS ``` ### `EXTENSION_API_VERSION` -**Kind:** constant/type +**Kind:** constant/type
**Signature:** (value is not callable) **Behavior:** Extension API version negotiated through entry-point metadata. @@ -1719,7 +1721,7 @@ value = base_cli.EXTENSION_API_VERSION ``` ### `DEFAULT_SCHEMA_REGISTRY` -**Kind:** constant/type +**Kind:** constant/type
**Signature:** (value is not callable) **Behavior:** Process-local registry containing built-in and consumer record schemas. @@ -1734,7 +1736,7 @@ value = base_cli.DEFAULT_SCHEMA_REGISTRY ``` ### `LIFECYCLE_META_KEY` -**Kind:** constant/type +**Kind:** constant/type
**Signature:** (value is not callable) **Behavior:** Metadata key used to mark lifecycle-owned Click parameters. @@ -1749,7 +1751,7 @@ value = base_cli.LIFECYCLE_META_KEY ``` ### `NULLABLE_STRING` -**Kind:** constant/type +**Kind:** constant/type
**Signature:** (value is not callable) **Behavior:** Schema field descriptor for a nullable string command-record value. @@ -1764,7 +1766,7 @@ value = base_cli.NULLABLE_STRING ``` ### `STRING` -**Kind:** constant/type +**Kind:** constant/type
**Signature:** (value is not callable) **Behavior:** Schema field descriptor for a required string command-record value. @@ -1779,7 +1781,7 @@ value = base_cli.STRING ``` ### `JSON_CONTRACT_VERSION` -**Kind:** constant/type +**Kind:** constant/type
**Signature:** (value is not callable) **Behavior:** Version number for the stable JSON output, error, and log contracts. @@ -1794,7 +1796,7 @@ value = base_cli.JSON_CONTRACT_VERSION ``` ### `JSON_ERROR_SCHEMA` -**Kind:** constant/type +**Kind:** constant/type
**Signature:** (value is not callable) **Behavior:** Schema identifier emitted by machine-readable error envelopes. @@ -1809,7 +1811,7 @@ value = base_cli.JSON_ERROR_SCHEMA ``` ### `JSON_LOG_SCHEMA` -**Kind:** constant/type +**Kind:** constant/type
**Signature:** (value is not callable) **Behavior:** Schema identifier emitted by structured JSON log records. @@ -1824,7 +1826,7 @@ value = base_cli.JSON_LOG_SCHEMA ``` ### `JSON_OUTPUT_SCHEMA` -**Kind:** constant/type +**Kind:** constant/type
**Signature:** (value is not callable) **Behavior:** Schema identifier emitted by successful JSON output envelopes. @@ -1839,7 +1841,7 @@ value = base_cli.JSON_OUTPUT_SCHEMA ``` ### `MAX_JSON_LOG_MESSAGE_LENGTH` -**Kind:** constant/type +**Kind:** constant/type
**Signature:** (value is not callable) **Behavior:** Maximum retained message length for one structured JSON log record. @@ -1854,7 +1856,7 @@ value = base_cli.MAX_JSON_LOG_MESSAGE_LENGTH ``` ### `NDJSON_SCHEMA` -**Kind:** constant/type +**Kind:** constant/type
**Signature:** (value is not callable) **Behavior:** Schema identifier emitted by versioned newline-delimited record output. @@ -1869,7 +1871,7 @@ value = base_cli.NDJSON_SCHEMA ``` ### `NDJSON_SCHEMA_VERSION` -**Kind:** constant/type +**Kind:** constant/type
**Signature:** (value is not callable) **Behavior:** Version number for the newline-delimited record contract. @@ -1884,7 +1886,7 @@ value = base_cli.NDJSON_SCHEMA_VERSION ``` ### `PUBLIC_OUTPUT_FORMATS` -**Kind:** constant/type +**Kind:** constant/type
**Signature:** (value is not callable) **Behavior:** Supported public output format names and their resolution order. @@ -1899,7 +1901,7 @@ value = base_cli.PUBLIC_OUTPUT_FORMATS ``` ### `StructuredRecord` -**Kind:** constant/type +**Kind:** constant/type
**Signature:** (value is not callable) **Behavior:** Mapping shape accepted by the versioned command-record serializers. @@ -1914,7 +1916,7 @@ value = base_cli.StructuredRecord ``` ### `RECORD_SCHEMAS` -**Kind:** constant/type +**Kind:** constant/type
**Signature:** (value is not callable) **Behavior:** Mutable registry mapping consumer record names to FieldSpec definitions. @@ -1929,7 +1931,7 @@ value = base_cli.RECORD_SCHEMAS ``` ### `PLUGIN_ENTRY_POINT_GROUP` -**Kind:** constant/type +**Kind:** constant/type
**Signature:** (value is not callable) **Behavior:** Entry-point group name used to discover optional plugin extensions. @@ -1944,7 +1946,7 @@ value = base_cli.PLUGIN_ENTRY_POINT_GROUP ``` ### `PROFILE_ENTRY_POINT_GROUP` -**Kind:** constant/type +**Kind:** constant/type
**Signature:** (value is not callable) **Behavior:** Entry-point group name used to discover profile extensions. @@ -1959,7 +1961,7 @@ value = base_cli.PROFILE_ENTRY_POINT_GROUP ``` ### `ServicesT` -**Kind:** constant/type +**Kind:** constant/type
**Signature:** (value is not callable) **Behavior:** Consumer-owned service-container type parameter carried by Context. diff --git a/docs/api-stability.md b/docs/api-stability.md index 67bf451..e5c6744 100644 --- a/docs/api-stability.md +++ b/docs/api-stability.md @@ -14,6 +14,21 @@ the other modules listed in their module-level `__all__`) are public as well. Names beginning with `_`, modules not documented here, and implementation details are private and may change without notice. +The reviewed facade is classified in [`api-manifest.json`](api-manifest.json). +Every facade symbol belongs to exactly one of four tiers: + +- **core** — the small lifecycle surface recommended for new applications; +- **integration** — optional parser, telemetry, and presentation adapters; +- **advanced** — explicit configuration, contract, extension, and runtime APIs; +- **provisional** — public for compatibility, but not a 1.0 default. + +New applications should begin with `import base_cli.core as cli`. The broad +`base_cli` facade remains available during the 0.x compatibility window so +existing consumers can migrate deliberately. The manifest and its validator +prevent an export from appearing without a reviewed tier classification. +Optional integration packages remain lazy: importing `base_cli.core` does not +require Typer, Rich, or OpenTelemetry. + The following machine-facing contracts are public and versioned: - command framing defaults to `COMMAND_PROTOCOL_V1`; record schemas are owned diff --git a/docs/index.md b/docs/index.md index 35ef02d..0dd2c18 100644 --- a/docs/index.md +++ b/docs/index.md @@ -64,6 +64,8 @@ application. for dated CI results and the permissioned-adopter program. - Read [API stability](api-stability.md) and the [migration guide](migrations.md) before upgrading across a compatibility boundary. +- Start new applications with the small `base_cli.core` surface and consult the + [API manifest](api-manifest.json) for advanced and integration tiers. - Browse the [complete public API reference](api-reference.md) for exported symbols, signatures, and usage shapes. - Follow [consumer profiles](consumer-profiles.md) when your application owns diff --git a/lib/python/base_cli/core.py b/lib/python/base_cli/core.py new file mode 100644 index 0000000..34becdb --- /dev/null +++ b/lib/python/base_cli/core.py @@ -0,0 +1,83 @@ +"""Small, stable starting surface for production CLI applications. + +The compatibility facade at :mod:`base_cli` remains broad during the 0.x +series. New applications should prefer this module for the lifecycle core; +integration and advanced APIs stay behind their explicit modules. +""" + +from __future__ import annotations + +from . import __version__, testing +from .app import ( + App, + argument, + attach, + command, + option, + run_app, +) +from .context import ( + ApplicationStateT, + ConfigT, + Context, + ServicesT, + get_current_context, +) +from .deprecations import ( + BaseCliDeprecationWarning, + deprecated, +) +from .errors import ( + ConfigurationError, +) +from .exit_codes import ( + ExitCode, +) +from .lifecycle_options import ( + LIFECYCLE_META_KEY, + LifecycleOption, + LifecycleOptions, + LifecycleValues, + get_lifecycle_values, +) +from .logging import ( + configure_logger, + log_critical, + log_debug, + log_error, + log_info, + log_warning, +) + +__all__ = [ + "App", + "ApplicationStateT", + "BaseCliDeprecationWarning", + "ConfigT", + "ConfigurationError", + "Context", + "ExitCode", + "LIFECYCLE_META_KEY", + "LifecycleOption", + "LifecycleOptions", + "LifecycleValues", + "ServicesT", + "__version__", + "argument", + "attach", + "command", + "configure_logger", + "deprecated", + "get_current_context", + "get_lifecycle_values", + "log_critical", + "log_debug", + "log_error", + "log_info", + "log_warning", + "option", + "run_app", + "testing", +] + +# generated by scripts/generate_core_surface.py; do not edit diff --git a/scripts/api_manifest.py b/scripts/api_manifest.py new file mode 100644 index 0000000..ec52683 --- /dev/null +++ b/scripts/api_manifest.py @@ -0,0 +1,21 @@ +"""Shared loading helpers for the reviewed public API manifest.""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + + +def load_manifest(path: Path) -> dict[str, Any]: + """Load one API manifest and turn malformed input into a short error.""" + + try: + value = json.loads(path.read_text(encoding="utf-8")) + except OSError as exc: + raise ValueError(f"cannot read {path}: {exc}") from exc + except json.JSONDecodeError as exc: + raise ValueError(f"invalid JSON in {path}: {exc.msg} at line {exc.lineno}, column {exc.colno}") from exc + if not isinstance(value, dict): + raise ValueError("API manifest must contain a JSON object") + return value diff --git a/scripts/generate_api_reference.py b/scripts/generate_api_reference.py index 381051c..9f21763 100644 --- a/scripts/generate_api_reference.py +++ b/scripts/generate_api_reference.py @@ -12,12 +12,16 @@ import inspect import re import sys +import typing from pathlib import Path from typing import Any +from api_manifest import load_manifest + ROOT = Path(__file__).resolve().parents[1] PACKAGE_ROOT = ROOT / "lib" / "python" REFERENCE = ROOT / "docs" / "api-reference.md" +MANIFEST = ROOT / "docs" / "api-manifest.json" MARKER = "" HEADING_PATTERN = re.compile(r"^### `([^`]+)`$", re.MULTILINE) @@ -59,6 +63,11 @@ def _load_package() -> Any: def _kind(value: Any) -> str: + # Generic aliases changed from non-classes to class-like objects across + # supported Python versions. Treat typing aliases as data first so the + # generated reference is byte-identical on Python 3.10 through 3.14. + if typing.get_origin(value) is not None: + return "constant/type" if inspect.ismodule(value): return "module" if inspect.isclass(value): @@ -72,6 +81,8 @@ def _kind(value: Any) -> str: def _signature(name: str, value: Any) -> str: try: + if typing.get_origin(value) is not None: + return "(value is not callable)" if inspect.isclass(value) or inspect.isfunction(value) or inspect.ismethod(value): signature = str(inspect.signature(value)) # Default callback reprs contain process-specific memory addresses. @@ -110,7 +121,7 @@ def render(base_cli: Any) -> str: "\n".join( ( f"### `{name}`", - f"**Kind:** {kind} ", + f"**Kind:** {kind}
", f"**Signature:** {_signature(name, value)}", "", f"**Behavior:** {_description(name, value)}", @@ -128,8 +139,10 @@ def render(base_cli: Any) -> str: lines = [ "# Public API reference", "", - "This page is generated from `base_cli.__all__`. It is the complete stable", - "facade: private modules and names are deliberately excluded. Each entry", + "This page is generated from `base_cli.__all__` and checked against", + "`api-manifest.json`. It is the compatibility facade: private modules and", + "names are deliberately excluded. New applications should start with the", + "smaller `base_cli.core` surface described in `api-stability.md`. Each entry", "includes the runtime signature when Python can inspect one, the public", "docstring summary, compatibility guidance, and a minimal usage shape.", "For behavior that is shared by several symbols, follow the linked", @@ -160,12 +173,22 @@ def main() -> None: base_cli = _load_package() rendered = render(base_cli) if check: + manifest = load_manifest(MANIFEST) current = REFERENCE.read_text(encoding="utf-8") if REFERENCE.exists() else "" if current != rendered: print(f"{REFERENCE.relative_to(ROOT)} is stale; run scripts/generate_api_reference.py", file=sys.stderr) raise SystemExit(1) documented = set(HEADING_PATTERN.findall(current)) exported = set(base_cli.__all__) + tiers = manifest.get("tiers") + manifest_exports = ( + {name for names in tiers.values() if isinstance(names, list) for name in names if isinstance(name, str)} + if isinstance(tiers, dict) + else set() + ) + if manifest_exports != exported: + print("generated API manifest does not match base_cli.__all__", file=sys.stderr) + raise SystemExit(1) if documented != exported: print("generated API headings do not match base_cli.__all__", file=sys.stderr) raise SystemExit(1) diff --git a/scripts/generate_core_surface.py b/scripts/generate_core_surface.py new file mode 100644 index 0000000..8f86410 --- /dev/null +++ b/scripts/generate_core_surface.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +"""Generate the small ``base_cli.core`` facade from the API manifest.""" + +from __future__ import annotations + +import sys +from pathlib import Path +from typing import Any + +from api_manifest import load_manifest + +ROOT = Path(__file__).resolve().parents[1] +MANIFEST = ROOT / "docs" / "api-manifest.json" +CORE = ROOT / "lib" / "python" / "base_cli" / "core.py" +MARKER = "# generated by scripts/generate_core_surface.py; do not edit" + +# These are the package modules that own the stable core names. The manifest +# remains the source of truth for which names belong to the core tier; this +# mapping only records how Python resolves those names for the generated file. +CORE_SOURCE_MODULES = { + "App": ".app", + "argument": ".app", + "attach": ".app", + "command": ".app", + "option": ".app", + "run_app": ".app", + "ApplicationStateT": ".context", + "ConfigT": ".context", + "Context": ".context", + "ServicesT": ".context", + "get_current_context": ".context", + "BaseCliDeprecationWarning": ".deprecations", + "deprecated": ".deprecations", + "ConfigurationError": ".errors", + "ExitCode": ".exit_codes", + "LIFECYCLE_META_KEY": ".lifecycle_options", + "LifecycleOption": ".lifecycle_options", + "LifecycleOptions": ".lifecycle_options", + "LifecycleValues": ".lifecycle_options", + "get_lifecycle_values": ".lifecycle_options", + "configure_logger": ".logging", + "log_critical": ".logging", + "log_debug": ".logging", + "log_error": ".logging", + "log_info": ".logging", + "log_warning": ".logging", + "__version__": ".", + "testing": ".", +} + + +def _core_names(manifest: dict[str, Any]) -> list[str]: + tiers = manifest.get("tiers") + if not isinstance(tiers, dict) or not isinstance(tiers.get("core"), list): + raise ValueError("API manifest tiers.core must be a list") + names = tiers["core"] + if any(not isinstance(name, str) for name in names): + raise ValueError("API manifest tiers.core must contain only strings") + missing = sorted(set(names) - set(CORE_SOURCE_MODULES)) + if missing: + raise ValueError(f"no core import mapping exists for: {missing}") + return names + + +def render(manifest: dict[str, Any]) -> str: + names = _core_names(manifest) + grouped: dict[str, list[str]] = {} + for name in names: + grouped.setdefault(CORE_SOURCE_MODULES[name], []).append(name) + + lines = [ + '"""Small, stable starting surface for production CLI applications.', + "", + "The compatibility facade at :mod:`base_cli` remains broad during the 0.x", + "series. New applications should prefer this module for the lifecycle core;", + "integration and advanced APIs stay behind their explicit modules.", + '"""', + "", + "from __future__ import annotations", + "", + ] + for module in ( + ".", + ".app", + ".context", + ".deprecations", + ".errors", + ".exit_codes", + ".lifecycle_options", + ".logging", + ): + module_names = grouped.get(module) + if not module_names: + continue + prefix = "from . import" if module == "." else f"from {module} import" + if module == ".": + lines.append(f"{prefix} {', '.join(module_names)}") + else: + lines.append(f"{prefix} (") + lines.extend(f" {name}," for name in module_names) + lines.append(")") + lines.extend(["", "__all__ = ["]) + lines.extend(f' "{name}",' for name in names) + lines.extend(["]", "", MARKER, ""]) + return "\n".join(lines) + + +def main() -> None: + check = "--check" in sys.argv[1:] + try: + rendered = render(load_manifest(MANIFEST)) + except ValueError as exc: + print(f"core surface generation failed: {exc}", file=sys.stderr) + raise SystemExit(1) from exc + if check: + current = CORE.read_text(encoding="utf-8") if CORE.exists() else "" + if current != rendered: + print(f"{CORE.relative_to(ROOT)} is stale; run scripts/generate_core_surface.py", file=sys.stderr) + raise SystemExit(1) + print(f"Validated {CORE.relative_to(ROOT)}.") + return + CORE.write_text(rendered, encoding="utf-8") + print(f"Generated {CORE.relative_to(ROOT)}.") + + +if __name__ == "__main__": + main() diff --git a/scripts/validate_api_manifest.py b/scripts/validate_api_manifest.py new file mode 100644 index 0000000..bf40ebb --- /dev/null +++ b/scripts/validate_api_manifest.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Validate the reviewed public API tier manifest against the package facade.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +from api_manifest import load_manifest +from generate_api_reference import _load_package + +ROOT = Path(__file__).resolve().parents[1] +MANIFEST = ROOT / "docs" / "api-manifest.json" +PACKAGE_ROOT = ROOT / "lib" / "python" + + +def _validate() -> None: + base_cli = _load_package() + import base_cli.core as core + + manifest = load_manifest(MANIFEST) + if manifest.get("schema_version") != 1: + raise ValueError("unsupported API manifest schema version") + if manifest.get("facade") != "base_cli": + raise ValueError("API manifest facade must be base_cli") + + tiers = manifest.get("tiers") + if not isinstance(tiers, dict): + raise ValueError("API manifest tiers must be an object") + expected_tiers = {"core", "integration", "advanced", "provisional"} + if set(tiers) != expected_tiers: + raise ValueError(f"API manifest tiers must be exactly {sorted(expected_tiers)}") + + tier_names: list[str] = [] + for tier in sorted(expected_tiers): + names = tiers[tier] + if not isinstance(names, list) or any(not isinstance(name, str) for name in names): + raise ValueError(f"API manifest tier {tier!r} must be a list of strings") + if names != sorted(names): + raise ValueError(f"API manifest tier {tier!r} must be sorted") + tier_names.extend(names) + + duplicates = sorted(name for name in set(tier_names) if tier_names.count(name) > 1) + if duplicates: + raise ValueError(f"API manifest contains duplicate symbols: {duplicates}") + + exported = set(base_cli.__all__) + manifest_exports = set(tier_names) + if manifest_exports != exported: + missing = sorted(exported - manifest_exports) + extra = sorted(manifest_exports - exported) + raise ValueError(f"API manifest mismatch; missing={missing}, extra={extra}") + if any(name.startswith("_") and name != "__version__" for name in manifest_exports): + raise ValueError("private names other than __version__ cannot be public API") + for name in manifest_exports: + if not hasattr(base_cli, name): + raise ValueError(f"facade export is not resolvable: {name}") + + core_names = tiers["core"] + if set(core.__all__) != set(core_names): + raise ValueError("base_cli.core.__all__ must match the manifest core lifecycle symbols") + if any(not hasattr(core, name) for name in core.__all__): + raise ValueError("base_cli.core contains an unresolved export") + for name in core_names: + if getattr(core, name) is not getattr(base_cli, name): + raise ValueError(f"base_cli.core.{name} is not identical to base_cli.{name}") + + print(f"Validated API manifest: {len(exported)} facade symbols, {len(core.__all__)} core symbols.") + + +def main() -> None: + try: + _validate() + except (OSError, ValueError, TypeError) as exc: + print(f"API manifest validation failed: {exc}", file=sys.stderr) + raise SystemExit(1) from exc + + +if __name__ == "__main__": + main() diff --git a/tests/full_validate.sh b/tests/full_validate.sh index 9dec3d8..49a8dca 100755 --- a/tests/full_validate.sh +++ b/tests/full_validate.sh @@ -60,6 +60,8 @@ run_contracts() { require_commands python python scripts/validate_docs.py python scripts/validate_changelog.py + python scripts/validate_api_manifest.py + python scripts/generate_core_surface.py --check python scripts/validate_schemas.py python scripts/validate_contract_fixtures.py if command -v node >/dev/null 2>&1; then diff --git a/tests/test_api_manifest.py b/tests/test_api_manifest.py new file mode 100644 index 0000000..a6eec87 --- /dev/null +++ b/tests/test_api_manifest.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +import json +import os +import subprocess +import sys +import unittest +from pathlib import Path + +import base_cli +import base_cli.core as core + + +class ApiManifestTests(unittest.TestCase): + def test_manifest_partitions_the_facade(self) -> None: + manifest_path = Path(__file__).resolve().parents[1] / "docs" / "api-manifest.json" + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + tiers = manifest["tiers"] + names = [name for tier in tiers.values() for name in tier] + + self.assertEqual(len(names), len(set(names))) + self.assertEqual(set(names), set(base_cli.__all__)) + self.assertEqual(set(core.__all__), set(tiers["core"])) + self.assertTrue(all(hasattr(base_cli, name) for name in names)) + self.assertTrue(all(getattr(core, name) is getattr(base_cli, name) for name in tiers["core"])) + + def test_core_surface_has_no_optional_integration_imports(self) -> None: + package_root = Path(__file__).resolve().parents[1] / "lib" / "python" + environment = os.environ.copy() + environment["PYTHONPATH"] = str(package_root) + probe = subprocess.run( + [ + sys.executable, + "-c", + "import sys; import base_cli.core; " + "assert all(name not in sys.modules for name in ('typer', 'rich', 'opentelemetry'))", + ], + check=False, + capture_output=True, + text=True, + env=environment, + ) + self.assertEqual(probe.returncode, 0, probe.stderr) + + self.assertIn("App", core.__all__) + self.assertIn("run_app", core.__all__) + + +if __name__ == "__main__": + unittest.main()