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
« 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
5from core.auxiliary.services.result_series_identity import (
6 ResultSeriesIndexedItemPayload,
7)
8from pydantic import BaseModel, ConfigDict, Field, RootModel, field_validator
11class HistogramBinSelection(StrEnum):
12 auto = "auto"
13 ten = "10"
14 twenty = "20"
15 thirty = "30"
16 fifty = "50"
19class ResultHistogramOptions(BaseModel):
20 model_config = ConfigDict(extra="forbid")
22 histogram_bins: HistogramBinSelection = HistogramBinSelection.auto
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
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)
37class ResultHistogramBinOverride(ResultHistogramOptions):
38 key: str
41class ResultSummaryRequest(BaseModel):
42 model_config = ConfigDict(extra="forbid")
44 scenario_id: int
45 simulation_object_id: int
46 histogram_bin_counts: list[ResultHistogramBinOverride] = Field(
47 default_factory=list
48 )
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 ]
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()
81class ResultStats(BaseModel):
82 model_config = ConfigDict(extra="forbid")
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
94class ResultMetricMatchKind(StrEnum):
95 exact = "exact"
96 closest = "closest"
97 first = "first"
100class ResultMetricRowTarget(BaseModel):
101 model_config = ConfigDict(extra="forbid")
103 row_index: int
104 value: float
105 target_value: float
106 match_count: int
107 match_kind: ResultMetricMatchKind
110class ResultMetricTargets(BaseModel):
111 model_config = ConfigDict(extra="forbid")
113 first: ResultMetricRowTarget | None
114 mean: ResultMetricRowTarget | None
115 median: ResultMetricRowTarget | None
116 mode: ResultMetricRowTarget | None
117 min: ResultMetricRowTarget | None
118 max: ResultMetricRowTarget | None
121class HistogramBin(BaseModel):
122 model_config = ConfigDict(extra="forbid")
124 label: str
125 start: float
126 end: float
127 count: int
130class ResultSummary(BaseModel):
131 model_config = ConfigDict(extra="forbid")
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]
143class ResultSummariesResponse(RootModel[list[ResultSummary]]):
144 """List response wrapper so OpenAPI keeps result summaries typed as an array."""