mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-09-25 00:34:32 +00:00
Come up with a common base class for backwards-compatibility translation wrappers between the different API versions #143
This commit is contained in:
@@ -222,6 +222,7 @@ class APISpotHandler(tornado.web.RequestHandler):
|
|||||||
# duplicate with what immediately comes back from the API. But if we weren't asked to send it upstream, or
|
# duplicate with what immediately comes back from the API. But if we weren't asked to send it upstream, or
|
||||||
# we were but it failed, we should still add it to our database anyway.
|
# we were but it failed, we should still add it to our database anyway.
|
||||||
if not submit_upstream or upstream_warning:
|
if not submit_upstream or upstream_warning:
|
||||||
|
spot.source = "API"
|
||||||
spot.infer_missing()
|
spot.infer_missing()
|
||||||
self._spots.set(spot.id, spot)
|
self._spots.set(spot.id, spot)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
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")
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
from webserver.handlers.api.compatibility.compatibility import (
|
||||||
|
CompatibilityWrapper,
|
||||||
|
rename_keys_and_values,
|
||||||
|
translate_json_body,
|
||||||
|
)
|
||||||
|
from webserver.handlers.api.compatibility.v2_compatibility import (
|
||||||
|
V2APIAlertsHandler,
|
||||||
|
V2APIAlertsStreamHandler,
|
||||||
|
V2APIDxStatsHandler,
|
||||||
|
V2APILookupCallHandler,
|
||||||
|
V2APILookupGridHandler,
|
||||||
|
V2APILookupSigRefHandler,
|
||||||
|
V2APIOptionsHandler,
|
||||||
|
V2APISolarConditionsHandler,
|
||||||
|
V2APISpotHandler,
|
||||||
|
V2APISpotsHandler,
|
||||||
|
V2APISpotsStreamHandler,
|
||||||
|
V2APIStatusHandler,
|
||||||
|
)
|
||||||
|
|
||||||
|
# QRZ/HamQTH credentials were provided as query parameters in v1, but as headers in v2
|
||||||
|
_V1_QUERY_PARAMS_TO_V2_HEADERS = {
|
||||||
|
"qrz_username": "X-QRZ-Username",
|
||||||
|
"qrz_password": "X-QRZ-Password",
|
||||||
|
"qrz_session_key": "X-QRZ-Session-Key",
|
||||||
|
"hamqth_username": "X-HamQTH-Username",
|
||||||
|
"hamqth_password": "X-HamQTH-Password",
|
||||||
|
"hamqth_session_id": "X-HamQTH-Session-ID",
|
||||||
|
}
|
||||||
|
# DX location source of "GRID" from a v2 response becomes "SPOT" to a v1 client.
|
||||||
|
_V2_TO_V1_RESPONSE_VALUES = {
|
||||||
|
"dx_location_source": {"GRID": "SPOT"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class V1CompatibilityWrapper(CompatibilityWrapper):
|
||||||
|
"""Translates v1 requests to v2 on the way in, and v2 responses to v1 on the way out. This logic captures the
|
||||||
|
majority of changes needed for each endpoint."""
|
||||||
|
|
||||||
|
def translate_request(self):
|
||||||
|
"""This one is a bit more than a simple translation of query params beceause we also need to move some query
|
||||||
|
params to instead be headers."""
|
||||||
|
|
||||||
|
for param, header in _V1_QUERY_PARAMS_TO_V2_HEADERS.items():
|
||||||
|
if header in self.request.headers:
|
||||||
|
continue
|
||||||
|
value = self.get_query_argument(param, default=None)
|
||||||
|
if value:
|
||||||
|
self.request.headers[header] = value
|
||||||
|
super().translate_request()
|
||||||
|
|
||||||
|
def translate_response_object(self, obj):
|
||||||
|
obj = super().translate_response_object(obj)
|
||||||
|
return rename_keys_and_values(obj, {}, _V2_TO_V1_RESPONSE_VALUES)
|
||||||
|
|
||||||
|
|
||||||
|
class V1APISpotsHandler(V1CompatibilityWrapper, V2APISpotsHandler):
|
||||||
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
|
class V1APISpotsStreamHandler(V1CompatibilityWrapper, V2APISpotsStreamHandler):
|
||||||
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
|
class V1APIAlertsHandler(V1CompatibilityWrapper, V2APIAlertsHandler):
|
||||||
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
|
class V1APIAlertsStreamHandler(V1CompatibilityWrapper, V2APIAlertsStreamHandler):
|
||||||
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
|
class V1APISolarConditionsHandler(V1CompatibilityWrapper, V2APISolarConditionsHandler):
|
||||||
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
|
class V1APIDxStatsHandler(V1CompatibilityWrapper, V2APIDxStatsHandler):
|
||||||
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
|
class V1APIOptionsHandler(V1CompatibilityWrapper, V2APIOptionsHandler):
|
||||||
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
|
class V1APIStatusHandler(V1CompatibilityWrapper, V2APIStatusHandler):
|
||||||
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
|
class V1APILookupCallHandler(V1CompatibilityWrapper, V2APILookupCallHandler):
|
||||||
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
|
class V1APILookupSigRefHandler(V1CompatibilityWrapper, V2APILookupSigRefHandler):
|
||||||
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
|
class V1APILookupGridHandler(V1CompatibilityWrapper, V2APILookupGridHandler):
|
||||||
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
|
class V1APISpotHandler(V1CompatibilityWrapper, V2APISpotHandler):
|
||||||
|
"""Some special handling required for this one, the v1 add spot call took its data in from query parameters, but in
|
||||||
|
v2 we changed that to using a request body with the data in, so we need to recreate that here before we pass on
|
||||||
|
handling to the v2 call."""
|
||||||
|
|
||||||
|
def translate_request(self):
|
||||||
|
def translate(body):
|
||||||
|
if isinstance(body, dict):
|
||||||
|
return {"spot": body}
|
||||||
|
return body
|
||||||
|
|
||||||
|
translate_json_body(self.request, translate)
|
||||||
|
super().translate_request()
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
from webserver.handlers.api.addspot import APISpotHandler
|
||||||
|
from webserver.handlers.api.alerts import APIAlertsHandler, APIAlertsStreamHandler
|
||||||
|
from webserver.handlers.api.compatibility.compatibility import (
|
||||||
|
CompatibilityWrapper,
|
||||||
|
RequestCompatibilityWrapper,
|
||||||
|
StreamCompatibilityWrapper,
|
||||||
|
rename_keys_and_values,
|
||||||
|
rename_query_params,
|
||||||
|
translate_json_body,
|
||||||
|
)
|
||||||
|
from webserver.handlers.api.dxstats import APIDxStatsHandler
|
||||||
|
from webserver.handlers.api.lookups import APILookupActivityRefHandler, APILookupCallHandler, APILookupGridHandler
|
||||||
|
from webserver.handlers.api.options import APIOptionsHandler
|
||||||
|
from webserver.handlers.api.solar_conditions import APISolarConditionsHandler
|
||||||
|
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"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class V2CompatibilityWrapper(CompatibilityWrapper):
|
||||||
|
"""Translates v2 requests to v3 on the way in, and v3 responses to v2 on the way out. This logic captures the
|
||||||
|
majority of changes needed for each endpoint."""
|
||||||
|
|
||||||
|
def translate_request(self):
|
||||||
|
rename_query_params(self.request, _V2_TO_V3_QUERY_PARAMS, _V2_TO_V3_QUERY_VALUES)
|
||||||
|
super().translate_request()
|
||||||
|
|
||||||
|
def translate_response_object(self, obj):
|
||||||
|
obj = super().translate_response_object(obj)
|
||||||
|
return rename_keys_and_values(obj, _V3_TO_V2_RESPONSE_KEYS, _V3_TO_V2_RESPONSE_VALUES)
|
||||||
|
|
||||||
|
|
||||||
|
class V2APISpotsHandler(V2CompatibilityWrapper, RequestCompatibilityWrapper, APISpotsHandler):
|
||||||
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
|
class V2APISpotsStreamHandler(V2CompatibilityWrapper, StreamCompatibilityWrapper, APISpotsStreamHandler):
|
||||||
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
|
class V2APIAlertsHandler(V2CompatibilityWrapper, RequestCompatibilityWrapper, APIAlertsHandler):
|
||||||
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
|
class V2APIAlertsStreamHandler(V2CompatibilityWrapper, StreamCompatibilityWrapper, APIAlertsStreamHandler):
|
||||||
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
|
class V2APISolarConditionsHandler(V2CompatibilityWrapper, RequestCompatibilityWrapper, APISolarConditionsHandler):
|
||||||
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
|
class V2APIDxStatsHandler(V2CompatibilityWrapper, RequestCompatibilityWrapper, APIDxStatsHandler):
|
||||||
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
|
class V2APIOptionsHandler(V2CompatibilityWrapper, RequestCompatibilityWrapper, APIOptionsHandler):
|
||||||
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
|
class V2APIStatusHandler(V2CompatibilityWrapper, RequestCompatibilityWrapper, APIStatusHandler):
|
||||||
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
|
class V2APILookupCallHandler(V2CompatibilityWrapper, RequestCompatibilityWrapper, APILookupCallHandler):
|
||||||
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
|
class V2APILookupSigRefHandler(V2CompatibilityWrapper, RequestCompatibilityWrapper, APILookupActivityRefHandler):
|
||||||
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
|
class V2APILookupGridHandler(V2CompatibilityWrapper, RequestCompatibilityWrapper, APILookupGridHandler):
|
||||||
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
|
class V2APISpotHandler(V2CompatibilityWrapper, RequestCompatibilityWrapper, APISpotHandler):
|
||||||
|
"""Some special handling for add spot, because unlike the other calls we have to deal with a request body.
|
||||||
|
Because the definition of a spot changed (e.g. sig to activity) we need to apply the same translation to
|
||||||
|
spots that are coming in via the add spot call."""
|
||||||
|
|
||||||
|
def translate_request(self):
|
||||||
|
def translate(body):
|
||||||
|
if isinstance(body, dict) and isinstance(body.get("spot"), dict):
|
||||||
|
body["spot"] = self._translate_v2_spot(body["spot"])
|
||||||
|
return body
|
||||||
|
|
||||||
|
translate_json_body(self.request, translate)
|
||||||
|
super().translate_request()
|
||||||
|
|
||||||
|
|
||||||
|
def _translate_v2_spot(self, spot_data):
|
||||||
|
"""Translate a spot provided by a client calling the add spot method 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
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
import logging
|
|
||||||
import re
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import tornado
|
|
||||||
from tornado import httputil
|
|
||||||
from tornado.web import Application
|
|
||||||
|
|
||||||
from core.activity_utils import get_ref_regex_for_activity
|
|
||||||
from core.config import ALLOW_SPOTTING
|
|
||||||
from core.constants import UNKNOWN_BAND
|
|
||||||
from core.utils import infer_band_from_freq, safe_json_dumps
|
|
||||||
from data.spot import Spot
|
|
||||||
from webserver.handlers.api.v2_compatibility import translate_v2_spot
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class V1APISpotHandler(tornado.web.RequestHandler):
|
|
||||||
"""API request handler for /api/v1/spot (POST). Included in Spothole v2 onwards for backwards
|
|
||||||
compatibility."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
application: "Application",
|
|
||||||
request: httputil.HTTPServerRequest,
|
|
||||||
**kwargs: Any,
|
|
||||||
):
|
|
||||||
self._spots = None
|
|
||||||
super().__init__(application, request, **kwargs)
|
|
||||||
|
|
||||||
def initialize(self, spots):
|
|
||||||
self._spots = spots
|
|
||||||
|
|
||||||
def post(self):
|
|
||||||
try:
|
|
||||||
# Reject if not allowed
|
|
||||||
if not ALLOW_SPOTTING:
|
|
||||||
self.set_status(401)
|
|
||||||
self.write(safe_json_dumps("Error - this server does not allow new spots to be added via the API."))
|
|
||||||
self.set_header("Cache-Control", "no-store")
|
|
||||||
self.set_header("Content-Type", "application/json")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Reject if format not json
|
|
||||||
if not self.request.headers.get("Content-Type", "").startswith("application/json"):
|
|
||||||
self.set_status(415)
|
|
||||||
self.write(safe_json_dumps("Error - request Content-Type must be application/json"))
|
|
||||||
self.set_header("Cache-Control", "no-store")
|
|
||||||
self.set_header("Content-Type", "application/json")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Reject if request body is empty
|
|
||||||
post_data = self.request.body
|
|
||||||
if not post_data:
|
|
||||||
self.set_status(422)
|
|
||||||
self.write(safe_json_dumps("Error - request body is empty"))
|
|
||||||
self.set_header("Cache-Control", "no-store")
|
|
||||||
self.set_header("Content-Type", "application/json")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Read in the request body as JSON then convert to a Spot object. The v1 spot format uses the same field
|
|
||||||
# names as v2, so needs the same translation to v3 field names.
|
|
||||||
json_spot = translate_v2_spot(tornado.escape.json_decode(post_data))
|
|
||||||
spot = Spot(**json_spot)
|
|
||||||
|
|
||||||
# Reject if no timestamp, frequency, dx_call or de_call
|
|
||||||
if not spot.time or not spot.dx_call or not spot.freq or not spot.de_call:
|
|
||||||
self.set_status(422)
|
|
||||||
self.write(
|
|
||||||
safe_json_dumps("Error - 'time', 'dx_call', 'freq' and 'de_call' must be provided as a minimum.")
|
|
||||||
)
|
|
||||||
self.set_header("Cache-Control", "no-store")
|
|
||||||
self.set_header("Content-Type", "application/json")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Reject invalid-looking callsigns
|
|
||||||
if not re.match(r"^[A-Za-z0-9/\-]*$", spot.dx_call):
|
|
||||||
self.set_status(422)
|
|
||||||
self.write(safe_json_dumps(f"Error - '{spot.dx_call}' does not look like a valid callsign."))
|
|
||||||
self.set_header("Cache-Control", "no-store")
|
|
||||||
self.set_header("Content-Type", "application/json")
|
|
||||||
return
|
|
||||||
if not re.match(r"^[A-Za-z0-9/\-]*$", spot.de_call):
|
|
||||||
self.set_status(422)
|
|
||||||
self.write(safe_json_dumps(f"Error - '{spot.de_call}' does not look like a valid callsign."))
|
|
||||||
self.set_header("Cache-Control", "no-store")
|
|
||||||
self.set_header("Content-Type", "application/json")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Reject if frequency not in a known band
|
|
||||||
if infer_band_from_freq(spot.freq) == UNKNOWN_BAND:
|
|
||||||
self.set_status(422)
|
|
||||||
self.write(safe_json_dumps(f"Error - Frequency of {spot.freq / 1000.0!s}kHz is not in a known band."))
|
|
||||||
self.set_header("Cache-Control", "no-store")
|
|
||||||
self.set_header("Content-Type", "application/json")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Reject if grid formatting incorrect
|
|
||||||
if spot.dx_grid and not re.match(
|
|
||||||
r"^([A-R]{2}[0-9]{2}[A-X]{2}[0-9]{2}[A-X]{2}|[A-R]{2}[0-9]{2}[A-X]{2}[0-9]{2}|[A-R]{2}[0-9]{2}[A-X]{2}|[A-R]{2}[0-9]{2})$",
|
|
||||||
spot.dx_grid.upper(),
|
|
||||||
):
|
|
||||||
self.set_status(422)
|
|
||||||
self.write(safe_json_dumps(f"Error - '{spot.dx_grid}' does not look like a valid Maidenhead grid."))
|
|
||||||
self.set_header("Cache-Control", "no-store")
|
|
||||||
self.set_header("Content-Type", "application/json")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Reject if activity ref format incorrect for activity
|
|
||||||
if (
|
|
||||||
spot.activity
|
|
||||||
and spot.activity_refs
|
|
||||||
and len(spot.activity_refs) > 0
|
|
||||||
and spot.activity_refs[0].id
|
|
||||||
and get_ref_regex_for_activity(spot.activity)
|
|
||||||
and not re.match(get_ref_regex_for_activity(spot.activity), spot.activity_refs[0].id)
|
|
||||||
):
|
|
||||||
self.set_status(422)
|
|
||||||
self.write(
|
|
||||||
safe_json_dumps(
|
|
||||||
f"Error - '{spot.activity_refs[0].id}' does not look like a valid reference for {spot.activity}."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.set_header("Cache-Control", "no-store")
|
|
||||||
self.set_header("Content-Type", "application/json")
|
|
||||||
return
|
|
||||||
|
|
||||||
# infer missing data, and add it to our database.
|
|
||||||
spot.source = "API"
|
|
||||||
spot.infer_missing()
|
|
||||||
self._spots.set(spot.id, spot)
|
|
||||||
|
|
||||||
self.write(safe_json_dumps("OK"))
|
|
||||||
self.set_status(201)
|
|
||||||
self.set_header("Cache-Control", "no-store")
|
|
||||||
self.set_header("Content-Type", "application/json")
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Exception when handling client request to add spot API")
|
|
||||||
self.write(safe_json_dumps("Error - an internal server error occurred."))
|
|
||||||
self.set_status(500)
|
|
||||||
self.set_header("Cache-Control", "no-store")
|
|
||||||
self.set_header("Content-Type", "application/json")
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
import tornado
|
|
||||||
from tornado.httpclient import AsyncHTTPClient
|
|
||||||
from tornado.httputil import HTTPHeaders
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
_LEGACY_PARAM_TO_HEADER_MAP = {
|
|
||||||
"qrz_username": "X-QRZ-Username",
|
|
||||||
"qrz_password": "X-QRZ-Password",
|
|
||||||
"qrz_session_key": "X-QRZ-Session-Key",
|
|
||||||
"hamqth_username": "X-HamQTH-Username",
|
|
||||||
"hamqth_password": "X-HamQTH-Password",
|
|
||||||
"hamqth_session_id": "X-HamQTH-Session-ID",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class V1RedirectHandler(tornado.web.RequestHandler):
|
|
||||||
"""Transparently proxies requests from the old API to the new one,
|
|
||||||
returning whatever the v2 endpoint returns, for endpoints with no breaking changes."""
|
|
||||||
|
|
||||||
async def _proxy(self, path):
|
|
||||||
new_url = f"{self.request.protocol}://{self.request.host}/api/v2/{path}"
|
|
||||||
if self.request.query:
|
|
||||||
new_url += f"?{self.request.query}"
|
|
||||||
|
|
||||||
# Copy the incoming headers so we can add translated legacy credentials without changing the original
|
|
||||||
# request.
|
|
||||||
headers = HTTPHeaders(self.request.headers)
|
|
||||||
for param, header in _LEGACY_PARAM_TO_HEADER_MAP.items():
|
|
||||||
value = self.get_query_argument(param, default=None)
|
|
||||||
if value:
|
|
||||||
headers[header] = value
|
|
||||||
|
|
||||||
client = AsyncHTTPClient()
|
|
||||||
try:
|
|
||||||
response = await client.fetch(
|
|
||||||
new_url,
|
|
||||||
method=self.request.method,
|
|
||||||
headers=headers,
|
|
||||||
body=None if self.request.method == "GET" else (self.request.body or b""),
|
|
||||||
raise_error=False,
|
|
||||||
follow_redirects=False,
|
|
||||||
request_timeout=10.0,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception("Exception when proxying legacy v1 API request")
|
|
||||||
raise tornado.web.HTTPError(502, reason=str(e))
|
|
||||||
|
|
||||||
self.set_status(response.code, response.reason)
|
|
||||||
if isinstance(response.headers, HTTPHeaders):
|
|
||||||
for name, value in response.headers.get_all():
|
|
||||||
# Let Tornado recompute these for the outgoing response
|
|
||||||
if name.lower() not in (
|
|
||||||
"content-length",
|
|
||||||
"transfer-encoding",
|
|
||||||
"connection",
|
|
||||||
):
|
|
||||||
self.add_header(name, value)
|
|
||||||
if response.body:
|
|
||||||
self.write(response.body)
|
|
||||||
|
|
||||||
async def get(self, path):
|
|
||||||
await self._proxy(path)
|
|
||||||
|
|
||||||
async def post(self, path):
|
|
||||||
await self._proxy(path)
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
from webserver.handlers.api.v2_compatibility import V2APISpotsHandler, V2APISpotsStreamHandler
|
|
||||||
|
|
||||||
_GRID_SOURCE_RE = re.compile(r'"dx_location_source":\s*"GRID"')
|
|
||||||
_LEGACY_PARAM_TO_HEADER_MAP = {
|
|
||||||
"qrz_username": "X-QRZ-Username",
|
|
||||||
"qrz_password": "X-QRZ-Password",
|
|
||||||
"qrz_session_key": "X-QRZ-Session-Key",
|
|
||||||
"hamqth_username": "X-HamQTH-Username",
|
|
||||||
"hamqth_password": "X-HamQTH-Password",
|
|
||||||
"hamqth_session_id": "X-HamQTH-Session-ID",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _handle_legacy_params(handler):
|
|
||||||
"""Copy v1 query-string QRZ/HamQTH credentials into the v2 headers, so the v2 handler can see them"""
|
|
||||||
|
|
||||||
for param, header in _LEGACY_PARAM_TO_HEADER_MAP.items():
|
|
||||||
if header in handler.request.headers:
|
|
||||||
continue
|
|
||||||
value = handler.get_query_argument(param, default=None)
|
|
||||||
if value:
|
|
||||||
handler.request.headers[header] = value
|
|
||||||
|
|
||||||
|
|
||||||
class V1APISpotsHandler(V2APISpotsHandler):
|
|
||||||
"""API request handler for /api/v1/spots (GET). Included in Spothole v2 onwards for backwards
|
|
||||||
compatibility."""
|
|
||||||
|
|
||||||
def prepare(self):
|
|
||||||
_handle_legacy_params(self)
|
|
||||||
super().prepare()
|
|
||||||
|
|
||||||
def write(self, chunk):
|
|
||||||
if isinstance(chunk, str):
|
|
||||||
chunk = _GRID_SOURCE_RE.sub('"dx_location_source": "SPOT"', chunk)
|
|
||||||
super().write(chunk)
|
|
||||||
|
|
||||||
|
|
||||||
class V1APISpotsStreamHandler(V2APISpotsStreamHandler):
|
|
||||||
"""API request handler for /api/v1/spots/stream (SSE). Included in Spothole v2 onwards for backwards
|
|
||||||
compatibility."""
|
|
||||||
|
|
||||||
def prepare(self):
|
|
||||||
_handle_legacy_params(self)
|
|
||||||
super().prepare()
|
|
||||||
|
|
||||||
def write_message(self, *args, **kwargs):
|
|
||||||
args = list(args)
|
|
||||||
for i, a in enumerate(args):
|
|
||||||
if isinstance(a, str) and '"dx_location_source"' in a:
|
|
||||||
args[i] = _GRID_SOURCE_RE.sub('"dx_location_source": "SPOT"', a)
|
|
||||||
for k, v in kwargs.items():
|
|
||||||
if isinstance(v, str) and '"dx_location_source"' in v:
|
|
||||||
kwargs[k] = _GRID_SOURCE_RE.sub('"dx_location_source": "SPOT"', v)
|
|
||||||
super().write_message(*args, **kwargs)
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
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()
|
|
||||||
+68
-21
@@ -17,6 +17,34 @@ from core.data_providers import DATA_PROVIDERS
|
|||||||
from core.data_store import DATA_STORE
|
from core.data_store import DATA_STORE
|
||||||
from webserver.handlers.api.addspot import APISpotHandler
|
from webserver.handlers.api.addspot import APISpotHandler
|
||||||
from webserver.handlers.api.alerts import APIAlertsHandler, APIAlertsStreamHandler
|
from webserver.handlers.api.alerts import APIAlertsHandler, APIAlertsStreamHandler
|
||||||
|
from webserver.handlers.api.compatibility.v1_compatibility import (
|
||||||
|
V1APIAlertsHandler,
|
||||||
|
V1APIAlertsStreamHandler,
|
||||||
|
V1APIDxStatsHandler,
|
||||||
|
V1APILookupCallHandler,
|
||||||
|
V1APILookupGridHandler,
|
||||||
|
V1APILookupSigRefHandler,
|
||||||
|
V1APIOptionsHandler,
|
||||||
|
V1APISolarConditionsHandler,
|
||||||
|
V1APISpotHandler,
|
||||||
|
V1APISpotsHandler,
|
||||||
|
V1APISpotsStreamHandler,
|
||||||
|
V1APIStatusHandler,
|
||||||
|
)
|
||||||
|
from webserver.handlers.api.compatibility.v2_compatibility import (
|
||||||
|
V2APIAlertsHandler,
|
||||||
|
V2APIAlertsStreamHandler,
|
||||||
|
V2APIDxStatsHandler,
|
||||||
|
V2APILookupCallHandler,
|
||||||
|
V2APILookupGridHandler,
|
||||||
|
V2APILookupSigRefHandler,
|
||||||
|
V2APIOptionsHandler,
|
||||||
|
V2APISolarConditionsHandler,
|
||||||
|
V2APISpotHandler,
|
||||||
|
V2APISpotsHandler,
|
||||||
|
V2APISpotsStreamHandler,
|
||||||
|
V2APIStatusHandler,
|
||||||
|
)
|
||||||
from webserver.handlers.api.dxstats import APIDxStatsHandler
|
from webserver.handlers.api.dxstats import APIDxStatsHandler
|
||||||
from webserver.handlers.api.lookups import (
|
from webserver.handlers.api.lookups import (
|
||||||
APILookupActivityRefHandler,
|
APILookupActivityRefHandler,
|
||||||
@@ -27,19 +55,6 @@ from webserver.handlers.api.options import APIOptionsHandler
|
|||||||
from webserver.handlers.api.solar_conditions import APISolarConditionsHandler
|
from webserver.handlers.api.solar_conditions import APISolarConditionsHandler
|
||||||
from webserver.handlers.api.spots import APISpotsHandler, APISpotsStreamHandler
|
from webserver.handlers.api.spots import APISpotsHandler, APISpotsStreamHandler
|
||||||
from webserver.handlers.api.status import APIStatusHandler
|
from webserver.handlers.api.status import APIStatusHandler
|
||||||
from webserver.handlers.api.v1_addspot import V1APISpotHandler
|
|
||||||
from webserver.handlers.api.v1_compatability import V1RedirectHandler
|
|
||||||
from webserver.handlers.api.v1_spots import V1APISpotsHandler, V1APISpotsStreamHandler
|
|
||||||
from webserver.handlers.api.v2_compatibility import (
|
|
||||||
V2APIAlertsHandler,
|
|
||||||
V2APIAlertsStreamHandler,
|
|
||||||
V2APILookupSigRefHandler,
|
|
||||||
V2APIOptionsHandler,
|
|
||||||
V2APISpotHandler,
|
|
||||||
V2APISpotsHandler,
|
|
||||||
V2APISpotsStreamHandler,
|
|
||||||
V2APIStatusHandler,
|
|
||||||
)
|
|
||||||
from webserver.handlers.manifesthandler import ManifestHandler
|
from webserver.handlers.manifesthandler import ManifestHandler
|
||||||
from webserver.handlers.metrics import PrometheusMetricsHandler
|
from webserver.handlers.metrics import PrometheusMetricsHandler
|
||||||
from webserver.handlers.pagetemplate import PageTemplateHandler
|
from webserver.handlers.pagetemplate import PageTemplateHandler
|
||||||
@@ -169,7 +184,7 @@ class WebServer:
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
# v2 API compatibility routes
|
# v2 API compatibility routes. Translation wrappers convert data to the v3 format and back.
|
||||||
v2_compat_routes = [
|
v2_compat_routes = [
|
||||||
(
|
(
|
||||||
r"/api/v2/spots",
|
r"/api/v2/spots",
|
||||||
@@ -193,12 +208,12 @@ class WebServer:
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
r"/api/v2/solar",
|
r"/api/v2/solar",
|
||||||
APISolarConditionsHandler,
|
V2APISolarConditionsHandler,
|
||||||
{"solar_conditions": self._data_store.solar_conditions.get()},
|
{"solar_conditions": self._data_store.solar_conditions.get()},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
r"/api/v2/dxstats",
|
r"/api/v2/dxstats",
|
||||||
APIDxStatsHandler,
|
V2APIDxStatsHandler,
|
||||||
{"spots": self._data_store.spots},
|
{"spots": self._data_store.spots},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -211,9 +226,9 @@ class WebServer:
|
|||||||
V2APIStatusHandler,
|
V2APIStatusHandler,
|
||||||
{"status_data": self._data_store.status.get()},
|
{"status_data": self._data_store.status.get()},
|
||||||
),
|
),
|
||||||
(r"/api/v2/lookup/call", APILookupCallHandler),
|
(r"/api/v2/lookup/call", V2APILookupCallHandler),
|
||||||
(r"/api/v2/lookup/sigref", V2APILookupSigRefHandler),
|
(r"/api/v2/lookup/sigref", V2APILookupSigRefHandler),
|
||||||
(r"/api/v2/lookup/grid", APILookupGridHandler),
|
(r"/api/v2/lookup/grid", V2APILookupGridHandler),
|
||||||
(
|
(
|
||||||
r"/api/v2/spot",
|
r"/api/v2/spot",
|
||||||
V2APISpotHandler,
|
V2APISpotHandler,
|
||||||
@@ -224,27 +239,59 @@ class WebServer:
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
# v1 API redirects. Most v1 enpoints are unchanged in v2, and are proxied to the v2 API (which in turn is
|
# Translation wrappers convert data to the v3 format and back.
|
||||||
# translated from v3). The ones that have the major breaking changes get a bespoke handler.
|
|
||||||
v1_compat_routes = [
|
v1_compat_routes = [
|
||||||
(
|
(
|
||||||
r"/api/v1/spots",
|
r"/api/v1/spots",
|
||||||
V1APISpotsHandler,
|
V1APISpotsHandler,
|
||||||
{"spots": self._data_store.spots},
|
{"spots": self._data_store.spots},
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
r"/api/v1/alerts",
|
||||||
|
V1APIAlertsHandler,
|
||||||
|
{"alerts": self._data_store.alerts},
|
||||||
|
),
|
||||||
(
|
(
|
||||||
r"/api/v1/spots/stream",
|
r"/api/v1/spots/stream",
|
||||||
V1APISpotsStreamHandler,
|
V1APISpotsStreamHandler,
|
||||||
{"sse_spot_broadcaster": self._spot_broadcaster},
|
{"sse_spot_broadcaster": self._spot_broadcaster},
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
r"/api/v1/alerts/stream",
|
||||||
|
V1APIAlertsStreamHandler,
|
||||||
|
{"sse_alert_broadcaster": self._alert_broadcaster},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r"/api/v1/solar",
|
||||||
|
V1APISolarConditionsHandler,
|
||||||
|
{"solar_conditions": self._data_store.solar_conditions.get()},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r"/api/v1/dxstats",
|
||||||
|
V1APIDxStatsHandler,
|
||||||
|
{"spots": self._data_store.spots},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r"/api/v1/options",
|
||||||
|
V1APIOptionsHandler,
|
||||||
|
{"status_data": self._data_store.status.get()},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r"/api/v1/status",
|
||||||
|
V1APIStatusHandler,
|
||||||
|
{"status_data": self._data_store.status.get()},
|
||||||
|
),
|
||||||
|
(r"/api/v1/lookup/call", V1APILookupCallHandler),
|
||||||
|
(r"/api/v1/lookup/sigref", V1APILookupSigRefHandler),
|
||||||
|
(r"/api/v1/lookup/grid", V1APILookupGridHandler),
|
||||||
(
|
(
|
||||||
r"/api/v1/spot",
|
r"/api/v1/spot",
|
||||||
V1APISpotHandler,
|
V1APISpotHandler,
|
||||||
{
|
{
|
||||||
"spots": self._data_store.spots,
|
"spots": self._data_store.spots,
|
||||||
|
"spot_providers": self._data_providers,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(r"/api/v1/(.*)", V1RedirectHandler),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# If in API-only mode, serve a basic homepage; in normal mode, serve the usual UI routes
|
# If in API-only mode, serve a basic homepage; in normal mode, serve the usual UI routes
|
||||||
|
|||||||
Reference in New Issue
Block a user