diff --git a/nsc/cli/app.py b/nsc/cli/app.py index 87944dc..5c807c0 100644 --- a/nsc/cli/app.py +++ b/nsc/cli/app.py @@ -33,7 +33,6 @@ from nsc.config import default_paths from nsc.config.loader import ConfigParseError, load_config from nsc.config.models import Config, OutputFormat -from nsc.http.errors import NetBoxAPIError, NetBoxClientError from nsc.schema.source import SchemaSourceError # Mutable dict used as a single-slot holder to avoid `global` statements. @@ -289,10 +288,6 @@ def main() -> None: app() except typer.Exit: raise - except (NetBoxAPIError, NetBoxClientError) as exc: - env = map_error(exc) - code = emit_envelope(env, output_format=OutputFormat.TABLE) - raise typer.Exit(code) from exc except Exception as exc: # catch-all to produce internal envelope env = map_error(exc) code = emit_envelope(env, output_format=OutputFormat.TABLE) diff --git a/nsc/cli/commands_dump.py b/nsc/cli/commands_dump.py index 69fb33f..1b67bdb 100644 --- a/nsc/cli/commands_dump.py +++ b/nsc/cli/commands_dump.py @@ -1,9 +1,4 @@ -"""`nsc commands` — dump the generated CommandModel as JSON. - -In Phase 1 this is the only useful subcommand. It exists so an agent or human -can see exactly what the schema-derived command tree would look like, without -needing the dynamic Typer registration that Phase 2 will add. -""" +"""`nsc commands` — dump the generated CommandModel as JSON.""" from __future__ import annotations @@ -31,13 +26,13 @@ def commands_dump( schema: str = typer.Option( ..., "--schema", - help="Path or URL to an OpenAPI schema. Required in Phase 1.", + help="Path or URL to an OpenAPI schema.", ), output: _Output = typer.Option( # noqa: B008 _Output.JSON, "--output", "-o", - help="Output format. Phase 1 supports `json` only.", + help="Output format.", ), compact: bool = typer.Option( False, diff --git a/nsc/cli/globals.py b/nsc/cli/globals.py index 99eee18..400e31e 100644 --- a/nsc/cli/globals.py +++ b/nsc/cli/globals.py @@ -8,18 +8,14 @@ from nsc.cli.runtime import ( CLIOverrides, - NoProfileError, - ResolvedProfile, RuntimeContext, - UnknownProfileError, resolve_profile, ) from nsc.config import default_paths -from nsc.config.loader import ConfigParseError from nsc.config.models import Config from nsc.http.client import NetBoxClient from nsc.output.render import select_format -from nsc.schema.source import SchemaSourceError, resolve_command_model +from nsc.schema.source import resolve_command_model @dataclass @@ -58,12 +54,6 @@ def build_runtime_context(state: GlobalState) -> RuntimeContext: __all__ = [ - "ConfigParseError", "GlobalState", - "NetBoxClient", - "NoProfileError", - "ResolvedProfile", - "SchemaSourceError", - "UnknownProfileError", "build_runtime_context", ] diff --git a/nsc/cli/handlers.py b/nsc/cli/handlers.py index 047ab53..c20fd10 100644 --- a/nsc/cli/handlers.py +++ b/nsc/cli/handlers.py @@ -60,6 +60,10 @@ _STATUS_NOT_FOUND_DELETE = 404 +def _out(stream: TextIO | None) -> TextIO: + return stream if stream is not None else sys.stdout + + def parse_filters(raw: list[str]) -> dict[str, str]: out: dict[str, str] = {} for item in raw: @@ -95,7 +99,7 @@ def handle_list( rows, format=ctx.output_format, columns=ctx.resolve_columns(op_tag, op_resource, operation), - stream=stream if stream is not None else sys.stdout, + stream=_out(stream), compact=ctx.compact, ) except (NetBoxAPIError, NetBoxClientError) as exc: @@ -120,7 +124,7 @@ def handle_get( obj, format=ctx.output_format, columns=ctx.resolve_columns(op_tag, op_resource, operation), - stream=stream if stream is not None else sys.stdout, + stream=_out(stream), compact=ctx.compact, ) except (NetBoxAPIError, NetBoxClientError) as exc: @@ -231,7 +235,7 @@ def _handle_write( stream: TextIO | None, require_id: bool, ) -> None: - out = stream if stream is not None else sys.stdout + out = _out(stream) try: if ctx.fetch_all: refuse_all_on_writes(operation_id=operation.operation_id) @@ -395,7 +399,6 @@ def _decide_routing( ) except UnsupportedBulkError as exc: refuse_unsupported_bulk(exc, operation_id=operation.operation_id) - raise # unreachable; refuse_unsupported_bulk always raises if capability is BulkCapability.AMBIGUOUS: print( f"warning: bulk capability for {operation.operation_id} is ambiguous; " diff --git a/nsc/cli/runtime.py b/nsc/cli/runtime.py index 5e37b71..5154161 100644 --- a/nsc/cli/runtime.py +++ b/nsc/cli/runtime.py @@ -2,8 +2,7 @@ `RuntimeContext` carries the live `NetBoxClient`, command model, config, and output preferences for a single invocation. It is populated by the bootstrap -pipeline in the root Typer callback (Task 12) and consumed by the dynamic -read handlers. +pipeline in the root Typer callback and consumed by the dynamic handlers. """ from __future__ import annotations @@ -72,21 +71,12 @@ class UnknownProfileError(Exception): """A profile was requested by name but is not in the config.""" -def resolve_transport_settings( +def _resolve_ssl_and_timeout( config: Config, overrides: CLIOverrides, env: Mapping[str, str], + base: Profile | None, ) -> tuple[bool, float]: - """Compute (verify_ssl, timeout) without requiring URL/token. - - Used by meta commands like `nsc commands` that fetch a schema directly but - don't need a fully-resolved profile. Honours `--insecure`, `NSC_INSECURE`, - and the active profile's `verify_ssl`/`timeout` in the same precedence - order as `resolve_profile`. - """ - name = overrides.profile or env.get("NSC_PROFILE") or config.default_profile - base = config.profiles.get(name) if name else None - insecure_env = _bool_env(env.get("NSC_INSECURE")) if overrides.insecure is not None: verify_ssl = not overrides.insecure @@ -96,11 +86,27 @@ def resolve_transport_settings( verify_ssl = base.verify_ssl else: verify_ssl = True - timeout = base.timeout if (base and base.timeout is not None) else config.defaults.timeout return verify_ssl, timeout +def resolve_transport_settings( + config: Config, + overrides: CLIOverrides, + env: Mapping[str, str], +) -> tuple[bool, float]: + """Compute (verify_ssl, timeout) without requiring URL/token. + + Used by meta commands like `nsc commands` that fetch a schema directly but + don't need a fully-resolved profile. Honours `--insecure`, `NSC_INSECURE`, + and the active profile's `verify_ssl`/`timeout` in the same precedence + order as `resolve_profile`. + """ + name = overrides.profile or env.get("NSC_PROFILE") or config.default_profile + base = config.profiles.get(name) if name else None + return _resolve_ssl_and_timeout(config, overrides, env, base) + + def resolve_profile( config: Config, overrides: CLIOverrides, @@ -116,17 +122,7 @@ def resolve_profile( "pass --url and --token, or configure a profile in ~/.nsc/config.yaml)" ) - insecure_env = _bool_env(env.get("NSC_INSECURE")) - if overrides.insecure is not None: - verify_ssl = not overrides.insecure - elif insecure_env is not None: - verify_ssl = not insecure_env - elif base is not None: - verify_ssl = base.verify_ssl - else: - verify_ssl = True - - timeout = base.timeout if (base and base.timeout is not None) else config.defaults.timeout + verify_ssl, timeout = _resolve_ssl_and_timeout(config, overrides, env, base) schema_url_raw = _first_set( _url_only(overrides.schema_override), @@ -175,7 +171,7 @@ def _url_only(value: str | None) -> str | None: populate the profile's `schema_url` (which is HttpUrl-validated). The schema_override flow consumes the raw value directly from `CLIOverrides`. """ - if value is None or not value: + if not value: return None if value.startswith(("http://", "https://")): return value @@ -297,9 +293,7 @@ def map_error( operation_id=operation_id, details={"cause": "connect", "retry_safe": True}, ) - if isinstance(exc, NoProfileError): - return ErrorEnvelope(error=str(exc), type=ErrorType.CONFIG) - if isinstance(exc, UnknownProfileError): + if isinstance(exc, (NoProfileError, UnknownProfileError)): return ErrorEnvelope(error=str(exc), type=ErrorType.CONFIG) return ErrorEnvelope( error=f"internal error: {exc}",