Introduction
In this article, we are going to build an application that sends appointment notifications to users over whatsapp via twilio. According to statistics, 90.4% of the young generation are active on social media so this approach of sending notifications will be more effective in setting remiders without being intrusive.
User Journey
An API request is sent to the application containing the appointment details (title, description, phone and time). Once the appointment time is reached, the user gets notified on whatsapp about the appointment.
Tutorial Requirements
To follow this tutorial, you are expected to:
- Have sufficient understanding of Python and Flask
- Have Python 3 installed on your machine
- Have MongoDB installed on your machine
- Have a smartphone with an active phone number and Whatsapp installed
- Have a Twilio account. You can create a free account if you are new to Twilio.
Setting up the Twilio Whatsapp Sandbox
First of all, you need to create a Twilio account if you don’t have one or sign in if you have one. You will need to activate Twilio Whatsapp sandbox since we are going to be working with Whatsapp. The sandbox allows you to immediately test with Whatsapp using a phone number without waiting for you Twilio number to be approved by Whatsapp. You can request production access for your Twilio phone number here.
To connect to the sandbox, log into your Twilio Console and select Programmable SMS on the side menu. After that, click on Whatsapp. This will open the Whatsapp sandbox page which contains your sandbox phone number and a join code. Send the join code starting with the word “join” to your sandbox phone number to enable the Whatsapp sandbox for your phone. You will then receive a message confirming the activation of the phone number to use the sandbox.
Application Setup
We are going to be using the Flask framework to build the application and MongoDB as our preferred database.
Creating the Application Directory and Virtual Environment
Run the following commands in your terminal to create the project folder and virtual environment for this project.
- Create project directory named
whatsapp_appointments
mkdir whatsapp_appointments - Enter into the project directory cd whatsapp_appointments
- Create a virtual environment for the project python3 -m venv venv
- Activate the virtual environment
For macOS and Linux users:
source venv/bin/activate
For windows users:
venv\Scripts\activate
The virtual environment helps create an isolated environment separate from the global python environment to run the project.
Structuring the Project
In the application directory, run the commands below to set up the folder structure:
mkdir controllers jobs services utils
touch app.py
The above commands create 4 folders and a file called app.py
in the application directory.
Your application directory should look like this:
Installing Project Dependencies
Finally, let’s install all the dependencies used in this project.
- Flask: This library will be used for running our web server.
- Pymongo: This library will be used to interface with the MongoDB database on your computer.
- Twilio Python Helper Library: This library will be used to send Whatsapp messages to users.
Run the command below to install these dependencies:
pip install Flask pymongo twilio
Building appointment reminders
In this section, we are going to write the logic for appointment reminders. We are going to use the mutiprocessing
module in python to build the reminders logic. The main reason we are using processes over threads in this section is because in python, there is no safe way to terminate threads and we will need to edit and cancel appointments 🤷🏿♂️.
First of all, create 2 files called __init__.py
and appointments.py
in the jobs/
directory. Also, create a file called whatsapp.py
in the utils/
directory.
Copy the code below into the whatsapp.py
file:
import os
from twilio.rest import Client
ACCOUNT_SID = os.getenv('ACCOUNT_SID')
AUTH_TOKEN = os.getenv('AUTH_TOKEN')
TWILIO_NUMBER = os.getenv('TWILIO_NUMBER')
client = Client(ACCOUNT_SID, AUTH_TOKEN)
def format_message(appointment):
message = "You have an appointment\n"
message += f"Title: {appointment['title']}\n"
message += f"Description: {appointment['description']}\n"
message += f"Time: {appointment['time']}"
return message
def send_message(appointment):
message = client.messages.create(
from_=f'whatsapp:{TWILIO_NUMBER}',
body=format_message(appointment),
to=f"whatsapp:{appointment['phone']}"
)
return message.sid
In the above code, we have the logic for sending whatsapp messages. Note that the ACCOUNT_SID
, AUTH_TOKEN
, TWILIO_NUMBER
are set as environment variables and we use os.getenv
to access them. To send whatsapp messages, you must prefix the phone numbers with whatsapp:
else it will be sent as sms. The format_message
function accepts an appointment object and represents it in the following format:
You have an appointment
Title: <Appointment Title>
Description: <Appointment Description>
Time: <Appointment Time>
Next, we are going to write the reminder’s logic. Open the file jobs/appointments.py
and copy the following code inside:
import time
from datetime import datetime
from utils.whatsapp import send_message
def start_appointment_job(appointment):
print(f"=====> Scheduled Appointment {appointment['_id']} for :> {appointment['time']}")
if datetime.now() > appointment['time']:
return
diff = appointment['time'] - datetime.now()
time.sleep(diff.seconds)
send_message(appointment)
The start_appointment_job
function first checks if the appointment is in the past and if so, it returns. We use the time.sleep
function to tell the application to pause execution for the number of seconds it takes for the appointment’s time to be reached. Once it finishes sleeping, it calls the send_message
function written earlier to notify the user of the appointment via whatsapp.
Next, we are going to write the service functions for creating, getting, updating and deleting appointments. Create a file called appointments.py
in the services/
folder and copy the following inside:
from bson.objectid import ObjectId
from pymongo import MongoClient
client = MongoClient('mongodb://localhost/')
appointments = client['twilio']['appointments']
def create_appointment(data):
appointments.insert_one(data)
def get_appointment(appointment_id):
result = appointments.find_one({'_id': ObjectId(appointment_id)})
return dict(result) if result else None
def get_appointments(conditions):
if '_id' in conditions:
conditions['_id'] = ObjectId(conditions['_id'])
results = appointments.find(conditions)
data = []
for result in results:
data.append(dict(result))
return data
def update_appointment(appointment_id, data):
appointment = appointments.find_one_and_update({'_id': ObjectId(appointment_id)}, {'$set': data},
return_document=True)
return appointment
def delete_appointment(appointment_id):
appointments.find_one_and_delete({'_id': ObjectId(appointment_id)})
In the above code, we write functions to perform CRUD operations on the appointments
collections in mongodb. We use the pymongo
library to connect to our local mongodb instance. Note that the _id
field cannot be sent as a string else no data will be returned, we have to cast it to an ObjectId
instance which mongodb uses.
Finally, we are going to write the logic for creating processes for an appointment and modifying them based on the change in appointments. Open jobs/__init__.py
and copy the following code inside:
from multiprocessing import Process
from datetime import datetime
from .appointments import start_appointment_job
from services.appointments import get_appointments
WORKERS = {}
def terminate_worker(worker):
try:
worker.terminate()
worker.join()
worker.close()
except Exception as err:
print('====> Error occurred terminating process', err)
def schedule_appointment(appointment):
appointment_id = str(appointment['_id'])
worker = Process(target=start_appointment_job, args=(appointment,))
worker.start()
WORKERS[appointment_id] = worker
def update_scheduled_appointment(appointment_id, updated_appt):
worker = WORKERS[appointment_id]
terminate_worker(worker)
new_worker = Process(target=start_appointment_job, args=(updated_appt,))
new_worker.start()
WORKERS[appointment_id] = new_worker
def delete_scheduled_appointment(appointment_id):
worker = WORKERS[appointment_id]
terminate_worker(worker)
del WORKERS[appointment_id]
def init_workers():
print('=====> Initializing workers')
appts = get_appointments({})
for appt in appts:
if datetime.now() > appt['time']:
continue
schedule_appointment(appt)
def close_workers():
for appointment_id, worker in WORKERS.items():
terminate_worker(worker)
In the above code, when we want to schedule an appointment, we create a Process
targeting the start_appointment_job
function with the appointment
object as argument. Note that we have a dictionary objection named WORKERS
where we store the process object with the appointment_id
as key, this is what we use to get the Process
object in case of an update.
To update an appointment, we get the process object via the appointment_id
and terminate it. We then create a new Process
object with the updated details and update the reference in the WORKERS
dictionary.
To delete an appointment, we get the Process
object and terminate it. We then delete the key from the WORKERS
dictionary so that it doesn’t have a reference there again.
Note that, to terminate a Process
object, we first call the terminate
function on it when sends a SIGTERM
signal to the process. The join
function waits for the Process
object to terminate and the close
function tells the process to release the resources that it holds.
Lets say our application restarts, we want to be able to recreate our Process objects and that’s where the init_workers
function comes into play. It gets all the appointments and calls the schedule_appointment
function on appointments that are still pending.
Building the API
In this section, we are going to be building the api routes which we are going use to create, update, get and delete appointments. First of all, we have to create utility functions for handling both our requests and our api responses. Create 2 files called request.py
and response.py
in the utils/
folder. Copy the code below into the request.py
file:
from datetime import datetime
def validate_body(data, required_fields):
for field in required_fields:
if field not in data:
return False, field
return True, None
def parse_appointment(body):
try:
if body.get('time'):
time_obj = datetime.strptime(body['time'], '%Y-%m-%d %H:%M')
body['time'] = time_obj
return True, None
except Exception as err:
return False, str(err)
From the above code, we can see that the validate_body
function takes in a data dictionary and a list of required fields. It checks the required fields against the data dictionary and returns a status of False if a field is absent. The parse_appointment
function is used to convert the time
field in an appointment object into a datetime object by the use of datetime.strptime
function which accepts the time string and the time format. The function returns false if it’s not able to convert the string into a datetime object.
Next, copy the code below into the response.py
file:
import json
import time
from flask import jsonify
def stringify_objectid(data):
str_data = json.dumps(data, default=str)
return json.loads(str_data)
def response(status, message, data, status_code=200):
"""
:param status : Boolean Status of the request
:param status_code: Status Code of response
:param message : String message to be sent out as description of the message
:param data : dictionary representing extra data
"""
if data:
data = stringify_objectid(data)
res = {'status': status, 'message': message, 'data': data, 'timestamp': timestamp()}
return jsonify(res), status_code
def error_response(message, status='error', code='R0', status_code=400):
res = {'message': message, 'status': status, 'code': code}
return jsonify(res), status_code
def timestamp():
"""
Helper Function to generate the current time
"""
return time.time()
In the above code, we have helper functions to return api responses and error messages. The stringify_objectid
function is used to convert the mongodb ObjectId object into a string.
Next, we are going to write our controller functions. Create a file called appointments.py
in the controllers/
folder and copy the following inside:
from flask import Blueprint, request
from services import appointments as apt_service
from utils.request import validate_body, parse_appointment
from utils.response import response, error_response
from utils.whatsapp import send_sms
import jobs
appointments = Blueprint('appointments', __name__)
@appointments.route('/', methods=['POST'])
def create():
body = request.get_json()
status, missing_field = validate_body(body, ['title', 'phone', 'description', 'time'])
if not status:
return error_response(f'{missing_field} is required')
status, error = parse_appointment(body)
if not status:
return error_response(error)
try:
apt_service.create_appointment(body)
jobs.schedule_appointment(body)
return response(True, 'Appointment created successfully', body)
except Exception as err:
print('=====> Error', err)
return error_response(str(err))
@appointments.route('/')
def view():
conditions = dict(request.args)
try:
data = apt_service.get_appointments(conditions)
return response(True, 'Appointments', data)
except Exception as err:
print('=====> Error', err)
return error_response(str(err))
@appointments.route('/<appointment_id>')
def view_one(appointment_id):
try:
data = apt_service.get_appointment(appointment_id)
return response(True, 'Appointment', data)
except Exception as err:
print('=====> Error', err)
return error_response(str(err))
@appointments.route('/<appointment_id>', methods=['PUT'])
def update(appointment_id):
body = request.get_json()
try:
parse_appointment(body)
appointment = apt_service.update_appointment(appointment_id, body)
jobs.update_scheduled_appointment(appointment_id, appointment)
return response(True, 'Updated Appointment', appointment)
except Exception as err:
print('=====> Error', err)
return error_response(str(err))
@appointments.route('/<appointment_id>', methods=['DELETE'])
def delete(appointment_id):
try:
apt_service.delete_appointment(appointment_id)
jobs.delete_scheduled_appointment(appointment_id)
return response(True, 'Appointment deleted successfully', None)
except Exception as err:
print('====> Error', err)
return error_response(str(err))
In the above code, we created a new flask blueprint and functions to create appointments, get appointments, update appointments and delete appointments. As you can see from the above code, we call the schedule appointment function to start the scheduling process when an appointment is created. The update function calls the update_scheduled_appointment
function to reshedule the appointment and the delete function calls the delete_scheduled_appointment
function to delete the scheduling process.
Finally, open the app.py
file and copy the following inside:
import atexit
from flask import Flask
from controllers.appointments import appointments
from jobs import close_workers, init_workers
def create_app():
init_workers()
atexit.register(close_workers)
return Flask(__name__)
app = create_app()
app.register_blueprint(appointments, url_prefix='/api/appointments')
if __name__ == '__main__':
app.run()
In the above code, the use the register_blueprint
function to make sure that urls starting with /api/appointments
are routed through the appointments blueprint and our controller functions are executed for matching urls. We use the atexit
module to perform a task when the application is closing. In the create_app
function, we are telling the system to first schedule pending appointments and then close all running processes when the application is closing.
Testing the Application
To run the application, open your terminal and execute python app.py
in your application’s directory. You should see something like the image below:
Now, you can send a POST request to http://127.0.0.1:5000/api/appointments
to create an appointment and you will get a whatsapp message when the appointment time has been reached. Below is a sample request body:
{
"title": "Data Structures & Algorithms",
"phone": "+2349094739283",
"description": "Watch Youtube videos on Data Structures and Algorithms",
"time":"2020-02-23 18:10"
}
You should receive a whatsapp message when the appointment time is reached as shown in the image below
Conclusion
We have now come to the end of the tutorial. We have successfully built an application that sends appointment notification over Whatsapp using Twilio, Python and Flask.
You will find the source code for this tutorial here.
Top comments (0)