It's been roughly a year since MongoDB launched their Stitch: a "back-end as a service" cloud offering. I've been tinkering with Mongo on the cloud ever since... Alright fine, "tinkering with" may better be described as "accidentally became dependent on it after developing new features in production environments," but I can't really complain thus-far. If you're not familiar, MongoDB Atlas is MongoDB's cloud-hosted database offering; that is to say, the same as any other MongoDB database, except very expensive.
The jury is still out on how MongoDB Atlas and its counterpart Stitch will fit into the picture of next generation cloud services. That said, I can vouch that Mongo products are simply fun to use for developers, especially when compared to traditional rigid alternatives. Since I would also group Python and Flask in the 'fun to use' category, selecting MongoDB as the database for your Flask app makes a lot of sense.
For this tutorial we're going to set up a simple app where users can submit information via a form to MongoDB. After writing to our database, we'll query the db to see the results. The result will be a Flask app with the following file structure:
my-flask-project
├── templates/
├── static/
├── app.py
├── config.py
├── currenttime.py
└── form.py
Connect to your Database with PyMongo
PyMongo is Python's go-to library for interacting with MongoDB.
We'll keep all database connection logic within db.py. After importing PyMongo, most of the configuration we need to handle happens in a single line containing our MongoDB URI: the massive string which contains our DB location, creds, and authorization DB. The string is broken down like this:
mongodb+srv://[username]:[password]@[projectname]-gktww.gcp.mongodb.net/[authDB]
Authenticate with a [username] and [password] you’ve set up in whichever database handles authentication for your MongoDB instance (this is also what [authDB] is referring to).
[projectname] is the unique name of your cloud instance. The rest of the URI contains some nonsense, including the host of your particular instance (I’m using Google Cloud, hence the .gcp in the URI). Most of this information can be found just by jumping on mongodb.com and investigating your URI via the "connect" popup:
Now we can set up our connection:
import pymongo
mongo = pymongo.MongoClient('mongodb+srv://username:password@hackerdata-gktww.gcp.mongodb.net/admin', maxPoolSize=50, connect=False)
Note that we intentionally set the connection to False. Otherwise, we're going to find ourselves in a hell of managing open connections every time we interact with the DB.
Speaking of the DB, we need to specify which database and collection we want to interact with. This brings our config file to something as follows:
import pymongo
mongo = pymongo.MongoClient('mongodb+srv://username:password@myInstance-gktww.gcp.mongodb.net/admin', maxPoolSize=50, connect=False)
db = pymongo.database.Database(mongo, 'mydatabase')
col = pymongo.collection.Collection(db, 'mycollection')
Lastly, if you'd like to access, say, all the objects inside of a collection (or similar query), we'll just need to add a few lines line to ensure we're reading the collection's data:
import pymongo
from bson.json_util import dumps
import json
mongo = pymongo.MongoClient('mongodb+srv://username:password@myInstance-gktww.gcp.mongodb.net/admin', maxPoolSize=50, connect=False)
db = pymongo.database.Database(mongo, 'mydatabase')
col = pymongo.collection.Collection(db, 'mycollection')
col_results = json.loads(dumps(col.find().limit(5).sort("time", -1)))
Remember that Mongo returns BSON objects as opposed to JSON objects, which isn't very useful for our purposes. To alleviate this we'll do a messy little dance to convert Mongo's BSON into a string, and convert this to JSON using json.dumps().
Note: the need to do this may have been something changed in recent versions of Mongo, as I have older application functioning where this wasn't the case. ¯\_(ツ)_/¯.
Creating a Form
Heading over to form.py, we just need to set up a simple single-field form for users to submit their URLs. For the sake of Python, let's say we're only accepting URLs for Jupyter noteboooks:
from wtforms import Form, StringField, validators
from wtforms.validators import DataRequired, Regexp
class myForm(Form):
"""Homepage form."""
PlotlyURL = StringField('Provide a raw .ipynb URL from Github',
validators=[
DataRequired(),
Regexp(".*\.ipynb$",
message="Please provide a URL ending in ipynb"),
])
We could have an entire tutorial just about Flask's WTForms, but let's stay on topic and move on to currenttime.py.
Adding Time Metadata
In a lot of cases where we store information to a database, we at least want to add certain metadata such as the time something was added. This allows us to arrange results by most recently updated, which we'll be doing in this example.
from datetime import datetime, timezone
def getTime():
"""Get user's current time"""
rightnow = datetime.today()
return rightnow
def getPrettyTime():
"""Get user's pretty current time"""
rightnow = datetime.today()
prettytime = rightnow.ctime()
return prettytime
yourtime = getTime()
prettytime = getPrettyTime()
The variable yourtime will be a datetime string representing the local time of the user creating a new record. We will use this value to sort the queried results by time. On the contrary, prettytime will be the same time, only formatted in a way that is readable to humans.
Putting the Pieces Together
Finally we get to move on app.py and get this thing moving. We'll initiate our app by importing the necessary libraries, as well as the scripts we just created:
from flask import Flask, render_template, Markup, request, redirect
from config import col, col_results
import requests
from form import myForm
from flask_static_compress import FlaskStaticCompress
from currenttime import yourtime, prettytime
import logging
Note that we need to import from the DB config we set earlier is the "col" variable; we'll only be interacting directly with the collection we want to modify, and the rest is assumed within the config file itself. Now let's build a route for our homepage that does two things:
- Allows users to submit a URL via the simple form we created
- Displays all previous searches by all users.
from flask import Flask, render_template, Markup, request, redirect
from config import col, col_results
import requests
from form import myForm
from flask_static_compress import FlaskStaticCompress
from currenttime import yourtime, prettytime
import logging
@app.route('/', methods=['GET', 'POST', 'OPTIONS'])
def home():
"""Landing page."""
recent_searches = list(col_results)
return render_template('/index.html', form=myForm(), recents=recent_searches, template="home-template")
There's only two significant lines here, but let's break them down piece by piece.
recent_searches
First we set a recent_searches variable which is essentially a query against our collection to retrieve a list of previous searches. We ask that these be returned as a list() upfront. Typically the find() method would contain the constraints of our query, but we're simply asking to return all results in the collection, with a limit() up to 5. Finally, we sort() the results by the field we refer to as 'time' is descending order, as noted by the -1 argument.
This is all probably very difficult to visualize without a graphic. Here's a snapshot of the collection we're defining with dummy data added:
render_template
We already know the basics of serving templates and assets in Flask, so it shouldn't be too difficult to break down the last line in our route:
'/index.html'
specifies the base template we'll be serving up.form=myForm()
passes the form class we created earlier to the form partial we're including as part of the index page.recents=recent_searches
passes the query of previous searches to the template, with which we can build a widget.template="home-template"
is a simple variable passed which we'll utilize as a class on the page we're loading.
The Result
From everything we've completed, you should be expecting to see a somewhat worthless page where users can submit links via a form, simply to see results posted by previous posters. If we expand on this idea just a bit, we can see how something so simple can actually be extended to a full product:
Planet Jupyter
Style your Jupyter Notebooks.
Planet Jupyter is demo product we built at H&S to style Jupyter notebooks. Perhaps 60% of the logic behind Planet Jupyter is the simple DB interactions we just covered, with the rest being added flair.
This is not a shameless plug for the barely functioning toys we've built, mind you, but rather an example of simple DB interactions using Flask can be easily extensible into relevant, useful, products.
We hope you’ve found this tutorial to be useful!