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:
- A free Twilio account. If you use this link to register, you will receive $10 credit when you upgrade to a paid account.
- Python 3.6 or newer.
- A free PythonAnywhere account.
- A phone line that can receive SMS messages.
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 Pythonos
module to check whether thereminder.json
file exists. Theos.path.isfile()
method is used to check whether a certain file exists. This method returnsTrue
orFalse
accordingly. - The
read_reminder_json()
function is used to fetch reminders from thereminder.json
file. Using thereminder_json_exists()
function, we check to see if thereminder.json
file has already been created. If the file has already been created, theopen()
function is used to read the file. The file is then parsed using thejson.load()
method which gives us a dictionary nameddata
. Thereminders
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, thereminder_json_exists()
is called once again. The function accepts a reminderdictionary
as an argument. If thereminder.json
file does not exist, an emptydictionary
is created, a reminders key with an emptylist
is then assigned within the dictionary. The reminder that was passed in as an argument is then appended to the list and thewrite_reminder_json()
function is called to write the file to disk. If the file already exists, theupdate_reminder_json()
function is called instead. - The
update_reminder_json()
accepts a reminder and appends it to the existing reminderlist
within the json file. - The
write_reminder_json()
is used to write the JSON representation of the reminders to thereminder.json
file. The function opens thereminder.json
file in writing mode using ‘w’. If the file doesn’t already exist, it’ll be created. Thenjson.dumps()
transforms thedata
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.
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.
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.
To Get all reminders, send a HTTP GET request to the http://localhost:5000/api/reminders
endpoint.
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.
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
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.
Finally, it’s time to edit the WSGI configuration file. Under the “Code” section, select the “WSGI configuration file”
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.
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_