timezone: Use standard library datetime.timezone.utc consistently.

datetime.timezone is available in Python ≥ 3.2.  This also lets us
remove a pytz dependency from the PostgreSQL scripts.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
Anders Kaseorg
2020-06-04 21:55:20 -07:00
committed by Tim Abbott
parent 29b8e11e20
commit 1f565a9f41
20 changed files with 75 additions and 102 deletions

View File

@@ -2,14 +2,13 @@ import datetime
from typing import Any, Dict from typing import Any, Dict
from django.core.management.base import BaseCommand, CommandParser from django.core.management.base import BaseCommand, CommandParser
from django.utils.timezone import utc
from zerver.lib.statistics import seconds_usage_between from zerver.lib.statistics import seconds_usage_between
from zerver.models import UserProfile from zerver.models import UserProfile
def analyze_activity(options: Dict[str, Any]) -> None: def analyze_activity(options: Dict[str, Any]) -> None:
day_start = datetime.datetime.strptime(options["date"], "%Y-%m-%d").replace(tzinfo=utc) day_start = datetime.datetime.strptime(options["date"], "%Y-%m-%d").replace(tzinfo=datetime.timezone.utc)
day_end = day_start + datetime.timedelta(days=options["duration"]) day_end = day_start + datetime.timedelta(days=options["duration"])
user_profile_query = UserProfile.objects.all() user_profile_query = UserProfile.objects.all()

View File

@@ -2,12 +2,12 @@ import os
import time import time
from argparse import ArgumentParser from argparse import ArgumentParser
from typing import Any, Dict from typing import Any, Dict
from datetime import timezone
from django.conf import settings from django.conf import settings
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils.dateparse import parse_datetime from django.utils.dateparse import parse_datetime
from django.utils.timezone import now as timezone_now from django.utils.timezone import now as timezone_now
from django.utils.timezone import utc as timezone_utc
from analytics.lib.counts import COUNT_STATS, logger, process_count_stat from analytics.lib.counts import COUNT_STATS, logger, process_count_stat
from scripts.lib.zulip_tools import ENDC, WARNING from scripts.lib.zulip_tools import ENDC, WARNING
@@ -60,11 +60,11 @@ class Command(BaseCommand):
fill_to_time = parse_datetime(options['time']) fill_to_time = parse_datetime(options['time'])
if options['utc']: if options['utc']:
fill_to_time = fill_to_time.replace(tzinfo=timezone_utc) fill_to_time = fill_to_time.replace(tzinfo=timezone.utc)
if fill_to_time.tzinfo is None: if fill_to_time.tzinfo is None:
raise ValueError("--time must be timezone aware. Maybe you meant to use the --utc option?") raise ValueError("--time must be timezone aware. Maybe you meant to use the --utc option?")
fill_to_time = floor_to_hour(fill_to_time.astimezone(timezone_utc)) fill_to_time = floor_to_hour(fill_to_time.astimezone(timezone.utc))
if options['stat'] is not None: if options['stat'] is not None:
stats = [COUNT_STATS[options['stat']]] stats = [COUNT_STATS[options['stat']]]

View File

@@ -1,4 +1,4 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Tuple, Type from typing import Any, Dict, List, Optional, Tuple, Type
from unittest import mock from unittest import mock
@@ -8,7 +8,6 @@ from django.db import models
from django.db.models import Sum from django.db.models import Sum
from django.test import TestCase from django.test import TestCase
from django.utils.timezone import now as timezone_now from django.utils.timezone import now as timezone_now
from django.utils.timezone import utc as timezone_utc
from analytics.lib.counts import COUNT_STATS, CountStat, get_count_stats, \ from analytics.lib.counts import COUNT_STATS, CountStat, get_count_stats, \
DependentCountStat, LoggingCountStat, do_aggregate_to_summary_table, \ DependentCountStat, LoggingCountStat, do_aggregate_to_summary_table, \
@@ -33,7 +32,7 @@ class AnalyticsTestCase(TestCase):
MINUTE = timedelta(seconds = 60) MINUTE = timedelta(seconds = 60)
HOUR = MINUTE * 60 HOUR = MINUTE * 60
DAY = HOUR * 24 DAY = HOUR * 24
TIME_ZERO = datetime(1988, 3, 14).replace(tzinfo=timezone_utc) TIME_ZERO = datetime(1988, 3, 14, tzinfo=timezone.utc)
TIME_LAST_HOUR = TIME_ZERO - HOUR TIME_LAST_HOUR = TIME_ZERO - HOUR
def setUp(self) -> None: def setUp(self) -> None:

View File

