From f95a23e0e5c23b7ec13e63501b6ab6df540cb695 Mon Sep 17 00:00:00 2001 From: Wolfgang Müller Date: Sun, 2 May 2021 12:38:56 +0200 Subject: Handle timezones correctly Quassel uses UTC message timestamps in its database, but does not save any timezone information along with them. Up until now, we were reading those timestamps from the database naively - resulting in datetime objects that could not be identified as UTC. The same happened with timestamps we got from dateutil.isoparse. If the user did not specify an offset explicitly, the timestamp would be parsed and passed to the program "as is", effectively being interpreted as UTC because they were compared to database timestamps. This commit will ensure that the correct timezone is saved for every datetime object we encounter. Timestamps from the database are marked as UTC. If the user does not explicitly specify an offset, the timestamp is assumed to be in local time. Furthermore, when printing out message timestamps, make sure to convert them to the user's local timezone first. --- quarg/database/tables.py | 20 ++++++++++++++++++-- quarg/quassel/formatter.py | 4 +++- quarg/utils.py | 4 ++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/quarg/database/tables.py b/quarg/database/tables.py index 10e6057..743ffa2 100644 --- a/quarg/database/tables.py +++ b/quarg/database/tables.py @@ -1,10 +1,26 @@ +import datetime + from sqlalchemy.schema import Column, ForeignKey -from sqlalchemy.types import BigInteger, Boolean, DateTime, Integer, Text +from sqlalchemy.types import BigInteger, Boolean, DateTime, Integer, Text, TypeDecorator from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship # pylint: disable=too-few-public-methods +# Timestamps are saved in the database in UTC without timezone info, so attach +# a UTC timezone to the datetime object +class DateTimeUTC(TypeDecorator): + # pylint complains that process_{bind,literal}_param and python_type are + # abstract but not overriden. This seems to not be necessary with + # SQLAlchemy, so squash those warnings + # pylint: disable=abstract-method + impl = DateTime + + def process_result_value(self, value, dialect): + if value is not None: + value = value.replace(tzinfo=datetime.timezone.utc) + return value + Base = declarative_base() # Note: We have commented out unused columns to keep SQLAlchemy from selecting @@ -13,7 +29,7 @@ Base = declarative_base() class Backlog(Base): __tablename__ = 'backlog' messageid = Column(BigInteger, primary_key=True) - time = Column(DateTime) + time = Column(DateTimeUTC) bufferid = Column(Integer, ForeignKey('buffer.bufferid')) type = Column(Integer) flags = Column(Integer) diff --git a/quarg/quassel/formatter.py b/quarg/quassel/formatter.py index b6235e8..51eec00 100644 --- a/quarg/quassel/formatter.py +++ b/quarg/quassel/formatter.py @@ -33,7 +33,9 @@ class Message(NamedTuple): def format_from(backlog_row): message = Message.from_backlog(backlog_row) formatter = FORMATTERS[message.type] - timestamp = message.time.isoformat(sep=' ', timespec='seconds') + + # make sure to convert timestamps to local time before printing + timestamp = message.time.astimezone().isoformat(sep=' ', timespec='seconds') return f'{timestamp}\t{message.buffer}\t{formatter(message)}' diff --git a/quarg/utils.py b/quarg/utils.py index 7fa270e..903c057 100644 --- a/quarg/utils.py +++ b/quarg/utils.py @@ -12,4 +12,8 @@ def parse_isodate(date): except OverflowError as err: errx(f'isoparse: date overflows: \'{date}\'') + # If no offset is given, assume local time + if parsed.tzinfo is None: + parsed = parsed.astimezone() + return parsed -- cgit v1.2.3-2-gb3c3