Build a RESTful Flask API - The TDD Way - scotch.io Tutorial 1 Notes

The Build a RESTful Flask API - The TDD Way tutorial by Jee Gikera is one of the best tutorials I have followed and it seemed to meet an impossible combination of my learning needs (Flask + API + TDD + Python + PostgreSQL + SQLAlchemy). Jee’s attention to detail and scalable architecture are two areas missing sometimes from other tutorials.

Below are my notes after completing the first tutorial. The name of my project used throughout these notes is strobla and Activity is the data model.

Where’s my project GitHub link? I am using Atlassian Bitbucket as a free private source code repository whilst my project is in its early stages. Open sourcing my project, either through Bitbucket or GitHub, is a decision I’ll make later..

Virtual Environment

On Windows, autoenv did not set environment variables from .env when I cd into the project folder.

After trying alternatives (source, etc), I created the following file in my home directory:

# C:\Users\naaman\.env_strobla.ps1

$env:PROJECT_HOME="C:\Users\naaman\OneDrive\Documents\Code\strobla-python"
cd $env:PROJECT_HOME
.\venv\Scripts\activate.ps1
$env:FLASK_APP="run.py"
$env:SECRET="secret"
$env:ROLLBAR_TOKEN="token"
$env:APP_STAGE="dev"
$env:DATABASE_URL="postgresql://strobla:password@localhost/strobla"
$env:DATABASE_URL_TESTING="postgresql://strobla:password@localhost/strobla_testing"
atom

The .env_.ps1 file can be easily run each time you open a new PowerShell console. The file also helps to keep secrets out of the version-controlled project directory.

APP_STAGE replaces APP_SETTINGS in my project and was changed as a personal preference and readability. Throughout my notes, I will make minor changes to variable names however I recommend using the same directory structures and naming conventions.

Environment Configurations

I pointed the Testing database URI to a new environment variable to keep passwords out of source control. Also note the different environment names I am using..

# instance\config.py

import os

class Config(object):
    """ Parent configuration class """
    DEBUG = False
    CSRF_ENABLED = True
    SECRET = os.getenv('SECRET')
    SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL')

class TestingConfig(Config):
    """ Testing environment configuration """
    """  - with a separate test database """
    TESTING = True
    SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL_TESTING')
    DEBUG = True

class DevConfig(Config):
    """ Dev stage configuration """
    DEBUG = True

class QAConfig(Config):
    """ QA stage configuration """
    DEBUG = True

class ProdConfig(Config):
    """ Prod stage configuration """
    DEBUG = False
    TESTING = False

app_config = {
    'testing': TestingConfig,
    'dev': DevConfig,
    'qa': QAConfig,
    'prod': ProdConfig,
}

Data Model

My project uses retrieves JSON data from the Strava API and saves it into the Postgres database. For simplicity, my table contains two columns:

  • id : Derived from the JSON data instead of auto creating new IDs
  • data : a JSONB type Postgres column to store the JSON object

When issuing PUT API calls, I was getting Duplicate Key errors. I added an update() function to the model to support all PUT calls. An alternative is to use Activity.query.filter_by(id=id).update() within app\__init__.py.

# app\models.py

from app import db
from sqlalchemy.dialects.postgresql import JSONB
import json

class Activity(db.Model):
    """ This class represents the Strava activities table """

    __tablename__ = 'activities'

    id = db.Column(db.Integer, primary_key=True)
    data = db.Column(JSONB)
    date_created = db.Column(
        db.DateTime,
        default=db.func.current_timestamp())
    date_modified = db.Column(
        db.DateTime,
        default=db.func.current_timestamp(),
        onupdate=db.func.current_timestamp())

    def __init__(self, data):
        """ initialise with activity id & JSON data """
        self.id = data['id']
        self.data = data

    def save(self):
        db.session.add(self)
        db.session.commit()

    def update(self):
        id_from_data = self.data['id']
        stmt = self.__table__.update().\
            where(self.id==id_from_data).\
            values(data=self.data)
        result = db.session.execute(stmt)
        db.session.commit()
        return result

    ### AS PER TUTORIAL ###

API Functionality

A note separate to the tutorial is the handling of JSON data through the API. When submitting JSON through the API, eg. self.client().put('/activities/id, data={JSON}'), the JSON must be wrapped in a json.dumps() call to avoid Flask ValueErrors. Similarly, use json.loads() to convert back to the original JSON format.

A minor change was made to the PUT section to use activity.update() instead of .save() and the DELETE section was changed as follows as I was unable to get the tutorial code to work:

# app\__init__.py

  if request.method == 'DELETE':
      activity.delete()
      response = jsonify({
          'message': 'activity {} deleted'.format(activity.id)
      })
      response.status_code = 200
      return response

Conclusion

The tutorial helps understand how the numerous pieces fit together in a fully-featured Flask-based API application. I was able to adapt my project easily to fit within this architecture and continue learning how to write Python code. Looking forward to Part 2..