Build an SMS Reminder Service using Python and Twilio

August 17, 2020
Written by
Dotun Jolaoso
Contributor
Opinions expressed by Twilio contributors are their own

Build an SMS Reminder Service Using Python and Twilio

There are different scenarios within your applications where you would like to send reminders. For example, maybe you would like to send appointment reminders to your users or happy birthday wishes, this tutorial can serve as a great starting point for that. In this tutorial, we’ll be looking at how we can build an SMS Reminder Service using Python and Twilio.

Technical Requirements

To follow along, you’ll need the following:

Creating a Python environment

Let’s create a directory where our project will reside. From the terminal, run the following command:

$ mkdir twilio_sms_reminders

Next, cd into the project directory and run the following command to create a virtual environment.

$ python -m venv venv

To activate the virtual environment on a Linux or MacOS computer, run the following command:

$ source venv/bin/activate

If you are using a Windows computer, then the activation command is different:

$ venv\Scripts\activate

Next, we’ll need to install all the dependencies our project will need:

  • Flask: a Python web framework.
  • Twilio: A Python helper library that makes it easy to interact with the Twilio API.
  • python-dateutil: This library provides powerful extensions to the standard datetime module already provided by Python.
  • python-dotenv: A library for importing environment variables from a .env file.

To install all the dependencies at once, run the following command:

$ pip install flask twilio python-dateutil python-dotenv

Next run the following command:

$ pip freeze > requirements.txt

This will generate a requirements.txt file for us which contains all our project’s dependencies along with their versions.

Creating A JSON Helper File

We won’t be making use of an actual database to store the reminders, instead we'll be making use of a JSON file. All reminders will be stored within a reminder.json file.

To get started, we’ll create a simple JSON helper module for handling reading and writing reminders to the reminder.json file. From the root of your project, create a reminder_json_helper.py file and add the following code to the file:

import os
import json


def reminder_json_exists():
    return os.path.isfile('reminder.json')


def read_reminder_json():
    if reminder_json_exists():
        with open('reminder.json') as reminder_json:
            data = json.load(reminder_json)
            return data['reminders']
    else:
        return {}


def create_reminder_json(reminder):
    if not reminder_json_exists():
        data = {}
        data['reminders'] = []
        data['reminders'].append(reminder)
        write_reminder_json(data)
    else:
        update_reminder_json(reminder)


def update_reminder_json(reminder):
    with open('reminder.json') as reminder_json:
        data = json.load(reminder_json)
        reminders = data['reminders']
        reminders.append(reminder)
        write_reminder_json(data)


def write_reminder_json(data, filename='reminder.json'):
    with open(filename, 'w') as outfile:
        json.dump(data, outfile, indent=4)

Here are some notes about this module:

  • In the reminder_json_exists() function, we use the Python os module to check whether the reminder.json file exists. The os.path.isfile() method is used to check whether a certain file exists. This method returns True or False accordingly.
  •  The read_reminder_json() function is used to fetch reminders from the reminder.json file. Using the reminder_json_exists() function, we check to see if the reminder.json file has already been created. If the file has already been created, the open() function is used to read the file. The file is then parsed using the json.load() method which gives us a dictionary named data. The reminders key is then returned from the dictionary. If the file does not exist, we return an empty dictionary.
  • Similarly, in the create_reminder_json() function, the reminder_json_exists() is called once again. The function accepts a reminder dictionary as an argument. If the reminder.json file does not exist, an empty dictionary is created, a reminders key with an empty list is then assigned within the dictionary. The reminder that was passed in as an argument is then appended to the list and the write_reminder_json() function is called to write the file to disk. If the file already exists, the update_reminder_json() function is called instead.
  • The update_reminder_json() accepts a reminder and appends it to the existing reminder list within the json file.
  • The write_reminder_json() is used to write the JSON representation of the reminders to the reminder.json file. The function opens the reminder.json file in writing mode using ‘w’. If the file doesn’t already exist, it’ll be created. Then json.dumps() transforms the data dictionary into a JSON string which will be saved to the file.

Getting All Reminders

