Skip to content

Commit

Permalink
Refactor code
Browse files Browse the repository at this point in the history
Refactors code to follow PEP8 standards.
  • Loading branch information
Sliicy committed May 10, 2020
1 parent b211e00 commit 6bc9179
Showing 1 changed file with 134 additions and 45 deletions.
179 changes: 134 additions & 45 deletions eruv_alerts.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# PYTHON_ARGCOMPLETE_OK

# This script collects candle-lighting times from hebcal.com, weather reports from openweathermap.org, and sends an SMS to every subscriber from the Eruv Alerts Google Spreadsheet, based on the city.
# This script collects candle-lighting times from hebcal.com, weather
# reports from openweathermap.org, and sends an SMS to every subscriber
# from the Eruv Alerts Google Spreadsheet, based on the city.

# Imports:
import random
from random import randint
from time import sleep
import urllib.request
import json
import argparse

# 3rd party additional imports:
import argcomplete
from twilio.rest import Client
import gspread
from oauth2client.service_account import ServiceAccountCredentials
import urllib.request, json, argparse, argcomplete

# Add random time delay to prevent being flagged:
from random import randint
from time import sleep
import random

# This function, being data-agnostic, extracts all key-values found in any multilevel JSON file, without knowing the order/hierarchy of the data:
# Obtained from: https://hackersandslackers.com/extract-data-from-complex-json-python/
def extract_values(obj, key):
"""Pull all values of specified key from nested JSON."""
"""This function, being data-agnostic, extracts all key-values found in any multilevel JSON file, without knowing the order/hierarchy of the data.
Obtained from: https://hackersandslackers.com/extract-data-from-complex-json-python/
Pull all values of specified key from nested JSON."""
arr = []

def extract(obj, arr, key):
"""Recursively search for values of key in JSON tree."""
if isinstance(obj, dict):
Expand All @@ -35,6 +41,7 @@ def extract(obj, arr, key):
results = extract(obj, arr, key)
return results


def shorten_message(message):
"""This function recursively tries to shorten a message
to under 160 characters"""
Expand All @@ -45,24 +52,61 @@ def shorten_message(message):
return shorten_message(message.replace(' (50 min)', ''))
else:
# Warn if message still exceeds 160 characters:
print('Message for ' + city + ' exceeds 160 character limit!\nMessage: ' + message)
print(
'Message for ' +
city +
' exceeds 160 character limit!\nMessage: ' +
message)
quit()


# Define a list of random greetings to reduce spam detection and add variety:
greetings = ['a great', 'a wonderful', 'an amazing', 'a good']

# Initialize argument interpretation:
parser = argparse.ArgumentParser(description='This script sends SMS messages via Twilio to subscribers on a Google Sheet.')
parser.add_argument('--available-cities', help='Get a list of all available cities.', action='store_true')
parser.add_argument('--delayed', help='Slowly send out each SMS between 0 - 2 seconds.', action='store_true')
parser.add_argument('--donate', help='Append reminder to donate for select cities.', action='store_true')
parser.add_argument('--blacklist', nargs='+', help='Append a list of cities (space delimited) to skip alerting (cities with 2+ names should be enclosed in quotes). Available cities can be found using the --available-cities flag. This argument will override the whitelist argument.')
parser.add_argument('--no-candlelighting', help='Skip appending candle-lighting times.', action='store_true')
parser.add_argument('--no-havdalah', help='Skip appending havdalah times.', action='store_true')
parser.add_argument('--no-weather', help='Skip checking for weather updates. May be dangerous if there is major weather to report.', action='store_true')
parser.add_argument('--whitelist', nargs='+', help='Append a list of cities (space delimited) to only alert (cities with 2+ names should be enclosed in quotes). All other cities will be skipped. Available cities can be found using the --available-cities flag. The blacklist argument will override this.')
parser.add_argument('--test', help='Test run without actually sending.', action='store_true')
parser.add_argument('-v', '--verbose', help='Verbose output. Useful for debugging.', action='store_true')
parser = argparse.ArgumentParser(
description='This script sends SMS messages via Twilio to subscribers on a Google Sheet.')
parser.add_argument(
'--available-cities',
help='Get a list of all available cities.',
action='store_true')
parser.add_argument(
'--delayed',
help='Slowly send out each SMS between 0 - 2 seconds.',
action='store_true')
parser.add_argument(
'--donate',
help='Append reminder to donate for select cities.',
action='store_true')
parser.add_argument(
'--blacklist',
nargs='+',
help='Append a list of cities (space delimited) to skip alerting (cities with 2+ names should be enclosed in quotes). Available cities can be found using the --available-cities flag. This argument will override the whitelist argument.')
parser.add_argument(
'--no-candlelighting',
help='Skip appending candle-lighting times.',
action='store_true')
parser.add_argument(
'--no-havdalah',
help='Skip appending havdalah times.',
action='store_true')
parser.add_argument(
'--no-weather',
help='Skip checking for weather updates. May be dangerous if there is major weather to report.',
action='store_true')
parser.add_argument(
'--whitelist',
nargs='+',
help='Append a list of cities (space delimited) to only alert (cities with 2+ names should be enclosed in quotes). All other cities will be skipped. Available cities can be found using the --available-cities flag. The blacklist argument will override this.')
parser.add_argument(
'--test',
help='Test run without actually sending.',
action='store_true')
parser.add_argument(
'-v',
'--verbose',
help='Verbose output. Useful for debugging.',
action='store_true')
argcomplete.autocomplete(parser)
arguments = parser.parse_args()

