cloudcircuit 0.2.0__tar.gz → 0.3.7__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.7
4
4
  Summary: CloudCircuit Python package.
5
5
  Author: CloudCircuit Contributors
6
6
  License: MIT
@@ -23,6 +23,8 @@ Requires-Dist: ruff>=0.6.9; extra == 'dev'
23
23
  Requires-Dist: twine>=5.1.1; extra == 'dev'
24
24
  Description-Content-Type: text/markdown
25
25
 
26
+ ![cloudcircuit Banner](assets/images/banner.svg)
27
+
26
28
  # cloudcircuit
27
29
 
28
30
  CloudCircuit is a Python library for **cloud cost control**, **real-time spend anomaly detection**, **budget overrun prevention**, and **circuit-breaker style safety automation**.
@@ -68,6 +70,7 @@ python -m pip install cloudcircuit
68
70
 
69
71
  ```python
70
72
  from cloudcircuit import (
73
+ check_anomaly_robust,
71
74
  check_anomaly_spike,
72
75
  check_budget,
73
76
  check_burn_rate,
@@ -79,6 +82,7 @@ from cloudcircuit import (
79
82
 
80
83
  budget = check_budget(current_spend=920.0, budget_limit=1000.0, warning_ratio=0.9)
81
84
  anomaly = check_anomaly_spike([120.0, 118.0, 121.0, 250.0], spike_multiplier=1.6)
85
+ robust_anomaly = check_anomaly_robust([120.0, 118.0, 121.0, 250.0], method="mad", z_threshold=3.5)
82
86
  burn = check_burn_rate([120.0, 118.0, 121.0, 250.0], hot_multiplier=1.4)
83
87
  breaker = evaluate_circuit_breaker(consecutive_failures=0, failure_threshold=3)
84
88
  forecast = forecast_budget_breach(
@@ -94,9 +98,25 @@ alert = make_alert_payload(policy, service="billing-worker", environment="prod")
94
98
  print(policy.action, policy.severity)
95
99
  print(forecast.projected_total_spend, forecast.will_breach)
96
100
  print(burn.burn_rate_ratio, burn.is_hot)
101
+ print(robust_anomaly.threshold, robust_anomaly.is_spike)
97
102
  print(alert)
98
103
  ```
99
104
 
105
+ ## Robust anomaly detection (MAD / percentile)
106
+
107
+ Mean-based thresholds can be brittle when cloud spend is heavy-tailed or bursty. For more robust
108
+ detection, use `check_anomaly_robust`:
109
+
110
+ ```python
111
+ from cloudcircuit import check_anomaly_robust
112
+
113
+ mad = check_anomaly_robust([120.0, 118.0, 121.0, 250.0], method="mad", z_threshold=3.5)
114
+ p95 = check_anomaly_robust([120.0, 118.0, 121.0, 250.0], method="percentile", percentile=0.95)
115
+
116
+ print(mad.threshold, mad.is_spike)
117
+ print(p95.threshold, p95.is_spike)
118
+ ```
119
+
100
120
  ## Common developer problems solved
101
121
 
102
122
  1. **Runaway cron or queue workers**
@@ -117,6 +137,7 @@ print(alert)
117
137
 
118
138
  - `check_budget(...) -> BudgetCheckResult`
119
139
  - `check_anomaly_spike(...) -> AnomalyCheckResult`
140
+ - `check_anomaly_robust(...) -> AnomalyCheckResult`
120
141
  - `check_burn_rate(...) -> BurnRateResult`
121
142
  - `forecast_budget_breach(...) -> ForecastResult`
122
143
  - `evaluate_circuit_breaker(...) -> CircuitBreakerDecision`