Now that we’re done with the helper file for reading and writing reminders to the JSON file, we can start implementing the API endpoints. Create a main.py file at the root of your project and add the following code to the top of the file:

import os
from flask import Flask, request, jsonify, abort
from reminder_json_helper import read_reminder_json, create_reminder_json, write_reminder_json
import uuid

app = Flask(__name__)

Here we’ve imported the helper functions we defined earlier in the reminder_json_helper file along with Python’s uuid module.

Next, add the following function:

@app.route('/api/reminders', methods=['GET'])
def get_reminders():
    reminders = read_reminder_json()
    return jsonify({'reminders': reminders})

Here, we’ve defined a get_reminders() function which is associated with the /api/reminders endpoint and only supports the HTTP GET method. The function obtains all the available reminders using the read_reminder_json() helper function and then returns a JSON response using Flask’s jsonify function.

Creating A Reminder

Each reminder will consist of a message, phone number, due date and an interval. It’s important to note that the phone number must use the canonical E.164 format. The due date will be the date at which the reminders should be sent out. The interval is the frequency at which the reminder should be sent out and will default to monthly for this tutorial. Add the following functions to the main.py file:

@app.route('/api/reminders', methods=['POST'])
def create_reminder():
    req_data = request.get_json()

    if not all(item in req_data for item in ("phone_number", "message", "due_date")):
        abort(400)

    reminder = {
        'id': uuid.uuid4().hex,
        'phone_number': req_data['phone_number'],
        'message': req_data['message'],
        'interval': 'monthly',
        'due_date': req_data['due_date']
    }

    create_reminder_json(reminder)
    return jsonify({'reminder': reminder}), 201


@app.errorhandler(400)
def bad_request(error):
    return jsonify({'error': 'Bad Request'}), 400

The create_reminder() function is responsible for creating a new reminder and storing it. Within the function, a simple check is carried out to ensure the required fields are present in the request payload. We then create a new reminder dictionary, setting the interval field to monthly. The reminder is then passed as an argument to the create_reminder_json() helper function which is responsible for storing the reminder in the reminder.json file.

The bad_request() function is an error handler that will return a JSON response whenever our application is trying to handle a request with a HTTP status code of 400.

Deleting A Reminder

To delete a reminder, add the following functions to the main.py file:

@app.route('/api/reminders/<reminder_id>', methods=['DELETE'])
def delete_reminder(reminder_id):
    reminders = read_reminder_json()
    reminder = [reminder for reminder in reminders if reminder['id'] == reminder_id]
    if len(reminder) == 0:
        abort(404)
    reminders.remove(reminder[0])
    data = {}
    data['reminders'] = reminders
    write_reminder_json(data)
    return jsonify({'message': 'Reminder has been removed successfully'})


@app.errorhandler(404)
def not_found(error):
    return jsonify({'error': 'Not Found'}), 404

In the delete_reminder() function, the id of the reminder is obtained from the URI, which is in turn translated by Flask into the reminder_id argument we receive in the function. Next, all available reminders are obtained using read_reminder_json(). A search is then carried out within the reminders array to find the reminder_id argument that was passed to the function. If the id cannot be found, a 404 response is returned. If the reminder is found, it is removed from the existing reminders array and the updated reminders array is written back to the JSON file using write_reminder_json()

Similar to the bad_request() error handler we defined earlier, not_found() is an error handler for returning a structured JSON response whenever our application is trying to handle a request with a HTTP status code of 404.

Starting the Web Server

To complete main.py we have to add the code that starts the Flask web server at the bottom:

if __name__ == '__main__':
    app.run()

Bringing it all together, the final main.py file looks like this:

import os
from flask import Flask, request, jsonify, abort
from reminder_json_helper import read_reminder_json, create_reminder_json, write_reminder_json
import uuid

app = Flask(__name__)


@app.route('/api/reminders', methods=['GET'])
def get_reminders():
    reminders = read_reminder_json()
    return jsonify({'reminders': reminders})


