Appointment Reminders with Python and Flask

This web application shows how you can use Twilio to send your customers a text message reminding them of upcoming appointments.

We use Flask to build out the web application that supports our user interface, and Celery to send the reminder text messages to our customers at the right time.

In this tutorial, we'll point out the key bits of code that make this application work. Check out the project README on GitHub to see how to run the code yourself.

Check out how Yelp uses SMS to confirm restaurant reservations for diners.

Let's get started! Click the button below to get started.

Configure the application to use Twilio

Before we can use the Twilio API to send reminder text messages, we need to configure our account credentials. These can be found on your Twilio Console. You'll also need an SMS-enabled phone number - you can find or purchase a new one to use here.

We put these environment variables in a .env file and use autoenv to apply them every time we work on the project. More information on how to configure this application can be found in the project README.

Loading Code Samples...
Language
# Environment variables for appointment-reminders-flask

# App settings
export DATABASE_URL=postgres://someuser:withsomepassword@localhost:5432/appointments
export SECRET_KEY=asupersecr3tkeyshouldgo
export CELERY_URL=redis://localhost:6379

# Twilio settings
export TWILIO_ACCOUNT_SID=ACXXXXXXXXXXXXXXXXX
export TWILIO_AUTH_TOKEN=YYYYYYYYYYYYYYYYYY
export TWILIO_NUMBER=+###########
.env.example
Configure the environment variables

.env.example

Now that the configuration is taken care of. We'll move on to the application structure.

The application structure

The Application object is the heart of any Flask app. Our's initializes the app, sets the URLs, and pulls in all our environment variables.

The celery method is boilerplate to configure Celery using settings and context from our Flask application. Our app uses Redis as a broker for Celery. But you can also use any of the other available Celery brokers.

To get Celery to run locally on your machine, follow the instructions in the README.

Loading Code Samples...
Language
import flask
import flask.ext.sqlalchemy
from celery import Celery
from views.appointment import AppointmentResourceDelete, AppointmentFormResource
from views.appointment import AppointmentResourceCreate, AppointmentResourceIndex


class Route(object):

    def __init__(self, url, route_name, resource):
        self.url = url
        self.route_name = route_name
        self.resource = resource

handlers = [
    Route('/', 'appointment.index', AppointmentResourceIndex),
    Route('/appointment', 'appointment.create', AppointmentResourceCreate),
    Route('/appointment/<int:id>/delete',
          'appointment.delete', AppointmentResourceDelete),
    Route('/appointment/new', 'appointment.new', AppointmentFormResource),
]


class Application(object):

    def __init__(self, routes, config, debug=True):
        self.flask_app = flask.Flask(__name__)
        self.routes = routes
        self.debug = debug
        self._configure_app(config)
        self._set_routes()

    def celery(self):
        app = self.flask_app
        celery = Celery(app.import_name, broker=app.config[
                        'CELERY_BROKER_URL'])
        celery.conf.update(app.config)

        TaskBase = celery.Task

        class ContextTask(TaskBase):
            abstract = True

            def __call__(self, *args, **kwargs):
                with app.app_context():
                    return TaskBase.__call__(self, *args, **kwargs)
        celery.Task = ContextTask

        return celery

    def _set_routes(self):
        for route in self.routes:
            app_view = route.resource.as_view(route.route_name)
            self.flask_app.add_url_rule(route.url, view_func=app_view)

    def _configure_app(self, env):
        celery_url = env.get('CELERY_URL')

        self.flask_app.config[
            'SQLALCHEMY_DATABASE_URI'] = env.get('DATABASE_URL')

        self.flask_app.config['CELERY_BROKER_URL'] = env.get(
            'REDIS_URL', celery_url)
        self.flask_app.config['CELERY_RESULT_BACKEND'] = env.get(
            'REDIS_URL', celery_url)

        self.flask_app.config['TWILIO_ACCOUNT_SID'] = env.get(
            'TWILIO_ACCOUNT_SID')
        self.flask_app.config['TWILIO_AUTH_TOKEN'] = env.get(
            'TWILIO_AUTH_TOKEN')
        self.flask_app.config['TWILIO_NUMBER'] = env.get('TWILIO_NUMBER')

        self.flask_app.secret_key = env.get('SECRET_KEY')

        self.db = flask.ext.sqlalchemy.SQLAlchemy(self.flask_app)

    def start_app(self):
        self.flask_app.run(debug=self.debug)
application.py
Our core application code

application.py

With our Application ready, let's create an Appointment model.

The Appointment model

Our Appointment model is pretty simple. The name and phone_number fields tell us who to send the reminder to. The time, timezone, and delta fields tell us when to send the reminder.

We use SQLAlchemy to power our model and give us a nice ORM interface to use it with.

We added an extra method, get_notification_time, to help us determine the right time to send our reminders. The handy arrow library makes this kind of time arithmatic easy.

Loading Code Samples...
Language
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, DateTime, String
import arrow

Base = declarative_base()


class Appointment(Base):
    __tablename__ = 'appointments'

    id = Column(Integer, primary_key=True)
    name = Column(String(50), nullable=False)
    phone_number = Column(String(50), nullable=False)
    delta = Column(Integer, nullable=False)
    time = Column(DateTime, nullable=False)
    timezone = Column(String(50), nullable=False)

    def __init__(self, name, phone_number, delta, time, timezone):
        self.name = name
        self.phone_number = phone_number
        self.delta = delta
        self.time = time
        self.timezone = timezone

    def __repr__(self):
        return '<Appointment %r>' % self.name

    def get_notification_time(self):
        appointment_time = arrow.get(self.time)
        reminder_time = appointment_time.replace(minutes=-self.delta)
        return reminder_time
models/appointment.py
The Appointment model

models/appointment.py

Next we will use this model to create a new Appointment and schedule a reminder.

Scheduling new reminders

This view handles creating new appointments and scheduling new reminders. It accepts POST data sent to the /appointment URL.

We use WTForms to validate the form data using a class called NewAppointmentForm that we defined in forms/new_appointment.py.

After that we use arrow to convert the time zone of the appointment's time to UTC time.

We then save our new Appointment object and schedule the reminder using a Celery task we defined called send_sms_reminder.

Loading Code Samples...
Language
from flask.views import MethodView
from flask import render_template
from models.appointment import Appointment
from forms.new_appointment import NewAppointmentForm
from flask import request, redirect, url_for
import reminders
import arrow


class AppointmentResourceDelete(MethodView):

    def post(self, id):
        appt = reminders.db.session.query(Appointment).filter_by(id=id).one()
        reminders.db.session.delete(appt)
        reminders.db.session.commit()

        return redirect(url_for('appointment.index'), code=303)


class AppointmentResourceCreate(MethodView):

    def post(self):
        form = NewAppointmentForm(request.form)

        if form.validate():
            from tasks import send_sms_reminder

            appt = Appointment(**form.data)
            appt.time = arrow.get(appt.time, appt.timezone).to('utc').naive

            reminders.db.session.add(appt)
            reminders.db.session.commit()
            send_sms_reminder.apply_async(
                args=[appt.id], eta=appt.get_notification_time())

            return redirect(url_for('appointment.index'), code=303)
        else:
            return render_template('appointments/new.html', form=form), 400


class AppointmentResourceIndex(MethodView):

    def get(self):
        all_appointments = reminders.db.session.query(Appointment).all()
        return render_template('appointments/index.html',
                               appointments=all_appointments)


class AppointmentFormResource(MethodView):

    def get(self):
        form = NewAppointmentForm()
        return render_template('appointments/new.html', form=form)
views/appointment.py
Scheduling new reminders

views/appointment.py

We'll look at that task next.

Set up a Twilio API client

Our tasks.py module contains the definition for our send_sms_reminder task. At the top of this module we use the twilio-python library to create a new instance of Client.

We'll use this client object to send a text message using the Twilio API in our send_sms_reminder function.

Loading Code Samples...
Language
from reminders import celery, db, app
from models.appointment import Appointment
from sqlalchemy.orm.exc import NoResultFound
from twilio.rest import Client
import arrow

twilio_account_sid = app.flask_app.config['TWILIO_ACCOUNT_SID']
twilio_auth_token = app.flask_app.config['TWILIO_AUTH_TOKEN']
twilio_number = app.flask_app.config['TWILIO_NUMBER']

client = Client(twilio_account_sid, twilio_auth_token)


@celery.task()
def send_sms_reminder(appointment_id):
    try:
        appointment = db.session.query(
            Appointment).filter_by(id=appointment_id).one()
    except NoResultFound:
        return

    time = arrow.get(appointment.time).to(appointment.timezone)
    body = "Hello {0}. You have an appointment at {1}!".format(
        appointment.name,
        time.format('h:mm a')
    )

    to = appointment.phone_number,
    client.messages.create(
        to,
        from_=twilio_number,
        body=body)
tasks.py
Set up a Twilio API client

tasks.py

Let's look at send_sms_reminder now.

Sending a reminder

This is the send_sms_reminder function we called in our appointment.create view. Our function starts with an appointment_id parameter, which we use to retrieve an Appointment object from the database - a Celery best practice.