Expand All @@ -72,25 +116,32 @@ def shorten_message(message):

# Display ignored cities:
if arguments.blacklist and arguments.verbose and not arguments.whitelist:
print('Cities that will be skipped: ' + str([x.title() for x in arguments.blacklist]) + '\n')
print('Cities that will be skipped: ' +
str([x.title() for x in arguments.blacklist]) + '\n')

# Display whitelisted cities:
if arguments.whitelist and arguments.verbose:
if arguments.blacklist:
print('Only these cities will be notified: ' + str(list(set([x.title() for x in arguments.whitelist]) - set([x.title() for x in arguments.blacklist]))) + '\n')
print('Only these cities will be notified: ' +
str(list(set([x.title() for x in arguments.whitelist]) -
set([x.title() for x in arguments.blacklist]))) +
'\n')
else:
print('Only these cities will be notified: ' + str([x.title() for x in arguments.whitelist]) + '\n')
print('Only these cities will be notified: ' +
str([x.title() for x in arguments.whitelist]) + '\n')

# Google Authentication from external JSON file:
scope = ['https://spreadsheets.google.com/feeds', 'https://www.googleapis.com/auth/drive']
creds = ServiceAccountCredentials.from_json_keyfile_name('keys/google_auth.json', scope)
scope = ['https://spreadsheets.google.com/feeds',
'https://www.googleapis.com/auth/drive']
creds = ServiceAccountCredentials.from_json_keyfile_name(
'keys/google_auth.json', scope)
gclient = gspread.authorize(creds)
if arguments.verbose:
print('Google Authenticated successfully.\n')

# Twilio Authentication from external JSON file:
with open('keys/twilio_auth.json') as file:
twilio_file = json.load(file)
twilio_file = json.load(file)
client = Client(twilio_file['account-sid'], twilio_file['password'])
if arguments.verbose:
print('Twilio Authenticated successfully.\n')
Expand All @@ -106,7 +157,8 @@ def shorten_message(message):
if arguments.verbose:
print('Google Sheets loaded successfully.\n')

# Create arrays of all elements from the sheets. Skip top row (Timestamp, Phone Number, City, Rabbi's City, Zip Code):
# Create arrays of all elements from the sheets. Skip top row (Timestamp,
# Phone Number, City, Rabbi's City, Zip Code):
all_numbers = subscriber_sheet.col_values(2)[1:]
all_user_cities = subscriber_sheet.col_values(3)[1:]
all_rabbi_cities = rabbi_sheet.col_values(3)[1:]
Expand Down Expand Up @@ -136,7 +188,10 @@ def shorten_message(message):
# Skip if whitelist enabled and city isn't whitelisted (case insensitive):
if arguments.whitelist:
if not city.lower() in [x.lower() for x in arguments.whitelist]:
print('\nSkipping ' + str(city) + " because it isn't whitelisted!\n")
print(
'\nSkipping ' +
str(city) +
" because it isn't whitelisted!\n")
city_index += 1
continue

Expand All @@ -161,18 +216,28 @@ def shorten_message(message):
quit()

# Get Candle-lighting, Havdalah, and Parsha/Chag from hebcal.com:
hebcal_URL = 'https://www.hebcal.com/shabbat/?cfg=json&zip=' + str(zipcode) + '&m=50&a=on'
response = json.loads(urllib.request.urlopen(hebcal_URL, timeout=15).read().decode('utf-8'))
hebcal_URL = 'https://www.hebcal.com/shabbat/?cfg=json&zip=' + \
str(zipcode) + '&m=50&a=on'
response = json.loads(
urllib.request.urlopen(
hebcal_URL,
timeout=15).read().decode('utf-8'))

# Find first occurrence of Candle-lighting from JSON:
candle_lighting = [i for i in extract_values(response, 'title') if 'Candle' in i][0]
candle_lighting = [
i for i in extract_values(
response,
'title') if 'Candle' in i][0]

# Find first occurrence of Havdalah from JSON only if Havdalah exists:
havdalah = ''