@app.route('/api/reminders', methods=['POST'])
def create_reminder():
    req_data = request.get_json()

    if not all(item in req_data
               for item in ("phone_number", "message", "due_date")):
        abort(400)

    reminder = {
        'id': uuid.uuid4().hex,
        'phone_number': req_data['phone_number'],
        'message': req_data['message'],
        'interval': 'monthly',
        'due_date': req_data['due_date']
    }

    create_reminder_json(reminder)
    return jsonify({'reminder': reminder}), 201


@app.errorhandler(400)
def bad_request(error):
    return jsonify({'error': 'Bad Request'}), 400


@app.route('/api/reminders/<reminder_id>', methods=['DELETE'])
def delete_reminder(reminder_id):
    reminders = read_reminder_json()
    reminder = [
        reminder for reminder in reminders if reminder['id'] == reminder_id
    ]
    if len(reminder) == 0:
        abort(404)
    reminders.remove(reminder[0])
    data = {}
    data['reminders'] = reminders
    write_reminder_json(data)
    return jsonify({'message': 'Reminder has been removed successfully'})


@app.errorhandler(404)
def not_found(error):
    return jsonify({'error': 'Not Found'}), 404


if __name__ == '__main__':
    app.run()

Setting up Twilio

On your Twilio Console, copy your Account SID and Auth Token. We are going to need these values to authenticate with the Twilio service. You will also need to set up a Twilio phone number that can send SMS messages. You can add a phone number to your account in the Buy a Number page if you don’t already have one.

twilio credentials

At the root of the project’s directory, create a .env file and add your Twilio credentials, along with your Twilio phone number associated with your account:

TWILIO_ACCOUNT_SID=xxxx
TWILIO_AUTH_TOKEN=xxxx
TWILIO_SMS_FROM=xxxx

For the Twilio phone number  use the canonical E.164 format.

Sending SMS Reminders

To get started with sending SMS Reminders, we first need to handle fetching all reminders that are due.  Create a new send_reminders.py file and add the following imports at the top of the file:

import os
from reminder_json_helper import read_reminder_json, write_reminder_json
from dateutil.relativedelta import relativedelta
from datetime import datetime, date
from twilio.rest import Client
from twilio.http.http_client import TwilioHttpClient
from dotenv import load_dotenv

Next, just below the imports, add the following code:

load_dotenv()

proxy_client = TwilioHttpClient(proxy={'http': os.getenv("http_proxy"), 'https': os.getenv("https_proxy")})
twilio_client = Client(http_client=proxy_client)

The load_dotenv() function loads our environment variables from the .env file.

Later we’ll be deploying our application to PythonAnywhere, and free accounts on this service use a proxy server to be able to access the Internet. This will in turn further affect how the Twilio Helper Library invokes the Twilio REST API. In order to avoid this pitfall after deploying, we can modify the default HttpClient that comes bundled with the Twilio Helper Library to make use of our Proxy server. The proxy_client object is simply a TwilioHttpClient class that accepts the credentials of our Proxy server.

Note that we haven’t defined values for the  http_proxy and https_proxy attributes in our .env file. Those values aren’t needed at this stage and everything should still work smoothly.

The twilio_client object will be used for interacting with the Twilio API, while specifying the http_client to be the proxy_client object that was defined earlier. The TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN environment variables loaded by the load_dotenv() function will be automatically used to authenticate against the Twilio service.

Next, add the following functions to the send_reminders.py file:

def find_reminders_due():
    reminders = read_reminder_json()
    reminders_due = [
        reminder for reminder in reminders
        if reminder['due_date'] == str(date.today())
    ]
    if len(reminders_due) > 0:
        send_sms_reminder(reminders_due)


def send_sms_reminder(reminders):
    for reminder in reminders:
        twilio_from = os.getenv("TWILIO_SMS_FROM")
        to_phone_number = reminder['phone_number']
        twilio_client.messages.create(
            body=reminder['message'],
            from_=f"{twilio_from}",
            to=f"{to_phone_number}")
        update_due_date(reminder)


