first draft completed

This commit is contained in:
2017-10-26 15:54:34 -04:00
parent f6f70f890c
commit 0908ce057f
3 changed files with 254 additions and 66 deletions

View File

@@ -34,6 +34,15 @@ def bot():
response = f'Okay, I added {sitter_name.title()} to sitters, with phone # {sitter_num}. '
print(sitters)
elif any(remove_word in body for remove_word in ['remove', 'delete']):
try:
sitter_name = remove_sitter(body)
except KeyError:
response = 'No such sitter. Please write "delete [sitter\'s first name]."'
else:
response = f'Okay, I removed {sitter_name.title()} from the sitters.'
resp = MessagingResponse()
resp.message(response)
return str(resp)
@@ -55,10 +64,22 @@ def add_sitter(body: str) -> Tuple[str, str]:
assert len(num_only) == 10
sitters[lowercase_name] = f'+1{num_only}'
phone_number = f'+1{num_only}'
sitters[lowercase_name] = {'num': phone_number,
'name': lowercase_name}
persist_sitters()
return name, sitters[lowercase_name]
return name, phone_number
def remove_sitter(body: str) -> str:
sitter_first_name = body.split(' ')[1]
sitter = sitters.get(sitter_first_name)
if sitter is None:
raise KeyError
del sitters[sitter_first_name]
persist_sitters()
return sitter_first_name
def persist_sitters():

View File

@@ -5,6 +5,7 @@ click==6.7
decorator==4.0.11
falcon==1.2.0
Flask==0.12.2
future==0.16.0
google-api-python-client==1.6.2
httplib2==0.10.3
hug==2.3.0
@@ -16,6 +17,7 @@ jedi==0.10.2
Jinja2==2.9.6
MarkupSafe==1.0
oauth2client==4.1.1
parsedatetime==2.4
parso==0.1.0
pexpect==4.2.1
pickleshare==0.7.4

View File

