Commit 89864450 authored by Jacek Furmankiewicz's avatar Jacek Furmankiewicz

major change to enable custom routing to REST services from a single parent Resource

parent 39d43ed7
......@@ -2,6 +2,25 @@
Common classes
'''
from zope.interface import Interface, Attribute
#########################################################
#
# INTERFACES
#
#########################################################
class IRestServiceContainer(Interface):
"""An interface for all REST services that can be added within a root CorePost resource"""
services = Attribute("All the REST services contained in this resource")
#########################################################
#
# CLASSES
#
#########################################################
class Response:
"""
Custom response object, can be returned instead of raw string response
......
......@@ -5,13 +5,13 @@ Created on 2011-10-03
Common routing classes, regardless of whether used in HTTP or multiprocess context
'''
from collections import defaultdict
from corepost import Response
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
from twisted.internet import reactor, defer
from twisted.web.http import parse_qs
from twisted.web.resource import Resource
......@@ -19,6 +19,7 @@ from twisted.web.server import Site, NOT_DONE_YET
import re, copy, exceptions, json, yaml
from xml.etree import ElementTree
from xml.etree.ElementTree import Element
from zope.interface.verify import verifyObject
class UrlRouter:
''' Common class for containing info related to routing a request to a function '''
......@@ -28,20 +29,21 @@ class UrlRouter:
__typeConverters = {"int":int,"float":float}
def __init__(self,f,url,methods,accepts,produces,cache):
self.__f = f
self.__url = url
self.__methods = methods if isinstance(methods,tuple) else (methods,)
self.__accepts = accepts if isinstance(accepts,tuple) else (accepts,)
self.__produces = produces
self.__cache = cache
self.__f = f
self.__argConverters = {} # dict of arg names -> group index
self.__validators = {}
self.__mandatory = getMandatoryArgumentNames(f)[2:]
def compileMatcherForFullUrl(self):
"""Compiles the regex matches once the URL has been updated to include the full path from the parent class"""
#parse URL into regex used for matching
m = UrlRouter.__urlMatcher.findall(url)
self.__matchUrl = "^%s$" % url
m = UrlRouter.__urlMatcher.findall(self.url)
self.__matchUrl = "^%s$" % self.url
for match in m:
if len(match[0]) == 0:
# string
......@@ -56,6 +58,7 @@ class UrlRouter:
self.__matcher = re.compile(self.__matchUrl)
@property
def cache(self):
'''Indicates if this URL should be cached or not'''
......@@ -101,19 +104,31 @@ class UrlRouter:
if arg not in kwargs:
raise TypeError("Missing mandatory argument '%s'" % arg)
return self.__f(instance,request,**kwargs)
def __str__(self):
return "%s %s" % (self.url, self.methods)
class UrlRouterInstance():
"""Combines a UrlRouter with a class instance it should be executed against"""
def __init__(self,clazz,urlRouter):
self.clazz = clazz
self.urlRouter = urlRouter
def __str__(self):
return self.urlRouter.url
class CachedUrl:
'''
Used for caching URLs that have been already routed once before. Avoids the overhead
of regex processing on every incoming call for commonly accessed REST URLs
'''
def __init__(self,router,args):
self.__router = router
def __init__(self,urlRouterInstance,args):
self.__urlRouterInstance = urlRouterInstance
self.__args = args
@property
def router(self):
return self.__router
def urlRouterInstance(self):
return self.__urlRouterInstance
@property
def args(self):
......@@ -124,16 +139,16 @@ class RequestRouter:
Class that handles request->method routing functionality to any type of resource
'''
def __init__(self,urlContainer,schema=None,filters=()):
def __init__(self,restServiceContainer,schema=None,filters=()):
'''
Constructor
'''
self.__urls = {Http.GET: defaultdict(dict),Http.POST: defaultdict(dict),Http.PUT: defaultdict(dict),Http.DELETE: defaultdict(dict)}
self.__cachedUrls = {Http.GET: defaultdict(dict),Http.POST: defaultdict(dict),Http.PUT: defaultdict(dict),Http.DELETE: defaultdict(dict)}
self.__routers = {}
self.__urlRouterInstances = {}
self.__schema = schema
self.__registerRouters(urlContainer)
self.__urlContainer = urlContainer
self.__registerRouters(restServiceContainer)
self.__urlContainer = restServiceContainer
self.__requestFilters = []
self.__responseFilters = []
......@@ -155,61 +170,80 @@ class RequestRouter:
def path(self):
return self.__path
def __registerRouters(self,urlContainer):
def __registerRouters(self,restServiceContainer):
"""Main method responsible for registering routers"""
from types import FunctionType
for _,func in urlContainer.__class__.__dict__.iteritems():
if type(func) == FunctionType and hasattr(func,'corepostRequestRouter'):
rq = func.corepostRequestRouter
for method in rq.methods:
for accepts in rq.accepts:
self.__urls[method][rq.url][accepts] = rq
self.__routers[func] = rq # needed so that we cdan lookup the router for a specific function
def route(self,url,methods=(Http.GET,),accepts=MediaType.WILDCARD,produces=None,cache=True):
'''Obsolete'''
raise RuntimeError("Do not @app.route() any more, as of 0.0.6 API has been re-designed around class methods, see docs and examples")
for service in restServiceContainer.services:
# check if the service has a root path defined, which is optional
rootPath = service.__class__.path if "path" in service.__class__.__dict__ else ""
for key in service.__class__.__dict__:
func = service.__class__.__dict__[key]
# handle REST resources directly on the CorePost resource
if type(func) == FunctionType and hasattr(func,'corepostRequestRouter'):
# if specified, add class path to each function's path
rq = func.corepostRequestRouter
rq.url = "%s%s" % (rootPath,rq.url)
# remove trailing '/' to standardize URLs
if rq.url != "/" and rq.url[-1] == "/":
rq.url = rq.url[:-1]
# now that the full URL is set, compile the matcher for it
rq.compileMatcherForFullUrl()
for method in rq.methods:
for accepts in rq.accepts:
urlRouterInstance = UrlRouterInstance(service,rq)
self.__urls[method][rq.url][accepts] = urlRouterInstance
self.__urlRouterInstances[func] = urlRouterInstance # needed so that we can lookup the urlRouterInstance for a specific function
def getResponse(self,request):
"""Finds the appropriate router and dispatches the request to the registered function. Returns the appropriate Response object"""
"""Finds the appropriate instance 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)
# standardize URL and remove trailing "/" if necessary
standardized_postpath = request.postpath if (request.postpath[-1] != '' or request.postpath == ['']) else request.postpath[:-1]
path = '/' + '/'.join(standardized_postpath)
path = '/' + '/'.join(request.postpath)
contentType = MediaType.WILDCARD if HttpHeader.CONTENT_TYPE not in request.received_headers else request.received_headers[HttpHeader.CONTENT_TYPE]
urlrouter, pathargs = None, None
urlRouterInstance, pathargs = None, None
# fetch URL arguments <-> function from cache if hit at least once before
if contentType in self.__cachedUrls[request.method][path]:
cachedUrl = self.__cachedUrls[request.method][path][contentType]
urlrouter,pathargs = cachedUrl.router, cachedUrl.args
urlRouterInstance,pathargs = cachedUrl.urlRouterInstance, cachedUrl.args
else:
# first time this URL is called
router = None
instance = None
for contentTypeFunctions in self.__urls[request.method].values():
# go through all the URLs, pick up the ones matching by content type
# and then validate which ones match by path/argument to a particular UrlRouterInstance
for contentTypeInstances in self.__urls[request.method].values():
if contentType in contentTypeFunctions:
if contentType in contentTypeInstances:
# there is an exact function for this incoming content type
router = contentTypeFunctions[contentType]
elif MediaType.WILDCARD in contentTypeFunctions:
instance = contentTypeInstances[contentType]
elif MediaType.WILDCARD in contentTypeInstances:
# fall back to any wildcard method
router = contentTypeFunctions[MediaType.WILDCARD]
instance = contentTypeInstances[MediaType.WILDCARD]
if router != None:
if instance != None:
# see if the path arguments match up against any function @route definition
args = router.getArguments(path)
args = instance.urlRouter.getArguments(path)
if args != None:
if router.cache:
self.__cachedUrls[request.method][path][contentType] = CachedUrl(router, args)
urlrouter,pathargs = router,args
if instance.urlRouter.cache:
self.__cachedUrls[request.method][path][contentType] = CachedUrl(instance, args)
urlRouterInstance,pathargs = instance,args
break
#actual call
if urlrouter != None and pathargs != None:
if urlRouterInstance != None and pathargs != None:
allargs = copy.deepcopy(pathargs)
# handler for weird Twisted logic where PUT does not get form params
# see: http://twistedmatrix.com/pipermail/twisted-web/2007-March/003338.html
......@@ -227,7 +261,8 @@ class RequestRouter:
try:
# if POST/PUT, check if we need to automatically parse JSON
self.__parseRequestData(request)
val = urlrouter.call(self.__urlContainer,request,**allargs)
urlRouter = urlRouterInstance.urlRouter
val = urlRouter.call(urlRouterInstance.clazz,request,**allargs)
#handle Deferreds natively
if isinstance(val,defer.Deferred):
......
......@@ -3,7 +3,7 @@ Argument extraction tests
@author: jacekf
'''
from corepost.web import CorePost, validate, route
from corepost.web import RestServiceContainer, validate, route
from corepost.enums import Http
from formencode import Schema, validators
......@@ -11,7 +11,7 @@ class TestSchema(Schema):
allow_extra_fields = True
childId = validators.Regex(regex="^jacekf|test$")
class ArgumentApp(CorePost):
class ArgumentApp():
@route("/int/<int:intarg>/float/<float:floatarg>/string/<stringarg>",Http.GET)
def test(self,request,intarg,floatarg,stringarg,**kwargs):
......@@ -29,5 +29,5 @@ class ArgumentApp(CorePost):
return "%s - %s - %s" % (rootId,childId,kwargs)
def run_app_arguments():
app = ArgumentApp()
app = RestServiceContainer((ArgumentApp(),))
app.run(8082)
\ No newline at end of file
......@@ -3,21 +3,21 @@ Server tests
@author: jacekf
'''
from corepost.web import CorePost, route
from corepost.web import RestServiceContainer, route
from corepost.enums import Http
from corepost.filters import IRequestFilter, IResponseFilter
import zope.interface
from zope.interface import implements
class AddCustomHeaderFilter():
"""Implements just a request filter"""
zope.interface.implements(IRequestFilter)
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)
implements(IResponseFilter)
def filterResponse(self,request,response):
if response.code == 404:
......@@ -25,7 +25,7 @@ class Change404to503Filter():
class WrapAroundFilter():
"""Implements both types of filters in one class"""
zope.interface.implements(IRequestFilter,IResponseFilter)
implements(IRequestFilter,IResponseFilter)
def filterRequest(self,request):
request.received_headers["X-Wrap-Input"] = "Input"
......@@ -33,14 +33,15 @@ class WrapAroundFilter():
def filterResponse(self,request,response):
response.headers["X-Wrap-Output"] = "Output"
class FilterApp(CorePost):
class FilterService():
path = "/"
@route("/",Http.GET)
def root(self,request,**kwargs):
return request.received_headers
def run_filter_app():
app = FilterApp(filters=(Change404to503Filter(),AddCustomHeaderFilter(),WrapAroundFilter(),))
app = RestServiceContainer(services=(FilterService(),),filters=(Change404to503Filter(),AddCustomHeaderFilter(),WrapAroundFilter(),))
app.run(8083)
if __name__ == "__main__":
......
......@@ -3,17 +3,15 @@ Server tests
@author: jacekf
'''
from corepost.web import CorePost, route
from corepost.web import RestServiceContainer, route
from corepost.enums import Http, MediaType, HttpHeader
from twisted.internet import defer
from xml.etree import ElementTree
from UserDict import UserDict
import json, yaml
class HomeApp(CorePost):
class HomeApp():
def __init__(self,*args,**kwargs):
CorePost.__init__(self, *args, **kwargs)
self.issue1 = "issue 1"
@route("/",Http.GET)
......@@ -117,7 +115,7 @@ class HomeApp(CorePost):
return self.issue1
def run_app_home():
app = HomeApp()
app = RestServiceContainer((HomeApp(),))
app.run()
if __name__ == "__main__":
......
'''
A CorePost module1 that can be merged into the main CorePost Resource
A RestServiceContainer module1 that can be merged into the main RestServiceContainer Resource
'''
from corepost.web import CorePost, route
from corepost.web import RestServiceContainer, route
from corepost.enums import Http
from twisted.web.resource import Resource
from twisted.internet import reactor
from twisted.web.server import Site
class HomeApp(CorePost):
class HomeApp():
@route("/")
def home_root(self,request,**kwargs):
return "HOME %s" % kwargs
class Module1(CorePost):
class Module1():
path = "/module1"
@route("/",Http.GET)
def module1_get(self,request,**kwargs):
......@@ -24,7 +22,8 @@ class Module1(CorePost):
def module1e_sub(self,request,**kwargs):
return request.path
class Module2(CorePost):
class Module2():
path = "/module2"
@route("/",Http.GET)
def module2_get(self,request,**kwargs):
......@@ -35,12 +34,8 @@ class Module2(CorePost):
return request.path
def run_app_multi():
app = Resource()
app.putChild('', HomeApp())
app.putChild('module1',Module1())
app.putChild('module2',Module2())
factory = Site(app)
reactor.listenTCP(8081, factory) #@UndefinedVariable
reactor.run() #@UndefinedVariable
app = RestServiceContainer((HomeApp(),Module1(),Module2()))
app.run(8081)
if __name__ == "__main__":
run_app_multi()
\ No newline at end of file
'''
Server tests
@author: jacekf
'''
from corepost import Response
from corepost.web import RestServiceContainer
class DB():
"""Fake in-memory DB for testing"""
customers = {}
class Customer():
"""Represents customer entity"""
def __init__(self,customerId,firstName,lastName):
(self.customerId,self.firstName,self.lastName) = (customerId,firstName,lastName)
self.addresses = {}
class CustomerAddress():
"""Represents customer address entity"""
def __init__(self,customer,streetNumber,streetName,stateCode,countryCode):
(self.customer,self.streetNumber,self.streetName.self.stateCode,self.countryCode) = (customer,streetNumber,streetName,stateCode,countryCode)
class CustomerRestService():
path = "/customer"
def getAll(self,request):
return DB.customers
def get(self,request,customerId):
return DB.customers[customerId] if customerId in DB.customers else Response(404, "Customer %s not found" % customerId)
def post(self,request,customerId,firstName,lastName):
if customerId in DB.customers:
return Response(409,"Customer %s already exists" % customerId)
else:
DB.customers[customerId] = Customer(customerId, firstName, lastName)
return Response(201)
def put(self,request,customerId,firstName,lastName):
if customerId in DB.customers:
DB.customers[customerId].firstName = firstName
DB.customers[customerId].lastName = lastName
return Response(200)
else:
return Response(404, "Customer %s not found" % customerId)
def delete(self,request,customerId):
if customerId in DB.customers:
del(DB.customers[customerId])
return Response(200)
else:
return Response(404, "Customer %s not found" % customerId)
def deleteAll(self,request):
DB.customers.clear()
return Response(200)
class CustomerAddressRestService():
path = "/customer/<customerId>/address"
def getAll(self,request,customerId):
return DB.customers
def get(self,request,customerId):
return DB.customers[customerId] if customerId in DB.customers else Response(404, "Customer %s not found" % customerId)
def post(self,request,customerId,firstName,lastName):
if customerId in DB.customers:
return Response(409,"Customer %s already exists" % customerId)
else:
DB.customers[customerId] = Customer(customerId, firstName, lastName)
return Response(201)
def put(self,request,customerId,firstName,lastName):
if customerId in DB.customers:
DB.customers[customerId].firstName = firstName
DB.customers[customerId].lastName = lastName
return Response(200)
else:
return Response(404, "Customer %s not found" % customerId)
def delete(self,request,customerId):
if customerId in DB.customers:
del(DB.customers[customerId])
return Response(200)
else:
return Response(404, "Customer %s not found" % customerId)
def deleteAll(self,request):
DB.customers.clear()
return Response(200)
def run_rest_app():
app = RestServiceContainer(restServices=(CustomerRestService(),))
app.run(8085)
if __name__ == "__main__":
run_rest_app()
\ No newline at end of file
......@@ -22,3 +22,11 @@ def convertToJson(obj):
return json.dumps(obj)
except Exception as ex:
raise RuntimeError(str(ex))
def checkExpectedInterfaces(objects,expectedInterface):
"""Verifies that all the objects implement the expected interface"""
for obj in objects:
if not expectedInterface.providedBy(obj):
raise RuntimeError("Object %s does not implement %s interface" % (obj,expectedInterface))
\ No newline at end of file
......@@ -3,32 +3,35 @@ Main server classes
@author: jacekf
'''
from collections import defaultdict
from corepost import Response
from corepost.enums import Http, HttpHeader
from corepost.utils import getMandatoryArgumentNames, convertToJson
from corepost.routing import UrlRouter, CachedUrl, RequestRouter
from corepost import Response, IRestServiceContainer
from corepost.enums import Http
from corepost.routing import UrlRouter, RequestRouter
from enums import MediaType
from formencode import FancyValidator, Invalid
from twisted.internet import reactor, defer
from twisted.web.http import parse_qs
from twisted.internet import reactor
from twisted.internet.defer import Deferred
from twisted.web.resource import Resource
from twisted.web.server import Site, NOT_DONE_YET
import re, copy, exceptions, json, yaml
from xml.etree import ElementTree
from xml.etree.ElementTree import Element
from twisted.internet.defer import Deferred
from zope.interface import implements
#########################################################
#
# CLASSES
#
#########################################################
class CorePost(Resource):
class RestServiceContainer(Resource):
'''
Main resource responsible for routing REST requests to the implementing methods
'''
isLeaf = True
implements(IRestServiceContainer)
def __init__(self,schema=None,filters=()):
def __init__(self,services=(),schema=None,filters=()):
'''
Constructor
'''
self.services = services
self.__router = RequestRouter(self,schema,filters)
Resource.__init__(self)
......@@ -82,7 +85,6 @@ class CorePost(Resource):
factory = Site(self)
reactor.listenTCP(port, factory) #@UndefinedVariable
reactor.run() #@UndefinedVariable
##################################################################################################
#
......
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