cloudcircuit 0.1.0__py3-none-any.whl
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.
- cloudcircuit/__init__.py +4 -0
- cloudcircuit/py.typed +0 -0
- cloudcircuit/safeguards.py +115 -0
- cloudcircuit-0.1.0.dist-info/METADATA +163 -0
- cloudcircuit-0.1.0.dist-info/RECORD +6 -0
- cloudcircuit-0.1.0.dist-info/WHEEL +4 -0
cloudcircuit/__init__.py
ADDED
cloudcircuit/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Core cloud spend safeguard logic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Sequence
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class BudgetCheckResult:
|
|
11
|
+
current_spend: float
|
|
12
|
+
budget_limit: float
|
|
13
|
+
usage_ratio: float
|
|
14
|
+
remaining_budget: float
|
|
15
|
+
is_warning: bool
|
|
16
|
+
is_breached: bool
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class AnomalyCheckResult:
|
|
21
|
+
latest_spend: float
|
|
22
|
+
baseline_mean: float
|
|
23
|
+
threshold: float
|
|
24
|
+
is_spike: bool
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class CircuitBreakerDecision:
|
|
29
|
+
is_open: bool
|
|
30
|
+
allow_operation: bool
|
|
31
|
+
retry_after_steps: int
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def check_budget(
|
|
35
|
+
current_spend: float,
|
|
36
|
+
budget_limit: float,
|
|
37
|
+
warning_ratio: float = 0.9,
|
|
38
|
+
) -> BudgetCheckResult:
|
|
39
|
+
"""Evaluate budget usage against warning and breach thresholds."""
|
|
40
|
+
if budget_limit <= 0:
|
|
41
|
+
raise ValueError("budget_limit must be > 0")
|
|
42
|
+
if not (0 < warning_ratio <= 1):
|
|
43
|
+
raise ValueError("warning_ratio must be in (0, 1]")
|
|
44
|
+
if current_spend < 0:
|
|
45
|
+
raise ValueError("current_spend must be >= 0")
|
|
46
|
+
|
|
47
|
+
usage_ratio = current_spend / budget_limit
|
|
48
|
+
remaining_budget = budget_limit - current_spend
|
|
49
|
+
is_warning = usage_ratio >= warning_ratio
|
|
50
|
+
is_breached = current_spend >= budget_limit
|
|
51
|
+
return BudgetCheckResult(
|
|
52
|
+
current_spend=current_spend,
|
|
53
|
+
budget_limit=budget_limit,
|
|
54
|
+
usage_ratio=usage_ratio,
|
|
55
|
+
remaining_budget=remaining_budget,
|
|
56
|
+
is_warning=is_warning,
|
|
57
|
+
is_breached=is_breached,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def check_anomaly_spike(
|
|
62
|
+
spend_series: Sequence[float],
|
|
63
|
+
spike_multiplier: float = 1.5,
|
|
64
|
+
min_baseline: float = 0.0,
|
|
65
|
+
) -> AnomalyCheckResult:
|
|
66
|
+
"""
|
|
67
|
+
Detect whether the latest spend point is an anomalous spike.
|
|
68
|
+
|
|
69
|
+
Baseline is computed from all values except the latest.
|
|
70
|
+
"""
|
|
71
|
+
if len(spend_series) < 2:
|
|
72
|
+
raise ValueError("spend_series must contain at least 2 points")
|
|
73
|
+
if spike_multiplier <= 1.0:
|
|
74
|
+
raise ValueError("spike_multiplier must be > 1.0")
|
|
75
|
+
if min_baseline < 0:
|
|
76
|
+
raise ValueError("min_baseline must be >= 0")
|
|
77
|
+
if any(value < 0 for value in spend_series):
|
|
78
|
+
raise ValueError("spend_series values must be >= 0")
|
|
79
|
+
|
|
80
|
+
latest_spend = float(spend_series[-1])
|
|
81
|
+
history = spend_series[:-1]
|
|
82
|
+
baseline_mean = sum(history) / len(history)
|
|
83
|
+
effective_baseline = max(baseline_mean, min_baseline)
|
|
84
|
+
threshold = effective_baseline * spike_multiplier
|
|
85
|
+
is_spike = latest_spend > threshold
|
|
86
|
+
return AnomalyCheckResult(
|
|
87
|
+
latest_spend=latest_spend,
|
|
88
|
+
baseline_mean=baseline_mean,
|
|
89
|
+
threshold=threshold,
|
|
90
|
+
is_spike=is_spike,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def evaluate_circuit_breaker(
|
|
95
|
+
consecutive_failures: int,
|
|
96
|
+
failure_threshold: int = 3,
|
|
97
|
+
cooldown_steps_remaining: int = 0,
|
|
98
|
+
) -> CircuitBreakerDecision:
|
|
99
|
+
"""Return deterministic breaker state from failure and cooldown counters."""
|
|
100
|
+
if consecutive_failures < 0:
|
|
101
|
+
raise ValueError("consecutive_failures must be >= 0")
|
|
102
|
+
if failure_threshold <= 0:
|
|
103
|
+
raise ValueError("failure_threshold must be > 0")
|
|
104
|
+
if cooldown_steps_remaining < 0:
|
|
105
|
+
raise ValueError("cooldown_steps_remaining must be >= 0")
|
|
106
|
+
|
|
107
|
+
threshold_reached = consecutive_failures >= failure_threshold
|
|
108
|
+
is_open = threshold_reached or cooldown_steps_remaining > 0
|
|
109
|
+
allow_operation = not is_open
|
|
110
|
+
retry_after_steps = cooldown_steps_remaining if is_open else 0
|
|
111
|
+
return CircuitBreakerDecision(
|
|
112
|
+
is_open=is_open,
|
|
113
|
+
allow_operation=allow_operation,
|
|
114
|
+
retry_after_steps=retry_after_steps,
|
|
115
|
+
)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cloudcircuit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CloudCircuit Python package.
|
|
5
|
+
Author: CloudCircuit Contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: build>=1.2.2; extra == 'dev'
|
|
20
|
+
Requires-Dist: mypy>=1.11.2; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest>=8.3.3; extra == 'dev'
|
|
22
|
+
Requires-Dist: ruff>=0.6.9; extra == 'dev'
|
|
23
|
+
Requires-Dist: twine>=5.1.1; extra == 'dev'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# cloudcircuit
|
|
27
|
+
|
|
28
|
+
Lightweight, deterministic safeguards you can embed into cloud spend enforcement:
|
|
29
|
+
|
|
30
|
+
- Budget usage checks (`check_budget`)
|
|
31
|
+
- Spend spike detection (`check_anomaly_spike`)
|
|
32
|
+
- Simple circuit-breaker decisioning (`evaluate_circuit_breaker`)
|
|
33
|
+
|
|
34
|
+
This package focuses on the core logic only. You bring your own metrics ingestion (billing exports, CloudWatch,
|
|
35
|
+
BigQuery, etc.) and enforcement (alerts, policy, throttling, kill-switches).
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
From PyPI (once published):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
python -m pip install cloudcircuit
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For local development from this repo:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
cd cloudcircuit
|
|
49
|
+
python -m pip install -e ".[dev]"
|
|
50
|
+
pytest
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
54
|
+
|
|
55
|
+
The public APIs currently live in `cloudcircuit.safeguards`:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from cloudcircuit.safeguards import (
|
|
59
|
+
check_anomaly_spike,
|
|
60
|
+
check_budget,
|
|
61
|
+
evaluate_circuit_breaker,
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
All functions are deterministic and raise `ValueError` on invalid inputs (negative spend, invalid thresholds, etc.).
|
|
66
|
+
|
|
67
|
+
## Quickstart
|
|
68
|
+
|
|
69
|
+
### 1) Budget guardrail
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from cloudcircuit.safeguards import check_budget
|
|
73
|
+
|
|
74
|
+
result = check_budget(current_spend=920.0, budget_limit=1000.0, warning_ratio=0.9)
|
|
75
|
+
|
|
76
|
+
if result.is_breached:
|
|
77
|
+
# Hard stop: disable non-essential workloads, block expensive operations, etc.
|
|
78
|
+
print("BUDGET BREACHED:", result)
|
|
79
|
+
elif result.is_warning:
|
|
80
|
+
# Soft stop: alert, reduce concurrency, apply stricter policy.
|
|
81
|
+
print("BUDGET WARNING:", result)
|
|
82
|
+
else:
|
|
83
|
+
print("OK:", result)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 2) Spike detection (latest point vs baseline)
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from cloudcircuit.safeguards import check_anomaly_spike
|
|
90
|
+
|
|
91
|
+
# Example: daily spend totals (or hourly), latest point last.
|
|
92
|
+
series = [120.0, 125.0, 118.0, 260.0]
|
|
93
|
+
|
|
94
|
+
result = check_anomaly_spike(series, spike_multiplier=1.5, min_baseline=0.0)
|
|
95
|
+
if result.is_spike:
|
|
96
|
+
print("SPEND SPIKE:", result.latest_spend, "threshold:", result.threshold)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 3) Circuit-breaker decisioning
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from cloudcircuit.safeguards import evaluate_circuit_breaker
|
|
103
|
+
|
|
104
|
+
decision = evaluate_circuit_breaker(
|
|
105
|
+
consecutive_failures=3,
|
|
106
|
+
failure_threshold=3,
|
|
107
|
+
cooldown_steps_remaining=0,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if not decision.allow_operation:
|
|
111
|
+
print("BREAKER OPEN; retry after steps:", decision.retry_after_steps)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Architecture
|
|
115
|
+
|
|
116
|
+
Current modules:
|
|
117
|
+
|
|
118
|
+
- `src/cloudcircuit/safeguards.py`: core dataclasses and guardrail functions:
|
|
119
|
+
- `BudgetCheckResult`, `check_budget`
|
|
120
|
+
- `AnomalyCheckResult`, `check_anomaly_spike`
|
|
121
|
+
- `CircuitBreakerDecision`, `evaluate_circuit_breaker`
|
|
122
|
+
- `tests/test_safeguards.py`: contract tests for edge cases and input validation.
|
|
123
|
+
|
|
124
|
+
Design goals:
|
|
125
|
+
|
|
126
|
+
- Deterministic, side-effect free logic (easy to unit-test and reason about)
|
|
127
|
+
- Minimal dependencies (safe to embed in CLIs, jobs, and services)
|
|
128
|
+
- Typed, explicit return objects (so callers can log/audit decisions)
|
|
129
|
+
|
|
130
|
+
Non-goals (by design):
|
|
131
|
+
|
|
132
|
+
- Cloud provider integrations (metrics ingestion / policy enforcement)
|
|
133
|
+
- Stateful breaker implementation (persistence, jitter/backoff, distributed locks)
|
|
134
|
+
|
|
135
|
+
## Deployment Guide
|
|
136
|
+
|
|
137
|
+
Typical deployment looks like a thin integration layer around `cloudcircuit`:
|
|
138
|
+
|
|
139
|
+
1. Collect spend and operational signals.
|
|
140
|
+
- Spend totals: billing exports, cost explorer, warehouse tables, or internal chargeback.
|
|
141
|
+
- Failure counters: request error rates, provider API failures, budget retrieval failures.
|
|
142
|
+
2. Evaluate guardrails.
|
|
143
|
+
- `check_budget()` for hard/soft budget thresholds.
|
|
144
|
+
- `check_anomaly_spike()` to catch sudden jumps early.
|
|
145
|
+
- `evaluate_circuit_breaker()` to gate high-cost operations when systems are unstable.
|
|
146
|
+
3. Enforce decisions.
|
|
147
|
+
- Alerting: Slack/email/PagerDuty with `*_Result` fields for audit context.
|
|
148
|
+
- Throttling: reduce concurrency, disable non-critical jobs, or switch to cheaper defaults.
|
|
149
|
+
- Kill-switch: block expensive actions when `is_breached` is true.
|
|
150
|
+
4. Persist state externally when needed.
|
|
151
|
+
- Store the last N spend points, consecutive failure counts, and cooldown counters in your DB/redis.
|
|
152
|
+
- Run the evaluator on a schedule (cron / Airflow / Cloud Scheduler) or inline per request.
|
|
153
|
+
|
|
154
|
+
Recommended patterns:
|
|
155
|
+
|
|
156
|
+
- Treat spend series as monotonic time buckets (hourly/daily) and pass the newest point last.
|
|
157
|
+
- Log the full result objects (they are small and serializable) so you can explain why a safeguard tripped.
|
|
158
|
+
- Prefer "fail closed" for high-risk operations: if your metrics pipeline is down, open the breaker or block
|
|
159
|
+
expensive actions until signals recover.
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT (see `pyproject.toml` for the current license declaration).
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
cloudcircuit/__init__.py,sha256=2n1PpRG-K5Gfjkwcx-OsRKS6hUQHzvtTaq6B0PV1yDc,76
|
|
2
|
+
cloudcircuit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
cloudcircuit/safeguards.py,sha256=0mkUbEH7Iry66OwDEUIW13rsppAcUDnBanW1OcwUUQw,3567
|
|
4
|
+
cloudcircuit-0.1.0.dist-info/METADATA,sha256=JGQN3YOM2foR4qshbsRCvQ7-jtOl0mxgDIcjgKBDse4,5400
|
|
5
|
+
cloudcircuit-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
6
|
+
cloudcircuit-0.1.0.dist-info/RECORD,,
|