-
Notifications
You must be signed in to change notification settings - Fork 17
/
_base_types.py
132 lines (102 loc) · 3.99 KB
/
_base_types.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# License: MIT
# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
"""Timeseries basic types."""
import functools
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Callable, Iterator, Self, overload
UNIX_EPOCH = datetime.fromtimestamp(0.0, tz=timezone.utc)
"""The UNIX epoch (in UTC)."""
@dataclass(frozen=True, order=True)
class Sample:
"""A measurement taken at a particular point in time.
The `value` could be `None` if a component is malfunctioning or data is
lacking for another reason, but a sample still needs to be sent to have a
coherent view on a group of component metrics for a particular timestamp.
"""
timestamp: datetime
"""The time when this sample was generated."""
value: float | None = None
"""The value of this sample."""
@dataclass(frozen=True)
class Sample3Phase:
"""A 3-phase measurement made at a particular point in time.
Each of the `value` fields could be `None` if a component is malfunctioning
or data is lacking for another reason, but a sample still needs to be sent
to have a coherent view on a group of component metrics for a particular
timestamp.
"""
timestamp: datetime
"""The time when this sample was generated."""
value_p1: float | None
"""The value of the 1st phase in this sample."""
value_p2: float | None
"""The value of the 2nd phase in this sample."""
value_p3: float | None
"""The value of the 3rd phase in this sample."""
def __iter__(self) -> Iterator[float | None]:
"""Return an iterator that yields values from each of the phases.
Yields:
Per-phase measurements one-by-one.
"""
yield self.value_p1
yield self.value_p2
yield self.value_p3
@overload
def max(self, default: float) -> float:
...
@overload
def max(self, default: None = None) -> float | None:
...
def max(self, default: float | None = None) -> float | None:
"""Return the max value among all phases, or default if they are all `None`.
Args:
default: value to return if all phases are `None`.
Returns:
Max value among all phases, if available, default value otherwise.
"""
if not any(self):
return default
value: float = functools.reduce(
lambda x, y: x if x > y else y,
filter(None, self),
)
return value
@overload
def min(self, default: float) -> float:
...
@overload
def min(self, default: None = None) -> float | None:
...
def min(self, default: float | None = None) -> float | None:
"""Return the min value among all phases, or default if they are all `None`.
Args:
default: value to return if all phases are `None`.
Returns:
Min value among all phases, if available, default value otherwise.
"""
if not any(self):
return default
value: float = functools.reduce(
lambda x, y: x if x < y else y,
filter(None, self),
)
return value
def map(
self, function: Callable[[float], float], default: float | None = None
) -> Self:
"""Apply the given function on each of the phase values and return the result.
If a phase value is `None`, replace it with `default` instead.
Args:
function: The function to apply on each of the phase values.
default: The value to apply if a phase value is `None`.
Returns:
A new instance, with the given function applied on values for each of the
phases.
"""
return self.__class__(
timestamp=self.timestamp,
value_p1=default if self.value_p1 is None else function(self.value_p1),
value_p2=default if self.value_p2 is None else function(self.value_p2),
value_p3=default if self.value_p3 is None else function(self.value_p3),
)