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 ...@@ -8,25 +8,26 @@ for JSON/XML/YAML output
import inspect, collections import inspect, collections
from jinja2 import Template 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>""") 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>""") 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""" """Converts anything (clas,tuples,list) to the safe serializable equivalent"""
if isinstance(object, dict): if isinstance(obj, dict) or isinstance(obj,DictMixin):
return traverseDict(object) return traverseDict(obj)
elif isClassInstance(object): elif isClassInstance(obj):
return convertClassToDict(object) return convertClassToDict(obj)
elif isinstance(object,collections.Iterable): elif isinstance(obj,collections.Iterable):
# iterable # iterable
values = [] values = []
for val in object: for val in obj:
values.append(convertForSerialization(val)) values.append(convertForSerialization(val))
return values return values
else: else:
# return as-is # return as-is
return object return obj
def convertClassToDict(clazz): def convertClassToDict(clazz):
"""Converts a class to a dictionary""" """Converts a class to a dictionary"""
...@@ -46,7 +47,6 @@ def traverseDict(dictObject): ...@@ -46,7 +47,6 @@ def traverseDict(dictObject):
if inspect.isclass(val): if inspect.isclass(val):
# call itself recursively # call itself recursively
val = convertClassToDict(val) val = convertClassToDict(val)
newDict[prop] = val newDict[prop] = val
return newDict 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 @@ ...@@ -2,12 +2,13 @@
Created on 2011-10-03 Created on 2011-10-03
@author: jacekf @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 collections import defaultdict
from corepost.enums import Http, HttpHeader from corepost.enums import Http, HttpHeader
from corepost.utils import getMandatoryArgumentNames, convertToJson from corepost.utils import getMandatoryArgumentNames, convertToJson
from corepost.convert import convertForSerialization, generateXml from corepost.convert import convertForSerialization, generateXml
from corepost.filters import IRequestFilter, IResponseFilter
from corepost import Response from corepost import Response
from enums import MediaType from enums import MediaType
from formencode import FancyValidator, Invalid from formencode import FancyValidator, Invalid
...@@ -123,7 +124,7 @@ class RequestRouter: ...@@ -123,7 +124,7 @@ class RequestRouter:
Class that handles request->method routing functionality to any type of resource 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 Constructor
''' '''
...@@ -133,6 +134,22 @@ class RequestRouter: ...@@ -133,6 +134,22 @@ class RequestRouter:
self.__schema = schema self.__schema = schema
self.__registerRouters(urlContainer) self.__registerRouters(urlContainer)
self.__urlContainer = 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 @property
def path(self): def path(self):
...@@ -156,7 +173,11 @@ class RequestRouter: ...@@ -156,7 +173,11 @@ class RequestRouter:
def getResponse(self,request): def getResponse(self,request):
"""Finds the appropriate router and dispatches the request to the registered function. Returns the appropriate Response object""" """Finds the appropriate router and dispatches the request to the registered function. Returns the appropriate Response object"""
# see if already cached # see if already cached
response = None
try: try:
if len(self.__requestFilters) > 0:
self.__filterRequests(request)
path = '/' + '/'.join(request.postpath) path = '/' + '/'.join(request.postpath)
contentType = MediaType.WILDCARD if HttpHeader.CONTENT_TYPE not in request.received_headers else request.received_headers[HttpHeader.CONTENT_TYPE] 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: ...@@ -222,18 +243,24 @@ class RequestRouter:
else: else:
request.setResponseCode(201) request.setResponseCode(201)
return self.__generateResponse(request, val, request.code) response = self.__generateResponse(request, val, request.code)
except exceptions.TypeError as ex: except exceptions.TypeError as ex:
return self.__createErrorResponse(request,400,"%s" % ex) response = self.__createErrorResponse(request,400,"%s" % ex)
except Exception as 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: 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: 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): def __generateResponse(self,request,response,code=200):
""" """
...@@ -290,19 +317,29 @@ class RequestRouter: ...@@ -290,19 +317,29 @@ class RequestRouter:
def __parseRequestData(self,request): def __parseRequestData(self,request):
'''Automatically parses JSON,XML,YAML if present''' '''Automatically parses JSON,XML,YAML if present'''
if request.method in (Http.POST,Http.PUT) and HttpHeader.CONTENT_TYPE in request.received_headers.keys(): if request.method in (Http.POST,Http.PUT) and HttpHeader.CONTENT_TYPE in request.received_headers.keys():
type = request.received_headers["content-type"] contentType = request.received_headers["content-type"]
if type == MediaType.APPLICATION_JSON: if contentType == MediaType.APPLICATION_JSON:
try: try:
request.json = json.loads(request.content.read()) request.json = json.loads(request.content.read())
except Exception as ex: except Exception as ex:
raise TypeError("Unable to parse JSON body: %s" % 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: try:
request.xml = ElementTree.XML(request.content.read()) request.xml = ElementTree.XML(request.content.read())
except Exception as ex: except Exception as ex:
raise TypeError("Unable to parse XML body: %s" % ex) raise TypeError("Unable to parse XML body: %s" % ex)
elif type == MediaType.TEXT_YAML: elif contentType == MediaType.TEXT_YAML:
try: try:
request.yaml = yaml.safe_load(request.content.read()) request.yaml = yaml.safe_load(request.content.read())
except Exception as ex: except Exception as ex:
raise TypeError("Unable to parse YAML body: %s" % 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 ...@@ -10,8 +10,9 @@ from urllib import urlencode
from corepost.test.home_resource import run_app_home from corepost.test.home_resource import run_app_home
from corepost.test.multi_resource import run_app_multi from corepost.test.multi_resource import run_app_multi
from corepost.test.arguments import run_app_arguments 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' NULL = 'None'
......
...@@ -25,11 +25,11 @@ class CorePost(Resource): ...@@ -25,11 +25,11 @@ class CorePost(Resource):
''' '''
isLeaf = True isLeaf = True
def __init__(self,schema=None): def __init__(self,schema=None,filters=()):
''' '''
Constructor Constructor
''' '''
self.__router = RequestRouter(self,schema) self.__router = RequestRouter(self,schema,filters)
Resource.__init__(self) Resource.__init__(self)
def render_GET(self,request): def render_GET(self,request):
......
...@@ -63,9 +63,9 @@ from setuptools import setup ...@@ -63,9 +63,9 @@ from setuptools import setup
setup( setup(
name="CorePost", name="CorePost",
version="0.0.10", version="0.0.11",
author="Jacek Furmankiewicz", author="Jacek Furmankiewicz",
author_email="jacekeadE99@gmail.com", author_email="jacek99@gmail.com",
description=("A Twisted Web REST micro-framework"), description=("A Twisted Web REST micro-framework"),
license="BSD", license="BSD",
keywords="twisted rest flask sinatra get post put delete web", 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