To compose the body of our text message, we use arrow again to convert the UTC time stored in our appointment to the local time zone of our customer.

After that, sending the message itself is a simple call to client.messages.create(). We use our customer's phone number as the to argument and our Twilio number as the from_ argument.

Loading Code Samples...
Language
from reminders import celery, db, app
from models.appointment import Appointment
from sqlalchemy.orm.exc import NoResultFound
from twilio.rest import Client
import arrow

twilio_account_sid = app.flask_app.config['TWILIO_ACCOUNT_SID']
twilio_auth_token = app.flask_app.config['TWILIO_AUTH_TOKEN']
twilio_number = app.flask_app.config['TWILIO_NUMBER']

client = Client(twilio_account_sid, twilio_auth_token)


@celery.task()
def send_sms_reminder(appointment_id):
    try:
        appointment = db.session.query(
            Appointment).filter_by(id=appointment_id).one()
    except NoResultFound:
        return

    time = arrow.get(appointment.time).to(appointment.timezone)
    body = "Hello {0}. You have an appointment at {1}!".format(
        appointment.name,
        time.format('h:mm a')
    )

    to = appointment.phone_number,
    client.messages.create(
        to,
        from_=twilio_number,
        body=body)
tasks.py
Perform the actual task of sending a SMS

tasks.py

That's it! Our Flask application is all set to send out reminders for upcoming appointments.

Where to next?

We hope you found this sample application useful.

If you're a Python developer working with Twilio and Flask, you might enjoy these other tutorials:

Click to Call

Put a button on your web page that connects visitors to live support or sales people via telephone.

Two-Factor Authentication

Improve the security of your Flask app's login functionality by adding two-factor authentication via text message.

Did this help?

Thanks for checking out this tutorial! If you have any feedback to share with us, please reach out on Twitter... we'd love to hear your thoughts, and know what you're building!

Andrew Baker
David Prothero
Agustin Camino

Need some help?

We all do sometimes; code is hard. Get help now from our support team, or lean on the wisdom of the crowd browsing the Twilio tag on Stack Overflow.

1 / 1
Loading Code Samples...
# Environment variables for appointment-reminders-flask

# App settings
export DATABASE_URL=postgres://someuser:withsomepassword@localhost:5432/appointments
export SECRET_KEY=asupersecr3tkeyshouldgo
export CELERY_URL=redis://localhost:6379

# Twilio settings
export TWILIO_ACCOUNT_SID=ACXXXXXXXXXXXXXXXXX
export TWILIO_AUTH_TOKEN=YYYYYYYYYYYYYYYYYY
export TWILIO_NUMBER=+###########
import flask
import flask.ext.sqlalchemy
from celery import Celery
from views.appointment import AppointmentResourceDelete, AppointmentFormResource
from views.appointment import AppointmentResourceCreate, AppointmentResourceIndex


class Route(object):

    def __init__(self, url, route_name, resource):
        self.url = url
        self.route_name = route_name
        self.resource = resource

handlers = [
    Route('/', 'appointment.index', AppointmentResourceIndex),
    Route('/appointment', 'appointment.create', AppointmentResourceCreate),
    Route('/appointment/<int:id>/delete',
          'appointment.delete', AppointmentResourceDelete),
    Route('/appointment/new', 'appointment.new', AppointmentFormResource),
]


class Application(object):

    def __init__(self, routes, config, debug=True):
        self.flask_app = flask.Flask(__name__)
        self.routes = routes
        self.debug = debug
        self._configure_app(config)
        self._set_routes()

    def celery(self):
        app = self.flask_app
        celery = Celery(app.import_name, broker=app.config[
                        'CELERY_BROKER_URL'])
        celery.conf.update(app.config)

        TaskBase = celery.Task

        class ContextTask(TaskBase):
            abstract = True

            def __call__(self, *args, **kwargs):
                with app.app_context():
                    return TaskBase.__call__(self, *args, **kwargs)
        celery.Task = ContextTask

        return celery

    def _set_routes(self):
        for route in self.routes:
            app_view = route.resource.as_view(route.route_name)
            self.flask_app.add_url_rule(route.url, view_func=app_view)

    def _configure_app(self, env):
        celery_url = env.get('CELERY_URL')

        self.flask_app.config[
            'SQLALCHEMY_DATABASE_URI'] = env.get('DATABASE_URL')

        self.flask_app.config['CELERY_BROKER_URL'] = env.get(
            'REDIS_URL', celery_url)
        self.flask_app.config['CELERY_RESULT_BACKEND'] = env.get(
            'REDIS_URL', celery_url)

        self.flask_app.config['TWILIO_ACCOUNT_SID'] = env.get(
            'TWILIO_ACCOUNT_SID')
        self.flask_app.config['TWILIO_AUTH_TOKEN'] = env.get(
            'TWILIO_AUTH_TOKEN')
        self.flask_app.config['TWILIO_NUMBER'] = env.get('TWILIO_NUMBER')

        self.flask_app.secret_key = env.get('SECRET_KEY')

        self.db = flask.ext.sqlalchemy.SQLAlchemy(self.flask_app)

    def start_app(self):
        self.flask_app.run(debug=self.debug)
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, DateTime, String
import arrow

