Coverage for mcp/metric_readers/aggregate.py: 100%

23 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-15 15:07 +0000

1"""Reduce a sequence of observed values down to one number. 

2 

3Several metric sources carry history rather than a single point: a training 

4run logs a loss every step, a CSV column holds one value per row, a log tail 

5matches a pattern many times. A threshold check, though, wants exactly one 

6number. This module bridges that gap with a single pure function, 

7:func:`reduce_sequence`, that collapses a sequence into one finite number 

8according to a caller-chosen mode. 

9 

10The supported modes are: 

11 

12* ``last`` — the most recent number (the last one in the original order). 

13* ``first`` — the earliest number (the first one in the original order). 

14* ``min`` — the smallest number. 

15* ``max`` — the largest number. 

16* ``mean`` — the arithmetic mean of every number. 

17 

18Non-numeric entries (booleans, NaN, the infinities, strings, ``None``, 

19containers) are never counted: every mode reduces only the values that pass 

20the numeric guard, so ``last`` and ``first`` mean "the most recent / earliest 

21*number*", skipping anything in between that is not one. 

22 

23The reducer distinguishes two empty-ish failures on purpose. A sequence that 

24carried no entries at all fails with :attr:`~.shape.ErrorCode.EMPTY_SEQUENCE`; 

25a sequence that had entries but none that were numbers fails with 

26:attr:`~.shape.ErrorCode.NO_NUMERIC_VALUE`. Keeping them apart lets a caller 

27tell "the source was empty" from "the source had data but none of it was a 

28number". 

29""" 

30 

31from __future__ import annotations 

32 

33from collections.abc import Sequence 

34from typing import Literal, cast 

35 

36from . import shape 

37 

38# The aggregation modes a caller may ask for. Kept as both a typing Literal 

39# (for signatures) and a runtime frozenset (for membership checks). 

40AggregationMode = Literal["last", "first", "min", "max", "mean"] 

41VALID_MODES: frozenset[str] = frozenset({"last", "first", "min", "max", "mean"}) 

42 

43 

44def reduce_sequence(values: Sequence[object], mode: str) -> float: 

45 """Collapse ``values`` to a single finite number using ``mode``. 

46 

47 The reduction proceeds in fixed order so the failure reported is always 

48 the most specific one that applies: 

49 

50 1. An unrecognized ``mode`` raises 

51 :class:`~.shape.MetricReaderError` with code 

52 :attr:`~.shape.ErrorCode.INVALID_AGGREGATION_MODE`. 

53 2. A sequence with no entries at all raises 

54 :attr:`~.shape.ErrorCode.EMPTY_SEQUENCE`. 

55 3. Entries that are not real, finite numbers are dropped (booleans, NaN, 

56 the infinities, strings, ``None``, containers — anything 

57 :func:`~.shape.is_numeric_value` rejects). 

58 4. If nothing survives the filter, raise 

59 :attr:`~.shape.ErrorCode.NO_NUMERIC_VALUE`. 

60 5. Otherwise reduce the surviving numbers: ``last`` and ``first`` pick the 

61 most recent / earliest survivor in the original order; ``min``, 

62 ``max``, and ``mean`` reduce across all of them. 

63 

64 The returned value is always a real, finite number. 

65 """ 

66 if mode not in VALID_MODES: 

67 raise shape.MetricReaderError( 

68 shape.ErrorCode.INVALID_AGGREGATION_MODE, 

69 {"mode": mode, "valid_modes": sorted(VALID_MODES)}, 

70 ) 

71 

72 if len(values) == 0: 

73 raise shape.MetricReaderError( 

74 shape.ErrorCode.EMPTY_SEQUENCE, 

75 {"mode": mode}, 

76 ) 

77 

78 # Keep only real, finite numbers, preserving their original order so 

79 # ``last`` and ``first`` stay meaningful. 

80 numeric: list[float] = [cast(float, v) for v in values if shape.is_numeric_value(v)] 

81 

82 if not numeric: 

83 raise shape.MetricReaderError( 

84 shape.ErrorCode.NO_NUMERIC_VALUE, 

85 {"mode": mode, "entry_count": len(values)}, 

86 ) 

87 

88 if mode == "last": 

89 return numeric[-1] 

90 if mode == "first": 

91 return numeric[0] 

92 if mode == "min": 

93 return min(numeric) 

94 if mode == "max": 

95 return max(numeric) 

96 # mode == "mean": membership in VALID_MODES guarantees this is the only 

97 # remaining possibility. 

98 return sum(numeric) / len(numeric)