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
5 changes: 0 additions & 5 deletions nsc/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
11 changes: 3 additions & 8 deletions nsc/cli/commands_dump.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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,
Expand Down
12 changes: 1 addition & 11 deletions nsc/cli/globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -58,12 +54,6 @@ def build_runtime_context(state: GlobalState) -> RuntimeContext:


__all__ = [
"ConfigParseError",
"GlobalState",
"NetBoxClient",
"NoProfileError",
"ResolvedProfile",
"SchemaSourceError",
"UnknownProfileError",
"build_runtime_context",
]
11 changes: 7 additions & 4 deletions nsc/cli/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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; "
Expand Down
52 changes: 23 additions & 29 deletions nsc/cli/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}",
Expand Down
Loading