Skip to content

Commit 0973670

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 9c0b280 commit 0973670

File tree

7 files changed

+78
-64
lines changed

7 files changed

+78
-64
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: 10 additions & 9 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
@@ -63,7 +64,7 @@ class MovingWindow:
6364
resampled_data_recv=resampled_data_recv,
6465
)
6566
66-
time_start = datetime.now()
67+
time_start = datetime.now(tz=timezone.utc)
6768
time_end = time_start + timedelta(minutes=5)
6869
6970
# ... wait for 5 minutes until the buffer is filled
@@ -88,8 +89,8 @@ class MovingWindow:
8889
asyncio.sleep(60*60*24)
8990
9091
# create a polars series with one full day of data
91-
time_start = datetime(2023, 1, 1)
92-
time_end = datetime(2023, 1, 2)
92+
time_start = datetime(2023, 1, 1, tzinfo=timezone.utc)
93+
time_end = datetime(2023, 1, 2, tzinfo=timezone.utc)
9394
s = pl.Series("Jan_1", mv[time_start:time_end])
9495
```
9596
"""
@@ -100,7 +101,7 @@ def __init__( # pylint: disable=too-many-arguments
100101
resampled_data_recv: Receiver[Sample],
101102
input_sampling_period: timedelta,
102103
resampler_config: ResamplerConfig | None = None,
103-
window_alignment: datetime = datetime(1, 1, 1),
104+
window_alignment: datetime = datetime(1, 1, 1, tzinfo=timezone.utc),
104105
) -> None:
105106
"""
106107
Initialize the MovingWindow.
@@ -117,7 +118,7 @@ def __init__( # pylint: disable=too-many-arguments
117118
resampler_config: The resampler configuration in case resampling is required.
118119
window_alignment: A datetime object that defines a point in time to which
119120
the window is aligned to modulo window size.
120-
(default is midnight 01.01.01)
121+
(default is midnight 01.01.01 UTC)
121122
For further information, consult the class level documentation.
122123
123124
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,7 +72,7 @@ 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
7878

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)