# Verify there's a Havdalah entry first:
if len([i for i in extract_values(response, 'title') if 'Havdalah' in i]) > 0:
havdalah = [i for i in extract_values(response, 'title') if 'Havdalah' in i][0]
havdalah = [
i for i in extract_values(
response,
'title') if 'Havdalah' in i][0]
havdalah = havdalah + '. '
if len(havdalah) == 0:
print('No Havdalah time detected!')
Expand All @@ -184,7 +249,10 @@ def shorten_message(message):
if [i for i in extract_values(response, 'title') if 'Parsha' in i]:

# Find first occurrence of Parsha from JSON:
parsha = [i for i in extract_values(response, 'title') if 'Parsha' in i][0] + '.'
parsha = [
i for i in extract_values(
response,
'title') if 'Parsha' in i][0] + '.'

else:

Expand All @@ -199,8 +267,15 @@ def shorten_message(message):
if arguments.no_weather:
print('No weather is being reported!\n')
else:
weather_response = json.loads(urllib.request.urlopen('https://api.openweathermap.org/data/2.5/weather?zip=' + str(zipcode) + ',us&appid=' + open_weather_map['api-key'], timeout=15).read().decode('utf-8'))
temperature = 'Temperature: ' + str(int(1.8 * (weather_response['main']['temp'] - 273.15) + 32)) + 'F'
weather_response = json.loads(
urllib.request.urlopen(
'https://api.openweathermap.org/data/2.5/weather?zip=' +
str(zipcode) +
',us&appid=' +
open_weather_map['api-key'],
timeout=15).read().decode('utf-8'))
temperature = 'Temperature: ' + \
str(int(1.8 * (weather_response['main']['temp'] - 273.15) + 32)) + 'F'
humidity = str(weather_response['main']['humidity']) + '% humid'
if arguments.verbose and not arguments.no_weather:
print('Reported temperature for ' + city + ': ' + temperature + '\n')
Expand All @@ -210,7 +285,8 @@ def shorten_message(message):
prequel = ' The '
sequel = ''

if [i for i in extract_values(weather_response, 'description') if 'thunderstorm' in i or 'tornado' in i]:
if [i for i in extract_values(
weather_response, 'description') if 'thunderstorm' in i or 'tornado' in i]:
prequel = ' As of now, the '
sequel = 'If winds exceed 35 mph, consider the Eruv down. '

Expand All @@ -228,32 +304,45 @@ def shorten_message(message):
havdalah = ''

# Final message:
message = parsha + prequel + city + ' Eruv is ' + city_statuses[city_index] + '. ' + sequel + candle_lighting + '. ' + havdalah + ('Have ' + random.choice(greetings) + ' Shabbos' + holiday + '!' if sequel == '' else '.')
message = parsha + prequel + city + ' Eruv is ' + city_statuses[city_index] + '. ' + sequel + candle_lighting + '. ' + havdalah + (
'Have ' + random.choice(greetings) + ' Shabbos' + holiday + '!' if sequel == '' else '.')

# Try to shorten the message if necessary:
message = shorten_message(message)

# Add optional parameters to select cities: (links may be flagged as spam)
# Add optional parameters to select cities: (links may be
# flagged as spam)
if arguments.donate and city == 'North Miami Beach':
message = message + ' Please visit bit.ly/nmberuv to cover the costs.'

# Sanitize the phone number from special characters:
clean_number = '+1' + str(all_numbers[user_index]).replace("-", "").replace(" ", "").replace("(", "").replace(")", "").replace(".", "").replace("_", "")
clean_number = '+1' + str(
all_numbers[user_index]).replace("-", "").replace(" ", "").replace("(", "").replace(")", "").replace(".", "").replace("_", "")

# Display and send:
if arguments.verbose:
print(clean_number + ' > ' + message)

# Send if no testing argument:
if not arguments.test:
twilio_message = client.messages.create(to=clean_number, from_=twilio_file['phone'], body=message)
twilio_message = client.messages.create(
to=clean_number, from_=twilio_file['phone'], body=message)

# Wait a random amount of seconds between sending (0 - 2 seconds):
# Wait a random amount of seconds between sending (0 - 2
# seconds):
if arguments.delayed:
sleep(randint(0,2))
sleep(randint(0, 2))

# Keep track of total # of users:
population += 1
user_index += 1
print('\n' + str(population) + ' users ' + ('would have been ' if arguments.test else '') + 'notified that ' + city + ' is ' + city_statuses[city_index] + '.\n')
print('\n' +
str(population) +
' users ' +
('would have been ' if arguments.test else '') +
'notified that ' +
city +
' is ' +
city_statuses[city_index] +
'.\n')
city_index += 1

0 comments on commit 6bc9179

Please sign in to comment.