Partial fix for mypy issues

This commit is contained in:
Ian Renton
2026-09-20 20:42:16 +01:00
parent 93ea27510f
commit 203758fa2d
25 changed files with 244 additions and 147 deletions
+8 -2
View File
@@ -144,13 +144,14 @@ class APISpotHandler(tornado.web.RequestHandler):
return
# Reject if activity ref format incorrect for activity
ref_regex = get_ref_regex_for_activity(spot.sig) if spot.sig else None
if (
spot.sig
and spot.sig_refs
and len(spot.sig_refs) > 0
and spot.sig_refs[0].id
and get_ref_regex_for_activity(spot.sig)
and not re.match(get_ref_regex_for_activity(spot.sig), spot.sig_refs[0].id)
and ref_regex
and not re.match(ref_regex, spot.sig_refs[0].id)
):
self.set_status(422)
self.write(
@@ -202,6 +203,8 @@ class APISpotHandler(tornado.web.RequestHandler):
# Submit upstream if requested
upstream_warning = None
if submit_upstream and upstream_provider_name:
# spot.sig was already validated non-empty above, under the same submit_upstream/upstream_provider_name gate
assert spot.sig is not None
provider = self._find_provider(upstream_provider_name, spot.sig)
if provider:
try:
@@ -224,6 +227,8 @@ class APISpotHandler(tornado.web.RequestHandler):
# we were but it failed, we should still add it to our database anyway.
if not submit_upstream or upstream_warning:
spot.infer_missing()
assert self._spots is not None, "initialize() must be called before post()"
assert spot.id is not None, "infer_missing() always assigns an id"
self._spots.set(spot.id, spot)
if upstream_warning:
@@ -245,6 +250,7 @@ class APISpotHandler(tornado.web.RequestHandler):
def _find_provider(self, provider_name: str, activity: str) -> SpotProvider | None:
"""Find an enabled provider by name that can submit spots for the given activity."""
assert self._spot_providers is not None, "initialize() must be called before _find_provider()"
for p in self._spot_providers:
if p.enabled and p.name == provider_name and p.can_submit_spot(activity):
return p
+21 -19
View File
@@ -51,13 +51,13 @@ class APIAlertsHandler(tornado.web.RequestHandler):
# Fetch all alerts matching the query, then optionally enrich with online data
credentials = extract_credentials(self.request.headers)
data = get_alert_list_with_filters(self._alerts, query_params)
assert self._alerts is not None, "initialize() must be called before get()"
alerts = 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:
data = self._enrich(data, credentials)
alerts = self._enrich(alerts, credentials)
# Filter for only the required fields, if necessary
if fields:
data = filter_fields(data, fields)
data: list[Alert] | list[dict[str, Any]] = filter_fields(alerts, fields) if fields else alerts
self.write(safe_json_dumps(data))
self.set_status(200)
except ValueError as e:
@@ -104,6 +104,7 @@ class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
# Register to handle new alerts arriving. The callback() method will get called with the new alert as an
# argument.
assert self._sse_alert_broadcaster is not None, "initialize() must be called before open()"
self._sse_alert_broadcaster.register(self)
except Exception:
@@ -113,6 +114,7 @@ class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
def close(self) -> None:
"""When the user closes the socket, deregister ourselves from the alert broadcaster"""
assert self._sse_alert_broadcaster is not None, "initialize() must be called before close()"
self._sse_alert_broadcaster.unregister(self)
super().close()
@@ -121,15 +123,15 @@ class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
try:
# If the new alert matches our param filters, send it to the client. If not, ignore it.
assert self._query_params is not None, "open() must be called before callback()"
if alert_allowed_by_query(alert, self._query_params):
# Add lookup data if we have credentials
if self._credentials:
alert = copy.deepcopy(alert)
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))
output: Alert | dict[str, Any] = filter_fields([alert], self._fields)[0] if self._fields else alert
self.write_message(msg=safe_json_dumps(output))
except Exception:
logger.exception("Exception in SSE callback, connection will be closed")
self.close()
@@ -151,7 +153,7 @@ def get_alert_list_with_filters(all_alerts: LiveDataCache, query: dict[str, str]
alerts = sorted(alerts, key=lambda alert: alert.start_time if alert and alert.start_time else 0)
alerts = list(filter(lambda alert: alert_allowed_by_query(alert, query), alerts))
if "limit" in query:
alerts = alerts[: int(query.get("limit"))]
alerts = alerts[: int(query["limit"])]
return alerts
@@ -162,11 +164,11 @@ def alert_allowed_by_query(alert: Alert, query: dict[str, str]) -> bool:
for k in query:
match k:
case "received_since":
since = datetime.fromtimestamp(float(query.get(k)), pytz.UTC)
since = datetime.fromtimestamp(float(query[k]), pytz.UTC).timestamp()
if not alert.received_time or alert.received_time <= since:
return False
case "max_duration":
max_duration = int(query.get(k))
max_duration = int(query[k])
# Check the duration if end_time is provided. If end_time is not provided, assume the activation is
# "short", i.e. it always passes this check. If dxpeditions_skip_max_duration_check is true and
# the alert is a dxpedition, or contests_skip_max_duration_check and the alert is a contest, it also
@@ -174,42 +176,42 @@ def alert_allowed_by_query(alert: Alert, query: dict[str, str]) -> bool:
if (
alert.sig == ActivityName.DXPEDITION
and "dxpeditions_skip_max_duration_check" in query
and query.get("dxpeditions_skip_max_duration_check").upper() == "TRUE"
and query["dxpeditions_skip_max_duration_check"].upper() == "TRUE"
):
continue
if (
alert.sig == ActivityName.CONTEST
and "contests_skip_max_duration_check" in query
and query.get("contests_skip_max_duration_check").upper() == "TRUE"
and query["contests_skip_max_duration_check"].upper() == "TRUE"
):
continue
if alert.end_time and alert.start_time and alert.end_time - alert.start_time > max_duration:
return False
case "source":
sources = query.get(k).split(",")
sources = query[k].split(",")
if not alert.source or alert.source not in sources:
return False
case "sig":
# If a list of activities is provided, the alert must have an activity and it must match one of them.
# The special activity "NO_SIG", when supplied in the list, matches alerts with no activity.
activities = query.get(k).split(",")
activities = query[k].split(",")
include_no_activity = "NO_SIG" in activities
if not alert.sig and not include_no_activity:
return False
if alert.sig and alert.sig not in activities:
return False
case "dx_continent":
dxconts = query.get(k).split(",")
dxconts = query[k].split(",")
if not alert.dx_continent or alert.dx_continent not in dxconts:
return False
case "dx_call_includes":
dx_call_includes = query.get(k).strip()
if not alert.dx_call or dx_call_includes.upper() not in alert.dx_call.upper():
dx_call_includes = query[k].strip()
if not alert.dx_calls or not any(dx_call_includes.upper() in c.upper() for c in alert.dx_calls if c):
return False
case "text_includes":
text_includes = query.get(k).strip()
text_includes = query[k].strip()
if (
(not alert.dx_call or text_includes.upper() not in alert.dx_call.upper())
(not alert.dx_calls or not any(text_includes.upper() in c.upper() for c in alert.dx_calls if c))
and (not alert.comment or text_includes.upper() not in alert.comment.upper())
and (not alert.freqs_modes or text_includes.upper() not in alert.freqs_modes.upper())
):
+2 -1
View File
@@ -37,8 +37,9 @@ class APIDxStatsHandler(tornado.web.RequestHandler):
def get(self) -> None:
try:
assert self._spots is not None, "initialize() must be called before get()"
one_hour_ago = (datetime.now(pytz.UTC) - timedelta(hours=1)).timestamp()
counts = Counter()
counts: Counter[tuple[str, str, str]] = Counter()
for key in self._spots.keys(): # noqa: SIM118
spot = self._spots.get(key)
+2 -3
View File
@@ -85,9 +85,8 @@ class APILookupActivityRefHandler(tornado.web.RequestHandler):
activity = str(query_params.get("sig")).upper()
ref_id = str(query_params.get("id")).upper()
if get_activity_by_name(activity):
if not get_ref_regex_for_activity(activity) or re.match(
get_ref_regex_for_activity(activity), ref_id
):
ref_regex = get_ref_regex_for_activity(activity)
if not ref_regex or re.match(ref_regex, ref_id):
data = populate_missing_activity_ref_info(ActivityRef(id=ref_id, sig=activity))
self.write(safe_json_dumps(data))
+11 -17
View File
@@ -34,8 +34,10 @@ class APIOptionsHandler(tornado.web.RequestHandler):
def get(self) -> None:
try:
assert self._status_data is not None, "initialize() must be called before get()"
# Build a map of activity name -> list of provider names that can submit spots for that activity
spot_submit_providers = {}
spot_submit_providers: dict[str, list[str]] = {}
# Spothole v2.0 - disable this for now, API changes are in but this functionality is not ready yet. TODO
# for provider in self._spot_providers:
@@ -47,23 +49,15 @@ class APIOptionsHandler(tornado.web.RequestHandler):
# 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_providers: list = [
p["name"] for p in filter(lambda p: p["enabled"], self._status_data["spot_providers"])
]
alert_providers = [p["name"] for p in filter(lambda p: p["enabled"], self._status_data["alert_providers"])]
callsign_data_providers = [
p["name"]
for p in filter(
lambda p: p["enabled"],
self._status_data["callsign_data_providers"],
)
]
spot_provider_configs: list[dict[str, Any]] = self._status_data["spot_providers"]
alert_provider_configs: list[dict[str, Any]] = self._status_data["alert_providers"]
callsign_data_provider_configs: list[dict[str, Any]] = self._status_data["callsign_data_providers"]
spot_providers: list = [p["name"] for p in spot_provider_configs if p["enabled"]]
alert_providers = [p["name"] for p in alert_provider_configs if p["enabled"]]
callsign_data_providers = [p["name"] for p in callsign_data_provider_configs if p["enabled"]]
spot_providers_enabled_by_default = [
p["name"]
for p in filter(
lambda p: p["enabled"] and p["enabled_by_default_in_web_ui"],
self._status_data["spot_providers"],
)
p["name"] for p in spot_provider_configs if p["enabled"] and p["enabled_by_default_in_web_ui"]
]
# If spotting to this server is enabled, "API" is another valid spot source even though it does not come from
@@ -28,6 +28,7 @@ class APISolarConditionsHandler(tornado.web.RequestHandler):
def get(self) -> None:
try:
assert self._solar_conditions is not None, "initialize() must be called before get()"
self.write(self._solar_conditions.to_json())
self.set_status(200)
self.set_header("Cache-Control", "no-store")
+28 -26
View File
@@ -51,12 +51,12 @@ class APISpotsHandler(tornado.web.RequestHandler):
# Fetch all spots matching the query, then optionally enrich with online data
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)
assert self._spots is not None, "initialize() must be called before get()"
spots = get_spot_list_with_filters(self._spots, query_params)
if credentials:
data = self._enrich(data, credentials)
spots = self._enrich(spots, credentials)
# Filter for only the required fields, if necessary
if fields:
data = filter_fields(data, fields)
data: list[Spot] | list[dict[str, Any]] = filter_fields(spots, fields) if fields else spots
self.write(safe_json_dumps(data))
self.set_status(200)
except ValueError as e:
@@ -105,6 +105,7 @@ class APISpotsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
# Register to handle new spots arriving. The callback() method will get called with the new spot as an
# argument.
assert self._sse_spot_broadcaster is not None, "initialize() must be called before open()"
self._sse_spot_broadcaster.register(self)
except Exception:
@@ -114,6 +115,7 @@ class APISpotsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
def close(self) -> None:
"""When the user closes the socket, deregister ourselves from the spot broadcaster"""
assert self._sse_spot_broadcaster is not None, "initialize() must be called before close()"
self._sse_spot_broadcaster.unregister(self)
super().close()
@@ -122,15 +124,15 @@ class APISpotsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
try:
# If the new spot matches our param filters, send it to the client. If not, ignore it.
assert self._query_params is not None, "open() must be called before callback()"
if spot_allowed_by_query(spot, self._query_params):
# Add lookup data if we have credentials
if self._credentials:
spot = copy.deepcopy(spot)
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))
output: Spot | dict[str, Any] = filter_fields([spot], self._fields)[0] if self._fields else spot
self.write_message(msg=safe_json_dumps(output))
except Exception:
logger.exception("Exception in SSE callback, connection will be closed")
self.close()
@@ -152,7 +154,7 @@ def get_spot_list_with_filters(all_spots: LiveDataCache, query: dict[str, str])
spots = sorted(spots, key=lambda spot: spot.time if spot and spot.time else 0, reverse=True)
spots = list(filter(lambda spot: spot_allowed_by_query(spot, query), spots))
if "limit" in query:
spots = spots[: int(query.get("limit"))]
spots = spots[: int(query["limit"])]
# Ensure only the latest spot of each callsign-SSID combo is present in the list. This relies on the
# list being in reverse time order, so if any future change allows re-ordering the list, that should
@@ -162,7 +164,7 @@ def get_spot_list_with_filters(all_spots: LiveDataCache, query: dict[str, str])
# duplicates are fine in the main spot list (e.g. different cluster spots of the same DX) this doesn't
# work well for the other views.
if "dedupe" in query:
dedupe = query.get("dedupe").upper() == "TRUE"
dedupe = query["dedupe"].upper() == "TRUE"
if dedupe:
spots_temp = []
already_seen = []
@@ -183,26 +185,26 @@ def spot_allowed_by_query(spot: Spot, query: dict[str, str]) -> bool:
for k in query:
match k:
case "since":
since = datetime.fromtimestamp(int(query.get(k)), pytz.UTC).timestamp()
since = datetime.fromtimestamp(int(query[k]), pytz.UTC).timestamp()
if not spot.time or spot.time <= since:
return False
case "max_age":
max_age = int(query.get(k))
max_age = int(query[k])
since = (datetime.now(pytz.UTC) - timedelta(seconds=max_age)).timestamp()
if not spot.time or spot.time <= since:
return False
case "received_since":
since = datetime.fromtimestamp(float(query.get(k)), pytz.UTC).timestamp()
since = datetime.fromtimestamp(float(query[k]), pytz.UTC).timestamp()
if not spot.received_time or spot.received_time <= since:
return False
case "source":
sources = query.get(k).split(",")
sources = query[k].split(",")
if not spot.source or spot.source not in sources:
return False
case "sig":
# If a list of activities is provided, the spot must have an activity and it must match one of them.
# The special activity "NO_SIG", when supplied in the list, matches spots with no activity.
activities = query.get(k).split(",")
activities = query[k].split(",")
include_no_activity = "NO_SIG" in activities
if not spot.sig and not include_no_activity:
return False
@@ -211,56 +213,56 @@ def spot_allowed_by_query(spot: Spot, query: dict[str, str]) -> bool:
case "needs_sig":
# If true, an activity is required, regardless of what it is, it just can't be missing. Mutually
# exclusive with supplying the special "NO_SIG" parameter to the "sig" query param.
needs_activity = query.get(k).upper() == "TRUE"
needs_activity = query[k].upper() == "TRUE"
if needs_activity and not spot.sig:
return False
case "needs_sig_ref":
# If true, at least one activity ref is required, regardless of what it is, it just can't be missing.
needs_activity_ref = query.get(k).upper() == "TRUE"
needs_activity_ref = query[k].upper() == "TRUE"
if needs_activity_ref and (not spot.sig_refs or len(spot.sig_refs) == 0):
return False
case "band":
bands = query.get(k).split(",")
bands = query[k].split(",")
if not spot.band or spot.band not in bands:
return False
case "mode":
modes = query.get(k).split(",")
modes = query[k].split(",")
if not spot.mode or spot.mode not in modes:
return False
case "mode_type":
mode_types = query.get(k).split(",")
mode_types = query[k].split(",")
if not spot.mode_type or spot.mode_type not in mode_types:
return False
case "dx_continent":
dxconts = query.get(k).split(",")
dxconts = query[k].split(",")
if not spot.dx_continent or spot.dx_continent not in dxconts:
return False
case "de_continent":
deconts = query.get(k).split(",")
deconts = query[k].split(",")
if not spot.de_continent or spot.de_continent not in deconts:
return False
case "comment_includes":
comment_includes = query.get(k).strip()
comment_includes = query[k].strip()
if not spot.comment or comment_includes.upper() not in spot.comment.upper():
return False
case "dx_call_includes":
dx_call_includes = query.get(k).strip()
dx_call_includes = query[k].strip()
if not spot.dx_call or dx_call_includes.upper() not in spot.dx_call.upper():
return False
case "text_includes":
text_includes = query.get(k).strip()
text_includes = query[k].strip()
if (not spot.dx_call or text_includes.upper() not in spot.dx_call.upper()) and (
not spot.comment or text_includes.upper() not in spot.comment.upper()
):
return False
case "allow_qrt":
# If false, spots that are flagged as QRT are not returned.
prevent_qrt = query.get(k).upper() == "FALSE"
prevent_qrt = query[k].upper() == "FALSE"
if prevent_qrt and spot.qrt:
return False
case "needs_good_location":
# If true, spots require a "good" location to be returned
needs_good_location = query.get(k).upper() == "TRUE"
needs_good_location = query[k].upper() == "TRUE"
if needs_good_location and not spot.dx_location_good:
return False
return True
+5 -2
View File
@@ -106,13 +106,14 @@ class V1APISpotHandler(tornado.web.RequestHandler):
return
# Reject if activity ref format incorrect for activity
ref_regex = get_ref_regex_for_activity(spot.sig) if spot.sig else None
if (
spot.sig
and spot.sig_refs
and len(spot.sig_refs) > 0
and spot.sig_refs[0].id
and get_ref_regex_for_activity(spot.sig)
and not re.match(get_ref_regex_for_activity(spot.sig), spot.sig_refs[0].id)
and ref_regex
and not re.match(ref_regex, spot.sig_refs[0].id)
):
self.set_status(422)
self.write(
@@ -127,6 +128,8 @@ class V1APISpotHandler(tornado.web.RequestHandler):
# infer missing data, and add it to our database.
spot.source = "API"
spot.infer_missing()
assert self._spots is not None, "initialize() must be called before post()"
assert spot.id is not None, "infer_missing() always assigns an id"
self._spots.set(spot.id, spot)
self.write(safe_json_dumps("OK"))