@@ -1,8 +1,7 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from typing import List, Optional from typing import List, Optional
from unittest import mock from unittest import mock
from django.utils.timezone import utc
from django.http import HttpResponse from django.http import HttpResponse
import ujson import ujson
from django.utils.timezone import now as timezone_now from django.utils.timezone import now as timezone_now
@@ -607,8 +606,8 @@ class TestGetChartDataHelpers(ZulipTestCase):
# the only function that uses it at the moment # the only function that uses it at the moment
def test_last_successful_fill(self) -> None: def test_last_successful_fill(self) -> None:
self.assertIsNone(last_successful_fill('non-existant')) self.assertIsNone(last_successful_fill('non-existant'))
a_time = datetime(2016, 3, 14, 19).replace(tzinfo=utc) a_time = datetime(2016, 3, 14, 19, tzinfo=timezone.utc)
one_hour_before = datetime(2016, 3, 14, 18).replace(tzinfo=utc) one_hour_before = datetime(2016, 3, 14, 18, tzinfo=timezone.utc)
fillstate = FillState.objects.create(property='property', end_time=a_time, fillstate = FillState.objects.create(property='property', end_time=a_time,
state=FillState.DONE) state=FillState.DONE)
self.assertEqual(last_successful_fill('property'), a_time) self.assertEqual(last_successful_fill('property'), a_time)
@@ -631,9 +630,9 @@ class TestTimeRange(ZulipTestCase):
HOUR = timedelta(hours=1) HOUR = timedelta(hours=1)
DAY = timedelta(days=1) DAY = timedelta(days=1)
a_time = datetime(2016, 3, 14, 22, 59).replace(tzinfo=utc) a_time = datetime(2016, 3, 14, 22, 59, tzinfo=timezone.utc)
floor_hour = datetime(2016, 3, 14, 22).replace(tzinfo=utc) floor_hour = datetime(2016, 3, 14, 22, tzinfo=timezone.utc)
floor_day = datetime(2016, 3, 14).replace(tzinfo=utc) floor_day = datetime(2016, 3, 14, tzinfo=timezone.utc)
# test start == end # test start == end
self.assertEqual(time_range(a_time, a_time, CountStat.HOUR, None), []) self.assertEqual(time_range(a_time, a_time, CountStat.HOUR, None), [])

View File

