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,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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"]:
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user