Files
spothole/webserver/handlers/api/v2_compatibility.py
T

170 lines
6.5 KiB
Python

import json
import tornado
from core.utils import safe_json_dumps
from webserver.handlers.api.addspot import APISpotHandler
from webserver.handlers.api.alerts import APIAlertsHandler, APIAlertsStreamHandler
from webserver.handlers.api.lookups import APILookupActivityRefHandler
from webserver.handlers.api.options import APIOptionsHandler
from webserver.handlers.api.spots import APISpotsHandler, APISpotsStreamHandler
from webserver.handlers.api.status import APIStatusHandler
# Query parameters renamed in v3
_V2_TO_V3_QUERY_PARAMS = {
"sig": "activity",
"needs_sig": "needs_activity",
"needs_sig_ref": "needs_activity_ref",
}
# Values of query parameters renamed in v3
_V2_TO_V3_QUERY_VALUES = {
"activity": {"NO_SIG": "NO_ACTIVITY"},
"fields": {"sig": "activity", "sig_refs": "activity_refs"},
}
# Keys of JSON objects in API responses renamed in v3
_V3_TO_V2_RESPONSE_KEYS = {
"activity": "sig",
"activity_refs": "sig_refs",
"activity_type": "sig_type",
"activities": "sigs",
"activity_ref_data_providers": "sig_ref_data_providers",
"activity_name": "sig_name",
}
# Values of JSON objects in API responses renamed in v3
_V3_TO_V2_RESPONSE_VALUES = {
"dx_location_source": {"ACTIVITY REF LOOKUP": "SIG REF LOOKUP"},
}
def _translate_v2_query_params(handler):
"""Rename any v2 query parameters (and values) in the request to their v3 equivalents, so the v3 handler can
understand them."""
for arguments in (handler.request.arguments, handler.request.query_arguments):
for v2_name, v3_name in _V2_TO_V3_QUERY_PARAMS.items():
if v2_name in arguments:
arguments[v3_name] = arguments.pop(v2_name)
for name, value_map in _V2_TO_V3_QUERY_VALUES.items():
if name in arguments:
arguments[name] = [
",".join(value_map.get(item.strip(), item) for item in v.decode("utf-8").split(",")).encode("utf-8")
for v in arguments[name]
]
def _translate_v3_response_object(obj):
"""Rename keys and values in an object from their v3 names to their v2 names."""
if isinstance(obj, dict):
translated = {}
for k, v in obj.items():
if k in _V3_TO_V2_RESPONSE_VALUES and isinstance(v, str):
v = _V3_TO_V2_RESPONSE_VALUES[k].get(v, v)
translated[_V3_TO_V2_RESPONSE_KEYS.get(k, k)] = _translate_v3_response_object(v)
return translated
if isinstance(obj, list):
return [_translate_v3_response_object(i) for i in obj]
return obj
def translate_v3_response(chunk):
"""Translate a JSON string output by a v3 handler into its v2 equivalent"""
if not isinstance(chunk, str):
return chunk
try:
return safe_json_dumps(_translate_v3_response_object(json.loads(chunk)))
except ValueError:
return chunk
def translate_v2_spot(spot_data):
"""Translate a spot provided by a client in v2 format into v3 format"""
spot_data = dict(spot_data)
if "sig" in spot_data:
spot_data["activity"] = spot_data.pop("sig")
if "sig_refs" in spot_data:
spot_data["activity_refs"] = spot_data.pop("sig_refs")
if isinstance(spot_data.get("activity_refs"), list):
refs = []
for ref in spot_data["activity_refs"]:
if isinstance(ref, dict) and "sig" in ref:
ref = dict(ref)
ref["activity"] = ref.pop("sig")
refs.append(ref)
spot_data["activity_refs"] = refs
return spot_data
class _V2ResponseTranslationMixin:
"""Mixin for request handlers that translates v2 query params to v3 on the way in, and v3 JSON responses to v2 on
the way out"""
def prepare(self):
_translate_v2_query_params(self)
super().prepare()
def write(self, chunk):
super().write(translate_v3_response(chunk))
class _V2StreamTranslationMixin:
"""Mixin for SSE handlers that translates v2 query params to v3 on the way in, and v3 JSON messages to v2 on the way
out"""
def prepare(self):
_translate_v2_query_params(self)
super().prepare()
def write_message(self, name=None, msg=True, wait=None, evt_id=None):
if not name:
msg = translate_v3_response(msg)
return super().write_message(name=name, msg=msg, wait=wait, evt_id=evt_id)
class V2APISpotsHandler(_V2ResponseTranslationMixin, APISpotsHandler):
"""API request handler for /api/v2/spots (GET). Included in Spothole v3 for backwards compatibility."""
class V2APISpotsStreamHandler(_V2StreamTranslationMixin, APISpotsStreamHandler):
"""API request handler for /api/v2/spots/stream (SSE). Included in Spothole v3 for backwards compatibility."""
class V2APIAlertsHandler(_V2ResponseTranslationMixin, APIAlertsHandler):
"""API request handler for /api/v2/alerts (GET). Included in Spothole v3 for backwards compatibility."""
class V2APIAlertsStreamHandler(_V2StreamTranslationMixin, APIAlertsStreamHandler):
"""API request handler for /api/v2/alerts/stream (SSE). Included in Spothole v3 for backwards compatibility."""
class V2APIOptionsHandler(_V2ResponseTranslationMixin, APIOptionsHandler):
"""API request handler for /api/v2/options (GET). Included in Spothole v3 for backwards compatibility."""
class V2APIStatusHandler(_V2ResponseTranslationMixin, APIStatusHandler):
"""API request handler for /api/v2/status (GET). Included in Spothole v3 for backwards compatibility."""
class V2APILookupSigRefHandler(_V2ResponseTranslationMixin, APILookupActivityRefHandler):
"""API request handler for /api/v2/lookup/sigref (GET). Included in Spothole v3 for backwards compatibility. This
is the v2 equivalent of /api/v3/lookup/activityref."""
class V2APISpotHandler(APISpotHandler):
"""API request handler for /api/v2/spot (POST). Included in Spothole v3 for backwards compatibility. Translates the
spot in the request body from v2 to v3 format. The response is a plain status message so needs no translation."""
def post(self):
# Translate the request body if we can. If the body is empty or invalid JSON, leave it alone and let the v3
# handler return the appropriate error.
try:
json_body = tornado.escape.json_decode(self.request.body)
if isinstance(json_body, dict) and isinstance(json_body.get("spot"), dict):
json_body["spot"] = translate_v2_spot(json_body["spot"])
self.request.body = json.dumps(json_body).encode("utf-8")
except ValueError:
pass
super().post()