A 'skill' that's always fascinated me is just how long some engineers can make it in their career while carrying glaringly obvious gaps in their knowledge of the systems they use every day. To my surprise, I've turned corners where I myself have been that engineer all along, and there's perhaps no better example of this then the time I've spent with Flask.

WARNING! Highly opinionated statement incoming: Flask is everything a framework should be. That is to say, it isn't really a framework a fully-fledged framework at all. Sure, the term microframework might seem like a cute PR term, but that doesn't negate the fact that there's something about Flask that's different. When I write apps in Flask, I feel as though I'm writing apps in Python. On the other hand, when I write apps in Django, I feel like I'm just writing apps in Django. A disciplined programmer might feel that overly structured frameworks damper creativity and they're probably right: these are the backbones of businesses, thus it makes sense to keep people from deviating from the norm.

The upside of Flask is also its downside: there's nearly an infinite number of ways to solve a single problem. Every Stackoverflow regular has their own preference, and sometimes, just none of them seem... right. The problem is compounded by some of the phrasing coming from Flask's documentation itself. Flask touts the importance of structuring apps with factories and Blueprints, while simultaneously expressing the power behind the application context. What you'll notice over time is that in Flask's own examples, these two 'very important things' never both appear at the same time: that's because they're simply incompatible with one another. This is a highly understated contradiction of philosophies.

Communication Breakdown?

Here's Flask's take on Application factories:

If you are already using packages and blueprints for your application (Modular Applications with Blueprints) there are a couple of really nice ways to further improve the experience. A common pattern is creating the application object when the blueprint is imported.

And here's their description of the Application context:

The application context keeps track of the application-level data during a request, CLI command, or other activity. Rather than passing the application around to each function, the current_app and g proxies are accessed instead.

Considering g is intended to stand for "global" it seems safe from the previous statements that setting attributes of g can be accessed globally within an application... but they can't. This is where we backpedal and get into messy territory:

However, importing the app instance within the modules in your project is prone to circular import issues. When using the app factory pattern or writing reusable blueprints or extensions there won’t be an app instance to import at all.

Flask solves this issue with the application context. Rather than referring to an app directly, you use the the current_app proxy, which points to the application handling the current activity.

Okay, fine. So if I instantiate an application factory with app.app_context(): (which is the only sensible way to create a factory at all)  I should be able to register blueprints within that context, and reference the app context, correct?

I could be crazy but this never seems to work within blueprints. Whether they exist as peer modules or submodules, the words 'from application import current_app as app' always seems to result in the same "missing application context" error. Conveniently it seems, all working examples of the application context seem to be when the Flask developers opt to serve single-file app examples. This stranger from Stackoverflow clears things up a bit:

This happens because the data are lost when the context (with app.app_context()) ends (doc).
Inside the context, everything is ok :

from flask import Flask, g
app = Flask(__name__)
with app.app_context():
   g.my_db = 'database ok'
   print(g.my_db)

this prints 'database ok'

But outside, you cannot access the attribute:

from flask import Flask, g
app = Flask(__name__)
with app.app_context():
   g.my_db = 'database ok'

print(g.my_db)

this throws RuntimeError: Working outside of application context

even if you create a new context:

from flask import Flask, g
app = Flask(__name__)
with app.app_context():
   g.my_db = 'database ok'

with app.app_context():
   print(g.my_db)

this throws AttributeError: '_AppCtxGlobals' object has no attribute 'my_db'

Alas, here I am. Doomed writing posts to fill in the blanks of documentation left behind by others.

Flask Sessions: The REAL Slim Shady

Flask-Session is the MVP when it comes sharing temporary information across modularized parts of our program. In fact, it's a bit odd this isn't encouraged more-so than g. But whatever. We're here to heal.

Sessions can handled in a number of different ways besides cookies. Take a look at the choices we have for storing session-based values in an instance of Flask:

SESSION_TYPE

Specifies which type of session interface to use. Built-in session types:

  • null: NullSessionInterface (default)
  • redis: RedisSessionInterface
  • memcached: MemcachedSessionInterface
  • filesystem: FileSystemSessionInterface
  • mongodb: MongoDBSessionInterface
  • sqlalchemy: SqlAlchemySessionInterface
SESSION_PERMANENT Whether use permanent session or not, default to be True
SESSION_USE_SIGNER Whether sign the session cookie sid or not, if set to True, you have to set flask.Flask.secret_key, default to be False
SESSION_KEY_PREFIX A prefix that is added before all session keys. This makes it possible to use the same backend storage server for different apps, default “session:”
SESSION_REDIS A redis.Redis instance, default connect to 127.0.0.1:6379
SESSION_MEMCACHED A memcache.Client instance, default connect to 127.0.0.1:11211
SESSION_FILE_DIR The directory where session files are stored. Default to use flask_session directory under current working directory.
SESSION_FILE_THRESHOLD The maximum number of items the session stores before it starts deleting some, default 500
SESSION_FILE_MODE The file mode wanted for the session files, default 0600
SESSION_MONGODB A pymongo.MongoClient instance, default connect to 127.0.0.1:27017
SESSION_MONGODB_DB The MongoDB database you want to use, default “flask_session”
SESSION_MONGODB_COLLECT The MongoDB collection you want to use, default “sessions”
SESSION_SQLALCHEMY A flask.ext.sqlalchemy.SQLAlchemy instance whose database connection URI is configured using the SQLALCHEMY_DATABASE_URI parameter
SESSION_SQLALCHEMY_TABLE The name of the SQL table you want to use, default “sessions”