Base = declarative_base()


class Appointment(Base):
    __tablename__ = 'appointments'

    id = Column(Integer, primary_key=True)
    name = Column(String(50), nullable=False)
    phone_number = Column(String(50), nullable=False)
    delta = Column(Integer, nullable=False)
    time = Column(DateTime, nullable=False)
    timezone = Column(String(50), nullable=False)

    def __init__(self, name, phone_number, delta, time, timezone):
        self.name = name
        self.phone_number = phone_number
        self.delta = delta
        self.time = time
        self.timezone = timezone

    def __repr__(self):
        return '<Appointment %r>' % self.name

    def get_notification_time(self):
        appointment_time = arrow.get(self.time)
        reminder_time = appointment_time.replace(minutes=-self.delta)
        return reminder_time
from flask.views import MethodView
from flask import render_template
from models.appointment import Appointment
from forms.new_appointment import NewAppointmentForm
from flask import request, redirect, url_for
import reminders
import arrow


class AppointmentResourceDelete(MethodView):

    def post(self, id):
        appt = reminders.db.session.query(Appointment).filter_by(id=id).one()
        reminders.db.session.delete(appt)
        reminders.db.session.commit()

        return redirect(url_for('appointment.index'), code=303)


class AppointmentResourceCreate(MethodView):

    def post(self):
        form = NewAppointmentForm(request.form)

        if form.validate():
            from tasks import send_sms_reminder

            appt = Appointment(**form.data)
            appt.time = arrow.get(appt.time, appt.timezone).to('utc').naive

            reminders.db.session.add(appt)
            reminders.db.session.commit()
            send_sms_reminder.apply_async(
                args=[appt.id], eta=appt.get_notification_time())

            return redirect(url_for('appointment.index'), code=303)
        else:
            return render_template('appointments/new.html', form=form), 400


class AppointmentResourceIndex(MethodView):

    def get(self):
        all_appointments = reminders.db.session.query(Appointment).all()
        return render_template('appointments/index.html',
                               appointments=all_appointments)


class AppointmentFormResource(MethodView):

    def get(self):
        form = NewAppointmentForm()
        return render_template('appointments/new.html', form=form)
from reminders import celery, db, app
from models.appointment import Appointment
from sqlalchemy.orm.exc import NoResultFound
from twilio.rest import Client
import arrow

twilio_account_sid = app.flask_app.config['TWILIO_ACCOUNT_SID']
twilio_auth_token = app.flask_app.config['TWILIO_AUTH_TOKEN']
twilio_number = app.flask_app.config['TWILIO_NUMBER']

client = Client(twilio_account_sid, twilio_auth_token)


@celery.task()
def send_sms_reminder(appointment_id):
    try:
        appointment = db.session.query(
            Appointment).filter_by(id=appointment_id).one()
    except NoResultFound:
        return

    time = arrow.get(appointment.time).to(appointment.timezone)
    body = "Hello {0}. You have an appointment at {1}!".format(
        appointment.name,
        time.format('h:mm a')
    )

    to = appointment.phone_number,
    client.messages.create(
        to,
        from_=twilio_number,
        body=body)
from reminders import celery, db, app
from models.appointment import Appointment
from sqlalchemy.orm.exc import NoResultFound
from twilio.rest import Client
import arrow

twilio_account_sid = app.flask_app.config['TWILIO_ACCOUNT_SID']
twilio_auth_token = app.flask_app.config['TWILIO_AUTH_TOKEN']
twilio_number = app.flask_app.config['TWILIO_NUMBER']

client = Client(twilio_account_sid, twilio_auth_token)


@celery.task()
def send_sms_reminder(appointment_id):
    try:
        appointment = db.session.query(
            Appointment).filter_by(id=appointment_id).one()
    except NoResultFound:
        return

    time = arrow.get(appointment.time).to(appointment.timezone)
    body = "Hello {0}. You have an appointment at {1}!".format(
        appointment.name,
        time.format('h:mm a')
    )

    to = appointment.phone_number,
    client.messages.create(
        to,
        from_=twilio_number,
        body=body)