Commit 4ecf20e7 authored by Jacek Furmankiewicz's avatar Jacek Furmankiewicz

Merge branch 'master' of https://github.com/kaosat-dev/corepost

parents b2b41a31 4204008e
...@@ -7,27 +7,41 @@ for JSON/XML/YAML output ...@@ -7,27 +7,41 @@ for JSON/XML/YAML output
''' '''
import collections import collections
import logging
import json
from UserDict import DictMixin from UserDict import DictMixin
from twisted.python import log
advanced_json = False
try:
import jsonpickle
advanced_json = True
except ImportError: pass
primitives = (int, long, float, bool, str,unicode) primitives = (int, long, float, bool, str,unicode)
def convertForSerialization(obj): def convertForSerialization(obj):
"""Converts anything (clas,tuples,list) to the safe serializable equivalent""" """Converts anything (clas,tuples,list) to the safe serializable equivalent"""
if type(obj) in primitives: try:
if type(obj) in primitives:
# no conversion # no conversion
return obj return obj
elif isinstance(obj, dict) or isinstance(obj,DictMixin): elif isinstance(obj, dict) or isinstance(obj,DictMixin):
return traverseDict(obj) return traverseDict(obj)
elif isClassInstance(obj): elif isClassInstance(obj):
return convertClassToDict(obj) return convertClassToDict(obj)
elif isinstance(obj,collections.Iterable) and not isinstance(obj,str): elif isinstance(obj,collections.Iterable) and not isinstance(obj,str):
# iterable # iterable
values = [] values = []
for val in obj: 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 obj
except AttributeError as ex:
log.msg(ex,logLevel=logging.WARN)
return obj return obj
def convertClassToDict(clazz): def convertClassToDict(clazz):
...@@ -48,6 +62,19 @@ def traverseDict(dictObject): ...@@ -48,6 +62,19 @@ def traverseDict(dictObject):
newDict[prop] = convertForSerialization(val) newDict[prop] = convertForSerialization(val)
return newDict return newDict
def convertToJson(obj):
"""Converts to JSON, including Python classes that are not JSON serializable by default"""
if advanced_json:
try:
return jsonpickle.encode(obj, unpicklable=False)
except Exception as ex:
raise RuntimeError(str(ex))
try:
return json.dumps(obj)
except Exception as ex:
raise RuntimeError(str(ex))
def generateXml(obj): def generateXml(obj):
"""Generates basic XML from an object that has already been converted for serialization""" """Generates basic XML from an object that has already been converted for serialization"""
......
...@@ -4,20 +4,26 @@ Common enums ...@@ -4,20 +4,26 @@ Common enums
@author: jacekf @author: jacekf
''' '''
class Http: class Http:
"""Enumerates HTTP methods""" """Enumerates HTTP methods"""
GET = "GET" GET = "GET"
POST = "POST" POST = "POST"
PUT = "PUT" PUT = "PUT"
DELETE = "DELETE" DELETE = "DELETE"
OPTIONS = "OPTIONS"
HEAD = "HEAD"
PATCH = "PATCH"
class HttpHeader: class HttpHeader:
"""Enumerates common HTTP headers""" """Enumerates common HTTP headers"""
CONTENT_TYPE = "content-type" CONTENT_TYPE = "content-type"
ACCEPT = "accept" ACCEPT = "accept"
class MediaType: class MediaType:
"""Enumerates media types""" """Enumerates media types"""
WILDCARD = "*/*" WILDCARD = "*/*"
APPLICATION_XML = "application/xml" APPLICATION_XML = "application/xml"
APPLICATION_ATOM_XML = "application/atom+xml" APPLICATION_ATOM_XML = "application/atom+xml"
......
...@@ -7,23 +7,31 @@ Common routing classes, regardless of whether used in HTTP or multiprocess conte ...@@ -7,23 +7,31 @@ Common routing classes, regardless of whether used in HTTP or multiprocess conte
from collections import defaultdict from collections import defaultdict
from corepost import Response, RESTException from corepost import Response, RESTException
from corepost.enums import Http, HttpHeader from corepost.enums import Http, HttpHeader
from corepost.utils import getMandatoryArgumentNames, convertToJson, safeDictUpdate from corepost.utils import getMandatoryArgumentNames, safeDictUpdate
from corepost.convert import convertForSerialization, generateXml from corepost.convert import convertForSerialization, generateXml, convertToJson
from corepost.filters import IRequestFilter, IResponseFilter from corepost.filters import IRequestFilter, IResponseFilter
from enums import MediaType from enums import MediaType
from twisted.internet import defer from twisted.internet import defer
from twisted.web.http import parse_qs from twisted.web.http import parse_qs
from twisted.python import log from twisted.python import log
import re, copy, exceptions, json, yaml, logging import re, copy, exceptions, yaml,json, logging
from xml.etree import ElementTree from xml.etree import ElementTree
import uuid
advanced_json = False
try:
import jsonpickle
advanced_json = True
except ImportError: pass
class UrlRouter: class UrlRouter:
''' Common class for containing info related to routing a request to a function ''' ''' Common class for containing info related to routing a request to a function '''
__urlMatcher = re.compile(r"<(int|float|):?([^/]+)>") __urlMatcher = re.compile(r"<(int|float|uuid|):?([^/]+)>")
__urlRegexReplace = {"":r"(?P<arg>([^/]+))","int":r"(?P<arg>\d+)","float":r"(?P<arg>\d+.?\d*)"} __urlRegexReplace = {"":r"(?P<arg>([^/]+))","int":r"(?P<arg>\d+)","float":r"(?P<arg>\d+.?\d*)","uuid":r"(?P<arg>[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})"}
__typeConverters = {"int":int,"float":float} __typeConverters = {"int":int,"float":float,"uuid":uuid.UUID}
def __init__(self,f,url,methods,accepts,produces,cache): def __init__(self,f,url,methods,accepts,produces,cache):
self.__f = f self.__f = f
...@@ -140,16 +148,16 @@ class RequestRouter: ...@@ -140,16 +148,16 @@ class RequestRouter:
''' '''
Constructor Constructor
''' '''
self.__urls = {Http.GET: defaultdict(dict),Http.POST: defaultdict(dict),Http.PUT: defaultdict(dict),Http.DELETE: defaultdict(dict)} self.__urls = {Http.GET: defaultdict(dict),Http.POST: defaultdict(dict),Http.PUT: defaultdict(dict),Http.DELETE: defaultdict(dict),Http.OPTIONS: defaultdict(dict),Http.PATCH: defaultdict(dict),Http.HEAD: defaultdict(dict)}
self.__cachedUrls = {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),Http.OPTIONS: defaultdict(dict),Http.PATCH: defaultdict(dict),Http.HEAD: defaultdict(dict)}
self.__urlRouterInstances = {} self.__urlRouterInstances = {}
self.__schema = schema self.__schema = schema
self.__urlsMehods = {}
self.__registerRouters(restServiceContainer) self.__registerRouters(restServiceContainer)
self.__urlContainer = restServiceContainer self.__urlContainer = restServiceContainer
self.__requestFilters = [] self.__requestFilters = []
self.__responseFilters = [] self.__responseFilters = []
#filters
if filters != None: if filters != None:
for webFilter in filters: for webFilter in filters:
valid = False valid = False
...@@ -159,18 +167,18 @@ class RequestRouter: ...@@ -159,18 +167,18 @@ class RequestRouter:
if IResponseFilter.providedBy(webFilter): if IResponseFilter.providedBy(webFilter):
self.__responseFilters.append(webFilter) self.__responseFilters.append(webFilter)
valid = True valid = True
if not valid: if not valid:
raise RuntimeError("filter %s must implement IRequestFilter or IResponseFilter" % webFilter.__class__.__name__) raise RuntimeError("filter %s must implement IRequestFilter or IResponseFilter" % webFilter.__class__.__name__)
@property @property
def path(self): def path(self):
return self.__path return self.__path
def __registerRouters(self,restServiceContainer): def __registerRouters(self, restServiceContainer):
"""Main method responsible for registering routers""" """Main method responsible for registering routers"""
from types import FunctionType from types import FunctionType
for service in restServiceContainer.services: for service in restServiceContainer.services:
# check if the service has a root path defined, which is optional # check if the service has a root path defined, which is optional
rootPath = service.__class__.path if "path" in service.__class__.__dict__ else "" rootPath = service.__class__.path if "path" in service.__class__.__dict__ else ""
...@@ -179,24 +187,27 @@ class RequestRouter: ...@@ -179,24 +187,27 @@ class RequestRouter:
func = service.__class__.__dict__[key] func = service.__class__.__dict__[key]
# handle REST resources directly on the CorePost resource # handle REST resources directly on the CorePost resource
if type(func) == FunctionType and hasattr(func,'corepostRequestRouter'): if type(func) == FunctionType and hasattr(func,'corepostRequestRouter'):
# if specified, add class path to each function's path # if specified, add class path to each function's path
rq = func.corepostRequestRouter rq = func.corepostRequestRouter
rq.url = "%s%s" % (rootPath,rq.url) #workaround for multiple passes of __registerRouters (for unit tests etc)
# remove first and trailing '/' to standardize URLs if not hasattr(rq, 'urlAdapted'):
start = 1 if rq.url[0:1] == "/" else 0 rq.url = "%s%s" % (rootPath,rq.url)
end = -1 if rq.url[len(rq.url) -1] == '/' else len(rq.url) # remove first and trailing '/' to standardize URLs
rq.url = rq.url[start:end] start = 1 if rq.url[0:1] == "/" else 0
end = -1 if rq.url[len(rq.url) -1] == '/' else len(rq.url)
rq.url = rq.url[start:end]
setattr(rq,'urlAdapted',True)
# now that the full URL is set, compile the matcher for it # now that the full URL is set, compile the matcher for it
rq.compileMatcherForFullUrl() rq.compileMatcherForFullUrl()
for method in rq.methods: for method in rq.methods:
for accepts in rq.accepts: for accepts in rq.accepts:
urlRouterInstance = UrlRouterInstance(service,rq) urlRouterInstance = UrlRouterInstance(service,rq)
self.__urls[method][rq.url][accepts] = urlRouterInstance self.__urls[method][rq.url][accepts] = urlRouterInstance
self.__urlRouterInstances[func] = urlRouterInstance # needed so that we can lookup the urlRouterInstance for a specific function self.__urlRouterInstances[func] = urlRouterInstance # needed so that we can lookup the urlRouterInstance for a specific function
if self.__urlsMehods.get(rq.url, None) is None:
self.__urlsMehods[rq.url] = []
self.__urlsMehods[rq.url].append(method)
def getResponse(self,request): def getResponse(self,request):
"""Finds the appropriate instance 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"""
...@@ -211,7 +222,7 @@ class RequestRouter: ...@@ -211,7 +222,7 @@ class RequestRouter:
path = '/'.join(standardized_postpath) path = '/'.join(standardized_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]
urlRouterInstance, pathargs = None, None urlRouterInstance, pathargs = None, None
# fetch URL arguments <-> function from cache if hit at least once before # fetch URL arguments <-> function from cache if hit at least once before
if contentType in self.__cachedUrls[request.method][path]: if contentType in self.__cachedUrls[request.method][path]:
...@@ -231,16 +242,16 @@ class RequestRouter: ...@@ -231,16 +242,16 @@ class RequestRouter:
elif MediaType.WILDCARD in contentTypeInstances: elif MediaType.WILDCARD in contentTypeInstances:
# fall back to any wildcard method # fall back to any wildcard method
instance = contentTypeInstances[MediaType.WILDCARD] instance = contentTypeInstances[MediaType.WILDCARD]
if instance != None: if instance != None:
# see if the path arguments match up against any function @route definition # see if the path arguments match up against any function @route definition
args = instance.urlRouter.getArguments(path) args = instance.urlRouter.getArguments(path)
if args != None: if args != None:
if instance.urlRouter.cache: if instance.urlRouter.cache:
self.__cachedUrls[request.method][path][contentType] = CachedUrl(instance, args) self.__cachedUrls[request.method][path][contentType] = CachedUrl(instance, args)
urlRouterInstance,pathargs = instance,args urlRouterInstance,pathargs = instance,args
break break
#actual call #actual call
if urlRouterInstance != None and pathargs != None: if urlRouterInstance != None and pathargs != None:
allargs = copy.deepcopy(pathargs) allargs = copy.deepcopy(pathargs)
...@@ -284,8 +295,12 @@ class RequestRouter: ...@@ -284,8 +295,12 @@ class RequestRouter:
except Exception as ex: except Exception as ex:
log.err(ex) log.err(ex)
response = 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))
#if a url is defined, but not the requested method
elif not request.method in self.__urlsMehods.get(path, []) and self.__urlsMehods.get(path, []) != []:
response = self.__createErrorResponse(request,501, "")
else: else:
log.msg("URL %s not found" % path,logLevel=logging.WARN) log.msg("URL %s not found" % path,logLevel=logging.WARN)
response = self.__createErrorResponse(request,404,"URL '%s' not found\n" % request.path) response = self.__createErrorResponse(request,404,"URL '%s' not found\n" % request.path)
...@@ -305,9 +320,7 @@ class RequestRouter: ...@@ -305,9 +320,7 @@ class RequestRouter:
Takes care of automatically rendering the response and converting it to appropriate format (text,XML,JSON,YAML) Takes care of automatically rendering the response and converting it to appropriate format (text,XML,JSON,YAML)
depending on what the caller can accept. Returns Response depending on what the caller can accept. Returns Response
""" """
if isinstance(response, str): if isinstance(response, Response):
return Response(code,response,{HttpHeader.CONTENT_TYPE: MediaType.TEXT_PLAIN})
elif isinstance(response, Response):
return response return response
else: else:
(content,contentType) = self.__convertObjectToContentType(request, response) (content,contentType) = self.__convertObjectToContentType(request, response)
...@@ -318,20 +331,27 @@ class RequestRouter: ...@@ -318,20 +331,27 @@ class RequestRouter:
Takes care of converting an object (non-String) response to the appropriate format, based on the what the caller can accept. Takes care of converting an object (non-String) response to the appropriate format, based on the what the caller can accept.
Returns a tuple of (content,contentType) Returns a tuple of (content,contentType)
""" """
obj = convertForSerialization(obj)
if HttpHeader.ACCEPT in request.received_headers: if HttpHeader.ACCEPT in request.received_headers:
accept = request.received_headers[HttpHeader.ACCEPT] accept = request.received_headers[HttpHeader.ACCEPT]
if MediaType.APPLICATION_JSON in accept: if MediaType.APPLICATION_JSON in accept:
if not advanced_json:
obj = convertForSerialization(obj)
return (convertToJson(obj),MediaType.APPLICATION_JSON) return (convertToJson(obj),MediaType.APPLICATION_JSON)
elif MediaType.TEXT_YAML in accept: elif MediaType.TEXT_YAML in accept:
obj = convertForSerialization(obj)
return (yaml.dump(obj),MediaType.TEXT_YAML) return (yaml.dump(obj),MediaType.TEXT_YAML)
elif MediaType.APPLICATION_XML in accept or MediaType.TEXT_XML in accept: elif MediaType.APPLICATION_XML in accept or MediaType.TEXT_XML in accept:
obj = convertForSerialization(obj)
return (generateXml(obj),MediaType.APPLICATION_XML) return (generateXml(obj),MediaType.APPLICATION_XML)
else: else:
# no idea, let's do JSON # no idea, let's do JSON
if not advanced_json:
obj = convertForSerialization(obj)
return (convertToJson(obj),MediaType.APPLICATION_JSON) return (convertToJson(obj),MediaType.APPLICATION_JSON)
else: else:
if not advanced_json:
obj = convertForSerialization(obj)
# called has no accept header, let's default to JSON # called has no accept header, let's default to JSON
return (convertToJson(obj),MediaType.APPLICATION_JSON) return (convertToJson(obj),MediaType.APPLICATION_JSON)
...@@ -352,7 +372,7 @@ class RequestRouter: ...@@ -352,7 +372,7 @@ class RequestRouter:
"""Finishes any Defered/inlineCallback methods that raised an error. Returns Response""" """Finishes any Defered/inlineCallback methods that raised an error. Returns Response"""
log.err(error, "Deferred failed") log.err(error, "Deferred failed")
return self.__createErrorResponse(request, 500,"Internal server error") return self.__createErrorResponse(request, 500,"Internal server error")
def __createErrorResponse(self,request,code,message): def __createErrorResponse(self,request,code,message):
"""Common method for rendering errors""" """Common method for rendering errors"""
return Response(code=code, entity=message, headers={"content-type": MediaType.TEXT_PLAIN}) return Response(code=code, entity=message, headers={"content-type": MediaType.TEXT_PLAIN})
...@@ -361,19 +381,20 @@ class RequestRouter: ...@@ -361,19 +381,20 @@ class RequestRouter:
'''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():
contentType = request.received_headers["content-type"] contentType = request.received_headers["content-type"]
data = request.content.read()
if contentType == MediaType.APPLICATION_JSON: if contentType == MediaType.APPLICATION_JSON:
try: try:
request.json = json.loads(request.content.read()) request.json = json.loads(data) if data else {}
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 contentType 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(data)
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 contentType == MediaType.TEXT_YAML: elif contentType == MediaType.TEXT_YAML:
try: try:
request.yaml = yaml.safe_load(request.content.read()) request.yaml = yaml.safe_load(data)
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)
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
Various CorePost utilities Various CorePost utilities
''' '''
from inspect import getargspec from inspect import getargspec
import json
def getMandatoryArgumentNames(f): def getMandatoryArgumentNames(f):
'''Returns a tuple of the mandatory arguments required in a function''' '''Returns a tuple of the mandatory arguments required in a function'''
...@@ -11,17 +11,12 @@ def getMandatoryArgumentNames(f): ...@@ -11,17 +11,12 @@ def getMandatoryArgumentNames(f):
return args return args
else: else:
return args[0:len(args) - len(defaults)] return args[0:len(args) - len(defaults)]
def getRouterKey(method,url): def getRouterKey(method,url):
'''Returns the common key used to represent a function that a request can be routed to''' '''Returns the common key used to represent a function that a request can be routed to'''
return "%s %s" % (method,url) return "%s %s" % (method,url)
def convertToJson(obj):
"""Converts to JSON, including Python classes that are not JSON serializable by default"""
try:
return json.dumps(obj)
except Exception as ex:
raise RuntimeError(str(ex))
def checkExpectedInterfaces(objects,expectedInterface): def checkExpectedInterfaces(objects,expectedInterface):
"""Verifies that all the objects implement the expected interface""" """Verifies that all the objects implement the expected interface"""
...@@ -33,4 +28,3 @@ def safeDictUpdate(dictObject,key,value): ...@@ -33,4 +28,3 @@ def safeDictUpdate(dictObject,key,value):
"""Only adds a key to a dictionary. If key exists, it leaves it untouched""" """Only adds a key to a dictionary. If key exists, it leaves it untouched"""
if key not in dictObject: if key not in dictObject:
dictObject[key] = value dictObject[key] = value
\ No newline at end of file
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