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.

Files changed (85) hide show
  1. {margin-0.9.3/margin.egg-info → margin-0.9.4}/PKG-INFO +1 -1
  2. {margin-0.9.3 → margin-0.9.4}/margin/calibrate.py +18 -4
  3. {margin-0.9.3 → margin-0.9.4}/margin/streaming.py +28 -3
  4. {margin-0.9.3 → margin-0.9.4/margin.egg-info}/PKG-INFO +1 -1
  5. {margin-0.9.3 → margin-0.9.4}/pyproject.toml +1 -1
  6. {margin-0.9.3 → margin-0.9.4}/tests/test_calibrate.py +32 -0
  7. {margin-0.9.3 → margin-0.9.4}/tests/test_streaming.py +47 -4
  8. {margin-0.9.3 → margin-0.9.4}/LICENSE +0 -0
  9. {margin-0.9.3 → margin-0.9.4}/NOTICE +0 -0
  10. {margin-0.9.3 → margin-0.9.4}/README.md +0 -0
  11. {margin-0.9.3 → margin-0.9.4}/margin/__init__.py +0 -0
  12. {margin-0.9.3 → margin-0.9.4}/margin/__main__.py +0 -0
  13. {margin-0.9.3 → margin-0.9.4}/margin/algebra.py +0 -0
  14. {margin-0.9.3 → margin-0.9.4}/margin/anomaly.py +0 -0
  15. {margin-0.9.3 → margin-0.9.4}/margin/bridge.py +0 -0
  16. {margin-0.9.3 → margin-0.9.4}/margin/causal.py +0 -0
  17. {margin-0.9.3 → margin-0.9.4}/margin/composite.py +0 -0
  18. {margin-0.9.3 → margin-0.9.4}/margin/confidence.py +0 -0
  19. {margin-0.9.3 → margin-0.9.4}/margin/config.py +0 -0
  20. {margin-0.9.3 → margin-0.9.4}/margin/contract.py +0 -0
  21. {margin-0.9.3 → margin-0.9.4}/margin/correlate.py +0 -0
  22. {margin-0.9.3 → margin-0.9.4}/margin/diff.py +0 -0
  23. {margin-0.9.3 → margin-0.9.4}/margin/drift.py +0 -0
  24. {margin-0.9.3 → margin-0.9.4}/margin/events.py +0 -0
  25. {margin-0.9.3 → margin-0.9.4}/margin/forecast.py +0 -0
  26. {margin-0.9.3 → margin-0.9.4}/margin/health.py +0 -0
  27. {margin-0.9.3 → margin-0.9.4}/margin/intent.py +0 -0
  28. {margin-0.9.3 → margin-0.9.4}/margin/ledger.py +0 -0
  29. {margin-0.9.3 → margin-0.9.4}/margin/loop.py +0 -0
  30. {margin-0.9.3 → margin-0.9.4}/margin/observation.py +0 -0
  31. {margin-0.9.3 → margin-0.9.4}/margin/persist.py +0 -0
  32. {margin-0.9.3 → margin-0.9.4}/margin/policy/__init__.py +0 -0
  33. {margin-0.9.3 → margin-0.9.4}/margin/policy/compose.py +0 -0
  34. {margin-0.9.3 → margin-0.9.4}/margin/policy/core.py +0 -0
  35. {margin-0.9.3 → margin-0.9.4}/margin/policy/temporal.py +0 -0
  36. {margin-0.9.3 → margin-0.9.4}/margin/policy/trace.py +0 -0
  37. {margin-0.9.3 → margin-0.9.4}/margin/policy/tuning.py +0 -0
  38. {margin-0.9.3 → margin-0.9.4}/margin/policy/validate.py +0 -0
  39. {margin-0.9.3 → margin-0.9.4}/margin/predicates.py +0 -0
  40. {margin-0.9.3 → margin-0.9.4}/margin/provenance.py +0 -0
  41. {margin-0.9.3 → margin-0.9.4}/margin/transitions.py +0 -0
  42. {margin-0.9.3 → margin-0.9.4}/margin/uncertain.py +0 -0
  43. {margin-0.9.3 → margin-0.9.4}/margin/validity.py +0 -0
  44. {margin-0.9.3 → margin-0.9.4}/margin.egg-info/SOURCES.txt +0 -0
  45. {margin-0.9.3 → margin-0.9.4}/margin.egg-info/dependency_links.txt +0 -0
  46. {margin-0.9.3 → margin-0.9.4}/margin.egg-info/top_level.txt +0 -0
  47. {margin-0.9.3 → margin-0.9.4}/setup.cfg +0 -0
  48. {margin-0.9.3 → margin-0.9.4}/tests/test_algebra.py +0 -0
  49. {margin-0.9.3 → margin-0.9.4}/tests/test_anomaly.py +0 -0
  50. {margin-0.9.3 → margin-0.9.4}/tests/test_bridge.py +0 -0
  51. {margin-0.9.3 → margin-0.9.4}/tests/test_causal.py +0 -0
  52. {margin-0.9.3 → margin-0.9.4}/tests/test_composite.py +0 -0
  53. {margin-0.9.3 → margin-0.9.4}/tests/test_confidence.py +0 -0
  54. {margin-0.9.3 → margin-0.9.4}/tests/test_config.py +0 -0
  55. {margin-0.9.3 → margin-0.9.4}/tests/test_contract.py +0 -0
  56. {margin-0.9.3 → margin-0.9.4}/tests/test_correlate.py +0 -0
  57. {margin-0.9.3 → margin-0.9.4}/tests/test_diff.py +0 -0
  58. {margin-0.9.3 → margin-0.9.4}/tests/test_drift.py +0 -0
  59. {margin-0.9.3 → margin-0.9.4}/tests/test_events.py +0 -0
  60. {margin-0.9.3 → margin-0.9.4}/tests/test_forecast.py +0 -0
  61. {margin-0.9.3 → margin-0.9.4}/tests/test_health.py +0 -0
  62. {margin-0.9.3 → margin-0.9.4}/tests/test_healthcare.py +0 -0
  63. {margin-0.9.3 → margin-0.9.4}/tests/test_intent.py +0 -0
  64. {margin-0.9.3 → margin-0.9.4}/tests/test_ledger.py +0 -0
  65. {margin-0.9.3 → margin-0.9.4}/tests/test_loop.py +0 -0
  66. {margin-0.9.3 → margin-0.9.4}/tests/test_neuro.py +0 -0
  67. {margin-0.9.3 → margin-0.9.4}/tests/test_observation.py +0 -0
  68. {margin-0.9.3 → margin-0.9.4}/tests/test_persist.py +0 -0
  69. {margin-0.9.3 → margin-0.9.4}/tests/test_policy.py +0 -0
  70. {margin-0.9.3 → margin-0.9.4}/tests/test_policy_compose.py +0 -0
  71. {margin-0.9.3 → margin-0.9.4}/tests/test_policy_temporal.py +0 -0
  72. {margin-0.9.3 → margin-0.9.4}/tests/test_policy_trace.py +0 -0
  73. {margin-0.9.3 → margin-0.9.4}/tests/test_policy_tuning.py +0 -0
  74. {margin-0.9.3 → margin-0.9.4}/tests/test_policy_validate.py +0 -0
  75. {margin-0.9.3 → margin-0.9.4}/tests/test_predicates.py +0 -0
  76. {margin-0.9.3 → margin-0.9.4}/tests/test_printer3d.py +0 -0
  77. {margin-0.9.3 → margin-0.9.4}/tests/test_provenance.py +0 -0
  78. {margin-0.9.3 → margin-0.9.4}/tests/test_reverse_bridge.py +0 -0
  79. {margin-0.9.3 → margin-0.9.4}/tests/test_ros2.py +0 -0
  80. {margin-0.9.3 → margin-0.9.4}/tests/test_staleness.py +0 -0
  81. {margin-0.9.3 → margin-0.9.4}/tests/test_transformer.py +0 -0
  82. {margin-0.9.3 → margin-0.9.4}/tests/test_transitions.py +0 -0
  83. {margin-0.9.3 → margin-0.9.4}/tests/test_uncertain.py +0 -0
  84. {margin-0.9.3 → margin-0.9.4}/tests/test_validity.py +0 -0
  85. {margin-0.9.3 → margin-0.9.4}/tests/test_windowing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: margin
3
- Version: 0.9.3
3
+ Version: 0.9.4
4
4
  Summary: Typed health classification, uncertainty algebra, and correction auditing for any system that measures things and needs to explain what happened.
5
5
  Author: Cope Labs LLC
6
6
  License: MIT
@@ -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
- ) -> tuple[dict[str, float], dict[str, Thresholds]]:
160
+ return_parser: bool = False,
161
+ ):
161
162
  """
162
- Calibrate multiple components at once. Returns (baselines, thresholds)
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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: margin
3
- Version: 0.9.3
3
+ Version: 0.9.4
4
4
  Summary: Typed health classification, uncertainty algebra, and correction auditing for any system that measures things and needs to explain what happened.
5
5
  Author: Cope Labs LLC
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "margin"
7
- version = "0.9.3"
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 test_status_omits_disabled_feature_keys(self):
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
- # drift disabled → "drift" key absent
340
- assert "drift" not in s
341
- assert "anomaly" not in s
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