This page guides you through developing a globally-available web application. It is the fourth section of the Develop and Deploy a Global Application tutorial.
This tutorial uses SERIALIZABLE
isolation. Client-side retry handling is not necessary under READ COMMITTED
isolation.
Before you begin
Before you begin this section, complete the previous sections of the tutorial, ending with Set Up a Virtual Environment for Developing Global Applications.
Project structure
The application needs to handle requests from clients, namely web browsers. To translate these kinds of requests into database transactions, the application stack consists of the following components:
- A multi-region database schema that defines the tables and indexes for user, vehicle, and ride data. The database schema is covered on a separate page, Create a Multi-Region Database Schema.
- A multi-node geographically-distributed CockroachDB cluster where each node's locality corresponds to a cloud provider region.
Database deployment is covered on two separate pages:
- To set up a demo cluster, refer to Set Up a Virtual Environment for Developing Multi-Region Applications.
- To set up up a multi-region cluster, refer to Deploy a Global Application.
- Python class definitions that map to the tables in the database. Refer to Mappings.
- Functions that wrap database transactions. Refer to Transactions.
- A backend API that defines the application's connection to the database. Refer to Database connection.
- A Flask server that handles requests from clients. Refer to Web application.
- HTML files that the Flask server can render into web pages. Refer to User interface.
In the sections that follow, we go over each of the files and folders in the project, with a focus on the backend and database components. The project's code is structured as follows:
movr
├── Dockerfile ## Defines the Docker container build
├── LICENSE ## A license file for your application
├── Pipfile ## Lists PyPi dependencies for pipenv
├── Pipfile.lock
├── README.md ## Contains instructions on running the application
├── __init__.py
├── dbinit.sql ## Initializes the database, and partitions the tables by region
├── init.sh ## Initializes the environment variables for debugging
├── movr
│  ├── __init__.py
│  ├── models.py ## Defines classes that map to tables in the movr database
│ ├── movr.py ## Defines the primary backend API
│  └── transactions.py ## Defines transaction callback functions
├── requirements.txt ## Lists PyPi dependencies for Docker container
├── server.py ## Defines a Flask web application to handle requests from clients and render HTML files
├── static ## Static resources for the web frontend
│  ├── css
│  ├── img
│  └── js
├── templates ## HTML templates for the web UI
│  ├── layouts
│  ├── _nav.html
│  ├── login.html
│  ├── register.html
│  ├── rides.html
│  ├── user.html
│  ├── users.html
│  ├── vehicles-add.html
│  └── vehicles.html
└── web
├── __init__.py
├── config.py ## Contains Flask configuration settings
├── forms.py ## Defines FlaskForm classes
└── gunicorn.py ## Contains gunicorn configuration settings
SQLAlchemy with CockroachDB
Object Relational Mappers (ORMs) map classes to tables, class instances to rows, and class methods to transactions on the rows of a table. The sqlalchemy
package includes some base classes and methods that you can use to connect to your database's cluster from a Python application, and then map tables in that database to Python classes.
In our example, we use SQLAlchemy's Declarative extension, which is built on the mapper()
and Table
data structures. We also use the sqlalchemy-cockroachdb
Python package, which defines the CockroachDB SQLAlchemy dialect. The package includes some functions that help you handle transactions in a running CockroachDB cluster.
Mappings
After completing the Create a Multi-Region Database Schema section, you should be familiar with the movr
database and each of the tables in the database (users
, vehicles
, and rides
).
Open movr/models.py
, and look at the first 10 lines of the file:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, String, DateTime, Boolean, Interval, ForeignKey, PrimaryKeyConstraint
from sqlalchemy.types import DATE
from sqlalchemy.dialects.postgresql import UUID
import datetime
from werkzeug.security import generate_password_hash
from flask_login import UserMixin
Base = declarative_base()
The first data structure that the file imports is declarative_base
, a constructor for the declarative base class, built on SQLAlchemy's Declarative extension. All mapped objects inherit from this object. We assign a declarative base object to the Base
variable right below the imports.
The models.py
file also imports some other standard SQLAlchemy data structures that represent database objects (like columns), data types, and constraints, in addition to a standard Python library to help with default values (i.e., datetime
).
Since the application handles user logins and authentication, we also need to import some security libraries from the Flask ecosystem. We'll cover the web development libraries in more detail in the Web application section below.
The User
class
Recall that each instance of a table class represents a row in the table, so we name our table classes as if they were individual rows of their parent table, since that's what they'll represent when we construct class objects.
Take a look at the User
class definition:
class User(Base, UserMixin):
"""
Represents rows of the users table.
Arguments:
Base {DeclaritiveMeta} -- Base class for declarative SQLAlchemy class definitions that produces appropriate `sqlalchemy.schema.Table` objects.
UserMixin {UserMixin} -- Mixin object that provides default implementations for the methods that Flask-Login expects user objects to have.
Returns:
User -- Instance of the User class.
"""
__tablename__ = 'users'
id = Column(UUID, primary_key=True)
city = Column(String)
first_name = Column(String)
last_name = Column(String)
email = Column(String)
username = Column(String, unique=True)
password_hash = Column(String)
is_owner = Column(Boolean)
def set_password(self, password):
"""
Hash the password set by the user at registration.
"""
self.password_hash = generate_password_hash(password)
def __repr__(self):
return "<User(city='{0}', id='{1}', name='{2}')>".format(
self.city, self.id, self.first_name + ' ' + self.last_name)
The User
class has the following attributes:
__tablename__
, which holds the stored name of the table in the database. SQLAlchemy requires this attribute for all classes that map to tables.- All of the other attributes of the
User
class (id
,city
,first_name
, etc.), stored asColumn
objects. These attributes represent columns of theusers
table. The constructor for eachColumn
takes the column data type as its first argument, and then any additional arguments, such asprimary_key
. - To help define column objects, SQLAlchemy also includes classes for SQL data types and column constraints. For the columns in this table, we use
UUID
andString
data types. - The
__repr__
function, which defines the string representation of the object.
The Vehicle
class
Next, look at the Vehicle
class definition:
class Vehicle(Base):
"""
Represents rows of the vehicles table.
Arguments:
Base {DeclaritiveMeta} -- Base class for declarative SQLAlchemy class definitions that produces appropriate `sqlalchemy.schema.Table` objects.
Returns:
Vehicle -- Instance of the Vehicle class.
"""
__tablename__ = 'vehicles'
id = Column(UUID, primary_key=True)
city = Column(String)
type = Column(String)
owner_id = Column(UUID, ForeignKey('users.id'))
date_added = Column(DATE, default=datetime.date.today)
status = Column(String)
last_location = Column(String)
color = Column(String)
brand = Column(String)
def __repr__(self):
return "<Vehicle(city='{0}', id='{1}', type='{2}', status='{3}')>".format(
self.city, self.id, self.type, self.status)
Recall that the vehicles
table contains more columns and data types than the users
table. It also contains a foreign key constraint (on the users
table), and a default value. These differences are reflected in the Vehicle
class.
The Ride
class
Take a look at the Ride
class:
class Ride(Base):
"""
Represents rows of the rides table.
Arguments:
Base {DeclaritiveMeta} -- Base class for declarative SQLAlchemy class definitions that produces appropriate `sqlalchemy.schema.Table` objects.
Returns:
Ride -- Instance of the Ride class.
"""
__tablename__ = 'rides'
id = Column(UUID, primary_key=True)
city = Column(String, ForeignKey('vehicles.city'))
rider_id = Column(UUID, ForeignKey('users.id'))
vehicle_id = Column(UUID, ForeignKey('vehicles.id'))
start_location = Column(String)
end_location = Column(String)
start_time = Column(DateTime)
end_time = Column(DateTime)
length = Column(Interval)
def __repr__(self):
return "<Ride(city='{0}', id='{1}', rider_id='{2}', vehicle_id='{3}')>".format(
self.city, self.id, self.rider_id, self.vehicle_id)
The rides
table has three foreign key constraints, one on the users
table and two on the vehicles
table.
Transactions
After you create a class for each table in the database, you can start defining functions that bundle together common SQL operations as atomic transactions.
The SQLAlchemy CockroachDB dialect
The sqlalchemy-cockroachdb
Python library handles transactions in SQLAlchemy with the run_transaction()
function. This function takes a transactor
, which can be an Engine
, Connection
, or sessionmaker
object, and a callback function. It then uses the transactor
to connect to the database, and executes the callback as a database transaction. For some examples of run_transaction()
usage, see Transaction callback functions below.
run_transaction()
abstracts the details of client-side transaction retries away from your application code. Transactions may require retries if they experience deadlock or read/write contention with other concurrent transactions that cannot be resolved without allowing potential serializable anomalies. As a result, a CockroachDB transaction may have to be tried more than once before it can commit. This is part of how we ensure that our transaction ordering guarantees meet the ANSI SERIALIZABLE isolation level.
run_transaction()
has the following additional benefits:
- When passed a
sqlalchemy.orm.session.sessionmaker
object, it ensures that a new session is created exclusively for use by the callback, which protects you from accidentally reusing objects via any sessions created outside the transaction. Note that asessionmaker
objects is different from asession
object, which is not an allowabletransactor
forrun_transaction()
. By abstracting transaction retry logic away from your application, it keeps your application code portable across different databases. Because all callback functions are passed to
run_transaction()
, theSession
method calls within those callback functions are written a little differently than the typical SQLAlchemy application. Most importantly, those functions must not change the session and/or transaction state. This is in line with the recommendations of the SQLAlchemy FAQs, which state (with emphasis added by the original author) thatAs a general rule, the application should manage the lifecycle of the session externally to functions that deal with specific data. This is a fundamental separation of concerns which keeps data-specific operations agnostic of the context in which they access and manipulate that data.
and
Keep the lifecycle of the session (and usually the transaction) separate and external.
In keeping with the above recommendations from the official docs, we strongly recommend avoiding any explicit mutations of the transaction state inside the callback passed to
run_transaction()
. Specifically, we do not make calls to the following functions from insiderun_transaction()
:sqlalchemy.orm.Session.commit()
(or other variants ofcommit()
)
This is not necessary because
run_transaction()
handles the savepoint/commit logic for you. -sqlalchemy.orm.Session.rollback()
(or other variants ofrollback()
)This is not necessary because
run_transaction()
handles the commit/rollback logic for you. -Session.flush()
This will not work as expected with CockroachDB because CockroachDB does not support nested transactions, which are necessary for
Session.flush()
to work properly. If the call toSession.flush()
encounters an error and aborts, it will try to roll back. This will not be allowed by the currently-executing CockroachDB transaction created byrun_transaction()
, and will result in an error message like the following:sqlalchemy.orm.exc.DetachedInstanceError: Instance <FooModel at 0x12345678> is not bound to a Session; attribute refresh operation cannot proceed (Background on this error at: http://sqlalche.me/e/bhk3).
In the example application, all calls to
run_transaction()
are located within the methods of theMovR
class, which is defined inmovr/movr.py
. This class represents the connection to the running database. Requests to the web application frontend, which are defined inserver.py
, are routed to theMovR
class methods.
Transaction callback functions
To separate concerns, we define all callback functions passed to run_transaction()
calls in a separate file, movr/transactions.py
. These callback functions wrap Session
method calls, like session.query()
and session.add()
, to perform database operations within a transaction.
We recommend that you use a sessionmaker
object, bound to an existing Engine
, as the transactor
that you pass to run_transaction()
. This protects you from accidentally reusing objects via any sessions created outside the transaction. Every time run_transaction()
is called, it uses the sessionmaker
object to create a new Session
object for the callback. If the sessionmaker
is bound to an existing Engine
, the same database connection can be reused.
transactions.py
imports all of the table classes that we defined in movr/models.py
, in addition to some standard Python data structures needed to generate correctly-typed row values that the ORM can write to the database.
from movr.models import Vehicle, Ride, User
import datetime
import uuid
Reading
A common query that a client might want to run is a read of the rides
table. The transaction callback function for this query is defined here as get_rides_txn()
:
def get_rides_txn(session, rider_id):
"""
Select the rows of the rides table for a specific user.
Arguments:
session {.Session} -- The active session for the database connection.
rider_id {UUID} -- The user's unique ID.
Returns:
List -- A list of dictionaries containing ride information.
"""
rides = session.query(Ride).filter(
Ride.rider_id == rider_id).order_by(Ride.start_time).all()
return list(map(lambda ride: {'city': ride.city,
'id': ride.id,
'vehicle_id': ride.vehicle_id,
'start_time': ride.start_time,
'end_time': ride.end_time,
'rider_id': ride.rider_id,
'length': ride.length},
rides))
The get_rides_txn()
function takes a Session
object and a rider_id
string as its inputs, and it outputs a list of dictionaries containing the columns-value pairs of a row in the rides
table. To retrieve the data from the database bound to a particular Session
object, we use Session.query()
, a method of the Session
class. This method returns a Query
object, with methods for filtering and ordering query results.
Note that get_rides_txn()
gets all rides for a specific rider, which might be located across multiple regions. As discussed in MovR: A Global Application Use-Case, running queries on data in multiple regions of a multi-region deployment can lead to latency problems. Because this function defines a read operation, it requires fewer trips between replicated nodes than a write operation, but will likely be slower than a read operation on data constrained to a single region.
Another common query would be to read the registered vehicles in a particular city, to see which vehicles are available for riding. Unlike get_rides_txn()
, the get_vehicles_txn()
function takes the city
string as an input.
def get_vehicles_txn(session, city):
"""
Select the rows of the vehicles table for a specific city.
Arguments:
session {.Session} -- The active session for the database connection.
city {String} -- The vehicle's city.
Returns:
List -- A list of dictionaries containing vehicle information.
"""
vehicles = session.query(Vehicle).filter(
Vehicle.city == city, Vehicle.status != 'removed').all()
return list(
map(
lambda vehicle: {
'city': vehicle.city,
'id': vehicle.id,
'owner_id': vehicle.owner_id,
'type': vehicle.type,
'last_location': vehicle.last_location + ', ' + vehicle.city,
'status': vehicle.status,
'date_added': vehicle.date_added,
'color': vehicle.color,
'brand': vehicle.brand},
vehicles))
This function filters the query on the city
column. vehicle
rows with the same value for city
are inserted from the same region, making city
values implicitly correspond to a specific region. Because the vehicles
table has a REGIONAL BY ROW
locality, CockroachDB can locality-optimize queries from nodes with a locality matching the hidden crdb_region
column. This limits latency, as the query only needs to travel to database deployments in a single region.
Writing
There are two basic types of write operations: creating new rows and updating existing rows. In SQL terminology, these are INSERT
/UPSERT
statements and UPDATE
statements. All transaction callback functions that update existing rows include a session.query()
call. All functions adding new rows call session.add()
. Some functions do both.
For example, start_ride_txn()
, which is called when a user starts a ride, adds a new row to the rides
table, and then updates a row in the vehicles
table.
def start_ride_txn(session, city, rider_id, vehicle_id):
"""
Insert a new row into the rides table and update a row of the vehicles table.
Arguments:
session {.Session} -- The active session for the database connection.
city {String} -- The vehicle's city.
rider_id {UUID} -- The user's unique ID.
rider_city {String} -- The city in which the rider is registered.
vehicle_id {UUID} -- The vehicle's unique ID.
"""
v = session.query(Vehicle).filter(Vehicle.id == vehicle_id).first()
r = Ride(
city=city,
id=str(
uuid.uuid4()),
rider_id=rider_id,
vehicle_id=vehicle_id,
start_location=v.last_location,
start_time=datetime.datetime.now(
datetime.timezone.utc))
session.add(r)
v.status = "unavailable"
The function takes the city
string, rider_id
UUID, rider_city
string, and vehicle_id
UUID as inputs. It queries the vehicles
table for all vehicles of a specific ID. It also creates a Ride
object, representing a row of the rides
table. To add the ride to the table in the database bound to the Session
, the function calls Session.add()
. To update a row in the vehicles
table, it modifies the object attribute. start_ride_txn()
is called by run_transaction()
, which commits the transaction to the database.
Be sure to review the other callback functions in movr/transactions.py
before moving on to the next section.
Now that we've covered the table classes and some transaction functions, we can look at the interface that connects web requests to a running CockroachDB cluster.
Database connection
The MovR
class, defined in movr/movr.py
, handles connections to CockroachDB using SQLAlchemy's Engine
class.
Let's start with the beginning of the file:
from movr.transactions import start_ride_txn, end_ride_txn, add_user_txn, add_vehicle_txn, get_users_txn, get_user_txn, get_vehicles_txn, get_rides_txn, remove_user_txn, remove_vehicle_txn
from cockroachdb.sqlalchemy import run_transaction
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.dialects import registry
registry.register("cockroachdb", "cockroachdb.sqlalchemy.dialect",
"CockroachDBDialect")
class MovR:
"""
Wraps the database connection. The class methods wrap database transactions.
"""
def __init__(self, conn_string):
"""
Establish a connection to the database, creating Engine and Sessionmaker objects.
Arguments:
conn_string {String} -- CockroachDB connection string.
"""
self.engine = create_engine(conn_string, convert_unicode=True)
self.sessionmaker = sessionmaker(bind=self.engine)
movr.py
first imports the transaction callback functions that we defined in movr/transactions.py
. It then imports the run_transaction()
function. The MovR
class methods call run_transaction()
to execute the transaction callback functions as transactions.
The file also imports some sqlalchemy
libraries to create instances of the Engine
and Session
classes, and to register the CockroachDB as a dialect.
When called, the MovR
class constructor creates an instance of the Engine
class using an input connection string. This Engine
object represents the connection to the database, and is used by the sessionmaker()
function to create Session
objects for each transaction.
Backend API
The MovR
class methods function as the "backend API" for the application. Frontend requests get routed to the these methods.
We've already defined the transaction logic in the transaction callback functions, in movr/transactions.py
. We can now wrap calls to the run_transaction()
function in the MovR
class methods.
For example, look at the start_ride()
method, the method to which frontend requests to start a ride are routed:
def start_ride(self, city, rider_id, vehicle_id):
"""
Wraps a `run_transaction` call that starts a ride.
Arguments:
city {String} -- The ride's city.
rider_id {UUID} -- The user's unique ID.
vehicle_id {UUID} -- The vehicle's unique ID.
"""
return run_transaction(
self.sessionmaker, lambda session: start_ride_txn(
session, city, rider_id, vehicle_id))
This method takes some keyword arguments, and then returns a run_transaction()
call. It passes sessionmaker(bind=self.engine)
as the first argument to run_transaction()
, which creates a new Session
object that binds to the Engine
instance initialized by the MovR
constructor. It also passes city
, rider_id
, rider_city
, and vehicle_id
, values passed from the frontend request, as inputs. These are the same keyword arguments, and should be of the same types, as the inputs for the transaction callback function start_ride_txn()
.
Be sure to review the other functions in movr/movr.py
before moving on to the next section.
Web application
The application needs a frontend and a user interface. We use Flask for the web server, routing, and forms. For the web UI, we use some basic, Bootstrapped HTML templates and just a little CSS and JS.
Most of the web application components are found in the web
and templates
folders, and in the server.py
file.
Configuration
We store the Flask configuration settings in a Config
class, defined in web/config.py
:
# This file defines classes for flask configuration
import os
class Config:
"""Flask configuration class.
"""
DEBUG = os.environ['DEBUG']
SECRET_KEY = os.environ['SECRET_KEY']
API_KEY = os.environ['API_KEY']
DB_URI = os.environ['DB_URI']
REGION = os.environ['REGION']
PREFERRED_URL_SCHEME = ('https', 'http')[DEBUG == 'True']
CITY_MAP = {'gcp-us-east1': ['new york', 'boston', 'washington dc'],
'gcp-us-west1': ['san francisco', 'seattle', 'los angeles'],
'gcp-europe-west1': ['amsterdam', 'paris', 'rome']}
This file imports the os
library, which is used to read from environment variables. When debugging a local deployment, these environment variables are set by the .env
file that Pipenv
reads. In a multi-region deployment, the environment variables are set by the Dockerfile
, and by the managed cloud deployment service.
The application sets a global Flask configuration variable (DEBUG
), which it checks to determine whether or not to run against a demo cluster, or a real multi-region deployment.
Web forms
Forms make up an important part of most web application frontends. We define these in web/forms.py
, using some data structures from the flask_wtf
and wtforms
libraries.
We will not go into much detail about these forms. The important thing to know is that they help handle POST
requests in the web UI.
The CredentialForm
class, for example, defines the fields of the login form that users interface with to send a login request to the server:
class CredentialForm(FlaskForm):
"""Login form class.
"""
username = StringField('Username: ', validators=[data_required()])
password = PasswordField('Password: ', validators=[data_required()])
submit = SubmitField('Sign In')
Most of the forms defined on this page take the inputs for database reads and writes.
VehicleForm
, for example, defines the fields of the vehicle registration form. Users enter information about a vehicle they would like to register, and the data is routed to the add_vehicle()
method defined in movr/movr.py
:
class VehicleForm(FlaskForm):
"""Vehicle registration form class.
"""
type = SelectField(label='Type',
choices=[('bike', 'Bike'), ('scooter', 'Scooter'),
('skateboard', 'Skateboard')])
color = StringField(label='Color', validators=[data_required()])
brand = StringField(label='Brand')
location = StringField(label='Current location: ',
validators=[data_required()])
submit = SubmitField('Add vehicle')
Initialization
server.py
defines the main process of the application: the web server. After initializing the database, we run python server.py
to start up the web server.
Let's look at the first ten lines of server.py
:
from flask import Flask, render_template, session, redirect, flash, url_for, Markup, request, Response
from flask_bootstrap import Bootstrap, WebCDN
from flask_login import LoginManager, current_user, login_user, logout_user, login_required
from werkzeug.security import check_password_hash
from movr.movr import MovR
from web.forms import CredentialForm, RegisterForm, VehicleForm, StartRideForm, EndRideForm, RemoveUserForm, RemoveVehicleForm
from web.config import Config
from sqlalchemy.exc import DBAPIError
The first line imports standard Flask libraries for connecting, routing, and rendering web pages. The next three lines import some libraries from the Flask ecosystem, for bootstrapping, authentication, and security.
The next few lines import other web resources that we've defined separately in our project:
- The
MovR
class, which handles the connection and interaction with the running CockroachDB cluster. - Several
FlaskForm
superclasses, which define the structure of web forms. - The
Config
class, which holds configuration information for the Flask application.
Finally, we import the DBAPIError
type from sqlalchemy
, for error handling.
The next five or so lines initialize the application:
DEFAULT_ROUTE_AUTHENTICATED = "vehicles"
DEFAULT_ROUTE_NOT_AUTHENTICATED = "login_page"
# Initialize the app
app = Flask(__name__)
app.config.from_object(Config)
Bootstrap(app)
login = LoginManager(app)
protocol = ('https', 'http')[app.config.get('DEBUG') == 'True']
Calling the Flask()
constructor initializes our Flask web server. By assigning this to a variable (app
), we can then configure the application, store variables in its attributes, and call it with functions from other libraries, to add features and functionality to the application.
For example, we can bootstrap the application for enhanced HTML and form generation with Bootstrap(app)
. We can also add authentication with LoginManager(app)
, and then control the default routes, based on authentication, with the DEFAULT_ROUTE_AUTHENTICATED
and DEFAULT_ROUTE_AUTHENTICATED
constants.
Note that we also define a protocol
variable that the application later uses to determine its protocol scheme. You should always use HTTPS for secure connections when building an application that accepts user login information.
After initializing the application, we can connect to the database:
conn_string = app.config.get('DB_URI')
movr = MovR(conn_string)
These two lines connect the application to the database. First the application retrieves the connection string that is stored as a configuration variable of the app
. Then it calls the MovR()
constructor to establish a connection to the database at the location we provided to the Config
object.
User authentication
User authentication is handled with the Flask-Login
library. This library manages user logins with the LoginManager
, and some other functions that help verify if a user has been authenticated or not.
To control whether certain routes are accessible to a client session, we define a user_loader()
function:
@login.user_loader
def load_user(user_id):
return movr.get_user(user_id=user_id)
To restrict access to a certain page to users that are logged in with the LoginManager
, we add the @login_required
decorator function to the route. We'll go over some examples in the Routing section below.
Routing
We define all Flask routing functions directly in server.py
.
Flask applications use @app.route()
decorators to handle client requests to specific URLs. When a request is sent to a URL served by the Flask app instance, the server calls the function defined within the decorator (the routing function).
Flask provides a few useful callbacks to use within a routing function definition:
redirect()
, which redirects a request to a different URL.render_template()
, which renders an HTML page, with Jinja2 templating, into a static webpage.flash()
, which sends messages from the application output to the webpage.
In addition to calling these functions, and some other standard Flask and Python libraries, the application's routing functions need to call some of the methods that we defined in movr/movr.py
(the "backend API").
For example, look at the login()
route:
@app.route('/login', methods=['GET', 'POST'])
def login_page():
if current_user.is_authenticated:
return redirect(url_for(DEFAULT_ROUTE_AUTHENTICATED, _external=True, _scheme=protocol))
else:
form = CredentialForm()
if form.validate_on_submit():
try:
user = movr.get_user(username=form.username.data)
if user is None or not check_password_hash(
user.password_hash, form.password.data):
flash(
Markup(
'Invalid user credentials.<br>If you aren\'t registered with MovR, go <a href="{0}">Sign Up</a>!'
).format(
url_for('register',
_external=True,
_scheme=protocol)))
return redirect(
url_for('login_page', _external=True,
_scheme=protocol))
login_user(user)
return redirect(
url_for(DEFAULT_ROUTE_AUTHENTICATED, _external=True, _scheme=protocol))
except Exception as error:
flash('{0}'.format(error))
return redirect(
url_for('login_page', _external=True, _scheme=protocol))
return render_template('login.html',
title='Log In',
form=form,
available=session['region'])
Client Location
To optimize for latency in a global application, when a user arrives at the website, their request needs to be routed to the application deployment closest to the location from which they made the request. This step is handled outside of the application logic, by a cloud-hosted, global load balancer. In our example multi-region deployment, we use a GCP external load balancer that distributes traffic based on the location of the request.
Note that the application uses a region
variable to keep track of the region in which the application is deployed. This variable is read in from the REGION
environment variable, which is set during application deployment. The application uses this region
variable to limit the cities in which a user can look for vehicles to ride to the supported cities in the region.
User interface
For the example application, we limit the web UI to some static HTML web pages, rendered using Flask's built-in Jinja-2 engine. We will not spend much time covering the web UI. Just note that the forms take input from the user, and that input is usually passed to the backend where it is translated into and executed as a database transaction.
We've also added some Bootstrap syntax and Google Maps, for UX purposes. As you can see, the Google Maps API requires a key. For debugging, you can define this key in the .env
file. If you decide to use an embedded service like Google Maps in production, you should restrict your Google Maps API key to a specific hostname or IP address from within the cloud provider's console, as the API key could be publicly visible in the HTML. In Deploying a Multi-Region Web Application, we use GCP secrets to the store the API keys.
Next Steps
After you finish developing and debugging your application, you can learn about deploying the application.