Click here to Skip to main content
15,886,794 members
Articles / Hosted Services / Azure
Article

Creating a Python Cloud-Native Webapp, Part 2: Adding an Azure Cosmos DB Database

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
6 Jan 2022CPOL8 min read 8.5K   2   1
In this article, we’ll build on what we started by adding an Azure Cosmos DB database to our Flask web app.
We’ll create it using Azure Databases for VSCode, and we’ll connect to it using the Cosmos DB API for MongoDB and the PyMongo client. We’ll add the code needed to get our app hooked up to its new database, and we’ll show how to run a migration to automatically create our database schema.

This article is a sponsored article. Articles such as these are intended to provide you with information on products and services that we consider useful and of value to developers

In the previous article, we created a web app service in Azure that hosts a Python-based Flask task management application. However, our application currently doesn’t have any functionality, such as saving or retrieving data.

Because we’re Python developers and not database administrators, we want to avoid setting up and managing any servers or other infrastructure. So, in this final article of the series, we’ll add a database to our application using the Azure Cosmos DB service. Then, we’ll finally have a fully functional basic task management web application.

Creating a Cosmos DB Service

Image 1

Before creating our database and saving data, we need a way for Visual Studio Code (VS Code) to open and manage our service. We'll use the Azure Database extension for this.

First, we open the project folder from the previous article in VS Code and go to the Extensions menu (CTRL+SHIFT+X) on the left. We search for the extension Azure Databases for VS Code and install it. When we have established this extension, we open the Azure (CTRL+SHIFT+A) menu on the left. There should now be a Databases section.

Image 2

We need to create a new database for our application next. So, we click on the plus (+) icon next to the Databases section to begin the creation process. To create a database, we need to:

  1. Specify the subscription where we want to make the resource.
  2. Choose the database server type. For Python, we use the Azure Cosmos DB for MongoDB API.
  3. Provide an account name, like "flasktutorialdbaccount."
  4. Specify a capacity model. We select serverless here, but if you need guaranteed performance, you can choose Provisioned Throughput.
  5. Select or Create a resource group. Our previous App Service created a resource group called "appsvc_linux_centralus" (or similar), so we use that here.
  6. Select a location, like "US East."

Image 3

When this process is complete, we should now have a Cosmos DB service under our subscription in the Database section of the Azure menu. We right-click on this service and select the copy connection string option.

We'll store the connection string as an environment variable in our Azure App Service. So, we open the App Service section of the Azure tab, select the subscription, then select our Web App. Under our Web App is an area for Application Settings. We right-click on the Application Settings heading and select Add New Setting. We name it, for example, "MONGODB," and paste the connection string.

We’ll also add this database to our local application to test saving and entering data locally. A better development practice would be to run a different database service or run the service locally (this is challenging since the Storage Explorer doesn’t fully support the MongoDB API at this time).

To keep it simple, we'll reuse our current database connection by going to the Run menu, selecting Open Configurations, and adding the following line to the env section:

"MONGODB": "<your_connection_string>"

Connecting to Cosmos DB with PyMongo

We’ll use the PyMongo client to connect our application to the database. We can install this client with the command:

Python
pip install pymongo

We’ll also need to update our requirements.txt file manually by adding the line pymongo==3.12.1 or by using the command pip freeze > requirements.txt. Once we have installed PyMongo, we can import it into our application similarly to Flask.

Now we create a new file in our base directory called "database.py." This file will hold all our database code to save, update, and retrieve tasks. Then, we add the following code to the top of our file:

Python
from pymongo import MongoClient
import os, datetime, uuid
 
mongo_client = MongoClient(os.environ["MONGODB"])
mongo_db = mongo_client['TaskManagerDB']
mongo_collection_tasks = mongo_db['Tasks']

The first two lines import the Mongo client and some system libraries we'll need later to access environment variables, use date and time functions, and generate unique identifiers.

Next, we create the Mongo client by loading it with the MONGODB environment variable as the connection string. Finally, we create a variable that points to a database called "TaskManagerDB" with a collection set called "Tasks."

Cosmos DB stores data in documents rather than rows within a table, like JSON files familiar to Python developers. However, we need to initialize our database with some temporary data because neither the database nor the collection exists.

So, let’s create our initial data set by generating a task JSON in a function called "initialize_db." We enter the following code in our database.py file:

