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

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

parents b2b41a31 4204008e
...@@ -7,12 +7,23 @@ for JSON/XML/YAML output ...@@ -7,12 +7,23 @@ 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"""
try:
if type(obj) in primitives: if type(obj) in primitives:
# no conversion # no conversion
return obj return obj
...@@ -29,6 +40,9 @@ def convertForSerialization(obj): ...@@ -29,6 +40,9 @@ def convertForSerialization(obj):
else: else:
# return as-is # return as-is
return obj return obj
except AttributeError as ex:
log.msg(ex,logLevel=logging.WARN)
return obj
def convertClassToDict(clazz): def convertClassToDict(clazz):
"""Converts a class to a dictionary""" """Converts a class to a dictionary"""
...@@ -49,6 +63,19 @@ def traverseDict(dictObject): ...@@ -49,6 +63,19 @@ def traverseDict(dictObject):
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"""
if isinstance(obj, dict) or isinstance(obj,DictMixin): if isinstance(obj, dict) or isinstance(obj,DictMixin):
......
...@@ -4,18 +4,24 @@ Common enums ...@@ -4,18 +4,24 @@ 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 = "*/*"
......
...@@ -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
...@@ -167,7 +175,7 @@ class RequestRouter: ...@@ -167,7 +175,7 @@ class RequestRouter:
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
...@@ -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
#workaround for multiple passes of __registerRouters (for unit tests etc)
if not hasattr(rq, 'urlAdapted'):
rq.url = "%s%s" % (rootPath,rq.url) rq.url = "%s%s" % (rootPath,rq.url)
# remove first and trailing '/' to standardize URLs # remove first and trailing '/' to standardize URLs
start = 1 if rq.url[0:1] == "/" else 0 start = 1 if rq.url[0:1] == "/" else 0
end = -1 if rq.url[len(rq.url) -1] == '/' else len(rq.url) end = -1 if rq.url[len(rq.url) -1] == '/' else len(rq.url)
rq.url = rq.url[start:end] 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"""
...@@ -236,11 +247,11 @@ class RequestRouter: ...@@ -236,11 +247,11 @@ class RequestRouter:
# 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)
...@@ -286,6 +297,10 @@ class RequestRouter: ...@@ -286,6 +297,10 @@ class RequestRouter:
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)
...@@ -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'''
...@@ -12,16 +12,11 @@ def getMandatoryArgumentNames(f): ...@@ -12,16 +12,11 @@ def getMandatoryArgumentNames(f):
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