mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-09-25 08:44:33 +00:00
106 lines
4.9 KiB
Python
106 lines
4.9 KiB
Python
import json
|
|
|
|
import tornado.web
|
|
from tornado_eventsource.handler import EventSourceHandler
|
|
|
|
from core.utils import safe_json_dumps
|
|
|
|
|
|
class CompatibilityWrapper(tornado.web.RequestHandler):
|
|
"""Base class for compatibility wrappers. These provide translation between the different API versions, so while
|
|
the code itself only has proper handlers for the latest API version, we can still support the older APIs by wrapping
|
|
our API in one of these. The wrapper handles translating the old format of incoming data into the latest format,
|
|
then translating the output back into the format that an older client will expect. We actually do this in stages
|
|
because the API version change sets sit on top of each other, so for example a v1 request might go through two
|
|
wrappers, one to bring it up to v2, and the next to bring it up to v3, then the API call happens, then we go back
|
|
down through the wrappers in the other direction."""
|
|
|
|
def prepare(self):
|
|
self.translate_request()
|
|
super().prepare()
|
|
|
|
def translate_request(self):
|
|
"""Translate self.request in place from an older API version to the newer one. Overrides should do their own
|
|
translation, *then* call super(), so translation proceeds from oldest to newest."""
|
|
|
|
def translate_response_object(self, obj):
|
|
"""Translate a decoded JSON response from the newer API version to the older one. Overrides should call super()
|
|
*first*, then do their own translation, so translation proceeds from newest to oldest."""
|
|
|
|
return obj
|
|
|
|
def _translate_response(self, chunk):
|
|
"""Translate a JSON string output by the newer API version into its older equivalent. Anything that isn't a
|
|
JSON string is passed through untouched."""
|
|
|
|
if not isinstance(chunk, str):
|
|
return chunk
|
|
try:
|
|
return safe_json_dumps(self.translate_response_object(json.loads(chunk)))
|
|
except ValueError:
|
|
return chunk
|
|
|
|
|
|
class RequestCompatibilityWrapper(CompatibilityWrapper):
|
|
"""Compatibility wrapper for normal requests, which translates everything the handler produces."""
|
|
|
|
def write(self, chunk):
|
|
super().write(self._translate_response(chunk))
|
|
|
|
|
|
class StreamCompatibilityWrapper(CompatibilityWrapper, EventSourceHandler):
|
|
"""Special case for the SSE stream handlers, which write messages one at a time."""
|
|
|
|
def write_message(self, name=None, msg=True, wait=None, evt_id=None):
|
|
if not name:
|
|
msg = self._translate_response(msg)
|
|
return super().write_message(name=name, msg=msg, wait=wait, evt_id=evt_id)
|
|
|
|
|
|
def rename_query_params(request, name_map, value_map):
|
|
"""Utility method to rename query parameters in the request according to name_map ({old: new}), and rename values
|
|
of query parameters according to value_map ({param name: {old: new}}). Comma-separated lists of values are
|
|
supported, because we need to handle mapping e.g. "sig=POTA,NO_SIG" to "activity=POTA,NO_ACTIVITY" between v2 and
|
|
v3. value_map uses the new parameter names."""
|
|
|
|
for arguments in (request.arguments, request.query_arguments):
|
|
for old_name, new_name in name_map.items():
|
|
if old_name in arguments:
|
|
arguments[new_name] = arguments.pop(old_name)
|
|
for name, values in value_map.items():
|
|
if name in arguments:
|
|
arguments[name] = [
|
|
",".join(values.get(item.strip(), item) for item in v.decode("utf-8").split(",")).encode("utf-8")
|
|
for v in arguments[name]
|
|
]
|
|
|
|
|
|
def rename_keys_and_values(obj, key_map, value_map):
|
|
"""Utility method to rename keys in a JSON object according to key_map ({old: new}), and string values according
|
|
to value_map ({old key name: {old: new}}). The object can be list-like or dict-like. This function calls itself
|
|
recursively as it goes down the tree of stuff inside a dict."""
|
|
|
|
if isinstance(obj, dict):
|
|
translated = {}
|
|
for k, v in obj.items():
|
|
if k in value_map and isinstance(v, str):
|
|
v = value_map[k].get(v, v)
|
|
translated[key_map.get(k, k)] = rename_keys_and_values(v, key_map, value_map)
|
|
return translated
|
|
if isinstance(obj, list):
|
|
return [rename_keys_and_values(i, key_map, value_map) for i in obj]
|
|
return obj
|
|
|
|
|
|
def translate_json_body(request, translate):
|
|
"""Utility method to replace the request body by decoding it as JSON, passing it through the supplied translate
|
|
function, and re-encoding it. This is used to handle the add spot API call where the user is supplying JSON data as
|
|
a request body that contains the spot information. If the body is empty or invalid JSON, it is returned as-is so the
|
|
handler can return the appropriate error."""
|
|
|
|
try:
|
|
body = json.loads(request.body)
|
|
except ValueError:
|
|
return
|
|
request.body = json.dumps(translate(body)).encode("utf-8")
|