costguard-cli 2.1.0__tar.gz → 2.2.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 (23) hide show
  1. {costguard_cli-2.1.0 → costguard_cli-2.2.0}/PKG-INFO +1 -1
  2. {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli/__init__.py +1 -1
  3. {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli/formatters/markdown.py +25 -5
  4. {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli.egg-info/PKG-INFO +1 -1
  5. {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli.egg-info/SOURCES.txt +2 -1
  6. {costguard_cli-2.1.0 → costguard_cli-2.2.0}/pyproject.toml +1 -1
  7. costguard_cli-2.2.0/tests/test_resource_table_collapse.py +54 -0
  8. {costguard_cli-2.1.0 → costguard_cli-2.2.0}/LICENSE +0 -0
  9. {costguard_cli-2.1.0 → costguard_cli-2.2.0}/README.md +0 -0
  10. {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli/__main__.py +0 -0
  11. {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli/formatters/__init__.py +0 -0
  12. {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli/formatters/html_report.py +0 -0
  13. {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli/formatters/json_report.py +0 -0
  14. {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli/formatters/terminal.py +0 -0
  15. {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli/platforms.py +0 -0
  16. {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli/py.typed +0 -0
  17. {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli/validate.py +0 -0
  18. {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli.egg-info/dependency_links.txt +0 -0
  19. {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli.egg-info/entry_points.txt +0 -0
  20. {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli.egg-info/requires.txt +0 -0
  21. {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli.egg-info/top_level.txt +0 -0
  22. {costguard_cli-2.1.0 → costguard_cli-2.2.0}/setup.cfg +0 -0
  23. {costguard_cli-2.1.0 → costguard_cli-2.2.0}/tests/test_formatters_cost_diff.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: costguard-cli
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: CostGuard CI/CD validation CLI — shift-left cost governance for cloud infrastructure
5
5
  Author-email: SKYXOPS <engineering@skyxops.com>
6
6
  License-Expression: LicenseRef-Proprietary
@@ -1,3 +1,3 @@
1
1
  """CostGuard CLI — shift-left cost governance for CI/CD pipelines."""
2
2
 
3
- __version__ = "2.1.0"
3
+ __version__ = "2.2.0"
@@ -17,6 +17,11 @@ class MarkdownFormatter:
17
17
  "low": "🔵",
18
18
  }
19
19
 
20
+ # Wrap per-resource table in <details> when row count exceeds this.
21
+ # Big plans (50-100+ resources) otherwise bury budget/violations/AI
22
+ # narrative below an unreadable wall of rows.
23
+ RESOURCE_COLLAPSE_THRESHOLD = 10
24
+
20
25
  def format(self, response: dict, tag: str | None = None, **kwargs) -> str:
21
26
  """Format the full CostGuard API response as Markdown."""
22
27
  sections: list[str] = []
@@ -155,7 +160,7 @@ class MarkdownFormatter:
155
160
  header = "| Resource | Type | Region | Action | Past | Planned | Δ |"
156
161
  divider = "|----------|------|--------|--------|-----:|--------:|---:|"
157
162
 
158
- lines = ["### Per-Resource Costs\n", header, divider]
163
+ rows = [header, divider]
159
164
 
160
165
  for r in resources:
161
166
  name = r.get("resource_name") or r.get("resource_id", "unknown")
@@ -179,21 +184,36 @@ class MarkdownFormatter:
179
184
 
180
185
  if pure_create:
181
186
  cost_str = fmt(planned if planned is not None else mc)
182
- lines.append(f"| `{name}` | {rtype} | {region} | {cost_str}{status} |")
187
+ rows.append(f"| `{name}` | {rtype} | {region} | {cost_str}{status} |")
183
188
  elif pure_delete:
184
189
  savings_str = fmt_signed(delta) if delta is not None else fmt_signed(-(past or 0))
185
- lines.append(f"| `{name}` | {rtype} | {region} | {savings_str}{status} |")
190
+ rows.append(f"| `{name}` | {rtype} | {region} | {savings_str}{status} |")
186
191
  else:
187
192
  action_str = (action or "-").capitalize() if action else "-"
188
193
  past_str = "—" if action == "create" else fmt(past)
189
194
  planned_str = "—" if action == "delete" else fmt(planned)
190
195
  delta_str = fmt_signed(delta) if delta is not None else fmt(mc)
191
- lines.append(
196
+ rows.append(
192
197
  f"| `{name}` | {rtype} | {region} | {action_str} | "
193
198
  f"{past_str} | {planned_str} | {delta_str}{status} |"
194
199
  )
195
200
 
196
- return "\n".join(lines)
201
+ table = "\n".join(rows)
202
+ count = len(resources)
203
+
204
+ # Long resource lists make PR/MR comments huge and dominate the review
205
+ # surface. Wrap the table in <details> when it crosses the threshold so
206
+ # reviewers see the summary by default and can expand on demand.
207
+ # GitLab and GitHub both render <details>/<summary> as native folds.
208
+ if count > self.RESOURCE_COLLAPSE_THRESHOLD:
209
+ return (
210
+ "### Per-Resource Costs\n\n"
211
+ f"<details>\n"
212
+ f"<summary>Show {count} resources</summary>\n\n"
213
+ f"{table}\n\n"
214
+ f"</details>"
215
+ )
216
+ return f"### Per-Resource Costs\n\n{table}"
197
217
 
198
218
  def _budget_section(self, budget: dict) -> str:
199
219
  budget_name = budget.get("budget_name", "Unknown")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: costguard-cli
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: CostGuard CI/CD validation CLI — shift-left cost governance for cloud infrastructure
5
5
  Author-email: SKYXOPS <engineering@skyxops.com>
6
6
  License-Expression: LicenseRef-Proprietary
@@ -17,4 +17,5 @@ costguard_cli/formatters/html_report.py
17
17
  costguard_cli/formatters/json_report.py
18
18
  costguard_cli/formatters/markdown.py
19
19
  costguard_cli/formatters/terminal.py
20
- tests/test_formatters_cost_diff.py
20
+ tests/test_formatters_cost_diff.py
21
+ tests/test_resource_table_collapse.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "costguard-cli"
7
- version = "2.1.0"
7
+ version = "2.2.0"
8
8
  description = "CostGuard CI/CD validation CLI — shift-left cost governance for cloud infrastructure"
9
9
  requires-python = ">=3.9"
10
10
  license = "LicenseRef-Proprietary"
@@ -0,0 +1,54 @@
1
+ """AI-XMOD-202: collapse per-resource cost table when > RESOURCE_COLLAPSE_THRESHOLD."""
2
+
3
+ from costguard_cli.formatters.markdown import MarkdownFormatter
4
+
5
+
6
+ def _response(n_resources: int) -> dict:
7
+ return {
8
+ "endpoint_data": {"decision": "ALLOW", "budget": {}, "violations": []},
9
+ "results": {
10
+ "total_monthly_usd": n_resources * 10,
11
+ "resources": [
12
+ {
13
+ "resource_name": f"aws_instance.r{i}",
14
+ "resource_type": "aws_instance",
15
+ "region": "us-east-1",
16
+ "monthly_cost": 10.0,
17
+ "extensions": {"action": "create", "planned_monthly_cost": 10.0},
18
+ }
19
+ for i in range(n_resources)
20
+ ],
21
+ },
22
+ "request_id": "test",
23
+ "timestamp": "2026-05-24T00:00:00",
24
+ "processing_time_ms": 1,
25
+ }
26
+
27
+
28
+ def test_short_table_renders_flat():
29
+ out = MarkdownFormatter().format(_response(5))
30
+ assert "### Per-Resource Costs" in out
31
+ assert "<summary>Show 5 resources" not in out
32
+ assert "aws_instance.r0" in out
33
+ assert "aws_instance.r4" in out
34
+
35
+
36
+ def test_long_table_wrapped_in_details():
37
+ out = MarkdownFormatter().format(_response(15))
38
+ assert "### Per-Resource Costs" in out
39
+ assert "<details>" in out
40
+ assert "<summary>Show 15 resources</summary>" in out
41
+ assert "</details>" in out
42
+ assert "aws_instance.r0" in out
43
+ assert "aws_instance.r14" in out
44
+
45
+
46
+ def test_threshold_boundary_exact_match_not_collapsed():
47
+ # At exactly the threshold, stay flat (strictly greater than collapses).
48
+ out = MarkdownFormatter().format(_response(MarkdownFormatter.RESOURCE_COLLAPSE_THRESHOLD))
49
+ assert "<summary>Show" not in out
50
+
51
+
52
+ def test_threshold_boundary_one_over_collapses():
53
+ out = MarkdownFormatter().format(_response(MarkdownFormatter.RESOURCE_COLLAPSE_THRESHOLD + 1))
54
+ assert "<summary>Show" in out
File without changes
File without changes
File without changes