Python
def initialize_db():
    if "Tasks" not in mongo_db.list_collection_names():
        mongo_collection_tasks.insert_one({ 
            "taskid": str(uuid.uuid4()),
            "name": "Test Task 1", 
            "Description": "Description of Test Task 1", 
            "Due": datetime.datetime.utcnow() + datetime.timedelta(days=20),
            "Completed": False,
            "Created": datetime.datetime.utcnow(),
            "Modified": datetime.datetime.utcnow()
            })

This function will check to ensure Tasks doesn’t exist (Collections only exist if they contain data.) If Tasks doesn’t exist, the function will insert a task and generate a unique ID.

If you want more starting data, you can repeat this command a few times to insert more than one task (we created four but simplified the code above).

Before we leave this database script, let’s also create two more functions to retrieve data by entering in the following code:

Python
def find_tasks(starting_point):
        tasks = []
        for task in mongo_collection_tasks.find(skip=starting_point, limit=10):
            tasks.append(task)
        return tasks
 
    def find_task(task_id):
        return mongo_collection_tasks.find_one({"taskid": task_id})

These two functions will either retrieve ten tasks from a starting point (so we can page our data) or the specific task based on an internal ID. We switch back to our app.py and put a new import on the top of the file using the code from .database import database.

Next, after defining our app variable, let’s also initialize our database using the code database.initialize_db().

Finally, let’s also change our home function to take data from our find_tasks function as follows:

Python
@app.route("/")
def home():
    return render_template(
        "home.html", data=database.find_tasks(0)
    )

This change pushes a variable called "data" into our template that has an array of task objects we can use.

Our final step is to update our home.html template to remove much of the placeholder data and loop through the data element for our database data with the following code:

HTML
{% extends "layout.html" %}
{% block body %}
<div class="container px-4 py-5">
    <h2 class="pb-2 border-bottom">Current Tasks</h2>
    <div class="row row-cols-1 row-cols-sm-1 row-cols-md-2 row-cols-lg-3 g-4 py-5">
        <!-- Repeat Block for Each Task-->
        {% for task in data %}
        <div class="col d-flex align-items-center card">
            <div class="card-body">
                <h4 class="fw-bold mb-0">{{task.name}}</h4>
                <p>{{task.description}}</p>
                <a href="/viewtask/{{task._id}}" class="btn btn-primary">View Task</a>
                <a href="/edittask/{{task._id}}" class="btn btn-primary">Edit Task</a>
            </div>
        </div>
        {% endfor %}
        <!-- End Repeat -->
    </div>
</div>
{% endblock %}

Image 4

The {% for task in data %} line in our code now loops through the array we pass in. Then, it generates the card div with all the data from our database. If we run this code locally and open the address http://127.0.0.1:5000/, we should now see all the tasks we created.

At this stage, we can also deploy our web application to our Azure App Service. We open the Azure tab on the left, expand the App Service option, select our subscription, right-click on our Web App, then click Deploy to Web App. After the deployment process runs for a little while, we should see the same task list using our external URL.

Viewing and Updating Tasks

Now that our application is pulling in a list of tasks from our database, let’s do the same for our view and edit task functions. First, we need to update the view_task and the GET route of our edit_task methods in a similar way to our home method, with the following code:

Python
@app.route("/viewtask/<string:task_id>", methods=['GET'])
def view_task(task_id):
    return render_template(
        "viewtask.html", data=database.find_task(task_id)
    )
 
@app.route("/edittask/<string:task_id>", methods=['GET'])
def edit_task(task_id):
    return render_template(
        "edittask.html", data=database.find_task(task_id)
    )

Like our home route, this code passes in a data object with the task details from the database. Let’s now update the view_task.html file to remove the placeholder values and put in the database data values:

HTML
{% extends "layout.html" %}
{% block body %}
<div class="container container-md px-4 py-5">
    <h2 class="pb-2 border-bottom">View Task</h2>
    <div class="row">
        <table class="table table-hover">
            <tr><td>Name: </td><td>{{data.name}}</td></tr>
            <tr><td>Description: </td><td>{{data.description}}</td></tr>
            <tr><td>Due On: </td><td>{{data.due}}</td></tr>
            <tr><td>Completed: </td><td>{{data.completed}}</td></tr>
            <tr><td>Created On: </td><td>{{data.created}}</td></tr>
            <tr><td>Modified On: </td><td>{{data.modified}}</td></tr>
        </table>
    </div>
