How to Create a Python API Using Flask and Connexion
By: Randal Kamradt Sr
Tags: Programming, Python3, Flask, Docker, Python

How to Create a Python API Using Flask and Connexion

Image by Dean Moriarty from Pixabay

In my article Modern PHP, I created a full-stack application. Then later I wrote Microservices with PHP and Lumen which presented a partial REST API using PHP and Lumen to simply read the data produced by the original full-stack application and make it available as JSON. Today I’m going to make the create, update, and delete a portion of the API to complement the read microservice. And I’m going to do it in Python because one of the freedoms of microservice architecture is you can use different expression languages for each service.

This may seem crazy but think about if you ran a development shop and you had one coder expert at PHP and another coder expert at Python. You could fully utilize both, in their field of expertise.

What about common code? If your microservice is done right, you shouldn’t have common code. You might have common payloads, and those would have to be duplicated. But those types of data structures are so simple to code, repeating yourself is a minor sin. Everyone says always be DRY (don’t repeat yourself), but I’m saying it’s ok to be a little damp. Especially if the repeated code is not logic.

Our open source community is wonderfully mature, so most common code can come from external dependencies. A good microservice should be able to be done in a single file (with maybe some boilerplate files depending on the language).

This is also why unit tests have grown out of favor with me. If your microservices need unit tests, it may not be micro anymore and you might think about how to decompose that. The best way to test microservices, it to deploy them and inspect the inputs and outputs. Postman can be a very handy tool for that and has collections that can be used as a simple test suite.

Authentication

Now that I’ve given my spiel on microservices, let’s jump into the code. The first thing we’re going to accomplish is authentication. Because we will be writing an API that modifies the database, we need to restrict access. The read API I created early can be read by anyone, there’s no user-specific data to it so no need to restrict it. My preferred authentication right now is Okta.

Technically, you could skip to the section on ‘Filling out the API’ if you don’t want authentication. That’s fine if you’re just running locally. But the goal here is to have an API that’s cloud-ready; never put anything live on the Internet that can modify a database or the file system or anything else that can store data and retrieve it later without some restrictions. Leave a server exposed on the internet a few days and take a look at the access logs, they’ll be filled with vulnerability probes.

We are going to reuse the authentication system that we used in the Modern PHP series, specifically part 3 which promises single sign-on for all your applications. For best results in following along, you should read and try to implement the applications shown in all three parts.

How do you make your backend API authenticated? Your front-end authenticates with a third-party service (Okta), receives a token (in this case a JWT), and then sends the JWT in the Authorization header of any request. Then the API can verify that JWT with the same third-party service and break it up into the individual aspects, such as a user-name and roles so that it can do authorization (see this article on authentication vs authorization).

I’m going to start with the Okta Python sample application that can be found in their samples GitHub repository. Let’s clone that and copy the parts we need as a beginning codebase. Starting from a directory where you like to keep your different repositories:

Now set up your new project directory:

mkdir flaskapi

