Skip to content

Commit 4295bbd

Browse files
Add timezone to datetime types
There were inconsistencies in the way the SDK set datetime variables in the MovingWindow/RingBuffer without tz/tzinfo and in the Resampler with tz/tzinfo. This patch sets the timezone UTC for all datetime variables set in the MovingWindow and RingBuffer to make them consistent to the datetime variables set in the Resampler. Signed-off-by: Daniel Zullo <[email protected]>
1 parent a50fb26 commit 4295bbd

File tree

7 files changed

+79
-65
lines changed

7 files changed

+79
-65
lines changed

benchmarks/timeseries/benchmark_ringbuffer.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import random
77
import timeit
8-
from datetime import datetime, timedelta
8+
from datetime import datetime, timedelta, timezone
99
from typing import Any, Dict, TypeVar
1010

1111
import numpy as np
@@ -23,7 +23,7 @@
2323
def fill_buffer(days: int, buffer: OrderedRingBuffer[Any]) -> None:
2424
"""Fill the given buffer up to the given amount of days, one sample per minute."""
2525
random.seed(0)
26-
basetime = datetime(2022, 1, 1)
26+
basetime = datetime(2022, 1, 1, tzinfo=timezone.utc)
2727
print("..filling", end="", flush=True)
2828

2929
for day in range(days):
@@ -36,7 +36,7 @@ def fill_buffer(days: int, buffer: OrderedRingBuffer[Any]) -> None:
3636

3737
def test_days(days: int, buffer: OrderedRingBuffer[Any]) -> None:
3838
"""Gets the data for each of the 29 days."""
39-
basetime = datetime(2022, 1, 1)
39+
basetime = datetime(2022, 1, 1, tzinfo=timezone.utc)
4040