</div>
{% endblock %}
We also need to do something similar to our edit screen by updating the edittask.html using the following code:
{% extends "layout.html" %}
{% block body %}
<div class="container container-md px-4 py-5">
    <h2 class="pb-2 border-bottom">Edit Task</h2>
    <form method="POST" enctype="multipart/form-data">
        <div class="mb-3">
            <label for="name" class="form-label">Task Name</label>
            <input type="text" class="form-control" name="name" id="name" value="{{ data.name }}">
        </div>
        <div class="mb-3">
            <label for="description" class="form-label">Description</label>
            <textarea  class="form-control" name="description" id="description" rows="5">{{ data.description }}</textarea>
        </div>
        <div class="mb-3">
            <label for="due" class="form-label">Due On</label>
            <input type="text" class="form-control" name="due" id="due"  value="{{ data.due }}">
        </div>
        <div class="form-check mb-3">
            <input type="checkbox" class="form-check-input" name="completed" id="completed" value="{{ data.completed }}">
            <label for="completed" class="form-check-label">Completed</label>
        </div>
        <div class="container"><p></p></div>
        <div class="mb-3">
            <div class="row">
                <div class="col-md-4">
                    <label for="created" class="form-label">Created</label>
                    <input type="text" class="form-control" name="created" id="created"  value="{{ data.created }}" disabled>
                </div>
                <div class="col-md-4">
                    <label for="modified" class="form-label">Modified</label>
                    <input type="text" class="form-control" name="modified" id="modified" value="{{ data.modified }}" disabled>
                </div>
            </div>  
        </div>
        <input class="btn btn-primary" type="submit" value="Update Task" />
        <a href="/" class="btn btn-secondary" type="button">Home</a>
    </form>
</div>
{% endblock %}

Image 5

When we run our application now, we see that the various fields display the data from our database.

We could take this a little further and improve the date and time fields and the completed field, but the core functionality is working, so let’s move on to updating data in our database.

To handle the POST response from our form, we’ll expand our edittask route further using the following code:

Python
@app.route("/edittask/<string:task_id>", methods=['GET', 'POST'])
def edit_task(task_id):
    if request.method == 'GET':
        return render_template(
            "edittask.html", data=database.find_task(task_id)
        )
    if request.method == 'POST':
        data = database.find_task(request.view_args['task_id'])
        data['name'] = request.form['name']
        data['description'] = request.form['description']
        data['due'] = request.form['due']
        if 'completed' in request.form.keys():
            data['completed'] = True
            
        database.update_task(data)
        return view_task(request.view_args['task_id'])

Our edit function has now changed quite a bit. This route will now accept GET and POST methods. The function now runs the previous GET functionality if a GET method comes in. If the code uses the POST method, we use the initial task ID in the URL to look up the task from the database. We then update that task’s name, description, due date, and completion status using the data in our form.

Finally, we call a new function to update the task using our composited data before redirecting the user to the "view task" page. For this action to work, we also need to create our update_task method in our database file by entering the following code:

Python
def update_task(data):
        data['modified'] = datetime.datetime.utcnow()
        return mongo_collection_tasks.update({"taskid": data['taskid']}, data)

Image 6

This method updates the modified field in the passed JSON file with the current date and time before using the MongoDB update function to update our document.

When we run our application now and edit a task, we should be able to modify our tasks and save the modifications back to our database. We can also deploy the application back to our App Service and see the same results.

Next Steps

We have now written a minimal task management application with little Python code. The application can read and write data to an Azure Cosmos DB service and deploy it to an Azure App Service without leaving our code editor.

You can expand this application by adding a "create new task" function, similar to the current edit task page, which uses the same "insert_one" method that we used to initialize our database. You can also add more data elements, build better type handling in the form, and add validation to complete the project.

Now that you know the possibilities of building Python web applications quickly and easily using Azure resources, you may be inspired to create a unique application. Sign up for a free trial to experience for yourself just how easy it is to run and scale Python web apps on Azure.

To learn more about how deploy a Python web app to App Service on Linux, check out Quickstart: Create a Python app.

This article is part of the series 'Cloud Native Python on Azure View All

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
United States United States
Hi! I'm a Solution Architect, planning and designing systems based in Denver, Colorado. I also occasionally develop web applications and games, as well as write. My blog has articles, tutorials and general thoughts based on more than twenty years of misadventures in IT.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA7-Jan-22 3:53
professionalȘtefan-Mihai MOGA7-Jan-22 3:53 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.