def update_due_date(reminder):
    reminders = read_reminder_json()
    data = {}
    reminders.remove(reminder)
    new_due_date = datetime.strptime(
        reminder['due_date'], '%Y-%m-%d').date() + relativedelta(months=1)
    reminder['due_date'] = str(new_due_date)
    reminders.append(reminder)
    data['reminders'] = reminders
    write_reminder_json(data)


if __name__ == '__main__':
    find_reminders_due()

The find_reminders_due() function returns all the reminders that have a due date equal to today’s date. If there are reminders due, the send_sms_reminder() function is subsequently called, passing in all the due reminders as an argument.

The send_sms_reminder() function loops through each of the available reminders and for each of them sends an SMS notification to the phone number tied to the reminder. The twilio_client.messages.create() invokes the Twilio API to send a notification. The body argument is the message attribute of the reminder. The from_ argument indicates the number of the sender, which is your Twilio phone number. The to argument is the phone number attached to the reminder. After the message has been sent, the update_due_date() function is called.

The update_due_date() function accepts a single reminder as an argument and updates the reminder’s due date by simply adding an extra month to it.

Here’s the final outlook for the send_reminders.py file:

import os
from reminder_json_helper import read_reminder_json, write_reminder_json
from dateutil.relativedelta import relativedelta
from datetime import datetime, date
from twilio.rest import Client
from twilio.http.http_client import TwilioHttpClient
from dotenv import load_dotenv

load_dotenv()

proxy_client = TwilioHttpClient(proxy={'http': os.getenv("http_proxy"), 'https': os.getenv("https_proxy")})
twilio_client = Client(http_client=proxy_client)


def find_reminders_due():
    reminders = read_reminder_json()
    reminders_due = [
        reminder for reminder in reminders
        if reminder['due_date'] == str(date.today())
    ]
    if len(reminders_due) > 0:
        send_sms_reminder(reminders_due)


def send_sms_reminder(reminders):
    for reminder in reminders:
        twilio_from = os.getenv("TWILIO_SMS_FROM")
        to_phone_number = reminder['phone_number']
        twilio_client.messages.create(
            body=reminder['message'],
            from_=f"{twilio_from}",
            to=f"{to_phone_number}")
        update_due_date(reminder)


def update_due_date(reminder):
    reminders = read_reminder_json()
    data = {}
    reminders.remove(reminder)
    new_due_date = datetime.strptime(
        reminder['due_date'], '%Y-%m-%d').date() + relativedelta(months=1)
    reminder['due_date'] = str(new_due_date)
    reminders.append(reminder)
    data['reminders'] = reminders
    write_reminder_json(data)


if __name__ == '__main__':
    find_reminders_due()

Every time the send_reminders.py file is executed, the find_reminders_due() function will be called.

Testing

To get started, run the following command:

$ python main.py

Next, I’ll be making use of Postman to test the API endpoints we’ve created. You can download it here if you don’t already have it installed. You’re also free to make use of any API client of your choice.

To create a reminder, head over to Postman. Make Sure Build is selected at the bottom right. Click the “+” button to open a new request tab and then paste in the URL http://localhost:5000/api/reminders in the URL field. Next, select the “Body” tab, which allows us to specify the data we need to send along with the request. Within the “Body” tab, select “raw” and then select “JSON” within the type drop-down list to indicate the format of our data.

postman screenshot

Now you can send a HTTP POST request to the http://localhost:5000/api/reminders endpoint passing in the message, phone number and due date as the JSON payload.

create a reminder from postman

To Get all reminders, send a HTTP GET request to the http://localhost:5000/api/reminders  endpoint.

get all reminders from postman

To delete a reminder, make a HTTP DELETE request to http://localhost:5000/api/reminders/reminder_id where reminder_id is the id of the reminder.

delete reminder from postman

To send reminders that are due, the send_reminders.py file needs to be executed regularly. Open a second terminal window, activate the virtual environment and then from the root of your project’s directory, run the following command:

$ python send_reminders.py

Here’s an example of the reminder received via SMS

sms reminder demo

Deploying to PythonAnywhere

In this section, we’ll be covering how we can deploy the service to PythonAnywhere. If you don’t have an account on PythonAnywhere, you can create one here.

