🐛 A proper cross-version solution for the utcnow() issue.

No need to maintain two different pieces of logic - a `utcnow()` for
Python < 3.11 and `now(datetime.UTC)` for Python >= 3.11.

`datetime.timezone.utc` existed long before datetime.UTC and that's what
the `utcnow` facade should use.

This means that all the `utcnow()` will always have `tzinfo=UTC`
regardless of the Python version.

There's still a problem with the `utcnow()`-generated timestamps that
have been generated by previous versions of Python and stored on the db.

Therefore, when the code performs comparisons with timestamps fetched
from the db, it should always explicitly do a `.replace(tzinfo=utc)` to
ensure that we always compare offset-aware datetime representations.

See blog post for technical details:
https://manganiello.blog/wheres-my-time-again
This commit is contained in:
Fabio Manganiello 2024-06-01 01:34:47 +02:00
parent 1067ab04d9
commit c9a5c29a4a
Signed by untrusted user: blacklight
GPG key ID: D90FBA7F76362774
7 changed files with 19 additions and 21 deletions

View file

@ -1,10 +1,8 @@
from datetime import timedelta
import logging
from threading import Event
from time import time
from typing import Collection, Optional
from platypush.utils import utcnow
from ._base import (
Entity,
EntityKey,
@ -47,8 +45,8 @@ def get_entities_engine(timeout: Optional[float] = None) -> EntitiesEngine:
:param timeout: Timeout in seconds (default: None).
"""
time_start = utcnow()
while not timeout or (utcnow() - time_start < timedelta(seconds=timeout)):
time_start = time()
while not timeout or (time() - time_start < timeout):
if _engine:
break

View file

@ -1,4 +1,4 @@
from datetime import timedelta
from datetime import timedelta, timezone
from logging import getLogger
from queue import Queue
from typing import Callable, Collection, Dict, Final, List, Optional, Type
@ -99,7 +99,8 @@ event_matchers: Dict[
)
and (
not (old and old.updated_at)
or utcnow() - old.updated_at >= timedelta(seconds=_rssi_update_interval)
or utcnow() - old.updated_at.replace(tzinfo=timezone.utc)
>= timedelta(seconds=_rssi_update_interval)
)
),
}

View file

@ -97,8 +97,7 @@ class CalendarIcalPlugin(Plugin, CalendarInterface):
if (
event['status'] != 'cancelled'
and event['end'].get('dateTime')
and event['end']['dateTime']
>= utcnow().replace(tzinfo=datetime.timezone.utc).isoformat()
and event['end']['dateTime'] >= utcnow().isoformat()
and (
(
only_participating

View file

@ -71,7 +71,7 @@ class GoogleCalendarPlugin(GooglePlugin, CalendarInterface):
:meth:`platypush.plugins.calendar.CalendarPlugin.get_upcoming_events`.
"""
now = utcnow().isoformat() + 'Z'
now = utcnow().replace(tzinfo=None).isoformat() + 'Z'
service = self.get_service('calendar', 'v3')
result = (
service.events()

View file

@ -68,7 +68,7 @@ class SunPlugin(RunnablePlugin):
dt = datetime.datetime.strptime(
f'{now.year}-{now.month:02d}-{now.day:02d} {t}',
'%Y-%m-%d %I:%M:%S %p',
).replace(tzinfo=datetime.UTC)
).replace(tzinfo=datetime.timezone.utc)
if dt < now:
dt += datetime.timedelta(days=1)

View file

@ -115,9 +115,13 @@ class UserManager:
.first()
)
if not user_session or (
user_session.expires_at and user_session.expires_at < utcnow()
):
expires_at = (
user_session.expires_at.replace(tzinfo=datetime.timezone.utc)
if user_session and user_session.expires_at
else None
)
if not user_session or (expires_at and expires_at < utcnow()):
return None, None
user = session.query(User).filter_by(user_id=user_session.user_id).first()

View file

@ -816,14 +816,10 @@ def wait_for_either(*events, timeout: Optional[float] = None, cls: Type = Event)
def utcnow():
"""
A workaround util to maintain compatibility both with Python >= 3.12 (which
deprecated datetime.utcnow) and Python < 3.12 (which doesn't have
datetime.UTC).
utcnow() without tears. It always returns a datetime object in UTC
timezone.
"""
if hasattr(datetime, 'UTC'):
return datetime.datetime.now(datetime.UTC)
return datetime.datetime.utcnow()
return datetime.datetime.now(datetime.timezone.utc)
# vim:sw=4:ts=4:et: