Skip to content
This repository has been archived by the owner on Dec 15, 2019. It is now read-only.

Commit

Permalink
Merge 434146c into 3c91b11
Browse files Browse the repository at this point in the history
  • Loading branch information
hawkowl committed Sep 6, 2014
2 parents 3c91b11 + 434146c commit 9218023
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 109 deletions.
202 changes: 115 additions & 87 deletions saratoga/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
DoesNotExist,
APIError,
outputFormats,
__gitversion__
__version__
)

from base64 import b64decode
Expand All @@ -37,31 +37,59 @@ def __init__(self, api):
self.api = api

def render(self, request):
"""
Render a response to this Saratoga server.
"""
# Some vars we'll need later
args, api, version, processor = ({}, None, None, {})
method = request.method.upper()

# Figure out what output format we're going to be using
outputFormat = self.api.outputRegistry.getFormat(request)

if not outputFormat:
# If there's no output format, return 406 Not Acceptable, as they
# have asked for something we can't give them
request.setResponseCode(406)
request.write("406 Not Acceptable, please use one of: ")
request.write(", ".join(self.api.outputRegistry._outputFormatsPreference))
request.finish()
return 1

def _write(result, request, api, processor):
# Set the content type according to what was given before
request.setHeader("Content-Type", outputFormat + "; charset=utf-8")
# Say who we are!
request.setHeader("Server", "Saratoga {} on Twisted {}".format(
__version__, twisted.__version__))

def _write(result):
"""
Serialise and write a successful result.
"""
if result is None:
result = {}

schema = processor.get("responseSchema", None)
schema = processor.get("responseSchema")

if schema:
v = Draft4Validator(schema)
errors = sorted(v.iter_errors(result), key=lambda e: e.path)

if errors:
raise BadResponseParams(
", ".join(e.message for e in errors))

finishedResult = self.api.outputRegistry.renderAutomaticResponse(
request, "success", result)

request.write(finishedResult)
request.finish()


def _error(failure, request, api, processor):


def _error(failure):
"""
Something's gone wrong, write out an error.
"""
error = failure.value
errorcode = 500

Expand All @@ -84,107 +112,111 @@ def _error(failure, request, api, processor):

finishedResult = self.api.outputRegistry.renderAutomaticResponse(
request, errstatus, errmessage)

request.write(finishedResult)
request.finish()

return 1


def _runAPICall(authParams, args, userParams, res):

api, version, processor = res
def _runAPICall(authParams):
"""
Run the function that we've looked up.
"""
userParams["auth"] = authParams

d = maybeDeferred(func, self.api.serviceClass, request, userParams, *args)

return d
return maybeDeferred(func, self.api.serviceClass, request,
userParams, *args)

def _quickfail(fail):
return _error(Failure(fail), request, None, None)

_error(Failure(fail))
return 1

outputFormat = self.api.outputRegistry.getFormat(request)
endpointMethods = self.api.endpoints.get(method)

if not outputFormat:
request.setResponseCode(406)
request.write("406 Not Acceptable, please use one of: ")
request.write(", ".join(self.api.outputRegistry._outputFormatsPreference))
request.finish()
return 1
if not endpointMethods:
fail = BadRequestParams("Method not allowed.")
fail.code = 405
return _quickfail(fail)

request.setHeader("Content-Type", outputFormat + ";" +
"charset=utf-8")
request.setHeader("Server", "Saratoga {} on Twisted {}".format(
__gitversion__, twisted.__version__))

api, version, processor = self.api.endpoints[request.method].get(
request.path, (None, None, None))
args = {}
# Try and look up the static route in the endpoints list
pathLookup = endpointMethods.get(request.path)

if not processor:
for item in self.api.endpoints[request.method]:
if not pathLookup:
# If there's no static route, try dynamic Django-style routes
for item in endpointMethods:
a = re.compile("^" + item + "$")
match = a.match(request.path)
if match:
api, version, processor = self.api.endpoints[request.method][item]
# We found it, so break out of here
pathLookup = endpointMethods[item]
args = match.groups()

break

if processor:
if not pathLookup:
fail = DoesNotExist("Endpoint does not exist.")
return _quickfail(fail)
else:
# Some variables we'll need later
params = {}

# Expand out the looked up path
api, version, processor = pathLookup

# Read in the content from the request
requestContent = request.content.read()

try:
# Parse it from JSON
# XXX: We should do this according to what it is, but JSON will
# do for now
params = json.loads(requestContent)
except ValueError:
params = {}

pass

# If there's no content body, but there are query args, put them in
# a dictionary for us
if not params and request.args is not None:
params = {}
for key, val in request.args.iteritems():
if type(val) in [list, tuple] and len(val) == 1:
params[key] = val[0]
else:
params[key] = val

# Make sure it's a dict, even if it's empty
params = {} if not params else params

userParams = {"params": params}
schema = processor.get("requestSchema", None)
schema = processor.get("requestSchema")

if params is None:
params = {}

if schema:
# Validate the schema, if we've got it
v = Draft4Validator(schema)
errors = sorted(v.iter_errors(params), key=lambda e: e.path)

if errors:
return _quickfail(BadRequestParams(", ".join(
e.message for e in errors)))

