Tutorial: Adding Values To Requests¶
Requests are preloaded in Eynnyd with all the values from the raw WSGI request. However, as you
are processing your request, you may wish to add additional details to it. For example, on routes secured by a
session, you may want to load that session from your database in an
request interceptor and put that loaded value onto your request for later
use (without reloading it). Because python allows you to manipulate objects after they have been built, you
could do this simply by doing request.session = session_dao.get_session(request.headers["session"])
but
that wouldn’t be very explicit and mutation like this can lead to hidden bugs, surprised readers, and much more.
A more explicit way of doing this is shown below.
Some might argue that the mutation method we just talked about is more “pythonic” than the explicit version we
are about to show you, however we would direct them to the zen of python (type import this
into any
python terminal) which specifically (and correctly) states: “Explicit is better than implicit”.
As a simple example, let’s assume we want to add a random ID to every request so that when we log things about it the ID can be matched up.
This tutorial builds on the response interceptors tutorial so if you have not read that yet, and you find something confusing in here, it is recommended you look there for your answer. First we will show you the code and then we will explain the relevant parts (AKA the parts not in the prior tutorials).
# hello_world_app.py
import logging
from eynnyd import AbstractRequest
from eynnyd import RoutesBuilder
from eynnyd import EynnydWebappBuilder
from eynnyd import ResponseBuilder
from eynnyd import ErrorHandlersBuilder
from http import HTTPStatus
import uuid
LOG = logging.getLogger("hello_world_app")
class IDEnhancedRequest(AbstractRequest):
def __init__(self, original_request, request_id):
self._request_id = request_id
self._original_request = original_request
@property
def request_id(self):
return self._request_id
@property
def http_method(self):
return self._original_request.http_method
@property
def request_uri(self):
return self._original_request.request_uri
@property
def forwarded_request_uri(self):
return self._original_request.forwarded_request_uri
@property
def headers(self):
return self._original_request.headers
@property
def client_ip_address(self):
return self._original_request.client_ip_address
@property
def cookies(self):
return self._original_request.cookies
@property
def query_parameters(self):
return self._original_request.query_parameters
@property
def path_parameters(self):
return self._original_request.path_parameters
@property
def byte_body(self):
return self._original_request.byte_body
@property
def utf8_body(self):
return self._original_request.utf8_body
def __str__(self):
return "[{i}]<{m} {p}>".format(i=self._request_id, m=self.http_method, p=self.request_uri)
def hello_world(request):
return ResponseBuilder() \
.set_status(HTTPStatus.OK) \
.set_utf8_body("Hello World")\
.build()
def add_id_to_request(request):
return IDEnhancedRequest(request, uuid.uuid4())
def log_request(request):
LOG.info("Got Request: {r}".format(r=request))
return request
def log_response(request, response):
LOG.info("Built Response: {s} for Request: {r}".format(s=response, r=request))
return response
def build_application():
routes = \
RoutesBuilder() \
.add_request_interceptor("/", add_id_to_request) \
.add_request_interceptor("/", log_request) \
.add_handler("GET", "/hello", hello_world) \
.add_response_interceptor("/", log_response)\
.build()
return EynnydWebappBuilder() \
.set_routes(routes) \
.build()
application = build_application()
New Imports¶
For our new work we need two new imports.
from eynnyd import AbstractRequest
...
import uuid
The AbstractRequest represents all the functionality a request must contain (at minimum). These values are already provided to your code, loaded from the WSGI server. We are also using the uuid module here to generate us random IDs. Collisions this way are pretty uncommon and since these IDs are short lived (only for the duration of a request) we feel this method is pretty reasonable.
Building An Explicit Request Wrapper Class¶
We now build a class which implements our Eynnyd AbstractRequest
from above explicitly and it also provides
us with a request_id property
.
class IDEnhancedRequest(AbstractRequest):
def __init__(self, original_request, request_id):
self._request_id = request_id
self._original_request = original_request
@property
def request_id(self):
return self._request_id
@property
def http_method(self):
return self._original_request.http_method
@property
def request_uri(self):
return self._original_request.request_uri
@property
def forwarded_request_uri(self):
return self._original_request.forwarded_request_uri
@property
def headers(self):
return self._original_request.headers
@property
def client_ip_address(self):
return self._original_request.client_ip_address
@property
def cookies(self):
return self._original_request.cookies
@property
def query_parameters(self):
return self._original_request.query_parameters
@property
def path_parameters(self):
return self._original_request.path_parameters
@property
def byte_body(self):
return self._original_request.byte_body
@property
def utf8_body(self):
return self._original_request.utf8_body
def __str__(self):
return "[{i}]<{m} {p}>".format(i=self._request_id, m=self.http_method, p=self.request_uri)
There are 3 unique things to note about this class. The first is that every property except the request_id
property just returns the value from the original request object. The second note-worthy item is the
request_id
property itself, which just returns any id set in the constructor. And finally, it is also
worth noting we have updated our __str__
method, which means that any logging of this request will now
start with a prefixed request id value.
Updating the Request¶
Now we just need a request interceptor to update our incoming request with new values.
def add_id_to_request(request):
return IDEnhancedRequest(request, uuid.uuid4())
Because IDEnhancedRequest
extends AbstractRequest
this code is legal (wont fail Eynnyd’s request
interceptor validation). All we are doing is returning the wrapped request with a newly added, random id.
Adding the Interceptor¶
Finally, we just need to add this interceptor to our routes at the root level and make sure it runs before all other interceptors.
def build_application():
routes = \
RoutesBuilder() \
.add_request_interceptor("/", add_id_to_request) \
.add_request_interceptor("/", log_request) \
.add_handler("GET", "/hello", hello_world) \
.add_response_interceptor("/", log_response)\
.build()
Note we added this interceptor before the other root interceptors to insure it runs first. With this change both
the log_request
request interceptor and the log_response
response interceptor will log out the
request including our new id value.