# Send Appointment Reminders with Python and Flask

> \[!NOTE]
>
> Ahoy! We now recommend you build your appointment reminders with Twilio's built-in Message Scheduling functionality. Head on over to the [Message Scheduling documentation](/docs/messaging/features/message-scheduling) to learn more about scheduling messages.

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

We use [Flask](http://flask.pocoo.org/) to build out the web application that supports our user interface, and [Celery](https://www.celeryproject.org/) 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](https://github.com/TwilioDevEd/appointment-reminders-flask/blob/master/README.md) on GitHub to see how to run the code yourself.

*[Check out how Yelp uses SMS to confirm restaurant reservations for diners.](https://customers.twilio.com/1154/yelp/?utm_source=docs\&utm_campaign=docs_to_stories)*

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](/console). You'll also need an SMS-enabled phone number - you can find or purchase a new one to use [here](/user/account/phone-numbers/incoming).

We put these environment variables in a `.env` file and use [autoenv](https://github.com/kennethreitz/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](https://github.com/TwilioDevEd/appointment-reminders-flask).

```bash title="Configure the environment variables" description=".env.example"
# !mark(8:11)
# Environment variables for appointment-reminders-flask

# App settings
export DATABASE_URI=
export SECRET_KEY=asupersecr3tkeyshouldgo
export CELERY_BROKER_URL=redis://localhost:6379
export CELERY_RESULT_BACKEND=redis://localhost:6379

# Twilio settings
export TWILIO_ACCOUNT_SID=ACXXXXXXXXXXXXXXXXX
export TWILIO_AUTH_TOKEN=YYYYYYYYYYYYYYYYYY
export TWILIO_NUMBER=+###########
```

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. Ours 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](https://redis.io/) as a broker for Celery. But you can also use any of the other available [Celery brokers](https://docs.celeryq.dev/en/stable/getting-started/backends-and-brokers/index.html).

To get Celery to run locally on your machine, follow the instructions in the [README](https://github.com/TwilioDevEd/appointment-reminders-flask).

```py title="Our core application code" description="application.py"
# !mark(30:85)
import flask
from flask_migrate import Migrate

from flask_sqlalchemy import SQLAlchemy

from celery import Celery

from config import config_classes
from views.appointment import (
    AppointmentFormResource,
    AppointmentResourceCreate,
    AppointmentResourceDelete,
    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, environment):
        self.flask_app = flask.Flask(__name__)
        self.routes = routes
        self._configure_app(environment)
        self._set_routes()

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

        TaskBase = celery.Task

        class ContextTask(TaskBase):
            abstract = True

            def __call__(self, *args, **kwargs):
                with self.flask_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):
        self.flask_app.config.from_object(config_classes[env])
        self.db = SQLAlchemy(self.flask_app)
        self.migrate = Migrate()
        self.migrate.init_app(self.flask_app, self.db)
```

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

## The Appointment model

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](https://www.sqlalchemy.org/) 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](https://github.com/crsmithdev/arrow) library helps with this kind of time arithmetic.

```py title="The Appointment model" description="models/appointment.py"
from database import db

import arrow


class Appointment(db.Model):
    __tablename__ = 'appointments'

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

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

    def get_notification_time(self):
        appointment_time = arrow.get(self.time)
        reminder_time = appointment_time.shift(minutes=-self.delta)
        return reminder_time
```

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](https://flask-wtf.readthedocs.org/en/latest/index.html) to validate the form data using a class called `NewAppointmentForm` that we defined in `forms/new_appointment.py`.

After that we use [arrow](https://github.com/crsmithdev/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`.

```py title="Scheduling new reminders" description="views/appointment.py"
# !mark(20:46)
import arrow

from flask.views import MethodView
from flask import render_template, request, redirect, url_for

from database import db
from models.appointment import Appointment
from forms.new_appointment import NewAppointmentForm


class AppointmentResourceDelete(MethodView):
    def post(self, id):
        appt = db.session.query(Appointment).filter_by(id=id).one()
        db.session.delete(appt)
        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(
                name=form.data['name'],
                phone_number=form.data['phone_number'],
                delta=form.data['delta'],
                time=form.data['time'],
                timezone=form.data['timezone'],
            )

            appt.time = arrow.get(appt.time, appt.timezone).to('utc').naive

            db.session.add(appt)
            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 = 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)
```

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](https://github.com/twilio/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.

```py title="Set up a Twilio API client" description="tasks.py"
# !mark(1:11)
import arrow

from celery import Celery
from sqlalchemy.orm.exc import NoResultFound
from twilio.rest import Client

from reminders import db, app
from models.appointment import Appointment

twilio_account_sid = app.config['TWILIO_ACCOUNT_SID']
twilio_auth_token = app.config['TWILIO_AUTH_TOKEN']
twilio_number = app.config['TWILIO_NUMBER']
client = Client(twilio_account_sid, twilio_auth_token)

celery = Celery(app.import_name)
celery.conf.update(app.config)


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


celery.Task = ContextTask


@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)
```

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](https://github.com/crsmithdev/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 call to `client.messages.create()`. We use our customer's phone number as the `to` argument and our Twilio number as the `from_` argument.

```py title="Perform the actual task of sending a SMS" description="tasks.py"
# !mark(11:33)
import arrow

from celery import Celery
from sqlalchemy.orm.exc import NoResultFound
from twilio.rest import Client

from reminders import db, app
from models.appointment import Appointment

twilio_account_sid = app.config['TWILIO_ACCOUNT_SID']
twilio_auth_token = app.config['TWILIO_AUTH_TOKEN']
twilio_number = app.config['TWILIO_NUMBER']
client = Client(twilio_account_sid, twilio_auth_token)

celery = Celery(app.import_name)
celery.conf.update(app.config)


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


celery.Task = ContextTask


@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)
```

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](/docs/voice/sdks/javascript/get-started)**

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

**[Two-Factor Authentication](/docs/authy/tutorials/two-factor-authentication-python-flask)**

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