Tutorial: Decorators¶
In the previous tutorial on Tutorial: Error Handlers we saw the use of request Interceptors to do simple authorization. The truth is throwing exceptions like this may be pythonic but it it’s a violation of using exceptions for control flow. Having requests which are not authorized isn’t really exceptional behaviour.
In this Tutorial we will build out an authorization validator using both request Interceptors and python decorators. Some frameworks implement their own versions of decorators (often calling them hooks or muddying them with their Interceptors) but this is needless because python decorators are pretty awesome.
Building out a proper authorization cycle also requires talking to a database. For this tutorial we won’t show the database code as it isn’t relevant to the point.
A final change to our prior tutorials is that we will be taking more of an OO approach to our code this time. We do this for two reasons, first to show you how everything we’ve done thus far is just as easy in an OO mindset, and second, because it allows for dependency injection.
With all of that said, first we will show you the full code for this tutorial and then we will work through the parts piece by piece to explain them further.
# auth_example_app.py
import functools
import os
import sys
from eynnyd import RoutesBuilder
from eynnyd import EynnydWebappBuilder
from eynnyd import ResponseBuilder
from eynnyd import ErrorHandlersBuilder
from http import HTTPStatus
from src.config_loader import ConfigLoader
from src.mysql_db_connection_pool import MySQLDBConnectionPool
from src.mysql_messages_dao import MySQLMessagesDAO
from src.mysql_sessions_dao import MySQLSessionsDAO
class MessagesHandler:
def __init__(self, messages_dao):
self._messages_dao = messages_dao
@request_secured_by_session
@requires_json_body
@request_json_field_existence_validation(["user_id"])
def get_user_messages(request):
messages = self._messages_dao.get_messages_for_user_id(request.json_body["user_id"])
return ResponseBuilder() \
.set_status(HTTPStatus.OK) \
.set_utf8_body(json.dumps(messages)) \
.build()
class RequestSessionBuildingInterceptor:
def __init__(self, sessions_dao):
self._sessions_dao = sessions_dao
def load_session_onto_request(self, request):
request.session = None
if "AUTH" not in request.headers:
return request
if not request.headers["auth"]:
return request
request.session = self._sessions_dao.get_valid_session_or_none(request.headers["auth"])
return request
def request_secured_by_session(decorated_function):
@functools.wraps(decorated_function)
def decorator(handler, request, *args, **kwargs):
if not request.session:
return ResponseBuilder() \
.set_status(HTTPStatus.UNAUTHORIZED) \
.set_utf8_body("Request require a valid session. Please login.") \
.build()
return decorated_function
return decorator
def build_application():
configuration = ConfigLoader(os.environ, sys.argv).load()
database_pool = MySQLDBConnectionPool(configuration.get_database_config())
messages_dao = MySQLMessagesDAO(database_pool)
sessions_dao = MySQLSessionsDAO(database_pool)
request_session_building_interceptor = RequestSessionBuildingInterceptor(sessions_dao)
messages_handler = MessagesHandler(messages_dao)
routes = \
RoutesBuilder() \
.add_request_interceptor("/", request_session_building_interceptor.load_session_onto_request) \
.add_handler("GET", "/messages", messages_handler.get_user_messages) \
.build()
return EynnydWebappBuilder() \
.set_routes(routes) \
.build()
application = build_application()
So what we have is an application with a single Route which returns a list of messages from our database
given a user_id
. This Route is secured by an authorization header. We use the request Interceptor
request_session_building_interceptor.load_session_onto_request
to load a valid session onto the
request object and then use the @request_secured_by_session
decorator to make the decision what to
do if it isn’t there. The value here is that we can now wrap any Handler we want to be secured using the
@request_secured_by_session
but if we have a non secured endpoint (for example a register endpoint)
then we can simply leave off the decorator and it is not secured. The information about the endpoint being
secured is at the definition site of the function, where it should be. Because the Interceptor is built
ahead of time, database access can be injected into it (where as this would involve something hackish to
do inside the decorator).
Now the Interceptor has one job: loading the session onto the request. The decorator has one job: returning an error response if the valid session does not exist. The Handler method has one job: getting the messages for the user id.
We will discuss all the parts of this code in much further detail below.
The Handler¶
First we have our Handler who’s responsibility is to get messages for a user. Ideally all other code isn’t in the Handler so that we don’t obfuscate the code.
class MessagesHandler:
def __init__(self, messages_dao):
self._messages_dao = messages_dao
@request_secured_by_session
@requires_json_body
@request_json_field_existence_validation(["user_id"])
def get_user_messages(request):
messages = self._messages_dao.get_messages_for_user_id(request.json_body["user_id"])
return ResponseBuilder() \
.set_status(HTTPStatus.OK) \
.set_utf8_body(json.dumps(messages)) \
.build()
Note that the code in the Handler function clearly states how we get the messages for the user and nothing else. However, using decorators we can see that before this function executes we:
Secure our request for sessions
Validates the body has json content (and in this case loads the json into request.json_body).
Validates that the json contains a field keyed on “user_id”
This is a lot of logic that is no longer muddying what our Handler does, but is still clearly visible as being executed for this Handler. More importantly, the many other Handler who would need this same functionality can have it, in a readable fashion, without obfuscating their logic either.
Also different from the other tutorials, this Handler is inside an object. We do this so that we can take
advantage of dependency injection. We injected a messages data access object (DAO) into this handling class.
This class does not care that this DAO is connecting us to a MySQL database, only that it has a method
called get_messages_for_user_id
that takes a user_id
and returns a list of messages.
The Interceptor¶
The next piece of code to look at is the class holding our Interceptor:
class RequestSessionBuildingInterceptor:
def __init__(self, sessions_dao):
self._sessions_dao = sessions_dao
def load_session_onto_request(self, request):
request.session = None
if "auth" not in request.headers:
return request
if not request.headers["auth"]:
return request
request.session = self._sessions_dao.get_valid_session_or_none(request.headers["auth"])
return request
As in the Handler above we have put this method inside a class because we want to exploit dependency injection of our sessions data access object.
You can quickly see that all this method does is either load a session onto the request from the database
or it sets the value to None. We actually wouldn’t use None
for this generally, but rather
optionals, but we figured this tutorial was not the platform to discuss that.
As should be expected, this Interceptor has nothing to do with getting a response back to the user, it simply mutates the request, loading new values onto it. We have removed the unnecessary exception raising from our Interceptor and saved ourselves one less violation of exceptions as control flow.
The Decorator¶
Instead of throwing exceptions and using Error Handlers to return a bad response we instead have a python decorator wrap our Handler function. The code for this decorator looks like:
def request_secured_by_session(decorated_function):
@functools.wraps(decorated_function)
def decorator(handler, request, *args, **kwargs):
if not request.session:
return ResponseBuilder() \
.set_status(HTTPStatus.UNAUTHORIZED) \
.set_utf8_body("Request require a valid session. Please login.") \
.build()
return decorated_function
return decorator
All this decorator does is check if the Interceptor put a valid session onto the request. If it didn’t we return an UNAUTHORIZED status response. If a valid session is present we call through to the wrapped function.
Wiring Up Dependencies¶
Another change you might have seen in this tutorial is that we build up a series of objects before we start building our Routes. These objects are our dependency chain. The code looks like:
configuration = ConfigLoader(os.environ, sys.argv).load()
database_pool = MySQLDBConnectionPool(configuration.get_database_config())
messages_dao = MySQLMessagesDAO(database_pool)
sessions_dao = MySQLSessionsDAO(database_pool)
request_session_building_interceptor = RequestSessionBuildingInterceptor(sessions_dao)
messages_handler = MessagesHandler(messages_dao)
First we have an object which loads configuration from various sources (the environment, command line, and any configuration files we happen to read in). We need this configuration to build other dependencies.
Next we have a database pool connection which requires a selection of values from our configuration result.
Then we have two DAOss, the messages_dao
and the sessions_dao
. Note that on the right side
of the assignment here we care that this is a MySQL implementation but on the left we just care that it is
a DAO. In a statically typed language we would be using an interface on the left, but this is python, so
life is easier. Note that into the DAOs we inject our database pool. These DAOs dont care about the specifics
of our MySQL driver, only that they can execute sql commands against a database.
Now that we have our DAOs we can build our Interceptors and Handlers. For this tutorial we just have the one of each. Into each of these we inject our built DAOs.
This kind of dependency build up allows code to be easy to read, debug, extend, and maintain. In fact, in his book :ref:`Clean Architecture <https://www.amazon.com/Clean-Architecture-Craftsmans-Software-Structure/dp/0134494164>`__ Robert C. Martin makes a very strong argument that dependency inversion like this is the only real advantage OO gave us. Several other WSGI frameworks prevent this kind of dependency injection.
Setting Up The Routes¶
Finally we have code which should look pretty familiar at this point throughout the tutorials. We build our Routes:
routes = \
RoutesBuilder() \
.add_request_interceptor("/", request_session_building_interceptor.load_session_onto_request) \
.add_handler("GET", "/messages", messages_handler.get_user_messages) \
.build()
The only reason to call attention to it here is so that you see how the function assignment works with Interceptors and Handlers which have been encapsulated into classes.