Autogenerated type safety parameterisation of all methods

This commit is contained in:
Ian Renton
2026-09-20 20:02:19 +01:00
parent 6037e742cc
commit 324dd1414b
132 changed files with 1228 additions and 706 deletions
+11 -8
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import logging
import re
import threading
@@ -11,6 +13,7 @@ from tornado.web import Application
from core.activity_utils import get_ref_regex_for_activity
from core.config import ALLOW_SPOTTING, ALLOW_UPSTREAM_SPOTTING, RECAPTCHA_SECRET_KEY
from core.constants import UNKNOWN_BAND
from core.live_data_cache import LiveDataCache
from core.utils import infer_band_from_freq, safe_json_dumps
from data.spot import Spot
from providers.spot.spot_provider import SpotProvider
@@ -25,19 +28,19 @@ class APISpotHandler(tornado.web.RequestHandler):
def __init__(
self,
application: "Application",
application: Application,
request: httputil.HTTPServerRequest,
**kwargs: Any,
):
self._spots = None
self._spot_providers = None
) -> None:
self._spots: LiveDataCache | None = None
self._spot_providers: list[SpotProvider] | None = None
super().__init__(application, request, **kwargs)
def initialize(self, spots, spot_providers=None):
def initialize(self, spots: LiveDataCache, spot_providers: list[SpotProvider] | None = None) -> None:
self._spots = spots
self._spot_providers = spot_providers or []
def post(self):
def post(self) -> None:
try:
# Reject if not allowed
if not ALLOW_SPOTTING:
@@ -241,7 +244,7 @@ class APISpotHandler(tornado.web.RequestHandler):
self.set_header("Cache-Control", "no-store")
self.set_header("Content-Type", "application/json")
def _find_provider(self, provider_name, activity) -> SpotProvider | None:
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."""
for p in self._spot_providers:
@@ -250,7 +253,7 @@ class APISpotHandler(tornado.web.RequestHandler):
return None
@staticmethod
def _verify_recaptcha(token):
def _verify_recaptcha(token: str) -> bool:
"""Verify a Google reCAPTCHA v2 token. Returns True if valid."""
try:
+25 -20
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import copy
import logging
from datetime import datetime
@@ -10,8 +12,11 @@ from tornado import httputil
from tornado.web import Application
from core.enums import ActivityName
from core.live_data_cache import LiveDataCache
from core.utils import safe_json_dumps
from data.lookup_credentials import extract_credentials
from data.alert import Alert
from data.lookup_credentials import LookupCredentials, extract_credentials
from webserver.sse_broadcaster import SSEBroadcaster
logger = logging.getLogger(__name__)
@@ -21,18 +26,18 @@ class APIAlertsHandler(tornado.web.RequestHandler):
def __init__(
self,
application: "Application",
application: Application,
request: httputil.HTTPServerRequest,
**kwargs: Any,
):
self._alerts = None
) -> None:
self._alerts: LiveDataCache | None = None
super().__init__(application, request, **kwargs)
def initialize(self, alerts):
def initialize(self, alerts: LiveDataCache) -> None:
self._alerts = alerts
@staticmethod
def _enrich(alerts, credentials):
def _enrich(alerts: list[Alert], credentials: LookupCredentials) -> list[Alert]:
enriched = []
for alert in alerts:
alert_copy = copy.deepcopy(alert)
@@ -40,7 +45,7 @@ class APIAlertsHandler(tornado.web.RequestHandler):
enriched.append(alert_copy)
return enriched
def get(self):
def get(self) -> None:
try:
# request.arguments contains lists for each param key because technically the client can supply multiple,
# reduce that to just the first entry, and convert bytes to string
@@ -71,22 +76,22 @@ class APIAlertsHandler(tornado.web.RequestHandler):
class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
"""API request handler for /api/v2/alerts/stream"""
def __init__(self, application, request, **kwargs: Any):
self._sse_alert_broadcaster = None
self._query_params = None
self._credentials = None
self._fields = None
def __init__(self, application: Application, request: httputil.HTTPServerRequest, **kwargs: Any) -> None:
self._sse_alert_broadcaster: SSEBroadcaster | None = None
self._query_params: dict[str, str] | None = None
self._credentials: LookupCredentials | None = None
self._fields: list[str] | None = None
super().__init__(application, request, **kwargs)
def initialize(self, sse_alert_broadcaster):
def initialize(self, sse_alert_broadcaster: SSEBroadcaster) -> None:
self._sse_alert_broadcaster = sse_alert_broadcaster
def custom_headers(self):
def custom_headers(self) -> dict[str, str]:
"""Custom headers to avoid e.g. nginx reverse proxy from buffering SSE data"""
return {"Cache-Control": "no-store", "X-Accel-Buffering": "no"}
def open(self):
def open(self) -> None:
try:
# request.arguments contains lists for each param key because technically the client can supply multiple,
# reduce that to just the first entry, and convert bytes to string
@@ -107,13 +112,13 @@ class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
logger.exception("Exception when serving SSE socket")
self.close()
def close(self):
def close(self) -> None:
"""When the user closes the socket, deregister ourselves from the alert broadcaster"""
self._sse_alert_broadcaster.unregister(self)
super().close()
def callback(self, alert):
def callback(self, alert: Alert) -> None:
"""Callback when a new alert arrives"""
try:
@@ -132,7 +137,7 @@ class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
self.close()
def get_alert_list_with_filters(all_alerts, query):
def get_alert_list_with_filters(all_alerts: LiveDataCache, query: dict[str, str]) -> list[Alert]:
"""Utility method to apply filters to the overall alert list and return only a subset. Enables query parameters in
the main "alerts" GET call."""
@@ -152,7 +157,7 @@ def get_alert_list_with_filters(all_alerts, query):
return alerts
def alert_allowed_by_query(alert, query):
def alert_allowed_by_query(alert: Alert, query: dict[str, str]) -> bool:
"""Given URL query params and an alert, figure out if the alert "passes" the requested filters or is rejected. The list
of query parameters and their function is defined in the API docs."""
@@ -214,7 +219,7 @@ def alert_allowed_by_query(alert, query):
return True
def filter_fields(alerts, fields):
def filter_fields(alerts: list[Alert], fields: list[str]) -> list[dict[str, Any]]:
"""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]
+8 -5
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import json
import logging
from collections import Counter
@@ -11,6 +13,7 @@ from tornado.web import Application
from core.constants import BANDS
from core.enums import Continent
from core.live_data_cache import LiveDataCache
from core.utils import safe_json_dumps
logger = logging.getLogger(__name__)
@@ -24,17 +27,17 @@ class APIDxStatsHandler(tornado.web.RequestHandler):
def __init__(
self,
application: "Application",
application: Application,
request: httputil.HTTPServerRequest,
**kwargs: Any,
):
self._spots = None
) -> None:
self._spots: LiveDataCache | None = None
super().__init__(application, request, **kwargs)
def initialize(self, spots):
def initialize(self, spots: LiveDataCache) -> None:
self._spots = spots
def get(self):
def get(self) -> None:
try:
one_hour_ago = (datetime.now(pytz.UTC) - timedelta(hours=1)).timestamp()
counts = Counter()
+11 -9
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import logging
import re
from typing import Any
@@ -26,13 +28,13 @@ class APILookupCallHandler(tornado.web.RequestHandler):
def __init__(
self,
application: "Application",
application: Application,
request: httputil.HTTPServerRequest,
**kwargs: Any,
):
) -> None:
super().__init__(application, request, **kwargs)
def get(self):
def get(self) -> None:
try:
# request.arguments contains lists for each param key because technically the client can supply multiple,
# reduce that to just the first entry, and convert bytes to string
@@ -67,13 +69,13 @@ class APILookupActivityRefHandler(tornado.web.RequestHandler):
def __init__(
self,
application: "Application",
application: Application,
request: httputil.HTTPServerRequest,
**kwargs: Any,
):
) -> None:
super().__init__(application, request, **kwargs)
def get(self):
def get(self) -> None:
try:
# request.arguments contains lists for each param key because technically the client can supply multiple,
# reduce that to just the first entry, and convert bytes to string
@@ -119,13 +121,13 @@ class APILookupGridHandler(tornado.web.RequestHandler):
def __init__(
self,
application: "Application",
application: Application,
request: httputil.HTTPServerRequest,
**kwargs: Any,
):
) -> None:
super().__init__(application, request, **kwargs)
def get(self):
def get(self) -> None:
try:
# request.arguments contains lists for each param key because technically the client can supply multiple,
# reduce that to just the first entry, and convert bytes to string
+9 -6
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import logging
from typing import Any
@@ -10,6 +12,7 @@ from core.constants import BANDS, PROPAGATION_MODES
from core.enums import Continent, Mode, ModeType
from core.utils import safe_json_dumps
from data.activities import ACTIVITIES
from providers.spot.spot_provider import SpotProvider
logger = logging.getLogger(__name__)
@@ -19,19 +22,19 @@ class APIOptionsHandler(tornado.web.RequestHandler):
def __init__(
self,
application: "Application",
application: Application,
request: httputil.HTTPServerRequest,
**kwargs: Any,
):
self._status_data = None
self._spot_providers = None
) -> None:
self._status_data: dict[str, Any] | None = None
self._spot_providers: list[SpotProvider] | None = None
super().__init__(application, request, **kwargs)
def initialize(self, status_data, spot_providers=None):
def initialize(self, status_data: dict[str, Any], spot_providers: list[SpotProvider] | None = None) -> None:
self._status_data = status_data
self._spot_providers = spot_providers or []
def get(self):
def get(self) -> None:
try:
# Build a map of activity name -> list of provider names that can submit spots for that activity
spot_submit_providers = {}
+8 -5
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import logging
from typing import Any
@@ -6,6 +8,7 @@ from tornado import httputil
from tornado.web import Application
from core.utils import safe_json_dumps
from data.solar_conditions import SolarConditions
logger = logging.getLogger(__name__)
@@ -15,17 +18,17 @@ class APISolarConditionsHandler(tornado.web.RequestHandler):
def __init__(
self,
application: "Application",
application: Application,
request: httputil.HTTPServerRequest,
**kwargs: Any,
):
self._solar_conditions = None
) -> None:
self._solar_conditions: SolarConditions | None = None
super().__init__(application, request, **kwargs)
def initialize(self, solar_conditions):
def initialize(self, solar_conditions: SolarConditions) -> None:
self._solar_conditions = solar_conditions
def get(self):
def get(self) -> None:
try:
self.write(self._solar_conditions.to_json())
self.set_status(200)
+25 -20
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import copy
import logging
from datetime import datetime, timedelta
@@ -9,8 +11,11 @@ import tornado_eventsource.handler
from tornado import httputil
from tornado.web import Application
from core.live_data_cache import LiveDataCache
from core.utils import safe_json_dumps
from data.lookup_credentials import extract_credentials
from data.lookup_credentials import LookupCredentials, extract_credentials
from data.spot import Spot
from webserver.sse_broadcaster import SSEBroadcaster
logger = logging.getLogger(__name__)
@@ -20,18 +25,18 @@ class APISpotsHandler(tornado.web.RequestHandler):
def __init__(
self,
application: "Application",
application: Application,
request: httputil.HTTPServerRequest,
**kwargs: Any,
):
self._spots = None
) -> None:
self._spots: LiveDataCache | None = None
super().__init__(application, request, **kwargs)
def initialize(self, spots):
def initialize(self, spots: LiveDataCache) -> None:
self._spots = spots
@staticmethod
def _enrich(spots, credentials):
def _enrich(spots: list[Spot], credentials: LookupCredentials) -> list[Spot]:
enriched = []
for spot in spots:
spot_copy = copy.deepcopy(spot)
@@ -39,7 +44,7 @@ class APISpotsHandler(tornado.web.RequestHandler):
enriched.append(spot_copy)
return enriched
def get(self):
def get(self) -> None:
try:
# request.arguments contains lists for each param key because technically the client can supply multiple,
# reduce that to just the first entry, and convert bytes to string
@@ -70,22 +75,22 @@ class APISpotsHandler(tornado.web.RequestHandler):
class APISpotsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
"""API request handler for /api/v2/spots/stream"""
def __init__(self, application, request, **kwargs: Any):
self._sse_spot_broadcaster = None
self._query_params = None
self._credentials = None
self._fields = None
def __init__(self, application: Application, request: httputil.HTTPServerRequest, **kwargs: Any) -> None:
self._sse_spot_broadcaster: SSEBroadcaster | None = None
self._query_params: dict[str, str] | None = None
self._credentials: LookupCredentials | None = None
self._fields: list[str] | None = None
super().__init__(application, request, **kwargs)
def initialize(self, sse_spot_broadcaster):
def initialize(self, sse_spot_broadcaster: SSEBroadcaster) -> None:
self._sse_spot_broadcaster = sse_spot_broadcaster
def custom_headers(self):
def custom_headers(self) -> dict[str, str]:
"""Custom headers to avoid e.g. nginx reverse proxy from buffering SSE data"""
return {"Cache-Control": "no-store", "X-Accel-Buffering": "no"}
def open(self):
def open(self) -> None:
"""Called once on the client opening a connection, set things up"""
try:
@@ -108,13 +113,13 @@ class APISpotsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
logger.exception("Exception when serving SSE socket")
self.close()
def close(self):
def close(self) -> None:
"""When the user closes the socket, deregister ourselves from the spot broadcaster"""
self._sse_spot_broadcaster.unregister(self)
super().close()
def callback(self, spot):
def callback(self, spot: Spot) -> None:
"""Callback when a new spot arrives"""
try:
@@ -133,7 +138,7 @@ class APISpotsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
self.close()
def get_spot_list_with_filters(all_spots, query):
def get_spot_list_with_filters(all_spots: LiveDataCache, query: dict[str, str]) -> list[Spot]:
"""Utility method to apply filters to the overall spot list and return only a subset. Enables query parameters in
the main "spots" GET call."""
@@ -173,7 +178,7 @@ def get_spot_list_with_filters(all_spots, query):
return spots
def spot_allowed_by_query(spot, query):
def spot_allowed_by_query(spot: Spot, query: dict[str, str]) -> bool:
"""Given URL query params and a spot, figure out if the spot "passes" the requested filters or is rejected. The list
of query parameters and their function is defined in the API docs."""
@@ -263,7 +268,7 @@ def spot_allowed_by_query(spot, query):
return True
def filter_fields(spots, fields):
def filter_fields(spots: list[Spot], fields: list[str]) -> list[dict[str, Any]]:
"""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]
+7 -5
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import logging
from typing import Any
@@ -15,17 +17,17 @@ class APIStatusHandler(tornado.web.RequestHandler):
def __init__(
self,
application: "Application",
application: Application,
request: httputil.HTTPServerRequest,
**kwargs: Any,
):
self._status_data = None
) -> None:
self._status_data: dict[str, Any] | None = None
super().__init__(application, request, **kwargs)
def initialize(self, status_data):
def initialize(self, status_data: dict[str, Any]) -> None:
self._status_data = status_data
def get(self):
def get(self) -> None:
try:
self.write(safe_json_dumps(self._status_data))
self.set_status(200)
+8 -5
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import logging
import re
from typing import Any
@@ -9,6 +11,7 @@ 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.live_data_cache import LiveDataCache
from core.utils import infer_band_from_freq, safe_json_dumps
from data.spot import Spot
@@ -20,17 +23,17 @@ class V1APISpotHandler(tornado.web.RequestHandler):
def __init__(
self,
application: "Application",
application: Application,
request: httputil.HTTPServerRequest,
**kwargs: Any,
):
self._spots = None
) -> None:
self._spots: LiveDataCache | None = None
super().__init__(application, request, **kwargs)
def initialize(self, spots):
def initialize(self, spots: LiveDataCache) -> None:
self._spots = spots
def post(self):
def post(self) -> None:
try:
# Reject if not allowed
if not ALLOW_SPOTTING:
+5 -3
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import logging
import tornado
@@ -20,7 +22,7 @@ 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):
async def _proxy(self, path: str) -> None:
new_url = f"{self.request.protocol}://{self.request.host}/api/v2/{path}"
if self.request.query:
new_url += f"?{self.request.query}"
@@ -61,8 +63,8 @@ class V1RedirectHandler(tornado.web.RequestHandler):
if response.body:
self.write(response.body)
async def get(self, path):
async def get(self, path: str) -> None:
await self._proxy(path)
async def post(self, path):
async def post(self, path: str) -> None:
await self._proxy(path)
+10 -5
View File
@@ -1,4 +1,9 @@
from __future__ import annotations
import re
from typing import Any
from tornado.web import RequestHandler
from webserver.handlers.api.spots import APISpotsHandler, APISpotsStreamHandler
@@ -13,7 +18,7 @@ _LEGACY_PARAM_TO_HEADER_MAP = {
}
def _handle_legacy_params(handler):
def _handle_legacy_params(handler: RequestHandler) -> None:
"""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():
@@ -27,11 +32,11 @@ def _handle_legacy_params(handler):
class V1APISpotsHandler(APISpotsHandler):
"""API request handler for /api/v1/spots (GET). Included in early Spothole v2 for backwards compatibility."""
def prepare(self):
def prepare(self) -> None:
_handle_legacy_params(self)
super().prepare()
def write(self, chunk):
def write(self, chunk: str | bytes | dict[str, Any]) -> None:
if isinstance(chunk, str):
chunk = _GRID_SOURCE_RE.sub('"dx_location_source": "SPOT"', chunk)
super().write(chunk)
@@ -40,11 +45,11 @@ class V1APISpotsHandler(APISpotsHandler):
class V1APISpotsStreamHandler(APISpotsStreamHandler):
"""API request handler for /api/v1/spots/stream (SSE). Included in early Spothole v2 for backwards compatibility."""
def prepare(self):
def prepare(self) -> None:
_handle_legacy_params(self)
super().prepare()
def write_message(self, *args, **kwargs):
def write_message(self, *args: Any, **kwargs: Any) -> None:
args = list(args)
for i, a in enumerate(args):
if isinstance(a, str) and '"dx_location_source"' in a:
+3 -1
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import tornado.web
from core.config import BASE_URL
@@ -6,6 +8,6 @@ from core.config import BASE_URL
class ManifestHandler(tornado.web.RequestHandler):
"""Handler for manifest.webmanifest, which needs BASE_URL inserted and a custom content-type"""
def get(self):
def get(self) -> None:
self.set_header("Content-Type", "application/manifest+json; charset=UTF-8")
self.render("manifest.webmanifest", baseurl=BASE_URL)
+3 -1
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import tornado
from prometheus_client import CONTENT_TYPE_LATEST
@@ -7,7 +9,7 @@ from core.prometheus_metrics_handler import get_metrics
class PrometheusMetricsHandler(tornado.web.RequestHandler):
"""Handler for Prometheus metrics endpoint"""
def get(self):
def get(self) -> None:
self.write(get_metrics())
self.set_status(200)
self.set_header("Content-Type", CONTENT_TYPE_LATEST)
+7 -5
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
from typing import Any
import tornado
@@ -21,17 +23,17 @@ class PageTemplateHandler(tornado.web.RequestHandler):
def __init__(
self,
application: "Application",
application: Application,
request: httputil.HTTPServerRequest,
**kwargs: Any,
):
self._template_name = None
) -> None:
self._template_name: str | None = None
super().__init__(application, request, **kwargs)
def initialize(self, template_name):
def initialize(self, template_name: str) -> None:
self._template_name = template_name
def get(self):
def get(self) -> None:
# Load named template, and provide variables used in templates
self.render(
f"{self._template_name}.html",