Skip to content

Remove all uses of utcfromtimestamp #3236

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions listenbrainz/db/external_service_oauth.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from datetime import datetime
from datetime import datetime, timezone
from typing import List, Optional, Union

from sqlalchemy import text

from data.model.external_service import ExternalServiceType
from listenbrainz import db, utils
import sqlalchemy


Expand Down Expand Up @@ -32,7 +31,7 @@ def save_token(db_conn, user_id: int, service: ExternalServiceType, access_token
# to use the new values. any column which does not have a new value to be set should
# be explicitly set to the default value (which would have been used if the row was
# inserted instead).
token_expires = utils.unix_timestamp_to_datetime(token_expires_ts) if token_expires_ts else None
token_expires = datetime.fromtimestamp(token_expires_ts, timezone.utc) if token_expires_ts else None
result = db_conn.execute(sqlalchemy.text("""
INSERT INTO external_service_oauth AS eso
(user_id, external_user_id, service, access_token, refresh_token, token_expires, scopes)
Expand Down Expand Up @@ -121,7 +120,7 @@ def update_token(db_conn, user_id: int, service: ExternalServiceType, access_tok
refresh_token: the new token used to refresh access tokens, if omitted the old token in the database remains unchanged
expires_at: the unix timestamp at which the access token expires
"""
token_expires = utils.unix_timestamp_to_datetime(expires_at)
token_expires = datetime.fromtimestamp(expires_at, timezone.utc)
params = {
"access_token": access_token,
"token_expires": token_expires,
Expand Down
7 changes: 3 additions & 4 deletions listenbrainz/db/listens_importer.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import datetime
from datetime import datetime, timezone
from typing import Optional, Union

from sqlalchemy import text

from data.model.external_service import ExternalServiceType
from listenbrainz import utils
import sqlalchemy


Expand Down Expand Up @@ -51,12 +50,12 @@ def update_latest_listened_at(db_conn, user_id: int, service: ExternalServiceTyp
"""), {
'user_id': user_id,
'service': service.value,
'timestamp': utils.unix_timestamp_to_datetime(timestamp),
'timestamp': datetime.fromtimestamp(timestamp, timezone.utc),
})
db_conn.commit()


def get_latest_listened_at(db_conn, user_id: int, service: ExternalServiceType) -> Optional[datetime.datetime]:
def get_latest_listened_at(db_conn, user_id: int, service: ExternalServiceType) -> Optional[datetime]:
""" Returns the timestamp of the last listen imported for the user with
specified LB user ID from the given service.

Expand Down
7 changes: 3 additions & 4 deletions listenbrainz/db/pinned_recording.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import sqlalchemy
from datetime import datetime
from datetime import datetime, timezone

from listenbrainz import db
from listenbrainz.db.model.pinned_recording import PinnedRecording, WritablePinnedRecording
from typing import List, Iterable

Expand Down Expand Up @@ -223,8 +222,8 @@ def get_pins_for_feed(db_conn, user_ids: Iterable[int], min_ts: int, max_ts: int
LIMIT :count
""".format(columns=','.join(PINNED_REC_GET_COLUMNS))), {
"user_ids": tuple(user_ids),
"min_ts": datetime.utcfromtimestamp(min_ts),
"max_ts": datetime.utcfromtimestamp(max_ts),
"min_ts": datetime.fromtimestamp(min_ts, timezone.utc),
"max_ts": datetime.fromtimestamp(max_ts, timezone.utc),
"count": count,
})
return [PinnedRecording(**row) for row in result.mappings()]
Expand Down
6 changes: 3 additions & 3 deletions listenbrainz/db/user_relationship.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

from datetime import datetime
from datetime import datetime, timezone
from typing import List, Iterable

import sqlalchemy
Expand Down Expand Up @@ -158,8 +158,8 @@ def get_follow_events(db_conn, user_ids: Iterable[int], min_ts: float, max_ts: f
LIMIT :count
"""), {
"user_ids": tuple(user_ids),
"min_ts": datetime.utcfromtimestamp(min_ts),
"max_ts": datetime.utcfromtimestamp(max_ts),
"min_ts": datetime.fromtimestamp(min_ts, timezone.utc),
"max_ts": datetime.fromtimestamp(max_ts, timezone.utc),
"count": count
})

Expand Down
17 changes: 8 additions & 9 deletions listenbrainz/db/user_timeline_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import sqlalchemy
import orjson

from datetime import datetime
from datetime import datetime, timezone

from sqlalchemy import text

Expand All @@ -34,9 +34,8 @@
HiddenUserTimelineEvent,
PersonalRecordingRecommendationMetadata, WritePersonalRecordingRecommendationMetadata
)
from listenbrainz import db
from listenbrainz.db.exceptions import DatabaseException
from typing import List, Tuple, Iterable
from typing import List, Iterable

from listenbrainz.db.model.review import CBReviewTimelineMetadata

Expand Down Expand Up @@ -196,8 +195,8 @@ def get_user_timeline_events(
LIMIT :count
"""), {
"user_ids": tuple(user_ids),
"min_ts": datetime.utcfromtimestamp(min_ts),
"max_ts": datetime.utcfromtimestamp(max_ts),
"min_ts": datetime.fromtimestamp(min_ts, timezone.utc),
"max_ts": datetime.fromtimestamp(max_ts, timezone.utc),
"event_type": event_type.value,
"count": count,
})
Expand Down Expand Up @@ -255,8 +254,8 @@ def get_personal_recommendation_events_for_feed(db_conn, user_id: int, min_ts: i
LIMIT :count
"""), {
"user_id": user_id,
"min_ts": datetime.utcfromtimestamp(min_ts),
"max_ts": datetime.utcfromtimestamp(max_ts),
"min_ts": datetime.fromtimestamp(min_ts, timezone.utc),
"max_ts": datetime.fromtimestamp(max_ts, timezone.utc),
"count": count,
"event_type": UserTimelineEventType.PERSONAL_RECORDING_RECOMMENDATION.value,
})
Expand Down Expand Up @@ -297,8 +296,8 @@ def get_thanks_events_for_feed(db_conn, user_id: int, min_ts: int, max_ts: int,
LIMIT :count
"""), {
"user_id": user_id,
"min_ts": datetime.utcfromtimestamp(min_ts),
"max_ts": datetime.utcfromtimestamp(max_ts),
"min_ts": datetime.fromtimestamp(min_ts, timezone.utc),
"max_ts": datetime.fromtimestamp(max_ts, timezone.utc),
"count": count,
"event_type": UserTimelineEventType.THANKS.value,
})
Expand Down
12 changes: 5 additions & 7 deletions listenbrainz/listen.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# coding=utf-8
import calendar
from copy import deepcopy
from datetime import datetime
from datetime import datetime, timezone

import orjson

Expand Down Expand Up @@ -76,7 +74,7 @@ def __init__(self, user_id=None, user_name=None, timestamp=None, recording_msid=
# determine the type of timestamp and do the right thing
if isinstance(timestamp, int) or isinstance(timestamp, float):
self.ts_since_epoch = int(timestamp)
self.timestamp = datetime.utcfromtimestamp(self.ts_since_epoch)
self.timestamp = datetime.fromtimestamp(self.ts_since_epoch, timezone.utc)
else:
if timestamp:
self.timestamp = timestamp
Expand Down Expand Up @@ -105,12 +103,12 @@ def from_json(cls, j):
"""Factory to make Listen() objects from a dict"""
# Let's go play whack-a-mole with our lovely whicket of timestamp fields. Hopefully one will work!
try:
j['listened_at'] = datetime.utcfromtimestamp(float(j['listened_at']))
j['listened_at'] = datetime.fromtimestamp(float(j['listened_at']), timezone.utc)
except KeyError:
try:
j['listened_at'] = datetime.utcfromtimestamp(float(j['timestamp']))
j['listened_at'] = datetime.fromtimestamp(float(j['timestamp']), timezone.utc)
except KeyError:
j['listened_at'] = datetime.utcfromtimestamp(float(j['ts_since_epoch']))
j['listened_at'] = datetime.fromtimestamp(float(j['ts_since_epoch']), timezone.utc)

return cls(
user_id=j.get('user_id'),
Expand Down
46 changes: 31 additions & 15 deletions listenbrainz/listenstore/tests/test_timescale_utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime
from datetime import datetime, timezone

from sqlalchemy import text

Expand Down Expand Up @@ -35,8 +35,8 @@ def _get_count_and_timestamp(self, user):
row = result.fetchone()
return {
"count": row.count,
"min_listened_at": row.min_listened_at.replace(tzinfo=None),
"max_listened_at": row.max_listened_at.replace(tzinfo=None)
"min_listened_at": row.min_listened_at,
"max_listened_at": row.max_listened_at
}

def test_delete_listens_update_metadata(self):
Expand All @@ -49,35 +49,51 @@ def test_delete_listens_update_metadata(self):
update_user_listen_data()

metadata_1 = self._get_count_and_timestamp(user_1)
self.assertEqual(metadata_1["min_listened_at"], datetime.utcfromtimestamp(1400000000))
self.assertEqual(metadata_1["max_listened_at"], datetime.utcfromtimestamp(1400000200))
self.assertEqual(metadata_1["min_listened_at"], datetime.fromtimestamp(1400000000, timezone.utc))
self.assertEqual(metadata_1["max_listened_at"], datetime.fromtimestamp(1400000200, timezone.utc))
self.assertEqual(metadata_1["count"], 5)

metadata_2 = self._get_count_and_timestamp(user_2)
self.assertEqual(metadata_2["min_listened_at"], datetime.utcfromtimestamp(1400000000))
self.assertEqual(metadata_2["max_listened_at"], datetime.utcfromtimestamp(1400000200))
self.assertEqual(metadata_2["min_listened_at"], datetime.fromtimestamp(1400000000, timezone.utc))
self.assertEqual(metadata_2["max_listened_at"], datetime.fromtimestamp(1400000200, timezone.utc))
self.assertEqual(metadata_2["count"], 5)

# to test the case when the update script has not run since delete, so metadata in listen_user_metadata does
# account for this listen and deleting should not affect it either.
self._create_test_data(user_1, "timescale_listenstore_test_listens_2.json")
self.ls.delete_listen(datetime.utcfromtimestamp(1400000500), user_1["id"], "4269ddbc-9241-46da-935d-4fa9e0f7f371")
self.ls.delete_listen(
datetime.fromtimestamp(1400000500, timezone.utc),
user_1["id"],
"4269ddbc-9241-46da-935d-4fa9e0f7f371"
)

# test min_listened_at is updated if that listen is deleted for a user
self.ls.delete_listen(datetime.utcfromtimestamp(1400000000), user_1["id"], "4269ddbc-9241-46da-935d-4fa9e0f7f371")
self.ls.delete_listen(
datetime.fromtimestamp(1400000000, timezone.utc),
user_1["id"],
"4269ddbc-9241-46da-935d-4fa9e0f7f371"
)
# test max_listened_at is updated if that listen is deleted for a user
self.ls.delete_listen(datetime.utcfromtimestamp(1400000200), user_1["id"], "db072fa7-0c7f-4f55-b90f-a88da531b219")
self.ls.delete_listen(
datetime.fromtimestamp(1400000200, timezone.utc),
user_1["id"],
"db072fa7-0c7f-4f55-b90f-a88da531b219"
)
# test normal listen delete updates correctly
self.ls.delete_listen(datetime.utcfromtimestamp(1400000100), user_2["id"], "08ade1eb-800e-4ad8-8184-32941664ac02")
self.ls.delete_listen(
datetime.fromtimestamp(1400000100, timezone.utc),
user_2["id"],
"08ade1eb-800e-4ad8-8184-32941664ac02"
)

delete_listens()

metadata_1 = self._get_count_and_timestamp(user_1)
self.assertEqual(metadata_1["min_listened_at"], datetime.utcfromtimestamp(1400000050))
self.assertEqual(metadata_1["max_listened_at"], datetime.utcfromtimestamp(1400000150))
self.assertEqual(metadata_1["min_listened_at"], datetime.fromtimestamp(1400000050, timezone.utc))
self.assertEqual(metadata_1["max_listened_at"], datetime.fromtimestamp(1400000150, timezone.utc))
self.assertEqual(metadata_1["count"], 3)

metadata_2 = self._get_count_and_timestamp(user_2)
self.assertEqual(metadata_2["min_listened_at"], datetime.utcfromtimestamp(1400000000))
self.assertEqual(metadata_2["max_listened_at"], datetime.utcfromtimestamp(1400000200))
self.assertEqual(metadata_2["min_listened_at"], datetime.fromtimestamp(1400000000, timezone.utc))
self.assertEqual(metadata_2["max_listened_at"], datetime.fromtimestamp(1400000200, timezone.utc))
self.assertEqual(metadata_2["count"], 4)
9 changes: 0 additions & 9 deletions listenbrainz/tests/unit/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,9 @@
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

import listenbrainz.utils as utils
import time
import unittest
import uuid

from datetime import datetime
from listenbrainz.webserver import create_app
from listenbrainz.webserver.views.api_tools import is_valid_uuid

Expand All @@ -31,12 +28,6 @@ class ListenBrainzUtilsTestCase(unittest.TestCase):
def setUp(self):
self.app = create_app(debug=True) # create an app for config value access

def test_unix_timestamp_to_datetime(self):
t = int(time.time())
x = utils.unix_timestamp_to_datetime(t)
self.assertIsInstance(x, datetime)
self.assertEqual(int(x.strftime('%s')), t)

def test_valid_uuid(self):
self.assertTrue(is_valid_uuid(str(uuid.uuid4())))
self.assertFalse(is_valid_uuid('hjjkghjk'))
Expand Down
6 changes: 2 additions & 4 deletions listenbrainz/tests/utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
# coding=utf-8

from datetime import datetime, date
from datetime import datetime, timezone
import time
import sys
import os

from listenbrainz.listen import Listen
import uuid
Expand All @@ -21,7 +19,7 @@ def generate_data(db_conn, from_date, num_records, user_name):
item = Listen(
user_id=user['id'],
user_name=user_name,
timestamp=datetime.utcfromtimestamp(current_date),
timestamp=datetime.fromtimestamp(current_date, timezone.utc),
recording_msid=str(uuid.uuid4()),
data={
'artist_name': 'Test Artist Pls ignore',
Expand Down
12 changes: 0 additions & 12 deletions listenbrainz/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,6 @@ def create_channel_to_consume(connection, exchange: str, queue: str, callback_fu
return ch


def unix_timestamp_to_datetime(timestamp):
""" Converts expires_at timestamp received from Spotify to a datetime object

Args:
timestamp (int): the unix timestamp to be converted to datetime

Returns:
A datetime object with timezone UTC corresponding to the provided timestamp
"""
return datetime.utcfromtimestamp(timestamp).replace(tzinfo=timezone.utc)


def get_fallback_connection_name():
""" Get a connection name friendlier than docker gateway ip during connecting
to services like redis, rabbitmq etc."""
Expand Down
2 changes: 1 addition & 1 deletion listenbrainz/webserver/views/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ def delete_listen():
if "listened_at" not in data:
log_raise_400("Listen timestamp missing.")
try:
listened_at = datetime.utcfromtimestamp(int(data["listened_at"]))
listened_at = datetime.fromtimestamp(int(data["listened_at"]), timezone.utc)
except ValueError:
log_raise_400("%s: Listen timestamp invalid." % data["listened_at"])

Expand Down
18 changes: 9 additions & 9 deletions listenbrainz/webserver/views/user_timeline_event_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

import time
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from typing import List, Dict, Iterable

import pydantic
Expand Down Expand Up @@ -838,14 +838,14 @@ def get_listen_events(
# if neither is set, use current time as max_ts and subtract window
# length to get min_ts.
if min_ts and max_ts:
min_ts = datetime.utcfromtimestamp(min_ts)
max_ts = datetime.utcfromtimestamp(max_ts)
min_ts = datetime.fromtimestamp(min_ts, timezone.utc)
max_ts = datetime.fromtimestamp(max_ts, timezone.utc)
else:
if min_ts:
min_ts = datetime.utcfromtimestamp(min_ts)
min_ts = datetime.fromtimestamp(min_ts, timezone.utc)
max_ts = min_ts + DEFAULT_LISTEN_EVENT_WINDOW
elif max_ts:
max_ts = datetime.utcfromtimestamp(max_ts)
max_ts = datetime.fromtimestamp(max_ts, timezone.utc)
min_ts = max_ts - DEFAULT_LISTEN_EVENT_WINDOW
else:
max_ts = datetime.utcnow()
Expand Down Expand Up @@ -892,19 +892,19 @@ def get_all_listen_events(
# if neither is set, use current time as max_ts and subtract window
# length to get min_ts.
if min_ts and max_ts:
temp = datetime.utcfromtimestamp(min_ts)
max_ts = datetime.utcfromtimestamp(max_ts)
temp = datetime.fromtimestamp(min_ts, timezone.utc)
max_ts = datetime.fromtimestamp(max_ts, timezone.utc)
if max_ts - temp < DEFAULT_LISTEN_EVENT_WINDOW_NEW:
min_ts = temp
else:
# If the given interval for search is greater than :DEFAULT_LISTEN_EVENT_WINDOW_NEW:,
# then we must limit the search interval to :DEFAULT_LISTEN_EVENT_WINDOW_NEW:.
min_ts = max_ts - DEFAULT_LISTEN_EVENT_WINDOW_NEW
elif min_ts:
min_ts = datetime.utcfromtimestamp(min_ts)
min_ts = datetime.fromtimestamp(min_ts, timezone.utc)
max_ts = min_ts + DEFAULT_LISTEN_EVENT_WINDOW_NEW
elif max_ts:
max_ts = datetime.utcfromtimestamp(max_ts)
max_ts = datetime.fromtimestamp(max_ts, timezone.utc)
min_ts = max_ts - DEFAULT_LISTEN_EVENT_WINDOW_NEW
else:
max_ts = datetime.utcnow()
Expand Down