cp samples-python-flask/resource-server/* flaskapi

cp samples-python/requirements.txt .

cd flaskapi

# I use pip3 from brew on my Mac.

# You may also want to set up a virtual environment here

pip3 install -r requirements.txt

Go ahead and edit your README.md and add a LICENSE and anything else you normally keep in your projects. There is a file client_secrets.json.dist and this will keep your secrets from Okta. We don’t want to check this into GitHub once it’s filled in. My usual technique is to copy files with sensitive information to the parent directory of the repository, which never gets committed to git, and then reference it there:

mv client_secrets.json.dist ../client_secrets.json

Now you can edit it and replace the OKTA_DOMAIN , CLIENT_ID , and CLIENT_SECRET . If you had followed along with the Modern PHP series, you should already have a config.yml , that we used for the Vouch single sign-on proxy and has those values in the same directory as client_secrets.json . Otherwise, you will have to use values from Okta from when you set up the application.

Next, edit the main.py to change ./client_secrets.json to ../client_secrets.json . Start the server with:

python3 main.py

And browse to http://localhost:8000

Pretty boring so far! This introduces us to a chicken-and-egg problem, you need the front-end to log on and get the JWT so we can view the access-controlled path api/messages , right now we get:

Since this is part of single sign-on, I could use one of my other applications that use JWT, by using the Chrome developer tools to copy it. But there’s another way. If you have Postman (and you should) add in a GET request with http://localhost:8000/api/messages, click on the Authorization tab, Select Type of OAuth 2.0, and click the ‘Get New Access Token’ button.

Next, fill in the fields

My Callback URL is from the Vouch proxy that I set up earlier so I could have a single callback URL on my Okta client setup. You should use whatever you entered when you set up Okta.

On the next screen, click the ‘Use Token’ button and it will automatically fill in the Authorization header. Now you can press the ‘Send’ button and you should have access to the secure end-point

JWT tokens expire after a while, so you’ll need to get a new one when it does.

Filling out the API

Right now, we just have the example endpoints, let’s make some real endpoints. I’m going to use the Connexion library to turn Swagger markup into all the endpoints. The benefit to this approach is Swagger can not only define your API, it can be used to generate boilerplate code, and a UI that will help you test and document the parameter of the API for other people. Here’s the swagger.yml we’re going to use:

swagger: "2.0"

info:

description: Wine Cheese Pairing API Swagger

version: "1.0.0"

title: Wine Cheese Pairing API

consumes:

- application/json

produces:

- application/json basePath: /api # Paths supported by the server application

paths:

/pairing:

get:

operationId: winePairing.read_all

tags:

- WinePairing

summary: Read the entire list of Wine Cheese Pairings

description: Read the list of Wine Cheese Pairings

parameters:

- name: length

in: query

type: integer

description: Number of Wine Cheese Pairings to get

required: false

- name: offset

in: query

type: integer

description: Offset from beginning

required: false

responses:

200:

description: Successfully read list operation

schema:

type: array

items:

properties:

id:

type: integer

wine:

type: string

wine_descrption:

type: string

cheese:

type: string

cheese_descrption:

type: string

pairing_notes:

type: string post:

operationId: winePairing.create

tags:

- WinePairing

summary: Create a wine pairing and add it

description: Create a new wine pairing

parameters:

- name: pairing

in: body

description: Pairing to create

required: True

schema:

type: object

properties:

wine:

type: string

wine_descrption:

type: string

cheese:

type: string

cheese_descrption:

type: string

pairing_notes:

type: string

responses:

201:

description: Successfully created wine pairing /pairing/{id}:

get:

operationId: winePairing.read_one

tags:

- WinePairing

summary: Read one wine pairing

description: Read one wine pairing

parameters:

- name: id

in: path

description: Id of the pairing

type: integer

required: True

responses:

200:

description: Successfully read wine pairing

schema:

properties:

id:

type: integer

wine:

type: string

wine_descrption:

type: string

cheese:

type: string

cheese_descrption:

type: string

pairing_notes:

type: string put:

operationId: winePairing.update

tags:

- WinePairing

summary: Update a wine pairing

description: Update a wine pairing

parameters:

- name: id

in: path

description: Id of the wine pairing to update

type: integer

required: True

- name: person

in: body

schema:

type: object

properties:

id:

type: integer

wine:

type: string

wine_descrption:

type: string

cheese:

type: string

cheese_descrption:

type: string

pairing_notes:

type: string

responses:

200:

description: Successfully updated wine pairing delete:

operationId: winePairing.delete

tags:

- WinePairing

summary: Delete a wine pairing

description: Delete a wine pairing

parameters:

- name: id

in: path

type: integer

required: True

responses:

200:

description: Successfully deleted a wine pairing

To use Connexion, first install it:

pip3 install connexion

pip3 install "connexion[swagger-ui]"

Notice that connexion[swagger-ui] is quoted. The new shell on the Mac, zsh treats [] as special characters, so we need to put that in quotes. Also, add the new requirements to the end of your requirements.txt.

The operationId is where we connect to the code. So for instance, the winePairing.read_all needs to be a method read_all in the file winePairing.py . Here’s the first pass at winePairing.py :

def read_one(id):

return "this is a pairing" def read_all():

return "this is the pairing list" def create():

return "this is where we create" def update(id):

return "this is where we update" def delete(id):

return "this is where we delete"

Next, we can add the Connexion library into our main.py:

import json

import time from flask import Flask, render_template, url_for, redirect

from flask_cors import CORS

from flask_oidc import OpenIDConnect import connexion # Create the application instance

app = connexion.App(__name__, specification_dir='./') # Read the swagger.yml file to configure the endpoints

app.add_api('swagger.yml') CORS(app.app)

app.app.config.update({

'SECRET_KEY': 'SomethingNotEntirelySecret',

'OIDC_CLIENT_SECRETS': '../client_secrets.json',

'OIDC_ID_TOKEN_COOKIE_SECURE': False,

'OIDC_SCOPES': ["openid", "profile", "email"],

'OIDC_CALLBACK_ROUTE': '/authorization-code/callback'

}) oidc = OpenIDConnect(app.app)

def home():

return "Hello! There's not much to see here." \

"Please grab one of our front-end samples for use with this sample resource server" @app .route("/")def home():return "Hello! There's not much to see here." \"Please grab one of our front-end samples for use with this sample resource server"

.accept_token(True)

def messages():

response = {

'messages': [

{

'date': time.time(),

'text': 'I am a robot.'

},

{

'date': time.time()-3600,

'text': 'Hello, World!'

}

]

} @app .route("/api/messages") @oidc .accept_token(True)def messages():response = {'messages': ['date': time.time(),'text': 'I am a robot.'},'date': time.time()-3600,'text': 'Hello, World!' return json.dumps(response) if __name__ == '__main__':

app.run(host="localhost", port=8000, debug=True)

Now go ahead and try it out. Run:

python3 main.py

And browse to http://localhost:8000/api/ui

Pretty snazzy, huh? Click around and try out each of the endpoints. You won’t get anything back other than the phony messages we put in the winePairing.py file.

Adding a database

Not much point in having an API to modify data if you don’t have a database to store it in. I could go to the repository from my Modern PHP, and use the docker-compose.yml to bring up the database and initialize it, but I’m just going to copy it over here so it’s handy. Since we’re using Docker Compose, you will have to have Docker installed. Follow these instructions if you don’t. In addition, you should have an account at DockerHub

We’re going to be using MariaDB. In case you’re confused about MariaDB vs MySQL, when MySQL was acquired by Oracle, the creators of MySQL didn’t feel that Oracle would maintain MySQL as it’s a competition to their main cash-cow. So it was forked to MariaDB which is open-source. They’ve pretty much remained the same since being forked, so it’s usually pretty easy to swap one for the other. But if you’re a conscientious open-sourcer, use and support MariaDB.

There are three things we need to start up MariaDB, a docker-compose.yml (you could also just run docker from the command line), an init script, and a configuration script with secrets. I will be copying these from the projects created by the Modern PHP series, but I will present them here. Here’s the docker-compose.yml:

version: "3"

services:

mariadb:

image: mariadb

restart: always

volumes:

- ./init:/docker-entrypoint-initdb.d

env_file:

- ../mariadb.env

ports:

- 3306:3306

The image ‘mariadb’ comes from DockerHub, and is the official image. In ‘research’ mode, I usually don’t put an image tag on and just take latest. When you start real development, you should always use a tag so that things don’t mysteriously break. The init directory should contain a script that will be executed when MariaDB starts up the first time. Here’s what I have:

CREATE DATABASE IF NOT EXISTS winelist;

USE winelist;

CREATE TABLE IF NOT EXISTS wine_pairing

(id INT AUTO_INCREMENT NOT NULL,

wine VARCHAR(255) NOT NULL,

wine_description VARCHAR(255) NOT NULL,

cheese VARCHAR(255) NOT NULL,

cheese_description VARCHAR(255) NOT NULL,

pairing_notes VARCHAR(255) NOT NULL,

PRIMARY KEY(id))

DEFAULT CHARACTER SET utf8mb4

COLLATE `utf8mb4_unicode_ci`

ENGINE = InnoDB; INSERT INTO wine_pairing (wine, wine_description, cheese, cheese_description, pairing_notes)

VALUES('wine1', 'wine1description', 'cheese1', 'cheese1description', 'notes1'),

('wine2', 'wine2description', 'cheese2', 'cheese2description', 'notes2'),

('wine3', 'wine3description', 'cheese3', 'cheese3description', 'notes3');

Since this API will be working with my wine and cheese pairing app, it has the one table ‘wine_pairing’.

Finally, I will have the mariadb.env which will contain the secrets so that the application can talk to the database. I will keep this file in the parent directory since it has secrets and we don’t want to commit them to GitHub.

DATABASE_URL=mysql://root:<secrethere>@mariadb:3306/winelist

MYSQL_ROOT_PASSWORD=<secrethere>

MariaDB reads the MYSQL_ROOT_PASSWORD, the other will be used by the API.

With all this in place, we can start up MariaDB. But first, I want to ensure that I haven’t left any containers around from my last section:

docker ps -a

<stray containers listed here>

docker rm <containerId1> <containerId2> <etc...>

Then start her up!

docker-compose up

Notice I didn’t use the -d option, I like to see the logs in my terminal. To continue, you can hit Command-T and it will open a new terminal window. If you want to be able to look at the database, you can use the Docker Compose exec command:

randalkamradt@Fuji flaskapi % docker-compose exec mariadb /bin/sh

# mariadb -u root -p

Enter password:

Welcome to the MariaDB monitor. Commands end with ; or \g.

Your MariaDB connection id is 4

Server version: 10.5.5-MariaDB-1:10.5.5+maria~focal mariadb.org binary distribution Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. MariaDB [(none)]> use winelist;

Reading table information for completion of table and column names

You can turn off this feature to get a quicker startup with -A Database changed

MariaDB [winelist]> select * from wine_pairing;

+----+-------+------------------+---------+--------------------+---------------+

| id | wine | wine_description | cheese | cheese_description | pairing_notes |

+----+-------+------------------+---------+--------------------+---------------+

| 1 | wine1 | wine1description | cheese1 | cheese1description | notes1 |

| 2 | wine2 | wine2description | cheese2 | cheese2description | notes2 |

| 3 | wine3 | wine3description | cheese3 | cheese3description | notes3 |

+----+-------+------------------+---------+--------------------+---------------+

3 rows in set (0.002 sec) MariaDB [winelist]> exit

Bye

# exit

randalkamradt@Fuji flaskapi %

Talking to the database

This is where it gets weird. I spent a lot of time figuring out which drivers I need, and how to implement the solution. What I finally came up with is to split the files into main.py , config.py , models.py , and winePairing.py .

But first, the dependencies. Install these dependencies, and add them to the requirements.txt:

pip3 install marshmallow

pip3 install marshmallow-sqlalchemy

pip3 install sqlalchemy

pip3 install flask_sqlalchemy

pip3 install flask_marshmallow

pip3 install mysql-connector-python

The marshmallow library is for marshalling the input and output payloads. The sqlalchemy library provides ORM type functionality and the mysql-connector-python is the actual MySQL/MariaDB driver.

All of the configuration that needs to be shared across the modules is contained in the config.py, which looks like this:

import os

import connexion

from flask_sqlalchemy import SQLAlchemy

from flask_marshmallow import Marshmallow

from flask_cors import CORS basedir = os.path.abspath(os.path.dirname(__file__))

connex_app = connexion.App(__name__, specification_dir=basedir) app = connex_app.app app.config.from_pyfile('../flask.cfg') db = SQLAlchemy(app) ma = Marshmallow(app) CORS(app) app.config.update({

'SECRET_KEY': 'SomethingNotEntirelySecret',

'OIDC_CLIENT_SECRETS': '../client_secrets.json',

'OIDC_ID_TOKEN_COOKIE_SECURE': False,

'OIDC_SCOPES': ["openid", "profile", "email"],

'OIDC_CALLBACK_ROUTE': '/authorization-code/callback'

})

Here I create the connexion app, which contains the flask app. I created a new secrets file in the parent directory ../flask.cfg for the MariaDB configuration. It looks like this:

SQLALCHEMY_ECHO = True

SQLALCHEMY_DATABASE_URI = 'mysql+mysqlconnector://root:

<pw>@mariadb:3306/winelist'

SQLALCHEMY_TRACK_MODIFICATIONS = False

The config.py also initializes the database, the marshmallow marshaller, and adds the configuration for CORS and openid (Okta).

The final main.py got a lot simpler:

import config # Get the application instance

connex_app = config.connex_app

connex_app.add_api("swagger.yml") if __name__ == "__main__":

connex_app.run(debug=True)

All we do is load up the API definition file, and start the server. Be aware that this is not a production server, so you would want to front it with a WSGI implementation between Python and Nginx in a real environment.

Next is the model where we define the WinePairing data and schema. Since we’re adding this service to an existing database, we don’t need to take advantage of the database creation facilities. Really, we shouldn’t do that in the code anyway, as these are microservices. So there’s no one service that should be in charge of creating the database, it’s best to leave that to an external entity. Anyway, here’s the model.py :

from config import db, ma

from marshmallow_sqlalchemy import ModelSchema class WinePairing(db.Model):

__tablename__ = 'wine_pairing'

id = db.Column(db.Integer, primary_key=True)

wine = db.Column(db.String(32),)

wine_description = db.Column(db.String(32))

cheese = db.Column(db.String(32))

cheese_description = db.Column(db.String(32))

pairing_notes = db.Column(db.String(32)) class WinePairingSchema(ModelSchema):

class Meta:

model = WinePairing

sqla_session = db.session

Again, we are importing from the config.py the database and marshmallow. Then we define two classes, one as a data object and the other as a schema in the sqlalchemy ORM.

Finally, we get to the winePairing.py which now has all of the database calls filled out. Here’s what it looks like:

import connexion

from flask_oidc import OpenIDConnect

from flask import (

make_response,

abort,

request

)

from config import (

db,

app

) from models import (

WinePairing,

WinePairingSchema,

) oidc = OpenIDConnect(app) def read_one(id):

winePairing = WinePairing.query \

.filter(WinePairing.id == id) \

.one_or_none()

if winePairing is not None:

winePairing_schema = WinePairingSchema()

return winePairing_schema.dump(winePairing)

else:

abort(404, 'WinePairing not found for Id: {id}'.format(id=id)) def read_all():

winePairing = WinePairing.query \

.order_by(WinePairing.id) \

.all()

winePairing_schema = WinePairingSchema(many=True)

return winePairing_schema.dump(winePairing)

def create():

schema = WinePairingSchema()

newWinePairing = schema.load(request.json, session=db.session)

db.session.add(newWinePairing)

db.session.commit()

return schema.dump(newWinePairing), 201 @oidc .accept_token(True)def create():schema = WinePairingSchema()newWinePairing = schema.load(request.json, session=db.session)db.session.add(newWinePairing)db.session.commit()return schema.dump(newWinePairing), 201

def update(id):

updateWinePairing = WinePairing.query.filter(

WinePairing.id == id

).one_or_none()

if updateWinePairing is None:

abort(

404,

"WinePairing not found for Id: {id}".format(id=id),

)

schema = WinePairingSchema()

update = schema.load(request.json, session=db.session)

update.id = updateWinePairing.id

db.session.merge(update)

db.session.commit()

data = schema.dump(updateWinePairing)

return data, 200 @oidc .accept_token(True)def update(id):updateWinePairing = WinePairing.query.filter(WinePairing.id == id).one_or_none()if updateWinePairing is None:abort(404,"WinePairing not found for Id: {id}".format(id=id),schema = WinePairingSchema()update = schema.load(request.json, session=db.session)update.id = updateWinePairing.iddb.session.merge(update)db.session.commit()data = schema.dump(updateWinePairing)return data, 200

def delete(id):

winePairing = WinePairing.query.filter(WinePairing.id == id).one_or_none()

if winePairing is not None:

db.session.delete(winePairing)

db.session.commit()

return make_response(

"WinePairing {id} deleted".format(id=id), 200

)

else:

abort(

404,

"WinePairing not found for Id: {id}".format(id=id),

) @oidc .accept_token(True)def delete(id):winePairing = WinePairing.query.filter(WinePairing.id == id).one_or_none()if winePairing is not None:db.session.delete(winePairing)db.session.commit()return make_response("WinePairing {id} deleted".format(id=id), 200else:abort(404,"WinePairing not found for Id: {id}".format(id=id),

As you can see, it got a lot more complicated. One thing that caused me a lot of consternation is the fact that the endpoint methods couldn’t take more than one parameter. So instead of passing the marshaled request body for PUT and POST, I just pass in request.json, and fortunately, the schema.load method was able to consume JSON. It had something to do with using the @oidc.accept_token wrapper function. I’m not sure what I would do if any of the endpoints required extra path parameters.

Moving it all to the Cloud

Now we can containerize it and then add it into one of our cloud deployment scripts. Just one more file is needed, a Dockerfile to tell us how to put this all together. It’s pretty simple:

FROM python:3.9.0-alpine3.12

WORKDIR /project/app

ADD . /project/app

RUN pip install -r requirements.txt

CMD ["python","main.py"]

I’m going to adjust the config.py to get the config files from ../config so I can mount them to /project which is the parent directory of /project/app . I’ll try it out locally with a small addition to the docker-compose.yml :

version: "3"

services:

mariadb:

image: mariadb

restart: always

volumes:

- ./init:/docker-entrypoint-initdb.d

env_file:

- ../mariadb.env

ports:

- 3306:3306

api:

image: rlkamradt/winepairingapi

volumes:

- ../config:/project/config

ports:

- 5000:5000

First, build the image with docker build -t rlkamradt/winepairingapi . Then you can start it up with docker-compose up . Browse to http://localhost:5000/api/pairing, and you should get the list of wines. I notice from the logs that the Swagger UI didn’t get loaded for some reason, but I’m not going to worry about it. We probably shouldn’t have Swagger exposed in a production environment anyway.

Now push to DockerHub: docker push rlkamradt/winepairingapi (of course, use your own DockerHub username) and we should be ready to deploy to the cloud.

I’m going to replace the old read service with this service, since it can read as well as modify. This goes against my desire to keep them separate so that read can be scaled differently than modify and modify could also use a message queue to decouple it from the server making it more responsive in peak times. But I’ll go for simplicity for now, and maybe complicate things later as necessary.

You can find the deploy scripts that I use in this repository. I will simply replace the phprest app with the new pyrest app. I’m also going to keep the phprest.kamradtfamily.net DNS name so I don’t have to mess with CloudFlare. Here is the new section of the nginx.conf :



access_log /dev/stdout;

listen 443 ssl;

server_name phprest.kamradtfamily.net;

ssl_certificate /etc/certs/kamradtfamily.net.pem;

ssl_certificate_key /etc/certs/kamradtfamily.net.key;

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

ssl_ciphers HIGH:!aNULL:!MD5;

location / {

proxy_pass

} server {access_log /dev/stdout;listen 443 ssl;server_name phprest.kamradtfamily.net;ssl_certificate /etc/certs/kamradtfamily.net.pem;ssl_certificate_key /etc/certs/kamradtfamily.net.key;ssl_protocols TLSv1 TLSv1.1 TLSv1.2;ssl_ciphers HIGH:!aNULL:!MD5;location / {proxy_pass http://pyrest:5000

And the new section in the docker-compose.yml:

pyrest:

image: rlkamradt/winepairingapi

volumes:

- ../config:/project/config

ports:

- 5000:5000

Now we can commit and push this up to GitHub so we can start it up in our cloud VM.

Let’s get our new config files up on the cloud VM. Open up an ssh terminal to the VM, make a directory called config and change to it, then you can vi the client_secrets.json and flask.cfg , and copy and paste the contents from your laptop to the VM. We have to handle these files special because they contain secrets so they can’t be part of our GitHub repository which is how we normally get changes up to the VM.

Now we can cd to our ~/phpappprod directory on the cloud VM and get the latest configuration with git pull . Takedown the docker-compose.yml , clean things up, re-pull the images, and restart it:

sudo docker-compose down

sudo docker-compose system prune --all --volumes

sudo docker-compose pull

sudo docker-compose up

I didn’t use the -d option because I’m going to assume there’s a problem and I want to see the logs. And, of course, there is. But the problem is in the configuration file. I forgot to use the password for the MariaDB running on the cloud VM. I use a different password for the version in the cloud than the instance running on my laptop. Fixing that and bringing it back up gives me more errors!

The error in question is because of mismatched versions in dependencies. The fact that pip install doesn’t update requirements.txt seems like a real drawback. I rely on the requirements.txt to make the docker images work, and anyone who wants to run their code on other hosts will rely on it. I started updating some dependencies to see if I could get things to work together.

After a few changes, I was able to bring it up and run through all the endpoints.

So now our API is running in the cloud, available to all, but protected by that Authorization header (except reads).

I will probably make some enemies by saying this, but making this API in Python was the worst experience I’ve had in a long time. I spent a lot of time getting all the pieces to work together properly. I’ve used a lot of languages in my time, and I would not recommend Python as a general-purpose programming language to anyone. I can see its use as a special-purpose scripting language, but trying to put together anything even moderately complex seems a challenge. The language smells of hackery and unguided ‘improvements’ to keep it current. This will probably be the last time you will see me writing in Python, except for in the safe shell of my Jupyter notebook.

Thank you for hearing me out, and I hope I don’t offend too many people with my honest opinion. This has been a rather long article, but we accomplished a lot of things, from authentication, to reading from a database, to deploying in the cloud. Next on my agenda will be to make a React front-end that reads this and gives it a pretty face. Until then, never stop learning (no matter how frustrating it seems sometimes).

Referenced Repositories:

Articles Referenced: