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
+15 -6
View File
@@ -1,28 +1,37 @@
from __future__ import annotations
from datetime import datetime
from typing import TYPE_CHECKING, Any
import pytz
from core.data_store import DATA_STORE
from core.live_data_cache import LiveDataCache
if TYPE_CHECKING:
# Deferred to avoid a circular import: data.alert imports core.call_lookup_helper, which imports
# core.data_providers, which imports this module.
from data.alert import Alert
class AlertProvider:
"""Generic alert provider class. Subclasses of this query the individual APIs for alerts."""
def __init__(self, name, provider_config):
def __init__(self, name: str, provider_config: dict[str, Any]) -> None:
"""Constructor"""
self.name = name
self.enabled = provider_config.get("enabled", True)
self.last_update_time = datetime.min.replace(tzinfo=pytz.UTC)
self.status = "Not Started" if self.enabled else "Disabled"
self._alerts = DATA_STORE.alerts
self._alerts: LiveDataCache[Alert] = DATA_STORE.alerts
def start(self):
def start(self) -> None:
"""Start the provider. This should return immediately after spawning threads to access the remote resources"""
raise NotImplementedError("Subclasses must implement this method")
def _submit_batch(self, alerts):
def _submit_batch(self, alerts: list[Alert]) -> None:
"""Submit a batch of alerts retrieved from the provider. There is no timestamp checking like there is for spots,
because alerts could be created at any point for any time in the future. Rely on hashcode-based id matching
to deal with duplicates."""
@@ -35,11 +44,11 @@ class AlertProvider:
alert.infer_missing()
self._add_alert(alert)
def _add_alert(self, alert):
def _add_alert(self, alert: Alert) -> None:
if not alert.expired():
self._alerts.set(alert.id, alert)
def stop(self):
def stop(self) -> None:
"""Stop any threads and prepare for application shutdown"""
raise NotImplementedError("Subclasses must implement this method")
+6 -2
View File
@@ -1,6 +1,10 @@
from __future__ import annotations
from datetime import datetime, timedelta
from typing import Any
import pytz
import requests
from bs4 import BeautifulSoup
from core.enums import ActivityName
@@ -15,10 +19,10 @@ class BOTA(HTTPAlertProvider):
POLL_INTERVAL_SEC = 1800
ALERTS_URL = "https://www.beachesontheair.com/"
def __init__(self, provider_config):
def __init__(self, provider_config: dict[str, Any]) -> None:
super().__init__("BOTA", provider_config, self.ALERTS_URL, self.POLL_INTERVAL_SEC)
def _http_response_to_alerts(self, http_response):
def _http_response_to_alerts(self, http_response: requests.Response) -> list[Alert]:
new_alerts = []
# Find the table of upcoming alerts
bs = BeautifulSoup(http_response.content.decode("utf-8-sig"), features="lxml")
+6 -2
View File
@@ -1,6 +1,10 @@
from __future__ import annotations
from datetime import datetime
from typing import Any
import pytz
import requests
from core.enums import ActivityName
from data.activity_ref import ActivityRef
@@ -14,10 +18,10 @@ class Hamsat(HTTPAlertProvider):
POLL_INTERVAL_SEC = 1800
ALERTS_URL = "https://hams.at/api/alerts"
def __init__(self, provider_config):
def __init__(self, provider_config: dict[str, Any]) -> None:
super().__init__("Hamsat", provider_config, self.ALERTS_URL, self.POLL_INTERVAL_SEC)
def _http_response_to_alerts(self, http_response):
def _http_response_to_alerts(self, http_response: requests.Response) -> list[Alert]:
new_alerts = []
# Iterate through source data
for source_alert in http_response.json()["data"]:
+11 -7
View File
@@ -1,12 +1,16 @@
from __future__ import annotations
import logging
from datetime import datetime
from threading import Event, Thread
from typing import Any
import pytz
import requests
from requests.exceptions import ConnectionError, ConnectTimeout, JSONDecodeError, ReadTimeout
from core.constants import HTTP_HEADERS
from data.alert import Alert
from providers.alert.alert_provider import AlertProvider
logger = logging.getLogger(__name__)
@@ -16,34 +20,34 @@ class HTTPAlertProvider(AlertProvider):
"""Generic alert provider class for providers that request data via HTTP(S). Just for convenience to avoid code
duplication. Subclasses of this query the individual APIs for data."""
def __init__(self, name, provider_config, url, poll_interval):
def __init__(self, name: str, provider_config: dict[str, Any], url: str, poll_interval: int) -> None:
super().__init__(name, provider_config)
self._url = url
self._poll_interval = poll_interval
self._thread = None
self._thread: Thread | None = None
self._stop_event = Event()
def start(self):
def start(self) -> None:
# Fire off the polling thread. It will poll immediately on startup, then sleep for poll_interval between
# subsequent polls, so start() returns immediately and the application can continue starting.
logger.info(f"Set up query of {self.name} alert API every {self._poll_interval!s} seconds.")
self._thread = Thread(target=self._run, name=f"HTTPAlertProvider-{self.name}", daemon=True)
self._thread.start()
def stop(self):
def stop(self) -> None:
self._stop_event.set()
if self._thread:
self._thread.join(timeout=12)
if self._thread.is_alive():
logger.warning(f"{self.name} alert worker thread did not exit on time and will be killed.")
def _run(self):
def _run(self) -> None:
while True:
self._poll()
if self._stop_event.wait(timeout=self._poll_interval):
break
def _poll(self):
def _poll(self) -> None:
try:
# Request data from API
logger.debug(f"Polling {self.name} alert API...")
@@ -78,7 +82,7 @@ class HTTPAlertProvider(AlertProvider):
# Brief pause on error before the next poll, but still respond promptly to stop()
self._stop_event.wait(timeout=1)
def _http_response_to_alerts(self, http_response):
def _http_response_to_alerts(self, http_response: requests.Response) -> list[Alert]:
"""Convert an HTTP response returned by the API into alert data. The whole response is provided here so the subclass
implementations can check for HTTP status codes if necessary, and handle the response as JSON, XML, text, whatever
the API actually provides."""
+8 -5
View File
@@ -1,7 +1,10 @@
from datetime import datetime, time
from typing import cast
from __future__ import annotations
from datetime import date, datetime, time
from typing import Any, cast
import pytz
import requests
from icalendar import Calendar, Event
from data.alert import Alert
@@ -12,10 +15,10 @@ class ICALAlertProvider(HTTPAlertProvider):
"""Generic alert provider for iCal calendars. Defines an abstract method event_to_alert(event) that subclasses must
implement, and use it to convert an iCal event to an Alert object based on whatever format their iCal events use."""
def __init__(self, name, provider_config, url, poll_interval):
def __init__(self, name: str, provider_config: dict[str, Any], url: str, poll_interval: int) -> None:
super().__init__(name, provider_config, url, poll_interval)
def _http_response_to_alerts(self, http_response):
def _http_response_to_alerts(self, http_response: requests.Response) -> list[Alert]:
new_alerts = []
cal = Calendar.from_ical(http_response.content)
@@ -34,7 +37,7 @@ class ICALAlertProvider(HTTPAlertProvider):
"""Convert an ICal event to an Alert object. Subclasses must implement this method."""
@staticmethod
def _to_utc_timestamp(value):
def _to_utc_timestamp(value: datetime | date) -> float:
"""Convert a date or datetime value from an iCal field into a UTC UNIX timestamp."""
# Datetime object so we can treat it as-is, check if it has a non-UTC tz and convert it if necessary
+6 -3
View File
@@ -1,8 +1,11 @@
from __future__ import annotations
import re
from datetime import datetime
from typing import cast
from typing import Any, cast
import pytz
import requests
from rss_parser import Parser
from rss_parser.models.rss import RSS
@@ -18,10 +21,10 @@ class NG3K(HTTPAlertProvider):
ALERTS_URL = "https://www.ng3k.com/adxo.xml"
AS_CALL_PATTERN = re.compile("as ([a-z0-9/]+)", re.IGNORECASE)
def __init__(self, provider_config):
def __init__(self, provider_config: dict[str, Any]) -> None:
super().__init__("NG3K", provider_config, self.ALERTS_URL, self.POLL_INTERVAL_DAYS * 24 * 60 * 60)
def _http_response_to_alerts(self, http_response):
def _http_response_to_alerts(self, http_response: requests.Response) -> list[Alert]:
new_alerts = []
rss = cast(RSS, Parser.parse(http_response.content.decode("utf-8-sig")))
# Iterate through source data
+6 -2
View File
@@ -1,7 +1,11 @@
from __future__ import annotations
import logging
from datetime import datetime
from typing import Any
import pytz
import requests
from core.enums import ActivityName
from data.activity_ref import ActivityRef
@@ -17,10 +21,10 @@ class ParksNPeaks(HTTPAlertProvider):
POLL_INTERVAL_SEC = 1800
ALERTS_URL = "https://parksnpeaks.org/api/ALERTS/"
def __init__(self, provider_config):
def __init__(self, provider_config: dict[str, Any]) -> None:
super().__init__("ParksNPeaks", provider_config, self.ALERTS_URL, self.POLL_INTERVAL_SEC)
def _http_response_to_alerts(self, http_response):
def _http_response_to_alerts(self, http_response: requests.Response) -> list[Alert]:
new_alerts = []
# Iterate through source data
for source_alert in http_response.json():
+6 -2
View File
@@ -1,6 +1,10 @@
from __future__ import annotations
from datetime import datetime
from typing import Any
import pytz
import requests
from core.enums import ActivityName
from data.activity_ref import ActivityRef
@@ -14,10 +18,10 @@ class POTA(HTTPAlertProvider):
POLL_INTERVAL_SEC = 1800
ALERTS_URL = "https://api.pota.app/activation"
def __init__(self, provider_config):
def __init__(self, provider_config: dict[str, Any]) -> None:
super().__init__("POTA", provider_config, self.ALERTS_URL, self.POLL_INTERVAL_SEC)
def _http_response_to_alerts(self, http_response):
def _http_response_to_alerts(self, http_response: requests.Response) -> list[Alert]:
new_alerts = []
# Iterate through source data
for source_alert in http_response.json():
+4 -1
View File
@@ -1,4 +1,7 @@
from __future__ import annotations
import re
from typing import Any
from icalendar import Event
@@ -12,7 +15,7 @@ class RSGBICALAlertProvider(ICALAlertProvider):
handling specific to how RSGB's iCal events are formatted. This is still effectively an abstract class itself;
RSGB has two contest calendars (HF & VHF) that each subclass this."""
def __init__(self, name, provider_config, url, poll_interval):
def __init__(self, name: str, provider_config: dict[str, Any], url: str, poll_interval: int) -> None:
super().__init__(name, provider_config, url, poll_interval)
FREQ_PATTERN = re.compile(r"([\d.]+(?:MHz|GHz))|SHF")
+5 -1
View File
@@ -1,3 +1,7 @@
from __future__ import annotations
from typing import Any
from providers.alert.rsgb_ical_alert_provider import RSGBICALAlertProvider
@@ -7,5 +11,5 @@ class RSGBHFContests(RSGBICALAlertProvider):
POLL_INTERVAL_DAYS = 30
ALERTS_URL = "https://calendar.google.com/calendar/ical/a5ff31ebb1b4834dc7fff4c5415ae8251c6a9aa11f98c6af6e472b6c552b1915%40group.calendar.google.com/public/basic.ics"
def __init__(self, provider_config):
def __init__(self, provider_config: dict[str, Any]) -> None:
super().__init__("RSGB HF Contests", provider_config, self.ALERTS_URL, self.POLL_INTERVAL_DAYS * 24 * 60 * 60)
+5 -1
View File
@@ -1,3 +1,7 @@
from __future__ import annotations
from typing import Any
from providers.alert.rsgb_ical_alert_provider import RSGBICALAlertProvider
@@ -7,5 +11,5 @@ class RSGBVHFContests(RSGBICALAlertProvider):
POLL_INTERVAL_DAYS = 30
ALERTS_URL = "https://calendar.google.com/calendar/ical/40f3552bff39a016f1cdca205864177070dcad68d55be17eb061cb021f39f96c%40group.calendar.google.com/public/basic.ics"
def __init__(self, provider_config):
def __init__(self, provider_config: dict[str, Any]) -> None:
super().__init__("RSGB VHF Contests", provider_config, self.ALERTS_URL, self.POLL_INTERVAL_DAYS * 24 * 60 * 60)
+6 -2
View File
@@ -1,6 +1,10 @@
from __future__ import annotations
from datetime import datetime
from typing import Any
import pytz
import requests
from core.enums import ActivityName
from data.activity_ref import ActivityRef
@@ -14,10 +18,10 @@ class SOTA(HTTPAlertProvider):
POLL_INTERVAL_SEC = 1800
ALERTS_URL = "https://api-db2.sota.org.uk/api/alerts/365/all/all"
def __init__(self, provider_config):
def __init__(self, provider_config: dict[str, Any]) -> None:
super().__init__("SOTA", provider_config, self.ALERTS_URL, self.POLL_INTERVAL_SEC)
def _http_response_to_alerts(self, http_response):
def _http_response_to_alerts(self, http_response: requests.Response) -> list[Alert]:
new_alerts = []
# Iterate through source data
for source_alert in http_response.json():
+5 -1
View File
@@ -1,3 +1,7 @@
from __future__ import annotations
from typing import Any
from icalendar import Event
from core.enums import ActivityName
@@ -11,7 +15,7 @@ class WA7BNM(ICALAlertProvider):
POLL_INTERVAL_DAYS = 1
ALERTS_URL = "https://contestcalendar.com/weeklycontcustom.php"
def __init__(self, provider_config):
def __init__(self, provider_config: dict[str, Any]) -> None:
super().__init__(
"WA7BNM Contest Calendar", provider_config, self.ALERTS_URL, self.POLL_INTERVAL_DAYS * 24 * 60 * 60
)
+6 -3
View File
@@ -1,9 +1,12 @@
from __future__ import annotations
import logging
from datetime import datetime
from typing import cast
from typing import Any, cast
from xml.parsers.expat import ExpatError
import pytz
import requests
from rss_parser import Parser as RSSParser
from rss_parser.models.rss import RSS
@@ -22,10 +25,10 @@ class WOTA(HTTPAlertProvider):
ALERTS_URL = "https://www.wota.org.uk/alerts_rss.php"
RSS_DATE_TIME_FORMAT = "%a, %d %b %Y %H:%M:%S %z"
def __init__(self, provider_config):
def __init__(self, provider_config: dict[str, Any]) -> None:
super().__init__("WOTA", provider_config, self.ALERTS_URL, self.POLL_INTERVAL_SEC)
def _http_response_to_alerts(self, http_response):
def _http_response_to_alerts(self, http_response: requests.Response) -> list[Alert]:
new_alerts = []
try:
+6 -2
View File
@@ -1,6 +1,10 @@
from __future__ import annotations
from datetime import datetime
from typing import Any
import pytz
import requests
from core.enums import ActivityName
from data.activity_ref import ActivityRef
@@ -14,10 +18,10 @@ class WWFF(HTTPAlertProvider):
POLL_INTERVAL_SEC = 1800
ALERTS_URL = "https://spots.wwff.co/static/agendas.json"
def __init__(self, provider_config):
def __init__(self, provider_config: dict[str, Any]) -> None:
super().__init__("WWFF", provider_config, self.ALERTS_URL, self.POLL_INTERVAL_SEC)
def _http_response_to_alerts(self, http_response):
def _http_response_to_alerts(self, http_response: requests.Response) -> list[Alert]:
new_alerts = []
# Iterate through source data
for source_alert in http_response.json():