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