# Get the name of the endpoint
funcname = api.get("func") or api["endpoint"]
versionClass = getattr(
self.api.implementation, "v{}".format(version))
func = getattr(versionClass, "{}_{}".format(
funcname, request.method)).im_func
# Find the version class in the implementation
versionClass = getattr(self.api.implementation, "v{}".format(version))
# Do the dispatch, based on endpoint_method
func = getattr(versionClass, "{}_{}".format(funcname, method)).im_func

d = Deferred()

#############################
# Check for authentication. #
#############################

reqAuth = api.get("requiresAuthentication", False)

if reqAuth:
if api.get("requiresAuthentication", False):

if not hasattr(self.api.serviceClass, "auth"):
fail = APIError("Authentication required, but there is not"
" an available authenticator.")
return _quickfail(fail)

def _authAdditional(canonicalUsername):

return {"username": canonicalUsername}

auth = request.getHeader("Authorization")
Expand All @@ -193,61 +225,56 @@ def _authAdditional(canonicalUsername):
fail = AuthenticationRequired("Authentication required.")
return _quickfail(fail)


# Find out how we're authenticating
authType, authDetails = auth.split()
authType = authType.lower()

if not authType in ["basic"]:
if not authType.startswith("signature"):
fail = AuthenticationFailed(
if not authType in ["basic"] and not authType.startswith("signature"):
fail = AuthenticationFailed(
"Unsupported Authorization type '{}'".format(
authType.upper()))
return _quickfail(fail)
return _quickfail(fail)

if authType == "basic":
try:
authDetails = b64decode(authDetails)
authUser, authPassword = authDetails.split(":")
except:
fail = AuthenticationFailed(
"Malformed Authorization header.")
fail = AuthenticationFailed("Malformed Authorization header.")
return _quickfail(fail)

d.addCallback(lambda _:
self.api.serviceClass.auth.auth_usernameAndPassword(
authUser, authPassword))

if authType.startswith("signature"):

keyBits = request.getHeader("Authorization")\
.split("Signature ")[1].split(",")
keyBits = [z.split("=", 1) for z in keyBits]
keyBits = {x:y[1:-1] for x,y in keyBits}

d.addCallback(lambda _:
self.api.serviceClass.auth.auth_HMAC(keyBits["keyId"], request))
self.api.serviceClass.auth.auth_HMAC(keyBits["keyId"], request))

d.addCallback(_authAdditional)

d.addCallback(_runAPICall, args, userParams, (api, version, processor))
d.addCallback(_write, request, api, processor)
d.addErrback(_error, request, api, processor)
d.addCallback(_runAPICall)
d.addCallback(_write)
d.addErrback(_error)

# Kick off the chain
d.callback(None)

else:
fail = DoesNotExist("Endpoint does not exist.")
_quickfail(fail)

return 1

return 1


class SaratogaAPI(object):

def __init__(self, implementation, definition, serviceClass=None,
outputRegistry=None):
def __init__(self, implementation, definition, serviceClass=None, outputRegistry=None,
methods=["GET", "POST", "HEAD", "PUT", "DELETE", "PATCH"]):

methods = [x.upper() for x in methods]

if serviceClass:
self.serviceClass = serviceClass
Expand All @@ -260,8 +287,12 @@ def __init__(self, implementation, definition, serviceClass=None,
self.outputRegistry = outputFormats.OutputRegistry("application/json")
self.outputRegistry.register("application/json",
outputFormats.JSendJSONOutputFormat)

self.outputRegistry.register("application/debuggablejson",
outputFormats.DebuggableJSendJSONOutputFormat)

# Where the implementation comes from
self._implementation = implementation
# Where we put the implementation in
self.implementation = DefaultServiceClass()

if not definition.get("metadata"):
Expand All @@ -270,10 +301,7 @@ def __init__(self, implementation, definition, serviceClass=None,
self.APIMetadata = definition["metadata"]
self.APIDefinition = definition.get("endpoints", [])
self._versions = self.APIMetadata["versions"]
self.endpoints = {
"GET": {},
"POST": {}
}
self.endpoints = {x:{} for x in methods}

self.resource = SaratogaResource(self)

Expand All @@ -290,15 +318,15 @@ def __init__(self, implementation, definition, serviceClass=None,
setattr(self.implementation, "v{}".format(version), i)

for api in self.APIDefinition:
for verb in ["GET", "POST"]:
for verb in methods:
for processor in api.get("{}Processors".format(
verb.lower()), []):

schemas = {
"requestSchema": processor.get("requestSchema"),
"responseSchema": processor.get("responseSchema")
}

for key, val in schemas.iteritems():
if isinstance(val, basestring):
i = json.load(filepath.FilePath(".").preauthChild(val).open())
Expand All @@ -315,7 +343,7 @@ def __init__(self, implementation, definition, serviceClass=None,
funcName = api.get("func") or api["endpoint"]
if not hasattr(versionClass,
"{}_{}".format(funcName, verb)):
raise Exception("Implementation is missing the {} "
raise Exception("Implementation is missing the {} "
"processor in the v{} {} endpoint".format(
verb, version, funcName))

Expand Down

0 comments on commit 9218023

Please sign in to comment.