cloudcircuit 0.2.0__tar.gz → 0.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudcircuit
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: CloudCircuit Python package.
5
5
  Author: CloudCircuit Contributors
6
6
  License: MIT
@@ -68,6 +68,7 @@ python -m pip install cloudcircuit
68
68
 
69
69
  ```python
70
70
  from cloudcircuit import (
71
+ check_anomaly_robust,
71
72
  check_anomaly_spike,
72
73
  check_budget,
73
74
  check_burn_rate,
@@ -79,6 +80,7 @@ from cloudcircuit import (
79
80
 
80
81
  budget = check_budget(current_spend=920.0, budget_limit=1000.0, warning_ratio=0.9)
81
82
  anomaly = check_anomaly_spike([120.0, 118.0, 121.0, 250.0], spike_multiplier=1.6)
83
+ robust_anomaly = check_anomaly_robust([120.0, 118.0, 121.0, 250.0], method="mad", z_threshold=3.5)
82
84
  burn = check_burn_rate([120.0, 118.0, 121.0, 250.0], hot_multiplier=1.4)
83
85
  breaker = evaluate_circuit_breaker(consecutive_failures=0, failure_threshold=3)
84
86
  forecast = forecast_budget_breach(
@@ -94,9 +96,25 @@ alert = make_alert_payload(policy, service="billing-worker", environment="prod")
94
96
  print(policy.action, policy.severity)
95
97
  print(forecast.projected_total_spend, forecast.will_breach)
96
98
  print(burn.burn_rate_ratio, burn.is_hot)
99
+ print(robust_anomaly.threshold, robust_anomaly.is_spike)
97
100
  print(alert)
98
101
  ```
99
102
 
103
+ ## Robust anomaly detection (MAD / percentile)
104
+
105
+ Mean-based thresholds can be brittle when cloud spend is heavy-tailed or bursty. For more robust
106
+ detection, use `check_anomaly_robust`:
107
+
108
+ ```python
109
+ from cloudcircuit import check_anomaly_robust
110
+
111
+ mad = check_anomaly_robust([120.0, 118.0, 121.0, 250.0], method="mad", z_threshold=3.5)
112
+ p95 = check_anomaly_robust([120.0, 118.0, 121.0, 250.0], method="percentile", percentile=0.95)
113
+
114
+ print(mad.threshold, mad.is_spike)
115
+ print(p95.threshold, p95.is_spike)
116
+ ```
117
+
100
118
  ## Common developer problems solved
101
119
 
102
120
  1. **Runaway cron or queue workers**
@@ -117,6 +135,7 @@ print(alert)
117
135
 
118
136
  - `check_budget(...) -> BudgetCheckResult`
119
137
  - `check_anomaly_spike(...) -> AnomalyCheckResult`
138
+ - `check_anomaly_robust(...) -> AnomalyCheckResult`
120
139
  - `check_burn_rate(...) -> BurnRateResult`
121
140
  - `forecast_budget_breach(...) -> ForecastResult`
122
141
  - `evaluate_circuit_breaker(...) -> CircuitBreakerDecision`
@@ -43,6 +43,7 @@ python -m pip install cloudcircuit
43
43
 
44
44
  ```python
45
45
  from cloudcircuit import (
46
+ check_anomaly_robust,
46
47
  check_anomaly_spike,
47
48
  check_budget,
48
49
  check_burn_rate,
@@ -54,6 +55,7 @@ from cloudcircuit import (
54
55
 
55
56
  budget = check_budget(current_spend=920.0, budget_limit=1000.0, warning_ratio=0.9)
56
57
  anomaly = check_anomaly_spike([120.0, 118.0, 121.0, 250.0], spike_multiplier=1.6)
58
+ robust_anomaly = check_anomaly_robust([120.0, 118.0, 121.0, 250.0], method="mad", z_threshold=3.5)
57
59
  burn = check_burn_rate([120.0, 118.0, 121.0, 250.0], hot_multiplier=1.4)
58
60
  breaker = evaluate_circuit_breaker(consecutive_failures=0, failure_threshold=3)
59
61
  forecast = forecast_budget_breach(
@@ -69,9 +71,25 @@ alert = make_alert_payload(policy, service="billing-worker", environment="prod")
69
71
  print(policy.action, policy.severity)
70
72
  print(forecast.projected_total_spend, forecast.will_breach)
71
73
  print(burn.burn_rate_ratio, burn.is_hot)
74
+ print(robust_anomaly.threshold, robust_anomaly.is_spike)
72
75
  print(alert)
73
76
  ```
