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

Commit

Permalink
Merge remote-tracking branch 'origin/master' into feature-docs
Browse files Browse the repository at this point in the history
  • Loading branch information
hawkowl committed Sep 14, 2014
2 parents 8ad76a6 + 3c91b11 commit e6aea54
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 21 deletions.
2 changes: 1 addition & 1 deletion saratoga/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os

__version__ = "0.5.0"
__version__ = "0.6.0"
__gitversion__ = __version__

basePath = os.path.abspath(os.path.dirname(__file__))
Expand Down
48 changes: 29 additions & 19 deletions saratoga/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@
AuthenticationRequired,
DoesNotExist,
APIError,
outputFormats,
__gitversion__
)

from base64 import b64decode
from jsonschema import Draft4Validator

import json
import re

import json


class DefaultServiceClass(object):
Expand Down Expand Up @@ -53,14 +53,9 @@ def _write(result, request, api, processor):
raise BadResponseParams(
", ".join(e.message for e in errors))

response = {
"status": "success",
"data": result
}

finishedResult = json.dumps(response, sort_keys=True,
indent=4, separators=(',', ': '))

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

request.write(finishedResult)
request.finish()

Expand All @@ -87,13 +82,10 @@ def _error(failure, request, api, processor):
errstatus = "fail"
errmessage = error.message

response = {
"status": errstatus,
"data": errmessage
}

request.write(json.dumps(response, sort_keys=True,
indent=4, separators=(',', ': ')))
finishedResult = self.api.outputRegistry.renderAutomaticResponse(
request, errstatus, errmessage)

request.write(finishedResult)
request.finish()

return 1
Expand All @@ -112,7 +104,17 @@ def _quickfail(fail):
return _error(Failure(fail), request, None, None)


request.setHeader("Content-Type", "application/json; charset=utf-8")
outputFormat = self.api.outputRegistry.getFormat(request)

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

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

Expand Down Expand Up @@ -244,13 +246,21 @@ def _authAdditional(canonicalUsername):

class SaratogaAPI(object):

def __init__(self, implementation, definition, serviceClass=None):
def __init__(self, implementation, definition, serviceClass=None,
outputRegistry=None):

if serviceClass:
self.serviceClass = serviceClass
else:
self.serviceClass = DefaultServiceClass()

if outputRegistry:
self.outputRegistry = outputRegistry
else:
self.outputRegistry = outputFormats.OutputRegistry("application/json")
self.outputRegistry.register("application/json",
outputFormats.JSendJSONOutputFormat)

self._implementation = implementation
self.implementation = DefaultServiceClass()

Expand Down
64 changes: 64 additions & 0 deletions saratoga/outputFormats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from negotiator import ContentNegotiator, AcceptParameters, ContentType
import json


class OutputRegistry(object):

def __init__(self, defaultOutputFormat):
self._outputFormats = {}
self._outputFormatsPreference = []
self.defaultOutputFormat = defaultOutputFormat

def getFormat(self, request):

defaultOutput = AcceptParameters(
ContentType(self.defaultOutputFormat, params='q=0'))

acceptable = [defaultOutput] + [AcceptParameters(ContentType(x)) for x in self._outputFormatsPreference]

cn = ContentNegotiator(defaultOutput, acceptable)
if request.requestHeaders.hasHeader("Accept"):
kwargs = {"accept": request.requestHeaders.getRawHeaders("Accept")[0]}
else:
kwargs = {}

accp = cn.negotiate(**kwargs)

return str(accp.content_type) if accp else None


def renderAutomaticResponse(self, request, status, data):

f = self.getFormat(request)
return self._outputFormats[f](status, data)


def register(self, acceptHeader, func):
"""
Register an output format.
"""
self._outputFormats[acceptHeader] = func
self._outputFormatsPreference.append(acceptHeader)

def JSendJSONOutputFormat(status, data):
"""
Implements the JSend output format, serialised to JSON.
"""

