netbox-super-cli 1.3.1__tar.gz → 1.4.0__tar.gz

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