Using Redis For Cached Session Information

For the sake of trying something different, I've opted to pick up a tiny Redis instance from redislabs. I can't help myself but wasting money on new services to play with; after all, check out how cool this little red box looks:

Redis Enterprise: A Unique Primary Database

Perfomance at Scale
Built-in persistence
Failsafe high availability
Active-active geo distribution
Redis Labs
Multi-model
Intelligent tiered access to memory
(RAM and Flash)
Flexible deployment options
(cloud, on-prem, hybrid)
Redis Labs

Fast

Performance at scale
Built-in high performance search

Reliable

Built-in persistence
Failsafe high availability
Active-active geo distribution

Flexible

Multi-model
Flexible deployment options (cloud, hybrid, on-prem)
Intelligent tiered access to memory (ram and flash)
(Why am I not getting paid for this? Why did I take the time to even make that module?)

Redis is NoSQL datastore written in C intended to temporarily hold data in memory for users as they blaze mindlessly through your site. Other use cases include serving as the foundation for real-time chat apps via the publish/subscribe messaging paradigm; popular amongst stupid chat or dating apps slowly destroying our abilities as human beings to interact face-to-face. Powerful stuff.

Structuring init.py Correctly

Consider this to be the guide to Flask Application factories I wish I had months ago. A healthy application factory should:

  • Derive all app configuration values from a class or environment variables.
  • Allow Database interactions to occur at any point within the app.
  • Pass values globally outside of the application context.

This does all of those things:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_session import Session
from flask_redis import FlaskRedis

# Globally accessible libraries
db = SQLAlchemy()
r = FlaskRedis()


def create_app():
    """Initialize the core application."""
    app = Flask(__name__, instance_relative_config=False)
    app.config.from_object('config.Config')

    with app.app_context():
        # Set global session variables
        r.set('endpoint', str(app.config['ENDPOINT']).encode('utf-8'))
        r.set('post_query', str(app.config['POST_QUERY']).encode('utf-8'))
        
        # Initialize Global Libraries
        redis_store.init_app(app)
        db.init_app(app)

        # Include our Routes
        from . import routes

        return app

The order of operations here is critical.

Before we do anything related to the app itself, we create instances of flask_sqlalchemy and flask_redis. This will be initialized with our app once we actually have one created.

The first two lines of create_app() should be no surprise: we're just creating our Flask app, and stating that it should be configured using a class called Config in a file named config.py.

app = Flask(__name__, instance_relative_config=False)
app.config.from_object('config.Config')

Moving down the function comes the moment of truth: creating the app context. What happens in the app context stays in the app context... except for our sick new Redis setup. By using the Redis .set() method, we can assign key/value pairs for Redis hang on to, such as values from our app config which might be needed elsewhere in our app: r.set('endpoint', str(app.config['ENDPOINT']).encode('utf-8')).

Redis stores information as bytes by default, thus attempting to pass values such as strings will result in the infamous `b'leading letter b'` phenomenon. Be sure to encode your values as utf-8 when using set(), and decode when using get().

Making Redis Globally Available

The next part is important: we need to 'initialize' the services we want to use globally (such as database access or Redis) by using init_app(). This must happen inside the application context, with the parameter being app. This is our way of achieving singularity into inter-dimensional travel, thus breaking out of the dreaded application context long after it dies.

Let's Access Some Variables, Baby

The moment of truth: will this actually work? Or am I actually a filthy liar flooding the internet with more uselessly outdated Flask advice? Let's see:

# routes.py

from flask import current_app as app
from flask import make_response
import json
from . import models
from . import r

headers = { 'Access-Control-Allow-Headers': 'Content-Type' }


@app.route('/', methods=['GET', 'POST'])
def entry():
    readers = models.Readers.query.filter_by(username='john').all()
    print(readers)
    print(r.get('uri').decode('utf-8'))
    return make_response(str('readers'), 200, headers)

Eureka! This worthless entry-point prints two things: the value we assigned to our Redis block, and all records in our database of people named John:

>> [<User john>]
>> https://us1-hackersandslackers-543.cloudfunctions.net/link-endpoint?url=

As simple and stupid as it seems, developing an app to this point while understanding why it works is a victory for any developer. I complain about this nearly every post, but the fact of the matter is that the heroes who build much of today's technologies commonly fail to explain their own art in understandable terms. It's an understandable phenomenon resulting from isolated spurts of genius, perhaps, but it damages the growth of companies and humanity alike.

So I guess this is my calling: writing documentation for other people's accomplishments. "Marginally less confusing than 4 open Stackoverflow tabs." That's what I hope to have engraved on my gravestone.

Merry Christmas.