74
77
 
78
+ ## Robust anomaly detection (MAD / percentile)
79
+
80
+ Mean-based thresholds can be brittle when cloud spend is heavy-tailed or bursty. For more robust
81
+ detection, use `check_anomaly_robust`:
82
+
83
+ ```python
84
+ from cloudcircuit import check_anomaly_robust
85
+
86
+ mad = check_anomaly_robust([120.0, 118.0, 121.0, 250.0], method="mad", z_threshold=3.5)
87
+ p95 = check_anomaly_robust([120.0, 118.0, 121.0, 250.0], method="percentile", percentile=0.95)
88
+
89
+ print(mad.threshold, mad.is_spike)
90
+ print(p95.threshold, p95.is_spike)
91
+ ```
92
+
75
93
  ## Common developer problems solved
76
94
 
77
95
  1. **Runaway cron or queue workers**
@@ -92,6 +110,7 @@ print(alert)
92
110
 
93
111
  - `check_budget(...) -> BudgetCheckResult`
94
112
  - `check_anomaly_spike(...) -> AnomalyCheckResult`
113
+ - `check_anomaly_robust(...) -> AnomalyCheckResult`
95
114
  - `check_burn_rate(...) -> BurnRateResult`
96
115
  - `forecast_budget_breach(...) -> ForecastResult`
97
116
  - `evaluate_circuit_breaker(...) -> CircuitBreakerDecision`
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cloudcircuit"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "CloudCircuit Python package."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -7,6 +7,7 @@ from cloudcircuit.safeguards import (
7
7
  CircuitBreakerDecision,
8
8
  ForecastResult,
9
9
  PolicyDecision,
10
+ check_anomaly_robust,
10
11
  check_anomaly_spike,
11
12
  check_budget,
12
13
  check_burn_rate,
@@ -16,7 +17,7 @@ from cloudcircuit.safeguards import (
16
17
  make_alert_payload,
17
18
  )
18
19
 