@@ -4,7 +4,7 @@ import re
import time import time
import urllib import urllib
from collections import defaultdict from collections import defaultdict
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from decimal import Decimal from decimal import Decimal
from typing import Any, Callable, Dict, List, \ from typing import Any, Callable, Dict, List, \
@@ -18,7 +18,7 @@ from django.db.models.query import QuerySet
from django.http import HttpRequest, HttpResponse, HttpResponseNotFound from django.http import HttpRequest, HttpResponse, HttpResponseNotFound
from django.shortcuts import render from django.shortcuts import render
from django.template import loader from django.template import loader
from django.utils.timezone import now as timezone_now, utc as timezone_utc from django.utils.timezone import now as timezone_now
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.timesince import timesince from django.utils.timesince import timesince
from django.core.validators import URLValidator from django.core.validators import URLValidator
@@ -253,7 +253,7 @@ def get_chart_data(request: HttpRequest, user_profile: UserProfile, chart_name:
start = realm.date_created start = realm.date_created
if end is None: if end is None:
end = max(last_successful_fill(stat.property) or end = max(last_successful_fill(stat.property) or
datetime.min.replace(tzinfo=timezone_utc) for stat in stats) datetime.min.replace(tzinfo=timezone.utc) for stat in stats)
if start > end and (timezone_now() - start > MAX_TIME_FOR_FULL_ANALYTICS_GENERATION): if start > end and (timezone_now() - start > MAX_TIME_FOR_FULL_ANALYTICS_GENERATION):
logging.warning("User from realm %s attempted to access /stats, but the computed " logging.warning("User from realm %s attempted to access /stats, but the computed "

View File

@@ -1,4 +1,4 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from decimal import Decimal from decimal import Decimal
from functools import wraps from functools import wraps
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
@@ -14,7 +14,6 @@ import responses
from django.core import signing from django.core import signing
from django.urls.resolvers import get_resolver from django.urls.resolvers import get_resolver
from django.http import HttpResponse from django.http import HttpResponse
from django.utils.timezone import utc as timezone_utc
from django.conf import settings from django.conf import settings
from django.utils.timezone import now as timezone_now from django.utils.timezone import now as timezone_now
@@ -265,9 +264,9 @@ class StripeTestCase(ZulipTestCase):
self.signed_seat_count, self.salt = sign_string(str(self.seat_count)) self.signed_seat_count, self.salt = sign_string(str(self.seat_count))
# Choosing dates with corresponding timestamps below 1500000000 so that they are # Choosing dates with corresponding timestamps below 1500000000 so that they are
# not caught by our timestamp normalization regex in normalize_fixture_data # not caught by our timestamp normalization regex in normalize_fixture_data
self.now = datetime(2012, 1, 2, 3, 4, 5).replace(tzinfo=timezone_utc) self.now = datetime(2012, 1, 2, 3, 4, 5, tzinfo=timezone.utc)
self.next_month = datetime(2012, 2, 2, 3, 4, 5).replace(tzinfo=timezone_utc) self.next_month = datetime(2012, 2, 2, 3, 4, 5, tzinfo=timezone.utc)
self.next_year = datetime(2013, 1, 2, 3, 4, 5).replace(tzinfo=timezone_utc) self.next_year = datetime(2013, 1, 2, 3, 4, 5, tzinfo=timezone.utc)
def get_signed_seat_count_from_response(self, response: HttpResponse) -> Optional[str]: def get_signed_seat_count_from_response(self, response: HttpResponse) -> Optional[str]:
match = re.search(r'name=\"signed_seat_count\" value=\"(.+)\"', response.content.decode("utf-8")) match = re.search(r'name=\"signed_seat_count\" value=\"(.+)\"', response.content.decode("utf-8"))
@@ -1552,24 +1551,24 @@ class RequiresBillingAccessTest(ZulipTestCase):
class BillingHelpersTest(ZulipTestCase): class BillingHelpersTest(ZulipTestCase):
def test_next_month(self) -> None: def test_next_month(self) -> None:
anchor = datetime(2019, 12, 31, 1, 2, 3).replace(tzinfo=timezone_utc) anchor = datetime(2019, 12, 31, 1, 2, 3, tzinfo=timezone.utc)
period_boundaries = [ period_boundaries = [
anchor, anchor,
datetime(2020, 1, 31, 1, 2, 3).replace(tzinfo=timezone_utc), datetime(2020, 1, 31, 1, 2, 3, tzinfo=timezone.utc),
# Test that this is the 28th even during leap years # Test that this is the 28th even during leap years
datetime(2020, 2, 28, 1, 2, 3).replace(tzinfo=timezone_utc), datetime(2020, 2, 28, 1, 2, 3, tzinfo=timezone.utc),
datetime(2020, 3, 31, 1, 2, 3).replace(tzinfo=timezone_utc), datetime(2020, 3, 31, 1, 2, 3, tzinfo=timezone.utc),
datetime(2020, 4, 30, 1, 2, 3).replace(tzinfo=timezone_utc), datetime(2020, 4, 30, 1, 2, 3, tzinfo=timezone.utc),
datetime(2020, 5, 31, 1, 2, 3).replace(tzinfo=timezone_utc), datetime(2020, 5, 31, 1, 2, 3, tzinfo=timezone.utc),
datetime(2020, 6, 30, 1, 2, 3).replace(tzinfo=timezone_utc), datetime(2020, 6, 30, 1, 2, 3, tzinfo=timezone.utc),
datetime(2020, 7, 31, 1, 2, 3).replace(tzinfo=timezone_utc), datetime(2020, 7, 31, 1, 2, 3, tzinfo=timezone.utc),
datetime(2020, 8, 31, 1, 2, 3).replace(tzinfo=timezone_utc), datetime(2020, 8, 31, 1, 2, 3, tzinfo=timezone.utc),
datetime(2020, 9, 30, 1, 2, 3).replace(tzinfo=timezone_utc), datetime(2020, 9, 30, 1, 2, 3, tzinfo=timezone.utc),
datetime(2020, 10, 31, 1, 2, 3).replace(tzinfo=timezone_utc), datetime(2020, 10, 31, 1, 2, 3, tzinfo=timezone.utc),
datetime(2020, 11, 30, 1, 2, 3).replace(tzinfo=timezone_utc), datetime(2020, 11, 30, 1, 2, 3, tzinfo=timezone.utc),
datetime(2020, 12, 31, 1, 2, 3).replace(tzinfo=timezone_utc), datetime(2020, 12, 31, 1, 2, 3, tzinfo=timezone.utc),
datetime(2021, 1, 31, 1, 2, 3).replace(tzinfo=timezone_utc), datetime(2021, 1, 31, 1, 2, 3, tzinfo=timezone.utc),
datetime(2021, 2, 28, 1, 2, 3).replace(tzinfo=timezone_utc)] datetime(2021, 2, 28, 1, 2, 3, tzinfo=timezone.utc)]
with self.assertRaises(AssertionError): with self.assertRaises(AssertionError):
add_months(anchor, -1) add_months(anchor, -1)
# Explicitly test add_months for each value of MAX_DAY_FOR_MONTH and # Explicitly test add_months for each value of MAX_DAY_FOR_MONTH and
@@ -1586,9 +1585,9 @@ class BillingHelpersTest(ZulipTestCase):
def test_compute_plan_parameters(self) -> None: def test_compute_plan_parameters(self) -> None:
# TODO: test rounding down microseconds # TODO: test rounding down microseconds
anchor = datetime(2019, 12, 31, 1, 2, 3).replace(tzinfo=timezone_utc) anchor = datetime(2019, 12, 31, 1, 2, 3, tzinfo=timezone.utc)
month_later = datetime(2020, 1, 31, 1, 2, 3).replace(tzinfo=timezone_utc) month_later = datetime(2020, 1, 31, 1, 2, 3, tzinfo=timezone.utc)
year_later = datetime(2020, 12, 31, 1, 2, 3).replace(tzinfo=timezone_utc) year_later = datetime(2020, 12, 31, 1, 2, 3, tzinfo=timezone.utc)
test_cases = [ test_cases = [
# TODO test with Decimal(85), not 85 # TODO test with Decimal(85), not 85
# TODO fix the mypy error by specifying the exact type # TODO fix the mypy error by specifying the exact type

View File

@@ -153,19 +153,19 @@ Python allows datetime objects to not have an associated timezone, which can
cause time-related bugs that are hard to catch with a test suite, or bugs cause time-related bugs that are hard to catch with a test suite, or bugs
that only show up during daylight savings time. that only show up during daylight savings time.
Good ways to make timezone-aware datetimes are below. We import `timezone` Good ways to make timezone-aware datetimes are below. We import timezone
functions as `from django.utils.timezone import now as timezone_now` and libraries as `from datetime import datetime, timezone` and `from
`from django.utils.timezone import utc as timezone_utc`. django.utils.timezone import now as timezone_now`.
Use: Use:
* `timezone_now()` to get a datetime when Django is available, such as * `timezone_now()` to get a datetime when Django is available, such as
in `zerver/`. in `zerver/`.
* `datetime.now(tz=pytz.utc)` when Django is not available, such as * `datetime.now(tz=timezone.utc)` when Django is not available, such as
for bots and scripts. for bots and scripts.
* `datetime.fromtimestamp(timestamp, tz=timezone_utc)` if creating a * `datetime.fromtimestamp(timestamp, tz=timezone.utc)` if creating a
datetime from a timestamp. This is also available as datetime from a timestamp. This is also available as
`zerver.lib.timestamp.timestamp_to_datetime`. `zerver.lib.timestamp.timestamp_to_datetime`.
* `datetime.strptime(date_string, format).replace(tzinfo=timezone_utc)` if * `datetime.strptime(date_string, format).replace(tzinfo=timezone.utc)` if
creating a datetime from a formatted string that is in UTC. creating a datetime from a formatted string that is in UTC.
Idioms that result in timezone-naive datetimes, and should be avoided, are Idioms that result in timezone-naive datetimes, and should be avoided, are

View File

@@ -1,10 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import subprocess import subprocess
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
import dateutil.parser import dateutil.parser
import pytz
states = { states = {
"OK": 0, "OK": 0,
@@ -28,7 +27,7 @@ try:
except OSError: except OSError:
report('UNKNOWN', 'could not determine completion time of last Postgres backup') report('UNKNOWN', 'could not determine completion time of last Postgres backup')
if datetime.now(tz=pytz.utc) - last_backup > timedelta(hours=25): if datetime.now(tz=timezone.utc) - last_backup > timedelta(hours=25):
report('CRITICAL', 'last Postgres backup completed more than 25 hours ago: %s' % (last_backup,)) report('CRITICAL', 'last Postgres backup completed more than 25 hours ago: %s' % (last_backup,))
report('OK', 'last Postgres backup completed less than 25 hours ago: %s' % (last_backup,)) report('OK', 'last Postgres backup completed less than 25 hours ago: %s' % (last_backup,))

View File

@@ -7,9 +7,8 @@ import shlex
import subprocess import subprocess
import logging import logging
import dateutil.parser import dateutil.parser
import pytz
import time import time
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from typing import Dict, List from typing import Dict, List
logging.Formatter.converter = time.gmtime logging.Formatter.converter = time.gmtime
@@ -48,7 +47,7 @@ if len(pg_data_paths) != 1:
pg_data_path = pg_data_paths[0] pg_data_path = pg_data_paths[0]
run(['env-wal-e', 'backup-push', pg_data_path]) run(['env-wal-e', 'backup-push', pg_data_path])
now = datetime.now(tz=pytz.utc) now = datetime.now(tz=timezone.utc)
with open('/var/lib/nagios_state/last_postgres_backup', 'w') as f: with open('/var/lib/nagios_state/last_postgres_backup', 'w') as f:
f.write(now.isoformat()) f.write(now.isoformat())
f.write("\n") f.write("\n")

View File

@@ -15,10 +15,7 @@ class zulip::postgres_common {
# Postgres Nagios check plugin # Postgres Nagios check plugin
'check-postgres', 'check-postgres',
# Python modules used in our monitoring/worker threads # Python modules used in our monitoring/worker threads
'python3-tz', # TODO: use a virtualenv instead
'python-tz', # TODO: use a virtualenv instead
'python3-dateutil', # TODO: use a virtualenv instead 'python3-dateutil', # TODO: use a virtualenv instead
'python-dateutil', # TODO: use a virtualenv instead
] ]
$postgres_user_reqs = [ $postgres_user_reqs = [
Package[$postgresql], Package[$postgresql],
@@ -39,12 +36,8 @@ class zulip::postgres_common {
# https://bucardo.org/check_postgres/ # https://bucardo.org/check_postgres/
# 'check-postgres', # TODO # 'check-postgres', # TODO
] ]
exec {'pip2_deps':
# Python modules used in our monitoring/worker threads
command => '/usr/bin/pip2 install pytz python-dateutil'
}
exec {'pip3_deps': exec {'pip3_deps':
command => 'python3 -m pip install pytz python-dateutil' command => 'python3 -m pip install python-dateutil'
} }
group { 'ssl-cert': group { 'ssl-cert':
ensure => present, ensure => present,

View File

@@ -3,7 +3,6 @@ from typing import Any, Dict, List, Set, Tuple, Union
from collections import defaultdict from collections import defaultdict
import datetime import datetime
import logging import logging
import pytz
from django.conf import settings from django.conf import settings
from django.utils.timezone import now as timezone_now from django.utils.timezone import now as timezone_now
@@ -168,7 +167,7 @@ def handle_digest_email(user_profile_id: int, cutoff: float,
user_profile = get_user_profile_by_id(user_profile_id) user_profile = get_user_profile_by_id(user_profile_id)
# Convert from epoch seconds to a datetime object. # Convert from epoch seconds to a datetime object.
cutoff_date = datetime.datetime.fromtimestamp(int(cutoff), tz=pytz.utc) cutoff_date = datetime.datetime.fromtimestamp(int(cutoff), tz=datetime.timezone.utc)
context = common_context(user_profile) context = common_context(user_profile)

View File

@@ -9,7 +9,7 @@ from bs4 import BeautifulSoup
from django.conf import settings from django.conf import settings
from django.db import connection from django.db import connection
from django.db.models import Max from django.db.models import Max
from django.utils.timezone import utc as timezone_utc, now as timezone_now from django.utils.timezone import now as timezone_now
from typing import Any, Dict, List, Optional, Set, Tuple, \ from typing import Any, Dict, List, Optional, Set, Tuple, \
Iterable, cast Iterable, cast
@@ -109,7 +109,7 @@ def fix_datetime_fields(data: TableData, table: TableName) -> None:
for item in data[table]: for item in data[table]:
for field_name in DATE_FIELDS[table]: for field_name in DATE_FIELDS[table]:
if item[field_name] is not None: if item[field_name] is not None:
item[field_name] = datetime.datetime.fromtimestamp(item[field_name], tz=timezone_utc) item[field_name] = datetime.datetime.fromtimestamp(item[field_name], tz=datetime.timezone.utc)
def fix_upload_links(data: TableData, message_table: TableName) -> None: def fix_upload_links(data: TableData, message_table: TableName) -> None:
""" """

View File

@@ -1,14 +1,13 @@
# System documented in https://zulip.readthedocs.io/en/latest/subsystems/logging.html # System documented in https://zulip.readthedocs.io/en/latest/subsystems/logging.html
from django.utils.timezone import now as timezone_now from django.utils.timezone import now as timezone_now
from django.utils.timezone import utc as timezone_utc
import hashlib import hashlib
import logging import logging
import threading import threading
import traceback import traceback
from typing import Optional, Tuple from typing import Optional, Tuple
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from logging import Logger from logging import Logger
@@ -28,7 +27,7 @@ class _RateLimitFilter:
Adapted from https://djangosnippets.org/snippets/2242/. Adapted from https://djangosnippets.org/snippets/2242/.
""" """
last_error = datetime.min.replace(tzinfo=timezone_utc) last_error = datetime.min.replace(tzinfo=timezone.utc)
# This thread-local variable is used to detect recursive # This thread-local variable is used to detect recursive
# exceptions during exception handling (primarily intended for # exceptions during exception handling (primarily intended for
# when accessing the shared cache throws an exception). # when accessing the shared cache throws an exception).

View File

@@ -1,28 +1,25 @@
import datetime import datetime
import calendar import calendar
from django.utils.timezone import utc as timezone_utc
class TimezoneNotUTCException(Exception): class TimezoneNotUTCException(Exception):
pass pass
def verify_UTC(dt: datetime.datetime) -> None: def verify_UTC(dt: datetime.datetime) -> None:
if dt.tzinfo is None or dt.tzinfo.utcoffset(dt) != timezone_utc.utcoffset(dt): if dt.tzinfo is None or dt.tzinfo.utcoffset(dt) != datetime.timezone.utc.utcoffset(dt):
raise TimezoneNotUTCException("Datetime %s does not have a UTC timezone." % (dt,)) raise TimezoneNotUTCException("Datetime %s does not have a UTC timezone." % (dt,))
def convert_to_UTC(dt: datetime.datetime) -> datetime.datetime: def convert_to_UTC(dt: datetime.datetime) -> datetime.datetime:
if dt.tzinfo is None: if dt.tzinfo is None:
return dt.replace(tzinfo=timezone_utc) return dt.replace(tzinfo=datetime.timezone.utc)
return dt.astimezone(timezone_utc) return dt.astimezone(datetime.timezone.utc)
def floor_to_hour(dt: datetime.datetime) -> datetime.datetime: def floor_to_hour(dt: datetime.datetime) -> datetime.datetime:
verify_UTC(dt) verify_UTC(dt)
return datetime.datetime(*dt.timetuple()[:4]) \ return datetime.datetime(*dt.timetuple()[:4], tzinfo=datetime.timezone.utc)
.replace(tzinfo=timezone_utc)
def floor_to_day(dt: datetime.datetime) -> datetime.datetime: def floor_to_day(dt: datetime.datetime) -> datetime.datetime:
verify_UTC(dt) verify_UTC(dt)
return datetime.datetime(*dt.timetuple()[:3]) \ return datetime.datetime(*dt.timetuple()[:3], tzinfo=datetime.timezone.utc)
.replace(tzinfo=timezone_utc)
def ceiling_to_hour(dt: datetime.datetime) -> datetime.datetime: def ceiling_to_hour(dt: datetime.datetime) -> datetime.datetime:
floor = floor_to_hour(dt) floor = floor_to_hour(dt)
@@ -37,7 +34,7 @@ def ceiling_to_day(dt: datetime.datetime) -> datetime.datetime:
return floor + datetime.timedelta(days=1) return floor + datetime.timedelta(days=1)
def timestamp_to_datetime(timestamp: float) -> datetime.datetime: def timestamp_to_datetime(timestamp: float) -> datetime.datetime:
return datetime.datetime.fromtimestamp(float(timestamp), tz=timezone_utc) return datetime.datetime.fromtimestamp(float(timestamp), tz=datetime.timezone.utc)
def datetime_to_timestamp(dt: datetime.datetime) -> int: def datetime_to_timestamp(dt: datetime.datetime) -> int:
verify_UTC(dt) verify_UTC(dt)

View File

@@ -3,7 +3,6 @@ import time
from typing import Any from typing import Any
from django.core.management.base import CommandParser from django.core.management.base import CommandParser
from django.utils.timezone import utc as timezone_utc
from zerver.lib.management import ZulipBaseCommand from zerver.lib.management import ZulipBaseCommand
from zerver.models import Message, Recipient, Stream from zerver.models import Message, Recipient, Stream
@@ -26,7 +25,7 @@ class Command(ZulipBaseCommand):
streams = Stream.objects.filter(realm=realm, invite_only=False) streams = Stream.objects.filter(realm=realm, invite_only=False)
recipients = Recipient.objects.filter( recipients = Recipient.objects.filter(
type=Recipient.STREAM, type_id__in=[stream.id for stream in streams]) type=Recipient.STREAM, type_id__in=[stream.id for stream in streams])
cutoff = datetime.datetime.fromtimestamp(options["since"], tz=timezone_utc) cutoff = datetime.datetime.fromtimestamp(options["since"], tz=datetime.timezone.utc)
messages = Message.objects.filter(date_sent__gt=cutoff, recipient__in=recipients) messages = Message.objects.filter(date_sent__gt=cutoff, recipient__in=recipients)
for message in messages: for message in messages:

View File

@@ -17,7 +17,6 @@ from django.http import HttpResponse
from django.db import transaction from django.db import transaction
from django.db.models import F from django.db.models import F
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.timezone import utc as timezone_utc
from analytics.lib.counts import CountStat, LoggingCountStat from analytics.lib.counts import CountStat, LoggingCountStat
from analytics.models import InstallationCount, RealmCount from analytics.models import InstallationCount, RealmCount
@@ -383,7 +382,7 @@ class PushBouncerNotificationTest(BouncerTestCase):
self.assertEqual(len(tokens), 0) self.assertEqual(len(tokens), 0)
class AnalyticsBouncerTest(BouncerTestCase): class AnalyticsBouncerTest(BouncerTestCase):
TIME_ZERO = datetime.datetime(1988, 3, 14).replace(tzinfo=timezone_utc) TIME_ZERO = datetime.datetime(1988, 3, 14, tzinfo=datetime.timezone.utc)
@override_settings(PUSH_NOTIFICATION_BOUNCER_URL='https://push.zulip.org.example.com') @override_settings(PUSH_NOTIFICATION_BOUNCER_URL='https://push.zulip.org.example.com')
@mock.patch('zerver.lib.remote_server.requests.request') @mock.patch('zerver.lib.remote_server.requests.request')

View File

@@ -81,7 +81,6 @@ import ujson
from typing import Any, List, Optional from typing import Any, List, Optional
import urllib import urllib
import pytz
class RedirectAndLogIntoSubdomainTestCase(ZulipTestCase): class RedirectAndLogIntoSubdomainTestCase(ZulipTestCase):
def test_data(self) -> None: def test_data(self) -> None:
@@ -4087,22 +4086,22 @@ class FollowupEmailTest(ZulipTestCase):
def test_followup_day2_email(self) -> None: def test_followup_day2_email(self) -> None:
user_profile = self.example_user('hamlet') user_profile = self.example_user('hamlet')
# Test date_joined == Sunday # Test date_joined == Sunday
user_profile.date_joined = datetime.datetime(2018, 1, 7, 1, 0, 0, 0, pytz.UTC) user_profile.date_joined = datetime.datetime(2018, 1, 7, 1, 0, 0, 0, tzinfo=datetime.timezone.utc)
self.assertEqual(followup_day2_email_delay(user_profile), datetime.timedelta(days=2, hours=-1)) self.assertEqual(followup_day2_email_delay(user_profile), datetime.timedelta(days=2, hours=-1))
# Test date_joined == Tuesday # Test date_joined == Tuesday
user_profile.date_joined = datetime.datetime(2018, 1, 2, 1, 0, 0, 0, pytz.UTC) user_profile.date_joined = datetime.datetime(2018, 1, 2, 1, 0, 0, 0, tzinfo=datetime.timezone.utc)
self.assertEqual(followup_day2_email_delay(user_profile), datetime.timedelta(days=2, hours=-1)) self.assertEqual(followup_day2_email_delay(user_profile), datetime.timedelta(days=2, hours=-1))
# Test date_joined == Thursday # Test date_joined == Thursday
user_profile.date_joined = datetime.datetime(2018, 1, 4, 1, 0, 0, 0, pytz.UTC) user_profile.date_joined = datetime.datetime(2018, 1, 4, 1, 0, 0, 0, tzinfo=datetime.timezone.utc)
self.assertEqual(followup_day2_email_delay(user_profile), datetime.timedelta(days=1, hours=-1)) self.assertEqual(followup_day2_email_delay(user_profile), datetime.timedelta(days=1, hours=-1))
# Test date_joined == Friday # Test date_joined == Friday
user_profile.date_joined = datetime.datetime(2018, 1, 5, 1, 0, 0, 0, pytz.UTC) user_profile.date_joined = datetime.datetime(2018, 1, 5, 1, 0, 0, 0, tzinfo=datetime.timezone.utc)
self.assertEqual(followup_day2_email_delay(user_profile), datetime.timedelta(days=3, hours=-1)) self.assertEqual(followup_day2_email_delay(user_profile), datetime.timedelta(days=3, hours=-1))
# Time offset of America/Phoenix is -07:00 # Time offset of America/Phoenix is -07:00
user_profile.timezone = 'America/Phoenix' user_profile.timezone = 'America/Phoenix'
# Test date_joined == Friday in UTC, but Thursday in the user's timezone # Test date_joined == Friday in UTC, but Thursday in the user's timezone
user_profile.date_joined = datetime.datetime(2018, 1, 5, 1, 0, 0, 0, pytz.UTC) user_profile.date_joined = datetime.datetime(2018, 1, 5, 1, 0, 0, 0, tzinfo=datetime.timezone.utc)
self.assertEqual(followup_day2_email_delay(user_profile), datetime.timedelta(days=1, hours=-1)) self.assertEqual(followup_day2_email_delay(user_profile), datetime.timedelta(days=1, hours=-1))
class NoReplyEmailTest(ZulipTestCase): class NoReplyEmailTest(ZulipTestCase):

View File

@@ -1,21 +1,17 @@
from django.utils.timezone import utc as timezone_utc
from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.timestamp import floor_to_hour, floor_to_day, ceiling_to_hour, \ from zerver.lib.timestamp import floor_to_hour, floor_to_day, ceiling_to_hour, \
timestamp_to_datetime, datetime_to_timestamp, \ timestamp_to_datetime, datetime_to_timestamp, \
TimezoneNotUTCException, convert_to_UTC TimezoneNotUTCException, convert_to_UTC
from datetime import timedelta from datetime import timedelta, timezone
from dateutil import parser from dateutil import parser
import pytz
class TestTimestamp(ZulipTestCase): class TestTimestamp(ZulipTestCase):
def test_datetime_and_timestamp_conversions(self) -> None: def test_datetime_and_timestamp_conversions(self) -> None:
timestamp = 1483228800 timestamp = 1483228800
for dt in [ for dt in [
parser.parse('2017-01-01 00:00:00.123 UTC'), parser.parse('2017-01-01 00:00:00.123 UTC'),
parser.parse('2017-01-01 00:00:00.123').replace(tzinfo=timezone_utc), parser.parse('2017-01-01 00:00:00.123').replace(tzinfo=timezone.utc)]:
parser.parse('2017-01-01 00:00:00.123').replace(tzinfo=pytz.utc)]:
self.assertEqual(timestamp_to_datetime(timestamp), dt-timedelta(microseconds=123000)) self.assertEqual(timestamp_to_datetime(timestamp), dt-timedelta(microseconds=123000))
self.assertEqual(datetime_to_timestamp(dt), timestamp) self.assertEqual(datetime_to_timestamp(dt), timestamp)
@@ -28,7 +24,7 @@ class TestTimestamp(ZulipTestCase):
def test_convert_to_UTC(self) -> None: def test_convert_to_UTC(self) -> None:
utc_datetime = parser.parse('2017-01-01 00:00:00.123 UTC') utc_datetime = parser.parse('2017-01-01 00:00:00.123 UTC')
for dt in [ for dt in [
parser.parse('2017-01-01 00:00:00.123').replace(tzinfo=timezone_utc), parser.parse('2017-01-01 00:00:00.123').replace(tzinfo=timezone.utc),
parser.parse('2017-01-01 00:00:00.123'), parser.parse('2017-01-01 00:00:00.123'),
parser.parse('2017-01-01 05:00:00.123+05')]: parser.parse('2017-01-01 05:00:00.123+05')]:
self.assertEqual(convert_to_UTC(dt), utc_datetime) self.assertEqual(convert_to_UTC(dt), utc_datetime)

View File

@@ -1,9 +1,8 @@
from datetime import datetime from datetime import datetime, timezone
from typing import Any, Callable, Dict, List, Tuple from typing import Any, Callable, Dict, List, Tuple
import ujson import ujson
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.utils.timezone import utc as timezone_utc
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from zerver.decorator import api_key_only_webhook_view from zerver.decorator import api_key_only_webhook_view
@@ -41,7 +40,7 @@ class LibratoWebhookParser:
def parse_violation(self, violation: Dict[str, Any]) -> Tuple[str, str]: def parse_violation(self, violation: Dict[str, Any]) -> Tuple[str, str]:
metric_name = violation['metric'] metric_name = violation['metric']
recorded_at = datetime.fromtimestamp((violation['recorded_at']), recorded_at = datetime.fromtimestamp((violation['recorded_at']),
tz=timezone_utc).strftime('%Y-%m-%d %H:%M:%S') tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
return metric_name, recorded_at return metric_name, recorded_at
def parse_conditions(self) -> List[Dict[str, Any]]: def parse_conditions(self) -> List[Dict[str, Any]]:
@@ -90,7 +89,7 @@ class LibratoWebhookHandler(LibratoWebhookParser):
def handle_alert_clear_message(self) -> str: def handle_alert_clear_message(self) -> str:
alert_clear_template = "Alert [alert_name]({alert_url}) has cleared at {trigger_time} UTC!" alert_clear_template = "Alert [alert_name]({alert_url}) has cleared at {trigger_time} UTC!"
trigger_time = datetime.fromtimestamp((self.payload['trigger_time']), trigger_time = datetime.fromtimestamp((self.payload['trigger_time']),
tz=timezone_utc).strftime('%Y-%m-%d %H:%M:%S') tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
alert_id, alert_name, alert_url, alert_runbook_url = self.parse_alert() alert_id, alert_name, alert_url, alert_runbook_url = self.parse_alert()
content = alert_clear_template.format(alert_name=alert_name, content = alert_clear_template.format(alert_name=alert_name,
alert_url=alert_url, alert_url=alert_url,

View File

@@ -7,7 +7,6 @@ from django.core.validators import validate_email, URLValidator
from django.db import IntegrityError, transaction from django.db import IntegrityError, transaction
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.utils import timezone from django.utils import timezone
from django.utils.timezone import utc as timezone_utc
from django.utils.translation import ugettext as _, ugettext as err_ from django.utils.translation import ugettext as _, ugettext as err_
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
@@ -224,7 +223,7 @@ def remote_server_post_analytics(request: HttpRequest,
realm_id=row['realm'], realm_id=row['realm'],
remote_id=row['id'], remote_id=row['id'],
server=server, server=server,
end_time=datetime.datetime.fromtimestamp(row['end_time'], tz=timezone_utc), end_time=datetime.datetime.fromtimestamp(row['end_time'], tz=datetime.timezone.utc),
subgroup=row['subgroup'], subgroup=row['subgroup'],
value=row['value']) for row in realm_counts] value=row['value']) for row in realm_counts]
batch_create_table_data(server, RemoteRealmCount, row_objects) batch_create_table_data(server, RemoteRealmCount, row_objects)
@@ -233,7 +232,7 @@ def remote_server_post_analytics(request: HttpRequest,
property=row['property'], property=row['property'],
remote_id=row['id'], remote_id=row['id'],
server=server, server=server,
end_time=datetime.datetime.fromtimestamp(row['end_time'], tz=timezone_utc), end_time=datetime.datetime.fromtimestamp(row['end_time'], tz=datetime.timezone.utc),
subgroup=row['subgroup'], subgroup=row['subgroup'],
value=row['value']) for row in installation_counts] value=row['value']) for row in installation_counts]
batch_create_table_data(server, RemoteInstallationCount, row_objects) batch_create_table_data(server, RemoteInstallationCount, row_objects)
@@ -243,7 +242,7 @@ def remote_server_post_analytics(request: HttpRequest,
realm_id=row['realm'], realm_id=row['realm'],
remote_id=row['id'], remote_id=row['id'],
server=server, server=server,
event_time=datetime.datetime.fromtimestamp(row['event_time'], tz=timezone_utc), event_time=datetime.datetime.fromtimestamp(row['event_time'], tz=datetime.timezone.utc),
backfilled=row['backfilled'], backfilled=row['backfilled'],
extra_data=row['extra_data'], extra_data=row['extra_data'],
event_type=row['event_type']) for row in realmauditlog_rows] event_type=row['event_type']) for row in realmauditlog_rows]