From ec8f12f60c788b4ec39ff7b93b098843161f6402 Mon Sep 17 00:00:00 2001 From: Benjamin Cutler Date: Tue, 5 Dec 2023 17:59:11 -0700 Subject: [PATCH] remove explicit references to pytz [#184132800] --- .github/dependabot.yml | 23 ------------- setup.py | 4 +-- tests/apiv2/test_donations.py | 11 ++++--- tests/randgen.py | 9 +++--- tests/requirements.txt | 7 ++-- tests/test_api.py | 8 ++--- tests/test_consumers.py | 8 ++--- tests/test_delete_protection.py | 19 +++++++---- tests/test_donation.py | 11 ------- tests/test_donor.py | 6 ---- tests/test_event.py | 19 ++++++----- tests/test_prize.py | 15 ++++----- tests/test_search_filters.py | 7 ++-- tests/test_speedrun.py | 10 ++++-- tests/util.py | 26 +++++++++++---- tracker/admin/prize.py | 5 ++- tracker/consumers/ping.py | 3 +- .../commands/cache_giantbomb_info.py | 3 +- tracker/models/bid.py | 4 +-- tracker/models/donation.py | 4 +-- tracker/models/event.py | 32 ++++++++++++++----- tracker/models/prize.py | 25 +++++++++------ tracker/prizeutil.py | 3 +- tracker/search_feeds.py | 11 +++---- tracker/tasks.py | 7 ++-- tracker/util.py | 12 +++++-- tracker/views/donateviews.py | 2 -- 27 files changed, 147 insertions(+), 147 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b034f5ade..491deae0a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,26 +5,3 @@ updates: schedule: interval: daily open-pull-requests-limit: 10 - ignore: - - dependency-name: django - versions: - - ">= 3.a, < 4" - - dependency-name: pre-commit - versions: - - 2.11.0 - - 2.12.0 - - dependency-name: django-paypal - versions: - - "1.1" - - dependency-name: djangorestframework - versions: - - 3.12.3 - - dependency-name: responses - versions: - - 0.13.0 - - dependency-name: django-timezone-field - versions: - - 4.1.1 - - dependency-name: importlib-metadata - - dependency-name: pytz - - dependency-name: tzdata diff --git a/setup.py b/setup.py index c6bceed53..48fb4d302 100644 --- a/setup.py +++ b/setup.py @@ -49,6 +49,7 @@ def get_package_name(name): }, install_requires=[ 'backports.cached-property~=1.0.2;python_version<"3.8"', + 'backports.zoneinfo;python_version<"3.9"', 'celery~=5.0', 'channels>=2.0', 'Django>=3.2,!=4.0.*,<4.3', @@ -59,8 +60,7 @@ def get_package_name(name): 'django-post-office~=3.2', 'django-timezone-field>=3.1,<7.0', 'djangorestframework~=3.9', - 'python-dateutil~=2.8.1', - 'pytz>=2019.3', + 'python-dateutil~=2.8.1', # TODO: remove when 3.11 is oldest supported version 'requests>=2.27.1,<2.32.0', ], python_requires='>=3.7, <3.12', diff --git a/tests/apiv2/test_donations.py b/tests/apiv2/test_donations.py index f3b4adf1e..ad81fa241 100644 --- a/tests/apiv2/test_donations.py +++ b/tests/apiv2/test_donations.py @@ -1,7 +1,6 @@ import random from datetime import datetime, timedelta -import pytz from django.contrib.admin.models import CHANGE from django.contrib.auth.models import Permission, User from rest_framework.test import APIClient @@ -9,6 +8,7 @@ from tracker.api.serializers import DonationSerializer from tracker.api.views.donations import DONATION_CHANGE_LOG_MESSAGES from tracker.models import Donation, Event +from tracker.util import utcnow from .. import randgen from ..util import APITestCase @@ -30,7 +30,7 @@ def generate_donations( count=1, state: str, transactionstate='COMPLETED', - time: datetime = datetime.utcnow(), + time: datetime = None, ): commentstate = 'PENDING' readstate = 'PENDING' @@ -53,6 +53,9 @@ def generate_donations( commentstate = 'APPROVED' readstate = 'IGNORED' + if time is None: + time = utcnow() + donations = randgen.generate_donations( self.rand, event, @@ -112,7 +115,7 @@ def test_unprocessed_returns_only_after_timestamp(self): '/tracker/api/v2/donations/unprocessed/', { 'event_id': self.event.pk, - 'after': datetime.utcnow().astimezone(pytz.utc), + 'after': utcnow(), }, ) returned_ids = list(map(lambda d: d['id'], response.data)) @@ -183,7 +186,7 @@ def test_flagged_returns_only_after_timestamp(self): '/tracker/api/v2/donations/flagged/', { 'event_id': self.event.pk, - 'after': datetime.utcnow().astimezone(pytz.utc), + 'after': utcnow(), }, ) returned_ids = list(map(lambda d: d['id'], response.data)) diff --git a/tests/randgen.py b/tests/randgen.py index ba5285eaa..59e4564eb 100644 --- a/tests/randgen.py +++ b/tests/randgen.py @@ -4,8 +4,6 @@ import os from decimal import Decimal -import pytz - from tracker.models import ( Bid, Donation, @@ -21,6 +19,7 @@ SpeedRun, ) from tracker.models.donation import DonationDomainChoices, DonorVisibilityChoices +from tracker.util import utcnow def random_name(rand, base): @@ -105,7 +104,7 @@ def random_time(rand, start, end): result = start + datetime.timedelta( seconds=rand.randrange(int(delta.total_seconds())) ) - return result.astimezone(pytz.utc) + return result.astimezone(datetime.timezone.utc) def pick_random_from_queryset(rand, q): @@ -414,7 +413,7 @@ def generate_donation_for_prize( def generate_event(rand, start_time=None): event = Event() if not start_time: - start_time = datetime.datetime.utcnow().astimezone(pytz.utc) + start_time = utcnow() event.datetime = start_time event.name = random_event_name(rand) event.short = event.name @@ -649,7 +648,7 @@ def build_random_event( event = generate_event(rand, start_time=start_time) if not start_time: start_time = datetime.datetime.combine(event.date, datetime.time()).replace( - tzinfo=pytz.utc + tzinfo=datetime.timezone.utc ) event.save() diff --git a/tests/requirements.txt b/tests/requirements.txt index 18d9b7244..c761416f3 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -9,14 +9,13 @@ django-mptt==0.14.0 django-post-office==3.6.0 django-timezone-field==6.1.0 djangorestframework==3.14.0 +backports.zoneinfo==0.2.1 ; python_version<"3.9" +# TODO: can be removed when 3.11 is oldest supported version +python-dateutil==2.8.2 # can be removed when either 3.7 is not supported or once we upgrade Celery importlib_metadata==4.13.0 ; python_version<"3.8" pre-commit==3.5.0 ; python_version>="3.8" pre-commit==2.21.0 ; python_version<"3.8" -python-dateutil==2.8.2 -# lock these down until backports.zoneinfo is in use -pytz==2022.2.1 -tzdata==2022.2 webpack-manifest==2.1.1 # only for testing responses~=0.24.1 diff --git a/tests/test_api.py b/tests/test_api.py index bd2b52cd5..91798a13c 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,7 +1,7 @@ +import datetime import json from decimal import Decimal -import pytz from django.contrib.admin.models import ADDITION as LogEntryADDITION from django.contrib.admin.models import CHANGE as LogEntryCHANGE from django.contrib.admin.models import DELETION as LogEntryDELETION @@ -21,7 +21,7 @@ def format_time(dt): - return DjangoJSONEncoder().default(dt.astimezone(pytz.utc)) + return DjangoJSONEncoder().default(dt.astimezone(datetime.timezone.utc)) class TestGeneric(APITestCase): @@ -1022,9 +1022,7 @@ def add_event_fields(fields, event, prefix): except TypeError: pass fields[prefix + '__' + key] = value - fields[prefix + '__datetime'] = DjangoJSONEncoder().default( - event.datetime.astimezone(pytz.utc) - ) + fields[prefix + '__datetime'] = event.datetime fields[prefix + '__public'] = str(event) run_fields = {} diff --git a/tests/test_consumers.py b/tests/test_consumers.py index 6782c085c..b2ab4f65b 100644 --- a/tests/test_consumers.py +++ b/tests/test_consumers.py @@ -3,8 +3,6 @@ from decimal import Decimal from unittest import mock -import dateutil -import pytz from asgiref.sync import async_to_sync, sync_to_async from channels.testing import WebsocketCommunicator from django.contrib.auth.models import Permission, User @@ -15,6 +13,7 @@ from tracker.api.views.donations import DonationProcessingActionTypes from tracker.consumers import DonationConsumer, PingConsumer from tracker.consumers.processing import ProcessingConsumer, broadcast_processing_action +from tracker.util import utcnow from .util import today_noon @@ -29,9 +28,8 @@ async def test_ping_consumer(self): self.assertTrue(connected, 'Could not connect') await communicator.send_to(text_data='PING') result = await communicator.receive_from() - # TODO: python 3.7 has datetime.datetime.fromisoformat - date = dateutil.parser.parse(result) - now = datetime.datetime.now(pytz.utc) + date = datetime.datetime.fromisoformat(result) + now = utcnow() self.assertTrue( (date - now).total_seconds() < 5, msg=f'{date} and {now} differed by more than five seconds', diff --git a/tests/test_delete_protection.py b/tests/test_delete_protection.py index 4c32e1b81..fa3662baf 100644 --- a/tests/test_delete_protection.py +++ b/tests/test_delete_protection.py @@ -1,6 +1,5 @@ import datetime -import pytz from django.db.models import ProtectedError from django.test import TransactionTestCase @@ -12,7 +11,7 @@ def setUp(self): self.event = models.Event.objects.create( short='scratch', name='Scratch Event', - datetime=datetime.datetime(2000, 1, 1, 12, tzinfo=pytz.utc), + datetime=datetime.datetime(2000, 1, 1, 12, tzinfo=datetime.timezone.utc), targetamount=1000, ) @@ -54,8 +53,12 @@ def scratchPrizeTimed(self): name='Scratch Prize Timed', event=self.event, defaults=dict( - starttime=datetime.datetime(2000, 1, 1, 0, 0, 0, tzinfo=pytz.utc), - endtime=datetime.datetime(2000, 1, 1, 1, 0, 0, tzinfo=pytz.utc), + starttime=datetime.datetime( + 2000, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc + ), + endtime=datetime.datetime( + 2000, 1, 1, 1, 0, 0, tzinfo=datetime.timezone.utc + ), ), )[0] @@ -107,8 +110,12 @@ def scratchRun(self): name='Scratch Run', event=self.event, defaults=dict( - starttime=datetime.datetime(2000, 1, 1, 0, 0, 0, tzinfo=pytz.utc), - endtime=datetime.datetime(2000, 1, 1, 1, 0, 0, tzinfo=pytz.utc), + starttime=datetime.datetime( + 2000, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc + ), + endtime=datetime.datetime( + 2000, 1, 1, 1, 0, 0, tzinfo=datetime.timezone.utc + ), ), )[0] diff --git a/tests/test_donation.py b/tests/test_donation.py index 42df652aa..3a69aa632 100644 --- a/tests/test_donation.py +++ b/tests/test_donation.py @@ -8,7 +8,6 @@ from django.contrib.auth.models import Permission, User from django.test import TestCase, override_settings from django.urls import reverse -from django.utils import timezone from tracker import models @@ -26,7 +25,6 @@ def setUp(self): def test_anonymous(self): # Anonymous donation is anonymous donation = models.Donation( - timereceived=timezone.now(), amount=Decimal(1.5), domain='PAYPAL', requestedvisibility='ANON', @@ -36,7 +34,6 @@ def test_anonymous(self): # Donation from an anonymous donor with CURR is anonymous donation = models.Donation( - timereceived=timezone.now(), amount=Decimal(1.5), domain='PAYPAL', requestedvisibility='CURR', @@ -47,7 +44,6 @@ def test_anonymous(self): # Donation from a non-anonymous donor with CURR is not anonymous donation = models.Donation( - timereceived=timezone.now(), amount=Decimal(1.5), domain='PAYPAL', requestedvisibility='CURR', @@ -62,7 +58,6 @@ def test_approve_if_anonymous_and_no_comment(self): # If the comment was already read (or anything not pending), don't act donation = models.Donation.objects.create( - timereceived=timezone.now(), readstate='READ', amount=Decimal(1.5), domain='PAYPAL', @@ -74,7 +69,6 @@ def test_approve_if_anonymous_and_no_comment(self): # With no threshold given, leave as is donation = models.Donation.objects.create( - timereceived=timezone.now(), amount=Decimal(1.5), domain='PAYPAL', requestedvisibility='ANON', @@ -88,7 +82,6 @@ def test_approve_if_anonymous_and_no_comment(self): # With a threshold and a donation above it, send to reader donation = models.Donation.objects.create( - timereceived=timezone.now(), amount=Decimal(10), domain='PAYPAL', requestedvisibility='CURR', @@ -99,7 +92,6 @@ def test_approve_if_anonymous_and_no_comment(self): # With a threshold and a donation below it, ignore donation = models.Donation.objects.create( - timereceived=timezone.now(), amount=Decimal(1.5), domain='PAYPAL', requestedvisibility='ANON', @@ -110,7 +102,6 @@ def test_approve_if_anonymous_and_no_comment(self): # Donation with a non-anonymous donor should not bypass screening donation = models.Donation.objects.create( - timereceived=timezone.now(), amount=Decimal(10), domain='PAYPAL', requestedvisibility='ALIAS', @@ -121,7 +112,6 @@ def test_approve_if_anonymous_and_no_comment(self): # Donation with a comment should not bypass screening donation = models.Donation.objects.create( - timereceived=timezone.now(), amount=Decimal(10), comment='Hello', domain='PAYPAL', @@ -137,7 +127,6 @@ def test_approve_if_anonymous_and_no_comment(self): self.event.save() donation = models.Donation.objects.create( - timereceived=timezone.now(), amount=Decimal(10), domain='PAYPAL', requestedvisibility='CURR', diff --git a/tests/test_donor.py b/tests/test_donor.py index b955b4e9c..f9e9e776e 100644 --- a/tests/test_donor.py +++ b/tests/test_donor.py @@ -1,9 +1,7 @@ -import datetime import logging import random from decimal import Decimal -import pytz from django.contrib.auth.models import User from django.test import TestCase from django.urls import reverse @@ -37,7 +35,6 @@ def test_donor_cache(self): amount=5, domain='PAYPAL', transactionstate='COMPLETED', - timereceived=datetime.datetime.now(pytz.utc), ) self.assertEqual(2, models.DonorCache.objects.count()) d2 = models.Donation.objects.create( @@ -46,7 +43,6 @@ def test_donor_cache(self): amount=5, domain='PAYPAL', transactionstate='COMPLETED', - timereceived=datetime.datetime.now(pytz.utc), ) self.assertEqual(3, models.DonorCache.objects.count()) d3 = models.Donation.objects.create( @@ -55,7 +51,6 @@ def test_donor_cache(self): amount=10, domain='PAYPAL', transactionstate='COMPLETED', - timereceived=datetime.datetime.now(pytz.utc), ) self.assertEqual(3, models.DonorCache.objects.count()) d4 = models.Donation.objects.create( @@ -64,7 +59,6 @@ def test_donor_cache(self): amount=20, domain='PAYPAL', transactionstate='COMPLETED', - timereceived=datetime.datetime.now(pytz.utc), ) self.assertEqual(5, models.DonorCache.objects.count()) self.assertEqual( diff --git a/tests/test_event.py b/tests/test_event.py index 4459191a2..93e5f0479 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -5,16 +5,21 @@ import random import post_office.models -import pytz from django.contrib.auth.models import Group, Permission, User from django.test import TestCase, TransactionTestCase, override_settings from django.urls import reverse from tracker import models, settings +from tracker.util import utcnow from . import randgen from .util import long_ago_noon, today_noon, tomorrow_noon +try: + import zoneinfo +except ImportError: + from backports import zoneinfo + class TestEvent(TestCase): def setUp(self): @@ -62,7 +67,7 @@ def test_manager_current(self): ) with self.subTest('now'): - self.event.datetime = datetime.datetime.now(pytz.utc) + self.event.datetime = utcnow() self.event.save() self.assertEqual(models.Event.objects.current(), self.event) @@ -119,9 +124,7 @@ def test_manager_next(self): self.assertIs(models.Event.objects.next(next_event.datetime), None) with self.subTest('now'): - self.event.datetime = datetime.datetime.now(pytz.utc) + datetime.timedelta( - seconds=30 - ) + self.event.datetime = utcnow() + datetime.timedelta(seconds=30) self.event.save() self.assertEqual(models.Event.objects.next(), self.event) @@ -296,7 +299,7 @@ def setUp(self): self.super_user = User.objects.create_superuser( 'admin', 'admin@example.com', 'password' ) - timezone = pytz.timezone(settings.TIME_ZONE) + timezone = zoneinfo.ZoneInfo(settings.TIME_ZONE) self.event = models.Event.objects.create( targetamount=5, datetime=today_noon, @@ -781,8 +784,8 @@ def test_event_prize_report(self): prize.name, '2', # eligible donors '1', # exact donors - str(runs[0].starttime.astimezone(pytz.utc)), - str(runs[0].endtime.astimezone(pytz.utc)), + str(runs[0].start_time_utc), + str(runs[0].end_time_utc), ], msg='Normal prize was incorrect', ) diff --git a/tests/test_prize.py b/tests/test_prize.py index 9ebc3eb2f..224eab11e 100644 --- a/tests/test_prize.py +++ b/tests/test_prize.py @@ -6,8 +6,6 @@ import django import post_office.models -import pytz -from dateutil.parser import parse as parse_date from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME from django.contrib.auth.models import User from django.core.exceptions import ( @@ -18,13 +16,14 @@ from django.test import RequestFactory, TestCase, TransactionTestCase, override_settings from django.urls import reverse -from tracker import models, prizemail, prizeutil, settings +from tracker import models, prizemail, prizeutil, settings, util from . import randgen from .util import ( MigrationsTestCase, long_ago_noon, parse_test_mail, + parse_time, today_noon, tomorrow_noon, ) @@ -87,7 +86,7 @@ def test_time_prize_no_range(self): class TestPrizeDrawingGeneratedEvent(TransactionTestCase): def setUp(self): - self.eventStart = parse_date('2014-01-01 16:00:00Z') + self.eventStart = parse_time('2014-01-01 16:00:00Z') self.rand = random.Random(516273) self.event = randgen.build_random_event( self.rand, start_time=self.eventStart, num_donors=100, num_runs=50 @@ -908,9 +907,7 @@ def test_accept_deadline_offset(self): + datetime.timedelta(days=self.event.prize_accept_deadline_delta), ) - prizeWin.acceptdeadline = datetime.datetime.utcnow().replace( - tzinfo=pytz.utc - ) - datetime.timedelta(days=2) + prizeWin.acceptdeadline = util.utcnow() - datetime.timedelta(days=2) prizeWin.save() self.assertEqual(0, len(targetPrize.eligible_donors())) pastDue = prizeutil.get_past_due_prize_winners(self.event) @@ -1627,9 +1624,9 @@ def test_prize_mail_winners(self): f'Prize Winner {winner.id} did not have email sent flag set', ) self.assertEqual( - winner.acceptdeadline.astimezone(pytz.timezone('Etc/GMT-12')), + winner.acceptdeadline.astimezone(util.anywhere_on_earth_tz()), datetime.datetime( - 2020, 10, 22, 0, 0, 0, tzinfo=pytz.timezone('Etc/GMT-12') + 2020, 10, 22, 0, 0, 0, tzinfo=util.anywhere_on_earth_tz() ), ) diff --git a/tests/test_search_filters.py b/tests/test_search_filters.py index 732c96ca9..a50251926 100644 --- a/tests/test_search_filters.py +++ b/tests/test_search_filters.py @@ -2,7 +2,6 @@ import datetime import random -import pytz from django.contrib.auth.models import Permission, User from django.core.exceptions import PermissionDenied from django.db.models import Q @@ -204,8 +203,7 @@ def test_current_feed(self): speedrun__in=( r.pk for r in self.event.speedrun_set.filter( - endtime__lte=self.event.datetime.astimezone(pytz.utc) - + datetime.timedelta(hours=6) + endtime__lte=self.event.datetime + datetime.timedelta(hours=6) )[:5] ) ) @@ -226,8 +224,7 @@ def test_current_plus_feed(self): speedrun__in=( r.pk for r in self.event.speedrun_set.filter( - endtime__lte=self.event.datetime.astimezone(pytz.utc) - + datetime.timedelta(hours=6) + endtime__lte=self.event.datetime + datetime.timedelta(hours=6) )[:5] ) ) diff --git a/tests/test_speedrun.py b/tests/test_speedrun.py index a60257d6f..61202c7bd 100755 --- a/tests/test_speedrun.py +++ b/tests/test_speedrun.py @@ -3,7 +3,6 @@ from unittest import skipIf import django -import pytz from django.contrib.auth.models import User from django.core.exceptions import ValidationError from django.test import TransactionTestCase @@ -15,6 +14,11 @@ from . import randgen from .util import today_noon +try: + import zoneinfo +except ImportError: + from backports import zoneinfo + class TestSpeedRun(TransactionTestCase): def setUp(self): @@ -269,7 +273,9 @@ def setUp(self): self.event1 = models.Event.objects.create( datetime=today_noon, targetamount=5, - timezone=pytz.timezone(getattr(settings, 'TIME_ZONE', 'America/Denver')), + timezone=zoneinfo.ZoneInfo( + getattr(settings, 'TIME_ZONE', 'America/Denver') + ), ) self.run1 = models.SpeedRun.objects.create( name='Test Run 1', run_time='0:45:00', setup_time='0:05:00', order=1 diff --git a/tests/util.py b/tests/util.py index 1b9f45cc8..3f727d334 100644 --- a/tests/util.py +++ b/tests/util.py @@ -4,11 +4,10 @@ import json import logging import random +import sys import time import unittest -import dateutil.parser -import pytz from django.contrib.auth.models import AnonymousUser, Permission, User from django.contrib.staticfiles.testing import StaticLiveServerTestCase from django.core.serializers.json import DjangoJSONEncoder @@ -26,6 +25,11 @@ from tracker import models, settings from tracker.api.pagination import TrackerPagination +try: + import zoneinfo +except ImportError: + from backports import zoneinfo + def parse_test_mail(mail): lines = [ @@ -46,18 +50,28 @@ def parse_test_mail(mail): noon = datetime.time(12, 0) today = datetime.date.today() today_noon = datetime.datetime.combine(today, noon).astimezone( - pytz.timezone(settings.TIME_ZONE) + zoneinfo.ZoneInfo(settings.TIME_ZONE) ) tomorrow = today + datetime.timedelta(days=1) tomorrow_noon = datetime.datetime.combine(tomorrow, noon).astimezone( - pytz.timezone(settings.TIME_ZONE) + zoneinfo.ZoneInfo(settings.TIME_ZONE) ) long_ago = today - datetime.timedelta(days=180) long_ago_noon = datetime.datetime.combine(long_ago, noon).astimezone( - pytz.timezone(settings.TIME_ZONE) + zoneinfo.ZoneInfo(settings.TIME_ZONE) ) +# TODO: remove this when 3.11 is oldest supported version +def parse_time(value): + if sys.version_info < (3, 11): + import dateutil.parser + + return dateutil.parser.parse(value) + else: + return datetime.datetime.fromisoformat(value) + + class MigrationsTestCase(TransactionTestCase): # e.g # migrate_from = [('tracker', '0004_add_thing')] @@ -317,7 +331,7 @@ def _compare_value(self, expected, found): return True if not isinstance(expected, str) and isinstance(found, str): if isinstance(expected, datetime.datetime): - if expected == dateutil.parser.parse(found): + if expected == parse_time(found): return True else: try: diff --git a/tracker/admin/prize.py b/tracker/admin/prize.py index 5b191f68b..2ca1a0de0 100644 --- a/tracker/admin/prize.py +++ b/tracker/admin/prize.py @@ -1,7 +1,6 @@ import datetime from itertools import groupby -import pytz from django.conf import settings from django.contrib import messages from django.contrib.admin import register @@ -11,7 +10,7 @@ from django.shortcuts import render from django.urls import path, reverse -from tracker import forms, logutil, models, prizemail, viewutil +from tracker import forms, logutil, models, prizemail, util, viewutil from .filters import PrizeListFilter from .forms import DonorPrizeEntryForm, PrizeForm, PrizeKeyImportForm, PrizeWinnerForm @@ -447,7 +446,7 @@ def automail_prize_winners(request, event=None): form.cleaned_data['acceptdeadline'] + datetime.timedelta(days=1), datetime.time(0, 0), - ).replace(tzinfo=pytz.timezone('Etc/GMT-12')) + ).replace(tzinfo=util.anywhere_on_earth_tz()) prizewin.save() viewutil.tracker_log( diff --git a/tracker/consumers/ping.py b/tracker/consumers/ping.py index 8ec5678a9..ba196f614 100644 --- a/tracker/consumers/ping.py +++ b/tracker/consumers/ping.py @@ -1,6 +1,5 @@ import datetime -import pytz from channels.generic.websocket import AsyncWebsocketConsumer @@ -11,6 +10,6 @@ async def connect(self): async def receive(self, text_data=None, bytes_data=None): data = text_data or bytes_data.decode('utf-8') if data == 'PING': - await self.send(datetime.datetime.now(tz=pytz.utc).isoformat()) + await self.send(datetime.datetime.now(tz=datetime.timezone.utc).isoformat()) else: await self.close(400) diff --git a/tracker/management/commands/cache_giantbomb_info.py b/tracker/management/commands/cache_giantbomb_info.py index 93f940631..08c7b7883 100644 --- a/tracker/management/commands/cache_giantbomb_info.py +++ b/tracker/management/commands/cache_giantbomb_info.py @@ -5,7 +5,6 @@ import time import urllib -import dateutil.parser from django.core.management.base import CommandError from tracker import commandutil, models, settings, util, viewutil @@ -115,7 +114,7 @@ def build_query_url(self, id): def parse_query_results(self, searchResult): parsedReleaseDate = None if searchResult['original_release_date'] is not None: - parsedReleaseDate = dateutil.parser.parse( + parsedReleaseDate = datetime.datetime.fromisoformat( searchResult['original_release_date'] ).year return dict( diff --git a/tracker/models/bid.py b/tracker/models/bid.py index 155e0a7c2..87f63b63d 100644 --- a/tracker/models/bid.py +++ b/tracker/models/bid.py @@ -6,7 +6,6 @@ import mptt.managers import mptt.models -import pytz from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models @@ -15,6 +14,7 @@ from django.dispatch import receiver from django.urls import reverse +from tracker import util from tracker.analytics import AnalyticsEventTypes, analytics from tracker.validators import nonzero, positive @@ -441,7 +441,7 @@ def save(self, *args, skip_parent=False, **kwargs): if self.speedrun: self.event = self.speedrun.event if self.state in ['OPENED', 'CLOSED'] and not self.revealedtime: - self.revealedtime = datetime.utcnow().replace(tzinfo=pytz.utc) + self.revealedtime = util.utcnow() analytics.track( AnalyticsEventTypes.INCENTIVE_OPENED, { diff --git a/tracker/models/donation.py b/tracker/models/donation.py index 41b8fd97e..98fb1b68e 100644 --- a/tracker/models/donation.py +++ b/tracker/models/donation.py @@ -1,4 +1,3 @@ -import datetime import logging import random import time @@ -14,6 +13,7 @@ from django.urls import reverse from django.utils import timezone +from .. import util from ..validators import nonzero, positive from .fields import OneToOneOrNoneField from .util import LatestEvent @@ -261,7 +261,7 @@ def save(self, *args, **kwargs): if self.domain == 'LOCAL': # local donations are always complete, duh self.transactionstate = 'COMPLETED' if not self.timereceived: - self.timereceived = datetime.datetime.utcnow() + self.timereceived = util.utcnow() # reminder that this does not run during migrations tests so you have to provide the domainId yourself if not self.domainId: self.domainId = f'{int(time.time())}-{random.getrandbits(128)}' diff --git a/tracker/models/event.py b/tracker/models/event.py index a4d8afb21..e7f98dbbe 100644 --- a/tracker/models/event.py +++ b/tracker/models/event.py @@ -3,9 +3,7 @@ import itertools import logging -import dateutil.parser import post_office.models -import pytz from django.contrib.auth.models import User from django.core.exceptions import ValidationError from django.core.validators import validate_slug @@ -18,9 +16,17 @@ from tracker.validators import nonzero, positive +from .. import util from .fields import TimestampField from .util import LatestEvent +# TODO: remove when 3.9 is oldest supported version + +try: + import zoneinfo +except ImportError: + from backports import zoneinfo + # TODO: remove when 3.10 is oldest supported version try: @@ -43,7 +49,7 @@ def pairwise(iterable): 'Headset', ] -_timezoneChoices = [(x, x) for x in pytz.common_timezones] +_timezoneChoices = [(x, x) for x in zoneinfo.available_timezones()] _currencyChoices = (('USD', 'US Dollars'), ('CAD', 'Canadian Dollars')) @@ -52,7 +58,7 @@ def pairwise(iterable): class EventQuerySet(models.QuerySet): def current(self, timestamp=None): - timestamp = timestamp or datetime.datetime.now(pytz.utc) + timestamp = timestamp or util.utcnow() runs = SpeedRun.objects.filter(starttime__lte=timestamp, endtime__gte=timestamp) if len(runs) == 1: return self.filter(pk=runs.first().event_id).first() @@ -64,7 +70,7 @@ def current(self, timestamp=None): return None def next(self, timestamp=None): - timestamp = timestamp or datetime.datetime.now(pytz.utc) + timestamp = timestamp or util.utcnow() return self.filter(datetime__gt=timestamp).order_by('datetime').first() def with_annotations(self, ignore_order=False): @@ -391,11 +397,13 @@ def upcoming( ): queryset = self if now is None: - now = datetime.datetime.now(tz=pytz.utc) + now = util.utcnow() elif isinstance(now, str): - now = dateutil.parser.parse(now) + now = datetime.datetime.fromisoformat(now) + elif isinstance(now, datetime.datetime): + now = now.astimezone(datetime.timezone.utc) else: - now = now.astimezone(pytz.utc) + raise ValueError(f'Expected None, str, or datetime, got {type(now)}') if include_current: queryset = queryset.filter(endtime__gte=now) else: @@ -534,6 +542,14 @@ def run_time_ms(self): def setup_time_ms(self): return TimestampField.time_string_to_int(self.setup_time) + @property + def start_time_utc(self): + return self.starttime.astimezone(datetime.timezone.utc) + + @property + def end_time_utc(self): + return self.endtime.astimezone(datetime.timezone.utc) + def clean(self): if not self.name: raise ValidationError('Name cannot be blank') diff --git a/tracker/models/prize.py b/tracker/models/prize.py index 8030ef546..9720918af 100644 --- a/tracker/models/prize.py +++ b/tracker/models/prize.py @@ -1,7 +1,6 @@ import datetime from decimal import Decimal -import pytz from django.contrib.auth.models import User from django.contrib.sites import shortcuts as sites from django.core.exceptions import ImproperlyConfigured, ValidationError @@ -213,6 +212,14 @@ def get_absolute_url(self): def __str__(self): return str(self.name) + @property + def start_time_utc(self): + return self.starttime.astimezone(datetime.timezone.utc) + + @property + def end_time_utc(self): + return self.endtime.astimezone(datetime.timezone.utc) + def clean(self, winner=None): if not settings.TRACKER_SWEEPSTAKES_URL: raise ValidationError( @@ -416,9 +423,9 @@ def start_draw_time(self): return self.prev_run.endtime - datetime.timedelta( milliseconds=self.prev_run.setup_time_ms ) - return self.startrun.starttime.replace(tzinfo=pytz.utc) + return self.startrun.starttime.replace(tzinfo=datetime.timezone.utc) elif self.starttime: - return self.starttime.replace(tzinfo=pytz.utc) + return self.starttime.replace(tzinfo=datetime.timezone.utc) else: return None @@ -426,12 +433,10 @@ def end_draw_time(self): if self.endrun and self.endrun.order: if not self.next_run: # covers finale speeches - return self.endrun.endtime.replace( - tzinfo=pytz.utc - ) + datetime.timedelta(hours=1) - return self.endrun.endtime.replace(tzinfo=pytz.utc) + return self.endrun.end_time_utc + datetime.timedelta(hours=1) + return self.endrun.end_time_utc elif self.endtime: - return self.endtime.replace(tzinfo=pytz.utc) + return self.end_time_utc else: return None @@ -455,7 +460,7 @@ def maxed_winners(self): return self.current_win_count() == self.maxwinners def get_prize_winners(self, time=None): - time = time or datetime.datetime.now(tz=pytz.utc) + time = time or util.utcnow() return self.prizewinner_set.filter( Q(acceptcount__gt=0) | ( @@ -465,7 +470,7 @@ def get_prize_winners(self, time=None): ) def get_expired_winners(self, time=None): - time = time or datetime.datetime.utcnow().astimezone(pytz.utc) + time = time or util.utcnow() return self.prizewinner_set.filter(pendingcount__gt=0, acceptdeadline__lt=time) def get_accepted_winners(self): diff --git a/tracker/prizeutil.py b/tracker/prizeutil.py index 1ceba9402..000d412c0 100644 --- a/tracker/prizeutil.py +++ b/tracker/prizeutil.py @@ -2,7 +2,6 @@ import logging import random -import pytz from django.db import transaction from tracker.models import PrizeKey, PrizeWinner @@ -103,5 +102,5 @@ def draw_keys(prize, seed=None, rand=None): def get_past_due_prize_winners(event): - now = datetime.datetime.utcnow().astimezone(pytz.utc) + now = util.utcnow() return PrizeWinner.objects.filter(acceptdeadline__lte=now, pendingcount__gte=1) diff --git a/tracker/search_feeds.py b/tracker/search_feeds.py index f49e0796c..2108f9752 100644 --- a/tracker/search_feeds.py +++ b/tracker/search_feeds.py @@ -1,11 +1,10 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone -import dateutil.parser -import pytz from django.contrib.auth.models import AnonymousUser from django.core.exceptions import PermissionDenied from django.db.models import Q +from tracker import util from tracker.models import Bid, Donation, SpeedRun _DEFAULT_DONATION_DELTA = timedelta(hours=3) @@ -301,7 +300,7 @@ def canonical_bool(b): def default_time(time): if time is None: - time = datetime.now(tz=pytz.utc) + time = util.utcnow() elif isinstance(time, str): - time = dateutil.parser.parse(time) - return time.astimezone(pytz.utc) + time = datetime.fromisoformat(time) + return time.astimezone(timezone.utc) diff --git a/tracker/tasks.py b/tracker/tasks.py index cd8012533..9f3fa1739 100644 --- a/tracker/tasks.py +++ b/tracker/tasks.py @@ -1,12 +1,9 @@ -import datetime - -import pytz from asgiref.sync import async_to_sync from celery import shared_task from celery.utils.log import get_task_logger from channels.layers import get_channel_layer -from . import eventutil, prizeutil +from . import eventutil, prizeutil, util from .models import Donation, Prize logger = get_task_logger(__name__) @@ -24,7 +21,7 @@ def celery_test(): 'celery', { 'type': 'pong', - 'timestamp': datetime.datetime.now(tz=pytz.utc).isoformat(), + 'timestamp': util.utcnow().isoformat(), }, ) diff --git a/tracker/util.py b/tracker/util.py index af48f6d2c..c7c52d1b9 100644 --- a/tracker/util.py +++ b/tracker/util.py @@ -6,9 +6,13 @@ can use it in migrations, or inside the `model` files """ import collections.abc +import datetime import random -import pytz +try: + import zoneinfo +except ImportError: + from backports import zoneinfo def natural_list_parse(s, symbol_only=False): @@ -48,7 +52,7 @@ def try_parse_int(s, base=10, val=None): def anywhere_on_earth_tz(): """This is a trick used by academic conference submission deadlines to use the last possible timezone to define the end of a particular date""" - return pytz.timezone('Etc/GMT+12') + return zoneinfo.ZoneInfo('Etc/GMT-12') def make_rand(rand_source=None, rand_seed=None): @@ -120,3 +124,7 @@ def flatten(iterable): yield sub else: yield el + + +def utcnow() -> datetime.datetime: + return datetime.datetime.now(datetime.timezone.utc) diff --git a/tracker/views/donateviews.py b/tracker/views/donateviews.py index 449ce7a26..0a28dac39 100644 --- a/tracker/views/donateviews.py +++ b/tracker/views/donateviews.py @@ -5,7 +5,6 @@ from decimal import Decimal import post_office.mail -import pytz from django.db import transaction from django.http import Http404, HttpResponse, HttpResponsePermanentRedirect from django.urls import reverse @@ -115,7 +114,6 @@ def process_form(request, event): with transaction.atomic(): donation = models.Donation( amount=commentform.cleaned_data['amount'], - timereceived=pytz.utc.localize(datetime.datetime.utcnow()), domain='PAYPAL', domainId=str(random.getrandbits(128)), event=event,