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.
Files changed (48) hide show
  1. {sofe-0.4.2/sofe.egg-info → sofe-0.4.4}/PKG-INFO +2 -2
  2. {sofe-0.4.2 → sofe-0.4.4}/README.md +1 -1
  3. {sofe-0.4.2 → sofe-0.4.4}/pyproject.toml +1 -1
  4. {sofe-0.4.2 → sofe-0.4.4}/sofe/__init__.py +1 -1
  5. {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/__init__.py +36 -4
  6. {sofe-0.4.2 → sofe-0.4.4}/sofe/engine/__init__.py +5 -1
  7. {sofe-0.4.2 → sofe-0.4.4/sofe.egg-info}/PKG-INFO +2 -2
  8. {sofe-0.4.2 → sofe-0.4.4}/sofe.egg-info/SOURCES.txt +1 -0
  9. sofe-0.4.4/tests/test_savings_policies.py +65 -0
  10. {sofe-0.4.2 → sofe-0.4.4}/LICENSE +0 -0
  11. {sofe-0.4.2 → sofe-0.4.4}/setup.cfg +0 -0
  12. {sofe-0.4.2 → sofe-0.4.4}/sofe/cli.py +0 -0
  13. {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/__init__.py +0 -0
  14. {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/apigateway.py +0 -0
  15. {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/base.py +0 -0
  16. {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/cloudfront.py +0 -0
  17. {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/commitments.py +0 -0
  18. {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/cost.py +0 -0
  19. {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/dynamodb.py +0 -0
  20. {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/ebs.py +0 -0
  21. {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/ec2.py +0 -0
  22. {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/ecs.py +0 -0
  23. {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/eks.py +0 -0
  24. {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/elasticache.py +0 -0
  25. {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/elb.py +0 -0
  26. {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/lambda_.py +0 -0
  27. {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/natgateway.py +0 -0
  28. {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/rds.py +0 -0
  29. {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/recommendations.py +0 -0
  30. {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/redshift.py +0 -0
  31. {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/route53.py +0 -0
  32. {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/s3.py +0 -0
  33. {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/sagemaker.py +0 -0
  34. {sofe-0.4.2 → sofe-0.4.4}/sofe/collectors/aws/secretsmanager.py +0 -0
  35. {sofe-0.4.2 → sofe-0.4.4}/sofe/engine/architecture.py +0 -0
  36. {sofe-0.4.2 → sofe-0.4.4}/sofe/loader/__init__.py +0 -0
  37. {sofe-0.4.2 → sofe-0.4.4}/sofe/models/__init__.py +0 -0
  38. {sofe-0.4.2 → sofe-0.4.4}/sofe/remediation/__init__.py +0 -0
  39. {sofe-0.4.2 → sofe-0.4.4}/sofe/remediation/commands.py +0 -0
  40. {sofe-0.4.2 → sofe-0.4.4}/sofe.egg-info/dependency_links.txt +0 -0
  41. {sofe-0.4.2 → sofe-0.4.4}/sofe.egg-info/entry_points.txt +0 -0
  42. {sofe-0.4.2 → sofe-0.4.4}/sofe.egg-info/requires.txt +0 -0
  43. {sofe-0.4.2 → sofe-0.4.4}/sofe.egg-info/top_level.txt +0 -0
  44. {sofe-0.4.2 → sofe-0.4.4}/tests/test_architecture.py +0 -0
  45. {sofe-0.4.2 → sofe-0.4.4}/tests/test_cli_outputs.py +0 -0
  46. {sofe-0.4.2 → sofe-0.4.4}/tests/test_commitments.py +0 -0
  47. {sofe-0.4.2 → sofe-0.4.4}/tests/test_recommendations.py +0 -0
  48. {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.2
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-10-success?style=flat-square" alt="10 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-10-success?style=flat-square" alt="10 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,6 +1,6 @@
1
1
  [project]
2
2
  name = "sofe"
3
- version = "0.4.2"
3
+ version = "0.4.4"
4
4
  description = "Stairway Open FinOps Engine — FinOps Policies as Code for AWS"
5
5
  requires-python = ">=3.11"
6
6
  license = {text = "Apache-2.0"}
@@ -1,2 +1,2 @@
1
1
  """SOFE — Stairway Open FinOps Engine. Policies as Code for AWS."""
2
- __version__ = "0.1.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
- pass
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
- cond_value = resource.metrics.get(cond.field) or resource.properties.get(cond.field)
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.2
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-10-success?style=flat-square" alt="10 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>
@@ -42,4 +42,5 @@ tests/test_architecture.py
42
42
  tests/test_cli_outputs.py
43
43
  tests/test_commitments.py
44
44
  tests/test_recommendations.py
45
+ tests/test_savings_policies.py
45
46
  tests/test_validate.py
@@ -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