Add "fields" query parameter to spots and alerts API calls. Closes #140

This commit is contained in:
Ian Renton
2026-09-10 07:54:34 +01:00
parent 0fcc459cc4
commit be625c40a4
11 changed files with 77 additions and 15 deletions
+19
View File
@@ -58,8 +58,12 @@ class APIAlertsHandler(tornado.web.RequestHandler):
# Fetch all alerts matching the query, then optionally enrich with online data # Fetch all alerts matching the query, then optionally enrich with online data
credentials = extract_credentials(self.request.headers) credentials = extract_credentials(self.request.headers)
data = get_alert_list_with_filters(self._alerts, query_params) data = get_alert_list_with_filters(self._alerts, query_params)
fields = [f.strip() for f in query_params["fields"].split(",")] if "fields" in query_params else []
if credentials: if credentials:
data = self._enrich(data, credentials) data = self._enrich(data, credentials)
# Filter for only the required fields, if necessary
if fields:
data = filter_fields(data, fields)
self.write(safe_json_dumps(data)) self.write(safe_json_dumps(data))
self.set_status(200) self.set_status(200)
except ValueError as e: except ValueError as e:
@@ -81,6 +85,7 @@ class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
self._web_server_metrics = None self._web_server_metrics = None
self._query_params = None self._query_params = None
self._credentials = None self._credentials = None
self._fields = None
super().__init__(application, request, **kwargs) super().__init__(application, request, **kwargs)
def initialize(self, sse_alert_broadcaster, web_server_metrics): def initialize(self, sse_alert_broadcaster, web_server_metrics):
@@ -104,6 +109,9 @@ class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
# reduce that to just the first entry, and convert bytes to string # reduce that to just the first entry, and convert bytes to string
self._query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()} self._query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()}
self._credentials = extract_credentials(self.request.headers) self._credentials = extract_credentials(self.request.headers)
self._fields = (
[f.strip() for f in self._query_params["fields"].split(",")] if "fields" in self._query_params else []
)
# Flush headers immediately so nginx doesn't time out waiting for a response # Flush headers immediately so nginx doesn't time out waiting for a response
self.write_message("keepalive", "") self.write_message("keepalive", "")
@@ -126,10 +134,15 @@ class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
"""Callback when a new alert arrives""" """Callback when a new alert arrives"""
try: try:
# If the new alert matches our param filters, send it to the client. If not, ignore it.
if alert_allowed_by_query(alert, self._query_params): if alert_allowed_by_query(alert, self._query_params):
# Add lookup data if we have credentials
if self._credentials: if self._credentials:
alert = copy.deepcopy(alert) alert = copy.deepcopy(alert)
alert.infer_missing(self._credentials) alert.infer_missing(self._credentials)
# Filter fields returned if necessary
if self._fields:
alert = filter_fields([alert], self._fields)[0]
self.write_message(msg=safe_json_dumps(alert)) self.write_message(msg=safe_json_dumps(alert))
except Exception: except Exception:
logger.exception("Exception in SSE callback, connection will be closed") logger.exception("Exception in SSE callback, connection will be closed")
@@ -216,3 +229,9 @@ def alert_allowed_by_query(alert, query):
): ):
return False return False
return True return True
def filter_fields(alerts, fields):
"""Given a list of alert objects, return copies containing only the named fields."""
return [{k: v for k, v in alert.__dict__.items() if k in fields} for alert in alerts]
+18
View File
@@ -56,9 +56,13 @@ class APISpotsHandler(tornado.web.RequestHandler):
# Fetch all spots matching the query, then optionally enrich with online data # Fetch all spots matching the query, then optionally enrich with online data
credentials = extract_credentials(self.request.headers) credentials = extract_credentials(self.request.headers)
fields = [f.strip() for f in query_params["fields"].split(",")] if "fields" in query_params else []
data = get_spot_list_with_filters(self._spots, query_params) data = get_spot_list_with_filters(self._spots, query_params)
if credentials: if credentials:
data = self._enrich(data, credentials) data = self._enrich(data, credentials)
# Filter for only the required fields, if necessary
if fields:
data = filter_fields(data, fields)
self.write(safe_json_dumps(data)) self.write(safe_json_dumps(data))
self.set_status(200) self.set_status(200)
except ValueError as e: except ValueError as e:
@@ -80,6 +84,7 @@ class APISpotsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
self._web_server_metrics = None self._web_server_metrics = None
self._query_params = None self._query_params = None
self._credentials = None self._credentials = None
self._fields = None
super().__init__(application, request, **kwargs) super().__init__(application, request, **kwargs)
def initialize(self, sse_spot_broadcaster, web_server_metrics): def initialize(self, sse_spot_broadcaster, web_server_metrics):
@@ -105,6 +110,9 @@ class APISpotsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
# reduce that to just the first entry, and convert bytes to string # reduce that to just the first entry, and convert bytes to string
self._query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()} self._query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()}
self._credentials = extract_credentials(self.request.headers) self._credentials = extract_credentials(self.request.headers)
self._fields = (
[f.strip() for f in self._query_params["fields"].split(",")] if "fields" in self._query_params else []
)
# Flush headers immediately so nginx doesn't time out waiting for a response # Flush headers immediately so nginx doesn't time out waiting for a response
self.write_message("keepalive", "") self.write_message("keepalive", "")
@@ -129,9 +137,13 @@ class APISpotsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
try: try:
# If the new spot matches our param filters, send it to the client. If not, ignore it. # If the new spot matches our param filters, send it to the client. If not, ignore it.
if spot_allowed_by_query(spot, self._query_params): if spot_allowed_by_query(spot, self._query_params):
# Add lookup data if we have credentials
if self._credentials: if self._credentials:
spot = copy.deepcopy(spot) spot = copy.deepcopy(spot)
spot.infer_missing(self._credentials) spot.infer_missing(self._credentials)
# Filter fields returned if necessary
if self._fields:
spot = filter_fields([spot], self._fields)[0]
self.write_message(msg=safe_json_dumps(spot)) self.write_message(msg=safe_json_dumps(spot))
except Exception: except Exception:
logger.exception("Exception in SSE callback, connection will be closed") logger.exception("Exception in SSE callback, connection will be closed")
@@ -266,3 +278,9 @@ def spot_allowed_by_query(spot, query):
if needs_good_location and not spot.dx_location_good: if needs_good_location and not spot.dx_location_good:
return False return False
return True return True
def filter_fields(spots, fields):
"""Given a list of spot objects, return copies containing only the named fields."""
return [{k: v for k, v in spot.__dict__.items() if k in fields} for spot in spots]
+25
View File
@@ -24,6 +24,7 @@ info:
* Added `alert_type` and `url` to alert data * Added `alert_type` and `url` to alert data
* Added `contests_skip_max_duration_check` to alert query parameters * Added `contests_skip_max_duration_check` to alert query parameters
* SIG reference types (e.g. "Park") are now capitalised to match other enums * SIG reference types (e.g. "Park") are now capitalised to match other enums
* Added the ability to get only certain fields of spots and alerts from the API by using the `fields` query parameter.
### 2.0 ### 2.0
@@ -140,6 +141,7 @@ paths:
- $ref: '#/components/parameters/SpotTextIncludes' - $ref: '#/components/parameters/SpotTextIncludes'
- $ref: '#/components/parameters/SpotNeedsGoodLocation' - $ref: '#/components/parameters/SpotNeedsGoodLocation'
- $ref: '#/components/parameters/SpotAllowQrt' - $ref: '#/components/parameters/SpotAllowQrt'
- $ref: '#/components/parameters/SpotFields'
- $ref: '#/components/parameters/QrzUsername' - $ref: '#/components/parameters/QrzUsername'
- $ref: '#/components/parameters/QrzPassword' - $ref: '#/components/parameters/QrzPassword'
- $ref: '#/components/parameters/QrzSessionKey' - $ref: '#/components/parameters/QrzSessionKey'
@@ -180,6 +182,7 @@ paths:
- $ref: '#/components/parameters/SpotTextIncludes' - $ref: '#/components/parameters/SpotTextIncludes'
- $ref: '#/components/parameters/SpotNeedsGoodLocation' - $ref: '#/components/parameters/SpotNeedsGoodLocation'
- $ref: '#/components/parameters/SpotAllowQrt' - $ref: '#/components/parameters/SpotAllowQrt'
- $ref: '#/components/parameters/SpotFields'
- $ref: '#/components/parameters/QrzUsername' - $ref: '#/components/parameters/QrzUsername'
- $ref: '#/components/parameters/QrzPassword' - $ref: '#/components/parameters/QrzPassword'
- $ref: '#/components/parameters/QrzSessionKey' - $ref: '#/components/parameters/QrzSessionKey'
@@ -217,6 +220,7 @@ paths:
- $ref: '#/components/parameters/AlertDxContinent' - $ref: '#/components/parameters/AlertDxContinent'
- $ref: '#/components/parameters/AlertDxCallIncludes' - $ref: '#/components/parameters/AlertDxCallIncludes'
- $ref: '#/components/parameters/AlertTextIncludes' - $ref: '#/components/parameters/AlertTextIncludes'
- $ref: '#/components/parameters/AlertFields'
- $ref: '#/components/parameters/QrzUsername' - $ref: '#/components/parameters/QrzUsername'
- $ref: '#/components/parameters/QrzPassword' - $ref: '#/components/parameters/QrzPassword'
- $ref: '#/components/parameters/QrzSessionKey' - $ref: '#/components/parameters/QrzSessionKey'
@@ -252,6 +256,7 @@ paths:
- $ref: '#/components/parameters/AlertDxContinent' - $ref: '#/components/parameters/AlertDxContinent'
- $ref: '#/components/parameters/AlertDxCallIncludes' - $ref: '#/components/parameters/AlertDxCallIncludes'
- $ref: '#/components/parameters/AlertTextIncludes' - $ref: '#/components/parameters/AlertTextIncludes'
- $ref: '#/components/parameters/AlertFields'
- $ref: '#/components/parameters/QrzUsername' - $ref: '#/components/parameters/QrzUsername'
- $ref: '#/components/parameters/QrzPassword' - $ref: '#/components/parameters/QrzPassword'
- $ref: '#/components/parameters/QrzSessionKey' - $ref: '#/components/parameters/QrzSessionKey'
@@ -657,6 +662,16 @@ components:
schema: schema:
type: boolean type: boolean
default: true default: true
SpotFields:
name: fields
in: query
description: >
Filter the fields you receive in each spot, to conserve data bandwidth for constrained applications.
Supply a comma-separated list of the fields you want to receive, using the names of the fields returned by the
`/spots` call, e.g. `id,dx_call,freq,mode,time`. If the "fields" parameter is not supplied, all fields will be
included in the spot data.
schema:
type: string
AlertMaxDuration: AlertMaxDuration:
name: max_duration name: max_duration
in: query in: query
@@ -795,6 +810,16 @@ components:
you will get all the more recent alerts back, without duplicating the previous latest spot. you will get all the more recent alerts back, without duplicating the previous latest spot.
schema: schema:
type: number type: number
AlertFields:
name: fields
in: query
description: >
Filter the fields you receive in each alert, to conserve data bandwidth for constrained applications.
Supply a comma-separated list of the fields you want to receive, using the names of the fields returned by the
`/alerts` call, e.g. `id,dx_calls,freqs_modes,start_time`. If the "fields" parameter is not supplied, all fields
will be included in the alert data.
schema:
type: string
CallParam: CallParam:
name: call name: call
in: query in: query
+1 -1
View File
@@ -77,7 +77,7 @@
</div> </div>
<script src="/static/js/add-spot.js?v=1788600259"></script> <script src="/static/js/add-spot.js?v=1789023274"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-add-spot").addClass("active"); $("#nav-link-add-spot").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+1 -1
View File
@@ -83,7 +83,7 @@
</div> </div>
<script src="/static/js/alerts.js?v=1788600259"></script> <script src="/static/js/alerts.js?v=1789023274"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-alerts").addClass("active"); $("#nav-link-alerts").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -76,8 +76,8 @@
</div> </div>
<script src="/static/js/spotsbandsandmap.js?v=1788600259"></script> <script src="/static/js/spotsbandsandmap.js?v=1789023274"></script>
<script src="/static/js/bands.js?v=1788600259"></script> <script src="/static/js/bands.js?v=1789023274"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-bands").addClass("active"); $("#nav-link-bands").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+5 -5
View File
@@ -1,6 +1,6 @@
{% extends "skeleton.html" %} {% extends "skeleton.html" %}
{% block head_extra %} {% block head_extra %}
<link rel="stylesheet" href="/static/css/style.css?v=1788600258" type="text/css"> <link rel="stylesheet" href="/static/css/style.css?v=1789023274" type="text/css">
<link href="/static/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet"> <link href="/static/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet">
<link href="/static/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet"> <link href="/static/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet">
<link href="/static/vendor/css/solid-6.7.2.min.css" rel="stylesheet"> <link href="/static/vendor/css/solid-6.7.2.min.css" rel="stylesheet">
@@ -16,10 +16,10 @@
window.fetchEventSource = fetchEventSource; window.fetchEventSource = fetchEventSource;
</script> </script>
<script src="/static/js/utils.js?v=1788600258"></script> <script src="/static/js/utils.js?v=1789023274"></script>
<script src="/static/js/ui-ham.js?v=1788600258"></script> <script src="/static/js/ui-ham.js?v=1789023274"></script>
<script src="/static/js/geo.js?v=1788600258"></script> <script src="/static/js/geo.js?v=1789023274"></script>
<script src="/static/js/common.js?v=1788600258"></script> <script src="/static/js/common.js?v=1789023274"></script>
{% end %} {% end %}
{% block body %} {% block body %}
<div class="container"> <div class="container">
+1 -1
View File
@@ -284,7 +284,7 @@
</div> </div>
<script src="/static/vendor/js/chart-4.4.9.umd.min.js"></script> <script src="/static/vendor/js/chart-4.4.9.umd.min.js"></script>
<script src="/static/js/conditions.js?v=1788600259"></script> <script src="/static/js/conditions.js?v=1789023274"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-conditions").addClass("active"); $("#nav-link-conditions").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -113,8 +113,8 @@
const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}"; const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}";
</script> </script>
<script src="/static/js/spotsbandsandmap.js?v=1788600259"></script> <script src="/static/js/spotsbandsandmap.js?v=1789023274"></script>
<script src="/static/js/map.js?v=1788600259"></script> <script src="/static/js/map.js?v=1789023274"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-map").addClass("active"); $("#nav-link-map").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -113,8 +113,8 @@
</div> </div>
<script src="/static/js/spotsbandsandmap.js?v=1788600258"></script> <script src="/static/js/spotsbandsandmap.js?v=1789023274"></script>
<script src="/static/js/spots.js?v=1788600258"></script> <script src="/static/js/spots.js?v=1789023274"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-spots").addClass("active"); $("#nav-link-spots").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+1 -1
View File
@@ -86,7 +86,7 @@
</div> </div>
</div> </div>
<script src="/static/js/status.js?v=1788600259"></script> <script src="/static/js/status.js?v=1789023274"></script>
<script> <script>
$(document).ready(function () { $(document).ready(function () {
$("#nav-link-status").addClass("active"); $("#nav-link-status").addClass("active");