sofe 0.4.2__tar.gz → 0.4.4__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.
- {sofe-0.4.2/sofe.egg-info → sofe-0.4.4}/PKG-INFO +2 -2
- {sofe-0.4.2 → sofe-0.4.4}/README.md +1 -1
- {sofe-0.4.2 → sofe-0.4.4}/pyproject.toml +1 -1
- {sofe-0.4.2 → sofe-0.4.4}/sofe/__init__.py +1 -1
- {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/__init__.py +36 -4
- {sofe-0.4.2 → sofe-0.4.4}/sofe/engine/__init__.py +5 -1
- {sofe-0.4.2 → sofe-0.4.4/sofe.egg-info}/PKG-INFO +2 -2
- {sofe-0.4.2 → sofe-0.4.4}/sofe.egg-info/SOURCES.txt +1 -0
- sofe-0.4.4/tests/test_savings_policies.py +65 -0
- {sofe-0.4.2 → sofe-0.4.4}/LICENSE +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/setup.cfg +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/cli.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/__init__.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/apigateway.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/base.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/cloudfront.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/commitments.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/cost.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/dynamodb.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/ebs.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/ec2.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/ecs.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/eks.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/elasticache.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/elb.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/lambda_.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/natgateway.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/rds.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/recommendations.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/redshift.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/route53.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/s3.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/sagemaker.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/secretsmanager.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/engine/architecture.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/loader/__init__.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/models/__init__.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/remediation/__init__.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe/remediation/commands.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe.egg-info/dependency_links.txt +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe.egg-info/entry_points.txt +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe.egg-info/requires.txt +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/sofe.egg-info/top_level.txt +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/tests/test_architecture.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/tests/test_cli_outputs.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/tests/test_commitments.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/tests/test_recommendations.py +0 -0
- {sofe-0.4.2 → sofe-0.4.4}/tests/test_validate.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sofe
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
4
4
|
Summary: Stairway Open FinOps Engine — FinOps Policies as Code for AWS
|
|
5
5
|
Author-email: Carlos Cortez <carlos@cortez.cloud>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -51,7 +51,7 @@ Dynamic: license-file
|
|
|
51
51
|
<img src="https://img.shields.io/pypi/v/sofe?style=flat-square&logo=pypi&color=8A2BE2" alt="PyPI">
|
|
52
52
|
<img src="https://img.shields.io/badge/python-3.10%2B-blue?style=flat-square&logo=python" alt="Python">
|
|
53
53
|
<img src="https://img.shields.io/badge/license-Apache_2.0-8A2BE2?style=flat-square" alt="License">
|
|
54
|
-
<img src="https://img.shields.io/badge/policies-
|
|
54
|
+
<img src="https://img.shields.io/badge/policies-39-success?style=flat-square" alt="39 policies">
|
|
55
55
|
<img src="https://img.shields.io/badge/collectors-19-blue?style=flat-square" alt="19 collectors">
|
|
56
56
|
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen?style=flat-square" alt="PRs">
|
|
57
57
|
</p>
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
<img src="https://img.shields.io/pypi/v/sofe?style=flat-square&logo=pypi&color=8A2BE2" alt="PyPI">
|
|
24
24
|
<img src="https://img.shields.io/badge/python-3.10%2B-blue?style=flat-square&logo=python" alt="Python">
|
|
25
25
|
<img src="https://img.shields.io/badge/license-Apache_2.0-8A2BE2?style=flat-square" alt="License">
|
|
26
|
-
<img src="https://img.shields.io/badge/policies-
|
|
26
|
+
<img src="https://img.shields.io/badge/policies-39-success?style=flat-square" alt="39 policies">
|
|
27
27
|
<img src="https://img.shields.io/badge/collectors-19-blue?style=flat-square" alt="19 collectors">
|
|
28
28
|
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen?style=flat-square" alt="PRs">
|
|
29
29
|
</p>
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""SOFE — Stairway Open FinOps Engine. Policies as Code for AWS."""
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.4.4"
|
|
@@ -42,7 +42,7 @@ def collect_all(profile: str = None, resource_types: list[str] = None, regions:
|
|
|
42
42
|
_enrich_costs(session, resources, account_id)
|
|
43
43
|
|
|
44
44
|
# Enrich with commitment coverage/utilization (SP + RI)
|
|
45
|
-
_enrich_commitments(session, resources, account_id)
|
|
45
|
+
commit_metrics = _enrich_commitments(session, resources, account_id)
|
|
46
46
|
|
|
47
47
|
# S082 — collect savings/rightsizing recommendations (findings)
|
|
48
48
|
_enrich_recommendations(session, account_id, target_regions)
|
|
@@ -52,6 +52,10 @@ def collect_all(profile: str = None, resource_types: list[str] = None, regions:
|
|
|
52
52
|
for key in ['owner', 'env', 'costCenter', 'Environment', 'Name']:
|
|
53
53
|
r.metrics[f'has_tag:{key}'] = 1.0 if key in r.tags else 0.0
|
|
54
54
|
|
|
55
|
+
# S083 — synthetic account resource (account-level metrics for savings policies)
|
|
56
|
+
account_resource = _build_account_resource(account_id, commit_metrics, target_regions[0])
|
|
57
|
+
resources.append(account_resource)
|
|
58
|
+
|
|
55
59
|
return resources
|
|
56
60
|
|
|
57
61
|
|
|
@@ -129,8 +133,11 @@ def _enrich_costs(session: boto3.Session, resources: list[Resource], account_id:
|
|
|
129
133
|
pass # Graceful fallback — evaluation works without cost data
|
|
130
134
|
|
|
131
135
|
|
|
132
|
-
def _enrich_commitments(session: boto3.Session, resources: list[Resource], account_id: str):
|
|
133
|
-
"""Enrich resources with commitment coverage/utilization from Cost Explorer.
|
|
136
|
+
def _enrich_commitments(session: boto3.Session, resources: list[Resource], account_id: str) -> dict[str, float | None]:
|
|
137
|
+
"""Enrich resources with commitment coverage/utilization from Cost Explorer.
|
|
138
|
+
|
|
139
|
+
Returns the commitment metrics dict (S081) for account-level aggregation.
|
|
140
|
+
"""
|
|
134
141
|
from .aws.commitments import CommitmentsCollector
|
|
135
142
|
|
|
136
143
|
try:
|
|
@@ -149,8 +156,33 @@ def _enrich_commitments(session: boto3.Session, resources: list[Resource], accou
|
|
|
149
156
|
if shown:
|
|
150
157
|
print(f" 📊 Commitments: " + ", ".join(f"{k}={v:.1f}%" for k, v in shown.items()), file=sys.stderr)
|
|
151
158
|
|
|
159
|
+
return metrics
|
|
160
|
+
|
|
152
161
|
except Exception:
|
|
153
|
-
|
|
162
|
+
return {}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _build_account_resource(account_id: str, commit_metrics: dict[str, float | None], region: str) -> Resource:
|
|
166
|
+
"""Synthetic account-level resource (S083) carrying savings metrics for policies.
|
|
167
|
+
|
|
168
|
+
Policies scope to resource_types: [aws.account] to evaluate the account ONCE.
|
|
169
|
+
"""
|
|
170
|
+
metrics: dict[str, float] = {}
|
|
171
|
+
for k, v in commit_metrics.items():
|
|
172
|
+
if v is not None:
|
|
173
|
+
metrics[k] = v
|
|
174
|
+
potential = sum(f.estimated_savings or 0 for f in get_recommendations())
|
|
175
|
+
metrics["potential_savings_monthly"] = round(potential, 2)
|
|
176
|
+
|
|
177
|
+
return Resource(
|
|
178
|
+
resource_id=f"account-{account_id}",
|
|
179
|
+
resource_type="aws.account",
|
|
180
|
+
region=region,
|
|
181
|
+
account_id=account_id,
|
|
182
|
+
tags={},
|
|
183
|
+
properties={},
|
|
184
|
+
metrics=metrics,
|
|
185
|
+
)
|
|
154
186
|
|
|
155
187
|
|
|
156
188
|
def _enrich_recommendations(session: boto3.Session, account_id: str, target_regions: list[str]):
|
|
@@ -139,7 +139,11 @@ def _check_rule(resource: Resource, policy: Policy, context: ArchitectureContext
|
|
|
139
139
|
|
|
140
140
|
# Check additional conditions (AND logic)
|
|
141
141
|
for cond in rule.additional_conditions:
|
|
142
|
-
|
|
142
|
+
# Use explicit None checks — a valid 0.0 metric is falsy and must NOT
|
|
143
|
+
# fall through to properties (that dropped `== 0` conditions silently).
|
|
144
|
+
cond_value = resource.metrics.get(cond.field)
|
|
145
|
+
if cond_value is None:
|
|
146
|
+
cond_value = resource.properties.get(cond.field)
|
|
143
147
|
if cond_value is None:
|
|
144
148
|
return None # Missing data — skip
|
|
145
149
|
if not _apply_operator(float(cond_value), cond.operator, float(cond.value)):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sofe
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
4
4
|
Summary: Stairway Open FinOps Engine — FinOps Policies as Code for AWS
|
|
5
5
|
Author-email: Carlos Cortez <carlos@cortez.cloud>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -51,7 +51,7 @@ Dynamic: license-file
|
|
|
51
51
|
<img src="https://img.shields.io/pypi/v/sofe?style=flat-square&logo=pypi&color=8A2BE2" alt="PyPI">
|
|
52
52
|
<img src="https://img.shields.io/badge/python-3.10%2B-blue?style=flat-square&logo=python" alt="Python">
|
|
53
53
|
<img src="https://img.shields.io/badge/license-Apache_2.0-8A2BE2?style=flat-square" alt="License">
|
|
54
|
-
<img src="https://img.shields.io/badge/policies-
|
|
54
|
+
<img src="https://img.shields.io/badge/policies-39-success?style=flat-square" alt="39 policies">
|
|
55
55
|
<img src="https://img.shields.io/badge/collectors-19-blue?style=flat-square" alt="19 collectors">
|
|
56
56
|
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen?style=flat-square" alt="PRs">
|
|
57
57
|
</p>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Tests for S083 savings policies (evaluated against the synthetic account resource)."""
|
|
2
|
+
|
|
3
|
+
from sofe.loader import load_policies
|
|
4
|
+
from sofe.engine import evaluate
|
|
5
|
+
from sofe.models import Resource
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _account(metrics: dict[str, float]) -> Resource:
|
|
9
|
+
return Resource(
|
|
10
|
+
resource_id="account-123",
|
|
11
|
+
resource_type="aws.account",
|
|
12
|
+
region="us-east-1",
|
|
13
|
+
account_id="123",
|
|
14
|
+
tags={},
|
|
15
|
+
properties={},
|
|
16
|
+
metrics=metrics,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _savings_policies():
|
|
21
|
+
return [p for p in load_policies("policies/") if p.metadata.name in
|
|
22
|
+
("savings-plan-coverage-low", "ri-utilization-low", "sp-utilization-low")]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_low_coverage_fires():
|
|
26
|
+
policies = _savings_policies()
|
|
27
|
+
assert len(policies) == 3
|
|
28
|
+
findings = evaluate(policies, [_account({"sp_coverage_pct": 60.0, "ri_utilization_pct": 95.0, "sp_utilization_pct": 80.0})])
|
|
29
|
+
names = {f.policy_name for f in findings}
|
|
30
|
+
assert "savings-plan-coverage-low" in names
|
|
31
|
+
assert "ri-utilization-low" not in names # ri=95 ok
|
|
32
|
+
assert "sp-utilization-low" not in names # sp=80 ok
|
|
33
|
+
print("✓ low coverage fires")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_low_ri_and_sp_utilization_fire():
|
|
37
|
+
policies = _savings_policies()
|
|
38
|
+
findings = evaluate(policies, [_account({"sp_coverage_pct": 95.0, "ri_utilization_pct": 50.0, "sp_utilization_pct": 40.0})])
|
|
39
|
+
names = {f.policy_name for f in findings}
|
|
40
|
+
assert "ri-utilization-low" in names
|
|
41
|
+
assert "sp-utilization-low" in names
|
|
42
|
+
assert "savings-plan-coverage-low" not in names
|
|
43
|
+
print("✓ low ri/sp utilization fire")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_healthy_account_no_findings():
|
|
47
|
+
policies = _savings_policies()
|
|
48
|
+
findings = evaluate(policies, [_account({"sp_coverage_pct": 95.0, "ri_utilization_pct": 90.0, "sp_utilization_pct": 85.0})])
|
|
49
|
+
assert findings == []
|
|
50
|
+
print("✓ healthy account no findings")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_missing_metrics_skip():
|
|
54
|
+
policies = _savings_policies()
|
|
55
|
+
findings = evaluate(policies, [_account({})]) # no sp/ri metrics -> skip
|
|
56
|
+
assert findings == []
|
|
57
|
+
print("✓ missing metrics skip")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
test_low_coverage_fires()
|
|
62
|
+
test_low_ri_and_sp_utilization_fire()
|
|
63
|
+
test_healthy_account_no_findings()
|
|
64
|
+
test_missing_metrics_skip()
|
|
65
|
+
print("All S083 policy tests passed.")
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|