To simplify the deployment to PythonAnywhere we are going to commit our project to a GitHub repository. Head over to Github and create a new repository called twilio-sms-reminders. Once you’re done with that, head back to the root of your project’s directory and create a .gitignore file. This will ensure that certain files we made use of during development won’t be committed to version control. Add the following files and folder to the .gitignore file:

.env
venv
reminder.json
__pycache__

Next, run the following commands to deploy the project to Github

$ git init
$ git add .
$ git commit -m "Initial commit"
$ git remote add origin <my-github-repo-url>
$ git push -u origin master

Don’t forget to replace <my-github-repo-url> with the actual URL to your new GitHub repository, which should be https://github.com/github-username/twilio-sms-reminders.git where github-username is your GitHub username.

Head over to your PythonAnywhere dashboard, open up a Bash console and run the following commands:

$ git clone <my-github-repo-url>
$ cd twilio-sms-reminders
$ mkvirtualenv --python=/usr/bin/python3.7 venv
$ pip install -r requirements.txt

Here, we’ve cloned our project, created a virtualenv and installed all the dependencies our project will be needing. Next, we’ll need to create a .env file and define all our environment variables. Pythonanywhere already provides the HTTP proxy address in the environment, so there’s no need for us to define it in the .env file.

Run the following commands to set your environment variables:

$ echo "export TWILIO_ACCOUNT_SID=xxxx" >> .env
$ echo "export TWILIO_AUTH_TOKEN=xxxx" >> .env
$ echo "export TWILIO_SMS_FROM=xxxx" >> .env

Don’t forget to replace xxxx with their actual values.

Now that we’re done doing all the initial setups, we can get started with creating a web app. Go to the Web Tab section and select “Add a new web app”. Choose “Manual Configuration”, and then select the Python version to be 3.7. This needs to be consistent with the same version as we used while setting up the virtualenv. Once that is done, head over to the “Virtualenv” section and enter the name to our virtualenv, which in this case is venv. After you hit enter, it’ll update to the full path to the virtualenv.

virtualenv configuration

Finally, it’s time to edit the WSGI configuration file. Under the “Code” section, select the “WSGI configuration file”

wsgi configuration

This will open up the WSGI file in a web editor. Replace the content of the file with the following code:

import os
import sys
from dotenv import load_dotenv

path = '/home/yourusername/twilio-sms-reminders'
if path not in sys.path:
    sys.path.append(path)

project_folder = os.path.expanduser(path)
load_dotenv(os.path.join(project_folder, '.env'))
from main import app as application

Here we've loaded the .env file we created earlier using load_dotenv. In the path variable, don’t forget to replace yourusername with your actual PythonAnywhere username. Once you’re done, click “Save” at the top of the editor.

Before we visit our web app, let’s reload it, so that all the configuration we’ve made so far can take effect. Under the “Web” section, select the web app you created, just under the “Reload” section, select “Reload yourusername.pythonanywhere.com”. You can now go ahead to test the API endpoints via Postman at yourusername.pythonanywhere.com.

We have just one more step left to complete this deployment. We need a way to schedule the send_reminders.py` file so that it can be executed on a daily basis. Luckily, PythonAnywhere makes that simple. Head over to the “Tasks” Tab on your Dashboard, and create a new scheduled task that runs every day at 12 midnight UTC time (or your preferred time). Add the following command to the “run” field:

/home/yourusername/.virtualenvs/venv/bin/python
/home/yourusername/twilio-sms-reminders/send_reminders.py

Here we’ve specified the full path to the virtualenv python along with the full path to the send_reminders.py file. This scheduled task will be run within our virtualenv.

scheduled tasks

Select “Create”. Now, send_reminders.py script will be executed every day at the indicated time.

Conclusion

In this tutorial, we’ve seen how we can build a Simple SMS Reminder Service using Twilio and Flask. The source code for this tutorial can be found on Github.

Dotun Jolaoso

Website: https://dotunj.dev/
Github: https://github.com/Dotunj
Twitter: https://twitter.com/Dotunj_