1
0
mirror of https://github.com/linkedin/oncall.git synced 2025-11-26 23:10:47 +02:00

Py3 migration

This commit is contained in:
Daniel Wang
2019-02-27 17:14:27 -08:00
parent b688b3c885
commit 5a66b2f660
46 changed files with 70 additions and 90 deletions

View File

@@ -17,7 +17,7 @@ def require_db():
# Read config based on pytest root directory. Assumes config lives at oncall/configs/config.yaml
cfg_path = os.path.join(str(pytest.config.rootdir), 'configs/config.yaml')
with open(cfg_path) as f:
config = yaml.load(f)
config = yaml.safe_load(f)
db.init(config['db'])

View File

@@ -4,7 +4,7 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import urllib
import urllib.parse
import requests
from testutils import prefix, api_v0
@@ -163,5 +163,5 @@ def test_api_v0_schedules_with_spaces_in_roster_name(team):
assert re.status_code == 201
re = requests.get(api_v0('teams/%s/rosters/%s/schedules' %
(team_name, urllib.quote(roster_name, safe=''))))
(team_name, urllib.parse.quote(roster_name, safe=''))))
assert re.status_code == 200

View File

@@ -14,7 +14,7 @@ setuptools.setup(
packages=setuptools.find_packages('src'),
include_package_data=True,
install_requires=[
'falcon==1.1.0',
'falcon==1.4.1',
'falcon-cors',
'gevent',
'ujson',

View File

@@ -1,8 +1,6 @@
# Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license.
# See LICENSE in the project root for license information.
from __future__ import absolute_import
from falcon import HTTPNotFound

View File

@@ -12,7 +12,7 @@ from ...utils import (
)
from ...constants import EVENT_DELETED, EVENT_EDITED
from events import columns, all_columns
from .events import columns, all_columns
update_columns = {
'start': '`start`=%(start)s',

View File

@@ -76,7 +76,7 @@ def on_post(req, resp):
events = events_0 + events_1
# Validation checks
now = time.time()
if any(map(lambda ev: ev['start'] < now - constants.GRACE_PERIOD, events)):
if any([ev['start'] < now - constants.GRACE_PERIOD for ev in events]):
raise HTTPBadRequest('Invalid event swap request',
'Cannot edit events in the past')
if len(set(ev['team_id'] for ev in events)) > 1:

View File

@@ -166,7 +166,7 @@ def on_get(req, resp):
cursor = connection.cursor(db.DictCursor)
# Build where clause. If including subscriptions, deal with team parameters later
params = req.params.viewkeys() - TEAM_PARAMS if include_sub else req.params
params = req.params.keys() - TEAM_PARAMS if include_sub else req.params
for key in params:
val = req.get_param(key)
if key in constraints:
@@ -176,7 +176,7 @@ def on_get(req, resp):
# Deal with team subscriptions and team parameters
team_where = []
subs_vals = []
team_params = req.params.viewkeys() & TEAM_PARAMS
team_params = req.params.keys() & TEAM_PARAMS
if include_sub and team_params:
for key in team_params:

View File

@@ -47,7 +47,7 @@ def events_to_ical(events, identifier):
'%s %s shift: %s' % (event['team'], event['role'], full_name))
cal_event.add('description',
'%s\n' % full_name +
'\n'.join(['%s: %s' % (mode, dest) for mode, dest in user['contacts'].iteritems()]))
'\n'.join(['%s: %s' % (mode, dest) for mode, dest in user['contacts'].items()]))
# Attach info about the user oncall
attendee = vCalAddress('MAILTO:%s' % user['contacts'].get('email'))

View File

@@ -4,7 +4,7 @@
from ... import db
from ...utils import load_json_body
from ...auth import check_team_auth, login_required
from schedules import get_schedules
from .schedules import get_schedules
from falcon import HTTPNotFound
from oncall.bin.scheduler import load_scheduler

View File

@@ -2,7 +2,7 @@
# See LICENSE in the project root for license information.
from ... import db
from schedules import get_schedules
from .schedules import get_schedules
from falcon import HTTPNotFound
from oncall.bin.scheduler import load_scheduler
import operator

View File

@@ -1,7 +1,7 @@
# Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license.
# See LICENSE in the project root for license information.
from urllib import unquote
from urllib.parse import unquote
from falcon import HTTPError, HTTPNotFound, HTTPBadRequest
from ujson import dumps as json_dumps
@@ -129,7 +129,7 @@ def on_put(req, resp, team, roster):
AND team_id = (SELECT id from team WHERE name = %s))''',
(roster, team))
roster_users = {row[0] for row in cursor}
if not all(map(lambda x: x in roster_users, roster_order)):
if not all([x in roster_users for x in roster_order]):
raise HTTPBadRequest('Invalid roster order', 'All users in provided order must be part of the roster')
if not len(roster_order) == len(roster_users):
raise HTTPBadRequest('Invalid roster order', 'Roster order must include all roster members')

View File

@@ -1,7 +1,7 @@
# Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license.
# See LICENSE in the project root for license information.
from urllib import unquote
from urllib.parse import unquote
from falcon import HTTPNotFound, HTTPBadRequest, HTTP_200
from ...auth import login_required, check_team_auth

View File

@@ -1,7 +1,7 @@
# Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license.
# See LICENSE in the project root for license information.
from urllib import unquote
from urllib.parse import unquote
from falcon import HTTPError, HTTP_201, HTTPBadRequest, HTTPNotFound
from ujson import dumps as json_dumps

View File

@@ -1,7 +1,7 @@
# Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license.
# See LICENSE in the project root for license information.
from urllib import unquote
from urllib.parse import unquote
from falcon import HTTPError, HTTP_201, HTTPBadRequest
from ujson import dumps as json_dumps
from ...utils import load_json_body, invalid_char_reg, create_audit
@@ -27,7 +27,7 @@ def get_roster_by_team_id(cursor, team_id, params=None):
where_params = []
where_vals = []
if params:
for key, val in params.iteritems():
for key, val in params.items():
if key in constraints:
where_params.append(constraints[key])
where_vals.append(val)

View File

@@ -1,7 +1,7 @@
# Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license.
# See LICENSE in the project root for license information.
from urllib import unquote
from urllib.parse import unquote
from falcon import HTTP_201, HTTPError, HTTPBadRequest
from ujson import dumps as json_dumps
@@ -26,7 +26,7 @@ columns = {
'scheduler': '`scheduler`.`name` AS `scheduler`'
}
all_columns = columns.keys()
all_columns = list(columns.keys())
constraints = {
'id': '`schedule`.`id` = %s',
@@ -88,7 +88,7 @@ def get_schedules(filter_params, dbinfo=None, fields=None):
from_clause = ['`schedule`']
if fields is None:
fields = columns.keys()
fields = list(columns.keys())
if any(f not in columns for f in fields):
raise HTTPBadRequest('Bad fields', 'One or more invalid fields')
if 'roster' in fields:
@@ -106,7 +106,7 @@ def get_schedules(filter_params, dbinfo=None, fields=None):
if 'id' not in fields:
fields.append('id')
fields = map(columns.__getitem__, fields)
fields = list(map(columns.__getitem__, fields))
cols = ', '.join(fields)
from_clause = ' '.join(from_clause)
@@ -119,7 +119,7 @@ def get_schedules(filter_params, dbinfo=None, fields=None):
connection, cursor = dbinfo
where = ' AND '.join(constraints[key] % connection.escape(value)
for key, value in filter_params.iteritems()
for key, value in filter_params.items()
if key in constraints)
query = 'SELECT %s FROM %s' % (cols, from_clause)
if where:
@@ -158,7 +158,7 @@ def get_schedules(filter_params, dbinfo=None, fields=None):
start = row.pop('start')
duration = row.pop('duration')
ret[schedule_id]['events'].append({'start': start, 'duration': duration})
data = ret.values()
data = list(ret.values())
if scheduler:
for schedule in data:

View File

@@ -94,7 +94,7 @@ def on_get(req, resp, service, role=None):
continue
dest = row.pop('destination')
ret[user]['contacts'][mode] = dest
data = ret.values()
data = list(ret.values())
for event in data:
override_number = team_override_numbers.get(event['team'])
if override_number and event['role'] == 'primary':

View File

@@ -2,7 +2,7 @@
# See LICENSE in the project root for license information.
import uuid
import time
from urllib import unquote
from urllib.parse import unquote
from falcon import HTTPNotFound, HTTPBadRequest, HTTPError
from ujson import dumps as json_dumps

View File

@@ -1,7 +1,7 @@
# Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license.
# See LICENSE in the project root for license information.
from urllib import unquote
from urllib.parse import unquote
from falcon import HTTPError, HTTP_201, HTTPBadRequest
from ujson import dumps as json_dumps
from ... import db

View File

@@ -95,7 +95,7 @@ def on_get(req, resp, team, role=None):
continue
dest = row.pop('destination')
ret[user]['contacts'][mode] = dest
data = ret.values()
data = list(ret.values())
for event in data:
if override_number and event['role'] == 'primary':
event['contacts']['call'] = override_number

View File

@@ -1,7 +1,7 @@
# Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license.
# See LICENSE in the project root for license information.
from urllib import unquote
from urllib.parse import unquote
from falcon import HTTPNotFound

View File

@@ -1,7 +1,7 @@
# Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license.
# See LICENSE in the project root for license information.
from urllib import unquote
from urllib.parse import unquote
from falcon import HTTPError, HTTP_201
from ujson import dumps as json_dumps
from ...auth import login_required, check_team_auth

View File

@@ -1,7 +1,7 @@
# Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license.
# See LICENSE in the project root for license information.
from urllib import unquote
from urllib.parse import unquote
from falcon import HTTPNotFound

View File

@@ -1,7 +1,7 @@
# Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license.
# See LICENSE in the project root for license information.
from urllib import unquote
from urllib.parse import unquote
from falcon import HTTPError, HTTP_201, HTTPBadRequest
from ujson import dumps as json_dumps
from ...utils import load_json_body, invalid_char_reg, subscribe_notifications, create_audit

View File

@@ -77,7 +77,7 @@ def on_get(req, resp, user_name):
formatted.append(event)
else:
links[event['link_id']].append(event)
for events in links.itervalues():
for events in links.values():
first_event = min(events, key=operator.itemgetter('start'))
first_event['num_events'] = len(events)
formatted.append(first_event)

View File

@@ -169,7 +169,7 @@ def on_put(req, resp, user_name):
if set_contacts:
contacts = []
for mode, dest in data['contacts'].iteritems():
for mode, dest in data['contacts'].items():
contact = {}
contact['mode'] = mode
contact['destination'] = dest

View File

@@ -88,7 +88,7 @@ def on_get(req, resp, user_name):
cursor.close()
connection.close()
resp.body = json_dumps(data.values())
resp.body = json_dumps(list(data.values()))
@login_required

View File

@@ -79,7 +79,7 @@ def get_user_data(fields, filter_params, dbinfo=None):
connection, cursor = dbinfo
where = ' AND '.join(constraints[key] % connection.escape(value)
for key, value in filter_params.iteritems()
for key, value in filter_params.items()
if key in constraints)
query = 'SELECT %s FROM %s' % (cols, from_clause)
if where:
@@ -106,7 +106,7 @@ def get_user_data(fields, filter_params, dbinfo=None):
continue
dest = row.pop('destination')
ret[user_id]['contacts'][mode] = dest
data = ret.values()
data = list(ret.values())
return data

View File

@@ -1,9 +1,6 @@
# Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license.
# See LICENSE in the project root for license information.
from __future__ import absolute_import
from urllib import unquote_plus
from urllib.parse import unquote_plus
from importlib import import_module
import falcon

View File

@@ -1,8 +1,6 @@
# Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license.
# See LICENSE in the project root for license information.
from __future__ import absolute_import
import logging
import time
import hmac
@@ -254,7 +252,7 @@ def init(application, config):
app_key_cache[row[0]] = row[1]
cursor.close()
connection.close()
logger.debug('loaded applications: %s', app_key_cache.keys())
logger.debug('loaded applications: %s', list(app_key_cache.keys()))
auth = importlib.import_module(config['module'])
auth_manager = getattr(auth, 'Authenticator')(config)

View File

@@ -1,8 +1,6 @@
# Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license.
# See LICENSE in the project root for license information.
from __future__ import absolute_import
from falcon import HTTPNotFound, HTTPUnauthorized, HTTPBadRequest
from falcon.util import uri
from oncall.api.v0.users import get_user_data
@@ -15,7 +13,7 @@ allow_no_auth = True
def on_post(req, resp):
login_info = uri.parse_query_string(req.context['body'])
login_info = uri.parse_query_string(req.context['body'].decode('utf-8'))
user = login_info.get('username')
password = login_info.get('password')

View File

@@ -41,7 +41,7 @@ class Authenticator:
connection = ldap.initialize(self.ldap_url)
connection.set_option(ldap.OPT_REFERRALS, 0)
attrs = ['dn'] + self.attrs.values()
attrs = ['dn'] + list(self.attrs.values())
ldap_contacts = {}
if not password:
@@ -58,7 +58,7 @@ class Authenticator:
return False
auth_user = result[0][0]
ldap_attrs = result[0][1]
for key, val in self.attrs.iteritems():
for key, val in self.attrs.items():
if ldap_attrs.get(val):
if type(ldap_attrs.get(val)) == list:
ldap_contacts[key] = ldap_attrs.get(val)[0]

View File

@@ -1,8 +1,5 @@
# Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license.
# See LICENSE in the project root for license information.
from __future__ import absolute_import
import sys
import yaml
import logging
@@ -40,7 +37,7 @@ default_timezone = None
def load_config_file(config_path):
with open(config_path) as h:
config = yaml.load(h)
config = yaml.safe_load(h)
if 'init_config_hook' in config:
try:
@@ -151,7 +148,7 @@ def main():
init_messengers(config.get('messengers', []))
worker_tasks = [spawn(worker) for x in xrange(100)]
worker_tasks = [spawn(worker) for x in range(100)]
reminder_on = False
if config['reminder']['activated']:
reminder_worker = spawn(reminder.reminder, config['reminder'])

View File

@@ -9,6 +9,7 @@ import multiprocessing
import gunicorn.app.base
from gunicorn.six import iteritems
import oncall.utils
import importlib
class StandaloneApplication(gunicorn.app.base.BaseApplication):
@@ -26,7 +27,7 @@ class StandaloneApplication(gunicorn.app.base.BaseApplication):
def load(self):
import oncall
reload(oncall.utils)
importlib.reload(oncall.utils)
import oncall.app
app = oncall.app.get_wsgi_app()

View File

@@ -4,9 +4,6 @@
# See LICENSE in the project root for license information.
# -*- coding:utf-8 -*-
from __future__ import print_function
import sys
import time
import importlib
@@ -63,7 +60,7 @@ def main():
for schedule in get_schedules({'team_id': team['id']}):
schedule_map[schedule['scheduler']['name']].append(schedule)
for scheduler_name, schedules in schedule_map.iteritems():
for scheduler_name, schedules in schedule_map.items():
schedulers[scheduler_name].schedule(team, schedules, (connection, db_cursor))
# Sleep until next time

View File

@@ -1,8 +1,6 @@
# Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license.
# See LICENSE in the project root for license information.
from __future__ import absolute_import
from collections import defaultdict
import logging
import importlib

View File

@@ -36,7 +36,7 @@ class influx(object):
return
now = str(datetime.now())
payload = []
for metric, value in metrics.iteritems():
for metric, value in metrics.items():
data = {
'measurement': self.appname,
'tags': {},

View File

@@ -34,7 +34,7 @@ class prometheus(object):
def send_metrics(self, metrics):
if not self.enable_metrics:
return
for metric, value in metrics.iteritems():
for metric, value in metrics.items():
if metric not in self.gauges:
self.gauges[metric] = Gauge(self.appname + '_' + metric, '')
logger.info('Setting metrics gauge %s to %s', metric, value)

View File

@@ -185,7 +185,7 @@ class Scheduler(object):
def weekday_from_schedule_time(self, schedule_time):
'''Returns 0 for Monday, 1 for Tuesday...'''
return (schedule_time / SECONDS_IN_A_DAY - 1) % 7
return (schedule_time // SECONDS_IN_A_DAY - 1) % 7
def epoch_from_datetime(self, dt):
'''
@@ -221,7 +221,7 @@ class Scheduler(object):
date = (tz.localize(date, is_dst=1)).astimezone(utc)
td = date - UNIX_EPOCH
# Convert timedelta to seconds
return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / 10 ** 6
return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) // 10 ** 6
# End time helpers
@@ -244,7 +244,7 @@ class Scheduler(object):
first_event = min(events, key=operator.itemgetter('start'))
end = max(e['start'] + e['duration'] for e in events)
period = end - first_event['start']
return ((period + SECONDS_IN_A_WEEK - 1) / SECONDS_IN_A_WEEK)
return ((period + SECONDS_IN_A_WEEK - 1) // SECONDS_IN_A_WEEK)
def calculate_future_events(self, schedule, cursor, start_epoch=None):
period = self.get_period_len(schedule)
@@ -394,8 +394,8 @@ class Scheduler(object):
self.set_last_epoch(schedule['id'], last_epoch, cursor)
# Delete existing events from the start of the first event
future_events = [filter(lambda x: x['start'] >= start_time, evs) for evs in future_events]
future_events = filter(lambda x: x != [], future_events)
future_events = [[x for x in evs if x['start'] >= start_time] for evs in future_events]
future_events = [x for x in future_events if x != []]
if future_events:
first_event_start = min(future_events[0], key=lambda x: x['start'])['start']
query = 'DELETE FROM %s WHERE schedule_id = %%s AND start >= %%s' % table_name

View File

@@ -1,4 +1,4 @@
import default
from . import default
class Scheduler(default.Scheduler):

View File

@@ -1,5 +1,5 @@
from oncall.utils import gen_link_id
import default
from . import default
import logging
logger = logging.getLogger()

View File

@@ -1,8 +1,5 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from __future__ import print_function
from docutils import nodes
from docutils.statemachine import ViewList
@@ -49,7 +46,7 @@ class AutofalconDirective(Directive):
app = autohttp_import_object(self.arguments[0])
for method, path, handler in get_routes(app):
docstring = handler.__doc__
if not isinstance(docstring, unicode):
if not isinstance(docstring, str):
analyzer = ModuleAnalyzer.for_module(handler.__module__)
docstring = force_decode(docstring, analyzer.encoding)
if not docstring and 'include-empty-docstring' not in self.options:

View File

@@ -4,7 +4,6 @@
# All rights reserved. Licensed under the BSD-2 Clause license.
# See LICENSE in the project root for license information.
from __future__ import absolute_import
import logging
import re
@@ -63,7 +62,7 @@ mimes = {
INDEX_CONTENT_SETTING = {
'user_setting_note': '',
'footer': '<ul><li>Oncall © LinkedIn 2017</li></ul>'.decode('utf-8'),
'footer': '<ul><li>Oncall © LinkedIn 2017</li></ul>',
}
SLACK_INSTANCE = None

View File

@@ -103,7 +103,7 @@ def fetch_ldap():
}
base = LDAP_SETTINGS['base']
attrs = ['distinguishedName'] + LDAP_SETTINGS['attrs'].values()
attrs = ['distinguishedName'] + list(LDAP_SETTINGS['attrs'].values())
query = LDAP_SETTINGS['query']
users = {}
@@ -184,7 +184,7 @@ def import_user(username, ldap_contacts, engine):
logger.exception('Failed to add user %s' % username)
return
stats['users_added'] += 1
for key, value in ldap_contacts.iteritems():
for key, value in ldap_contacts.items():
if value and key in modes:
logger.debug('\t%s -> %s' % (key, value))
user_contact_add_sql = 'INSERT INTO `user_contact` (`user_id`, `mode_id`, `destination`) VALUES (%s, %s, %s)'
@@ -328,7 +328,7 @@ def sync(config, engine):
logger.exception('Failed to add user %s' % username)
continue
stats['users_added'] += 1
for key, value in ldap_users[username].iteritems():
for key, value in ldap_users[username].items():
if value and key in modes:
logger.debug('\t%s -> %s' % (key, value))
user_contact_add_sql = 'INSERT INTO `user_contact` (`user_id`, `mode_id`, `destination`) VALUES (%s, %s, %s)'
@@ -424,5 +424,5 @@ def main(config):
if __name__ == '__main__':
config_path = sys.argv[1]
with open(config_path, 'r') as config_file:
config = yaml.load(config_file)
config = yaml.safe_load(config_file)
main(config)

View File

@@ -87,7 +87,7 @@ def sync_action(slack_client):
mode_ids = {row[1]: row[0] for row in cursor}
cursor.close()
slack_usernames = set(slack_users.viewkeys())
slack_usernames = set(slack_users.keys())
oncall_usernames = set(fetch_oncall_usernames(connection))
users_to_insert = slack_usernames - oncall_usernames

View File

@@ -10,8 +10,8 @@ from falcon import HTTPBadRequest
from importlib import import_module
from datetime import datetime
from pytz import timezone
from constants import ONCALL_REMINDER
import constants
from .constants import ONCALL_REMINDER
from . import constants
import re
invalid_char_reg = re.compile(r'[!"#%-,\.\/;->@\[-\^`\{-~]+')
@@ -30,7 +30,7 @@ def update_notification(x, y):
def read_config(config_path):
with open(config_path, 'r') as config_file:
return yaml.load(config_file)
return yaml.safe_load(config_file)
def create_notification(context, team_id, role_ids, type_name, users_involved, cursor, **kwargs):
@@ -56,7 +56,7 @@ def create_notification(context, team_id, role_ids, type_name, users_involved, c
for notification in notifications:
tz = notification['time_zone'] if notification['time_zone'] else 'UTC'
for var_name, timestamp in kwargs.iteritems():
for var_name, timestamp in kwargs.items():
context[var_name] = ' '.join([datetime.fromtimestamp(timestamp,
timezone(tz)).strftime('%Y-%m-%d %H:%M:%S'),
tz])

View File

@@ -65,7 +65,7 @@ def test_calculate_future_events_7_12_shifts(mocker):
mocker.patch('time.time').return_value = time.mktime(mock_dt.timetuple())
start = 3 * DAY + 12 * HOUR # Wednesday at noon
events = []
for i in xrange(7):
for i in range(7):
events.append({'start': start + DAY * i, 'duration': 12 * HOUR})
schedule_foo = {
'timezone': 'US/Eastern',
@@ -95,7 +95,7 @@ def test_calculate_future_events_14_12_shifts(mocker):
mocker.patch('time.time').return_value = time.mktime(mock_dt.timetuple())
start = 3 * DAY + 12 * HOUR # Wednesday at noon
events = []
for i in xrange(14):
for i in range(14):
events.append({'start': start + DAY * i, 'duration': 12 * HOUR})
schedule_foo = {
'timezone': 'US/Central',
@@ -106,7 +106,7 @@ def test_calculate_future_events_14_12_shifts(mocker):
future_events, last_epoch = scheduler.calculate_future_events(schedule_foo, None)
assert len(future_events) == 2
assert len(future_events[1]) == 14
days = range(21, 31) + range(1, 6)
days = list(range(21, 31)) + list(range(1, 6))
for ev, day in zip(future_events[1], days):
start_dt = utc.localize(datetime.datetime.utcfromtimestamp(ev['start']))
start_dt = start_dt.astimezone(timezone('US/Central'))