Moving from Rails to Python
Part One: Controllers
Rails is a mature opinionated MVC framework. It is a relatively painless option for setting up web APIs with relational databases. For many use cases, Rails and Ruby (the language Rails is built on) get the job done. Ruby, however, is not the programing language of choice for big data applications and many of the libraries required to make these application work do not exist in Ruby.
A more popular language for big data application is Python. I recently decided to upgrade my existing Python skills to Python 3 and learn a Python based web framework to expanded the type of projects I could develop.
I did some research and decided my first project should be with Flask, a micro framework that would allow me to learn as I built much of the functionality myself. This series of blogs will go thru the steps I took.
If you want to get up to speed on Python 3, I highly recommend Corey Schafer’s YouTube Channel.
Flask
Flask is a micro framework that I am using strictly to receive and respond to HTTP requests. Flask’s documentation is good and suggest reading the quick start tutorial before going any further.
Routes
The Repo for the code in this blog is here.
The project folder is rails-to-python-routes and the first for the stop for this application is the app.py file:
> rails-to-python-routes
- app.py
from flask import Flask, request, jsonify
from http_method_override import HTTPMethodOverride
from dispatchers import controllersapp = Flask(__name__)
app.wsgi_app = HTTPMethodOverride(app.wsgi_app)version = "api/v1"@app.route(
f"/{version}/<path>",
defaults={'id': None},
methods=['GET', 'POST']
)
@app.route(
f"/{version}/<path>/<int:id>",
methods=['GET', 'PUT', 'PATCH', 'DELETE']
)
def route(**params):
try:
controller = controllers[params['path']](
params.get('id'),
request.get_json(silent=True)
)
except Exception as e:
return ("Bad Path", 404) method = getattr(controller, request.method.lower())
return jsonify(method())if __name__ == '__main__':
app.run(debug=True)
At the top of the file, I import Flask which I installed in my virtual environment like this:
(venv) [10:51:30] (master) rails-to-python-routes// $ pip install flak
The next two imports are my own files which I will later.
The next two lines of code sets the flask application object and middleware to allow for restful HTTP verbs:
> rails-to-python-routes
- app.py...
app = Flask(__name__)
app.wsgi_app = HTTPMethodOverride(app.wsgi_app)
...
The HTTPMethodOverride function comes straight from the Flask documentation and I keep it in separate file and imported it at the top of the app.py file.
> rails-to-python-routes
- http_method_override.py
class HTTPMethodOverride(object):
allowed_methods = frozenset([
'GET',
'HEAD',
'POST',
'DELETE',
'PUT',
'PATCH',
'OPTIONS'
])
bodyless_methods = frozenset(['GET', 'HEAD', 'OPTIONS', 'DELETE'])def __init__(self, app):
self.app = appdef __call__(self, environ, start_response):
method = environ.get('HTTP_X_HTTP_METHOD_OVERRIDE',
'').upper()
if method in self.allowed_methods:
method = method.encode('ascii', 'replace')
environ['REQUEST_METHOD'] = method
if method in self.bodyless_methods:
environ['CONTENT_LENGTH'] = '0'
return self.app(environ, start_response)
The last two lines of code:
> rails-to-python-routes
- app.py...
if __name__ == '__main__':
app.run(debug=True)
Run the Flask app when app.py is run. Adding debug=True, reloads the Flask app every time you save a change in the project folder.
The next block of code defines which HTTP requests to accept:
> rails-to-python-routes
- app.py...
version = "api/v1"@app.route(
f"/{version}/<path>",
defaults={'id': None},
methods=['GET', 'POST']
)
@app.route(
f"/{version}/<path>/<int:id>",
methods=['GET', 'PUT', 'PATCH', 'DELETE']
)...
I abstracted this code from the start. I could have written separate block of code for each route. The users route, for example, would have looked like this:
@app.route('/api/v1/users', defaults={'id': None}, methods=['GET', 'POST'])
@app.route('/api/v1/users/<int:id>', methods=['GET', 'PUT', 'PATCH', 'DELETE'])
The <> notation converts part of the URL to a variable which Flask passes to the function wrapped by the @app.route decorator. Flask refers to this argument as args, but I called it params. You can call it dog, but it must be start with ** in the function argument.
Here is the route function:
> rails-to-python-routes
- app.py...
def route(**params):
try:
controller = controllers[params['path']](
params.get('id'),
request.get_json(silent=True)
)
except Exception as e:
return ("Bad Path", 404)method = getattr(controller, request.method.lower())
return jsonify(method())
...
The function receives:
- A dict called params which includes the keys: ‘path’ and ‘id’.
- It has access to the global Flask object called request. I want to pull the json data from the HTTP request body, which I get by calling request.get_json(). I add the argument, silent=True, so it won’t throw and error if there is no data.
For each HTTP request, I need to determine which controller and which method I need.
I get the controller with a dict where the keys equal to the path names and values equal to the controller class I want to instantiate (these are known as first class function in python). I put this dict in dispatchers.py, which looks like this:
> rails-to-python-routes
- dispatchers.py
from controllers import UsersControllercontrollers = {
'users': UsersController
}
I get the method with a controller super class whose method names are equal the HTTP verbs which call the route controller methods which will be named for the restful routes (I am not adding routes for New or Edit as I am only going to use this as an API).
> rails-to-python-routes
> controllers
- app_controller.py
class AppController():
def get(self):
if self.id is None:
return self.index()
else:
return self.show() def post(self):
return self.create() def put(self):
return self.update() def patch(self):
return self.update() def delete(self):
return self.destroy()
I then need to import AppControl and pass it to the individual controllers.
> rails-to-python-routes
> controllers
- users_controller.pyfrom controllers.app_controller import AppControllerclass UsersController(AppController):
def __init__(self, id, data):
self.id = id
self.data = data def index(self):
return "User#Index" def create(self):
return "User#Create" def show(self):
return "User#Show" def update(self):
return "User#Update" def destroy(self):
return "User#Destroy"
An Example
Step 1: The server receives an HTTP GET request at:
http://127.0.0.1:5000/api/v1/users
Step 2: The request will be caught by:
> rails-to-python-routes
- app.py...
@app.route(
f"/{version}/<path>",
defaults={'id': None},
methods=['GET', 'POST']
)...
Which calls app.py#routes:
> rails-to-python-routes
- app.py...
def route(**params):
try:
controller = controllers[params['path']](
params.get('id'),
request.get_json(silent=True)
)
except Exception as e:
return ("Bad Path", 404)method = getattr(controller, request.method.lower())
return jsonify(method())...
Because, params[‘path’] = ‘users’
controller = a new instance of the class UserController with
- params[‘id’] = None
- requset.get_json(sillent=True) = None
Step 3: The last line of app.py#route determines which UserController method to call. The is done with the getattr() functions which allows me to pass class name and methods as arguemensts:
> rails-to-python-routes
- app.py...
method = getattr(controller, request.method.lower())
return jsonify(method())...
So, calling method() calls the inherited method get on our instance of UsersClass and pass the arguments (id=none, data=none):
> rails-to-python-routes
> controllers
- app_controller.py...
def get(self):
if self.id is None:
return self.index()
else:
return self.show()...
Which, calls self.index() because id is None.
> rails-to-python-routes
> controllers
- users_controller.py...
def index(self):
return "User#Index"...
Which return “User#Index” to:
> rails-to-python-routes
- app.py...method = getattr(controller, request.method.lower())
return jsonify(method())...
In my next blog in this series, I will start to add a database. I am going with MongoDB and will walk thru some issue I had with installing and getting MongoDB to run.