Source code for httk.httkweb.jsonapi
#!/usr/bin/env python
#
# Copyright 2019 Rickard Armiento
#
# This file is part of a Python candidate reference implementation of
# the optimade API [https://www.optimade.org/]
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import json
from httk.httkweb.webserver import WebError
_jsonapi_response_codes = {
'200': 'OK',
'201': 'Created',
'202': 'Accepted',
'204': 'No Content',
'403': 'Forbidden',
'400': 'Bad Request',
'404': 'Not Found',
'406': 'Not Acceptable',
'409': 'Conflict',
'415': 'Unsupported Media Type',
'500': 'Internal Server Error'
}
[docs]class JsonapiError(WebError):
def __init__(self, message, response_code, response_msg=None, longmsg=None, idstr=None, links=None, code=None, source=None, meta=None, indent=True):
self.content = longmsg if longmsg is not None else message
self.response_code = response_code
self.content_type = 'application/vnd.api+json'
self.response_msg = response_msg if response_msg is not None else _jsonapi_response_codes[str(response_code)]
self.id = idstr
self.links = links
self.code = code
self.source = source
self.meta = meta
self.indent = indent
errordata = {}
if idstr is not None:
errordata['id'] = idstr
if code is not None:
errordata['code'] = code
if source is not None:
errordata['source'] = source
if meta is not None:
errordata['meta'] = meta
errordata['status'] = str(response_code)
errordata['title'] = str(self.response_msg)
errordata['detail'] = str(self.content)
self.content_json = {'errors': errordata}
if indent:
message = json.dumps(self.content_json, indent=4, separators=(',', ': '), sort_keys=True)
else:
message = json.dumps(self.content_json, separators=(',', ': '), sort_keys=True)
super(JsonapiError, self).__init__(message, response_code, response_msg, longmsg, self.content_type)
[docs]def check_jsonapi_header_requirements(headers):
# Handle jsonapi MUSTs with regards to headers
if 'Content-Type' in headers:
_media_type, _sep, parameter = headers['Content-Type'].partition(";")
if parameter.strip() != '':
raise WebError("Requested content-type violates jsonapi requirements.", 415, "Unsupported Media Type")
# The following implments my interpretation of the combination of RFC2616
# and the jsonapi 1.0 specification:
#
# - The jsonapi 1.0 spec states that the request MUST be rejected with 406 Not Acceptable
# only if ALL these apply:
# (a) There is an Accept header
# (b) The Accept header explicitly specifies 'application/vnd.api+json' PLUS media parameters
# (c) The Accept header does NOT specify 'application/vnd.api+json' WITHOUT media parameters
#
# - RFC2616 specifies that if there is an Accept header and the server cannot send a response
# which is acceptable according to the combined Accept field value, then the server SHOULD
# reject the request with a 406 Not Acceptable.
#
# Hence, if we get an Accept header with no 'application/vnd.api+json', then we accept
# the wildcards '*/*' and 'application/*'. BUT, if 'application/vnd.api+json' + media parameters
# occur anywhere, we DON'T accept those wildcards.
#
# To reject the wildcards does seem to violate the sprit of the Accept header, but the json 1.0 spec
# seems pretty set in its formulation with no exception for wildcards. I cannot, however, really
# forsee a realistic scenario where this actually causes any trouble.
#
if 'accept' in headers:
accepts = [x.strip() for x in headers['accept'].split(',')]
may_accept_media_range = True
media_range_encountered = False
for accept in accepts:
if accept == 'application/vnd.api+json':
break
if accept.split(";")[0] == 'application/vnd.api+json':
may_accept_media_range = False
if accept.split(";")[0] == '*/*' or accept.split(";")[0] == 'application/*':
media_range_encountered = True
else:
if not (may_accept_media_range and media_range_encountered):
raise WebError("Accept header violates jsonapi requirements.", 406, "Not Acceptable")