netbox-super-cli 1.3.1__tar.gz → 1.4.0__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.3.1 → netbox_super_cli-1.4.0}/CHANGELOG.md +32 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/PKG-INFO +57 -4
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/README.md +56 -3
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/reference/config.md +3 -1
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/_version.py +1 -1
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/cli/app.py +16 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/cli/globals.py +13 -2
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/cli/handlers.py +3 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/cli/registration.py +51 -6
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/cli/runtime.py +19 -1
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/cli/tui_commands.py +51 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/config/models.py +14 -0
- netbox_super_cli-1.4.0/nsc/config/saved_searches.py +54 -0
- netbox_super_cli-1.4.0/nsc/output/colors.py +36 -0
- netbox_super_cli-1.4.0/nsc/output/flatten.py +72 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/output/render.py +2 -1
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/output/table.py +30 -1
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/__init__.py +11 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/app.py +34 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/columns.py +23 -9
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/keymap.py +5 -1
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/screens/filter.py +74 -1
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/screens/list.py +13 -3
- netbox_super_cli-1.4.0/nsc/tui/screens/saved_search_picker.py +92 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/styles.tcss +24 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/view.py +27 -4
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/widgets/help.py +1 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/pyproject.toml +1 -1
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/skills/netbox-super-cli/SKILL.md +5 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/test_registration.py +74 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/test_runtime_color.py +26 -2
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/test_tui_command.py +71 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/config/test_models.py +36 -1
- netbox_super_cli-1.4.0/tests/config/test_saved_searches.py +86 -0
- netbox_super_cli-1.4.0/tests/output/test_colors.py +28 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/output/test_flatten.py +97 -0
- netbox_super_cli-1.4.0/tests/output/test_table_color.py +277 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_app.py +47 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_columns.py +49 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_filter_screen.py +118 -2
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_keymap.py +6 -4
- netbox_super_cli-1.4.0/tests/tui/test_view.py +87 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/uv.lock +1 -1
- netbox_super_cli-1.3.1/nsc/output/flatten.py +0 -53
- netbox_super_cli-1.3.1/tests/output/test_table_color.py +0 -138
- netbox_super_cli-1.3.1/tests/tui/test_view.py +0 -27
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/.github/dependabot.yml +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/.github/workflows/bench.yml +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/.github/workflows/claude.yml +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/.github/workflows/docs.yml +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/.github/workflows/e2e.yml +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/.github/workflows/lint.yml +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/.github/workflows/release.yml +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/.github/workflows/skill-convention-review.yml +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/.github/workflows/test.yml +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/.gitignore +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/.pre-commit-config.yaml +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/.python-version +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/AGENTS.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/CLAUDE.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/LICENSE +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/architecture/caching.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/architecture/command-generation.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/architecture/http-client.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/architecture/overview.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/architecture/schema-loading.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/assets/tui/detail.svg +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/assets/tui/filter.svg +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/assets/tui/list.svg +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/assets/tui/picker.svg +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/assets/tui/search.svg +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/contributing/adding-bundled-schemas.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/contributing/branching.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/contributing/development.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/contributing/release-process.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/getting-started/concepts.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/getting-started/first-run.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/getting-started/install.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/guides/audit-log.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/guides/ci-and-automation.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/guides/interactive-tui.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/guides/managing-profiles.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/guides/output-formats.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/guides/using-with-ai-agents.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/guides/working-with-plugins.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/guides/writes-and-safety.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/index.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/reference/cli.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/reference/exit-codes.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/docs/reference/schemas.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/justfile +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/mkdocs.yml +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/__main__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/aliases/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/aliases/resolver.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/auth/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/auth/verify.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/builder/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/builder/build.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/cache/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/cache/store.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/cli/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/cli/aliases_commands.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/cli/cache_commands.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/cli/commands_dump.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/cli/config_commands.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/cli/init_commands.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/cli/login_commands.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/cli/profiles_commands.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/cli/skill_commands.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/cli/writes/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/cli/writes/apply.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/cli/writes/bulk.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/cli/writes/coercion.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/cli/writes/confirmation.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/cli/writes/input.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/cli/writes/preflight.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/completion/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/completion/cache_probe.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/completion/callbacks.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/completion/providers.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/config/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/config/loader.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/config/settings.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/config/writer.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/http/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/http/audit.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/http/client.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/http/errors.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/http/retry.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/model/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/model/command_model.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/output/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/output/_console.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/output/csv_.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/output/errors.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/output/explain.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/output/headers.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/output/json_.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/output/jsonl.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/output/yaml_.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/schema/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/schema/hashing.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/schema/loader.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/schema/models.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/schema/source.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/schemas/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/schemas/bundled/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/schemas/bundled/manifest.yaml +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/schemas/bundled/netbox-4.5.10.json.gz +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/schemas/bundled/netbox-4.6.0.json.gz +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/skill/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/_bindings.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/bulk.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/catalog.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/errors.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/filters.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/fk.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/forms.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/nav.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/relations.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/screens/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/screens/bulk_edit_form.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/screens/columns.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/screens/detail.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/screens/edit_form.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/screens/global_search.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/screens/picker.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/screens/record_picker.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/search.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/selection.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/widgets/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/widgets/_modal.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/widgets/bulk_diff.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/widgets/bulk_summary.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/widgets/confirm.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/widgets/diff.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/nsc/tui/widgets/nav_tree.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/scripts/gen_docs.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/scripts/sync_agents_md.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/aliases/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/aliases/test_resolver.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/audit/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/audit/test_redaction_full.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/auth/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/auth/test_verify.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/benchmarks/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/benchmarks/test_startup.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/builder/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/builder/test_build.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/builder/test_default_columns.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/builder/test_redaction_marking.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/builder/test_request_body_shape.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cache/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cache/test_prune.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cache/test_store.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/test_aliases_commands.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/test_app_smoke.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/test_cache_commands.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/test_commands_dump.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/test_completion_protocol_smoke.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/test_completion_smoke.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/test_config_commands.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/test_dynamic_completion.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/test_explain.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/test_globals_color.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/test_handlers.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/test_init_commands.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/test_lazy_textual_import.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/test_login_commands.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/test_meta_subcommands_under_broken_config.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/test_ndjson_input.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/test_profiles_commands.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/test_respx_integration.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/test_runtime.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/test_skill_commands.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/test_stdin_sniffer.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/test_writes_respx.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/writes/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/writes/test_apply.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/writes/test_bulk.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/writes/test_confirmation.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/writes/test_handler_helpers.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/writes/test_handlers_audit.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/writes/test_input.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/cli/writes/test_preflight.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/config/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/config/test_loader.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/config/test_settings_private_dir.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/config/test_writer_atomicity.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/config/test_writer_dotted_paths.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/config/test_writer_roundtrip.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/conftest.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/e2e/README.md +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/e2e/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/e2e/conftest.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/e2e/docker-compose.yml +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/e2e/test_audit_redaction.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/e2e/test_auth_error_envelope.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/e2e/test_bulk_create_with_loop_fallback.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/e2e/test_full_cycle.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/e2e/test_login.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/e2e/test_ndjson.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/e2e/test_preflight_blocks_apply.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/e2e/test_validation_error_envelope_from_netbox.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/e2e/wait_for_netbox.sh +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/fixtures/profiles/single_profile.yaml +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/fixtures/responses/auth_401.json +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/fixtures/responses/circuits_providers_list.json +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/fixtures/responses/dcim_devices_get.json +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/fixtures/responses/dcim_devices_list_p1.json +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/fixtures/responses/dcim_devices_list_p2.json +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/http/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/http/test_audit.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/http/test_audit_permissions.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/http/test_audit_redaction.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/http/test_client.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/http/test_client_redaction_threading.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/http/test_errors.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/http/test_retry.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/model/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/model/test_command_model.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/model/test_request_body_shape.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/output/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/output/test_console.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/output/test_csv.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/output/test_errors.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/output/test_errors_aliases.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/output/test_errors_color.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/output/test_explain.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/output/test_explain_color.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/output/test_input_error_envelope.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/output/test_json.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/output/test_jsonl.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/output/test_render_dispatch.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/output/test_table.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/output/test_yaml.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/schema/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/schema/test_hashing.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/schema/test_loader.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/schema/test_models.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/schema/test_source.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/schema/test_source_ttl.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/scripts/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/scripts/test_gen_docs.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/scripts/test_sync_agents_md.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/skill/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/skill/test_bundle.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/test_packaging.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/__init__.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_bindings.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_bulk_apply.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_bulk_diff.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_bulk_diff_modal.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_bulk_edit_form.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_bulk_import_isolation.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_bulk_summary.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_catalog.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_column_chooser.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_detail_screen.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_diff.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_diff_modal.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_edit_form.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_errors.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_filters.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_fk_resolve.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_forms.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_global_search.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_help_overlay.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_list_screen.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_picker.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_record_picker.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_relations.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_search.py +0 -0
- {netbox_super_cli-1.3.1 → netbox_super_cli-1.4.0}/tests/tui/test_selection.py +0 -0
|
@@ -2,6 +2,38 @@
|
|
|
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.4.0 — 2026-06-26
|
|
6
|
+
|
|
7
|
+
Minor release. Three user-facing features for the interactive TUI and table
|
|
8
|
+
output: individually selectable custom-field columns, local saved searches, and
|
|
9
|
+
NetBox-native object colors.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Individually selectable custom fields** ([#121], closes [#118]). The TUI
|
|
14
|
+
column chooser (`f` — Fields) now expands `custom_fields` into one toggleable
|
|
15
|
+
column per custom field (`custom_fields.<name>`) instead of a single opaque
|
|
16
|
+
object. On the CLI, `--columns custom_fields.<name>` selects an individual
|
|
17
|
+
custom field (now documented).
|
|
18
|
+
- **Saved searches** ([#123], closes [#119]). Named filter sets persist per
|
|
19
|
+
resource. In the TUI filter builder, `Ctrl+W` saves the current filters under
|
|
20
|
+
a name and `Ctrl+O` opens a picker to apply one (`d` deletes an entry). On the
|
|
21
|
+
CLI, `--saved <name>` re-applies a saved filter set on `list`; explicit
|
|
22
|
+
`--filter` and typed options take precedence over saved values. Names
|
|
23
|
+
containing `.` (or whitespace) are rejected to keep the config file well-formed.
|
|
24
|
+
- **NetBox object colors** ([#122], closes [#120]). Role, tag, and other
|
|
25
|
+
color-bearing fields render in their NetBox-defined colors in table output. A
|
|
26
|
+
new `object_colors` setting (`auto`/`on`/`off`) and the
|
|
27
|
+
`--object-colors/--no-object-colors` flag toggle this independently of
|
|
28
|
+
`--color`; `off`, `NO_COLOR`, and non-TTY output are always respected.
|
|
29
|
+
|
|
30
|
+
[#118]: https://github.com/thomaschristory/netbox-super-cli/issues/118
|
|
31
|
+
[#119]: https://github.com/thomaschristory/netbox-super-cli/issues/119
|
|
32
|
+
[#120]: https://github.com/thomaschristory/netbox-super-cli/issues/120
|
|
33
|
+
[#121]: https://github.com/thomaschristory/netbox-super-cli/pull/121
|
|
34
|
+
[#122]: https://github.com/thomaschristory/netbox-super-cli/pull/122
|
|
35
|
+
[#123]: https://github.com/thomaschristory/netbox-super-cli/pull/123
|
|
36
|
+
|
|
5
37
|
## v1.3.1 — 2026-06-25
|
|
6
38
|
|
|
7
39
|
Patch release. CI maintenance only — no runtime or user-facing changes.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: netbox-super-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
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
|
|
@@ -27,9 +27,29 @@ Requires-Dist: textual>=0.50
|
|
|
27
27
|
Requires-Dist: typer<0.27,>=0.26
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
<div align="center">
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
# netbox-super-cli · `nsc`
|
|
33
|
+
|
|
34
|
+
**A Python CLI for [NetBox](https://netbox.dev/) that builds its entire command tree from your install's live OpenAPI schema.**
|
|
35
|
+
|
|
36
|
+
One binary, any NetBox version, every plugin endpoint — the schema, not hand-written code, defines the surface.
|
|
37
|
+
|
|
38
|
+
<br/>
|
|
39
|
+
|
|
40
|
+
[](https://pypi.org/project/netbox-super-cli/)
|
|
41
|
+
[](https://pypi.org/project/netbox-super-cli/)
|
|
42
|
+
[](https://pypi.org/project/netbox-super-cli/)
|
|
43
|
+
[](LICENSE)
|
|
44
|
+
[](https://github.com/thomaschristory/netbox-super-cli/actions/workflows/test.yml)
|
|
45
|
+
[](https://thomaschristory.github.io/netbox-super-cli/)
|
|
46
|
+
[](https://github.com/thomaschristory/netbox-super-cli/stargazers)
|
|
47
|
+
|
|
48
|
+
[**📚 Documentation**](https://thomaschristory.github.io/netbox-super-cli/) · [**🚀 Install**](#install) · [**🖥️ Interactive TUI**](#interactive-tui) · [**🤖 AI Skill**](#bundled-skill-for-ai-agents)
|
|
49
|
+
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
---
|
|
33
53
|
|
|
34
54
|
> **Docs:** [thomaschristory.github.io/netbox-super-cli](https://thomaschristory.github.io/netbox-super-cli/) — the full guide, including install, first-run, guides, and the auto-generated CLI/config/exit-code reference.
|
|
35
55
|
|
|
@@ -74,7 +94,9 @@ nsc tui devices # jump straight into the devices list
|
|
|
74
94
|
Highlights: a collapsible resource picker, a web-UI-style filter builder (`/`),
|
|
75
95
|
inline record editing with a confirm-diff save, schema-derived relationship
|
|
76
96
|
drill-down, bulk edit with a per-record preview, a persisted column chooser
|
|
77
|
-
(`f`), and global search (`Ctrl`+`F`).
|
|
97
|
+
(`f`) that lists each custom field individually, and global search (`Ctrl`+`F`).
|
|
98
|
+
The same dotted paths work on the CLI: `nsc ls devices --columns id,name,custom_fields.rack_role`.
|
|
99
|
+
Full walkthrough in the
|
|
78
100
|
[Interactive TUI guide](https://thomaschristory.github.io/netbox-super-cli/guides/interactive-tui/).
|
|
79
101
|
|
|
80
102
|
## Reading
|
|
@@ -90,6 +112,37 @@ uv run nsc circuits providers list --output csv
|
|
|
90
112
|
uv run nsc ipam prefixes list --filter created__gte=2026-01-01 --output yaml
|
|
91
113
|
```
|
|
92
114
|
|
|
115
|
+
### Saved searches
|
|
116
|
+
|
|
117
|
+
Filter sets you save from the TUI (Ctrl+W in the filter builder) are stored
|
|
118
|
+
locally under `saved_searches` in `~/.nsc/config.yaml`, keyed by tag/resource.
|
|
119
|
+
Re-apply one on the CLI with `--saved`:
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
uv run nsc dcim devices list --saved active-switches
|
|
123
|
+
# explicit --filter / typed options override the saved values:
|
|
124
|
+
uv run nsc dcim devices list --saved active-switches --status offline
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
This is a local, per-config convenience and is unrelated to NetBox's
|
|
128
|
+
server-side saved filters (`extras saved-filters`), which store web-UI query
|
|
129
|
+
strings.
|
|
130
|
+
|
|
131
|
+
### Object colors
|
|
132
|
+
|
|
133
|
+
NetBox assigns colors to roles, tags, and similar fields. In table output `nsc`
|
|
134
|
+
renders those values in their NetBox-defined colors:
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
uv run nsc dcim devices list # colored when stdout is a TTY
|
|
138
|
+
uv run nsc dcim devices list --no-object-colors # plain text
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Toggle it independently of `--color` with the `object_colors` setting
|
|
142
|
+
(`auto`/`on`/`off`) or the `--object-colors/--no-object-colors` flag. `off`,
|
|
143
|
+
`NO_COLOR`, and non-TTY output are always respected, so piped or redirected
|
|
144
|
+
output stays uncolored.
|
|
145
|
+
|
|
93
146
|
## Writing
|
|
94
147
|
|
|
95
148
|
```sh
|
|
@@ -1,6 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# netbox-super-cli · `nsc`
|
|
4
|
+
|
|
5
|
+
**A Python CLI for [NetBox](https://netbox.dev/) that builds its entire command tree from your install's live OpenAPI schema.**
|
|
6
|
+
|
|
7
|
+
One binary, any NetBox version, every plugin endpoint — the schema, not hand-written code, defines the surface.
|
|
8
|
+
|
|
9
|
+
<br/>
|
|
10
|
+
|
|
11
|
+
[](https://pypi.org/project/netbox-super-cli/)
|
|
12
|
+
[](https://pypi.org/project/netbox-super-cli/)
|
|
13
|
+
[](https://pypi.org/project/netbox-super-cli/)
|
|
14
|
+
[](LICENSE)
|
|
15
|
+
[](https://github.com/thomaschristory/netbox-super-cli/actions/workflows/test.yml)
|
|
16
|
+
[](https://thomaschristory.github.io/netbox-super-cli/)
|
|
17
|
+
[](https://github.com/thomaschristory/netbox-super-cli/stargazers)
|
|
18
|
+
|
|
19
|
+
[**📚 Documentation**](https://thomaschristory.github.io/netbox-super-cli/) · [**🚀 Install**](#install) · [**🖥️ Interactive TUI**](#interactive-tui) · [**🤖 AI Skill**](#bundled-skill-for-ai-agents)
|
|
20
|
+
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
---
|
|
4
24
|
|
|
5
25
|
> **Docs:** [thomaschristory.github.io/netbox-super-cli](https://thomaschristory.github.io/netbox-super-cli/) — the full guide, including install, first-run, guides, and the auto-generated CLI/config/exit-code reference.
|
|
6
26
|
|
|
@@ -45,7 +65,9 @@ nsc tui devices # jump straight into the devices list
|
|
|
45
65
|
Highlights: a collapsible resource picker, a web-UI-style filter builder (`/`),
|
|
46
66
|
inline record editing with a confirm-diff save, schema-derived relationship
|
|
47
67
|
drill-down, bulk edit with a per-record preview, a persisted column chooser
|
|
48
|
-
(`f`), and global search (`Ctrl`+`F`).
|
|
68
|
+
(`f`) that lists each custom field individually, and global search (`Ctrl`+`F`).
|
|
69
|
+
The same dotted paths work on the CLI: `nsc ls devices --columns id,name,custom_fields.rack_role`.
|
|
70
|
+
Full walkthrough in the
|
|
49
71
|
[Interactive TUI guide](https://thomaschristory.github.io/netbox-super-cli/guides/interactive-tui/).
|
|
50
72
|
|
|
51
73
|
## Reading
|
|
@@ -61,6 +83,37 @@ uv run nsc circuits providers list --output csv
|
|
|
61
83
|
uv run nsc ipam prefixes list --filter created__gte=2026-01-01 --output yaml
|
|
62
84
|
```
|
|
63
85
|
|
|
86
|
+
### Saved searches
|
|
87
|
+
|
|
88
|
+
Filter sets you save from the TUI (Ctrl+W in the filter builder) are stored
|
|
89
|
+
locally under `saved_searches` in `~/.nsc/config.yaml`, keyed by tag/resource.
|
|
90
|
+
Re-apply one on the CLI with `--saved`:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
uv run nsc dcim devices list --saved active-switches
|
|
94
|
+
# explicit --filter / typed options override the saved values:
|
|
95
|
+
uv run nsc dcim devices list --saved active-switches --status offline
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This is a local, per-config convenience and is unrelated to NetBox's
|
|
99
|
+
server-side saved filters (`extras saved-filters`), which store web-UI query
|
|
100
|
+
strings.
|
|
101
|
+
|
|
102
|
+
### Object colors
|
|
103
|
+
|
|
104
|
+
NetBox assigns colors to roles, tags, and similar fields. In table output `nsc`
|
|
105
|
+
renders those values in their NetBox-defined colors:
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
uv run nsc dcim devices list # colored when stdout is a TTY
|
|
109
|
+
uv run nsc dcim devices list --no-object-colors # plain text
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Toggle it independently of `--color` with the `object_colors` setting
|
|
113
|
+
(`auto`/`on`/`off`) or the `--object-colors/--no-object-colors` flag. `off`,
|
|
114
|
+
`NO_COLOR`, and non-TTY output are always respected, so piped or redirected
|
|
115
|
+
output stays uncolored.
|
|
116
|
+
|
|
64
117
|
## Writing
|
|
65
118
|
|
|
66
119
|
```sh
|
|
@@ -11,8 +11,9 @@ 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'>, color_mode=<ColorMode.AUTO: 'auto'>, audit_redaction=<AuditRedaction.SAFE: 'safe'>)` |
|
|
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'>, object_colors=<ObjectColorMode.AUTO: 'auto'>, audit_redaction=<AuditRedaction.SAFE: 'safe'>)` |
|
|
15
15
|
| `columns` | `dict[str, dict[str, list[str]]]` | `{}` |
|
|
16
|
+
| `saved_searches` | `dict[str, dict[str, dict[str, dict[str, str]]]]` | `{}` |
|
|
16
17
|
|
|
17
18
|
## `Profile`
|
|
18
19
|
|
|
@@ -34,4 +35,5 @@ All fields below describe `~/.nsc/config.yaml`.
|
|
|
34
35
|
| `timeout` | `<class 'float'>` | `30.0` |
|
|
35
36
|
| `schema_refresh` | `<enum 'SchemaRefresh'>` | `<SchemaRefresh.DAILY: 'daily'>` |
|
|
36
37
|
| `color_mode` | `<enum 'ColorMode'>` | `<ColorMode.AUTO: 'auto'>` |
|
|
38
|
+
| `object_colors` | `<enum 'ObjectColorMode'>` | `<ObjectColorMode.AUTO: 'auto'>` |
|
|
37
39
|
| `audit_redaction` | `<enum 'AuditRedaction'>` | `<AuditRedaction.SAFE: 'safe'>` |
|
|
@@ -94,6 +94,14 @@ def _extract_global_overrides(args: list[str]) -> CLIOverrides:
|
|
|
94
94
|
kwargs["refresh_schema"] = True
|
|
95
95
|
i += 1
|
|
96
96
|
continue
|
|
97
|
+
if a == "--object-colors":
|
|
98
|
+
kwargs["object_colors"] = True
|
|
99
|
+
i += 1
|
|
100
|
+
continue
|
|
101
|
+
if a == "--no-object-colors":
|
|
102
|
+
kwargs["object_colors"] = False
|
|
103
|
+
i += 1
|
|
104
|
+
continue
|
|
97
105
|
i += 1
|
|
98
106
|
return CLIOverrides(**kwargs) # type: ignore[arg-type]
|
|
99
107
|
|
|
@@ -310,6 +318,13 @@ def _root(
|
|
|
310
318
|
),
|
|
311
319
|
] = False,
|
|
312
320
|
output: Annotated[str | None, typer.Option("--output", "-o")] = None,
|
|
321
|
+
object_colors: Annotated[
|
|
322
|
+
bool | None,
|
|
323
|
+
typer.Option(
|
|
324
|
+
"--object-colors/--no-object-colors",
|
|
325
|
+
help="Render NetBox-native object colors (role/tag/…) in table output.",
|
|
326
|
+
),
|
|
327
|
+
] = None,
|
|
313
328
|
debug: Annotated[bool, typer.Option("--debug")] = False,
|
|
314
329
|
) -> None:
|
|
315
330
|
"""Root callback — global options live here."""
|
|
@@ -321,6 +336,7 @@ def _root(
|
|
|
321
336
|
schema_override=schema,
|
|
322
337
|
refresh_schema=refresh_schema,
|
|
323
338
|
output=output,
|
|
339
|
+
object_colors=object_colors,
|
|
324
340
|
)
|
|
325
341
|
try:
|
|
326
342
|
config = load_config(default_paths().config_file)
|
|
@@ -10,10 +10,11 @@ from nsc.cli.runtime import (
|
|
|
10
10
|
CLIOverrides,
|
|
11
11
|
RuntimeContext,
|
|
12
12
|
resolve_color,
|
|
13
|
+
resolve_object_colors,
|
|
13
14
|
resolve_profile,
|
|
14
15
|
)
|
|
15
16
|
from nsc.config import default_paths
|
|
16
|
-
from nsc.config.models import Config
|
|
17
|
+
from nsc.config.models import Config, ObjectColorMode
|
|
17
18
|
from nsc.http.client import NetBoxClient
|
|
18
19
|
from nsc.output.render import select_format
|
|
19
20
|
from nsc.schema.source import resolve_command_model
|
|
@@ -48,6 +49,8 @@ def build_runtime_context(state: GlobalState) -> RuntimeContext:
|
|
|
48
49
|
redaction=state.config.defaults.audit_redaction,
|
|
49
50
|
profile_name=profile.name,
|
|
50
51
|
)
|
|
52
|
+
color = resolve_color(state.config.defaults.color_mode, is_tty=sys.stdout.isatty())
|
|
53
|
+
object_color_mode = _resolve_object_color_mode(state)
|
|
51
54
|
return RuntimeContext(
|
|
52
55
|
resolved_profile=profile,
|
|
53
56
|
config=state.config,
|
|
@@ -56,11 +59,19 @@ def build_runtime_context(state: GlobalState) -> RuntimeContext:
|
|
|
56
59
|
output_format=output,
|
|
57
60
|
debug=state.debug,
|
|
58
61
|
page_size=state.config.defaults.page_size,
|
|
59
|
-
color=
|
|
62
|
+
color=color,
|
|
60
63
|
color_stderr=resolve_color(state.config.defaults.color_mode, is_tty=sys.stderr.isatty()),
|
|
64
|
+
object_colors=resolve_object_colors(object_color_mode, color=color),
|
|
61
65
|
)
|
|
62
66
|
|
|
63
67
|
|
|
68
|
+
def _resolve_object_color_mode(state: GlobalState) -> ObjectColorMode:
|
|
69
|
+
override = state.overrides.object_colors
|
|
70
|
+
if override is None:
|
|
71
|
+
return state.config.defaults.object_colors
|
|
72
|
+
return ObjectColorMode.ON if override else ObjectColorMode.OFF
|
|
73
|
+
|
|
74
|
+
|
|
64
75
|
__all__ = [
|
|
65
76
|
"GlobalState",
|
|
66
77
|
"build_runtime_context",
|
|
@@ -104,6 +104,7 @@ def handle_list(
|
|
|
104
104
|
stream=_out(stream),
|
|
105
105
|
compact=ctx.compact,
|
|
106
106
|
color=ctx.color,
|
|
107
|
+
object_colors=ctx.object_colors,
|
|
107
108
|
)
|
|
108
109
|
except (NetBoxAPIError, NetBoxClientError) as exc:
|
|
109
110
|
env = map_error(exc, operation_id=operation.operation_id)
|
|
@@ -130,6 +131,7 @@ def handle_get(
|
|
|
130
131
|
stream=_out(stream),
|
|
131
132
|
compact=ctx.compact,
|
|
132
133
|
color=ctx.color,
|
|
134
|
+
object_colors=ctx.object_colors,
|
|
133
135
|
)
|
|
134
136
|
except (NetBoxAPIError, NetBoxClientError) as exc:
|
|
135
137
|
env = map_error(exc, operation_id=operation.operation_id)
|
|
@@ -616,6 +618,7 @@ def _render_response(
|
|
|
616
618
|
stream=stream,
|
|
617
619
|
compact=ctx.compact,
|
|
618
620
|
color=ctx.color,
|
|
621
|
+
object_colors=ctx.object_colors,
|
|
619
622
|
)
|
|
620
623
|
|
|
621
624
|
|
|
@@ -120,10 +120,39 @@ def _register_write(
|
|
|
120
120
|
|
|
121
121
|
|
|
122
122
|
_GLOBAL_FLAG_NAMES: frozenset[str] = frozenset(
|
|
123
|
-
{"output", "compact", "columns", "limit", "all_", "filter_"}
|
|
123
|
+
{"output", "compact", "columns", "limit", "all_", "filter_", "saved"}
|
|
124
124
|
)
|
|
125
125
|
|
|
126
126
|
|
|
127
|
+
def _resolve_saved_filters(
|
|
128
|
+
ctx: RuntimeContext,
|
|
129
|
+
tag_name: str,
|
|
130
|
+
resource_name: str,
|
|
131
|
+
saved_name: str | None,
|
|
132
|
+
explicit_filters: list[tuple[str, str]],
|
|
133
|
+
typed_kwargs: dict[str, Any],
|
|
134
|
+
) -> list[tuple[str, str]]:
|
|
135
|
+
"""Layer a `--saved` filter set under explicit `--filter` and typed options.
|
|
136
|
+
|
|
137
|
+
Saved params are the base; explicit `--filter` pairs and any non-None typed
|
|
138
|
+
query option (already in ``typed_kwargs``) win. Order matters: ``handle_list``
|
|
139
|
+
overlays ``ctx.filters`` onto the typed-option params, so saved keys that the
|
|
140
|
+
user also set via a typed flag must be dropped here to let the flag win.
|
|
141
|
+
"""
|
|
142
|
+
if saved_name is None:
|
|
143
|
+
return explicit_filters
|
|
144
|
+
from nsc.config.saved_searches import get_saved_search # noqa: PLC0415
|
|
145
|
+
|
|
146
|
+
saved = get_saved_search(ctx.config, tag_name, resource_name, saved_name)
|
|
147
|
+
if saved is None:
|
|
148
|
+
raise typer.BadParameter(
|
|
149
|
+
f"no saved search named {saved_name!r} for {tag_name}/{resource_name}"
|
|
150
|
+
)
|
|
151
|
+
typed_keys = {k for k, v in typed_kwargs.items() if v is not None}
|
|
152
|
+
base = [(k, v) for k, v in saved.items() if k not in typed_keys]
|
|
153
|
+
return base + explicit_filters
|
|
154
|
+
|
|
155
|
+
|
|
127
156
|
def _build_read_closure(
|
|
128
157
|
operation: Operation,
|
|
129
158
|
tag_name: str,
|
|
@@ -151,17 +180,19 @@ def _build_read_closure(
|
|
|
151
180
|
limit = kwargs.pop("limit", None)
|
|
152
181
|
fetch_all = kwargs.pop("all_", False)
|
|
153
182
|
filters_raw: list[str] = kwargs.pop("filter_", None) or []
|
|
183
|
+
saved_name: str | None = kwargs.pop("saved", None)
|
|
154
184
|
ctx = get_ctx()
|
|
185
|
+
explicit_filters = [
|
|
186
|
+
(item.split("=", 1)[0], item.split("=", 1)[1]) for item in filters_raw if "=" in item
|
|
187
|
+
]
|
|
155
188
|
update: dict[str, Any] = {
|
|
156
189
|
"compact": compact,
|
|
157
190
|
"columns_override": columns_csv.split(",") if columns_csv else None,
|
|
158
191
|
"limit": limit,
|
|
159
192
|
"fetch_all": fetch_all,
|
|
160
|
-
"filters":
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if "=" in item
|
|
164
|
-
],
|
|
193
|
+
"filters": _resolve_saved_filters(
|
|
194
|
+
ctx, tag_name, resource_name, saved_name, explicit_filters, kwargs
|
|
195
|
+
),
|
|
165
196
|
}
|
|
166
197
|
if output:
|
|
167
198
|
update["output_format"] = OutputFormat(output)
|
|
@@ -311,6 +342,20 @@ def _build_global_flag_params() -> tuple[inspect.Parameter, ...]:
|
|
|
311
342
|
annotation=list[str] | None,
|
|
312
343
|
default=typer.Option(None, "--filter"),
|
|
313
344
|
),
|
|
345
|
+
inspect.Parameter(
|
|
346
|
+
name="saved",
|
|
347
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
348
|
+
annotation=str | None,
|
|
349
|
+
default=typer.Option(
|
|
350
|
+
None,
|
|
351
|
+
"--saved",
|
|
352
|
+
help=(
|
|
353
|
+
"Apply a LOCAL named filter set saved from the TUI (config "
|
|
354
|
+
"`saved_searches`). Explicit --filter/typed options override it. "
|
|
355
|
+
"Unrelated to NetBox server-side saved filters."
|
|
356
|
+
),
|
|
357
|
+
),
|
|
358
|
+
),
|
|
314
359
|
)
|
|
315
360
|
|
|
316
361
|
|
|
@@ -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 ColorMode, Config, OutputFormat, Profile
|
|
18
|
+
from nsc.config.models import ColorMode, Config, ObjectColorMode, 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
|
|
@@ -52,6 +52,7 @@ class CLIOverrides(_Frozen):
|
|
|
52
52
|
schema_override: str | None = None
|
|
53
53
|
refresh_schema: bool = False
|
|
54
54
|
output: str | None = None
|
|
55
|
+
object_colors: bool | None = None
|
|
55
56
|
|
|
56
57
|
|
|
57
58
|
class ResolvedProfile(_Frozen):
|
|
@@ -186,6 +187,22 @@ def resolve_color(mode: ColorMode, *, is_tty: bool) -> bool:
|
|
|
186
187
|
return is_tty
|
|
187
188
|
|
|
188
189
|
|
|
190
|
+
def resolve_object_colors(mode: ObjectColorMode, *, color: bool) -> bool:
|
|
191
|
+
"""Resolve whether NetBox-native object colors render.
|
|
192
|
+
|
|
193
|
+
Always gated by the global `color` resolution (no ANSI when color is off),
|
|
194
|
+
but can be disabled independently: OFF turns object colors off while status
|
|
195
|
+
coloring stays on; ON/AUTO follow `color`.
|
|
196
|
+
|
|
197
|
+
Contract: ON is deliberately *not* a force — it stays identical to AUTO so
|
|
198
|
+
that `--object-colors` (and `NSC_OBJECT_COLORS=on`) can never override the
|
|
199
|
+
no-color decision (`--no-color`, `NO_COLOR`, non-tty). Only OFF diverges.
|
|
200
|
+
"""
|
|
201
|
+
if mode is ObjectColorMode.OFF:
|
|
202
|
+
return False
|
|
203
|
+
return color
|
|
204
|
+
|
|
205
|
+
|
|
189
206
|
class RuntimeContext(BaseModel):
|
|
190
207
|
"""Per-invocation runtime state.
|
|
191
208
|
|
|
@@ -208,6 +225,7 @@ class RuntimeContext(BaseModel):
|
|
|
208
225
|
compact: bool = False
|
|
209
226
|
color: bool = False
|
|
210
227
|
color_stderr: bool = False
|
|
228
|
+
object_colors: bool = False
|
|
211
229
|
apply: bool = False
|
|
212
230
|
explain: bool = False
|
|
213
231
|
strict: bool = False
|
|
@@ -41,6 +41,46 @@ def _save_columns(tag: str, resource: str, columns: list[str]) -> None:
|
|
|
41
41
|
atomic_write(path, dump_round_trip(doc))
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
def _save_search(tag: str, resource: str, name: str, params: dict[str, str]) -> None:
|
|
45
|
+
"""Persist a filter set to ``saved_searches.<tag>.<resource>.<name>``."""
|
|
46
|
+
from nsc.config.saved_searches import validate_saved_search_name # noqa: PLC0415
|
|
47
|
+
from nsc.config.settings import default_paths # noqa: PLC0415
|
|
48
|
+
from nsc.config.writer import ( # noqa: PLC0415
|
|
49
|
+
acquire_lock,
|
|
50
|
+
atomic_write,
|
|
51
|
+
dump_round_trip,
|
|
52
|
+
load_round_trip,
|
|
53
|
+
set_path,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Defensive: a dotted name would split into a nested map under set_path and
|
|
57
|
+
# corrupt the file. Validate before touching disk so the file is untouched.
|
|
58
|
+
validate_saved_search_name(name)
|
|
59
|
+
path = default_paths().config_file
|
|
60
|
+
with acquire_lock(path):
|
|
61
|
+
doc = load_round_trip(path)
|
|
62
|
+
set_path(doc, f"saved_searches.{tag}.{resource}.{name}", dict(params))
|
|
63
|
+
atomic_write(path, dump_round_trip(doc))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _delete_search(tag: str, resource: str, name: str) -> None:
|
|
67
|
+
"""Remove ``saved_searches.<tag>.<resource>.<name>`` and prune empty parents."""
|
|
68
|
+
from nsc.config.settings import default_paths # noqa: PLC0415
|
|
69
|
+
from nsc.config.writer import ( # noqa: PLC0415
|
|
70
|
+
acquire_lock,
|
|
71
|
+
atomic_write,
|
|
72
|
+
dump_round_trip,
|
|
73
|
+
load_round_trip,
|
|
74
|
+
unset_path,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
path = default_paths().config_file
|
|
78
|
+
with acquire_lock(path):
|
|
79
|
+
doc = load_round_trip(path)
|
|
80
|
+
unset_path(doc, f"saved_searches.{tag}.{resource}.{name}")
|
|
81
|
+
atomic_write(path, dump_round_trip(doc))
|
|
82
|
+
|
|
83
|
+
|
|
44
84
|
def register(app: typer.Typer) -> None:
|
|
45
85
|
def tui(
|
|
46
86
|
ctx: typer.Context,
|
|
@@ -54,12 +94,23 @@ def register(app: typer.Typer) -> None:
|
|
|
54
94
|
from nsc.tui import run_tui # noqa: PLC0415 # deferred: keeps Textual lazy.
|
|
55
95
|
|
|
56
96
|
column_prefs = {tag: dict(resources) for tag, resources in runtime.config.columns.items()}
|
|
97
|
+
saved_searches = {
|
|
98
|
+
tag: {
|
|
99
|
+
res: {name: dict(params) for name, params in sets.items()}
|
|
100
|
+
for res, sets in resources.items()
|
|
101
|
+
}
|
|
102
|
+
for tag, resources in runtime.config.saved_searches.items()
|
|
103
|
+
}
|
|
57
104
|
run_tui(
|
|
58
105
|
runtime.command_model,
|
|
59
106
|
runtime.client,
|
|
60
107
|
initial_resource=resource,
|
|
61
108
|
save_columns=_save_columns,
|
|
62
109
|
column_prefs=column_prefs,
|
|
110
|
+
object_colors=runtime.object_colors,
|
|
111
|
+
saved_searches=saved_searches,
|
|
112
|
+
save_search=_save_search,
|
|
113
|
+
delete_search=_delete_search,
|
|
63
114
|
)
|
|
64
115
|
|
|
65
116
|
# `tui` is canonical; `interactive` and `i` are hidden aliases for the same
|
|
@@ -37,6 +37,15 @@ class ColorMode(StrEnum):
|
|
|
37
37
|
OFF = "off"
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
class ObjectColorMode(StrEnum):
|
|
41
|
+
# Render NetBox-native hex colors (role/tag/…) independently of status
|
|
42
|
+
# coloring. Still gated by the global color resolution, so OFF never yields
|
|
43
|
+
# ANSI but ON/AUTO only color when color is otherwise enabled.
|
|
44
|
+
AUTO = "auto"
|
|
45
|
+
ON = "on"
|
|
46
|
+
OFF = "off"
|
|
47
|
+
|
|
48
|
+
|
|
40
49
|
class AuditRedaction(StrEnum):
|
|
41
50
|
# SAFE redacts only known secrets (passwords/tokens) in bodies; FULL omits
|
|
42
51
|
# every body, leaving routing metadata only — stricter compliance, harder
|
|
@@ -51,6 +60,7 @@ class Defaults(_Frozen):
|
|
|
51
60
|
timeout: float = 30.0
|
|
52
61
|
schema_refresh: SchemaRefresh = SchemaRefresh.DAILY
|
|
53
62
|
color_mode: ColorMode = ColorMode.AUTO
|
|
63
|
+
object_colors: ObjectColorMode = ObjectColorMode.AUTO
|
|
54
64
|
audit_redaction: AuditRedaction = AuditRedaction.SAFE
|
|
55
65
|
|
|
56
66
|
|
|
@@ -68,3 +78,7 @@ class Config(_Frozen):
|
|
|
68
78
|
profiles: dict[str, Profile] = Field(default_factory=dict)
|
|
69
79
|
defaults: Defaults = Field(default_factory=Defaults)
|
|
70
80
|
columns: dict[str, dict[str, list[str]]] = Field(default_factory=dict)
|
|
81
|
+
# Local, named filter sets keyed `<tag>.<resource>.<name> = {filter_key: value}`.
|
|
82
|
+
# The leaf dict is exactly a `FilterState.as_params()` payload so a saved search
|
|
83
|
+
# round-trips through the TUI filter screen and the CLI `--saved` flag alike.
|
|
84
|
+
saved_searches: dict[str, dict[str, dict[str, dict[str, str]]]] = Field(default_factory=dict)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Framework-free lookups over the `saved_searches` config mapping.
|
|
2
|
+
|
|
3
|
+
Keeps saved-search resolution out of `cli/` and `tui/` so both surfaces share
|
|
4
|
+
one implementation and the logic stays unit-testable without any I/O.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from nsc.config.models import Config
|
|
10
|
+
|
|
11
|
+
_MIN_PRINTABLE = 0x20
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InvalidSavedSearchName(ValueError):
|
|
15
|
+
"""Raised when a saved-search name is unsafe to persist."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def validate_saved_search_name(name: str) -> None:
|
|
19
|
+
"""Reject names that would corrupt config.yaml or surprise the user.
|
|
20
|
+
|
|
21
|
+
Saved searches are persisted at the dotted config path
|
|
22
|
+
``saved_searches.<tag>.<resource>.<name>`` and the writer splits on ``.``,
|
|
23
|
+
so a name containing ``.`` would be written as a *nested map* rather than a
|
|
24
|
+
leaf — making the whole file fail ``Config.model_validate`` on the next run.
|
|
25
|
+
Control characters and surrounding whitespace are rejected for the same
|
|
26
|
+
"don't persist a name that won't round-trip cleanly" reason.
|
|
27
|
+
"""
|
|
28
|
+
if not name or name != name.strip():
|
|
29
|
+
raise InvalidSavedSearchName(
|
|
30
|
+
"saved-search name must be non-empty and not start or end with whitespace"
|
|
31
|
+
)
|
|
32
|
+
if "." in name:
|
|
33
|
+
raise InvalidSavedSearchName(
|
|
34
|
+
f"saved-search name {name!r} may not contain '.' "
|
|
35
|
+
"(it is used as a config-path separator)"
|
|
36
|
+
)
|
|
37
|
+
if any((ch.isspace() and ch != " ") or ord(ch) < _MIN_PRINTABLE for ch in name):
|
|
38
|
+
raise InvalidSavedSearchName(
|
|
39
|
+
f"saved-search name {name!r} may not contain tabs, newlines, "
|
|
40
|
+
"or other control characters"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_saved_search(config: Config, tag: str, resource: str, name: str) -> dict[str, str] | None:
|
|
45
|
+
"""The stored filter params for `<tag>.<resource>.<name>`, or None if absent."""
|
|
46
|
+
params = config.saved_searches.get(tag, {}).get(resource, {}).get(name)
|
|
47
|
+
if params is None:
|
|
48
|
+
return None
|
|
49
|
+
return dict(params)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def list_saved_searches(config: Config, tag: str, resource: str) -> list[str]:
|
|
53
|
+
"""Sorted names of saved searches for `<tag>.<resource>` (empty if none)."""
|
|
54
|
+
return sorted(config.saved_searches.get(tag, {}).get(resource, {}))
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Framework-free value object + helper for NetBox-native hex colors.
|
|
2
|
+
|
|
3
|
+
NetBox FK/choice objects (roles, tags, …) carry a sibling ``color`` field — a
|
|
4
|
+
6-hex string (no leading ``#``) the web UI uses to tint the badge. The table and
|
|
5
|
+
TUI formatters preserve it by emitting a :class:`ColoredValue` instead of a bare
|
|
6
|
+
display string. This module imports nothing from cli/http/rich so it stays usable
|
|
7
|
+
by any formatter.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
_HEX = re.compile(r"\A[0-9a-fA-F]{6}\Z")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class ColoredValue:
|
|
20
|
+
text: str
|
|
21
|
+
color: str | None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def normalize_hex(raw: object) -> str | None:
|
|
25
|
+
"""Return a lowercased 6-hex color (no leading ``#``) or ``None``.
|
|
26
|
+
|
|
27
|
+
Accepts an optional leading ``#``. Anything that is not a string of exactly
|
|
28
|
+
six hex digits (after stripping one leading ``#``) — including ``None``,
|
|
29
|
+
non-strings, empty strings, and wrong lengths — yields ``None``.
|
|
30
|
+
"""
|
|
31
|
+
if not isinstance(raw, str):
|
|
32
|
+
return None
|
|
33
|
+
candidate = raw[1:] if raw.startswith("#") else raw
|
|
34
|
+
if _HEX.match(candidate):
|
|
35
|
+
return candidate.lower()
|
|
36
|
+
return None
|