Skip to content

Commit c0bda36

Browse files
authored
Use timezone-aware datetimes everywhere (#237)
* Use library function * Use timezone-aware datetimes everywhere
1 parent 7e81098 commit c0bda36

File tree

10 files changed

+21
-50
lines changed

10 files changed

+21
-50
lines changed

prod-config.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ pretix_cache_file = "pretix_cache.json"
3131

3232
[program_notifications]
3333
# UTC offset in hours (e.g. 2 for CEST)
34-
timezone_offset = 2
3534
api_url = "https://static.europython.eu/programme/ep2025/releases/current/schedule.json"
3635
schedule_cache_file = "schedule_cache.json"
3736
livestream_url_file = "livestreams.toml"

src/europython_discord/program_notifications/cog.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ def __init__(self, bot: Client, config: ProgramNotificationsConfig) -> None:
2020
self.config = config
2121
self.program_connector = ProgramConnector(
2222
api_url=self.config.api_url,
23-
timezone_offset=self.config.timezone_offset,
2423
cache_file=self.config.schedule_cache_file,
2524
simulated_start_time=self.config.simulated_start_time,
2625
fast_mode=self.config.fast_mode,
Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
from __future__ import annotations
22

33
from collections.abc import Mapping
4-
from datetime import datetime
54
from pathlib import Path
65

7-
from pydantic import BaseModel
6+
from pydantic import AwareDatetime, BaseModel
87

98

109
class ProgramNotificationsConfig(BaseModel):
11-
timezone_offset: int
1210
api_url: str
1311
schedule_cache_file: Path
1412
livestream_url_file: Path
1513
main_notification_channel_name: str
1614
rooms_to_channel_names: Mapping[str, str]
1715

18-
simulated_start_time: datetime | None = None
16+
simulated_start_time: AwareDatetime | None = None
1917
fast_mode: bool = False

src/europython_discord/program_notifications/models.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from __future__ import annotations
22

3-
from datetime import date, datetime
3+
from datetime import date
44

5-
from pydantic import BaseModel
5+
from pydantic import AwareDatetime, BaseModel
66

77

88
class DaySchedule(BaseModel):
@@ -25,7 +25,7 @@ class Break(BaseModel):
2525
title: str
2626
duration: int
2727
rooms: list[str]
28-
start: datetime
28+
start: AwareDatetime
2929

3030

3131
class Session(BaseModel):
@@ -41,7 +41,7 @@ class Session(BaseModel):
4141
level: str
4242
track: str | None
4343
rooms: list[str]
44-
start: datetime
44+
start: AwareDatetime
4545
website_url: str
4646
duration: int
4747

src/europython_discord/program_notifications/program_connector.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import asyncio
44
import json
55
import logging
6-
from datetime import date, datetime, timedelta, timezone
6+
from datetime import UTC, date, datetime, timedelta
77
from pathlib import Path
88

99
import aiofiles
@@ -18,21 +18,19 @@ class ProgramConnector:
1818
def __init__(
1919
self,
2020
api_url: str,
21-
timezone_offset: int,
2221
cache_file: Path,
2322
simulated_start_time: datetime | None = None,
2423
*,
2524
fast_mode: bool = False,
2625
) -> None:
2726
self._api_url = api_url
28-
self._timezone = timezone(timedelta(hours=timezone_offset))
2927
self._cache_file = cache_file
3028

3129
# time travel parameters for testing
3230
self._simulated_start_time = simulated_start_time
3331
if self._simulated_start_time:
3432
self._time_multiplier = 60 if fast_mode else 1
35-
self._real_start_time = datetime.now(tz=self._timezone)
33+
self._real_start_time = datetime.now(tz=UTC)
3634

3735
self._fetch_lock = asyncio.Lock()
3836
self.sessions_by_day: dict[date, list[Session]] | None = None
@@ -102,11 +100,11 @@ async def _get_schedule_from_cache(self) -> dict[date, list[Session]] | None:
102100
async def _get_now(self) -> datetime:
103101
"""Get the current time in the conference timezone."""
104102
if self._simulated_start_time:
105-
elapsed = datetime.now(tz=self._timezone) - self._real_start_time
103+
elapsed = datetime.now(tz=UTC) - self._real_start_time
106104
simulated_now = self._simulated_start_time + elapsed * self._time_multiplier
107-
return simulated_now.astimezone(self._timezone)
105+
return simulated_now.astimezone(UTC)
108106

109-
return datetime.now(tz=self._timezone)
107+
return datetime.now(tz=UTC)
110108

111109
async def get_sessions_by_date(self, date_now: date) -> list[Session]:
112110
if self.sessions_by_day is None:

src/europython_discord/program_notifications/session_to_embed.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import Final
77

88
from discord import Embed
9+
from discord.utils import format_dt
910

1011
from europython_discord.program_notifications.models import Session, Speaker
1112

@@ -34,7 +35,7 @@ def create_session_embed(session: Session, livestream_url: str | None) -> Embed:
3435
color=_get_color(session.level),
3536
)
3637

37-
embed.add_field(name="Start Time", value=_format_start_time(session.start), inline=True)
38+
embed.add_field(name="Start Time", value=format_dt(session.start), inline=True)
3839
embed.add_field(name="Room", value=_format_room(session.rooms), inline=True)
3940
embed.add_field(name="Track", value=_format_track(session.track), inline=True)
4041
embed.add_field(name="Duration", value=_format_duration(session.duration), inline=True)
@@ -91,16 +92,6 @@ def _format_title(title: str) -> str:
9192
return textwrap.shorten(title, width=_TITLE_WIDTH)
9293

9394

94-
def _format_start_time(start_time: datetime) -> str:
95-
"""Format the start time to a Discord timestamp string.
96-
97-
:param start_time: The start time
98-
:return: A start time value for the embed.
99-
"""
100-
start_time_timestamp = int(start_time.timestamp())
101-
return f"<t:{start_time_timestamp}:f>"
102-
103-
10495
def _format_footer(start_time: datetime) -> str:
10596
"""Create a footer with the local conference time.
10697

src/europython_discord/registration/pretix_connector.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import logging
66
import time
77
from collections import defaultdict
8-
from datetime import datetime, timedelta, timezone
8+
from datetime import UTC, datetime, timedelta
99
from pathlib import Path
1010

1111
import aiofiles
@@ -57,7 +57,7 @@ async def fetch_pretix_data(self) -> None:
5757
# if called during an ongoing fetch, the caller waits until the fetch is done...
5858
async with self._fetch_lock:
5959
# ... but does not trigger a second fetch
60-
now = datetime.now(tz=timezone.utc)
60+
now = datetime.now(tz=UTC)
6161
if self._last_fetch and now - self._last_fetch < timedelta(minutes=2):
6262
_logger.info(f"Skipping pretix fetch (last fetch was at {self._last_fetch})")
6363
return

test-config.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ pretix_cache_file = "pretix_cache.json"
3131

3232
[program_notifications]
3333
# UTC offset in hours (e.g. 2 for CEST)
34-
timezone_offset = 2
3534
api_url = "https://static.europython.eu/programme/ep2025/releases/current/schedule.json"
3635
schedule_cache_file = "schedule_cache.json"
3736
livestream_url_file = "test-livestreams.toml"

tests/program_notifications/test_program_connector.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,7 @@ def cache_file(tmp_path):
2727

2828
@pytest.fixture
2929
async def program_connector(cache_file):
30-
return ProgramConnector(
31-
api_url="http://test.api/schedule", timezone_offset=0, cache_file=cache_file
32-
)
30+
return ProgramConnector(api_url="http://test.api/schedule", cache_file=cache_file)
3331

3432

3533
@pytest.fixture

tests/program_notifications/test_session_to_embed.py

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from datetime import UTC, datetime
1+
from datetime import datetime, timedelta, timezone
22

33
import pytest
44

@@ -126,10 +126,12 @@ def test_embed_color(session: Session, level: str, expected_color: int) -> None:
126126

127127
def test_embed_fields_start_time(session: Session) -> None:
128128
"""Test the 'Start Time' field of the embed."""
129+
start_time = datetime(2024, 1, 2, 3, 4, 5, tzinfo=timezone(timedelta(hours=6)))
130+
session = session.model_copy(update={"start": start_time})
131+
129132
embed = session_to_embed.create_session_embed(session, None)
130133
assert embed.fields[0].name == "Start Time"
131-
assert embed.fields[0].value.startswith("<t:")
132-
assert embed.fields[0].value.endswith(":f>")
134+
assert embed.fields[0].value == f"<t:{int(start_time.timestamp())}>"
133135

134136

135137
def test_embed_fields_room(session: Session) -> None:
@@ -252,19 +254,6 @@ def test_embed_footer(session: Session) -> None:
252254
assert embed.footer.text == "This session starts at 08:00:00 (local conference time)"
253255

254256

255-
def test_format_start_time(session: Session) -> None:
256-
"""Test the _format_start_time function."""
257-
formatted_start_time = session_to_embed._format_start_time(session.start)
258-
assert formatted_start_time.startswith("<t:")
259-
assert formatted_start_time.endswith(":f>")
260-
261-
# The following code assumes that the start time in the mock data is in UTC.
262-
datetime_obj = datetime.fromtimestamp(
263-
int(formatted_start_time.replace("<t:", "").replace(":f>", "")), tz=UTC
264-
)
265-
assert datetime_obj == session.start
266-
267-
268257
def test_format_duration(session: Session) -> None:
269258
"""Test the _format_duration function."""
270259
formatted_duration = session_to_embed._format_duration(session.duration)

0 commit comments

Comments
 (0)