Commit bebba13a authored by Jacek Furmankiewicz's avatar Jacek Furmankiewicz

support for request/response filters

parent 1ca2f5b8
......@@ -8,25 +8,26 @@ for JSON/XML/YAML output
import inspect, collections
from jinja2 import Template
from UserDict import DictMixin
xmlListTemplate = Template("""<list>{% for item in items %}<item>{% for prop,val in item.iteritems() %}<{{prop}}>{{val}}</{{prop}}>{% endfor %}</item>{% endfor %}</list>""")
xmlTemplate = Template("""<item>{% for prop,val in item.iteritems() %}<{{prop}}>{{val}}</{{prop}}>{% endfor %}</item>""")
def convertForSerialization(object):
def convertForSerialization(obj):
"""Converts anything (clas,tuples,list) to the safe serializable equivalent"""
if isinstance(object, dict):
return traverseDict(object)
elif isClassInstance(object):
return convertClassToDict(object)
elif isinstance(object,collections.Iterable):
if isinstance(obj, dict) or isinstance(obj,DictMixin):
return traverseDict(obj)
elif isClassInstance(obj):
return convertClassToDict(obj)
elif isinstance(obj,collections.Iterable):
# iterable
values = []
for val in object:
for val in obj:
values.append(convertForSerialization(val))
return values
else:
# return as-is
return object
return obj
def convertClassToDict(clazz):
"""Converts a class to a dictionary"""
......@@ -46,7 +47,6 @@ def traverseDict(dictObject):
if inspect.isclass(val):
# call itself recursively
val = convertClassToDict(val)
newDict[prop] = val
return newDict
......
'''
Various filters & interceptors
@author: jacekf
'''
from zope.interface import Interface
class IRequestFilter(Interface):
"""Request filter interface"""
def filterRequest(self,request):
"""Allows to intercept and change an incoming request"""
pass
class IResponseFilter(Interface):
"""Response filter interface"""
def filterResponse(self,request,response):
"""Allows to intercept and change an outgoing response"""
pass
......@@ -2,12 +2,13 @@
Created on 2011-10-03
@author: jacekf
Common routing classes, regardless of whether used in HTTP or ZeroMQ context
Common routing classes, regardless of whether used in HTTP or multiprocess context
'''
from collections import defaultdict
from corepost.enums import Http, HttpHeader
from corepost.utils import getMandatoryArgumentNames, convertToJson
from corepost.convert import convertForSerialization, generateXml
from corepost.filters import IRequestFilter, IResponseFilter
from corepost import Response
from enums import MediaType
from formencode import FancyValidator, Invalid
......@@ -123,7 +124,7 @@ class RequestRouter:
Class that handles request->method routing functionality to any type of resource
'''
def __init__(self,urlContainer,schema=None):
def __init__(self,urlContainer,schema=None,filters=()):
'''
Constructor
'''
......@@ -133,6 +134,22 @@ class RequestRouter:
self.__schema = schema
self.__registerRouters(urlContainer)
self.__urlContainer = urlContainer
self.__requestFilters = []
self.__responseFilters = []
#filters
if filters != None:
for webFilter in filters:
valid = False
if IRequestFilter.providedBy(webFilter):
self.__requestFilters.append(webFilter)
valid = True
if IResponseFilter.providedBy(webFilter):
self.__responseFilters.append(webFilter)
valid = True
if not valid:
raise RuntimeError("filter %s must implement IRequestFilter or IResponseFilter" % webFilter.__class__.__name__)
@property
def path(self):
......@@ -156,7 +173,11 @@ class RequestRouter:
def getResponse(self,request):
"""Finds the appropriate router and dispatches the request to the registered function. Returns the appropriate Response object"""
# see if already cached
response = None
try:
if len(self.__requestFilters) > 0:
self.__filterRequests(request)
path = '/' + '/'.join(request.postpath)
contentType = MediaType.WILDCARD if HttpHeader.CONTENT_TYPE not in request.received_headers else request.received_headers[HttpHeader.CONTENT_TYPE]
......@@ -222,18 +243,24 @@ class RequestRouter:
else:
request.setResponseCode(201)
return self.__generateResponse(request, val, request.code)
response = self.__generateResponse(request, val, request.code)
except exceptions.TypeError as ex:
return self.__createErrorResponse(request,400,"%s" % ex)
response = self.__createErrorResponse(request,400,"%s" % ex)
except Exception as ex:
return self.__createErrorResponse(request,500,"Unexpected server error: %s\n%s" % (type(ex),ex))
response = self.__createErrorResponse(request,500,"Unexpected server error: %s\n%s" % (type(ex),ex))
else:
return self.__createErrorResponse(request,404,"URL '%s' not found\n" % request.path)
response = self.__createErrorResponse(request,404,"URL '%s' not found\n" % request.path)
except Exception as ex:
return self.__createErrorResponse(request,500,"Internal server error: %s" % ex)
response = self.__createErrorResponse(request,500,"Internal server error: %s" % ex)
# response handling
if response != None and len(self.__responseFilters) > 0:
self.__filterResponses(request,response)
return response
def __generateResponse(self,request,response,code=200):
"""
......@@ -290,19 +317,29 @@ class RequestRouter:
def __parseRequestData(self,request):
'''Automatically parses JSON,XML,YAML if present'''
if request.method in (Http.POST,Http.PUT) and HttpHeader.CONTENT_TYPE in request.received_headers.keys():
type = request.received_headers["content-type"]
if type == MediaType.APPLICATION_JSON:
contentType = request.received_headers["content-type"]
if contentType == MediaType.APPLICATION_JSON:
try:
request.json = json.loads(request.content.read())
except Exception as ex:
raise TypeError("Unable to parse JSON body: %s" % ex)
elif type in (MediaType.APPLICATION_XML,MediaType.TEXT_XML):
elif contentType in (MediaType.APPLICATION_XML,MediaType.TEXT_XML):
try:
request.xml = ElementTree.XML(request.content.read())
except Exception as ex:
raise TypeError("Unable to parse XML body: %s" % ex)
elif type == MediaType.TEXT_YAML:
elif contentType == MediaType.TEXT_YAML:
try:
request.yaml = yaml.safe_load(request.content.read())
except Exception as ex:
raise TypeError("Unable to parse YAML body: %s" % ex)
def __filterRequests(self,request):
"""Filters incoming requests"""
for webFilter in self.__requestFilters:
webFilter.filterRequest(request)
def __filterResponses(self,request,response):
"""Filters incoming requests"""
for webFilter in self.__responseFilters:
webFilter.filterResponse(request,response)
\ No newline at end of file
Using step definitions from: '../steps'
@filters
Feature: Filters
CorePost should be able to
filter incoming requests and outgoing responses
Background:
Given 'filter_resource' is running
Scenario: Filter turns 404 into 503
When as user 'None:None' I GET 'http://127.0.0.1:8083/wrongurl'
Then I expect HTTP code 503
Scenario: Request filter adds a header + wrap around requests
When I prepare HTTP header 'Accept' = 'application/json'
When as user 'None:None' I GET 'http://127.0.0.1:8083/'
Then I expect HTTP code 200
# 'custom-header' should be added
# 'x-wrap-input' should be added from wrap request filter
And I expect JSON content
"""
{
"accept": "application/json",
"accept-encoding": "gzip, deflate",
"custom-header": "Custom Header Value",
"host": "127.0.0.1:8083",
"user-agent": "Python-httplib2/0.7.2 (gzip)",
"x-wrap-input": "Input"
}
"""
# 'x-wrap-header' should be added from wrap response filter
And I expect 'x-wrap-output' header matches 'Output'
\ No newline at end of file
'''
Server tests
@author: jacekf
'''
from corepost.web import CorePost, route
from corepost.enums import Http
from corepost.filters import IRequestFilter, IResponseFilter
import zope.interface
class AddCustomHeaderFilter():
"""Implements just a request filter"""
zope.interface.implements(IRequestFilter)
def filterRequest(self,request):
request.received_headers["Custom-Header"] = "Custom Header Value"
class Change404to503Filter():
"""Implements just a response filter that changes 404 to 503 statuses"""
zope.interface.implements(IResponseFilter)
def filterResponse(self,request,response):
if response.code == 404:
response.code = 503
class WrapAroundFilter():
"""Implements both types of filters in one class"""
zope.interface.implements(IRequestFilter,IResponseFilter)
def filterRequest(self,request):
request.received_headers["X-Wrap-Input"] = "Input"
def filterResponse(self,request,response):
response.headers["X-Wrap-Output"] = "Output"
class FilterApp(CorePost):
@route("/",Http.GET)
def root(self,request,**kwargs):
return request.received_headers
def run_filter_app():
app = FilterApp(filters=(Change404to503Filter(),AddCustomHeaderFilter(),WrapAroundFilter(),))
app.run(8083)
if __name__ == "__main__":
run_filter_app()
\ No newline at end of file
......@@ -10,8 +10,9 @@ from urllib import urlencode
from corepost.test.home_resource import run_app_home
from corepost.test.multi_resource import run_app_multi
from corepost.test.arguments import run_app_arguments
from corepost.test.filter_resource import run_filter_app
apps = {'home_resource' : run_app_home,'multi_resource':run_app_multi,'arguments':run_app_arguments}
apps = {'home_resource' : run_app_home,'multi_resource':run_app_multi,'arguments':run_app_arguments, 'filter_resource':run_filter_app}
NULL = 'None'
......
......@@ -25,11 +25,11 @@ class CorePost(Resource):
'''
isLeaf = True
def __init__(self,schema=None):
def __init__(self,schema=None,filters=()):
'''
Constructor
'''
self.__router = RequestRouter(self,schema)
self.__router = RequestRouter(self,schema,filters)
Resource.__init__(self)
def render_GET(self,request):
......
......@@ -63,9 +63,9 @@ from setuptools import setup
setup(
name="CorePost",
version="0.0.10",
version="0.0.11",
author="Jacek Furmankiewicz",
author_email="jacekeadE99@gmail.com",
author_email="jacek99@gmail.com",
description=("A Twisted Web REST micro-framework"),
license="BSD",
keywords="twisted rest flask sinatra get post put delete web",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment