mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-09-20 14:27:42 +00:00
Add "fields" query parameter to spots and alerts API calls. Closes #140
This commit is contained in:
@@ -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]
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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">
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
Reference in New Issue
Block a user