19
- __version__ = "0.2.0"
20
+ __version__ = "0.3.0"
20
21
  __all__ = [
21
22
  "__version__",
22
23
  "AnomalyCheckResult",
@@ -25,6 +26,7 @@ __all__ = [
25
26
  "CircuitBreakerDecision",
26
27
  "ForecastResult",
27
28
  "PolicyDecision",
29
+ "check_anomaly_robust",
28
30
  "check_anomaly_spike",
29
31
  "check_budget",
30
32
  "check_burn_rate",
@@ -2,8 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from collections.abc import Sequence
5
6
  from dataclasses import dataclass
6
- from typing import Literal, Sequence
7
+ from math import ceil, floor
8
+ from typing import Literal
7
9
 
8
10
 
9
11
  @dataclass(frozen=True, slots=True)
@@ -123,6 +125,91 @@ def check_anomaly_spike(
123
125
  )
124
126
 
125
127
 
128
+ def _median(values: Sequence[float]) -> float:
129
+ if not values:
130
+ raise ValueError("values must be non-empty")
131
+ sorted_values = sorted(float(value) for value in values)
132
+ mid = len(sorted_values) // 2
133
+ if len(sorted_values) % 2 == 1:
134
+ return sorted_values[mid]
135
+ return (sorted_values[mid - 1] + sorted_values[mid]) / 2.0
136
+
137
+
138
+ def _quantile(values: Sequence[float], p: float) -> float:
139
+ if not values:
140
+ raise ValueError("values must be non-empty")
141
+ if not (0.0 < p < 1.0):
142
+ raise ValueError("p must be in (0, 1)")
143
+ sorted_values = sorted(float(value) for value in values)
144
+ if len(sorted_values) == 1:
145
+ return sorted_values[0]
146
+ rank = p * (len(sorted_values) - 1)
147
+ lo = int(floor(rank))
148
+ hi = int(ceil(rank))
149
+ if lo == hi:
150
+ return sorted_values[lo]
151
+ weight = rank - lo
152
+ return sorted_values[lo] + weight * (sorted_values[hi] - sorted_values[lo])
153
+
154
+
155
+ def check_anomaly_robust(
156
+ spend_series: Sequence[float],
157
+ *,
158
+ method: Literal["mad", "percentile"] = "mad",
159
+ z_threshold: float = 3.5,
160
+ min_mad: float = 0.0,
161
+ percentile: float = 0.95,
162
+ min_baseline: float = 0.0,
163
+ ) -> AnomalyCheckResult:
164
+ """
165
+ Robust spend spike detection using MAD or percentile thresholding.
166
+
167
+ - MAD method uses median + z * (MAD / 0.6745) as a robust z-score threshold.
168
+ - Percentile method uses a historical percentile as the spike threshold.
169
+
170
+ Baseline is computed from all values except the latest.
171
+ """
172
+ if len(spend_series) < 2:
173
+ raise ValueError("spend_series must contain at least 2 points")
174
+ if any(value < 0 for value in spend_series):
175
+ raise ValueError("spend_series values must be >= 0")
176
+ if min_baseline < 0:
177
+ raise ValueError("min_baseline must be >= 0")
178
+
179
+ if method == "mad":
180
+ if z_threshold <= 0:
181
+ raise ValueError("z_threshold must be > 0")
182
+ if min_mad < 0:
183
+ raise ValueError("min_mad must be >= 0")
184
+ elif method == "percentile":
185
+ if not (0.0 < percentile < 1.0):
186
+ raise ValueError("percentile must be in (0, 1)")
187
+ else:
188
+ raise ValueError("method must be 'mad' or 'percentile'")
189
+
190
+ latest_spend = float(spend_series[-1])
191
+ history = spend_series[:-1]
192
+
193
+ if method == "mad":
194
+ baseline_location = _median(history)
195
+ abs_deviations = [abs(float(value) - baseline_location) for value in history]
196
+ mad = max(_median(abs_deviations), float(min_mad))
197
+ effective_location = max(baseline_location, float(min_baseline))
198
+ robust_sigma = mad / 0.6745 if mad > 0 else 0.0
199
+ threshold = effective_location + (float(z_threshold) * robust_sigma)
200
+ else:
201
+ baseline_location = _quantile(history, float(percentile))
202
+ threshold = max(baseline_location, float(min_baseline))
203
+
204
+ is_spike = latest_spend > threshold
205
+ return AnomalyCheckResult(
206
+ latest_spend=latest_spend,
207
+ baseline_mean=float(baseline_location),
208
+ threshold=float(threshold),
209
+ is_spike=is_spike,
210
+ )
211
+
212
+
126
213
  def evaluate_circuit_breaker(
127
214
  consecutive_failures: int,
128
215
  failure_threshold: int = 3,
@@ -3,7 +3,6 @@ from __future__ import annotations
3
3
  import sys
4
4
  from pathlib import Path
5
5
 
6
-
7
6
  ROOT = Path(__file__).resolve().parents[1]
8
7
  SRC = ROOT / "src"
9
8
  sys.path.insert(0, str(SRC))
@@ -1,6 +1,7 @@
1
1
  import pytest
2
2
 
3
3
  from cloudcircuit.safeguards import (
4
+ check_anomaly_robust,
4
5
  check_anomaly_spike,
5
6
  check_budget,
6
7
  check_burn_rate,
@@ -60,6 +61,81 @@ def test_anomaly_invalid_inputs() -> None:
60
61
  check_anomaly_spike([1.0, -1.0], spike_multiplier=2.0)
61
62
 
62
63
 
64
+ def test_anomaly_robust_mad_detects_spike_with_zero_mad() -> None:
65
+ result = check_anomaly_robust([10.0, 10.0, 10.0, 25.0], method="mad", z_threshold=3.5)
66
+ assert result.baseline_mean == 10.0
67
+ assert result.threshold == 10.0
68
+ assert result.is_spike is True
69
+
70
+
71
+ def test_anomaly_robust_mad_not_spike_on_stable_series() -> None:
72
+ result = check_anomaly_robust([10.0, 10.0, 10.0, 10.0], method="mad")
73
+ assert result.baseline_mean == 10.0
74
+ assert result.threshold == 10.0
75
+ assert result.is_spike is False
76
+
77
+
78
+ def test_anomaly_robust_mad_detects_spike_with_nonzero_mad() -> None:
79
+ result = check_anomaly_robust([10.0, 10.0, 11.0, 10.0, 12.0, 11.0, 50.0], method="mad")
80
+ assert result.baseline_mean == 10.5
81
+ assert 0.0 < result.threshold < 50.0
82
+ assert result.is_spike is True
83
+
84
+
85
+ def test_anomaly_robust_percentile_detects_spike() -> None:
86
+ result = check_anomaly_robust(
87
+ [10.0, 10.0, 10.0, 10.0, 50.0],
88
+ method="percentile",
89
+ percentile=0.95,
90
+ )
91
+ assert result.baseline_mean == 10.0
92
+ assert result.threshold == 10.0
93
+ assert result.is_spike is True
94
+
95
+
96
+ def test_anomaly_robust_percentile_not_spike() -> None:
97
+ result = check_anomaly_robust(
98
+ [10.0, 12.0, 11.0, 13.0, 12.8],
99
+ method="percentile",
100
+ percentile=0.95,
101
+ )
102
+ assert result.is_spike is False
103
+
104
+
105
+ def test_anomaly_robust_invalid_inputs() -> None:
106
+ with pytest.raises(ValueError):
107
+ check_anomaly_robust([1.0], method="mad")
108
+ with pytest.raises(ValueError):
109
+ check_anomaly_robust([1.0, -1.0], method="mad")
110
+ with pytest.raises(ValueError):
111
+ check_anomaly_robust([1.0, 2.0], method="unknown") # type: ignore[arg-type]
112
+
113
+ with pytest.raises(ValueError):
114
+ check_anomaly_robust([1.0, 2.0], method="mad", z_threshold=0.0)
115
+ with pytest.raises(ValueError):
116
+ check_anomaly_robust([1.0, 2.0], method="mad", min_mad=-0.1)
117
+ with pytest.raises(ValueError):
118
+ check_anomaly_robust([1.0, 2.0], method="mad", min_baseline=-0.1)
119
+
120
+ with pytest.raises(ValueError):
121
+ check_anomaly_robust([1.0, 2.0], method="percentile", percentile=1.0)
122
+
123
+
124
+ def test_anomaly_robust_min_baseline_floor() -> None:
125
+ mad = check_anomaly_robust([0.0, 0.0, 0.0, 0.4], method="mad", min_baseline=0.5)
126
+ assert mad.threshold == 0.5
127
+ assert mad.is_spike is False
128
+
129
+ pct = check_anomaly_robust(
130
+ [0.0, 0.0, 0.0, 0.4],
131
+ method="percentile",
132
+ percentile=0.95,
133
+ min_baseline=0.5,
134
+ )
135
+ assert pct.threshold == 0.5
136
+ assert pct.is_spike is False
137
+
138
+
63
139
  def test_circuit_breaker_allows_operation_below_threshold() -> None:
64
140
  decision = evaluate_circuit_breaker(consecutive_failures=2, failure_threshold=3)
65
141
  assert decision.is_open is False
File without changes