Coverage for backend/django/core/auxiliary/services/result_summary/contracts.py: 94%

95 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-07-22 05:22 +0000

1from enum import StrEnum 

2import json 

3from math import ceil, sqrt 

4 

5from core.auxiliary.services.result_series_identity import ( 

6 ResultSeriesIndexedItemPayload, 

7) 

8from pydantic import BaseModel, ConfigDict, Field, RootModel, field_validator 

9 

10 

11class HistogramBinSelection(StrEnum): 

12 auto = "auto" 

13 ten = "10" 

14 twenty = "20" 

15 thirty = "30" 

16 fifty = "50" 

17 

18 

19class ResultHistogramOptions(BaseModel): 

20 model_config = ConfigDict(extra="forbid") 

21 

22 histogram_bins: HistogramBinSelection = HistogramBinSelection.auto 

23 

24 @field_validator("histogram_bins", mode="before") 

25 @classmethod 

26 def default_empty_histogram_bins(cls, value: str | None) -> str: 

27 if value is None or value == "": 27 ↛ 28line 27 didn't jump to line 28 because the condition on line 27 was never true

28 return HistogramBinSelection.auto.value 

29 return value 

30 

31 def bin_count_for(self, finite_value_count: int) -> int: 

32 if self.histogram_bins == HistogramBinSelection.auto: 

33 return min(20, ceil(sqrt(finite_value_count))) 

34 return int(self.histogram_bins) 

35 

36 

37class ResultHistogramBinOverride(ResultHistogramOptions): 

38 key: str 

39 

40 

41class ResultSummaryRequest(BaseModel): 

42 model_config = ConfigDict(extra="forbid") 

43 

44 scenario_id: int 

45 simulation_object_id: int 

46 histogram_bin_counts: list[ResultHistogramBinOverride] = Field( 

47 default_factory=list 

48 ) 

49 

50 @field_validator("histogram_bin_counts", mode="before") 

51 @classmethod 

52 def parse_histogram_bin_counts( 

53 cls, 

54 value: str | list[ResultHistogramBinOverride] | None, 

55 ) -> list[ResultHistogramBinOverride]: 

56 if value is None or value == "": 

57 return [] 

58 if isinstance(value, str): 

59 try: 

60 value = json.loads(value) 

61 except json.JSONDecodeError as error: 

62 raise ValueError( 

63 "histogram_bin_counts must be valid JSON" 

64 ) from error 

65 if not isinstance(value, list): 65 ↛ 66line 65 didn't jump to line 66 because the condition on line 65 was never true

66 raise ValueError("histogram_bin_counts must be a list") 

67 return [ 

68 item 

69 if isinstance(item, ResultHistogramBinOverride) 

70 else ResultHistogramBinOverride.model_validate(item) 

71 for item in value 

72 ] 

73 

74 def histogram_options_for(self, key: str) -> ResultHistogramOptions: 

75 for override in reversed(self.histogram_bin_counts): 

76 if override.key == key: 76 ↛ 75line 76 didn't jump to line 75 because the condition on line 76 was always true

77 return override 

78 return ResultHistogramOptions() 

79 

80 

81class ResultStats(BaseModel): 

82 model_config = ConfigDict(extra="forbid") 

83 

84 count: int 

85 omitted_count: int 

86 mean: float | None 

87 median: float | None 

88 mode: float | None 

89 mode_label: str 

90 min: float | None 

91 max: float | None 

92 

93 

94class ResultMetricMatchKind(StrEnum): 

95 exact = "exact" 

96 closest = "closest" 

97 first = "first" 

98 

99 

100class ResultMetricRowTarget(BaseModel): 

101 model_config = ConfigDict(extra="forbid") 

102 

103 row_index: int 

104 value: float 

105 target_value: float 

106 match_count: int 

107 match_kind: ResultMetricMatchKind 

108 

109 

110class ResultMetricTargets(BaseModel): 

111 model_config = ConfigDict(extra="forbid") 

112 

113 first: ResultMetricRowTarget | None 

114 mean: ResultMetricRowTarget | None 

115 median: ResultMetricRowTarget | None 

116 mode: ResultMetricRowTarget | None 

117 min: ResultMetricRowTarget | None 

118 max: ResultMetricRowTarget | None 

119 

120 

121class HistogramBin(BaseModel): 

122 model_config = ConfigDict(extra="forbid") 

123 

124 label: str 

125 start: float 

126 end: float 

127 count: int 

128 

129 

130class ResultSummary(BaseModel): 

131 model_config = ConfigDict(extra="forbid") 

132 

133 key: str 

134 label: str 

135 unit: str | None 

136 property_value_id: int 

137 indexed_items: list[ResultSeriesIndexedItemPayload] 

138 stats: ResultStats 

139 metric_targets: ResultMetricTargets 

140 histogram_bins: list[HistogramBin] 

141 

142 

143class ResultSummariesResponse(RootModel[list[ResultSummary]]): 

144 """List response wrapper so OpenAPI keeps result summaries typed as an array."""