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.
- {costguard_cli-2.1.0 → costguard_cli-2.2.0}/PKG-INFO +1 -1
- {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli/__init__.py +1 -1
- {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli/formatters/markdown.py +25 -5
- {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli.egg-info/PKG-INFO +1 -1
- {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli.egg-info/SOURCES.txt +2 -1
- {costguard_cli-2.1.0 → costguard_cli-2.2.0}/pyproject.toml +1 -1
- costguard_cli-2.2.0/tests/test_resource_table_collapse.py +54 -0
- {costguard_cli-2.1.0 → costguard_cli-2.2.0}/LICENSE +0 -0
- {costguard_cli-2.1.0 → costguard_cli-2.2.0}/README.md +0 -0
- {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli/__main__.py +0 -0
- {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli/formatters/__init__.py +0 -0
- {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli/formatters/html_report.py +0 -0
- {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli/formatters/json_report.py +0 -0
- {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli/formatters/terminal.py +0 -0
- {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli/platforms.py +0 -0
- {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli/py.typed +0 -0
- {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli/validate.py +0 -0
- {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli.egg-info/dependency_links.txt +0 -0
- {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli.egg-info/entry_points.txt +0 -0
- {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli.egg-info/requires.txt +0 -0
- {costguard_cli-2.1.0 → costguard_cli-2.2.0}/costguard_cli.egg-info/top_level.txt +0 -0
- {costguard_cli-2.1.0 → costguard_cli-2.2.0}/setup.cfg +0 -0
- {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.
|
|
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,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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|