Starting to implement alerts #17

This commit is contained in:
Ian Renton
2025-10-04 18:09:54 +01:00
parent 55893949b8
commit 74153a9d94
29 changed files with 552 additions and 109 deletions

View File

@@ -2,7 +2,6 @@ import json
import logging
from datetime import datetime, timedelta
from threading import Thread
from types import SimpleNamespace
import bottle
import pytz
@@ -30,11 +29,13 @@ class WebServer:
# Routes for API calls
bottle.get("/api/spots")(lambda: self.serve_api(self.get_spot_list_with_filters()))
bottle.get("/api/alerts")(lambda: self.serve_api(self.get_alert_list_with_filters()))
bottle.get("/api/options")(lambda: self.serve_api(self.get_options()))
bottle.get("/api/status")(lambda: self.serve_api(self.status_data))
bottle.post("/api/spot")(lambda: self.accept_spot())
# Routes for templated pages
bottle.get("/")(lambda: self.serve_template('webpage_spots'))
bottle.get("/alerts")(lambda: self.serve_template('webpage_alerts'))
bottle.get("/about")(lambda: self.serve_template('webpage_about'))
bottle.get("/apidocs")(lambda: self.serve_template('webpage_apidocs'))
# Default route to serve from "webassets"
@@ -90,7 +91,7 @@ class WebServer:
spot.source = "API"
spot.icon = "desktop"
spot.infer_missing()
self.spots.add(spot.guid, spot, expire=MAX_SPOT_AGE)
self.spots.add(spot.id, spot, expire=MAX_SPOT_AGE)
response.content_type = 'application/json'
response.set_header('Cache-Control', 'no-store')
@@ -114,7 +115,7 @@ class WebServer:
# Utility method to apply filters to the overall spot list and return only a subset. Enables query parameters in
# the main "spots" GET call.
def get_spot_list_with_filters(self):
# Get the query (and the right one, with Bottle magic. This is a MultiDict object
# Get the query (and the right one, with Bottle magic. This is a MultiDict object)
query = bottle.request.query
# Create a shallow copy of the spot list, ordered by spot time. We'll then filter it accordingly.
@@ -124,9 +125,9 @@ class WebServer:
# value or a comma-separated list.
# We can provide a "limit" number as well. Spots are always returned newest-first; "limit" limits to only the
# most recent X spots.
spot_guids = list(self.spots.iterkeys())
spot_ids = list(self.spots.iterkeys())
spots = []
for k in spot_guids:
for k in spot_ids:
spots.append(self.spots.get(k))
spots = sorted(spots, key=lambda spot: spot.time, reverse=True)
for k in query.keys():
@@ -167,6 +168,43 @@ class WebServer:
spots = spots[:int(query.get("limit"))]
return spots
# Utility method to apply filters to the overall alert list and return only a subset. Enables query parameters in
# the main "alerts" GET call.
def get_alert_list_with_filters(self):
# Get the query (and the right one, with Bottle magic. This is a MultiDict object)
query = bottle.request.query
# Create a shallow copy of the alert list, ordered by alert time. We'll then filter it accordingly.
# We can filter by received time with "received_since", which take a UNIX timestamp in seconds UTC.
# We can also filter by source, sig, and dx_continent. Each of these accepts a single
# value or a comma-separated list.
# We can provide a "limit" number as well. Alerts are always returned newest-first; "limit" limits to only the
# most recent X alerts.
alert_ids = list(self.spots.iterkeys())
alerts = []
for k in alert_ids:
alerts.append(self.spots.get(k))
alerts = sorted(alerts, key=lambda spot: spot.time, reverse=True)
for k in query.keys():
match k:
case "received_since":
since = datetime.fromtimestamp(int(query.get(k)), pytz.UTC)
alerts = [s for s in alerts if s.received_time > since]
case "source":
sources = query.get(k).split(",")
alerts = [s for s in alerts if s.source in sources]
case "sig":
sigs = query.get(k).split(",")
alerts = [s for s in alerts if s.sig in sigs]
case "dx_continent":
dxconts = query.get(k).split(",")
alerts = [s for s in alerts if s.dx_continent in dxconts]
# If we have a "limit" parameter, we apply that last, regardless of where it appeared in the list of keys.
if "limit" in query.keys():
alerts = alerts[:int(query.get("limit"))]
return alerts
# Return all the "options" for various things that the server is aware of. This can be fetched with an API call.
# The idea is that this will include most of the things that can be provided as queries to the main spots call,
# and thus a client can use this data to configure its filter controls.
@@ -175,7 +213,8 @@ class WebServer:
"modes": ALL_MODES,
"mode_types": MODE_TYPES,
"sigs": SIGS,
# Sources are filtered for only ones that are enabled in config, no point letting the user toggle things that aren't even available.
"sources": list(map(lambda p: p["name"], filter(lambda p: p["enabled"], self.status_data["providers"]))),
# Spot/alert sources are filtered for only ones that are enabled in config, no point letting the user toggle things that aren't even available.
"spot_sources": list(map(lambda p: p["name"], filter(lambda p: p["enabled"], self.status_data["spot_providers"]))),
"alert_sources": list(map(lambda p: p["name"], filter(lambda p: p["enabled"], self.status_data["alert_providers"]))),
"continents": CONTINENTS,
"max_spot_age": MAX_SPOT_AGE}