Skip to content

Commit e28159b

Browse files
committed
Handle prices with UTC timestamps in the raw data for non-FI regions
1 parent 4e0bc07 commit e28159b

4 files changed

Lines changed: 1301 additions & 15 deletions

File tree

spothinta_api/models.py

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
from dataclasses import dataclass
6-
from datetime import datetime, timedelta
6+
from datetime import datetime, timedelta, timezone
77
from typing import TYPE_CHECKING, Any
88

99
if TYPE_CHECKING:
@@ -257,16 +257,33 @@ def prices_today(self) -> dict[datetime, float]:
257257
The prices for today.
258258
259259
"""
260-
today = self.now_in_timezone().astimezone().date()
260+
today = self.now_in_timezone().date()
261261
prices = {
262262
timestamp: price
263263
for timestamp, price in self.prices.items()
264-
if timestamp.date() == today
264+
if timestamp.astimezone(self.time_zone).date() == today
265265
}
266266

267-
if (self.resolution == timedelta(minutes=15) and len(prices) == 96) or (
268-
self.resolution == timedelta(minutes=60) and len(prices) == 24
269-
):
267+
# Calculate expected intervals accounting for DST transitions.
268+
# On DST transition days, local time spans may be 23 or 25 hours,
269+
# not 24, due to the shifted/repeated hour. We count UTC hours that
270+
# correspond to the local date to handle DST correctly.
271+
day_start = datetime.combine(today, datetime.min.time(), tzinfo=self.time_zone)
272+
day_start_utc = day_start.astimezone(timezone.utc)
273+
274+
# Count UTC hours that fall within this local date
275+
hour_count = 0
276+
current_utc = day_start_utc
277+
while current_utc.astimezone(self.time_zone).date() == today:
278+
hour_count += 1
279+
current_utc = current_utc + timedelta(hours=1)
280+
281+
expected_intervals = max(
282+
1,
283+
int((hour_count * timedelta(hours=1)) / self.resolution),
284+
)
285+
286+
if len(prices) == expected_intervals:
270287
return prices
271288

272289
return {}
@@ -279,16 +296,37 @@ def prices_tomorrow(self) -> dict[datetime, float]:
279296
The prices for tomorrow.
280297
281298
"""
282-
tomorrow = (self.now_in_timezone() + timedelta(days=1)).astimezone().date()
299+
tomorrow = (self.now_in_timezone() + timedelta(days=1)).date()
283300
prices = {
284301
timestamp: price
285302
for timestamp, price in self.prices.items()
286-
if timestamp.date() == tomorrow
303+
if timestamp.astimezone(self.time_zone).date() == tomorrow
287304
}
288305

289-
if (self.resolution == timedelta(minutes=15) and len(prices) == 96) or (
290-
self.resolution == timedelta(minutes=60) and len(prices) == 24
291-
):
306+
# Calculate expected intervals accounting for DST transitions.
307+
# On DST transition days, local time spans may be 23 or 25 hours,
308+
# not 24, due to the shifted/repeated hour. We count UTC hours that
309+
# correspond to the local date to handle DST correctly.
310+
day_start = datetime.combine(
311+
tomorrow,
312+
datetime.min.time(),
313+
tzinfo=self.time_zone,
314+
)
315+
day_start_utc = day_start.astimezone(timezone.utc)
316+
317+
# Count UTC hours that fall within this local date
318+
hour_count = 0
319+
current_utc = day_start_utc
320+
while current_utc.astimezone(self.time_zone).date() == tomorrow:
321+
hour_count += 1
322+
current_utc = current_utc + timedelta(hours=1)
323+
324+
expected_intervals = max(
325+
1,
326+
int((hour_count * timedelta(hours=1)) / self.resolution),
327+
)
328+
329+
if len(prices) == expected_intervals:
292330
return prices
293331

294332
return {}

0 commit comments

Comments
 (0)