mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-09-21 06:47:42 +00:00
Autogenerated type safety parameterisation of all methods
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from typing import Any
|
||||
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado_eventsource.handler import EventSourceHandler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -10,19 +14,19 @@ class SSEBroadcaster:
|
||||
"""Bridge between DataStore listener callbacks (which fire on provider threads) to Tornado's async SSE handlers
|
||||
(which live on the IOLoop thread) to avoid any interdependency between them."""
|
||||
|
||||
def __init__(self):
|
||||
self._handlers = set()
|
||||
def __init__(self) -> None:
|
||||
self._handlers: set[EventSourceHandler] = set()
|
||||
self._lock = threading.Lock()
|
||||
self._loop = None
|
||||
self._loop: IOLoop | None = None
|
||||
|
||||
def bind_to_web_server_loop(self):
|
||||
def bind_to_web_server_loop(self) -> None:
|
||||
self._loop = IOLoop.current()
|
||||
|
||||
def register(self, handler):
|
||||
def register(self, handler: EventSourceHandler) -> None:
|
||||
with self._lock:
|
||||
self._handlers.add(handler)
|
||||
|
||||
def unregister(self, handler):
|
||||
def unregister(self, handler: EventSourceHandler) -> None:
|
||||
with self._lock:
|
||||
self._handlers.discard(handler)
|
||||
|
||||
@@ -31,10 +35,10 @@ class SSEBroadcaster:
|
||||
with self._lock:
|
||||
return len(self._handlers)
|
||||
|
||||
def publish(self, value):
|
||||
def publish(self, value: Any) -> None:
|
||||
self._loop.add_callback(self._broadcast, value)
|
||||
|
||||
def _broadcast(self, value):
|
||||
def _broadcast(self, value: Any) -> None:
|
||||
with self._lock:
|
||||
handlers = list(self._handlers)
|
||||
for handler in handlers:
|
||||
|
||||
+14
-11
@@ -1,10 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
from typing import Any
|
||||
|
||||
import tornado
|
||||
from tornado.web import StaticFileHandler
|
||||
from tornado.web import RequestHandler, StaticFileHandler
|
||||
|
||||
from core.config import (
|
||||
ALLOW_SPOTTING,
|
||||
@@ -44,7 +47,7 @@ _HERE = os.path.dirname(__file__ or "")
|
||||
class WebServer:
|
||||
"""Provides the public-facing web server."""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
"""Constructor"""
|
||||
|
||||
self._data_store = DATA_STORE
|
||||
@@ -54,11 +57,11 @@ class WebServer:
|
||||
self._port = WEB_SERVER_PORT
|
||||
self._api_only_mode = API_ONLY_MODE
|
||||
self._shutdown_event = asyncio.Event()
|
||||
self._loop = None
|
||||
self._thread = None
|
||||
self._loop: asyncio.AbstractEventLoop | None = None
|
||||
self._thread: threading.Thread | None = None
|
||||
self.web_server_metrics = WebServerMetrics()
|
||||
|
||||
def setup(self):
|
||||
def setup(self) -> None:
|
||||
# Listen for new spots and alerts being added to the cache, so we can notify SSE clients immediately
|
||||
DATA_STORE.spots.add_listener(self._spot_broadcaster.publish)
|
||||
DATA_STORE.alerts.add_listener(self._alert_broadcaster.publish)
|
||||
@@ -69,13 +72,13 @@ class WebServer:
|
||||
|
||||
return self._spot_broadcaster.client_count + self._alert_broadcaster.client_count
|
||||
|
||||
def start(self):
|
||||
def start(self) -> None:
|
||||
"""Start the web server"""
|
||||
|
||||
self._thread = threading.Thread(target=asyncio.run, args=(self._start_inner(),), name="WebServer", daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def stop(self):
|
||||
def stop(self) -> None:
|
||||
"""Stop the web server"""
|
||||
|
||||
if self._loop and self._loop.is_running():
|
||||
@@ -85,7 +88,7 @@ class WebServer:
|
||||
if self._thread.is_alive():
|
||||
logger.warning("Web server background thread did not exit on time and will be killed.")
|
||||
|
||||
def _handle_loop_exception(self, loop, context):
|
||||
def _handle_loop_exception(self, loop: asyncio.AbstractEventLoop, context: dict[str, Any]) -> None:
|
||||
"""Ignore "cancelled" exceptions from the asyncio loop to avoid printing exceptions to the log on shutdown when
|
||||
we cancel the SSE handler threads"""
|
||||
|
||||
@@ -94,7 +97,7 @@ class WebServer:
|
||||
return
|
||||
loop.default_exception_handler(context)
|
||||
|
||||
async def _start_inner(self):
|
||||
async def _start_inner(self) -> None:
|
||||
"""Start method (async). Sets up the Tornado application."""
|
||||
|
||||
self._loop = asyncio.get_running_loop()
|
||||
@@ -154,7 +157,7 @@ class WebServer:
|
||||
APISpotHandler,
|
||||
{
|
||||
"spots": self._data_store.spots,
|
||||
"spot_providers": self._data_providers,
|
||||
"spot_providers": self._data_providers.spot_providers,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -268,7 +271,7 @@ class WebServer:
|
||||
await self._shutdown_event.wait()
|
||||
|
||||
|
||||
def request_log(handler):
|
||||
def request_log(handler: RequestHandler) -> None:
|
||||
"""Custom log function to provide more data about requests when enabled, and to provide the ability to turn off
|
||||
web request logging altogetether. Also records the time of the request and status in the webserver metrics. Probably
|
||||
not what this method is supposed to be used for but it's a convenient thing that gets called on every request, so
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
@@ -10,12 +12,12 @@ class WebServerMetrics:
|
||||
"""Tracker for web server metrics. Stores the times pages and API endpoints were accessed for an hour, so we
|
||||
can display the rate of requests per hour, and also updates the equivalent Prometheus counters."""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self.status = "Starting"
|
||||
self._page_access_times = deque()
|
||||
self._api_access_times = deque()
|
||||
self._page_access_times: deque[datetime] = deque()
|
||||
self._api_access_times: deque[datetime] = deque()
|
||||
|
||||
def record(self, path: str, status_code: int):
|
||||
def record(self, path: str, status_code: int) -> None:
|
||||
"""Records data for a request, depending on whether it's a page, API, or other request, and making sure
|
||||
the response code isn't 404. Also sets the status of the web server."""
|
||||
|
||||
@@ -37,7 +39,7 @@ class WebServerMetrics:
|
||||
return self._count_and_prune_last_hour(self._api_access_times)
|
||||
|
||||
@staticmethod
|
||||
def _count_and_prune_last_hour(access_times: deque) -> int:
|
||||
def _count_and_prune_last_hour(access_times: deque[datetime]) -> int:
|
||||
cutoff = datetime.now(pytz.UTC) - timedelta(hours=1)
|
||||
while access_times and access_times[0] < cutoff:
|
||||
access_times.popleft()
|
||||
|
||||
Reference in New Issue
Block a user