panorama-super-cli 0.2.4__tar.gz → 0.2.5__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 (96) hide show
  1. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/CHANGELOG.md +11 -0
  2. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/PKG-INFO +1 -1
  3. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/_version.py +1 -1
  4. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/cli/find_cmds.py +3 -1
  5. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/output/format.py +11 -1
  6. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/pyproject.toml +1 -1
  7. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/tests/test_cli.py +8 -0
  8. panorama_super_cli-0.2.5/tests/test_output.py +65 -0
  9. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/uv.lock +577 -577
  10. panorama_super_cli-0.2.4/tests/test_output.py +0 -35
  11. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/.github/CODEOWNERS +0 -0
  12. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  13. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  14. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  15. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/.github/dependabot.yml +0 -0
  16. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/.github/workflows/docs.yml +0 -0
  17. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/.github/workflows/lint.yml +0 -0
  18. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/.github/workflows/release.yml +0 -0
  19. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/.github/workflows/test.yml +0 -0
  20. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/.gitignore +0 -0
  21. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/.pre-commit-config.yaml +0 -0
  22. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/.python-version +0 -0
  23. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/AGENTS.md +0 -0
  24. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/CLAUDE.md +0 -0
  25. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/LICENSE +0 -0
  26. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/README.md +0 -0
  27. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/docs/contributing/branching.md +0 -0
  28. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/docs/contributing/development.md +0 -0
  29. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/docs/contributing/release-process.md +0 -0
  30. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/docs/getting-started/concepts.md +0 -0
  31. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/docs/getting-started/first-run.md +0 -0
  32. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/docs/getting-started/install.md +0 -0
  33. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/docs/guides/duplicates-and-merging.md +0 -0
  34. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/docs/guides/finding-objects.md +0 -0
  35. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/docs/guides/live-vs-offline.md +0 -0
  36. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/docs/guides/naming.md +0 -0
  37. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/docs/guides/output-formats.md +0 -0
  38. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/docs/guides/references-and-audit.md +0 -0
  39. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/docs/guides/safety.md +0 -0
  40. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/docs/guides/using-with-ai-agents.md +0 -0
  41. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/docs/index.md +0 -0
  42. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/docs/reference/cli.md +0 -0
  43. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/docs/reference/config.md +0 -0
  44. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/docs/reference/exit-codes.md +0 -0
  45. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/justfile +0 -0
  46. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/mkdocs.yml +0 -0
  47. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/__init__.py +0 -0
  48. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/__main__.py +0 -0
  49. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/cli/__init__.py +0 -0
  50. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/cli/_plan.py +0 -0
  51. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/cli/app.py +0 -0
  52. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/cli/auth_cmds.py +0 -0
  53. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/cli/dedup_cmds.py +0 -0
  54. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/cli/name_cmds.py +0 -0
  55. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/cli/profile_cmds.py +0 -0
  56. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/cli/refs_cmds.py +0 -0
  57. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/cli/runtime.py +0 -0
  58. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/config/__init__.py +0 -0
  59. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/config/loader.py +0 -0
  60. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/config/models.py +0 -0
  61. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/core/__init__.py +0 -0
  62. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/core/apply_live.py +0 -0
  63. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/core/apply_xml.py +0 -0
  64. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/core/changeset.py +0 -0
  65. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/core/dedup.py +0 -0
  66. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/core/models.py +0 -0
  67. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/core/naming.py +0 -0
  68. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/core/normalize.py +0 -0
  69. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/core/parse.py +0 -0
  70. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/core/refs.py +0 -0
  71. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/core/resolve.py +0 -0
  72. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/core/setcmd.py +0 -0
  73. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/core/source.py +0 -0
  74. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/output/__init__.py +0 -0
  75. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/psc/output/errors.py +0 -0
  76. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/scripts/sync_agents_md.py +0 -0
  77. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/skills/panorama-super-cli/SKILL.md +0 -0
  78. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/tests/__init__.py +0 -0
  79. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/tests/conftest.py +0 -0
  80. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/tests/fixtures/nested-device-groups.xml +0 -0
  81. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/tests/fixtures/panorama-config.xml +0 -0
  82. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/tests/test_apply_xml.py +0 -0
  83. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/tests/test_auth.py +0 -0
  84. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/tests/test_changeset.py +0 -0
  85. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/tests/test_cli_auth.py +0 -0
  86. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/tests/test_dedup.py +0 -0
  87. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/tests/test_hardening.py +0 -0
  88. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/tests/test_live_apply.py +0 -0
  89. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/tests/test_naming.py +0 -0
  90. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/tests/test_nested_dg.py +0 -0
  91. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/tests/test_normalize.py +0 -0
  92. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/tests/test_parse.py +0 -0
  93. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/tests/test_refs.py +0 -0
  94. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/tests/test_resolve.py +0 -0
  95. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/tests/test_setcmd.py +0 -0
  96. {panorama_super_cli-0.2.4 → panorama_super_cli-0.2.5}/tests/test_source.py +0 -0
