Monday, May 11, 2020

Building a Spring - MVC Like Application in Python

As a long time Java/Spring and Spring-MVC user, when our team recently needed to build a REST API in Python, we set about trying to see how we can create something as similar as possible to what we are accustomed to (and love!) in Spring MVC. There was a lot learning along the way, so I would like to share the very satisfactory results that we had.
The first decision that we needed to make was which HTTP framework to use. We decided to base our application on Flask, one of the well known, easy to use, libraries for building web servers. On top of that, we used Flask-RESTX, which as their web site indicates it “adds support for quickly building REST APIs”. Flask-RESTX has many features, such as simplified generation of Swagger documentation and HTTP error handling. On top of that, we added Flask-Injector, enabling us to use Dependency Injection, a feature that is so basic for us Spring developers. Dependency injection makes it trivial to mock out our database and other dependencies in unit tests. It also makes it easy to set up a singleton connection or our database which can be used in multiple places in our requests
Now that we have our framework of technologies, we can get started. One of the confusing issues within Flask and Flask-RESTX, for those familiar with Flask, is how to organize our APIs. After trying out namespaces and blueprints, we concluded that for the sake of our Swagger documentation that we will generate, the best approach is to use namespaces.

So a simple API would look as follows:

from flask import jsonify
from flask_restx import Resource, Namespace
health_api = Namespace('health', 'health check api')
@health_api.route('health_check', doc=False)
class HealthCheck(Resource):
def get(self):
response = {"Status": "OK"}
return jsonify(response)
For each API we define a separate class. Notice doc=False. This will tell Flask-RESTX that we are not interested in including our health check in the generated swagger documentation. Also, note the naming of the function determines the HTTP Method used to invoke this action.

One of the nice features of Spring MVC is the simplicity with which we can set up interceptors or filters of all requests based on regex expressions. While we didn’t find a way to set up interceptors based on regex, the interceptor feature exists for before and after requests and can be used with dependency injection using Flask-Injector.
Here is a simple example:

from injector import inject
from flask import request, g
@inject
def api_before_request(db_connection: MyDbConnectionClass):
if not request.path.startswith("/health_check"):
logger.info(f"Request received to: {request.url}, headers: {request.headers}, body: {request.json}”)
g.user_info = db_connection.query_user_info(request.headers.get('Username’))

Our interceptor will log all incoming requests that are not the health check. After all, we do not want every ping of the load balancer to our service to be logged. In addition, on all regular user requests, we will pull the authenticated username (via AWS Cognito before our service is invoked) from the Header. We then query our injected datasource to pull additional user information. Once we have this information we insert it into our context using “g” so that this information will be available to all subsequent request handling.
There are two steps missing here to make this work, initialization of our datasource and registering our before (and after) request interceptor. Both of these happen as part of the code needed to initialize our Flask application. These steps include:
  • Create the flask app
  • Using the Flask Object created, add interceptors
  • Create a Class to inherit from injector’s module class to register all our singletons like our database connection for injection
Before we get to this, we will create our flask restx API. As we found in other sample projects we create this in our __init__.py file at the top of our modules of all our requests:

from flask_restx import Api
from .health_check.health import health_api as health_namespace
api = Api(
title='Title',
version='1.0',
description='description',
doc='/api-docs',
)
api.add_namespace(health_namespace, path='/')
view raw __init__.py hosted with ❤ by GitHub
Just as you see we added our health check you can add the rest of your namespaces here. Now, using this flask_restx we can complete our initialization:

import logging
import os
from flask import Flask,
from flask_injector import FlaskInjector
from injector import singleton, Injector, Module
from common_dal import DataAccessLayer, initialize_db_connection
from apis.api_queries import api_before_request
logger = logging.getLogger(__name__)
log_level = os.getenv('LOG_LEVEL', logging.DEBUG)
logger.setLevel(log_level)
logging.basicConfig()
class AppModule(Module):
def __init__(self, app, env, data_access_layer):
self.app = app
self.env = env
self.dal = data_access_layer
def configure(self, binder):
binder.bind(DataAccessLayer, to=self.dal, scope=singleton)
def init_flask():
flask_app = Flask(__name__, static_url_path='')
from apis import api
#api_before_request defined in namespace for our apis
flask_app.before_request(api_before_request)
api.init_app(flask_app, docs='/api-docs')
logger.debug(flask_app.url_map)
return flask_app
def create_run_time_flask():
#use env parameter to retrieve env specific value in our application
env = os.environ.get("ENV")
flask_app = init_flask()
#make the env value accessible globally to all requests
flask_app.config['ENV'] = env
data_access_layer = initialize_db_connection(env)
injector = Injector([AppModule(flask_app, env, dal)])
FlaskInjector(app=flask_app, injector=injector)
return flask_app
view raw flask_init.py hosted with ❤ by GitHub
The bottom function create_run_time_flask() shows how we put together all the initializations in this file. This function is used for runtime (as we run using gunicorn via Docker) whereas a similar function creating mocks for injection is used in our unit test module. In the unit test module, once we have our app set up, we use our flask_app.test_client to send all our REST requests.

Happy Flasking…

No comments:

Post a Comment