cloudcircuit 0.1.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.
@@ -0,0 +1,55 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ concurrency:
13
+ group: ci-${{ github.workflow }}-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
16
+ jobs:
17
+ test:
18
+ runs-on: ubuntu-latest
19
+ timeout-minutes: 15
20
+ strategy:
21
+ fail-fast: false
22
+ matrix:
23
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
24
+
25
+ steps:
26
+ - name: Check out repository
27
+ uses: actions/checkout@v4
28
+ with:
29
+ persist-credentials: false
30
+
31
+ - name: Set up Python
32
+ uses: actions/setup-python@v5
33
+ with:
34
+ python-version: ${{ matrix.python-version }}
35
+ cache: pip
36
+
37
+ - name: Install dependencies
38
+ run: |
39
+ python -m pip install --upgrade pip
40
+ python -m pip install -e .[dev]
41
+
42
+ - name: Lint
43
+ run: python -m ruff check .
44
+
45
+ - name: Type check
46
+ run: python -m mypy src
47
+
48
+ - name: Run tests
49
+ run: python -m pytest
50
+
51
+ - name: Build distribution
52
+ run: python -m build
53
+
54
+ - name: Verify distribution metadata
55
+ run: python -m twine check dist/*
@@ -0,0 +1,91 @@
1
+ name: publish
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ concurrency:
12
+ group: publish-${{ github.ref }}
13
+ cancel-in-progress: false
14
+
15
+ jobs:
16
+ build:
17
+ runs-on: ubuntu-latest
18
+ timeout-minutes: 15
19
+ permissions:
20
+ contents: read
21
+ steps:
22
+ - name: Check out repository
23
+ uses: actions/checkout@v4
24
+ with:
25
+ persist-credentials: false
26
+
27
+ - name: Set up Python
28
+ uses: actions/setup-python@v5
29
+ with:
30
+ python-version: "3.11"
31
+ cache: pip
32
+
33
+ - name: Install build tooling
34
+ run: |
35
+ python -m pip install --upgrade pip
36
+ python -m pip install build twine
37
+
38
+ - name: Build distribution
39
+ run: python -m build
40
+
41
+ - name: Verify distribution metadata
42
+ run: python -m twine check dist/*
43
+
44
+ - name: Upload distribution artifacts
45
+ uses: actions/upload-artifact@v4
46
+ with:
47
+ name: python-package-distributions
48
+ path: dist/
49
+ if-no-files-found: error
50
+
51
+ publish-trusted:
52
+ if: ${{ secrets.PYPI_API_TOKEN == '' }}
53
+ needs: build
54
+ runs-on: ubuntu-latest
55
+ timeout-minutes: 15
56
+ environment:
57
+ name: pypi
58
+ url: https://pypi.org/p/cloudcircuit
59
+ permissions:
60
+ id-token: write
61
+ steps:
62
+ - name: Download distribution artifacts
63
+ uses: actions/download-artifact@v4
64
+ with:
65
+ name: python-package-distributions
66
+ path: dist/
67
+
68
+ - name: Publish to PyPI with trusted publishing
69
+ uses: pypa/gh-action-pypi-publish@release/v1
70
+
71
+ publish-token:
72
+ if: ${{ secrets.PYPI_API_TOKEN != '' }}
73
+ needs: build
74
+ runs-on: ubuntu-latest
75
+ timeout-minutes: 15
76
+ environment:
77
+ name: pypi
78
+ url: https://pypi.org/p/cloudcircuit
79
+ permissions:
80
+ contents: read
81
+ steps:
82
+ - name: Download distribution artifacts
83
+ uses: actions/download-artifact@v4
84
+ with:
85
+ name: python-package-distributions
86
+ path: dist/
87
+
88
+ - name: Publish to PyPI with API token
89
+ uses: pypa/gh-action-pypi-publish@release/v1
90
+ with:
91
+ password: ${{ secrets.PYPI_API_TOKEN }}
@@ -0,0 +1,25 @@
1
+ # Byte-compiled / cache
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+
7
+ # Distribution / packaging
8
+ build/
9
+ dist/
10
+ *.egg-info/
11
+
12
+ # Virtual envs
13
+ .venv/
14
+ venv/
15
+
16
+ # Test/cache
17
+ .pytest_cache/
18
+ .mypy_cache/
19
+ .ruff_cache/
20
+
21
+ # OS/editor
22
+ .DS_Store
23
+ Thumbs.db
24
+ .vscode/
25
+ .idea/
@@ -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,138 @@
1
+ # cloudcircuit
2
+
3
+ Lightweight, deterministic safeguards you can embed into cloud spend enforcement:
4
+
5
+ - Budget usage checks (`check_budget`)
6
+ - Spend spike detection (`check_anomaly_spike`)
7
+ - Simple circuit-breaker decisioning (`evaluate_circuit_breaker`)
8
+
9
+ This package focuses on the core logic only. You bring your own metrics ingestion (billing exports, CloudWatch,
10
+ BigQuery, etc.) and enforcement (alerts, policy, throttling, kill-switches).
11
+
12
+ ## Install
13
+
14
+ From PyPI (once published):
15
+
16
+ ```bash
17
+ python -m pip install cloudcircuit
18
+ ```
19
+
20
+ For local development from this repo:
21
+
22
+ ```bash
23
+ cd cloudcircuit
24
+ python -m pip install -e ".[dev]"
25
+ pytest
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ The public APIs currently live in `cloudcircuit.safeguards`:
31
+
32
+ ```python
33
+ from cloudcircuit.safeguards import (
34
+ check_anomaly_spike,
35
+ check_budget,
36
+ evaluate_circuit_breaker,
37
+ )
38
+ ```
39
+
40
+ All functions are deterministic and raise `ValueError` on invalid inputs (negative spend, invalid thresholds, etc.).
41
+
42
+ ## Quickstart
43
+
44
+ ### 1) Budget guardrail
45
+
46
+ ```python
47
+ from cloudcircuit.safeguards import check_budget
48
+
49
+ result = check_budget(current_spend=920.0, budget_limit=1000.0, warning_ratio=0.9)
50
+
51
+ if result.is_breached:
52
+ # Hard stop: disable non-essential workloads, block expensive operations, etc.
53
+ print("BUDGET BREACHED:", result)
54
+ elif result.is_warning:
55
+ # Soft stop: alert, reduce concurrency, apply stricter policy.
56
+ print("BUDGET WARNING:", result)
57
+ else:
58
+ print("OK:", result)
59
+ ```
60
+
61
+ ### 2) Spike detection (latest point vs baseline)
62
+
63
+ ```python
64
+ from cloudcircuit.safeguards import check_anomaly_spike
65
+
66
+ # Example: daily spend totals (or hourly), latest point last.
67
+ series = [120.0, 125.0, 118.0, 260.0]
68
+
69
+ result = check_anomaly_spike(series, spike_multiplier=1.5, min_baseline=0.0)
70
+ if result.is_spike:
71
+ print("SPEND SPIKE:", result.latest_spend, "threshold:", result.threshold)
72
+ ```
73
+
74
+ ### 3) Circuit-breaker decisioning
75
+
76
+ ```python
77
+ from cloudcircuit.safeguards import evaluate_circuit_breaker
78
+
79
+ decision = evaluate_circuit_breaker(
80
+ consecutive_failures=3,
81
+ failure_threshold=3,
82
+ cooldown_steps_remaining=0,
83
+ )
84
+
85
+ if not decision.allow_operation:
86
+ print("BREAKER OPEN; retry after steps:", decision.retry_after_steps)
87
+ ```
88
+
89
+ ## Architecture
90
+
91
+ Current modules:
92
+
93
+ - `src/cloudcircuit/safeguards.py`: core dataclasses and guardrail functions:
94
+ - `BudgetCheckResult`, `check_budget`
95
+ - `AnomalyCheckResult`, `check_anomaly_spike`
96
+ - `CircuitBreakerDecision`, `evaluate_circuit_breaker`
97
+ - `tests/test_safeguards.py`: contract tests for edge cases and input validation.
98
+
99
+ Design goals:
100
+
101
+ - Deterministic, side-effect free logic (easy to unit-test and reason about)
102
+ - Minimal dependencies (safe to embed in CLIs, jobs, and services)
103
+ - Typed, explicit return objects (so callers can log/audit decisions)
104
+
105
+ Non-goals (by design):
106
+
107
+ - Cloud provider integrations (metrics ingestion / policy enforcement)
108
+ - Stateful breaker implementation (persistence, jitter/backoff, distributed locks)
109
+
110
+ ## Deployment Guide
111
+
112
+ Typical deployment looks like a thin integration layer around `cloudcircuit`:
113
+
114
+ 1. Collect spend and operational signals.
115
+ - Spend totals: billing exports, cost explorer, warehouse tables, or internal chargeback.
116
+ - Failure counters: request error rates, provider API failures, budget retrieval failures.
117
+ 2. Evaluate guardrails.
118
+ - `check_budget()` for hard/soft budget thresholds.
119
+ - `check_anomaly_spike()` to catch sudden jumps early.
120
+ - `evaluate_circuit_breaker()` to gate high-cost operations when systems are unstable.
121
+ 3. Enforce decisions.
122
+ - Alerting: Slack/email/PagerDuty with `*_Result` fields for audit context.
123
+ - Throttling: reduce concurrency, disable non-critical jobs, or switch to cheaper defaults.
124
+ - Kill-switch: block expensive actions when `is_breached` is true.
125
+ 4. Persist state externally when needed.
126
+ - Store the last N spend points, consecutive failure counts, and cooldown counters in your DB/redis.
127
+ - Run the evaluator on a schedule (cron / Airflow / Cloud Scheduler) or inline per request.
128
+
129
+ Recommended patterns:
130
+
131
+ - Treat spend series as monotonic time buckets (hourly/daily) and pass the newest point last.
132
+ - Log the full result objects (they are small and serializable) so you can explain why a safeguard tripped.
133
+ - Prefer "fail closed" for high-risk operations: if your metrics pipeline is down, open the breaker or block
134
+ expensive actions until signals recover.
135
+
136
+ ## License
137
+
138
+ MIT (see `pyproject.toml` for the current license declaration).
@@ -0,0 +1,76 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.26.0"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "cloudcircuit"
7
+ version = "0.1.0"
8
+ description = "CloudCircuit Python package."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "CloudCircuit Contributors" }]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: Implementation :: CPython",
23
+ "Typing :: Typed",
24
+ ]
25
+ dependencies = []
26
+
27
+ [project.optional-dependencies]
28
+ dev = [
29
+ "build>=1.2.2",
30
+ "mypy>=1.11.2",
31
+ "pytest>=8.3.3",
32
+ "ruff>=0.6.9",
33
+ "twine>=5.1.1",
34
+ ]
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["src/cloudcircuit"]
38
+
39
+ [tool.pytest.ini_options]
40
+ minversion = "8.0"
41
+ addopts = ["-ra", "--strict-config", "--strict-markers"]
42
+ testpaths = ["tests"]
43
+
44
+ [tool.ruff]
45
+ target-version = "py310"
46
+ line-length = 100
47
+ src = ["src", "tests"]
48
+
49
+ [tool.ruff.lint]
50
+ select = ["E", "F", "I", "B", "UP", "N", "ANN", "C4", "SIM"]
51
+ ignore = ["ANN101", "ANN102"]
52
+
53
+ [tool.ruff.lint.per-file-ignores]
54
+ "tests/**/*.py" = ["ANN201", "ANN202", "ANN001", "ANN002", "ANN003"]
55
+
56
+ [tool.ruff.format]
57
+ quote-style = "double"
58
+ indent-style = "space"
59
+ line-ending = "lf"
60
+
61
+ [tool.mypy]
62
+ python_version = "3.10"
63
+ packages = ["cloudcircuit"]
64
+ mypy_path = "src"
65
+ warn_return_any = true
66
+ warn_unused_configs = true
67
+ disallow_untyped_defs = true
68
+ disallow_incomplete_defs = true
69
+ check_untyped_defs = true
70
+ no_implicit_optional = true
71
+ strict_equality = true
72
+ show_error_codes = true
73
+ pretty = true
74
+
75
+ [tool.twine]
76
+ repository = "pypi"
@@ -0,0 +1,4 @@
1
+ """CloudCircuit package."""
2
+
3
+ __all__ = ["__version__"]
4
+ __version__ = "0.1.0"
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,9 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+
7
+ ROOT = Path(__file__).resolve().parents[1]
8
+ SRC = ROOT / "src"
9
+ sys.path.insert(0, str(SRC))
@@ -0,0 +1,87 @@
1
+ import pytest
2
+
3
+ from cloudcircuit.safeguards import (
4
+ check_anomaly_spike,
5
+ check_budget,
6
+ evaluate_circuit_breaker,
7
+ )
8
+
9
+
10
+ def test_budget_ok_without_warning() -> None:
11
+ result = check_budget(current_spend=40.0, budget_limit=100.0, warning_ratio=0.8)
12
+ assert result.usage_ratio == 0.4
13
+ assert result.remaining_budget == 60.0
14
+ assert result.is_warning is False
15
+ assert result.is_breached is False
16
+
17
+
18
+ def test_budget_warning_and_breach() -> None:
19
+ result = check_budget(current_spend=100.0, budget_limit=100.0, warning_ratio=0.8)
20
+ assert result.is_warning is True
21
+ assert result.is_breached is True
22
+ assert result.remaining_budget == 0.0
23
+
24
+
25
+ @pytest.mark.parametrize(
26
+ ("current_spend", "budget_limit", "warning_ratio"),
27
+ [(-1.0, 100.0, 0.9), (10.0, 0.0, 0.9), (10.0, 100.0, 0.0)],
28
+ )
29
+ def test_budget_invalid_inputs(
30
+ current_spend: float, budget_limit: float, warning_ratio: float
31
+ ) -> None:
32
+ with pytest.raises(ValueError):
33
+ check_budget(current_spend, budget_limit, warning_ratio)
34
+
35
+
36
+ def test_anomaly_spike_detected() -> None:
37
+ result = check_anomaly_spike([10.0, 10.0, 10.0, 20.0], spike_multiplier=1.5)
38
+ assert result.baseline_mean == 10.0
39
+ assert result.threshold == 15.0
40
+ assert result.is_spike is True
41
+
42
+
43
+ def test_anomaly_not_spike_with_min_baseline() -> None:
44
+ result = check_anomaly_spike([0.0, 0.0, 0.0, 0.4], spike_multiplier=2.0, min_baseline=0.5)
45
+ assert result.baseline_mean == 0.0
46
+ assert result.threshold == 1.0
47
+ assert result.is_spike is False
48
+
49
+
50
+ def test_anomaly_invalid_inputs() -> None:
51
+ with pytest.raises(ValueError):
52
+ check_anomaly_spike([1.0], spike_multiplier=1.5)
53
+ with pytest.raises(ValueError):
54
+ check_anomaly_spike([1.0, 2.0], spike_multiplier=1.0)
55
+ with pytest.raises(ValueError):
56
+ check_anomaly_spike([1.0, -1.0], spike_multiplier=2.0)
57
+
58
+
59
+ def test_circuit_breaker_allows_operation_below_threshold() -> None:
60
+ decision = evaluate_circuit_breaker(consecutive_failures=2, failure_threshold=3)
61
+ assert decision.is_open is False
62
+ assert decision.allow_operation is True
63
+ assert decision.retry_after_steps == 0
64
+
65
+
66
+ def test_circuit_breaker_opens_at_threshold() -> None:
67
+ decision = evaluate_circuit_breaker(consecutive_failures=3, failure_threshold=3)
68
+ assert decision.is_open is True
69
+ assert decision.allow_operation is False
70
+
71
+
72
+ def test_circuit_breaker_stays_open_during_cooldown() -> None:
73
+ decision = evaluate_circuit_breaker(
74
+ consecutive_failures=0, failure_threshold=3, cooldown_steps_remaining=2
75
+ )
76
+ assert decision.is_open is True
77
+ assert decision.allow_operation is False
78
+ assert decision.retry_after_steps == 2
79
+
80
+
81
+ def test_circuit_breaker_invalid_inputs() -> None:
82
+ with pytest.raises(ValueError):
83
+ evaluate_circuit_breaker(consecutive_failures=-1)
84
+ with pytest.raises(ValueError):
85
+ evaluate_circuit_breaker(consecutive_failures=0, failure_threshold=0)
86
+ with pytest.raises(ValueError):
87
+ evaluate_circuit_breaker(consecutive_failures=0, cooldown_steps_remaining=-1)
@@ -0,0 +1,5 @@
1
+ from cloudcircuit import __version__
2
+
3
+
4
+ def test_version_is_defined() -> None:
5
+ assert __version__