@@ -1,8 +1,12 @@
import datetime
from multiprocessing import Process
import os
from typing import Optional, Tuple
import time
from typing import Tuple, Dict
import parsedatetime as pdt
import pickle
from flask import request, Flask
from twilio.rest import Client as TwilioClient
from twilio.twiml.messaging_response import MessagingResponse
@@ -14,31 +18,26 @@ BOOKER_NUM = os.getenv('MY_TWILIO_NUM')
COUNTRY_CODE = f'+{os.getenv("TWILIO_COUNTRY_CODE")}'
TIMEOUT_MINUTES = 120
sitters = {}
if os.path.exists('sitters.p'):
sitters = pickle.load(open('sitters.p', 'rb'))
bookings = {}
if os.path.exists('bookings.p'):
bookings = pickle.load(open('bookings.p', 'rb'))
help_add = 'You can add a sitter by giving me their first name and 10-digit phone number'
help_text = help_add + ', or book a sitter by ' \
'specifying a date and time. You can also remove a sitter from the list ' \
'with "delete" or "remove" and then their first name.'
'specifying a date and time. You can also remove a sitter from the list ' \
'with "delete" or "remove" and then their first name.'
app = Flask(__name__)
app.config.from_object(__name__)
class NoneAvailable(Exception):
pass
cal = pdt.Calendar()
def say_hi_ask_for_sitters():
message = "Hey, can you tell me your sitters' info one at a time? " \
"First name then phone num. By the way, if you need help, type 'halp' at any time"
twilio_client.api.account.messages.create(to=MY_CELL, from_=BOOKER_NUM, body=message)
def load_from_pickle(var_name: str) -> dict:
payload = {}
if os.path.exists(f'{var_name}.p'):
payload = pickle.load(open(f'{var_name}.p', 'rb'))
return payload
sitters, bookings = load_from_pickle('sitters'), load_from_pickle('bookings')
sitters_num_name_lookup = {v['num']: k for k, v in sitters.items()}
@app.route('/bot', methods=['POST'])
@@ -47,66 +46,222 @@ def bot() -> str:
body = request.values.get('Body').lower()
resp = MessagingResponse()
response = None
response = ''
if not from_ == MY_CELL:
return str(resp.message(''))
if from_ == MY_CELL:
if has_phone_num(body):
try:
sitter_name, sitter_num = add_sitter(body)
except (AssertionError, ValueError):
response = 'Sorry, did you mean to add a sitter? Please try again.'
else:
response = f'Okay, I added {sitter_name.title()} to sitters, with phone # {sitter_num}. '
elif any(remove_word in body for remove_word in ['remove', 'delete']):
try:
sitter_name = remove_sitter(body)
except KeyError:
response = 'No such sitter. Please write "delete [sitter\'s first name]."'
else:
response = f'Okay, I removed {sitter_name.title()} from the sitters.'
if 'halp' in body:
if not sitters:
response = f'You don\'t have any sitters yet. {help_add}.'
else:
sitter_list = 'Your sitters are ' + ' and '.join(
f'{sitter_name.title()}' for sitter_name in sitters) + '.'
response = f'{sitter_list} {help_text}'
try:
start_time, end_time = request_booking(body)
except ValueError:
response = 'Please specify an end time (e.g. "tomorrow 5pm to 10pm").'
else:
booking_string = make_booking_string(start_time, end_time)
response = f'Okay, I will reach out to the sitters about sitting on {booking_string}.'
elif has_phone_num(body):
try:
sitter_name, sitter_num = add_sitter(body)
except (AssertionError, ValueError):
response = 'Sorry, did you mean to add a sitter? Please try again.'
else:
response = f'Okay, I added {sitter_name.title()} to sitters, with phone # {sitter_num}. '
elif any(remove_word in body for remove_word in ['remove', 'delete']):
try:
sitter_name = remove_sitter(body)
except KeyError:
response = 'No such sitter. Please write "delete [sitter\'s first name]."'
else:
response = f'Okay, I removed {sitter_name.title()} from the sitters.'
if response is None:
response = 'I wasn\'t sure what to do with your input. ' + help_text
else:
try:
book_sitter(body)
except (AssertionError, ValueError):
response = 'Sorry, did you mean to book a sitter? Please try again.'
if response is None:
response = 'I wasn\'t sure what to do with your input. ' + help_text
sitter_name = sitters_num_name_lookup.get(from_)
if sitter_name is not None:
response = accept_or_decline(sitter_name, body)
resp.message(response)
return str(resp)
def accept_or_decline(sitter_name: str, body: str) -> str:
body = body.strip()
global bookings
global sitters
bookings, sitters = load_from_pickle('bookings'), load_from_pickle('sitters')
sitter = sitters[sitter_name]
sitter_offers = [k for k, v in bookings.items()
if sitter_name in v['offered']
if v['offered'][sitter_name] not in ['yes', 'no']]
if body not in ['yes', 'no', 'n', 'y'] and not body.isnumeric():
return f'Hm, I\'m not sure what you meant, {sitter_name.title()}. Please write "yes", "no", ' \
f'or a number (if there are any pending bookings).'
action = None
if not body.isnumeric():
action = 'accept' if body in ['yes', 'y'] else 'decline'
if len(sitter_offers) == 0:
return f'Sorry, {sitter_name.title()}, it looks like either that gig ' \
f'is already booked or there aren\'t any pending gigs.'
elif len(sitter_offers) == 1:
offer = sitter_offers[0]
else:
sitter_offers_string = ", ".join([f'{idx + 1}) {make_booking_string(*sitter_offer)}'
for idx, sitter_offer in enumerate(sitter_offers)])
if body.isnumeric():
try:
offer = sitter_offers[int(body) - 1]
except IndexError:
action = sitter['next action']
return f'Sorry, which booking did you want to {action}? {sitter_offers_string}'
else:
sitter['next action'] = action
persist_sitters()
return f'Sorry, which booking did you want to {action}? {sitter_offers_string}'
try:
action = action or sitter.pop('next action')
except KeyError:
raise KeyError(f'no next action, and sitter_offers is {sitter_offers}, so offer is {offer}.')
booking_string = make_booking_string(*offer)
if action == 'accept':
if not bookings.get(offer):
return f'Sorry, {sitter_name.title()}, it looks like {booking_string} is already booked.'
if any(bookings[offer]['offered'][sitter_] == 'yes'
for sitter_ in bookings[offer]['offered'].keys()):
if bookings[offer]['offered'][sitter_name] == 'yes':
return f'You already accepted {booking_string}, {sitter_name.title()}!'
return f'Sorry, {sitter_name.title()}, it looks like {booking_string} is already booked.'
bookings[offer]['offered'][sitter_name] = 'yes'
persist_bookings()
update_client(f'{sitter_name.title()} agreed to babysit on {booking_string}!')
return f'Awesome, {sitter_name.title()}! See you on {booking_string}.'
else:
if bookings[offer]['offered'][sitter_name] == 'yes':
return f'You already accepted {booking_string}, {sitter_name.title()}!'
bookings[offer]['offered'][sitter_name] = 'no'
persist_bookings()
return f'Okay, no problem, {sitter_name.title()}! Next time.'
def make_booking_string(start_time: datetime.datetime, end_time: datetime.time) -> str:
start_time_and_date_string = start_time.strftime('%-m/%-d from %-I:%M%p')
end_time_string = end_time.strftime('%-I:%M%p')
return f'{start_time_and_date_string} to {end_time_string}'
def book_forever():
while True:
sitters_, bookings_ = load_from_pickle('sitters'), load_from_pickle('bookings')
if bookings_:
bookings_keys_to_delete = []
for booking_start_and_end, offered_dict in bookings_.items():
booking = bookings_[booking_start_and_end]
offers = booking['offered']
if any(v == 'yes' for k, v in offers.items()):
continue
sitter_to_offer_name = None
booking_string = make_booking_string(*booking_start_and_end)
if len(offers) == 0:
first_sitter_name = list(sitters_)[0]
sitter_to_offer_name = first_sitter_name
else:
last_offer_was_minutes_ago = 0
offers_without_a_no = {k: v for k, v in offers.items() if v != 'no'}
if len(offers_without_a_no) > 0:
last_offer: datetime.datetime = max(offers_without_a_no.values())
last_offer_was_minutes_ago \
= (datetime.datetime.now() - last_offer).total_seconds() / 60
if len(offers_without_a_no) == 0 or last_offer_was_minutes_ago > 1:
# if len(offers_without_a_no) == 0 or last_offer_was_minutes_ago > 60:
# if len(sitters_) == len(offers):
# bookings_keys_to_delete.append(booking_start_and_end)
# update_client(
# f'No babysitters are available for {booking_string}! Deleting request.')
# else:
for sitter in sitters_:
if sitter not in offers:
sitter_to_offer_name = sitter
break
if sitter_to_offer_name is not None:
sitter_to_offer = sitters_[sitter_to_offer_name]
offer_booking(sitter_to_offer, booking_string)
offers[sitter_to_offer_name] = datetime.datetime.now()
update_client(
f'Okay, I offered {booking_string} to {sitter_to_offer_name.title()}.')
if len(bookings_keys_to_delete) > 0:
for k in bookings_keys_to_delete:
del bookings_[k]
persist_bookings(bookings_)
# time.sleep(60)
time.sleep(5)
def offer_booking(sitter_dict: dict, booking_string: str) -> None:
message = f'{sitter_dict["name"].title()}, are you available to babysit on {booking_string}?'
twilio_client.api.account.messages.create(to=sitter_dict['num'],
from_=BOOKER_NUM,
body=message)
def update_client(string: str) -> None:
twilio_client.api.account.messages.create(to=MY_CELL, from_=BOOKER_NUM, body=string)
def has_phone_num(string):
return len([char for char in string if char.isnumeric()]) == 10
def syndicate_and_book(session_start: datetime.datetime, session_end: datetime.datetime) -> Optional[str]:
# blast out to all sitters
# give it to the first 'yes'
# handle late 'yeses' and all 'noes'
# wait until everyone says no or timeout_minutes runs out
pass
def request_booking(body: str) -> Tuple[datetime.datetime, datetime.time]:
session_start, session_end = parse_booking_request(body)
global bookings
bookings = load_from_pickle('bookings')
bookings[(session_start, session_end)] = {'offered': dict()}
persist_bookings()
return session_start, session_end
def book_sitter(body: str) -> Optional[str]:
session_start, session_end = parse_sitter_request(body)
syndicate_and_book(session_start, session_end)
def parse_booking_request(body: str) -> Tuple[datetime.datetime, datetime.time]:
start_string, end_string = body.split(' to ')
session_start = cal.parseDT(start_string)[0]
session_end_time = datetime.time(cal.parse(end_string)[0].tm_hour)
return session_start, session_end_time
def add_sitter(body: str) -> Tuple[str, str]:
@@ -120,9 +275,12 @@ def add_sitter(body: str) -> Tuple[str, str]:
assert len(num_only) == 10
sitters[lowercase_name] = f'{COUNTRY_CODE}num_only'
phone_number = f'{COUNTRY_CODE}{num_only}'
sitters[lowercase_name] = {'num': phone_number,
'name': lowercase_name}
persist_sitters()
return name, sitters[lowercase_name]
return name, phone_number
def remove_sitter(body: str) -> str:
@@ -139,7 +297,14 @@ def persist_sitters():
pickle.dump(sitters, open('sitters.p', 'wb'))
def persist_bookings(bookings_: dict = None):
pickle.dump(bookings_ if bookings_ is not None else bookings, open('bookings.p', 'wb'))
if __name__ == '__main__':
if not sitters:
say_hi_ask_for_sitters()
app.run(debug=True, port=4567)
update_client('Hi, this is Babysitter Bot, on the job! Send me a date with time range and '
'I\'ll try to book one of our sitters!')
p = Process(target=book_forever)
p.start()
app.run(debug=True, port=8000)
p.join()