netbox-super-cli 1.5.0__tar.gz → 1.6.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.5.0 → netbox_super_cli-1.6.0}/CHANGELOG.md +36 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/PKG-INFO +1 -1
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/_version.py +1 -1
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/handlers.py +36 -3
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/csv_.py +7 -1
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/render.py +10 -2
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/table.py +2 -1
- netbox_super_cli-1.6.0/nsc/savedfilters/custom_fields.py +151 -0
- netbox_super_cli-1.6.0/nsc/savedfilters/tags.py +62 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/app.py +13 -0
- netbox_super_cli-1.6.0/nsc/tui/forms.py +261 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/screens/bulk_edit_form.py +92 -20
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/screens/columns.py +9 -2
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/screens/detail.py +40 -19
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/screens/edit_form.py +73 -12
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/screens/list.py +37 -2
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/styles.tcss +33 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/view.py +8 -2
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/pyproject.toml +1 -1
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_handlers.py +48 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_csv.py +13 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_table.py +14 -0
- netbox_super_cli-1.6.0/tests/savedfilters/test_custom_fields.py +157 -0
- netbox_super_cli-1.6.0/tests/savedfilters/test_tags.py +54 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_bulk_edit_form.py +153 -2
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_column_chooser.py +30 -3
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_detail_screen.py +90 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_edit_form.py +181 -2
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_forms.py +85 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_list_screen.py +65 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/uv.lock +1 -1
- netbox_super_cli-1.5.0/nsc/tui/forms.py +0 -129
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/ISSUE_TEMPLATE/docs.yml +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/dependabot.yml +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/pull_request_template.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/workflows/bench.yml +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/workflows/claude.yml +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/workflows/docs.yml +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/workflows/e2e.yml +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/workflows/lint.yml +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/workflows/release.yml +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/workflows/skill-convention-review.yml +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/workflows/test.yml +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.gitignore +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.pre-commit-config.yaml +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.python-version +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/AGENTS.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/CLAUDE.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/LICENSE +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/README.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/architecture/caching.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/architecture/command-generation.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/architecture/http-client.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/architecture/overview.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/architecture/schema-loading.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/assets/tui/detail.svg +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/assets/tui/filter.svg +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/assets/tui/list.svg +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/assets/tui/picker.svg +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/assets/tui/search.svg +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/contributing/adding-bundled-schemas.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/contributing/branching.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/contributing/development.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/contributing/release-process.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/getting-started/concepts.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/getting-started/first-run.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/getting-started/install.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/guides/audit-log.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/guides/ci-and-automation.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/guides/interactive-tui.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/guides/managing-profiles.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/guides/output-formats.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/guides/using-with-ai-agents.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/guides/working-with-plugins.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/guides/writes-and-safety.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/index.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/reference/cli.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/reference/config.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/reference/exit-codes.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/reference/schemas.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/justfile +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/mkdocs.yml +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/__main__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/aliases/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/aliases/resolver.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/auth/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/auth/verify.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/builder/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/builder/build.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cache/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cache/store.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/aliases_commands.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/app.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/cache_commands.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/commands_dump.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/config_commands.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/globals.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/init_commands.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/login_commands.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/profiles_commands.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/registration.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/runtime.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/skill_commands.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/tui_commands.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/writes/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/writes/apply.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/writes/bulk.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/writes/coercion.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/writes/confirmation.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/writes/input.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/writes/preflight.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/completion/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/completion/cache_probe.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/completion/callbacks.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/completion/providers.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/config/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/config/loader.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/config/models.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/config/saved_searches.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/config/settings.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/config/writer.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/http/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/http/audit.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/http/client.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/http/errors.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/http/retry.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/model/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/model/command_model.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/_console.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/colors.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/errors.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/explain.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/flatten.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/headers.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/json_.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/jsonl.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/yaml_.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/savedfilters/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/savedfilters/objecttypes.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/savedfilters/params.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/savedfilters/store.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/schema/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/schema/hashing.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/schema/loader.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/schema/models.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/schema/source.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/schemas/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/schemas/bundled/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/schemas/bundled/manifest.yaml +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/schemas/bundled/netbox-4.5.10.json.gz +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/schemas/bundled/netbox-4.6.0.json.gz +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/skill/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/_bindings.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/bulk.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/catalog.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/columns.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/errors.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/filters.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/fk.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/keymap.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/nav.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/relations.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/screens/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/screens/filter.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/screens/global_search.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/screens/picker.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/screens/record_picker.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/screens/saved_search_picker.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/search.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/selection.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/widgets/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/widgets/_modal.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/widgets/bulk_diff.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/widgets/bulk_summary.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/widgets/confirm.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/widgets/diff.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/widgets/help.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/widgets/nav_tree.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/scripts/gen_docs.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/scripts/sync_agents_md.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/skills/netbox-super-cli/SKILL.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/aliases/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/aliases/test_resolver.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/audit/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/audit/test_redaction_full.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/auth/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/auth/test_verify.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/benchmarks/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/benchmarks/test_startup.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/builder/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/builder/test_build.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/builder/test_default_columns.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/builder/test_redaction_marking.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/builder/test_request_body_shape.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cache/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cache/test_prune.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cache/test_store.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_aliases_commands.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_app_smoke.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_cache_commands.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_commands_dump.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_completion_protocol_smoke.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_completion_smoke.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_config_commands.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_dynamic_completion.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_explain.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_globals_color.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_init_commands.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_lazy_textual_import.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_login_commands.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_meta_subcommands_under_broken_config.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_ndjson_input.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_profiles_commands.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_registration.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_respx_integration.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_runtime.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_runtime_color.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_skill_commands.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_stdin_sniffer.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_tui_command.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_writes_respx.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/writes/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/writes/test_apply.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/writes/test_bulk.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/writes/test_confirmation.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/writes/test_handler_helpers.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/writes/test_handlers_audit.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/writes/test_input.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/writes/test_preflight.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/config/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/config/test_loader.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/config/test_models.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/config/test_saved_search_fallback.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/config/test_saved_searches.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/config/test_settings_private_dir.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/config/test_writer_atomicity.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/config/test_writer_dotted_paths.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/config/test_writer_roundtrip.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/conftest.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/README.md +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/conftest.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/docker-compose.yml +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/test_audit_redaction.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/test_auth_error_envelope.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/test_bulk_create_with_loop_fallback.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/test_full_cycle.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/test_login.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/test_ndjson.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/test_preflight_blocks_apply.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/test_validation_error_envelope_from_netbox.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/wait_for_netbox.sh +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/fixtures/profiles/single_profile.yaml +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/fixtures/responses/auth_401.json +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/fixtures/responses/circuits_providers_list.json +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/fixtures/responses/dcim_devices_get.json +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/fixtures/responses/dcim_devices_list_p1.json +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/fixtures/responses/dcim_devices_list_p2.json +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/http/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/http/test_audit.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/http/test_audit_permissions.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/http/test_audit_redaction.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/http/test_client.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/http/test_client_redaction_threading.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/http/test_errors.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/http/test_retry.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/model/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/model/test_command_model.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/model/test_request_body_shape.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_colors.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_console.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_errors.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_errors_aliases.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_errors_color.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_explain.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_explain_color.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_flatten.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_input_error_envelope.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_json.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_jsonl.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_render_dispatch.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_table_color.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_yaml.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/savedfilters/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/savedfilters/test_objecttypes.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/savedfilters/test_params.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/savedfilters/test_store.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/schema/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/schema/test_hashing.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/schema/test_loader.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/schema/test_models.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/schema/test_source.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/schema/test_source_ttl.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/scripts/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/scripts/test_gen_docs.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/scripts/test_sync_agents_md.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/skill/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/skill/test_bundle.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/test_packaging.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/__init__.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_app.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_bindings.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_bulk_apply.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_bulk_diff.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_bulk_diff_modal.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_bulk_import_isolation.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_bulk_summary.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_catalog.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_columns.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_diff.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_diff_modal.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_errors.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_filter_screen.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_filters.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_fk_resolve.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_global_search.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_help_overlay.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_keymap.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_picker.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_record_picker.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_relations.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_saved_filter_integration.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_search.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_selection.py +0 -0
- {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_view.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.6.0 — 2026-06-29
|
|
6
|
+
|
|
7
|
+
Minor release. The interactive TUI gets human-readable custom-field columns,
|
|
8
|
+
clean rendering of list/M2M fields, and a far richer bulk-edit form.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Custom-field column labels** ([#132]). Custom-field columns now show the
|
|
13
|
+
field's human label (e.g. "Site Contact") instead of the raw
|
|
14
|
+
`custom_fields.<name>` key — in the TUI list headers and column chooser, and in
|
|
15
|
+
CLI `table`/`csv` output for `list`, `get`, and create/update responses. The
|
|
16
|
+
raw `custom_fields.<name>` key stays the selector, so `--columns` and saved
|
|
17
|
+
column prefs are unchanged. Labels come from the custom-field definitions
|
|
18
|
+
(`/api/extras/custom-fields/`, scoped to the resource's object type) and fall
|
|
19
|
+
back to a humanized name or the raw key when unavailable or ambiguous; JSON/YAML
|
|
20
|
+
output is untouched.
|
|
21
|
+
- **Bulk-edit: per-field custom fields and a tags dropdown** ([#134]). The TUI
|
|
22
|
+
bulk-edit form now expands `custom_fields` into one typed widget per field
|
|
23
|
+
(text/number/switch/select/multi-select), each with its own include toggle, and
|
|
24
|
+
renders `tags` as a multi-select populated from `/api/extras/tags/` (submitting
|
|
25
|
+
the NetBox-native `{name, slug}` list). The same per-field custom fields and
|
|
26
|
+
tag picker apply to single-record editing, and the bulk form's layout is
|
|
27
|
+
aligned with the single-edit experience. Falls back to plain inputs when the
|
|
28
|
+
definitions can't be fetched.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **Detail view rendered list fields as raw dicts** ([#133]). Tags and other
|
|
33
|
+
list-of-object fields in the record detail view showed a raw
|
|
34
|
+
`[{'id': …, 'display': …}]` repr; they now render as a clean comma-joined list
|
|
35
|
+
(colored when object colors are on), matching the list table.
|
|
36
|
+
|
|
37
|
+
[#132]: https://github.com/thomaschristory/netbox-super-cli/issues/132
|
|
38
|
+
[#133]: https://github.com/thomaschristory/netbox-super-cli/issues/133
|
|
39
|
+
[#134]: https://github.com/thomaschristory/netbox-super-cli/issues/134
|
|
40
|
+
|
|
5
41
|
## v1.5.0 — 2026-06-29
|
|
6
42
|
|
|
7
43
|
Minor release. Saved searches become interchangeable with the NetBox web UI.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: netbox-super-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.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: Documentation, https://thomaschristory.github.io/netbox-super-cli/
|
|
@@ -66,6 +66,33 @@ def _out(stream: TextIO | None) -> TextIO:
|
|
|
66
66
|
return stream if stream is not None else sys.stdout
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
def _custom_field_header_labels(
|
|
70
|
+
ctx: RuntimeContext, operation: Operation, columns: list[str] | None
|
|
71
|
+
) -> dict[str, str] | None:
|
|
72
|
+
"""Map ``custom_fields.<name>`` columns to their NetBox labels, or None.
|
|
73
|
+
|
|
74
|
+
Only fetches definitions when a custom-field column is actually selected, so
|
|
75
|
+
ordinary lists incur no extra request. Display-only: the raw key stays the
|
|
76
|
+
selector everywhere else.
|
|
77
|
+
"""
|
|
78
|
+
if not columns or not any(c.startswith("custom_fields.") for c in columns):
|
|
79
|
+
return None
|
|
80
|
+
from nsc.savedfilters.custom_fields import ( # noqa: PLC0415
|
|
81
|
+
CustomFieldResolver,
|
|
82
|
+
custom_field_labels,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Custom-field definitions are keyed by the collection (list) endpoint, so a
|
|
86
|
+
# detail path like /api/dcim/devices/{id}/ must reduce to /api/dcim/devices/.
|
|
87
|
+
list_path = operation.path
|
|
88
|
+
if list_path.endswith("{id}/"):
|
|
89
|
+
list_path = list_path[: -len("{id}/")]
|
|
90
|
+
defs = CustomFieldResolver(ctx.client).resolve(list_path)
|
|
91
|
+
if defs is None:
|
|
92
|
+
return None
|
|
93
|
+
return custom_field_labels(columns, defs)
|
|
94
|
+
|
|
95
|
+
|
|
69
96
|
def parse_filters(raw: list[str]) -> dict[str, str]:
|
|
70
97
|
out: dict[str, str] = {}
|
|
71
98
|
for item in raw:
|
|
@@ -97,10 +124,12 @@ def handle_list(
|
|
|
97
124
|
page_size=ctx.page_size,
|
|
98
125
|
)
|
|
99
126
|
)
|
|
127
|
+
columns = ctx.resolve_columns(op_tag, op_resource, operation)
|
|
100
128
|
render(
|
|
101
129
|
rows,
|
|
102
130
|
format=ctx.output_format,
|
|
103
|
-
columns=
|
|
131
|
+
columns=columns,
|
|
132
|
+
header_labels=_custom_field_header_labels(ctx, operation, columns),
|
|
104
133
|
stream=_out(stream),
|
|
105
134
|
compact=ctx.compact,
|
|
106
135
|
color=ctx.color,
|
|
@@ -124,10 +153,12 @@ def handle_get(
|
|
|
124
153
|
try:
|
|
125
154
|
params, path_vars = _split_params(operation, kwargs)
|
|
126
155
|
obj = ctx.client.get(operation.path.format(**path_vars), params)
|
|
156
|
+
columns = ctx.resolve_columns(op_tag, op_resource, operation)
|
|
127
157
|
render(
|
|
128
158
|
obj,
|
|
129
159
|
format=ctx.output_format,
|
|
130
|
-
columns=
|
|
160
|
+
columns=columns,
|
|
161
|
+
header_labels=_custom_field_header_labels(ctx, operation, columns),
|
|
131
162
|
stream=_out(stream),
|
|
132
163
|
compact=ctx.compact,
|
|
133
164
|
color=ctx.color,
|
|
@@ -611,10 +642,12 @@ def _render_response(
|
|
|
611
642
|
if is_delete:
|
|
612
643
|
_render_delete_ok(ctx, stream=stream)
|
|
613
644
|
return
|
|
645
|
+
columns = ctx.resolve_columns(op_tag, op_resource, operation)
|
|
614
646
|
render(
|
|
615
647
|
response,
|
|
616
648
|
format=ctx.output_format,
|
|
617
|
-
columns=
|
|
649
|
+
columns=columns,
|
|
650
|
+
header_labels=_custom_field_header_labels(ctx, operation, columns),
|
|
618
651
|
stream=stream,
|
|
619
652
|
compact=ctx.compact,
|
|
620
653
|
color=ctx.color,
|
|
@@ -14,6 +14,7 @@ def render(
|
|
|
14
14
|
*,
|
|
15
15
|
stream: TextIO = sys.stdout,
|
|
16
16
|
columns: list[str] | None = None,
|
|
17
|
+
header_labels: dict[str, str] | None = None,
|
|
17
18
|
) -> None:
|
|
18
19
|
records = [data] if isinstance(data, dict) else list(data)
|
|
19
20
|
if not records:
|
|
@@ -21,7 +22,12 @@ def render(
|
|
|
21
22
|
flat_records = [flatten(r, columns=columns) for r in records]
|
|
22
23
|
fieldnames = columns if columns is not None else _gather_fieldnames(flat_records)
|
|
23
24
|
writer = csv.DictWriter(stream, fieldnames=fieldnames, extrasaction="ignore")
|
|
24
|
-
|
|
25
|
+
if header_labels:
|
|
26
|
+
# DictWriter keys data rows by raw fieldname, so emit a relabeled header
|
|
27
|
+
# row manually (instead of writeheader) to keep header/data aligned.
|
|
28
|
+
csv.writer(stream).writerow([header_labels.get(f, f) for f in fieldnames])
|
|
29
|
+
else:
|
|
30
|
+
writer.writeheader()
|
|
25
31
|
for row in flat_records:
|
|
26
32
|
writer.writerow({k: ("" if v is None else v) for k, v in row.items()})
|
|
27
33
|
|
|
@@ -14,6 +14,7 @@ def render(
|
|
|
14
14
|
*,
|
|
15
15
|
format: OutputFormat,
|
|
16
16
|
columns: list[str] | None = None,
|
|
17
|
+
header_labels: dict[str, str] | None = None,
|
|
17
18
|
stream: TextIO = sys.stdout,
|
|
18
19
|
compact: bool = False,
|
|
19
20
|
color: bool = False,
|
|
@@ -26,9 +27,16 @@ def render(
|
|
|
26
27
|
elif format is OutputFormat.YAML:
|
|
27
28
|
yaml_.render(data, stream=stream)
|
|
28
29
|
elif format is OutputFormat.CSV:
|
|
29
|
-
csv_.render(data, stream=stream, columns=columns)
|
|
30
|
+
csv_.render(data, stream=stream, columns=columns, header_labels=header_labels)
|
|
30
31
|
elif format is OutputFormat.TABLE:
|
|
31
|
-
table.render(
|
|
32
|
+
table.render(
|
|
33
|
+
data,
|
|
34
|
+
stream=stream,
|
|
35
|
+
columns=columns,
|
|
36
|
+
color=color,
|
|
37
|
+
object_colors=object_colors,
|
|
38
|
+
header_labels=header_labels,
|
|
39
|
+
)
|
|
32
40
|
else: # pragma: no cover (StrEnum exhaustively covered above)
|
|
33
41
|
raise ValueError(f"unknown output format: {format!r}")
|
|
34
42
|
|
|
@@ -36,6 +36,7 @@ def render(
|
|
|
36
36
|
columns: list[str] | None = None,
|
|
37
37
|
color: bool = False,
|
|
38
38
|
object_colors: bool = False,
|
|
39
|
+
header_labels: dict[str, str] | None = None,
|
|
39
40
|
) -> None:
|
|
40
41
|
records = [data] if isinstance(data, dict) else list(data)
|
|
41
42
|
if not records:
|
|
@@ -48,7 +49,7 @@ def render(
|
|
|
48
49
|
|
|
49
50
|
table = Table(show_header=True, header_style="bold")
|
|
50
51
|
for col in fieldnames:
|
|
51
|
-
table.add_column(col)
|
|
52
|
+
table.add_column(header_labels.get(col, col) if header_labels else col)
|
|
52
53
|
for r in flat_records:
|
|
53
54
|
table.add_row(*[_format_cell(r.get(col, ""), color=color) for col in fieldnames])
|
|
54
55
|
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Resolve a model's custom-field definitions (name -> label, type, choices).
|
|
2
|
+
|
|
3
|
+
The list payload's ``custom_fields`` dict is keyed by field *name* only; the
|
|
4
|
+
human label and type live in the definitions at ``/api/extras/custom-fields/``,
|
|
5
|
+
filtered by ``object_type`` (e.g. ``dcim.device``). This mirrors
|
|
6
|
+
:class:`~nsc.savedfilters.objecttypes.ObjectTypeResolver`: fetch once per object
|
|
7
|
+
type, cache, and return ``None`` (never raise) when the registry can't be reached
|
|
8
|
+
so callers fall back to the raw key. Used to show custom-field column labels
|
|
9
|
+
(#132) and to build per-field edit widgets (#134).
|
|
10
|
+
|
|
11
|
+
Choice values for ``select``/``multiselect`` fields come from a referenced
|
|
12
|
+
choice set; they are fetched best-effort and left empty on any failure.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from collections.abc import Iterable
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Any, Protocol
|
|
20
|
+
|
|
21
|
+
from nsc.http.errors import NetBoxAPIError, NetBoxClientError
|
|
22
|
+
from nsc.savedfilters.objecttypes import ObjectTypeResolver
|
|
23
|
+
|
|
24
|
+
_CUSTOM_FIELDS_PATH = "/api/extras/custom-fields/"
|
|
25
|
+
_CHOICE_SETS_PATH = "/api/extras/custom-field-choice-sets/"
|
|
26
|
+
_CF_PREFIX = "custom_fields."
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _ClientLike(Protocol):
|
|
30
|
+
def paginate(
|
|
31
|
+
self, path: str, params: dict[str, Any] | None = ..., *, limit: int | None = ...
|
|
32
|
+
) -> Any: ...
|
|
33
|
+
|
|
34
|
+
def get(self, path: str, params: dict[str, Any] | None = ...) -> dict[str, Any]: ...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class CustomFieldDef:
|
|
39
|
+
name: str
|
|
40
|
+
label: str
|
|
41
|
+
type: str = "text"
|
|
42
|
+
choices: tuple[str, ...] = ()
|
|
43
|
+
required: bool = False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def humanize(name: str) -> str:
|
|
47
|
+
"""``site_contact`` -> ``Site Contact`` (fallback when a label is blank)."""
|
|
48
|
+
cleaned = name.replace("_", " ").strip()
|
|
49
|
+
return cleaned.title() if cleaned else name
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def custom_field_labels(
|
|
53
|
+
columns: Iterable[str], defs: dict[str, CustomFieldDef] | None
|
|
54
|
+
) -> dict[str, str]:
|
|
55
|
+
"""Map every column key to its display label.
|
|
56
|
+
|
|
57
|
+
Non-custom-field keys map to themselves. ``custom_fields.<name>`` keys map to
|
|
58
|
+
the field's resolved label. When two visible custom-field columns resolve to
|
|
59
|
+
the same label they are ambiguous, so both fall back to their raw key. Unknown
|
|
60
|
+
custom fields and a missing ``defs`` also fall back to the raw key.
|
|
61
|
+
"""
|
|
62
|
+
cols = list(columns)
|
|
63
|
+
if not defs:
|
|
64
|
+
return {col: col for col in cols}
|
|
65
|
+
resolved: dict[str, str] = {}
|
|
66
|
+
for col in cols:
|
|
67
|
+
if col.startswith(_CF_PREFIX) and (cf := defs.get(col[len(_CF_PREFIX) :])) is not None:
|
|
68
|
+
resolved[col] = cf.label
|
|
69
|
+
seen: dict[str, list[str]] = {}
|
|
70
|
+
for col, label in resolved.items():
|
|
71
|
+
seen.setdefault(label, []).append(col)
|
|
72
|
+
labels: dict[str, str] = {}
|
|
73
|
+
for col in cols:
|
|
74
|
+
unique = resolved.get(col)
|
|
75
|
+
if unique is not None and len(seen[unique]) == 1:
|
|
76
|
+
labels[col] = unique
|
|
77
|
+
else:
|
|
78
|
+
labels[col] = col
|
|
79
|
+
return labels
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class CustomFieldResolver:
|
|
83
|
+
"""Looks up custom-field definitions from the live API, cached per object type."""
|
|
84
|
+
|
|
85
|
+
def __init__(self, client: _ClientLike, object_types: ObjectTypeResolver | None = None) -> None:
|
|
86
|
+
self._client = client
|
|
87
|
+
self._object_types = object_types or ObjectTypeResolver(client)
|
|
88
|
+
self._cache: dict[str, dict[str, CustomFieldDef]] = {}
|
|
89
|
+
self._choice_cache: dict[int, tuple[str, ...]] = {}
|
|
90
|
+
|
|
91
|
+
def resolve(self, list_path: str) -> dict[str, CustomFieldDef] | None:
|
|
92
|
+
"""``{name: CustomFieldDef}`` for a list path, or ``None`` if unresolvable."""
|
|
93
|
+
object_type = self._object_types.resolve(list_path)
|
|
94
|
+
if object_type is None:
|
|
95
|
+
return None
|
|
96
|
+
if object_type in self._cache:
|
|
97
|
+
return self._cache[object_type]
|
|
98
|
+
try:
|
|
99
|
+
records = list(self._client.paginate(_CUSTOM_FIELDS_PATH, {"object_type": object_type}))
|
|
100
|
+
except (NetBoxAPIError, NetBoxClientError):
|
|
101
|
+
return None
|
|
102
|
+
defs = {d.name: d for d in (self._to_def(rec) for rec in records)}
|
|
103
|
+
self._cache[object_type] = defs
|
|
104
|
+
return defs
|
|
105
|
+
|
|
106
|
+
def _to_def(self, record: dict[str, Any]) -> CustomFieldDef:
|
|
107
|
+
name = str(record.get("name", ""))
|
|
108
|
+
raw_label = record.get("label")
|
|
109
|
+
label = raw_label if isinstance(raw_label, str) and raw_label else humanize(name)
|
|
110
|
+
cf_type = record.get("type")
|
|
111
|
+
type_value = cf_type.get("value") if isinstance(cf_type, dict) else cf_type
|
|
112
|
+
type_str = type_value if isinstance(type_value, str) else "text"
|
|
113
|
+
choices = self._choices_for(record.get("choice_set"))
|
|
114
|
+
return CustomFieldDef(
|
|
115
|
+
name=name,
|
|
116
|
+
label=label,
|
|
117
|
+
type=type_str,
|
|
118
|
+
choices=choices,
|
|
119
|
+
required=bool(record.get("required", False)),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def _choices_for(self, choice_set: Any) -> tuple[str, ...]:
|
|
123
|
+
if not isinstance(choice_set, dict):
|
|
124
|
+
return ()
|
|
125
|
+
cs_id = choice_set.get("id")
|
|
126
|
+
if not isinstance(cs_id, int):
|
|
127
|
+
return ()
|
|
128
|
+
if cs_id in self._choice_cache:
|
|
129
|
+
return self._choice_cache[cs_id]
|
|
130
|
+
try:
|
|
131
|
+
payload = self._client.get(f"{_CHOICE_SETS_PATH}{cs_id}/")
|
|
132
|
+
except (NetBoxAPIError, NetBoxClientError):
|
|
133
|
+
return ()
|
|
134
|
+
values = _extract_choice_values(payload)
|
|
135
|
+
self._choice_cache[cs_id] = values
|
|
136
|
+
return values
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _extract_choice_values(payload: dict[str, Any]) -> tuple[str, ...]:
|
|
140
|
+
raw = payload.get("choices")
|
|
141
|
+
if not isinstance(raw, list):
|
|
142
|
+
raw = payload.get("extra_choices")
|
|
143
|
+
if not isinstance(raw, list):
|
|
144
|
+
return ()
|
|
145
|
+
values: list[str] = []
|
|
146
|
+
for item in raw:
|
|
147
|
+
if isinstance(item, (list, tuple)) and item:
|
|
148
|
+
values.append(str(item[0]))
|
|
149
|
+
elif isinstance(item, str):
|
|
150
|
+
values.append(item)
|
|
151
|
+
return tuple(values)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Resolve the available NetBox tags for tag-picker widgets (#134).
|
|
2
|
+
|
|
3
|
+
Tags are global (not object-type scoped), so a single fetch of
|
|
4
|
+
``/api/extras/tags/`` is cached for the session. Mirrors the other resolvers:
|
|
5
|
+
returns ``None`` (never raises) when the API can't be reached, so the tag widget
|
|
6
|
+
degrades to free-text input. A writable ``tags`` PATCH wants a list of
|
|
7
|
+
``{name, slug}`` objects, so both are carried.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any, Protocol
|
|
14
|
+
|
|
15
|
+
from nsc.http.errors import NetBoxAPIError, NetBoxClientError
|
|
16
|
+
from nsc.output.colors import normalize_hex
|
|
17
|
+
|
|
18
|
+
_TAGS_PATH = "/api/extras/tags/"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _ClientLike(Protocol):
|
|
22
|
+
def paginate(
|
|
23
|
+
self, path: str, params: dict[str, Any] | None = ..., *, limit: int | None = ...
|
|
24
|
+
) -> Any: ...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class TagDef:
|
|
29
|
+
name: str
|
|
30
|
+
slug: str
|
|
31
|
+
color: str | None = None
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def label(self) -> str:
|
|
35
|
+
return self.name
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TagsResolver:
|
|
39
|
+
"""Fetches the global tag list once and caches it for the session."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, client: _ClientLike) -> None:
|
|
42
|
+
self._client = client
|
|
43
|
+
self._cache: tuple[TagDef, ...] | None = None
|
|
44
|
+
|
|
45
|
+
def resolve(self) -> tuple[TagDef, ...] | None:
|
|
46
|
+
"""All tags, or ``None`` if the API can't be reached."""
|
|
47
|
+
if self._cache is not None:
|
|
48
|
+
return self._cache
|
|
49
|
+
try:
|
|
50
|
+
records = list(self._client.paginate(_TAGS_PATH))
|
|
51
|
+
except (NetBoxAPIError, NetBoxClientError):
|
|
52
|
+
return None
|
|
53
|
+
tags = tuple(
|
|
54
|
+
TagDef(
|
|
55
|
+
name=str(rec.get("name", "")),
|
|
56
|
+
slug=str(rec.get("slug", "")),
|
|
57
|
+
color=normalize_hex(rec.get("color")),
|
|
58
|
+
)
|
|
59
|
+
for rec in records
|
|
60
|
+
)
|
|
61
|
+
self._cache = tags
|
|
62
|
+
return tags
|
|
@@ -53,6 +53,19 @@ class NscTuiApp(App[None]):
|
|
|
53
53
|
self._saved_filter_store = saved_filter_store
|
|
54
54
|
if saved_filter_store is not None and getattr(saved_filter_store, "on_error", None) is None:
|
|
55
55
|
saved_filter_store.on_error = self._notify_saved_filter_issue
|
|
56
|
+
from nsc.savedfilters.custom_fields import CustomFieldResolver # noqa: PLC0415
|
|
57
|
+
from nsc.savedfilters.tags import TagsResolver # noqa: PLC0415
|
|
58
|
+
|
|
59
|
+
self._custom_field_resolver = CustomFieldResolver(client)
|
|
60
|
+
self._tags_resolver = TagsResolver(client)
|
|
61
|
+
|
|
62
|
+
def custom_field_defs_for(self, tag: str, resource: str) -> dict[str, Any] | None:
|
|
63
|
+
"""Custom-field definitions for a resource (read by list/forms), or None."""
|
|
64
|
+
return self._custom_field_resolver.resolve(self._list_path(tag, resource))
|
|
65
|
+
|
|
66
|
+
def available_tags(self) -> Any | None:
|
|
67
|
+
"""All NetBox tags (read by the tag-picker widget), or None if unavailable."""
|
|
68
|
+
return self._tags_resolver.resolve()
|
|
56
69
|
|
|
57
70
|
def columns_for(self, tag: str, resource: str) -> list[str] | None:
|
|
58
71
|
"""Saved visible columns for a resource, if any (read by ListScreen)."""
|