netbox-super-cli 1.0.4__tar.gz → 1.0.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/CHANGELOG.md +36 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/PKG-INFO +1 -1
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/docs/guides/output-formats.md +14 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/docs/reference/config.md +2 -1
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/_version.py +1 -1
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/cli/globals.py +3 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/cli/handlers.py +18 -10
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/cli/runtime.py +13 -3
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/config/models.py +7 -0
- netbox_super_cli-1.0.6/nsc/output/_console.py +16 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/output/errors.py +11 -8
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/output/explain.py +17 -12
- netbox_super_cli-1.0.6/nsc/output/flatten.py +49 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/output/render.py +2 -1
- netbox_super_cli-1.0.6/nsc/output/table.py +82 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/pyproject.toml +1 -1
- netbox_super_cli-1.0.6/tests/cli/test_globals_color.py +63 -0
- netbox_super_cli-1.0.6/tests/cli/test_runtime_color.py +55 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/writes/test_handler_helpers.py +52 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/config/test_models.py +10 -1
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/e2e/test_login.py +4 -1
- netbox_super_cli-1.0.6/tests/output/test_console.py +40 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/output/test_csv.py +23 -0
- netbox_super_cli-1.0.6/tests/output/test_errors_color.py +47 -0
- netbox_super_cli-1.0.6/tests/output/test_explain_color.py +50 -0
- netbox_super_cli-1.0.6/tests/output/test_flatten.py +88 -0
- netbox_super_cli-1.0.6/tests/output/test_table_color.py +138 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/uv.lock +1 -1
- netbox_super_cli-1.0.4/nsc/output/flatten.py +0 -25
- netbox_super_cli-1.0.4/nsc/output/table.py +0 -50
- netbox_super_cli-1.0.4/tests/output/test_flatten.py +0 -34
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/.github/workflows/agents-md-sync.yml +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/.github/workflows/bench.yml +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/.github/workflows/claude.yml +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/.github/workflows/docs.yml +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/.github/workflows/e2e.yml +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/.github/workflows/lint.yml +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/.github/workflows/release.yml +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/.github/workflows/test.yml +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/.gitignore +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/.pre-commit-config.yaml +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/.python-version +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/AGENTS.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/CLAUDE.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/LICENSE +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/README.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/docs/architecture/caching.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/docs/architecture/command-generation.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/docs/architecture/http-client.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/docs/architecture/overview.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/docs/architecture/schema-loading.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/docs/contributing/adding-bundled-schemas.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/docs/contributing/branching.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/docs/contributing/development.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/docs/contributing/release-process.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/docs/getting-started/concepts.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/docs/getting-started/first-run.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/docs/getting-started/install.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/docs/guides/ci-and-automation.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/docs/guides/managing-profiles.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/docs/guides/using-with-ai-agents.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/docs/guides/working-with-plugins.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/docs/guides/writes-and-safety.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/docs/index.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/docs/reference/cli.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/docs/reference/exit-codes.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/docs/reference/schemas.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/justfile +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/mkdocs.yml +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/__main__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/aliases/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/aliases/resolver.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/auth/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/auth/verify.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/builder/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/builder/build.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/cache/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/cache/store.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/cli/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/cli/aliases_commands.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/cli/app.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/cli/cache_commands.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/cli/commands_dump.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/cli/config_commands.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/cli/init_commands.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/cli/login_commands.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/cli/profiles_commands.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/cli/registration.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/cli/skill_commands.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/cli/writes/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/cli/writes/apply.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/cli/writes/bulk.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/cli/writes/coercion.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/cli/writes/confirmation.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/cli/writes/input.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/cli/writes/preflight.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/config/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/config/loader.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/config/settings.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/config/writer.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/http/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/http/audit.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/http/client.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/http/errors.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/http/retry.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/model/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/model/command_model.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/output/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/output/csv_.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/output/headers.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/output/json_.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/output/jsonl.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/output/yaml_.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/schema/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/schema/hashing.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/schema/loader.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/schema/models.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/schema/source.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/schemas/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/schemas/bundled/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/schemas/bundled/manifest.yaml +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/schemas/bundled/netbox-4.5.10.json.gz +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/schemas/bundled/netbox-4.6.0.json.gz +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/nsc/skill/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/scripts/gen_docs.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/scripts/sync_agents_md.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/skills/netbox-super-cli/SKILL.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/aliases/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/aliases/test_resolver.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/auth/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/auth/test_verify.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/benchmarks/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/benchmarks/test_startup.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/builder/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/builder/test_build.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/builder/test_default_columns.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/builder/test_redaction_marking.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/builder/test_request_body_shape.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cache/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cache/test_prune.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cache/test_store.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/test_aliases_commands.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/test_app_smoke.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/test_cache_commands.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/test_commands_dump.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/test_completion_smoke.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/test_config_commands.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/test_explain.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/test_handlers.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/test_init_commands.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/test_login_commands.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/test_meta_subcommands_under_broken_config.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/test_ndjson_input.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/test_profiles_commands.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/test_registration.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/test_respx_integration.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/test_runtime.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/test_skill_commands.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/test_stdin_sniffer.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/test_writes_respx.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/writes/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/writes/test_apply.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/writes/test_bulk.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/writes/test_confirmation.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/writes/test_handlers_audit.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/writes/test_input.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/cli/writes/test_preflight.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/config/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/config/test_loader.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/config/test_writer_atomicity.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/config/test_writer_dotted_paths.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/config/test_writer_roundtrip.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/conftest.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/e2e/README.md +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/e2e/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/e2e/conftest.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/e2e/docker-compose.yml +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/e2e/test_audit_redaction.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/e2e/test_auth_error_envelope.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/e2e/test_bulk_create_with_loop_fallback.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/e2e/test_full_cycle.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/e2e/test_ndjson.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/e2e/test_preflight_blocks_apply.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/e2e/test_validation_error_envelope_from_netbox.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/e2e/wait_for_netbox.sh +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/fixtures/profiles/single_profile.yaml +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/fixtures/responses/auth_401.json +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/fixtures/responses/circuits_providers_list.json +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/fixtures/responses/dcim_devices_get.json +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/fixtures/responses/dcim_devices_list_p1.json +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/fixtures/responses/dcim_devices_list_p2.json +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/http/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/http/test_audit.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/http/test_audit_redaction.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/http/test_client.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/http/test_client_redaction_threading.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/http/test_errors.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/http/test_retry.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/model/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/model/test_command_model.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/model/test_request_body_shape.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/output/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/output/test_errors.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/output/test_errors_aliases.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/output/test_explain.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/output/test_input_error_envelope.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/output/test_json.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/output/test_jsonl.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/output/test_render_dispatch.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/output/test_table.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/output/test_yaml.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/schema/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/schema/test_hashing.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/schema/test_loader.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/schema/test_models.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/schema/test_source.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/schema/test_source_ttl.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/scripts/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/scripts/test_gen_docs.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/skill/__init__.py +0 -0
- {netbox_super_cli-1.0.4 → netbox_super_cli-1.0.6}/tests/skill/test_bundle.py +0 -0
|
@@ -2,6 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to netbox-super-cli are tracked here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loosely. From v1.0.0 onward, releases follow [Semantic Versioning](https://semver.org/) and the version in `pyproject.toml` matches the git tag. Pre-1.0 milestones (Phase 1-5) were pinned by tag while `pyproject.toml` stayed at `0.0.1`.
|
|
4
4
|
|
|
5
|
+
## v1.0.6 — 2026-06-09
|
|
6
|
+
|
|
7
|
+
Patch release. Fixes table/CSV column rendering for fields that are nested
|
|
8
|
+
objects or lists of objects (issue #78).
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Nested-object and list columns now render in `table`/`csv`** (issue #78).
|
|
13
|
+
A `--columns` (or `columns:` config) entry pointing to a nested object — a
|
|
14
|
+
NetBox brief FK such as `role`, `site`, `tenant`, `device_type` — rendered as
|
|
15
|
+
an empty cell; an entry pointing to a list of objects (e.g. `tags`) rendered
|
|
16
|
+
as a raw JSON blob. Such columns now collapse to the object's `display` label
|
|
17
|
+
(compact-JSON fallback for objects without one), and list columns render their
|
|
18
|
+
members' labels joined with `, ` (empty list → blank). Scalars, nulls, and
|
|
19
|
+
dotted leaf paths (`status.value`, `role.name`) are unchanged, as is the full
|
|
20
|
+
output of the `json`, `jsonl`, and `yaml` formats.
|
|
21
|
+
|
|
22
|
+
## v1.0.5 — 2026-05-21
|
|
23
|
+
|
|
24
|
+
Adds opt-in semantic terminal coloring (issue #24). No config migration is
|
|
25
|
+
needed — `color_mode` defaults to `auto`, which colors a stream only when it
|
|
26
|
+
is a TTY, so piped and redirected output is byte-for-byte unchanged.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **Semantic terminal coloring** (issue #24, PR #72). A new `defaults.color_mode`
|
|
31
|
+
config field — `auto` (default), `on`, or `off` — controls colored output.
|
|
32
|
+
Table cells are colored by meaning: status values (`active`/`enabled`/`online`
|
|
33
|
+
green, `planned`/`staged` yellow, `failed`/`disabled`/`offline` red), booleans,
|
|
34
|
+
and empty cells. `nsc explain` traces and Rich error panels are colored too.
|
|
35
|
+
stdout and stderr are gated independently by each stream's own TTY, so
|
|
36
|
+
`nsc … | jq` and `nsc … 2>err.log` no longer mis-color one stream or leak
|
|
37
|
+
ANSI into the other. Structured formats (`json`, `jsonl`, `yaml`, `csv`) are
|
|
38
|
+
never colored. All server- and user-sourced text is escaped before Rich
|
|
39
|
+
markup parsing, so stray `[`/`]` in API responses cannot garble output.
|
|
40
|
+
|
|
5
41
|
## v1.0.4 — 2026-05-16
|
|
6
42
|
|
|
7
43
|
Maintenance release: a codebase-wide internal simplification pass (no behavioural change), a full documentation parity audit, and a dependency bump. There are no CLI, config, or output-contract changes — upgrading from v1.0.3 is transparent.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: netbox-super-cli
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.6
|
|
4
4
|
Summary: Dynamic NetBox CLI generated from the live OpenAPI schema.
|
|
5
5
|
Project-URL: Homepage, https://github.com/thomaschristory/netbox-super-cli
|
|
6
6
|
Project-URL: Issues, https://github.com/thomaschristory/netbox-super-cli/issues
|
|
@@ -43,6 +43,20 @@ columns:
|
|
|
43
43
|
Unknown column names are silently ignored, so the same config remains valid
|
|
44
44
|
across NetBox versions and plugin changes.
|
|
45
45
|
|
|
46
|
+
### Nested objects and lists in columns
|
|
47
|
+
|
|
48
|
+
A column may name a nested field directly or drill in with a dotted path:
|
|
49
|
+
|
|
50
|
+
- A dotted path selects the leaf scalar: `status.value`, `site.name`.
|
|
51
|
+
- A bare nested object (`site`, `role`, `tenant`, `status`) collapses to its
|
|
52
|
+
`display` label — `role` renders `Router`, not an empty cell. An object with
|
|
53
|
+
no `display` field falls back to compact JSON.
|
|
54
|
+
- A list of objects renders as its members' labels joined with `, ` (e.g.
|
|
55
|
+
`tags` → `prod, edge`); an empty list renders blank.
|
|
56
|
+
|
|
57
|
+
This applies to both `table` and `csv`. The `json`, `jsonl`, and `yaml` formats
|
|
58
|
+
always keep the full nested structure untouched.
|
|
59
|
+
|
|
46
60
|
## Compact JSON
|
|
47
61
|
|
|
48
62
|
```sh
|
|
@@ -11,7 +11,7 @@ All fields below describe `~/.nsc/config.yaml`.
|
|
|
11
11
|
|---|---|---|
|
|
12
12
|
| `default_profile` | `str | None` | `None` |
|
|
13
13
|
| `profiles` | `dict[str, Profile]` | `{}` |
|
|
14
|
-
| `defaults` | `<class 'Defaults'>` | `Defaults(output=<OutputFormat.TABLE: 'table'>, page_size=50, timeout=30.0, schema_refresh=<SchemaRefresh.DAILY: 'daily'>)` |
|
|
14
|
+
| `defaults` | `<class 'Defaults'>` | `Defaults(output=<OutputFormat.TABLE: 'table'>, page_size=50, timeout=30.0, schema_refresh=<SchemaRefresh.DAILY: 'daily'>, color_mode=<ColorMode.AUTO: 'auto'>)` |
|
|
15
15
|
| `columns` | `dict[str, dict[str, list[str]]]` | `{}` |
|
|
16
16
|
|
|
17
17
|
## `Profile`
|
|
@@ -33,3 +33,4 @@ All fields below describe `~/.nsc/config.yaml`.
|
|
|
33
33
|
| `page_size` | `<class 'int'>` | `50` |
|
|
34
34
|
| `timeout` | `<class 'float'>` | `30.0` |
|
|
35
35
|
| `schema_refresh` | `<enum 'SchemaRefresh'>` | `<SchemaRefresh.DAILY: 'daily'>` |
|
|
36
|
+
| `color_mode` | `<enum 'ColorMode'>` | `<ColorMode.AUTO: 'auto'>` |
|
|
@@ -9,6 +9,7 @@ from dataclasses import dataclass
|
|
|
9
9
|
from nsc.cli.runtime import (
|
|
10
10
|
CLIOverrides,
|
|
11
11
|
RuntimeContext,
|
|
12
|
+
resolve_color,
|
|
12
13
|
resolve_profile,
|
|
13
14
|
)
|
|
14
15
|
from nsc.config import default_paths
|
|
@@ -50,6 +51,8 @@ def build_runtime_context(state: GlobalState) -> RuntimeContext:
|
|
|
50
51
|
output_format=output,
|
|
51
52
|
debug=state.debug,
|
|
52
53
|
page_size=state.config.defaults.page_size,
|
|
54
|
+
color=resolve_color(state.config.defaults.color_mode, is_tty=sys.stdout.isatty()),
|
|
55
|
+
color_stderr=resolve_color(state.config.defaults.color_mode, is_tty=sys.stderr.isatty()),
|
|
53
56
|
)
|
|
54
57
|
|
|
55
58
|
|
|
@@ -38,6 +38,7 @@ from nsc.config.settings import default_paths
|
|
|
38
38
|
from nsc.http.audit import AuditEntry, append_audit_jsonl
|
|
39
39
|
from nsc.http.errors import NetBoxAPIError, NetBoxClientError
|
|
40
40
|
from nsc.model.command_model import HttpMethod, Operation, ParameterLocation
|
|
41
|
+
from nsc.output._console import make_console
|
|
41
42
|
from nsc.output.errors import (
|
|
42
43
|
ClientError,
|
|
43
44
|
ErrorEnvelope,
|
|
@@ -101,10 +102,11 @@ def handle_list(
|
|
|
101
102
|
columns=ctx.resolve_columns(op_tag, op_resource, operation),
|
|
102
103
|
stream=_out(stream),
|
|
103
104
|
compact=ctx.compact,
|
|
105
|
+
color=ctx.color,
|
|
104
106
|
)
|
|
105
107
|
except (NetBoxAPIError, NetBoxClientError) as exc:
|
|
106
108
|
env = map_error(exc, operation_id=operation.operation_id)
|
|
107
|
-
code = emit_envelope(env, output_format=ctx.output_format)
|
|
109
|
+
code = emit_envelope(env, output_format=ctx.output_format, color=ctx.color_stderr)
|
|
108
110
|
raise typer.Exit(code) from exc
|
|
109
111
|
|
|
110
112
|
|
|
@@ -126,10 +128,11 @@ def handle_get(
|
|
|
126
128
|
columns=ctx.resolve_columns(op_tag, op_resource, operation),
|
|
127
129
|
stream=_out(stream),
|
|
128
130
|
compact=ctx.compact,
|
|
131
|
+
color=ctx.color,
|
|
129
132
|
)
|
|
130
133
|
except (NetBoxAPIError, NetBoxClientError) as exc:
|
|
131
134
|
env = map_error(exc, operation_id=operation.operation_id)
|
|
132
|
-
code = emit_envelope(env, output_format=ctx.output_format)
|
|
135
|
+
code = emit_envelope(env, output_format=ctx.output_format, color=ctx.color_stderr)
|
|
133
136
|
raise typer.Exit(code) from exc
|
|
134
137
|
|
|
135
138
|
|
|
@@ -316,7 +319,7 @@ def _handle_write(
|
|
|
316
319
|
is_delete=is_delete,
|
|
317
320
|
)
|
|
318
321
|
except ClientError as exc:
|
|
319
|
-
code = emit_envelope(exc.envelope, output_format=ctx.output_format)
|
|
322
|
+
code = emit_envelope(exc.envelope, output_format=ctx.output_format, color=ctx.color_stderr)
|
|
320
323
|
raise typer.Exit(code) from exc
|
|
321
324
|
except NDJSONParseError as exc:
|
|
322
325
|
env = input_error_envelope(
|
|
@@ -324,11 +327,11 @@ def _handle_write(
|
|
|
324
327
|
bad_lines=exc.bad_lines,
|
|
325
328
|
operation_id=operation.operation_id,
|
|
326
329
|
)
|
|
327
|
-
code = emit_envelope(env, output_format=ctx.output_format)
|
|
330
|
+
code = emit_envelope(env, output_format=ctx.output_format, color=ctx.color_stderr)
|
|
328
331
|
raise typer.Exit(code) from exc
|
|
329
332
|
except InputError as exc:
|
|
330
333
|
env = client_envelope(str(exc), operation_id=operation.operation_id)
|
|
331
|
-
code = emit_envelope(env, output_format=ctx.output_format)
|
|
334
|
+
code = emit_envelope(env, output_format=ctx.output_format, color=ctx.color_stderr)
|
|
332
335
|
raise typer.Exit(code) from exc
|
|
333
336
|
except (NetBoxAPIError, NetBoxClientError) as exc:
|
|
334
337
|
if (
|
|
@@ -340,7 +343,7 @@ def _handle_write(
|
|
|
340
343
|
_render_delete_already_absent(ctx, stream=out)
|
|
341
344
|
return
|
|
342
345
|
env = map_error(exc, operation_id=operation.operation_id)
|
|
343
|
-
code = emit_envelope(env, output_format=ctx.output_format)
|
|
346
|
+
code = emit_envelope(env, output_format=ctx.output_format, color=ctx.color_stderr)
|
|
344
347
|
raise typer.Exit(code) from exc
|
|
345
348
|
|
|
346
349
|
|
|
@@ -364,13 +367,13 @@ def _handle_dry_run_or_preflight(
|
|
|
364
367
|
_render_explain_or_dry_run(trace, ctx, stream=out)
|
|
365
368
|
if not preflight.ok:
|
|
366
369
|
env = _preflight_envelope(operation, preflight, applied=False)
|
|
367
|
-
code = emit_envelope(env, output_format=ctx.output_format)
|
|
370
|
+
code = emit_envelope(env, output_format=ctx.output_format, color=ctx.color_stderr)
|
|
368
371
|
raise typer.Exit(code)
|
|
369
372
|
return True
|
|
370
373
|
if not preflight.ok:
|
|
371
374
|
_emit_dry_run_audit(operation, resolved, preflight, ctx, preflight_blocked=True)
|
|
372
375
|
env = _preflight_envelope(operation, preflight, applied=False)
|
|
373
|
-
code = emit_envelope(env, output_format=ctx.output_format)
|
|
376
|
+
code = emit_envelope(env, output_format=ctx.output_format, color=ctx.color_stderr)
|
|
374
377
|
raise typer.Exit(code)
|
|
375
378
|
return False
|
|
376
379
|
|
|
@@ -480,7 +483,7 @@ def _execute_loop(
|
|
|
480
483
|
operation_id=operation.operation_id,
|
|
481
484
|
total_records=total_records,
|
|
482
485
|
)
|
|
483
|
-
code = emit_envelope(env, output_format=ctx.output_format)
|
|
486
|
+
code = emit_envelope(env, output_format=ctx.output_format, color=ctx.color_stderr)
|
|
484
487
|
raise typer.Exit(code)
|
|
485
488
|
|
|
486
489
|
|
|
@@ -539,7 +542,7 @@ def _render_explain_or_dry_run(trace: ExplainTrace, ctx: RuntimeContext, *, stre
|
|
|
539
542
|
if ctx.output_format is OutputFormat.JSON:
|
|
540
543
|
print(render_explain_json(trace), file=stream)
|
|
541
544
|
elif ctx.output_format is OutputFormat.TABLE:
|
|
542
|
-
render_explain_rich(trace, stream=stream)
|
|
545
|
+
render_explain_rich(trace, stream=stream, color=ctx.color)
|
|
543
546
|
else:
|
|
544
547
|
# CSV/YAML/JSONL on dry-run → JSON to stdout (the formatters expect rows,
|
|
545
548
|
# not a structured trace). Consistent with spec §4.2.3 fallback rules.
|
|
@@ -607,6 +610,7 @@ def _render_response(
|
|
|
607
610
|
columns=ctx.resolve_columns(op_tag, op_resource, operation),
|
|
608
611
|
stream=stream,
|
|
609
612
|
compact=ctx.compact,
|
|
613
|
+
color=ctx.color,
|
|
610
614
|
)
|
|
611
615
|
|
|
612
616
|
|
|
@@ -614,6 +618,8 @@ def _render_delete_ok(ctx: RuntimeContext, *, stream: TextIO) -> None:
|
|
|
614
618
|
payload = {"deleted": True}
|
|
615
619
|
if ctx.output_format is OutputFormat.JSON:
|
|
616
620
|
print(_json.dumps(payload), file=stream)
|
|
621
|
+
elif ctx.output_format is OutputFormat.TABLE and ctx.color:
|
|
622
|
+
make_console(stream, color=True).print("[yellow]deleted[/]")
|
|
617
623
|
else:
|
|
618
624
|
print("deleted", file=stream)
|
|
619
625
|
|
|
@@ -622,6 +628,8 @@ def _render_delete_already_absent(ctx: RuntimeContext, *, stream: TextIO) -> Non
|
|
|
622
628
|
payload = {"deleted": False, "reason": "already_absent"}
|
|
623
629
|
if ctx.output_format is OutputFormat.JSON:
|
|
624
630
|
print(_json.dumps(payload), file=stream)
|
|
631
|
+
elif ctx.output_format is OutputFormat.TABLE and ctx.color:
|
|
632
|
+
make_console(stream, color=True).print("[dim]already absent (no change)[/]")
|
|
625
633
|
else:
|
|
626
634
|
print("already absent (no change)", file=stream)
|
|
627
635
|
|
|
@@ -15,7 +15,7 @@ from typing import Any, Literal
|
|
|
15
15
|
from pydantic import BaseModel, ConfigDict, HttpUrl, SkipValidation
|
|
16
16
|
|
|
17
17
|
from nsc.cache.store import ADHOC_PROFILE
|
|
18
|
-
from nsc.config.models import Config, OutputFormat, Profile
|
|
18
|
+
from nsc.config.models import ColorMode, Config, OutputFormat, Profile
|
|
19
19
|
from nsc.config.settings import default_paths
|
|
20
20
|
from nsc.http.client import NetBoxClient
|
|
21
21
|
from nsc.http.errors import NetBoxAPIError, NetBoxClientError
|
|
@@ -178,6 +178,14 @@ def _url_only(value: str | None) -> str | None:
|
|
|
178
178
|
return None
|
|
179
179
|
|
|
180
180
|
|
|
181
|
+
def resolve_color(mode: ColorMode, *, is_tty: bool) -> bool:
|
|
182
|
+
if mode is ColorMode.ON:
|
|
183
|
+
return True
|
|
184
|
+
if mode is ColorMode.OFF:
|
|
185
|
+
return False
|
|
186
|
+
return is_tty
|
|
187
|
+
|
|
188
|
+
|
|
181
189
|
class RuntimeContext(BaseModel):
|
|
182
190
|
"""Per-invocation runtime state.
|
|
183
191
|
|
|
@@ -198,6 +206,8 @@ class RuntimeContext(BaseModel):
|
|
|
198
206
|
limit: int | None = None
|
|
199
207
|
fetch_all: bool = False
|
|
200
208
|
compact: bool = False
|
|
209
|
+
color: bool = False
|
|
210
|
+
color_stderr: bool = False
|
|
201
211
|
apply: bool = False
|
|
202
212
|
explain: bool = False
|
|
203
213
|
strict: bool = False
|
|
@@ -302,7 +312,7 @@ def map_error(
|
|
|
302
312
|
)
|
|
303
313
|
|
|
304
314
|
|
|
305
|
-
def emit_envelope(env: ErrorEnvelope, *, output_format: OutputFormat) -> int:
|
|
315
|
+
def emit_envelope(env: ErrorEnvelope, *, output_format: OutputFormat, color: bool = False) -> int:
|
|
306
316
|
"""Write the envelope to the right target and return the exit code."""
|
|
307
317
|
target = select_render_target(output_format=output_format, stdout_is_tty=sys.stdout.isatty())
|
|
308
318
|
if target is RenderTarget.JSON_STDOUT:
|
|
@@ -310,5 +320,5 @@ def emit_envelope(env: ErrorEnvelope, *, output_format: OutputFormat) -> int:
|
|
|
310
320
|
elif target is RenderTarget.JSON_STDERR:
|
|
311
321
|
print(render_to_json(env), file=sys.stderr)
|
|
312
322
|
else:
|
|
313
|
-
render_to_rich_stderr(env, stream=sys.stderr)
|
|
323
|
+
render_to_rich_stderr(env, stream=sys.stderr, color=color)
|
|
314
324
|
return EXIT_CODES.get(env.type, 1)
|
|
@@ -31,11 +31,18 @@ class SchemaRefresh(StrEnum):
|
|
|
31
31
|
WEEKLY = "weekly"
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
class ColorMode(StrEnum):
|
|
35
|
+
AUTO = "auto"
|
|
36
|
+
ON = "on"
|
|
37
|
+
OFF = "off"
|
|
38
|
+
|
|
39
|
+
|
|
34
40
|
class Defaults(_Frozen):
|
|
35
41
|
output: OutputFormat = OutputFormat.TABLE
|
|
36
42
|
page_size: int = 50
|
|
37
43
|
timeout: float = 30.0
|
|
38
44
|
schema_refresh: SchemaRefresh = SchemaRefresh.DAILY
|
|
45
|
+
color_mode: ColorMode = ColorMode.AUTO
|
|
39
46
|
|
|
40
47
|
|
|
41
48
|
class Profile(_Frozen):
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Shared Rich Console factory used across all output formatters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TextIO
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def make_console(stream: TextIO, *, color: bool, soft_wrap: bool = False) -> Console:
|
|
11
|
+
# highlight=False on both paths: Rich's auto-highlighter bolds parens and
|
|
12
|
+
# colorizes numbers/IPs in plain strings, which would corrupt our output and
|
|
13
|
+
# fight the explicit semantic markup applied by the formatters.
|
|
14
|
+
if color:
|
|
15
|
+
return Console(file=stream, force_terminal=True, highlight=False, soft_wrap=soft_wrap)
|
|
16
|
+
return Console(file=stream, no_color=True, highlight=False, soft_wrap=soft_wrap)
|
|
@@ -10,11 +10,12 @@ from enum import Enum, StrEnum
|
|
|
10
10
|
from typing import Any, Literal, TextIO
|
|
11
11
|
|
|
12
12
|
from pydantic import BaseModel, ConfigDict, Field
|
|
13
|
-
from rich.
|
|
13
|
+
from rich.markup import escape
|
|
14
14
|
from rich.panel import Panel
|
|
15
15
|
|
|
16
16
|
from nsc.config.models import OutputFormat
|
|
17
17
|
from nsc.model.command_model import HttpMethod
|
|
18
|
+
from nsc.output._console import make_console
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
class ErrorType(StrEnum):
|
|
@@ -97,25 +98,27 @@ def select_render_target(*, output_format: OutputFormat, stdout_is_tty: bool) ->
|
|
|
97
98
|
return RenderTarget.JSON_STDERR
|
|
98
99
|
|
|
99
100
|
|
|
100
|
-
def render_to_rich_stderr(env: ErrorEnvelope, *, stream: TextIO) -> None:
|
|
101
|
-
console =
|
|
101
|
+
def render_to_rich_stderr(env: ErrorEnvelope, *, stream: TextIO, color: bool = False) -> None:
|
|
102
|
+
console = make_console(stream, color=color, soft_wrap=True)
|
|
103
|
+
# env.error/endpoint/operation_id/audit_log_path/details carry server- and
|
|
104
|
+
# user-sourced text; escape it so stray [..] is not parsed as Rich markup.
|
|
102
105
|
body_lines = [
|
|
103
|
-
f"[bold red]{env.type.value}[/]: {env.error}",
|
|
106
|
+
f"[bold red]{env.type.value}[/]: {escape(env.error)}",
|
|
104
107
|
]
|
|
105
108
|
if env.endpoint:
|
|
106
|
-
body_lines.append(f"endpoint: {env.endpoint}")
|
|
109
|
+
body_lines.append(f"endpoint: {escape(env.endpoint)}")
|
|
107
110
|
if env.method is not None:
|
|
108
111
|
body_lines.append(f"method: {env.method.value}")
|
|
109
112
|
if env.status_code is not None:
|
|
110
113
|
body_lines.append(f"status: {env.status_code}")
|
|
111
114
|
if env.operation_id:
|
|
112
|
-
body_lines.append(f"op: {env.operation_id}")
|
|
115
|
+
body_lines.append(f"op: {escape(env.operation_id)}")
|
|
113
116
|
if env.attempt_n is not None:
|
|
114
117
|
body_lines.append(f"attempt: {env.attempt_n}")
|
|
115
118
|
if env.audit_log_path:
|
|
116
|
-
body_lines.append(f"audit: {env.audit_log_path}")
|
|
119
|
+
body_lines.append(f"audit: {escape(env.audit_log_path)}")
|
|
117
120
|
if env.details:
|
|
118
|
-
body_lines.append(f"details: {env.details}")
|
|
121
|
+
body_lines.append(f"details: {escape(str(env.details))}")
|
|
119
122
|
console.print(Panel("\n".join(body_lines), title="nsc error", border_style="red"))
|
|
120
123
|
|
|
121
124
|
|
|
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
from typing import Any, Literal, TextIO
|
|
12
12
|
|
|
13
13
|
from pydantic import BaseModel, ConfigDict, Field
|
|
14
|
-
from rich.
|
|
14
|
+
from rich.markup import escape
|
|
15
15
|
from rich.panel import Panel
|
|
16
16
|
|
|
17
17
|
from nsc.cli.writes.apply import ResolvedRequest
|
|
@@ -19,6 +19,7 @@ from nsc.cli.writes.bulk import RoutingDecision
|
|
|
19
19
|
from nsc.cli.writes.input import RawWriteInput
|
|
20
20
|
from nsc.cli.writes.preflight import PreflightResult
|
|
21
21
|
from nsc.model.command_model import Operation
|
|
22
|
+
from nsc.output._console import make_console
|
|
22
23
|
|
|
23
24
|
DECISION_CAP = 200
|
|
24
25
|
|
|
@@ -151,26 +152,28 @@ def render_to_json(trace: ExplainTrace) -> str:
|
|
|
151
152
|
return trace.model_dump_json()
|
|
152
153
|
|
|
153
154
|
|
|
154
|
-
def render_to_rich_stdout(trace: ExplainTrace, *, stream: TextIO) -> None:
|
|
155
|
-
console =
|
|
155
|
+
def render_to_rich_stdout(trace: ExplainTrace, *, stream: TextIO, color: bool = False) -> None:
|
|
156
|
+
console = make_console(stream, color=color, soft_wrap=True)
|
|
157
|
+
# Reasoning strings and decision values carry user/schema/server text that
|
|
158
|
+
# may contain [..]; escape it so it is not parsed as Rich markup.
|
|
156
159
|
body = [
|
|
157
|
-
f"[bold cyan]operation:[/] {trace.operation_id}",
|
|
160
|
+
f"[bold cyan]operation:[/] {escape(trace.operation_id)}",
|
|
158
161
|
]
|
|
159
162
|
if trace.operation_summary:
|
|
160
|
-
body.append(f"summary: {trace.operation_summary}")
|
|
161
|
-
body.append(f"method: {trace.method_reasoning}")
|
|
162
|
-
body.append(f"url: {trace.url_reasoning}")
|
|
163
|
+
body.append(f"summary: {escape(trace.operation_summary)}")
|
|
164
|
+
body.append(f"method: {escape(trace.method_reasoning)}")
|
|
165
|
+
body.append(f"url: {escape(trace.url_reasoning)}")
|
|
163
166
|
if trace.bulk_reasoning:
|
|
164
|
-
body.append(f"bulk: {trace.bulk_reasoning}")
|
|
167
|
+
body.append(f"bulk: {escape(trace.bulk_reasoning)}")
|
|
165
168
|
for r in trace.requests:
|
|
166
|
-
body.append(f" → {r.method.value} {r.url}")
|
|
169
|
+
body.append(f" → {r.method.value} {escape(r.url)}")
|
|
167
170
|
if r.body is not None:
|
|
168
|
-
body.append(f" body: {r.body}")
|
|
171
|
+
body.append(f" body: {escape(str(r.body))}")
|
|
169
172
|
if trace.preflight is not None and not trace.preflight.ok:
|
|
170
173
|
body.append("preflight: [red]FAIL[/]")
|
|
171
174
|
for issue in trace.preflight.issues:
|
|
172
175
|
loc = f"records[{issue.record_index}].{issue.field_path}"
|
|
173
|
-
body.append(f" [{issue.kind}] {loc}: {issue.message}")
|
|
176
|
+
body.append(escape(f" [{issue.kind}] {loc}: {issue.message}"))
|
|
174
177
|
elif trace.preflight is not None:
|
|
175
178
|
body.append("preflight: [green]OK[/]")
|
|
176
179
|
if trace.decisions:
|
|
@@ -178,7 +181,9 @@ def render_to_rich_stdout(trace: ExplainTrace, *, stream: TextIO) -> None:
|
|
|
178
181
|
for d in trace.decisions:
|
|
179
182
|
extra = f" — {d.note}" if d.note else ""
|
|
180
183
|
body.append(
|
|
181
|
-
|
|
184
|
+
escape(
|
|
185
|
+
f" {d.field_path} [{d.source}] {d.raw_value!r} → {d.resolved_value!r}{extra}"
|
|
186
|
+
)
|
|
182
187
|
)
|
|
183
188
|
if trace.decisions_truncated:
|
|
184
189
|
body.append(f" … truncated at {DECISION_CAP} entries")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Shared dotted-path flattener used by table and csv formatters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def flatten(record: dict[str, Any], *, columns: list[str] | None = None) -> dict[str, Any]:
|
|
10
|
+
if columns is None:
|
|
11
|
+
flat: dict[str, Any] = {}
|
|
12
|
+
_walk(record, "", flat)
|
|
13
|
+
return flat
|
|
14
|
+
return {col: _select(record, col) for col in columns}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _walk(value: Any, prefix: str, out: dict[str, Any]) -> None:
|
|
18
|
+
if isinstance(value, dict):
|
|
19
|
+
for k, v in value.items():
|
|
20
|
+
child = f"{prefix}.{k}" if prefix else k
|
|
21
|
+
_walk(v, child, out)
|
|
22
|
+
elif isinstance(value, list):
|
|
23
|
+
out[prefix] = json.dumps(value)
|
|
24
|
+
else:
|
|
25
|
+
out[prefix] = value
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _select(record: dict[str, Any], path: str) -> Any:
|
|
29
|
+
cur: Any = record
|
|
30
|
+
for part in path.split("."):
|
|
31
|
+
if isinstance(cur, dict) and part in cur:
|
|
32
|
+
cur = cur[part]
|
|
33
|
+
else:
|
|
34
|
+
return ""
|
|
35
|
+
return _displayify(cur)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _displayify(value: Any) -> Any:
|
|
39
|
+
if isinstance(value, dict):
|
|
40
|
+
display = value.get("display")
|
|
41
|
+
return display if isinstance(display, str) else json.dumps(value, separators=(",", ":"))
|
|
42
|
+
if isinstance(value, list):
|
|
43
|
+
return ", ".join(_as_cell(v) for v in value)
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _as_cell(value: Any) -> str:
|
|
48
|
+
rendered = _displayify(value)
|
|
49
|
+
return "" if rendered is None else str(rendered)
|
|
@@ -16,6 +16,7 @@ def render(
|
|
|
16
16
|
columns: list[str] | None = None,
|
|
17
17
|
stream: TextIO = sys.stdout,
|
|
18
18
|
compact: bool = False,
|
|
19
|
+
color: bool = False,
|
|
19
20
|
) -> None:
|
|
20
21
|
if format is OutputFormat.JSON:
|
|
21
22
|
json_.render(data, stream=stream, compact=compact)
|
|
@@ -26,7 +27,7 @@ def render(
|
|
|
26
27
|
elif format is OutputFormat.CSV:
|
|
27
28
|
csv_.render(data, stream=stream, columns=columns)
|
|
28
29
|
elif format is OutputFormat.TABLE:
|
|
29
|
-
table.render(data, stream=stream, columns=columns)
|
|
30
|
+
table.render(data, stream=stream, columns=columns, color=color)
|
|
30
31
|
else: # pragma: no cover (StrEnum exhaustively covered above)
|
|
31
32
|
raise ValueError(f"unknown output format: {format!r}")
|
|
32
33
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Rich table output formatter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Any, TextIO
|
|
7
|
+
|
|
8
|
+
from rich.markup import escape
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from nsc.output._console import make_console
|
|
12
|
+
from nsc.output.flatten import flatten
|
|
13
|
+
|
|
14
|
+
_STATUS_COLORS: dict[str, str] = {
|
|
15
|
+
"active": "green",
|
|
16
|
+
"enabled": "green",
|
|
17
|
+
"online": "green",
|
|
18
|
+
"connected": "green",
|
|
19
|
+
"planned": "yellow",
|
|
20
|
+
"staged": "yellow",
|
|
21
|
+
"decommissioning": "yellow",
|
|
22
|
+
"failed": "red",
|
|
23
|
+
"disabled": "red",
|
|
24
|
+
"offline": "red",
|
|
25
|
+
"error": "red",
|
|
26
|
+
"true": "green",
|
|
27
|
+
"false": "dim",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def render(
|
|
32
|
+
data: list[dict[str, Any]] | dict[str, Any],
|
|
33
|
+
*,
|
|
34
|
+
stream: TextIO = sys.stdout,
|
|
35
|
+
columns: list[str] | None = None,
|
|
36
|
+
color: bool = False,
|
|
37
|
+
) -> None:
|
|
38
|
+
records = [data] if isinstance(data, dict) else list(data)
|
|
39
|
+
if not records:
|
|
40
|
+
make_console(stream, color=color).print("(no records)")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
flat_records = [flatten(r, columns=columns) for r in records]
|
|
44
|
+
fieldnames = columns if columns is not None else _gather_fieldnames(flat_records)
|
|
45
|
+
|
|
46
|
+
table = Table(show_header=True, header_style="bold")
|
|
47
|
+
for col in fieldnames:
|
|
48
|
+
table.add_column(col)
|
|
49
|
+
for r in flat_records:
|
|
50
|
+
table.add_row(*[_format_cell(r.get(col, ""), color=color) for col in fieldnames])
|
|
51
|
+
|
|
52
|
+
make_console(stream, color=color).print(table)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _gather_fieldnames(records: list[dict[str, Any]]) -> list[str]:
|
|
56
|
+
seen: dict[str, None] = {}
|
|
57
|
+
for r in records:
|
|
58
|
+
for k in r:
|
|
59
|
+
seen.setdefault(k, None)
|
|
60
|
+
return list(seen.keys())
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _format_cell(value: Any, *, color: bool = False) -> str:
|
|
64
|
+
if value is None:
|
|
65
|
+
text = ""
|
|
66
|
+
elif isinstance(value, bool):
|
|
67
|
+
text = "true" if value else "false"
|
|
68
|
+
else:
|
|
69
|
+
text = str(value)
|
|
70
|
+
|
|
71
|
+
# Rich parses markup in table cells regardless of the no-color setting, so
|
|
72
|
+
# arbitrary cell values must be escaped on every return path.
|
|
73
|
+
if not color:
|
|
74
|
+
return escape(text)
|
|
75
|
+
|
|
76
|
+
if not text:
|
|
77
|
+
return "[dim]-[/]"
|
|
78
|
+
|
|
79
|
+
style = _STATUS_COLORS.get(text.lower())
|
|
80
|
+
if style:
|
|
81
|
+
return f"[{style}]{escape(text)}[/]"
|
|
82
|
+
return escape(text)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from nsc.cli import globals as globals_mod
|
|
8
|
+
from nsc.cli.globals import GlobalState, build_runtime_context
|
|
9
|
+
from nsc.cli.runtime import CLIOverrides
|
|
10
|
+
from nsc.config.models import Config
|
|
11
|
+
from nsc.model.command_model import CommandModel
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _state() -> GlobalState:
|
|
15
|
+
return GlobalState(overrides=CLIOverrides(), config=Config(), debug=False)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _stub_command_model(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
19
|
+
monkeypatch.setattr(
|
|
20
|
+
globals_mod,
|
|
21
|
+
"resolve_command_model",
|
|
22
|
+
lambda **kwargs: CommandModel(info_title="t", info_version="v", schema_hash="x"),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_build_runtime_context_gates_stderr_color_independently(
|
|
27
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""color (stdout) and color_stderr (stderr) must each follow their own TTY.
|
|
30
|
+
|
|
31
|
+
Regression: build_runtime_context derived both flags from sys.stdout.isatty(),
|
|
32
|
+
so `nsc ... 2>err.log` (stdout a TTY, stderr a file) leaked ANSI into the file.
|
|
33
|
+
"""
|
|
34
|
+
monkeypatch.setenv("NSC_URL", "https://nb.example/")
|
|
35
|
+
monkeypatch.setenv("NSC_TOKEN", "tok")
|
|
36
|
+
for var in ("NSC_PROFILE", "NSC_SCHEMA", "NSC_OUTPUT"):
|
|
37
|
+
monkeypatch.delenv(var, raising=False)
|
|
38
|
+
_stub_command_model(monkeypatch)
|
|
39
|
+
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
|
40
|
+
monkeypatch.setattr(sys.stderr, "isatty", lambda: False)
|
|
41
|
+
|
|
42
|
+
ctx = build_runtime_context(_state())
|
|
43
|
+
|
|
44
|
+
assert ctx.color is True
|
|
45
|
+
assert ctx.color_stderr is False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_build_runtime_context_gates_stdout_color_independently(
|
|
49
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""The reverse: stdout piped, stderr a TTY — error panels stay colored."""
|
|
52
|
+
monkeypatch.setenv("NSC_URL", "https://nb.example/")
|
|
53
|
+
monkeypatch.setenv("NSC_TOKEN", "tok")
|
|
54
|
+
for var in ("NSC_PROFILE", "NSC_SCHEMA", "NSC_OUTPUT"):
|
|
55
|
+
monkeypatch.delenv(var, raising=False)
|
|
56
|
+
_stub_command_model(monkeypatch)
|
|
57
|
+
monkeypatch.setattr(sys.stdout, "isatty", lambda: False)
|
|
58
|
+
monkeypatch.setattr(sys.stderr, "isatty", lambda: True)
|
|
59
|
+
|
|
60
|
+
ctx = build_runtime_context(_state())
|
|
61
|
+
|
|
62
|
+
assert ctx.color is False
|
|
63
|
+
assert ctx.color_stderr is True
|