margin 0.9.3__tar.gz → 0.9.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.
Potentially problematic release.
This version of margin might be problematic. Click here for more details.
- {margin-0.9.3/margin.egg-info → margin-0.9.4}/PKG-INFO +1 -1
- {margin-0.9.3 → margin-0.9.4}/margin/calibrate.py +18 -4
- {margin-0.9.3 → margin-0.9.4}/margin/streaming.py +28 -3
- {margin-0.9.3 → margin-0.9.4/margin.egg-info}/PKG-INFO +1 -1
- {margin-0.9.3 → margin-0.9.4}/pyproject.toml +1 -1
- {margin-0.9.3 → margin-0.9.4}/tests/test_calibrate.py +32 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_streaming.py +47 -4
- {margin-0.9.3 → margin-0.9.4}/LICENSE +0 -0
- {margin-0.9.3 → margin-0.9.4}/NOTICE +0 -0
- {margin-0.9.3 → margin-0.9.4}/README.md +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/__init__.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/__main__.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/algebra.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/anomaly.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/bridge.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/causal.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/composite.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/confidence.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/config.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/contract.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/correlate.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/diff.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/drift.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/events.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/forecast.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/health.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/intent.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/ledger.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/loop.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/observation.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/persist.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/policy/__init__.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/policy/compose.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/policy/core.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/policy/temporal.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/policy/trace.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/policy/tuning.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/policy/validate.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/predicates.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/provenance.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/transitions.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/uncertain.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin/validity.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin.egg-info/SOURCES.txt +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin.egg-info/dependency_links.txt +0 -0
- {margin-0.9.3 → margin-0.9.4}/margin.egg-info/top_level.txt +0 -0
- {margin-0.9.3 → margin-0.9.4}/setup.cfg +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_algebra.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_anomaly.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_bridge.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_causal.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_composite.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_confidence.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_config.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_contract.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_correlate.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_diff.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_drift.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_events.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_forecast.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_health.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_healthcare.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_intent.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_ledger.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_loop.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_neuro.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_observation.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_persist.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_policy.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_policy_compose.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_policy_temporal.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_policy_trace.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_policy_tuning.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_policy_validate.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_predicates.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_printer3d.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_provenance.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_reverse_bridge.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_ros2.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_staleness.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_transformer.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_transitions.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_uncertain.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_validity.py +0 -0
- {margin-0.9.3 → margin-0.9.4}/tests/test_windowing.py +0 -0
|
@@ -157,10 +157,10 @@ def calibrate_many(
|
|
|
157
157
|
use_std: bool = False,
|
|
158
158
|
intact_std_multiplier: float = 1.5,
|
|
159
159
|
ablated_std_multiplier: float = 3.0,
|
|
160
|
-
|
|
160
|
+
return_parser: bool = False,
|
|
161
|
+
):
|
|
161
162
|
"""
|
|
162
|
-
Calibrate multiple components at once.
|
|
163
|
-
ready to pass directly to Parser().
|
|
163
|
+
Calibrate multiple components at once.
|
|
164
164
|
|
|
165
165
|
Args:
|
|
166
166
|
component_values: {name: [healthy_measurements]}
|
|
@@ -171,12 +171,14 @@ def calibrate_many(
|
|
|
171
171
|
use_std: if True, use std-based thresholds (see calibrate())
|
|
172
172
|
intact_std_multiplier: std deviations for intact threshold (std mode)
|
|
173
173
|
ablated_std_multiplier: std deviations for ablated threshold (std mode)
|
|
174
|
+
return_parser: if True, return a ready-to-use Parser instead of the raw tuple
|
|
174
175
|
|
|
175
176
|
Returns:
|
|
176
|
-
(baselines_dict, thresholds_dict) suitable for:
|
|
177
|
+
When return_parser=False (default): (baselines_dict, thresholds_dict) suitable for:
|
|
177
178
|
Parser(baselines=baselines_dict,
|
|
178
179
|
thresholds=first_threshold,
|
|
179
180
|
component_thresholds=thresholds_dict)
|
|
181
|
+
When return_parser=True: a Parser with baselines and per-component thresholds set
|
|
180
182
|
"""
|
|
181
183
|
polarities = polarities or {}
|
|
182
184
|
baselines = {}
|
|
@@ -193,6 +195,18 @@ def calibrate_many(
|
|
|
193
195
|
baselines[name] = result.baseline
|
|
194
196
|
thresholds[name] = result.thresholds
|
|
195
197
|
|
|
198
|
+
if return_parser:
|
|
199
|
+
names = list(thresholds.keys())
|
|
200
|
+
if not names:
|
|
201
|
+
raise ValueError("No components to calibrate")
|
|
202
|
+
default_thresholds = thresholds[names[0]]
|
|
203
|
+
component_thresholds = {n: t for n, t in thresholds.items() if n != names[0]}
|
|
204
|
+
return Parser(
|
|
205
|
+
baselines=baselines,
|
|
206
|
+
thresholds=default_thresholds,
|
|
207
|
+
component_thresholds=component_thresholds,
|
|
208
|
+
)
|
|
209
|
+
|
|
196
210
|
return baselines, thresholds
|
|
197
211
|
|
|
198
212
|
|
|
@@ -34,6 +34,26 @@ class WindowConfig:
|
|
|
34
34
|
anomaly: Optional[int] = None
|
|
35
35
|
correlation: Optional[int] = None
|
|
36
36
|
|
|
37
|
+
def to_dict(self) -> dict:
|
|
38
|
+
"""Serialize to plain dict; omits None fields."""
|
|
39
|
+
d: dict = {}
|
|
40
|
+
if self.drift is not None:
|
|
41
|
+
d["drift"] = self.drift
|
|
42
|
+
if self.anomaly is not None:
|
|
43
|
+
d["anomaly"] = self.anomaly
|
|
44
|
+
if self.correlation is not None:
|
|
45
|
+
d["correlation"] = self.correlation
|
|
46
|
+
return d
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def from_dict(cls, d: dict) -> 'WindowConfig':
|
|
50
|
+
"""Deserialize from a plain dict produced by to_dict()."""
|
|
51
|
+
return cls(
|
|
52
|
+
drift=d.get("drift"),
|
|
53
|
+
anomaly=d.get("anomaly"),
|
|
54
|
+
correlation=d.get("correlation"),
|
|
55
|
+
)
|
|
56
|
+
|
|
37
57
|
from .confidence import Confidence
|
|
38
58
|
from .health import Health, Thresholds, classify
|
|
39
59
|
from .observation import Observation, Expression
|
|
@@ -478,18 +498,23 @@ class Monitor:
|
|
|
478
498
|
return self._step
|
|
479
499
|
|
|
480
500
|
def status(self) -> dict:
|
|
481
|
-
"""Full snapshot: health + drift + anomaly + correlations.
|
|
501
|
+
"""Full snapshot: health + drift + anomaly + correlations.
|
|
502
|
+
|
|
503
|
+
Keys are always present; disabled features or no-data trackers
|
|
504
|
+
return empty dicts so callers can index without key-existence checks.
|
|
505
|
+
"""
|
|
482
506
|
d: dict = {
|
|
483
507
|
"step": self._step,
|
|
484
508
|
"expression": self._expression.to_dict() if self._expression else None,
|
|
509
|
+
"drift": {},
|
|
510
|
+
"anomaly": {},
|
|
511
|
+
"correlations": {},
|
|
485
512
|
}
|
|
486
513
|
if "drift" in self.features:
|
|
487
|
-
d["drift"] = {}
|
|
488
514
|
for name, tracker in self._drift_trackers.items():
|
|
489
515
|
if tracker.classification:
|
|
490
516
|
d["drift"][name] = tracker.classification.to_dict()
|
|
491
517
|
if "anomaly" in self.features:
|
|
492
|
-
d["anomaly"] = {}
|
|
493
518
|
for name, tracker in self._anomaly_trackers.items():
|
|
494
519
|
if tracker.classification:
|
|
495
520
|
d["anomaly"][name] = tracker.classification.to_dict()
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "margin"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.4"
|
|
8
8
|
description = "Typed health classification, uncertainty algebra, and correction auditing for any system that measures things and needs to explain what happened."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -176,3 +176,35 @@ class TestNeedsRecalibrationMany:
|
|
|
176
176
|
flags = needs_recalibration_many(cal, recent)
|
|
177
177
|
assert flags["cpu"] is True
|
|
178
178
|
assert flags["mem"] is False
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class TestCalibrateManyReturnParser:
|
|
182
|
+
def test_return_parser_gives_parser(self):
|
|
183
|
+
from margin.observation import Parser
|
|
184
|
+
result = calibrate_many(
|
|
185
|
+
{"rps": [90.0, 100.0, 110.0], "latency": [48.0, 50.0, 52.0]},
|
|
186
|
+
polarities={"latency": False},
|
|
187
|
+
return_parser=True,
|
|
188
|
+
)
|
|
189
|
+
assert isinstance(result, Parser)
|
|
190
|
+
|
|
191
|
+
def test_return_parser_classifies_correctly(self):
|
|
192
|
+
result = calibrate_many(
|
|
193
|
+
{"rps": [100.0] * 10, "latency": [50.0] * 10},
|
|
194
|
+
polarities={"latency": False},
|
|
195
|
+
return_parser=True,
|
|
196
|
+
)
|
|
197
|
+
from margin.health import Health
|
|
198
|
+
expr = result.parse({"rps": 95.0, "latency": 52.0})
|
|
199
|
+
assert expr.health_of("rps") == Health.INTACT
|
|
200
|
+
assert expr.health_of("latency") == Health.INTACT
|
|
201
|
+
|
|
202
|
+
def test_return_parser_false_gives_tuple(self):
|
|
203
|
+
result = calibrate_many({"rps": [100.0] * 5})
|
|
204
|
+
assert isinstance(result, tuple)
|
|
205
|
+
assert len(result) == 2
|
|
206
|
+
|
|
207
|
+
def test_return_parser_empty_raises(self):
|
|
208
|
+
import pytest
|
|
209
|
+
with pytest.raises(ValueError):
|
|
210
|
+
calibrate_many({}, return_parser=True)
|
|
@@ -6,6 +6,7 @@ from margin import (
|
|
|
6
6
|
Observation, Health, Confidence, Thresholds, Parser,
|
|
7
7
|
DriftState, DriftDirection, AnomalyState,
|
|
8
8
|
DriftTracker, AnomalyTracker, CorrelationTracker, Monitor,
|
|
9
|
+
WindowConfig,
|
|
9
10
|
)
|
|
10
11
|
|
|
11
12
|
|
|
@@ -330,15 +331,16 @@ class TestMonitorFeatures:
|
|
|
330
331
|
m = Monitor(self._parser(), features={"health", "drift"})
|
|
331
332
|
assert isinstance(m.features, frozenset)
|
|
332
333
|
|
|
333
|
-
def
|
|
334
|
+
def test_status_always_has_drift_anomaly_correlations_keys(self):
|
|
334
335
|
m = Monitor(self._parser(), features={"health"})
|
|
335
336
|
for i in range(5):
|
|
336
337
|
m.update({"cpu": 50.0 - i, "mem": 70.0},
|
|
337
338
|
now=t0 + timedelta(seconds=i * 60))
|
|
338
339
|
s = m.status()
|
|
339
|
-
#
|
|
340
|
-
assert "drift"
|
|
341
|
-
assert "anomaly"
|
|
340
|
+
# Keys always present — empty dict when feature disabled or no data yet
|
|
341
|
+
assert "drift" in s and s["drift"] == {}
|
|
342
|
+
assert "anomaly" in s and s["anomaly"] == {}
|
|
343
|
+
assert "correlations" in s and s["correlations"] == {}
|
|
342
344
|
|
|
343
345
|
def test_repr_shows_features_when_partial(self):
|
|
344
346
|
m = Monitor(self._parser(), features={"health", "drift"})
|
|
@@ -405,3 +407,44 @@ class TestMonitorResetAnomalyReference:
|
|
|
405
407
|
m = Monitor(self._parser(), features={"health", "drift"})
|
|
406
408
|
# Should not raise even though anomaly trackers don't exist
|
|
407
409
|
m.reset_anomaly_reference()
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
# -----------------------------------------------------------------------
|
|
413
|
+
# WindowConfig serialization
|
|
414
|
+
# -----------------------------------------------------------------------
|
|
415
|
+
|
|
416
|
+
class TestWindowConfigSerialization:
|
|
417
|
+
def test_to_dict_full(self):
|
|
418
|
+
wc = WindowConfig(drift=10, anomaly=40, correlation=100)
|
|
419
|
+
d = wc.to_dict()
|
|
420
|
+
assert d == {"drift": 10, "anomaly": 40, "correlation": 100}
|
|
421
|
+
|
|
422
|
+
def test_to_dict_partial(self):
|
|
423
|
+
wc = WindowConfig(drift=10)
|
|
424
|
+
d = wc.to_dict()
|
|
425
|
+
assert d == {"drift": 10}
|
|
426
|
+
assert "anomaly" not in d
|
|
427
|
+
assert "correlation" not in d
|
|
428
|
+
|
|
429
|
+
def test_to_dict_empty(self):
|
|
430
|
+
wc = WindowConfig()
|
|
431
|
+
assert wc.to_dict() == {}
|
|
432
|
+
|
|
433
|
+
def test_from_dict_roundtrip(self):
|
|
434
|
+
wc = WindowConfig(drift=10, anomaly=40, correlation=100)
|
|
435
|
+
wc2 = WindowConfig.from_dict(wc.to_dict())
|
|
436
|
+
assert wc2.drift == 10
|
|
437
|
+
assert wc2.anomaly == 40
|
|
438
|
+
assert wc2.correlation == 100
|
|
439
|
+
|
|
440
|
+
def test_from_dict_partial(self):
|
|
441
|
+
wc = WindowConfig.from_dict({"drift": 20})
|
|
442
|
+
assert wc.drift == 20
|
|
443
|
+
assert wc.anomaly is None
|
|
444
|
+
assert wc.correlation is None
|
|
445
|
+
|
|
446
|
+
def test_from_dict_empty(self):
|
|
447
|
+
wc = WindowConfig.from_dict({})
|
|
448
|
+
assert wc.drift is None
|
|
449
|
+
assert wc.anomaly is None
|
|
450
|
+
assert wc.correlation is None
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|