@@ -7,6 +7,17 @@ project will follow [Semantic Versioning](https://semver.org/). While on
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## v0.2.5 — 2026-06-04
11
+
12
+ ### Fixed
13
+
14
+ - **`psc find ip` table output now separates each target's matches with a
15
+ horizontal rule** (#43). Resolving several targets at once (notably with
16
+ `--file`) previously printed every match as one undifferentiated block, so it
17
+ was hard to see where one target's matches ended and the next began. The
18
+ table now draws a rule between per-target blocks. Machine output formats
19
+ (json/jsonl/yaml/csv/set) are unchanged.
20
+
10
21
  ## v0.2.4 — 2026-06-04
11
22
 
12
23
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: panorama-super-cli
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary: Agent-friendly CLI for Palo Alto Panorama object management: find, dedup/merge, rename, and audit address/service objects safely.
5
5
  Project-URL: Homepage, https://github.com/thomaschristory/panorama-super-cli
6
6
  Project-URL: Documentation, https://thomaschristory.github.io/panorama-super-cli/
@@ -4,4 +4,4 @@
4
4
  Keep this in sync with `[project].version` in `pyproject.toml`.
5
5
  """
6
6
 
7
- __version__ = "0.2.4"
7
+ __version__ = "0.2.5"
@@ -78,7 +78,9 @@ def ip(
78
78
  raise PscError("no matching objects", ErrorType.NOT_FOUND)
79
79
 
80
80
  model = results if len(results) != 1 else results[0]
81
- render(rt.stdout, rt.output, model=model, rows=rows, table_title="find ip")
81
+ # Group the table by query so multi-target output (e.g. --file) draws a rule
82
+ # between each target's matches instead of one undifferentiated block.
83
+ render(rt.stdout, rt.output, model=model, rows=rows, table_title="find ip", group_by="query")
82
84
 
83
85
 
84
86
  @app.command("object")
@@ -104,12 +104,17 @@ def render(
104
104
  rows: list[dict[str, Any]] | None = None,
105
105
  set_lines: list[str] | None = None,
106
106
  table_title: str | None = None,
107
+ group_by: str | None = None,
107
108
  ) -> None:
108
109
  """Print `model`/`rows`/`set_lines` in `fmt` to `console`.
109
110
 
110
111
  The console must be the *stdout* console; errors go elsewhere. Table output
111
112
  is the only format that uses rich styling — every other format is plain text
112
113
  so pipes and captures stay clean.
114
+
115
+ `group_by` is a table-only hint: a column name whose changing value marks a
116
+ new block. A horizontal rule is drawn between blocks so multi-target output
117
+ (e.g. `find ip --file`) is easy to scan. It does not affect machine formats.
113
118
  """
114
119
  # `soft_wrap=True` is critical: without it rich wraps long lines to the
115
120
  # console width and injects newlines *inside* JSON string values, producing
@@ -135,8 +140,13 @@ def render(
135
140
  table = Table(title=table_title, header_style="bold cyan")
136
141
  for col in rows[0]:
137
142
  table.add_column(col)
138
- for row in rows:
143
+ prev: Any = None
144
+ for i, row in enumerate(rows):
145
+ if group_by and i > 0 and row.get(group_by) != prev:
146
+ table.add_section()
139
147
  table.add_row(*[_cell(row.get(col)) for col in rows[0]])
148
+ if group_by:
149
+ prev = row.get(group_by)
140
150
  console.print(table)
141
151
  else:
142
152
  console.print(_json(model), markup=False, highlight=False)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "panorama-super-cli"
3
- version = "0.2.4"
3
+ version = "0.2.5"
4
4
  description = "Agent-friendly CLI for Palo Alto Panorama object management: find, dedup/merge, rename, and audit address/service objects safely."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -64,6 +64,14 @@ def test_strict_not_found_exit_5() -> None:
64
64
  assert json.loads(cp.stdout)["type"] == "not_found"
65
65
 
66
66
 
67
+ def test_find_ip_table_separates_multiple_targets() -> None:
68
+ # Issue #43: multi-target table output must draw a rule between each
69
+ # target's matches. The interior divider uses box-drawing '├'.
70
+ cp = run("-c", str(FIXTURE), "-o", "table", "find", "ip", "10.0.0.10", "10.0.0.99")
71
+ assert cp.returncode == 0
72
+ assert "├" in cp.stdout
73
+
74
+
67
75
  _HOST_AND_NET_CONFIG = """<?xml version="1.0"?>
68
76
  <config version="11.0.0">
69
77
  <shared>
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import json
5
+
6
+ from rich.console import Console
7
+
8
+ from psc.core.models import SHARED, Location
9
+ from psc.core.refs import Reference, Target
10
+ from psc.output.format import OutputFormat, render, to_jsonable
11
+
12
+
13
+ def test_location_serializes_as_name() -> None:
14
+ assert to_jsonable(SHARED) == "shared"
15
+ assert to_jsonable(Location.dg("DG1")) == "DG1"
16
+
17
+
18
+ def test_location_roundtrips_from_string() -> None:
19
+ loc = Location.model_validate("DG1")
20
+ assert loc.device_group == "DG1"
21
+ assert Location.model_validate("shared").is_shared
22
+
23
+
24
+ def test_dataclass_is_jsonable() -> None:
25
+ ref = Reference(
26
+ target_name="x",
27
+ namespace="address",
28
+ referrer_kind="security-rule",
29
+ referrer_name="r",
30
+ referrer_location=SHARED,
31
+ field="source",
32
+ resolved=Target("address", "x", SHARED),
33
+ )
34
+ payload = to_jsonable(ref)
35
+ # round-trips through json without error
36
+ json.dumps(payload)
37
+ assert payload["resolved"]["location"] == "shared"
38
+ assert payload["referrer_location"] == "shared"
39
+
40
+
41
+ def _render_table(rows: list[dict[str, object]], **kw: object) -> str:
42
+ buf = io.StringIO()
43
+ console = Console(file=buf, width=120, force_terminal=False, color_system=None)
44
+ render(console, OutputFormat.TABLE, rows=rows, table_title="t", **kw)
45
+ return buf.getvalue()
46
+
47
+
48
+ def test_table_group_by_inserts_divider_between_groups() -> None:
49
+ rows = [
50
+ {"query": "10.0.0.1", "object": "a"},
51
+ {"query": "10.0.0.1", "object": "b"},
52
+ {"query": "10.0.0.2", "object": "c"},
53
+ ]
54
+ plain = _render_table(rows)
55
+ grouped = _render_table(rows, group_by="query")
56
+ # Grouping draws one extra horizontal rule between the two query blocks.
57
+ assert grouped.count("\n") == plain.count("\n") + 1
58
+
59
+
60
+ def test_table_group_by_single_group_no_extra_divider() -> None:
61
+ rows = [
62
+ {"query": "10.0.0.1", "object": "a"},
63
+ {"query": "10.0.0.1", "object": "b"},
64
+ ]
65
+ assert _render_table(rows, group_by="query") == _render_table(rows)