resp = {
"status": status,
"data": data
}

return json.dumps(resp)

def DebuggableJSendJSONOutputFormat(status, data):

resp = {
"status": status,
"data": data
}

return json.dumps(resp, sort_keys=True, indent=4, separators=(',', ': '))


7 changes: 7 additions & 0 deletions saratoga/test/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from shutil import copy

from saratoga.api import SaratogaAPI
from saratoga.outputFormats import OutputRegistry

import json

Expand Down Expand Up @@ -200,6 +201,12 @@ def setUp(self):

self.api = SaratogaAPI(APIImpl, APIDef)

def test_customOutputFormatRegistry(self):

o = OutputRegistry("application/json")
api = SaratogaAPI(APIImpl, APIDef, outputRegistry=o)
self.assertIs(o, api.outputRegistry)

def test_getResource(self):
"""
Check that Saratoga returns the correct resource.
Expand Down
106 changes: 106 additions & 0 deletions saratoga/test/test_outputFormats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from twisted.trial.unittest import TestCase

from saratoga import api, outputFormats
from saratoga.test.test_api import APIImpl, APIDef

class SaratogaAcceptHeaderTests(TestCase):

def setUp(self):

def respJSON(status, data):
return "JSON"

def respYAML(status, data):
return "YAML"

o = outputFormats.OutputRegistry("application/json")
o.register("application/yaml", respYAML)
o.register("application/json", respJSON)
o.register("application/debuggablejson",
outputFormats.DebuggableJSendJSONOutputFormat)

self.api = api.SaratogaAPI(APIImpl, APIDef, outputRegistry = o)

def test_noneGiven(self):

def rendered(request):
self.assertEqual(
request.getWrittenData(),
"JSON"
)
self.assertEqual(request.code, 200)

return self.api.test("/v1/example").addCallback(rendered)

def test_nonDefaultGiven(self):

def rendered(request):
self.assertEqual(
request.getWrittenData(),
"YAML"
)
self.assertEqual(request.code, 200)

return self.api.test("/v1/example",
headers={"Accept": ["application/yaml"]}
).addCallback(rendered)

def test_unknownDefaultGiven(self):

def rendered(request):
self.assertEqual(
request.getWrittenData(),
"406 Not Acceptable, please use one of: application/yaml, appli"
"cation/json, application/debuggablejson"
)
self.assertEqual(request.code, 406)

return self.api.test("/v1/example",
headers={"Accept": ["application/whatever"]}
).addCallback(rendered)

def test_debuggableJSend(self):

def rendered(request):
self.assertEqual(
request.getWrittenData(),
'{\n "data": {},\n "status": "success"\n}'
)
self.assertEqual(request.code, 200)

return self.api.test("/v1/example",
headers={"Accept": ["application/debuggablejson"]}
).addCallback(rendered)

def test_listDefaultGiven(self):

def rendered(request):
self.assertEqual(
request.getWrittenData(),
'JSON'
)
self.assertEqual(request.code, 200)

return self.api.test("/v1/example",
headers={"Accept": ["text/html,application/xhtml+xm"
"l,application/xml;q=0.9,"
"*/*;q=0.8"]}
).addCallback(rendered)

def test_qualityPreferencesGiven(self):

def rendered(request):
self.assertEqual(
request.getWrittenData(),
'YAML'
)
self.assertEqual(request.code, 200)

return self.api.test("/v1/example",
headers={"Accept": ["text/html,application/xhtml+xm"
"l,application/json;q=0.7,"
"application/yaml;q=0.8"]}
).addCallback(rendered)



3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
],
install_requires=[
"twisted",
"jsonschema"
"jsonschema",
"negotiator"
],
long_description=file('README.rst').read()
)
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ deps =
mock
jsonschema
httpsig
negotiator
commands = trial saratoga

0 comments on commit e6aea54

Please sign in to comment.