4141
for day in range(days):
4242
# pylint: disable=unused-variable
@@ -51,7 +51,7 @@ def test_slices(days: int, buffer: OrderedRingBuffer[Any], median: bool) -> None
5151
Takes a buffer, fills it up and then excessively gets
5252
the data for each day to calculate the average/median.
5353
"""
54-
basetime = datetime(2022, 1, 1)
54+
basetime = datetime(2022, 1, 1, tzinfo=timezone.utc)
5555

5656
total = 0.0
5757

src/frequenz/sdk/timeseries/_moving_window.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import logging
1010
import math
1111
from collections.abc import Sequence
12-
from datetime import datetime, timedelta
12+
from datetime import datetime, timedelta, timezone
1313
from typing import SupportsIndex, overload
1414

1515
import numpy as np
@@ -41,9 +41,10 @@ class MovingWindow:
4141
the point in time that defines the alignment can be outside of the time window.
4242
Modulo arithmetic is used to move the `window_alignment` timestamp into the
4343
latest window.
44-
If for example the `window_alignment` parameter is set to `datetime(1, 1, 1)`
45-
and the window size is bigger than one day then the first element will always
46-
be aligned to the midnight. For further information see also the
44+
If for example the `window_alignment` parameter is set to
45+
`datetime(1, 1, 1, tzinfo=timezone.utc)` and the window size is bigger than
46+
one day then the first element will always be aligned to the midnight.
47+
For further information see also the
4748
[`OrderedRingBuffer`][frequenz.sdk.timeseries._ringbuffer.OrderedRingBuffer]
4849
documentation.
4950
@@ -67,7 +68,7 @@ class MovingWindow:
6768
input_sampling_period=timedelta(seconds=1)
6869
)
6970
70-
time_start = datetime.now()
71+
time_start = datetime.now(tz=timezone.utc)
7172
time_end = time_start + timedelta(minutes=5)
7273
7374
# ... wait for 5 minutes until the buffer is filled
@@ -96,8 +97,8 @@ class MovingWindow:
9697
asyncio.sleep(60*60*24)
9798
9899
# create a polars series with one full day of data
99-
time_start = datetime(2023, 1, 1)
100-
time_end = datetime(2023, 1, 2)
100+
time_start = datetime(2023, 1, 1, tzinfo=timezone.utc)
101+
time_end = datetime(2023, 1, 2, tzinfo=timezone.utc)
101102
s = pl.Series("Jan_1", mv[time_start:time_end])
102103
```
103104
"""
@@ -108,7 +109,7 @@ def __init__( # pylint: disable=too-many-arguments
108109
resampled_data_recv: Receiver[Sample],
109110
input_sampling_period: timedelta,
110111
resampler_config: ResamplerConfig | None = None,
111-
window_alignment: datetime = datetime(1, 1, 1),
112+
window_alignment: datetime = datetime(1, 1, 1, tzinfo=timezone.utc),
112113
) -> None:
113114
"""
114115
Initialize the MovingWindow.
@@ -121,7 +122,7 @@ def __init__( # pylint: disable=too-many-arguments
121122
different sources at different sampling rates or periods, and it can be
122123
set by specifying the output sampling period parameter so that the user
123124
can control the granularity of the samples to be stored in the
124-
underlying ringbuffer.
125+
underlying buffer.
125126
126127
If resampling is not required, the output sampling period parameter
127128
can be set to None or be equal to the input sampling period in which
@@ -135,7 +136,7 @@ def __init__( # pylint: disable=too-many-arguments
135136
resampler_config: The resampler configuration in case resampling is required.
136137
window_alignment: A datetime object that defines a point in time to which
137138
the window is aligned to modulo window size.
138-
(default is midnight 01.01.01)
139+
(default is midnight 01.01.01 UTC)
139140
For further information, consult the class level documentation.
140141
141142
Raises:

src/frequenz/sdk/timeseries/_ringbuffer.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from collections.abc import Iterable
99
from copy import deepcopy
1010
from dataclasses import dataclass
11-
from datetime import datetime, timedelta
11+
from datetime import datetime, timedelta, timezone
1212
from typing import Generic, List, SupportsFloat, SupportsIndex, TypeVar, overload
1313

1414
import numpy as np
@@ -50,7 +50,7 @@ def __init__(
5050
self,
5151
buffer: FloatArray,
5252
sampling_period: timedelta,
53-
time_index_alignment: datetime = datetime(1, 1, 1),
53+
time_index_alignment: datetime = datetime(1, 1, 1, tzinfo=timezone.utc),
5454
) -> None:
5555
"""Initialize the time aware ringbuffer.
5656
@@ -76,8 +76,8 @@ def __init__(
7676
self._time_index_alignment: datetime = time_index_alignment
7777

7878
self._gaps: list[Gap] = []
79-
self._datetime_newest: datetime = datetime.min
80-
self._datetime_oldest: datetime = datetime.max
79+
self._datetime_newest: datetime = datetime.min.replace(tzinfo=timezone.utc)
80+
self._datetime_oldest: datetime = datetime.max.replace(tzinfo=timezone.utc)
8181
self._time_range: timedelta = (len(self._buffer) - 1) * sampling_period
8282

8383
@property
@@ -130,7 +130,10 @@ def update(self, sample: Sample) -> None:
130130
timestamp = self._normalize_timestamp(sample.timestamp)
131131

132132
# Don't add outdated entries
133-
if timestamp < self._datetime_oldest and self._datetime_oldest != datetime.max:
133+
if (
134+
timestamp < self._datetime_oldest
135+
and self._datetime_oldest != datetime.max.replace(tzinfo=timezone.utc)
136+
):
134137
raise IndexError(
135138
f"Timestamp {timestamp} too old (cut-off is at {self._datetime_oldest})."
136139
)
@@ -485,7 +488,7 @@ def __len__(self) -> int:
485488
Returns:
486489
The length.
487490
"""
488-
if self._datetime_newest == datetime.min:
491+
if self._datetime_newest == datetime.min.replace(tzinfo=timezone.utc):
489492
return 0
490493

491494
start_index = self.datetime_to_index(self._datetime_oldest)

src/frequenz/sdk/timeseries/_serializable_ringbuffer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from __future__ import annotations
88

99
import pickle
10-
from datetime import datetime, timedelta
10+
from datetime import datetime, timedelta, timezone
1111
from os.path import exists
1212

1313
from ._ringbuffer import FloatArray, OrderedRingBuffer
@@ -25,7 +25,7 @@ def __init__(
2525
buffer: FloatArray,
2626
sampling_period: timedelta,
2727
path: str,
28-
time_index_alignment: datetime = datetime(1, 1, 1),
28+
time_index_alignment: datetime = datetime(1, 1, 1, tzinfo=timezone.utc),
2929
) -> None:
3030
"""Initialize the time aware ringbuffer.
3131

tests/timeseries/test_moving_window.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"""Tests for the moving window."""
55

66
import asyncio
7-
from datetime import datetime, timedelta
7+
from datetime import datetime, timedelta, timezone
88
from typing import Sequence, Tuple
99

1010
import numpy as np
@@ -23,7 +23,7 @@ async def push_lm_data(sender: Sender[Sample], test_seq: Sequence[float]) -> Non
2323
sender: Sender for pushing resampled samples to the `MovingWindow`.
2424
test_seq: The Sequence that is pushed into the `MovingWindow`.
2525
"""
26-
start_ts: datetime = datetime(2023, 1, 1)
26+
start_ts: datetime = datetime(2023, 1, 1, tzinfo=timezone.utc)
2727
for i, j in zip(test_seq, range(0, len(test_seq))):
2828
timestamp = start_ts + timedelta(seconds=j)
2929
await sender.send(Sample(timestamp, float(i)))
@@ -58,7 +58,7 @@ async def test_access_window_by_timestamp() -> None:
5858
"""Test indexing a window by timestamp"""
5959
window, sender = init_moving_window(timedelta(seconds=1))
6060
await push_lm_data(sender, [1])
61-
assert np.array_equal(window[datetime(2023, 1, 1)], 1.0)
61+
assert np.array_equal(window[datetime(2023, 1, 1, tzinfo=timezone.utc)], 1.0)
6262

6363

6464
async def test_access_window_by_int_slice() -> None:
@@ -72,6 +72,6 @@ async def test_access_window_by_ts_slice() -> None:
7272
"""Test accessing a subwindow with a timestamp slice"""
7373
window, sender = init_moving_window(timedelta(seconds=5))
7474
await push_lm_data(sender, range(0, 5))
75-
time_start = datetime(2023, 1, 1) + timedelta(seconds=3)
75+
time_start = datetime(2023, 1, 1, tzinfo=timezone.utc) + timedelta(seconds=3)
7676
time_end = time_start + timedelta(seconds=2)
7777
assert np.array_equal(window[time_start:time_end], np.array([3.0, 4.0])) # type: ignore

tests/timeseries/test_ringbuffer.py

Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from __future__ import annotations
77

88
import random
9-
from datetime import datetime, timedelta
9+
from datetime import datetime, timedelta, timezone
1010
from itertools import cycle, islice
1111
from typing import Any
1212

@@ -30,7 +30,9 @@
3030
np.empty(shape=(24 * 1800,), dtype=np.float64),
3131
TWO_HUNDRED_MS,
3232
),
33-
OrderedRingBuffer([0.0] * 1800, TWO_HUNDRED_MS, datetime(2000, 1, 1)),
33+
OrderedRingBuffer(
34+
[0.0] * 1800, TWO_HUNDRED_MS, datetime(2000, 1, 1, tzinfo=timezone.utc)
35+
),
3436
],
3537
)
3638
def test_timestamp_ringbuffer(buffer: OrderedRingBuffer[Any]) -> None:
@@ -46,14 +48,16 @@ def test_timestamp_ringbuffer(buffer: OrderedRingBuffer[Any]) -> None:
4648
# Push in random order
4749
# for i in random.sample(range(size), size):
4850
for i in range(size):
49-
buffer.update(Sample(datetime.fromtimestamp(200 + i * resolution), i))
51+
buffer.update(
52+
Sample(datetime.fromtimestamp(200 + i * resolution, tz=timezone.utc), i)
53+
)
5054

5155
# Check all possible window sizes and start positions
5256
for i in range(0, size, 1000):
5357
for j in range(1, size - i, 987):
5458
assert i + j < size
55-
start = datetime.fromtimestamp(200 + i * resolution)
56-
end = datetime.fromtimestamp(200 + (j + i) * resolution)
59+
start = datetime.fromtimestamp(200 + i * resolution, tz=timezone.utc)
60+
end = datetime.fromtimestamp(200 + (j + i) * resolution, tz=timezone.utc)
5761

5862
tmp = list(islice(cycle(range(0, size)), i, i + j))
5963
assert list(buffer.window(start, end)) == list(tmp)
@@ -74,17 +78,17 @@ def test_timestamp_ringbuffer_overwrite(buffer: OrderedRingBuffer[Any]) -> None:
7478

7579
# Push in random order
7680
for i in random.sample(range(size), size):
77-
buffer.update(Sample(datetime.fromtimestamp(200 + i), i))
81+
buffer.update(Sample(datetime.fromtimestamp(200 + i, tz=timezone.utc), i))
7882

7983
# Push the same amount twice
8084
for i in random.sample(range(size), size):
81-
buffer.update(Sample(datetime.fromtimestamp(200 + i), i * 2))
85+
buffer.update(Sample(datetime.fromtimestamp(200 + i, tz=timezone.utc), i * 2))
8286

8387
# Check all possible window sizes and start positions
8488
for i in range(size):
8589
for j in range(1, size - i):
86-
start = datetime.fromtimestamp(200 + i)
87-
end = datetime.fromtimestamp(200 + j + i)
90+
start = datetime.fromtimestamp(200 + i, tz=timezone.utc)
91+
end = datetime.fromtimestamp(200 + j + i, tz=timezone.utc)
8892

8993
tmp = islice(cycle(range(0, size * 2, 2)), i, i + j)
9094
actual: float
@@ -110,28 +114,28 @@ def test_timestamp_ringbuffer_gaps(
110114

111115
# Add initial data
112116
for i in random.sample(range(size), size):
113-
buffer.update(Sample(datetime.fromtimestamp(200 + i), i))
117+
buffer.update(Sample(datetime.fromtimestamp(200 + i, tz=timezone.utc), i))
114118

115119
# Request window of the data
116120
buffer.window(
117-
datetime.fromtimestamp(200),
118-
datetime.fromtimestamp(202),
121+
datetime.fromtimestamp(200, tz=timezone.utc),
122+
datetime.fromtimestamp(202, tz=timezone.utc),
119123
)
120124

121125
# Add entry far in the future
122-
buffer.update(Sample(datetime.fromtimestamp(500 + size), 9999))
126+
buffer.update(Sample(datetime.fromtimestamp(500 + size, tz=timezone.utc), 9999))
123127

124128
# Expect exception for the same window
125129
with pytest.raises(IndexError):
126130
buffer.window(
127-
datetime.fromtimestamp(200),
128-
datetime.fromtimestamp(202),
131+
datetime.fromtimestamp(200, tz=timezone.utc),
132+
datetime.fromtimestamp(202, tz=timezone.utc),
129133
)
130134

131135
# Receive new window without exception
132136
buffer.window(
133-
datetime.fromtimestamp(501),
134-
datetime.fromtimestamp(500 + size),
137+
datetime.fromtimestamp(501, tz=timezone.utc),
138+
datetime.fromtimestamp(500 + size, tz=timezone.utc),
135139
)
136140

137141

@@ -149,30 +153,30 @@ def test_timestamp_ringbuffer_missing_parameter(
149153
buffer: OrderedRingBuffer[Any],
150154
) -> None:
151155
"""Test ordered ring buffer."""
152-
buffer.update(Sample(datetime(2, 2, 2, 0, 0), 0))
156+
buffer.update(Sample(datetime(2, 2, 2, 0, 0, tzinfo=timezone.utc), 0))
153157

154158
# pylint: disable=protected-access
155159
assert buffer._normalize_timestamp(buffer.gaps[0].start) == buffer.gaps[0].start
156160

157161
# Expecting one gap now, made of all the previous entries of the one just
158162
# added.
159163
assert len(buffer.gaps) == 1
160-
assert buffer.gaps[0].end == datetime(2, 2, 2)
164+
assert buffer.gaps[0].end == datetime(2, 2, 2, tzinfo=timezone.utc)
161165

162166
# Add entry so that a second gap appears
163167
# pylint: disable=protected-access
164-
assert buffer._normalize_timestamp(datetime(2, 2, 2, 0, 7, 31)) == datetime(
165-
2, 2, 2, 0, 10
166-
)
167-
buffer.update(Sample(datetime(2, 2, 2, 0, 7, 31), 0))
168+
assert buffer._normalize_timestamp(
169+
datetime(2, 2, 2, 0, 7, 31, tzinfo=timezone.utc)
170+
) == datetime(2, 2, 2, 0, 10, tzinfo=timezone.utc)
171+
buffer.update(Sample(datetime(2, 2, 2, 0, 7, 31, tzinfo=timezone.utc), 0))
168172

169173
assert buffer.datetime_to_index(
170-
datetime(2, 2, 2, 0, 7, 31)
171-
) == buffer.datetime_to_index(datetime(2, 2, 2, 0, 10))
174+
datetime(2, 2, 2, 0, 7, 31, tzinfo=timezone.utc)
175+
) == buffer.datetime_to_index(datetime(2, 2, 2, 0, 10, tzinfo=timezone.utc))
172176
assert len(buffer.gaps) == 2
173177

174178
# import pdb; pdb.set_trace()
175-
buffer.update(Sample(datetime(2, 2, 2, 0, 5), 0))
179+
buffer.update(Sample(datetime(2, 2, 2, 0, 5, tzinfo=timezone.utc), 0))
176180
assert len(buffer.gaps) == 1
177181

178182

@@ -213,7 +217,8 @@ def test_timestamp_ringbuffer_missing_parameter_smoke(
213217
buffer.update(
214218
Sample(
215219
datetime.fromtimestamp(
216-
200 + j * buffer.sampling_period.total_seconds()
220+
200 + j * buffer.sampling_period.total_seconds(),
221+
tz=timezone.utc,
217222
),
218223
None if missing else j,
219224
)
@@ -224,11 +229,11 @@ def test_timestamp_ringbuffer_missing_parameter_smoke(
224229
lambda x: Gap(
225230
# pylint: disable=protected-access
226231
start=buffer._normalize_timestamp(
227-
datetime.fromtimestamp(200 + x[0] * resolution)
232+
datetime.fromtimestamp(200 + x[0] * resolution, tz=timezone.utc)
228233
),
229234
# pylint: disable=protected-access
230235
end=buffer._normalize_timestamp(
231-
datetime.fromtimestamp(200 + x[1] * resolution)
236+
datetime.fromtimestamp(200 + x[1] * resolution, tz=timezone.utc)
232237
),
233238
),
234239
expected_gaps_concrete,
@@ -261,10 +266,10 @@ def test_len_ringbuffer_samples_fit_buffer_size() -> None:
261266
buffer = OrderedRingBuffer(
262267
np.empty(shape=len(test_samples), dtype=float),
263268
sampling_period=timedelta(seconds=1),
264-
time_index_alignment=datetime(1, 1, 1),
269+
time_index_alignment=datetime(1, 1, 1, tzinfo=timezone.utc),
265270
)
266271

267-
start_ts: datetime = datetime(2023, 1, 1)
272+
start_ts: datetime = datetime(2023, 1, 1, tzinfo=timezone.utc)
268273
for index, sample_value in enumerate(test_samples):
269274
timestamp = start_ts + timedelta(seconds=index)
270275
buffer.update(Sample(timestamp, float(sample_value)))
@@ -289,10 +294,10 @@ def test_len_ringbuffer_samples_overwrite_buffer() -> None:
289294
buffer = OrderedRingBuffer(
290295
np.empty(shape=half_buffer_size, dtype=float),
291296
sampling_period=timedelta(seconds=1),
292-
time_index_alignment=datetime(1, 1, 1),
297+
time_index_alignment=datetime(1, 1, 1, tzinfo=timezone.utc),
293298
)
294299

295-
start_ts: datetime = datetime(2023, 1, 1)
300+
start_ts: datetime = datetime(2023, 1, 1, tzinfo=timezone.utc)
296301
for index, sample_value in enumerate(test_samples):
297302
timestamp = start_ts + timedelta(seconds=index)
298303
buffer.update(Sample(timestamp, float(sample_value)))

0 commit comments

Comments
 (0)