@@ -1,124 +1,145 @@
1
- # cloudcircuit
2
-
3
- CloudCircuit is a Python library for **cloud cost control**, **real-time spend anomaly detection**, **budget overrun prevention**, and **circuit-breaker style safety automation**.
4
-
5
- If you are searching for solutions like:
6
- - prevent unexpected cloud bills
7
- - detect cloud spend spikes
8
- - add budget guardrails in Python
9
- - implement FinOps automation for developers
10
- - stop runaway infrastructure loops
11
-
12
- this package is built for that exact problem set.
13
-
14
- ## Why this library exists
15
-
16
- Many teams discover cloud overspend only after invoices land. A single misconfigured loop, unbounded worker fan-out, or retry storm can create massive costs in hours.
17
-
18
- CloudCircuit provides deterministic primitives so you can add spend safety in:
19
- - background jobs
20
- - CI/CD workflows
21
- - internal developer platforms
22
- - API middleware
23
- - scheduled FinOps monitors
24
-
25
- ## Features
26
-
27
- - **Budget guardrails**: warning and hard-breach checks with explicit thresholds
28
- - **Spend anomaly detection**: latest-point spike detection against baseline
29
- - **Burn-rate monitoring**: detect hot spend acceleration before full breach
30
- - **Forecasting**: project period-end spend and overrun amount
31
- - **Circuit breaker decisions**: open/close logic for high-risk operations
32
- - **Policy engine**: combine budget + anomaly + breaker into `allow/throttle/block`
33
- - **Alert payload builder**: provider-agnostic payloads for Slack/webhooks/ops pipelines
34
- - **Typed API**: dataclass results with clear fields for logs and audits
35
-
36
- ## Install
37
-
38
- ```bash
39
- python -m pip install cloudcircuit
40
- ```
41
-
42
- ## Quickstart
43
-
44
- ```python
45
- from cloudcircuit import (
46
- check_anomaly_spike,
47
- check_budget,
48
- check_burn_rate,
49
- evaluate_circuit_breaker,
50
- evaluate_spend_policy,
51
- forecast_budget_breach,
52
- make_alert_payload,
53
- )
54
-
55
- budget = check_budget(current_spend=920.0, budget_limit=1000.0, warning_ratio=0.9)
56
- anomaly = check_anomaly_spike([120.0, 118.0, 121.0, 250.0], spike_multiplier=1.6)
57
- burn = check_burn_rate([120.0, 118.0, 121.0, 250.0], hot_multiplier=1.4)
58
- breaker = evaluate_circuit_breaker(consecutive_failures=0, failure_threshold=3)
59
- forecast = forecast_budget_breach(
60
- current_spend=920.0,
61
- budget_limit=1000.0,
62
- periods_elapsed=22,
63
- total_periods=30,
64
- )
65
-
66
- policy = evaluate_spend_policy(budget=budget, anomaly=anomaly, breaker=breaker)
67
- alert = make_alert_payload(policy, service="billing-worker", environment="prod")
68
-
69
- print(policy.action, policy.severity)
70
- print(forecast.projected_total_spend, forecast.will_breach)
71
- print(burn.burn_rate_ratio, burn.is_hot)
72
- print(alert)
73
- ```
74
-
75
- ## Common developer problems solved
76
-
77
- 1. **Runaway cron or queue workers**
78
- - Use anomaly + burn-rate checks every interval.
79
- - Auto-throttle workers when policy returns `throttle`.
80
-
81
- 2. **Unexpected month-end budget breach**
82
- - Use forecast checks daily or hourly.
83
- - Trigger remediation before hard limit is hit.
84
-
85
- 3. **No standardized cost incident signal**
86
- - Use `make_alert_payload` for consistent, structured incident events.
87
-
88
- 4. **Need a safe default during telemetry failure**
89
- - Combine breaker logic with fail-closed policy in critical paths.
90
-
91
- ## API overview
92
-
93
- - `check_budget(...) -> BudgetCheckResult`
94
- - `check_anomaly_spike(...) -> AnomalyCheckResult`
95
- - `check_burn_rate(...) -> BurnRateResult`
96
- - `forecast_budget_breach(...) -> ForecastResult`
97
- - `evaluate_circuit_breaker(...) -> CircuitBreakerDecision`
98
- - `evaluate_spend_policy(...) -> PolicyDecision`
99
- - `make_alert_payload(...) -> dict[str, object]`
100
-
101
- ## SEO keywords (for discoverability)
102
-
103
- cloud cost control python, finops python library, cloud budget guardrails, spend anomaly detection, prevent cloud bill shock, cost circuit breaker, cloud billing safety, cloud overspend monitoring, infrastructure cost alerts, developer finops toolkit
104
-
105
- ## Development
106
-
107
- ```bash
108
- python -m pip install -e ".[dev]"
109
- python -m pytest
110
- python -m ruff check .
111
- python -m mypy src
112
- ```
113
-
114
- ## Roadmap ideas
115
-
116
- - rolling window and percentile-based anomaly models
117
- - provider adapters for AWS/GCP/Azure billing streams
118
- - async event pipeline helpers
119
- - OpenTelemetry metric exporters
120
- - policy DSL for org-wide spend controls
121
-
122
- ## License
123
-
1
+ ![cloudcircuit Banner](assets/images/banner.svg)
2
+
3
+ # cloudcircuit
4
+
5
+ CloudCircuit is a Python library for **cloud cost control**, **real-time spend anomaly detection**, **budget overrun prevention**, and **circuit-breaker style safety automation**.
6
+
7
+ If you are searching for solutions like:
8
+ - prevent unexpected cloud bills
9
+ - detect cloud spend spikes
10
+ - add budget guardrails in Python
11
+ - implement FinOps automation for developers
12
+ - stop runaway infrastructure loops
13
+
14
+ this package is built for that exact problem set.
15
+
16
+ ## Why this library exists
17
+
18
+ Many teams discover cloud overspend only after invoices land. A single misconfigured loop, unbounded worker fan-out, or retry storm can create massive costs in hours.
19
+
20
+ CloudCircuit provides deterministic primitives so you can add spend safety in:
21
+ - background jobs
22
+ - CI/CD workflows
23
+ - internal developer platforms
24
+ - API middleware
25
+ - scheduled FinOps monitors
26
+
27
+ ## Features
28
+
29
+ - **Budget guardrails**: warning and hard-breach checks with explicit thresholds
30
+ - **Spend anomaly detection**: latest-point spike detection against baseline
31
+ - **Burn-rate monitoring**: detect hot spend acceleration before full breach
32
+ - **Forecasting**: project period-end spend and overrun amount
33
+ - **Circuit breaker decisions**: open/close logic for high-risk operations
34
+ - **Policy engine**: combine budget + anomaly + breaker into `allow/throttle/block`
35
+ - **Alert payload builder**: provider-agnostic payloads for Slack/webhooks/ops pipelines
36
+ - **Typed API**: dataclass results with clear fields for logs and audits
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ python -m pip install cloudcircuit
42
+ ```
43
+
44
+ ## Quickstart
45
+
46
+ ```python
47
+ from cloudcircuit import (
48
+ check_anomaly_robust,
49
+ check_anomaly_spike,
50
+ check_budget,
51
+ check_burn_rate,
52
+ evaluate_circuit_breaker,
53
+ evaluate_spend_policy,
54
+ forecast_budget_breach,
55
+ make_alert_payload,
56
+ )
57
+
58
+ budget = check_budget(current_spend=920.0, budget_limit=1000.0, warning_ratio=0.9)
59
+ anomaly = check_anomaly_spike([120.0, 118.0, 121.0, 250.0], spike_multiplier=1.6)
60
+ robust_anomaly = check_anomaly_robust([120.0, 118.0, 121.0, 250.0], method="mad", z_threshold=3.5)
61
+ burn = check_burn_rate([120.0, 118.0, 121.0, 250.0], hot_multiplier=1.4)
62
+ breaker = evaluate_circuit_breaker(consecutive_failures=0, failure_threshold=3)
63
+ forecast = forecast_budget_breach(
64
+ current_spend=920.0,
65
+ budget_limit=1000.0,
66
+ periods_elapsed=22,
67
+ total_periods=30,
68
+ )
69
+
70
+ policy = evaluate_spend_policy(budget=budget, anomaly=anomaly, breaker=breaker)
71
+ alert = make_alert_payload(policy, service="billing-worker", environment="prod")
72
+
73
+ print(policy.action, policy.severity)
74
+ print(forecast.projected_total_spend, forecast.will_breach)
75
+ print(burn.burn_rate_ratio, burn.is_hot)
76
+ print(robust_anomaly.threshold, robust_anomaly.is_spike)
77
+ print(alert)
78
+ ```
79
+
80
+ ## Robust anomaly detection (MAD / percentile)
81
+
82
+ Mean-based thresholds can be brittle when cloud spend is heavy-tailed or bursty. For more robust
83
+ detection, use `check_anomaly_robust`:
84
+
85
+ ```python
86
+ from cloudcircuit import check_anomaly_robust
87
+
88
+ mad = check_anomaly_robust([120.0, 118.0, 121.0, 250.0], method="mad", z_threshold=3.5)
89
+ p95 = check_anomaly_robust([120.0, 118.0, 121.0, 250.0], method="percentile", percentile=0.95)
90
+
91
+ print(mad.threshold, mad.is_spike)
92
+ print(p95.threshold, p95.is_spike)
93
+ ```
94
+
95
+ ## Common developer problems solved
96
+
97
+ 1. **Runaway cron or queue workers**
98
+ - Use anomaly + burn-rate checks every interval.
99
+ - Auto-throttle workers when policy returns `throttle`.
100
+
101
+ 2. **Unexpected month-end budget breach**
102
+ - Use forecast checks daily or hourly.
103
+ - Trigger remediation before hard limit is hit.
104
+
105
+ 3. **No standardized cost incident signal**
106
+ - Use `make_alert_payload` for consistent, structured incident events.
107
+
108
+ 4. **Need a safe default during telemetry failure**
109
+ - Combine breaker logic with fail-closed policy in critical paths.
110
+
111
+ ## API overview
112
+
113
+ - `check_budget(...) -> BudgetCheckResult`
114
+ - `check_anomaly_spike(...) -> AnomalyCheckResult`
115
+ - `check_anomaly_robust(...) -> AnomalyCheckResult`
116
+ - `check_burn_rate(...) -> BurnRateResult`
117
+ - `forecast_budget_breach(...) -> ForecastResult`
118
+ - `evaluate_circuit_breaker(...) -> CircuitBreakerDecision`
119
+ - `evaluate_spend_policy(...) -> PolicyDecision`
120
+ - `make_alert_payload(...) -> dict[str, object]`
121
+
122
+ ## SEO keywords (for discoverability)
123
+
124
+ cloud cost control python, finops python library, cloud budget guardrails, spend anomaly detection, prevent cloud bill shock, cost circuit breaker, cloud billing safety, cloud overspend monitoring, infrastructure cost alerts, developer finops toolkit
125
+
126
+ ## Development
127
+
128
+ ```bash
129
+ python -m pip install -e ".[dev]"
130
+ python -m pytest
131
+ python -m ruff check .
132
+ python -m mypy src
133
+ ```
134
+
135
+ ## Roadmap ideas
136
+
137
+ - rolling window and percentile-based anomaly models
138
+ - provider adapters for AWS/GCP/Azure billing streams
139
+ - async event pipeline helpers
140
+ - OpenTelemetry metric exporters
141
+ - policy DSL for org-wide spend controls
142
+
143
+ ## License
144
+
124
145
  MIT
@@ -0,0 +1,7 @@
1
+ <svg width="800" height="200" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="800" height="200" fill="#06b6d4" rx="10"/>
3
+ <text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle"
4
+ font-family="Arial, sans-serif" font-size="24" fill="white" font-weight="bold">
5
+ Cloud Infrastructure
6
+ </text>
7
+ </svg>
@@ -0,0 +1,13 @@
1
+ <svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="200" height="200" fill="#f8f9fa" stroke="#dee2e6" stroke-width="2"/>
3
+ <text x="50%" y="30%" dominant-baseline="middle" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" fill="#495057">
4
+ Cloud Circuit
5
+ </text>
6
+ <text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#6c757d">
7
+ Cloud Infrastructure
8
+ </text>
9
+ <circle cx="100" cy="130" r="25" fill="#007bff" opacity="0.2"/>
10
+ <text x="50%" y="135%" dominant-baseline="middle" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#007bff">
11
+ Deployment
12
+ </text>
13
+ </svg>
@@ -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.7"
8
8
  description = "CloudCircuit Python package."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -74,3 +74,4 @@ pretty = true
74
74
 
75
75
  [tool.twine]
76
76
  repository = "pypi"
77
+
@@ -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