fleet-sensor-baseline 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.
@@ -0,0 +1,20 @@
1
+ """Fleet-wide sensor presence and configuration drift, across machines and time.
2
+
3
+ `bmc-sensor-audit` judges one machine against one declaration. This layer holds
4
+ the list of machines and the history of captures, and answers the two questions
5
+ the referee cannot: *which units differ from their cohort*, and *what changed on
6
+ this unit across time and firmware*.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ # One home for the version, read by `pyproject.toml` through hatchling. Two
12
+ # literals that happen to agree is the arrangement that drifts.
13
+ #
14
+ # **0.0.0 means unreleased, and it is load-bearing.** The community-file checks
15
+ # read this to decide whether the README may name a tag at all: an unreleased
16
+ # tree that announces `v0.1.0` hands a reader a tag to check out that does not
17
+ # exist. Bump this at step 1 of a release, not at step 9.
18
+ __version__ = "0.1.0"
19
+
20
+ __all__ = ["__version__"]
@@ -0,0 +1,234 @@
1
+ """Deriving a `fleet-baseline/1` -- an additional, labeled, downgraded declaration.
2
+
3
+ **Wherever a manufacturer declaration exists, it wins.** A fleet-derived
4
+ baseline is blind to an absence the whole cohort shares, which is the founding
5
+ problem of this family at fleet scale: two thousand trays that all lost the same
6
+ sensor in the same firmware agree with each other perfectly. Consensus reports
7
+ them clean. Only a declaration written by somebody who knew what the board has
8
+ can see it, and `bmc-sensor-audit coverage` is where that comparison belongs.
9
+
10
+ So this module derives, labels what it derived, and refuses to derive from a
11
+ cohort too small to mean anything.
12
+
13
+ **The scope is matched on DECLARED fields and never inferred.** `model` is the
14
+ operator's own name for a class of machine, compared as an opaque string;
15
+ `firmware.release` is a dotted version the collector declares. A range is never
16
+ matched by pattern-guessing at a vendor string like `GB200-fw-1.4.2`, because
17
+ the guess would be a hardcoded assumption about one vendor's formatting living
18
+ inside a tool that claims not to have any. A record with no `firmware.release`
19
+ is EXCLUDED FROM THE DENOMINATOR AND NAMED when a range is in use -- never
20
+ counted as absent, and never silently dropped.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from dataclasses import dataclass, field
26
+ from typing import Iterable, Sequence
27
+
28
+ from .formats import BASELINE_FORMAT, DOWNGRADE_NOTICE, PROVENANCE_DERIVED
29
+ from .store import surface_of
30
+
31
+ #: A cohort smaller than this is an anecdote wearing a format key. Lowering it
32
+ #: takes an explicit flag, so a nine-unit baseline is always somebody's decision.
33
+ DEFAULT_FLOOR = 20
34
+
35
+ DEFAULT_THRESHOLD = 0.99
36
+
37
+
38
+ class BaselineError(Exception):
39
+ """A derivation this module refuses, with the reason in the message."""
40
+
41
+
42
+ @dataclass
43
+ class Selection:
44
+ """What the scope selected, and what it could not judge."""
45
+ records: list[dict] = field(default_factory=list)
46
+ #: `(unit_key, reason)` for records a range could not be applied to. Kept
47
+ #: because a unit excluded for being unjudgeable is not a unit that failed.
48
+ excluded: list[tuple[str, str]] = field(default_factory=list)
49
+
50
+
51
+ def parse_range(text: str) -> list[tuple[str, tuple[int, ...]]]:
52
+ """`>=1.4,<1.5` into comparable clauses.
53
+
54
+ Deliberately small: `>=`, `>`, `<=`, `<`, `==`, over dotted integers.
55
+ Categorical presence needs explainable, auditable arithmetic, and a
56
+ comparator nobody can read by eye has no place deciding which machines are
57
+ in a denominator.
58
+ """
59
+ clauses: list[tuple[str, tuple[int, ...]]] = []
60
+ for raw in text.split(","):
61
+ piece = raw.strip()
62
+ if not piece:
63
+ continue
64
+ for operator in (">=", "<=", "==", ">", "<"):
65
+ if piece.startswith(operator):
66
+ value = piece[len(operator):].strip()
67
+ clauses.append((operator, _version(value, piece)))
68
+ break
69
+ else:
70
+ raise BaselineError(
71
+ f"{piece!r} does not start with one of >=, <=, ==, >, <; a "
72
+ f"firmware range is a list of comparisons")
73
+ if not clauses:
74
+ raise BaselineError(f"{text!r} contains no comparisons")
75
+ return clauses
76
+
77
+
78
+ def _version(value: str, where: str) -> tuple[int, ...]:
79
+ parts = value.split(".")
80
+ if not value or not all(part.isdigit() for part in parts):
81
+ raise BaselineError(
82
+ f"{where!r} compares against {value!r}, which is not a dotted "
83
+ f"numeric version")
84
+ return tuple(int(part) for part in parts)
85
+
86
+
87
+ def _pad(left: tuple[int, ...], right: tuple[int, ...]) -> tuple[tuple, tuple]:
88
+ """`1.4` and `1.4.2` compare as `1.4.0` and `1.4.2`.
89
+
90
+ Without this, `>=1.4` refuses `1.4.2` on a tuple comparison of unequal
91
+ length in some orderings and accepts it in others -- a range whose answer
92
+ depends on how many components somebody typed.
93
+ """
94
+ width = max(len(left), len(right))
95
+ return (left + (0,) * (width - len(left)), right + (0,) * (width - len(right)))
96
+
97
+
98
+ def in_range(release: str, clauses: Sequence[tuple[str, tuple[int, ...]]]) -> bool:
99
+ version = _version(release, release)
100
+ for operator, bound in clauses:
101
+ left, right = _pad(version, bound)
102
+ if operator == ">=" and not left >= right:
103
+ return False
104
+ if operator == ">" and not left > right:
105
+ return False
106
+ if operator == "<=" and not left <= right:
107
+ return False
108
+ if operator == "<" and not left < right:
109
+ return False
110
+ if operator == "==" and left != right:
111
+ return False
112
+ return True
113
+
114
+
115
+ def select(records: Iterable[dict], *, model: str | None = None,
116
+ firmware_range: str | None = None,
117
+ firmware: str | None = None) -> Selection:
118
+ """Records within a scope, plus the ones a range could not judge."""
119
+ clauses = parse_range(firmware_range) if firmware_range else None
120
+ out = Selection()
121
+ for record in records:
122
+ if model is not None and record.get("model") != model:
123
+ continue
124
+ info = record.get("firmware") or {}
125
+ if firmware is not None and info.get("version") != firmware:
126
+ continue
127
+ if clauses is not None:
128
+ release = info.get("release")
129
+ if not isinstance(release, str) or not release:
130
+ out.excluded.append((
131
+ record["unit_key"],
132
+ "no firmware.release to compare against the range; a "
133
+ "version string is not a version"))
134
+ continue
135
+ try:
136
+ if not in_range(release, clauses):
137
+ continue
138
+ except BaselineError as exc:
139
+ out.excluded.append((record["unit_key"], str(exc)))
140
+ continue
141
+ out.records.append(record)
142
+ return out
143
+
144
+
145
+ def latest_per_unit(records: Iterable[dict]) -> dict[str, list[dict]]:
146
+ """The newest capture per surface, grouped by unit.
147
+
148
+ **A unit contributes once to the denominator, however often it was
149
+ captured.** Otherwise a rack that reports hourly outvotes a rack that
150
+ reports weekly, and the baseline describes the collection schedule rather
151
+ than the fleet.
152
+ """
153
+ newest: dict[tuple, dict] = {}
154
+ for record in records:
155
+ key = surface_of(record)
156
+ current = newest.get(key)
157
+ if current is None or record.get("captured_at", "") >= current.get(
158
+ "captured_at", ""):
159
+ newest[key] = record
160
+ out: dict[str, list[dict]] = {}
161
+ for record in newest.values():
162
+ out.setdefault(record["unit_key"], []).append(record)
163
+ return out
164
+
165
+
166
+ def derive(present_by_unit: dict[str, set[str]],
167
+ paths_by_unit: dict[str, dict[str, str]],
168
+ *, scope: dict, threshold: float = DEFAULT_THRESHOLD,
169
+ floor: int = DEFAULT_FLOOR,
170
+ window: tuple[str, str] | None = None) -> dict:
171
+ """Build the `fleet-baseline/1`. Raises `BaselineError` below the floor."""
172
+ total = len(present_by_unit)
173
+ if total == 0:
174
+ raise BaselineError(
175
+ "the scope selected no units at all. A baseline over an empty "
176
+ "cohort is not a weak baseline, it is no measurement")
177
+ if total < floor:
178
+ raise BaselineError(
179
+ f"the scope selected {total} unit(s) and the floor is {floor}. A "
180
+ f"baseline of {total} is an anecdote wearing a format key: at that "
181
+ f"size one unlucky machine moves every ratio past the threshold. "
182
+ f"Raise the cohort, or lower the floor explicitly with --floor")
183
+ if not 0.0 < threshold <= 1.0:
184
+ raise BaselineError(
185
+ f"--present-threshold is {threshold}, outside (0, 1]")
186
+
187
+ counts: dict[str, int] = {}
188
+ for names in present_by_unit.values():
189
+ for name in names:
190
+ counts[name] = counts.get(name, 0) + 1
191
+
192
+ sensors = []
193
+ for name in sorted(counts):
194
+ ratio = counts[name] / total
195
+ if ratio < threshold:
196
+ continue
197
+ entry: dict = {"name": name, "present_ratio": round(ratio, 6)}
198
+ # **Only when the contributing units agree.** A baseline that asserts
199
+ # one URI while the cohort reports several is asserting something no
200
+ # unit said. Matching is by name regardless; this key is advisory, and
201
+ # advisory metadata that is wrong is worse than absent.
202
+ seen = {paths[name] for paths in paths_by_unit.values() if name in paths}
203
+ if len(seen) == 1:
204
+ entry["uri_suffix"] = seen.pop()
205
+ sensors.append(entry)
206
+
207
+ derived: dict = {"units": total, "present_threshold": threshold}
208
+ if window is not None:
209
+ derived["captured_between"] = list(window)
210
+ return {
211
+ "format": BASELINE_FORMAT,
212
+ "scope": scope,
213
+ "derived": derived,
214
+ "sensors": sensors,
215
+ "provenance": PROVENANCE_DERIVED,
216
+ "notice": DOWNGRADE_NOTICE,
217
+ }
218
+
219
+
220
+ def expected_names(baseline: dict) -> set[str]:
221
+ return {sensor["name"] for sensor in baseline.get("sensors", [])}
222
+
223
+
224
+ def derivation_line(baseline: dict) -> str:
225
+ """The one line every consumer prints above a judgment made with this.
226
+
227
+ Denominator, window and threshold, because a ratio without its predicate is
228
+ a number somebody will quote.
229
+ """
230
+ derived = baseline.get("derived", {})
231
+ window = derived.get("captured_between")
232
+ when = f", captured between {window[0]} and {window[1]}" if window else ""
233
+ return (f"derived from {derived.get('units')} unit(s) at a presence "
234
+ f"threshold of {derived.get('present_threshold')}{when}")