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.
Files changed (335) hide show
  1. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/CHANGELOG.md +36 -0
  2. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/PKG-INFO +1 -1
  3. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/_version.py +1 -1
  4. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/handlers.py +36 -3
  5. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/csv_.py +7 -1
  6. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/render.py +10 -2
  7. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/table.py +2 -1
  8. netbox_super_cli-1.6.0/nsc/savedfilters/custom_fields.py +151 -0
  9. netbox_super_cli-1.6.0/nsc/savedfilters/tags.py +62 -0
  10. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/app.py +13 -0
  11. netbox_super_cli-1.6.0/nsc/tui/forms.py +261 -0
  12. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/screens/bulk_edit_form.py +92 -20
  13. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/screens/columns.py +9 -2
  14. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/screens/detail.py +40 -19
  15. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/screens/edit_form.py +73 -12
  16. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/screens/list.py +37 -2
  17. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/styles.tcss +33 -0
  18. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/view.py +8 -2
  19. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/pyproject.toml +1 -1
  20. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_handlers.py +48 -0
  21. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_csv.py +13 -0
  22. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_table.py +14 -0
  23. netbox_super_cli-1.6.0/tests/savedfilters/test_custom_fields.py +157 -0
  24. netbox_super_cli-1.6.0/tests/savedfilters/test_tags.py +54 -0
  25. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_bulk_edit_form.py +153 -2
  26. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_column_chooser.py +30 -3
  27. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_detail_screen.py +90 -0
  28. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_edit_form.py +181 -2
  29. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_forms.py +85 -0
  30. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_list_screen.py +65 -0
  31. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/uv.lock +1 -1
  32. netbox_super_cli-1.5.0/nsc/tui/forms.py +0 -129
  33. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  34. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  35. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/ISSUE_TEMPLATE/docs.yml +0 -0
  36. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  37. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/dependabot.yml +0 -0
  38. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/pull_request_template.md +0 -0
  39. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/workflows/bench.yml +0 -0
  40. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/workflows/claude.yml +0 -0
  41. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/workflows/docs.yml +0 -0
  42. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/workflows/e2e.yml +0 -0
  43. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/workflows/lint.yml +0 -0
  44. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/workflows/release.yml +0 -0
  45. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/workflows/skill-convention-review.yml +0 -0
  46. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.github/workflows/test.yml +0 -0
  47. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.gitignore +0 -0
  48. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.pre-commit-config.yaml +0 -0
  49. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/.python-version +0 -0
  50. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/AGENTS.md +0 -0
  51. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/CLAUDE.md +0 -0
  52. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/LICENSE +0 -0
  53. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/README.md +0 -0
  54. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/architecture/caching.md +0 -0
  55. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/architecture/command-generation.md +0 -0
  56. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/architecture/http-client.md +0 -0
  57. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/architecture/overview.md +0 -0
  58. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/architecture/schema-loading.md +0 -0
  59. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/assets/tui/detail.svg +0 -0
  60. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/assets/tui/filter.svg +0 -0
  61. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/assets/tui/list.svg +0 -0
  62. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/assets/tui/picker.svg +0 -0
  63. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/assets/tui/search.svg +0 -0
  64. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/contributing/adding-bundled-schemas.md +0 -0
  65. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/contributing/branching.md +0 -0
  66. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/contributing/development.md +0 -0
  67. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/contributing/release-process.md +0 -0
  68. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/getting-started/concepts.md +0 -0
  69. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/getting-started/first-run.md +0 -0
  70. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/getting-started/install.md +0 -0
  71. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/guides/audit-log.md +0 -0
  72. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/guides/ci-and-automation.md +0 -0
  73. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/guides/interactive-tui.md +0 -0
  74. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/guides/managing-profiles.md +0 -0
  75. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/guides/output-formats.md +0 -0
  76. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/guides/using-with-ai-agents.md +0 -0
  77. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/guides/working-with-plugins.md +0 -0
  78. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/guides/writes-and-safety.md +0 -0
  79. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/index.md +0 -0
  80. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/reference/cli.md +0 -0
  81. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/reference/config.md +0 -0
  82. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/reference/exit-codes.md +0 -0
  83. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/docs/reference/schemas.md +0 -0
  84. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/justfile +0 -0
  85. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/mkdocs.yml +0 -0
  86. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/__init__.py +0 -0
  87. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/__main__.py +0 -0
  88. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/aliases/__init__.py +0 -0
  89. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/aliases/resolver.py +0 -0
  90. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/auth/__init__.py +0 -0
  91. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/auth/verify.py +0 -0
  92. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/builder/__init__.py +0 -0
  93. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/builder/build.py +0 -0
  94. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cache/__init__.py +0 -0
  95. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cache/store.py +0 -0
  96. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/__init__.py +0 -0
  97. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/aliases_commands.py +0 -0
  98. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/app.py +0 -0
  99. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/cache_commands.py +0 -0
  100. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/commands_dump.py +0 -0
  101. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/config_commands.py +0 -0
  102. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/globals.py +0 -0
  103. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/init_commands.py +0 -0
  104. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/login_commands.py +0 -0
  105. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/profiles_commands.py +0 -0
  106. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/registration.py +0 -0
  107. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/runtime.py +0 -0
  108. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/skill_commands.py +0 -0
  109. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/tui_commands.py +0 -0
  110. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/writes/__init__.py +0 -0
  111. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/writes/apply.py +0 -0
  112. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/writes/bulk.py +0 -0
  113. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/writes/coercion.py +0 -0
  114. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/writes/confirmation.py +0 -0
  115. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/writes/input.py +0 -0
  116. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/cli/writes/preflight.py +0 -0
  117. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/completion/__init__.py +0 -0
  118. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/completion/cache_probe.py +0 -0
  119. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/completion/callbacks.py +0 -0
  120. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/completion/providers.py +0 -0
  121. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/config/__init__.py +0 -0
  122. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/config/loader.py +0 -0
  123. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/config/models.py +0 -0
  124. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/config/saved_searches.py +0 -0
  125. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/config/settings.py +0 -0
  126. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/config/writer.py +0 -0
  127. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/http/__init__.py +0 -0
  128. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/http/audit.py +0 -0
  129. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/http/client.py +0 -0
  130. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/http/errors.py +0 -0
  131. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/http/retry.py +0 -0
  132. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/model/__init__.py +0 -0
  133. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/model/command_model.py +0 -0
  134. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/__init__.py +0 -0
  135. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/_console.py +0 -0
  136. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/colors.py +0 -0
  137. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/errors.py +0 -0
  138. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/explain.py +0 -0
  139. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/flatten.py +0 -0
  140. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/headers.py +0 -0
  141. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/json_.py +0 -0
  142. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/jsonl.py +0 -0
  143. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/output/yaml_.py +0 -0
  144. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/savedfilters/__init__.py +0 -0
  145. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/savedfilters/objecttypes.py +0 -0
  146. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/savedfilters/params.py +0 -0
  147. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/savedfilters/store.py +0 -0
  148. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/schema/__init__.py +0 -0
  149. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/schema/hashing.py +0 -0
  150. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/schema/loader.py +0 -0
  151. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/schema/models.py +0 -0
  152. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/schema/source.py +0 -0
  153. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/schemas/__init__.py +0 -0
  154. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/schemas/bundled/__init__.py +0 -0
  155. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/schemas/bundled/manifest.yaml +0 -0
  156. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/schemas/bundled/netbox-4.5.10.json.gz +0 -0
  157. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/schemas/bundled/netbox-4.6.0.json.gz +0 -0
  158. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/skill/__init__.py +0 -0
  159. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/__init__.py +0 -0
  160. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/_bindings.py +0 -0
  161. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/bulk.py +0 -0
  162. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/catalog.py +0 -0
  163. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/columns.py +0 -0
  164. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/errors.py +0 -0
  165. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/filters.py +0 -0
  166. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/fk.py +0 -0
  167. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/keymap.py +0 -0
  168. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/nav.py +0 -0
  169. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/relations.py +0 -0
  170. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/screens/__init__.py +0 -0
  171. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/screens/filter.py +0 -0
  172. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/screens/global_search.py +0 -0
  173. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/screens/picker.py +0 -0
  174. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/screens/record_picker.py +0 -0
  175. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/screens/saved_search_picker.py +0 -0
  176. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/search.py +0 -0
  177. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/selection.py +0 -0
  178. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/widgets/__init__.py +0 -0
  179. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/widgets/_modal.py +0 -0
  180. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/widgets/bulk_diff.py +0 -0
  181. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/widgets/bulk_summary.py +0 -0
  182. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/widgets/confirm.py +0 -0
  183. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/widgets/diff.py +0 -0
  184. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/widgets/help.py +0 -0
  185. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/nsc/tui/widgets/nav_tree.py +0 -0
  186. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/scripts/gen_docs.py +0 -0
  187. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/scripts/sync_agents_md.py +0 -0
  188. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/skills/netbox-super-cli/SKILL.md +0 -0
  189. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/__init__.py +0 -0
  190. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/aliases/__init__.py +0 -0
  191. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/aliases/test_resolver.py +0 -0
  192. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/audit/__init__.py +0 -0
  193. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/audit/test_redaction_full.py +0 -0
  194. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/auth/__init__.py +0 -0
  195. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/auth/test_verify.py +0 -0
  196. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/benchmarks/__init__.py +0 -0
  197. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/benchmarks/test_startup.py +0 -0
  198. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/builder/__init__.py +0 -0
  199. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/builder/test_build.py +0 -0
  200. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/builder/test_default_columns.py +0 -0
  201. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/builder/test_redaction_marking.py +0 -0
  202. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/builder/test_request_body_shape.py +0 -0
  203. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cache/__init__.py +0 -0
  204. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cache/test_prune.py +0 -0
  205. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cache/test_store.py +0 -0
  206. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/__init__.py +0 -0
  207. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_aliases_commands.py +0 -0
  208. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_app_smoke.py +0 -0
  209. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_cache_commands.py +0 -0
  210. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_commands_dump.py +0 -0
  211. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_completion_protocol_smoke.py +0 -0
  212. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_completion_smoke.py +0 -0
  213. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_config_commands.py +0 -0
  214. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_dynamic_completion.py +0 -0
  215. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_explain.py +0 -0
  216. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_globals_color.py +0 -0
  217. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_init_commands.py +0 -0
  218. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_lazy_textual_import.py +0 -0
  219. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_login_commands.py +0 -0
  220. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_meta_subcommands_under_broken_config.py +0 -0
  221. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_ndjson_input.py +0 -0
  222. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_profiles_commands.py +0 -0
  223. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_registration.py +0 -0
  224. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_respx_integration.py +0 -0
  225. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_runtime.py +0 -0
  226. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_runtime_color.py +0 -0
  227. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_skill_commands.py +0 -0
  228. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_stdin_sniffer.py +0 -0
  229. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_tui_command.py +0 -0
  230. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/test_writes_respx.py +0 -0
  231. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/writes/__init__.py +0 -0
  232. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/writes/test_apply.py +0 -0
  233. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/writes/test_bulk.py +0 -0
  234. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/writes/test_confirmation.py +0 -0
  235. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/writes/test_handler_helpers.py +0 -0
  236. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/writes/test_handlers_audit.py +0 -0
  237. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/writes/test_input.py +0 -0
  238. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/cli/writes/test_preflight.py +0 -0
  239. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/config/__init__.py +0 -0
  240. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/config/test_loader.py +0 -0
  241. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/config/test_models.py +0 -0
  242. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/config/test_saved_search_fallback.py +0 -0
  243. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/config/test_saved_searches.py +0 -0
  244. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/config/test_settings_private_dir.py +0 -0
  245. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/config/test_writer_atomicity.py +0 -0
  246. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/config/test_writer_dotted_paths.py +0 -0
  247. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/config/test_writer_roundtrip.py +0 -0
  248. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/conftest.py +0 -0
  249. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/README.md +0 -0
  250. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/__init__.py +0 -0
  251. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/conftest.py +0 -0
  252. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/docker-compose.yml +0 -0
  253. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/test_audit_redaction.py +0 -0
  254. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/test_auth_error_envelope.py +0 -0
  255. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/test_bulk_create_with_loop_fallback.py +0 -0
  256. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/test_full_cycle.py +0 -0
  257. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/test_login.py +0 -0
  258. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/test_ndjson.py +0 -0
  259. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/test_preflight_blocks_apply.py +0 -0
  260. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/test_validation_error_envelope_from_netbox.py +0 -0
  261. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/e2e/wait_for_netbox.sh +0 -0
  262. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/fixtures/profiles/single_profile.yaml +0 -0
  263. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/fixtures/responses/auth_401.json +0 -0
  264. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/fixtures/responses/circuits_providers_list.json +0 -0
  265. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/fixtures/responses/dcim_devices_get.json +0 -0
  266. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/fixtures/responses/dcim_devices_list_p1.json +0 -0
  267. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/fixtures/responses/dcim_devices_list_p2.json +0 -0
  268. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/http/__init__.py +0 -0
  269. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/http/test_audit.py +0 -0
  270. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/http/test_audit_permissions.py +0 -0
  271. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/http/test_audit_redaction.py +0 -0
  272. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/http/test_client.py +0 -0
  273. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/http/test_client_redaction_threading.py +0 -0
  274. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/http/test_errors.py +0 -0
  275. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/http/test_retry.py +0 -0
  276. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/model/__init__.py +0 -0
  277. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/model/test_command_model.py +0 -0
  278. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/model/test_request_body_shape.py +0 -0
  279. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/__init__.py +0 -0
  280. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_colors.py +0 -0
  281. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_console.py +0 -0
  282. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_errors.py +0 -0
  283. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_errors_aliases.py +0 -0
  284. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_errors_color.py +0 -0
  285. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_explain.py +0 -0
  286. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_explain_color.py +0 -0
  287. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_flatten.py +0 -0
  288. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_input_error_envelope.py +0 -0
  289. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_json.py +0 -0
  290. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_jsonl.py +0 -0
  291. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_render_dispatch.py +0 -0
  292. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_table_color.py +0 -0
  293. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/output/test_yaml.py +0 -0
  294. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/savedfilters/__init__.py +0 -0
  295. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/savedfilters/test_objecttypes.py +0 -0
  296. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/savedfilters/test_params.py +0 -0
  297. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/savedfilters/test_store.py +0 -0
  298. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/schema/__init__.py +0 -0
  299. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/schema/test_hashing.py +0 -0
  300. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/schema/test_loader.py +0 -0
  301. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/schema/test_models.py +0 -0
  302. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/schema/test_source.py +0 -0
  303. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/schema/test_source_ttl.py +0 -0
  304. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/scripts/__init__.py +0 -0
  305. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/scripts/test_gen_docs.py +0 -0
  306. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/scripts/test_sync_agents_md.py +0 -0
  307. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/skill/__init__.py +0 -0
  308. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/skill/test_bundle.py +0 -0
  309. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/test_packaging.py +0 -0
  310. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/__init__.py +0 -0
  311. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_app.py +0 -0
  312. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_bindings.py +0 -0
  313. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_bulk_apply.py +0 -0
  314. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_bulk_diff.py +0 -0
  315. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_bulk_diff_modal.py +0 -0
  316. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_bulk_import_isolation.py +0 -0
  317. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_bulk_summary.py +0 -0
  318. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_catalog.py +0 -0
  319. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_columns.py +0 -0
  320. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_diff.py +0 -0
  321. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_diff_modal.py +0 -0
  322. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_errors.py +0 -0
  323. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_filter_screen.py +0 -0
  324. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_filters.py +0 -0
  325. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_fk_resolve.py +0 -0
  326. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_global_search.py +0 -0
  327. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_help_overlay.py +0 -0
  328. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_keymap.py +0 -0
  329. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_picker.py +0 -0
  330. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_record_picker.py +0 -0
  331. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_relations.py +0 -0
  332. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_saved_filter_integration.py +0 -0
  333. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_search.py +0 -0
  334. {netbox_super_cli-1.5.0 → netbox_super_cli-1.6.0}/tests/tui/test_selection.py +0 -0
  335. {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.5.0
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/
@@ -1,3 +1,3 @@
1
1
  """Single source of truth for the package version."""
2
2
 
3
- __version__ = "1.5.0"
3
+ __version__ = "1.6.0"
@@ -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=ctx.resolve_columns(op_tag, op_resource, operation),
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=ctx.resolve_columns(op_tag, op_resource, operation),
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=ctx.resolve_columns(op_tag, op_resource, operation),
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
- writer.writeheader()
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(data, stream=stream, columns=columns, color=color, object_colors=object_colors)
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)."""