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
@@ -1,18 +1,24 @@
from __future__ import annotations
from typing import Any
import diskcache
from providers.callsigndata.callsign_data_provider import CallsignDataProvider
class APIQueryCallsignDataProvider(CallsignDataProvider):
"""Generic callsign data provider class for providers that fetch their data from the web on-demand using an API."""
def __init__(self, name, provider_config, storage):
def __init__(self, name: str, provider_config: dict[str, Any], storage: diskcache.Cache) -> None:
"""Set up the provider."""
super().__init__(name, provider_config, storage)
if self.enabled:
self.status = "Ready"
def start(self):
def start(self) -> None:
pass
def stop(self):
def stop(self) -> None:
pass
@@ -1,15 +1,21 @@
from datetime import datetime
from __future__ import annotations
from datetime import datetime
from typing import Any
import diskcache
import pytz
from core.data_store import DATA_STORE
from data.callsign import Callsign
from data.lookup_credentials import LookupCredentials
class CallsignDataProvider:
"""Generic callsign reference data provider class. Subclasses of this set up the various mechanisms via which
Spothole can look up data for callsigns."""
def __init__(self, name, provider_config, storage):
def __init__(self, name: str, provider_config: dict[str, Any], storage: diskcache.Cache) -> None:
"""Constructor. As well as name and config, provide the storage object from DATA_STORE that will be used to
store the result of lookups to speed up future access."""
@@ -21,18 +27,18 @@ class CallsignDataProvider:
self.lookup_count = 0
self._storage = storage
def start(self):
def start(self) -> None:
"""Start the provider. This should return immediately after spawning threads to access remote resources, if
needed."""
raise NotImplementedError("Subclasses must implement this method")
def stop(self):
def stop(self) -> None:
"""Stop any threads and prepare for application shutdown"""
raise NotImplementedError("Subclasses must implement this method")
def lookup(self, callsign, lookup_credentials):
def lookup(self, callsign: str, lookup_credentials: LookupCredentials | None) -> Callsign | None:
"""Looks up data for the provided callsign. Takes a LookupCredentials object, which provides any credentials
that have been provided by the user for this session (QRZ.com/HamQTH) to allow us to look up using those
services on the user's behalf. (Clublog is looked up using an API key owned by the server and provided in its
@@ -57,7 +63,7 @@ class CallsignDataProvider:
else:
return None
def _perform_new_lookup(self, callsign, lookup_credentials):
def _perform_new_lookup(self, callsign: str, lookup_credentials: LookupCredentials | None) -> Callsign | None:
"""Makes a new request to the data source for callsign data."""
raise NotImplementedError("Subclasses must implement this method")
+7 -3
View File
@@ -1,5 +1,8 @@
from __future__ import annotations
import logging
from datetime import datetime
from typing import Any
import pytz
from pyhamtools import Callinfo, LookupLib
@@ -7,6 +10,7 @@ from pyhamtools import Callinfo, LookupLib
from core.data_store import DATA_STORE
from core.utils import get_callsign_object_from_pyhamtools_callinfo
from data.callsign import Callsign
from data.lookup_credentials import LookupCredentials
from providers.callsigndata.api_query_callsign_data_provider import (
APIQueryCallsignDataProvider,
)
@@ -17,9 +21,9 @@ logger = logging.getLogger(__name__)
class ClublogAPI(APIQueryCallsignDataProvider):
"""Callsign data provider for Clublog's API."""
_callinfo = None
_callinfo: Callinfo | None = None
def __init__(self, provider_config):
def __init__(self, provider_config: dict[str, Any]) -> None:
# API key required for this provider
self._api_key = provider_config.get("api_key", "")
if self._api_key != "":
@@ -33,7 +37,7 @@ class ClublogAPI(APIQueryCallsignDataProvider):
super().__init__("Clublog API", provider_config, DATA_STORE.callsign_data_clublogapi)
def _perform_new_lookup(self, callsign, lookup_credentials):
def _perform_new_lookup(self, callsign: str, lookup_credentials: LookupCredentials | None) -> Callsign | None:
callsign_data = Callsign(call=callsign)
try:
+8 -4
View File
@@ -1,11 +1,15 @@
from __future__ import annotations
import gzip
import logging
from typing import Any
from pyhamtools import Callinfo, LookupLib
from core.data_store import DATA_STORE
from core.utils import get_callsign_object_from_pyhamtools_callinfo
from data.callsign import Callsign
from data.lookup_credentials import LookupCredentials
from providers.callsigndata.file_download_callsign_data_provider import (
FileDownloadCallsignDataProvider,
)
@@ -20,9 +24,9 @@ class ClublogXML(FileDownloadCallsignDataProvider):
DATA_URL = "https://cdn.clublog.org/cty.php"
CACHE_PATH_ZIPPED = "cache/cty.xml.gz"
CACHE_PATH_UNZIPPED = "cache/cty.xml"
_callinfo = None
_callinfo: Callinfo | None = None
def __init__(self, provider_config):
def __init__(self, provider_config: dict[str, Any]) -> None:
# API key required for this provider
self._api_key = provider_config.get("api_key", "")
if self._api_key == "":
@@ -40,7 +44,7 @@ class ClublogXML(FileDownloadCallsignDataProvider):
DATA_STORE.callsign_data_clublogxml,
)
def _handle_file(self, path):
def _handle_file(self, path: str) -> bool:
try:
# The download from Clublog is gzipped so we need to uncompress that and re-save as a separate file that
# the LookupLib can actually use.
@@ -60,7 +64,7 @@ class ClublogXML(FileDownloadCallsignDataProvider):
logger.exception("Exception when loading Clublog XML.")
return False
def _perform_new_lookup(self, callsign, lookup_credentials):
def _perform_new_lookup(self, callsign: str, lookup_credentials: LookupCredentials | None) -> Callsign | None:
callsign_data = Callsign(call=callsign)
try:
+8 -4
View File
@@ -1,10 +1,14 @@
from __future__ import annotations
import logging
from typing import Any
from pyhamtools import Callinfo, LookupLib
from core.data_store import DATA_STORE
from core.utils import get_callsign_object_from_pyhamtools_callinfo
from data.callsign import Callsign
from data.lookup_credentials import LookupCredentials
from providers.callsigndata.file_download_callsign_data_provider import (
FileDownloadCallsignDataProvider,
)
@@ -18,9 +22,9 @@ class CountryFiles(FileDownloadCallsignDataProvider):
POLL_INTERVAL_DAYS = 30
DATA_URL = "https://www.country-files.com/cty/cty.plist"
CACHE_PATH = "cache/cty.plist"
_callinfo = None
_callinfo: Callinfo | None = None
def __init__(self, provider_config):
def __init__(self, provider_config: dict[str, Any]) -> None:
super().__init__(
"CountryFiles.com",
provider_config,
@@ -30,7 +34,7 @@ class CountryFiles(FileDownloadCallsignDataProvider):
DATA_STORE.callsign_data_countryfiles,
)
def _handle_file(self, path):
def _handle_file(self, path: str) -> bool:
try:
lookuplib = LookupLib(lookuptype="countryfile", filename=path)
self._callinfo = Callinfo(lookuplib)
@@ -40,7 +44,7 @@ class CountryFiles(FileDownloadCallsignDataProvider):
logger.exception("Exception when loading Country Files cty.plist.")
return False
def _perform_new_lookup(self, callsign, lookup_credentials):
def _perform_new_lookup(self, callsign: str, lookup_credentials: LookupCredentials | None) -> Callsign | None:
callsign_data = Callsign(call=callsign)
try:
@@ -1,7 +1,11 @@
from __future__ import annotations
import logging
from datetime import datetime
from threading import Event, Thread
from typing import Any
import diskcache
import pytz
from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout
@@ -15,40 +19,48 @@ logger = logging.getLogger(__name__)
class FileDownloadCallsignDataProvider(CallsignDataProvider):
"""Generic callsign data provider class for providers that fetch their data from the web by downloading a file."""
def __init__(self, name, provider_config, url, cache_file_path, poll_interval, storage):
def __init__(
self,
name: str,
provider_config: dict[str, Any],
url: str,
cache_file_path: str,
poll_interval: int,
storage: diskcache.Cache,
) -> None:
"""Set up the provider, note poll_interval is in *days*."""
super().__init__(name, provider_config, storage)
self._url = url
self._cache_file_path = cache_file_path
self._poll_interval = poll_interval
self._thread = None
self._thread: Thread | None = None
self._stop_event = Event()
self._url_data_cache = URLDataCache(f"callsigndata_{name}")
if self.enabled:
self.status = "Ready"
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} callsign reference data every {self._poll_interval!s} days.")
self._thread = Thread(target=self._run, name=f"FileDownloadCallsignDataProvider-{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} callsign data 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 * 60 * 60 * 24):
break
def _poll(self):
def _poll(self) -> None:
try:
# Request the file. Use the data cache (with a TTL of 1 day) here, not as the main mechanism for
# caching, but just so continual restarts of the software during testing don't hammer the servers.
@@ -87,7 +99,7 @@ class FileDownloadCallsignDataProvider(CallsignDataProvider):
logger.exception(f"Exception in callsign reference data provider ({self.name})")
self._stop_event.wait(timeout=1)
def _handle_file(self, path):
def _handle_file(self, path: str) -> bool:
"""Handle an updated file downloaded from the server. Return true if successful, false otherwise."""
raise NotImplementedError("Subclasses must implement this method")
+7 -3
View File
@@ -1,6 +1,9 @@
from __future__ import annotations
import logging
import urllib.parse
from datetime import datetime, timedelta
from typing import Any
import pytz
import xmltodict
@@ -14,6 +17,7 @@ from core.data_store import CACHE_DIR, DATA_STORE
from core.enums import Continent
from core.url_data_cache import URLDataCache
from data.callsign import Callsign, LocationSourceForCallsign
from data.lookup_credentials import LookupCredentials
from providers.callsigndata.api_query_callsign_data_provider import (
APIQueryCallsignDataProvider,
)
@@ -24,7 +28,7 @@ logger = logging.getLogger(__name__)
class HamQTH(APIQueryCallsignDataProvider):
"""Callsign data provider for HamQTH."""
def __init__(self, provider_config):
def __init__(self, provider_config: dict[str, Any]) -> None:
super().__init__("HamQTH", provider_config, DATA_STORE.callsign_data_hamqth)
self._HAMQTH_BASE_URL = "https://www.hamqth.com/xml.php"
self._PRG = f"Spothole v{SOFTWARE_VERSION} operated by {SERVER_OWNER_CALLSIGN}".replace(" ", "_")
@@ -33,7 +37,7 @@ class HamQTH(APIQueryCallsignDataProvider):
# and password, this is valid for an hour, so our cache stores this specifically for 55 minutes.
self._CREDENTIALS_CACHE = CachedSession(f"{CACHE_DIR}/urls/hamqth-creds", expire_after=timedelta(minutes=55))
def _perform_new_lookup(self, callsign, lookup_credentials):
def _perform_new_lookup(self, callsign: str, lookup_credentials: LookupCredentials | None) -> Callsign | None:
# If we don't have HamQTH credentials, skip this lookup Return None so we don't *cache* the lack of data, because
# # someone might provide credentials next time around.
if not lookup_credentials or not (
@@ -117,7 +121,7 @@ class HamQTH(APIQueryCallsignDataProvider):
return None
@staticmethod
def hamqth_response_to_callsign(callsign, data):
def hamqth_response_to_callsign(callsign: str, data: dict[str, Any]) -> Callsign:
"""Convert the "Callsign" block in HamQTH's API response to our own Callsign object."""
# Check for sensible latitudes
+7 -3
View File
@@ -1,6 +1,9 @@
from __future__ import annotations
import logging
import urllib.parse
from datetime import datetime, timedelta
from typing import Any
import pytz
import xmltodict
@@ -13,6 +16,7 @@ from core.data_store import CACHE_DIR, DATA_STORE
from core.enums import Continent, LocationSourceForCallsign
from core.url_data_cache import URLDataCache
from data.callsign import Callsign
from data.lookup_credentials import LookupCredentials
from providers.callsigndata.api_query_callsign_data_provider import APIQueryCallsignDataProvider
logger = logging.getLogger(__name__)
@@ -21,7 +25,7 @@ logger = logging.getLogger(__name__)
class QRZ(APIQueryCallsignDataProvider):
"""Callsign data provider for QRZ.com."""
def __init__(self, provider_config):
def __init__(self, provider_config: dict[str, Any]) -> None:
super().__init__("QRZ.com", provider_config, DATA_STORE.callsign_data_qrz)
self._QRZ_BASE_URL = "https://xmldata.qrz.com/xml/current/"
self._URL_DATA_CACHE = URLDataCache("qrz")
@@ -29,7 +33,7 @@ class QRZ(APIQueryCallsignDataProvider):
# and password, this is valid for an hour, so our cache stores this specifically for 55 minutes.
self._CREDENTIALS_CACHE = CachedSession(f"{CACHE_DIR}/urls/qrz-creds", expire_after=timedelta(minutes=55))
def _perform_new_lookup(self, callsign, lookup_credentials):
def _perform_new_lookup(self, callsign: str, lookup_credentials: LookupCredentials | None) -> Callsign | None:
# If we don't have QRZ credentials, skip this lookup. Return None so we don't *cache* the lack of data, because
# someone might provide credentials next time around.
if not lookup_credentials or not (
@@ -125,7 +129,7 @@ class QRZ(APIQueryCallsignDataProvider):
return None
@staticmethod
def qrz_response_to_callsign(callsign, data):
def qrz_response_to_callsign(callsign: str, data: dict[str, Any] | list[Any]) -> Callsign:
"""Convert the "Callsign" block in QRZ's API response to our own Callsign object."""
# I have encountered a user passing multiple callsigns to the QRZ lookup function in a way that QRZ actually