Replace root logger calls with module-specific loggers

This commit is contained in:
Ian Renton
2026-08-15 08:35:06 +01:00
parent 74bcd9cded
commit dd89ff4af7
64 changed files with 328 additions and 216 deletions
+3 -1
View File
@@ -6,6 +6,8 @@ import pytz
from core.data_store import DATA_STORE from core.data_store import DATA_STORE
logger = logging.getLogger(__name__)
class CleanupTimer: class CleanupTimer:
"""Provides a timed cleanup of the spot list.""" """Provides a timed cleanup of the spot list."""
@@ -63,7 +65,7 @@ class CleanupTimer:
except Exception: except Exception:
self.status = "Error" self.status = "Error"
logging.exception("Exception in Cleanup thread") logger.exception("Exception in Cleanup thread")
self._stop_event.wait(timeout=1) self._stop_event.wait(timeout=1)
+4 -2
View File
@@ -4,9 +4,11 @@ import os
import yaml import yaml
logger = logging.getLogger(__name__)
# Check you have a config file # Check you have a config file
if not os.path.isfile("config.yml"): if not os.path.isfile("config.yml"):
logging.error( logger.error(
"Your config file is missing. Ensure you have copied config-example.yml to config.yml and updated it according to your needs." "Your config file is missing. Ensure you have copied config-example.yml to config.yml and updated it according to your needs."
) )
exit() exit()
@@ -14,7 +16,7 @@ if not os.path.isfile("config.yml"):
# Load config # Load config
with open("config.yml") as f: with open("config.yml") as f:
config = yaml.safe_load(f) config = yaml.safe_load(f)
logging.info("Loaded config.") logger.info("Loaded config.")
BASE_URL = config.get("base_url", "http://localhost:8080") BASE_URL = config.get("base_url", "http://localhost:8080")
MAX_SPOT_AGE = config.get("max_spot_age_sec", 3600) MAX_SPOT_AGE = config.get("max_spot_age_sec", 3600)
+3 -1
View File
@@ -3,6 +3,8 @@ import threading
from core.config import config, create_provider_from_config from core.config import config, create_provider_from_config
logger = logging.getLogger(__name__)
class DataProviders: class DataProviders:
"""Global object for storing data providers.""" """Global object for storing data providers."""
@@ -33,7 +35,7 @@ class DataProviders:
def start_providers(providers, provider_type): def start_providers(providers, provider_type):
"""Helper method to activate enabled providers in the list.""" """Helper method to activate enabled providers in the list."""
logging.info(f"Starting {provider_type} providers...") logger.info(f"Starting {provider_type} providers...")
for p in providers: for p in providers:
if p.enabled: if p.enabled:
p.start() p.start()
+6 -4
View File
@@ -8,6 +8,8 @@ from core.config import MAX_ALERT_AGE, MAX_SPOT_AGE
from core.live_data_cache import LiveDataCache from core.live_data_cache import LiveDataCache
from data.solar_conditions import SolarConditions from data.solar_conditions import SolarConditions
logger = logging.getLogger(__name__)
CACHE_DIR = "./cache/" CACHE_DIR = "./cache/"
@@ -64,7 +66,7 @@ class DataStore:
# of dict in diskcache absolutely destroys performance with unpickling huge dicts, so we have an ugly "SIG:ref" # of dict in diskcache absolutely destroys performance with unpickling huge dicts, so we have an ugly "SIG:ref"
# syntax for keys to keep it a single level. # syntax for keys to keep it a single level.
self.sigrefs = diskcache.Cache(f"{CACHE_DIR}sigrefs") self.sigrefs = diskcache.Cache(f"{CACHE_DIR}sigrefs")
logging.info(f"Loaded data for {len(self.sigrefs)} SIG references.") logger.info(f"Loaded data for {len(self.sigrefs)} SIG references.")
# Standard disk cache for callsign data. This data does have a TTL to trigger an occasional re-lookup. # Standard disk cache for callsign data. This data does have a TTL to trigger an occasional re-lookup.
# Old data *is* better than no data, but we can't have a background thread re-looking-up every callsign # Old data *is* better than no data, but we can't have a background thread re-looking-up every callsign
@@ -83,7 +85,7 @@ class DataStore:
self.callsign_data_hamqth, self.callsign_data_hamqth,
]: ]:
unique_keys.update(c) unique_keys.update(c)
logging.info(f"Loaded data for {len(unique_keys)} callsigns.") logger.info(f"Loaded data for {len(unique_keys)} callsigns.")
# Special caches for spots and alerts, which have TTL and write snapshots to disk at an interval. We # Special caches for spots and alerts, which have TTL and write snapshots to disk at an interval. We
# specifically load these caches *last* so that any sigref and callsign data is already loaded from disk cache # specifically load these caches *last* so that any sigref and callsign data is already loaded from disk cache
@@ -94,7 +96,7 @@ class DataStore:
snapshot_dir=f"{CACHE_DIR}spots", snapshot_dir=f"{CACHE_DIR}spots",
snapshot_interval_sec=self._SPOT_ALERT_SNAPSHOT_INTERVAL_SEC, snapshot_interval_sec=self._SPOT_ALERT_SNAPSHOT_INTERVAL_SEC,
) )
logging.info(f"Loaded {len(self.spots.keys())} spots from a previous run.") logger.info(f"Loaded {len(self.spots.keys())} spots from a previous run.")
self.alerts = LiveDataCache( self.alerts = LiveDataCache(
maxsize=self._MAX_ALERT_COUNT, maxsize=self._MAX_ALERT_COUNT,
@@ -102,7 +104,7 @@ class DataStore:
snapshot_dir=f"{CACHE_DIR}alerts", snapshot_dir=f"{CACHE_DIR}alerts",
snapshot_interval_sec=self._SPOT_ALERT_SNAPSHOT_INTERVAL_SEC, snapshot_interval_sec=self._SPOT_ALERT_SNAPSHOT_INTERVAL_SEC,
) )
logging.info(f"Loaded {len(self.alerts.keys())} alerts from a previous run.") logger.info(f"Loaded {len(self.alerts.keys())} alerts from a previous run.")
def regenerate_call_regex_to_dxcc_entity_map(self): def regenerate_call_regex_to_dxcc_entity_map(self):
"""DXCC entity data from K0SWE includes a regex which we can use to match a callsign, and determine which DXCC """DXCC entity data from K0SWE includes a regex which we can use to match a callsign, and determine which DXCC
+3 -1
View File
@@ -7,6 +7,8 @@ from shapely.geometry import Point, Polygon
from core.data_store import DATA_STORE from core.data_store import DATA_STORE
logger = logging.getLogger(__name__)
TRANSFORMER_OS_GRID_TO_WGS84 = Transformer.from_crs("EPSG:27700", "EPSG:4326") TRANSFORMER_OS_GRID_TO_WGS84 = Transformer.from_crs("EPSG:27700", "EPSG:4326")
TRANSFORMER_IRISH_GRID_TO_WGS84 = Transformer.from_crs("EPSG:29903", "EPSG:4326") TRANSFORMER_IRISH_GRID_TO_WGS84 = Transformer.from_crs("EPSG:29903", "EPSG:4326")
TRANSFORMER_CI_UTM_GRID_TO_WGS84 = Transformer.from_crs("+proj=utm +zone=30 +ellps=WGS84", "EPSG:4326") TRANSFORMER_CI_UTM_GRID_TO_WGS84 = Transformer.from_crs("+proj=utm +zone=30 +ellps=WGS84", "EPSG:4326")
@@ -172,7 +174,7 @@ def wab_wai_square_to_lat_lon(ref):
elif re.match(r"^W[AV][0-9]{2}$", ref): elif re.match(r"^W[AV][0-9]{2}$", ref):
return utm_grid_square_to_lat_lon(ref) return utm_grid_square_to_lat_lon(ref)
else: else:
logging.warning(f"Invalid WAB/WAI square: {ref}") logger.warning(f"Invalid WAB/WAI square: {ref}")
return None return None
+5 -3
View File
@@ -5,6 +5,8 @@ import time
import diskcache import diskcache
from cachetools import TTLCache from cachetools import TTLCache
logger = logging.getLogger(__name__)
class LiveDataCache: class LiveDataCache:
"""Cache for spots and alerts. Uses the fast in-memory TTLCache for normal data I/O, including the TTL to enforce """Cache for spots and alerts. Uses the fast in-memory TTLCache for normal data I/O, including the TTL to enforce
@@ -34,7 +36,7 @@ class LiveDataCache:
try: try:
callback(value) callback(value)
except Exception: except Exception:
logging.exception(f"Listener raised an exception for key {key}") logger.exception(f"Listener raised an exception for key {key}")
def get(self, key, default=None): def get(self, key, default=None):
with self._lock: with self._lock:
@@ -70,7 +72,7 @@ class LiveDataCache:
try: try:
self._disk_cache.set("snapshot", data) self._disk_cache.set("snapshot", data)
except Exception as e: except Exception as e:
logging.exception(f"Failed to write snapshot to {self._snapshot_dir}") logger.exception(f"Failed to write snapshot to {self._snapshot_dir}")
def _load_snapshot(self): def _load_snapshot(self):
data = self._disk_cache.get("snapshot") data = self._disk_cache.get("snapshot")
@@ -83,7 +85,7 @@ class LiveDataCache:
# Only restore entries that would still be within TTL # Only restore entries that would still be within TTL
if now - saved_at < self._ttl: if now - saved_at < self._ttl:
self._cache[key] = value self._cache[key] = value
logging.info(f"Loaded snapshot from {self._snapshot_dir}") logger.info(f"Loaded snapshot from {self._snapshot_dir}")
def _start_periodic_snapshot(self, interval): def _start_periodic_snapshot(self, interval):
def loop(): def loop():
+6 -4
View File
@@ -6,6 +6,8 @@ from core.data_store import DATA_STORE
from core.geo_utils import wab_wai_square_to_lat_lon from core.geo_utils import wab_wai_square_to_lat_lon
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
logger = logging.getLogger(__name__)
def get_sig_ref_info(sig, ref_id): def get_sig_ref_info(sig, ref_id):
"""Look up details of a SIG reference (e.g. POTA park) such as name, lat/lon, and grid. Takes in a sig name and """Look up details of a SIG reference (e.g. POTA park) such as name, lat/lon, and grid. Takes in a sig name and
@@ -14,7 +16,7 @@ def get_sig_ref_info(sig, ref_id):
SIG we are getting data for.""" SIG we are getting data for."""
if sig is None or sig == "" or ref_id is None or ref_id == "": if sig is None or sig == "" or ref_id is None or ref_id == "":
logging.debug("Failed to look up sig_ref info, sig or ref were not set.") logger.debug("Failed to look up sig_ref info, sig or ref were not set.")
return None return None
sig_ref = SIGRef(sig=sig, id=ref_id) sig_ref = SIGRef(sig=sig, id=ref_id)
@@ -59,7 +61,7 @@ def get_sig_ref_info(sig, ref_id):
sig_ref.latitude = ll[0] sig_ref.latitude = ll[0]
sig_ref.longitude = ll[1] sig_ref.longitude = ll[1]
except: except:
logging.warning("Invalid lat/lon received for WAB/WAI reference") logger.warning("Invalid lat/lon received for WAB/WAI reference")
return sig_ref return sig_ref
elif sig.upper() == "BOTA": elif sig.upper() == "BOTA":
@@ -83,10 +85,10 @@ def get_sig_ref_info(sig, ref_id):
else: else:
# Maybe a super new reference we don't know about yet, but more likely a typo or a test reference, # Maybe a super new reference we don't know about yet, but more likely a typo or a test reference,
# just silently ignore it. # just silently ignore it.
logging.debug(f"{sig} database did not contain data for ref {ref_id}") logger.debug(f"{sig} database did not contain data for ref {ref_id}")
except Exception: except Exception:
logging.exception(f"Exception when looking up sig_ref info for {sig} ref {ref_id}") logger.exception(f"Exception when looking up sig_ref info for {sig} ref {ref_id}")
return sig_ref return sig_ref
+3 -1
View File
@@ -16,6 +16,8 @@ from core.constants import (
from core.data_store import DATA_STORE from core.data_store import DATA_STORE
from data.callsign import Callsign from data.callsign import Callsign
logger = logging.getLogger(__name__)
def safe_json_dumps(obj): def safe_json_dumps(obj):
"""Safe version of json.dumps that also converts objects to dicts so they can be output, and ignores NaN floats """Safe version of json.dumps that also converts objects to dicts so they can be output, and ignores NaN floats
@@ -47,7 +49,7 @@ def infer_mode_type_from_mode(mode):
return "DATA" return "DATA"
else: else:
if mode.upper() != "OTHER": if mode.upper() != "OTHER":
logging.warning(f"Found an unrecognised mode: {mode}. Developer should categorise this.") logger.warning(f"Found an unrecognised mode: {mode}. Developer should categorise this.")
return None return None
+3 -1
View File
@@ -10,6 +10,8 @@ from core.call_lookup_helper import get_call_info
from core.sig_lookup_helper import populate_missing_sig_ref_info from core.sig_lookup_helper import populate_missing_sig_ref_info
from core.utils import get_flag_for_dxcc from core.utils import get_flag_for_dxcc
logger = logging.getLogger(__name__)
@dataclass @dataclass
class Alert: class Alert:
@@ -128,7 +130,7 @@ class Alert:
self.dx_names = list(map(lambda c: get_call_info(c, credentials).name, self.dx_calls)) self.dx_names = list(map(lambda c: get_call_info(c, credentials).name, self.dx_calls))
except Exception: except Exception:
logging.exception("Exception while inferring missing data from spot") logger.exception("Exception while inferring missing data from spot")
def to_json(self): def to_json(self):
"""JSON serialise""" """JSON serialise"""
+6 -4
View File
@@ -27,6 +27,8 @@ from core.utils import (
) )
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
logger = logging.getLogger(__name__)
@dataclass @dataclass
class Spot: class Spot:
@@ -340,7 +342,7 @@ class Spot:
self.propagation_mode = PROPAGATION_MODES[mode_tag] self.propagation_mode = PROPAGATION_MODES[mode_tag]
else: else:
self.propagation_mode = mode_tag self.propagation_mode = mode_tag
logging.info(f"Seen a new propagation mode tag not yet in the system: {mode_tag}") logger.info(f"Seen a new propagation mode tag not yet in the system: {mode_tag}")
# Parse "de_grid -> dx_grid" structures from the comment # Parse "de_grid -> dx_grid" structures from the comment
if self.comment: if self.comment:
@@ -363,12 +365,12 @@ class Spot:
self.dx_latitude = ll[0] self.dx_latitude = ll[0]
self.dx_longitude = ll[1] self.dx_longitude = ll[1]
except: except:
logging.debug("Invalid grid received for spot") logger.debug("Invalid grid received for spot")
if self.dx_latitude and self.dx_longitude and not self.dx_grid: if self.dx_latitude and self.dx_longitude and not self.dx_grid:
try: try:
self.dx_grid = latlong_to_locator(self.dx_latitude, self.dx_longitude, 8) self.dx_grid = latlong_to_locator(self.dx_latitude, self.dx_longitude, 8)
except: except:
logging.debug("Invalid lat/lon received for spot") logger.debug("Invalid lat/lon received for spot")
# QRT comment detection # QRT comment detection
if self.comment and not self.qrt: if self.comment and not self.qrt:
@@ -444,7 +446,7 @@ class Spot:
self.de_grid = de_call_info.grid self.de_grid = de_call_info.grid
except Exception: except Exception:
logging.exception("Exception while inferring missing data from spot") logger.exception("Exception while inferring missing data from spot")
def to_json(self): def to_json(self):
"""JSON serialise""" """JSON serialise"""
+9 -7
View File
@@ -9,6 +9,8 @@ from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout
from core.constants import HTTP_HEADERS from core.constants import HTTP_HEADERS
from providers.alert.alert_provider import AlertProvider from providers.alert.alert_provider import AlertProvider
logger = logging.getLogger(__name__)
class HTTPAlertProvider(AlertProvider): class HTTPAlertProvider(AlertProvider):
"""Generic alert provider class for providers that request data via HTTP(S). Just for convenience to avoid code """Generic alert provider class for providers that request data via HTTP(S). Just for convenience to avoid code
@@ -24,7 +26,7 @@ class HTTPAlertProvider(AlertProvider):
def start(self): def start(self):
# Fire off the polling thread. It will poll immediately on startup, then sleep for poll_interval between # 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. # subsequent polls, so start() returns immediately and the application can continue starting.
logging.info(f"Set up query of {self.name} alert API every {self._poll_interval!s} seconds.") 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}") self._thread = Thread(target=self._run, name=f"HTTPAlertProvider-{self.name}")
self._thread.start() self._thread.start()
@@ -40,7 +42,7 @@ class HTTPAlertProvider(AlertProvider):
def _poll(self): def _poll(self):
try: try:
# Request data from API # Request data from API
logging.debug(f"Polling {self.name} alert API...") logger.debug(f"Polling {self.name} alert API...")
http_response = requests.get(self._url, headers=HTTP_HEADERS, timeout=(5, 30)) http_response = requests.get(self._url, headers=HTTP_HEADERS, timeout=(5, 30))
# Check response code was good # Check response code was good
if http_response.ok: if http_response.ok:
@@ -52,18 +54,18 @@ class HTTPAlertProvider(AlertProvider):
self.status = "OK" self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC) self.last_update_time = datetime.now(pytz.UTC)
logging.debug(f"Received data from {self.name} alert API.") logger.debug(f"Received data from {self.name} alert API.")
else: else:
self.status = "Error" self.status = "Error"
logging.warning(f"HTTP {http_response.status_code} when calling {self.name} alerts API.") logger.warning(f"HTTP {http_response.status_code} when calling {self.name} alerts API.")
except ConnectionError: except ConnectionError:
logging.warning(f"Connection error when accessing {self.name} alerts API.") logger.warning(f"Connection error when accessing {self.name} alerts API.")
except (ConnectTimeout, ReadTimeout): except (ConnectTimeout, ReadTimeout):
logging.warning(f"Timeout when accessing {self.name} alerts API.") logger.warning(f"Timeout when accessing {self.name} alerts API.")
except Exception: except Exception:
self.status = "Error" self.status = "Error"
logging.exception(f"Exception in HTTP JSON Alert Provider ({self.name})") logger.exception(f"Exception in HTTP JSON Alert Provider ({self.name})")
# Brief pause on error before the next poll, but still respond promptly to stop() # Brief pause on error before the next poll, but still respond promptly to stop()
self._stop_event.wait(timeout=1) self._stop_event.wait(timeout=1)
+3 -1
View File
@@ -7,6 +7,8 @@ from data.alert import Alert
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from providers.alert.http_alert_provider import HTTPAlertProvider from providers.alert.http_alert_provider import HTTPAlertProvider
logger = logging.getLogger(__name__)
class ParksNPeaks(HTTPAlertProvider): class ParksNPeaks(HTTPAlertProvider):
"""Alert provider for Parks n Peaks""" """Alert provider for Parks n Peaks"""
@@ -64,7 +66,7 @@ class ParksNPeaks(HTTPAlertProvider):
"LLOTA", "LLOTA",
"QRP", "QRP",
]: ]:
logging.warning(f"PNP alert found with sig {sig}, developer needs to add support for this!") logger.warning(f"PNP alert found with sig {sig}, developer needs to add support for this!")
# If this is POTA, SOTA or WWFF data we already have it through other means, so ignore. Otherwise, add to # If this is POTA, SOTA or WWFF data we already have it through other means, so ignore. Otherwise, add to
# the alert list. Note that while ZLOTA has its own spots API, it doesn't have its own alerts API. So that # the alert list. Note that while ZLOTA has its own spots API, it doesn't have its own alerts API. So that
+4 -2
View File
@@ -11,6 +11,8 @@ from providers.callsigndata.api_query_callsign_data_provider import (
APIQueryCallsignDataProvider, APIQueryCallsignDataProvider,
) )
logger = logging.getLogger(__name__)
class ClublogAPI(APIQueryCallsignDataProvider): class ClublogAPI(APIQueryCallsignDataProvider):
"""Callsign data provider for Clublog's API.""" """Callsign data provider for Clublog's API."""
@@ -25,7 +27,7 @@ class ClublogAPI(APIQueryCallsignDataProvider):
self._callinfo = Callinfo(lookuplib) self._callinfo = Callinfo(lookuplib)
else: else:
provider_config["enabled"] = False provider_config["enabled"] = False
logging.warning( logger.warning(
"Clublog XML callsign data provider configured but no api key was provided, this has been disabled." "Clublog XML callsign data provider configured but no api key was provided, this has been disabled."
) )
@@ -45,6 +47,6 @@ class ClublogAPI(APIQueryCallsignDataProvider):
except Exception: except Exception:
self.status = "Error" self.status = "Error"
logging.exception("Exception when looking up data from Clublog API") logger.exception("Exception when looking up data from Clublog API")
return callsign_data return callsign_data
+5 -3
View File
@@ -10,6 +10,8 @@ from providers.callsigndata.file_download_callsign_data_provider import (
FileDownloadCallsignDataProvider, FileDownloadCallsignDataProvider,
) )
logger = logging.getLogger(__name__)
class ClublogXML(FileDownloadCallsignDataProvider): class ClublogXML(FileDownloadCallsignDataProvider):
"""Callsign data provider for ClubLog's Country File, which provides basic callsign to DXCC entity mapping.""" """Callsign data provider for ClubLog's Country File, which provides basic callsign to DXCC entity mapping."""
@@ -25,7 +27,7 @@ class ClublogXML(FileDownloadCallsignDataProvider):
self._api_key = provider_config.get("api_key", "") self._api_key = provider_config.get("api_key", "")
if self._api_key == "": if self._api_key == "":
provider_config["enabled"] = False provider_config["enabled"] = False
logging.warning( logger.warning(
"Clublog XML callsign data provider configured but no api key was provided, this has been disabled." "Clublog XML callsign data provider configured but no api key was provided, this has been disabled."
) )
@@ -55,7 +57,7 @@ class ClublogXML(FileDownloadCallsignDataProvider):
return True return True
except Exception: except Exception:
logging.exception("Exception when loading Clublog XML.") logger.exception("Exception when loading Clublog XML.")
return False return False
def _perform_new_lookup(self, callsign, lookup_credentials): def _perform_new_lookup(self, callsign, lookup_credentials):
@@ -71,6 +73,6 @@ class ClublogXML(FileDownloadCallsignDataProvider):
except Exception: except Exception:
self.status = "Error" self.status = "Error"
logging.exception("Exception when looking up data from Clublog XML data") logger.exception("Exception when looking up data from Clublog XML data")
return callsign_data return callsign_data
+4 -2
View File
@@ -9,6 +9,8 @@ from providers.callsigndata.file_download_callsign_data_provider import (
FileDownloadCallsignDataProvider, FileDownloadCallsignDataProvider,
) )
logger = logging.getLogger(__name__)
class CountryFiles(FileDownloadCallsignDataProvider): class CountryFiles(FileDownloadCallsignDataProvider):
"""Callsign data provider for Country-files.com, which provides basic callsign to DXCC entity mapping.""" """Callsign data provider for Country-files.com, which provides basic callsign to DXCC entity mapping."""
@@ -35,7 +37,7 @@ class CountryFiles(FileDownloadCallsignDataProvider):
return True return True
except Exception: except Exception:
logging.exception("Exception when loading Country Files cty.plist.") logger.exception("Exception when loading Country Files cty.plist.")
return False return False
def _perform_new_lookup(self, callsign, lookup_credentials): def _perform_new_lookup(self, callsign, lookup_credentials):
@@ -51,6 +53,6 @@ class CountryFiles(FileDownloadCallsignDataProvider):
except Exception: except Exception:
self.status = "Error" self.status = "Error"
logging.exception("Exception when looking up data from Country file") logger.exception("Exception when looking up data from Country file")
return callsign_data return callsign_data
@@ -10,6 +10,8 @@ from core.constants import HTTP_HEADERS
from core.url_data_cache import URLDataCache from core.url_data_cache import URLDataCache
from providers.callsigndata.callsign_data_provider import CallsignDataProvider from providers.callsigndata.callsign_data_provider import CallsignDataProvider
logger = logging.getLogger(__name__)
class FileDownloadCallsignDataProvider(CallsignDataProvider): class FileDownloadCallsignDataProvider(CallsignDataProvider):
"""Generic callsign data provider class for providers that fetch their data from the web by downloading a file.""" """Generic callsign data provider class for providers that fetch their data from the web by downloading a file."""
@@ -30,7 +32,7 @@ class FileDownloadCallsignDataProvider(CallsignDataProvider):
def start(self): def start(self):
# Fire off the polling thread. It will poll immediately on startup, then sleep for poll_interval between # 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. # subsequent polls, so start() returns immediately and the application can continue starting.
logging.info(f"Set up query of {self.name} callsign reference data every {self._poll_interval!s} days.") 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}") self._thread = Thread(target=self._run, name=f"FileDownloadCallsignDataProvider-{self.name}")
self._thread.start() self._thread.start()
@@ -47,7 +49,7 @@ class FileDownloadCallsignDataProvider(CallsignDataProvider):
try: try:
# Request the file. Use the data cache (with a TTL of 1 day) here, not as the main mechanism for # 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. # caching, but just so continual restarts of the software during testing don't hammer the servers.
logging.debug(f"Downloading {self.name} callsign reference data...") logger.debug(f"Downloading {self.name} callsign reference data...")
http_response = self._url_data_cache.get(self._url, headers=HTTP_HEADERS) http_response = self._url_data_cache.get(self._url, headers=HTTP_HEADERS)
# Check response code was good # Check response code was good
if http_response.ok: if http_response.ok:
@@ -60,26 +62,26 @@ class FileDownloadCallsignDataProvider(CallsignDataProvider):
if ok: if ok:
self.status = "OK" self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC) self.last_update_time = datetime.now(pytz.UTC)
logging.info(f"Updated callsign reference data from {self.name}") logger.info(f"Updated callsign reference data from {self.name}")
else: else:
self.status = "Error" self.status = "Error"
logging.warning(f"Error updating callsign reference data from {self.name}.") logger.warning(f"Error updating callsign reference data from {self.name}.")
else: else:
self.status = "Error" self.status = "Error"
logging.warning( logger.warning(
f"HTTP {http_response.status_code} when downloading callsign reference data from {self.name}." f"HTTP {http_response.status_code} when downloading callsign reference data from {self.name}."
) )
except ConnectionError: except ConnectionError:
self.status = "Error" self.status = "Error"
logging.warning(f"Connection error when downloading callsign reference data from {self.name}.") logger.warning(f"Connection error when downloading callsign reference data from {self.name}.")
except (ConnectTimeout, ReadTimeout): except (ConnectTimeout, ReadTimeout):
self.status = "Error" self.status = "Error"
logging.warning(f"Timeout when downloading callsign reference data from {self.name}.") logger.warning(f"Timeout when downloading callsign reference data from {self.name}.")
except Exception: except Exception:
self.status = "Error" self.status = "Error"
logging.exception(f"Exception in callsign reference data provider ({self.name})") logger.exception(f"Exception in callsign reference data provider ({self.name})")
self._stop_event.wait(timeout=1) self._stop_event.wait(timeout=1)
def _handle_file(self, path): def _handle_file(self, path):
+10 -8
View File
@@ -17,6 +17,8 @@ from providers.callsigndata.api_query_callsign_data_provider import (
APIQueryCallsignDataProvider, APIQueryCallsignDataProvider,
) )
logger = logging.getLogger(__name__)
class HamQTH(APIQueryCallsignDataProvider): class HamQTH(APIQueryCallsignDataProvider):
"""Callsign data provider for HamQTH.""" """Callsign data provider for HamQTH."""
@@ -55,10 +57,10 @@ class HamQTH(APIQueryCallsignDataProvider):
session_id = str(dict_data["HamQTH"]["session"]["session_id"]) session_id = str(dict_data["HamQTH"]["session"]["session_id"])
else: else:
# Log this failure at debug level only, not our problem if user entered the wrong password. # Log this failure at debug level only, not our problem if user entered the wrong password.
logging.debug("HamQTH login details incorrect, failed to look up with HamQTH.") logger.debug("HamQTH login details incorrect, failed to look up with HamQTH.")
return None return None
except Exception: except Exception:
logging.error("Exception when getting HamQTH session key") logger.error("Exception when getting HamQTH session key")
return None return None
if not session_id: if not session_id:
@@ -71,7 +73,7 @@ class HamQTH(APIQueryCallsignDataProvider):
if home_call != callsign: if home_call != callsign:
calls_to_try.append(home_call) calls_to_try.append(home_call)
except ValueError: except ValueError:
logging.debug(f"Could not look up home call for callsign {callsign}") logger.debug(f"Could not look up home call for callsign {callsign}")
# Try looking up each call using the API # Try looking up each call using the API
for lookup_call in calls_to_try: for lookup_call in calls_to_try:
@@ -90,18 +92,18 @@ class HamQTH(APIQueryCallsignDataProvider):
return self.hamqth_response_to_callsign(callsign, data) return self.hamqth_response_to_callsign(callsign, data)
elif not response.from_cache: elif not response.from_cache:
logging.warning(f"HTTP {response.status_code} looking up callsign {lookup_call} using HamQTH") logger.warning(f"HTTP {response.status_code} looking up callsign {lookup_call} using HamQTH")
except (KeyError, ValueError): except (KeyError, ValueError):
continue continue
except ConnectionError: except ConnectionError:
logging.warning(f"Connection error when looking up callsign {lookup_call} using HamQTH") logger.warning(f"Connection error when looking up callsign {lookup_call} using HamQTH")
continue continue
except (ConnectTimeout, ReadTimeout): except (ConnectTimeout, ReadTimeout):
logging.warning(f"Timeout when looking up callsign {lookup_call} using HamQTH") logger.warning(f"Timeout when looking up callsign {lookup_call} using HamQTH")
continue continue
except Exception: except Exception:
logging.exception(f"Exception when looking up callsign {lookup_call} using HamQTH") logger.exception(f"Exception when looking up callsign {lookup_call} using HamQTH")
continue continue
# Not found in HamQTH; return a Callsign object with no data so we cache that and don't keep retrying # Not found in HamQTH; return a Callsign object with no data so we cache that and don't keep retrying
@@ -109,7 +111,7 @@ class HamQTH(APIQueryCallsignDataProvider):
except Exception: except Exception:
self.status = "Error" self.status = "Error"
logging.exception("Exception when looking up data from HamQTH") logger.exception("Exception when looking up data from HamQTH")
# Return None, this won't be cached so we will be asked to query data again for this call next time. # Return None, this won't be cached so we will be asked to query data again for this call next time.
return None return None
+12 -10
View File
@@ -16,6 +16,8 @@ from providers.callsigndata.api_query_callsign_data_provider import (
APIQueryCallsignDataProvider, APIQueryCallsignDataProvider,
) )
logger = logging.getLogger(__name__)
class QRZ(APIQueryCallsignDataProvider): class QRZ(APIQueryCallsignDataProvider):
"""Callsign data provider for QRZ.com.""" """Callsign data provider for QRZ.com."""
@@ -53,10 +55,10 @@ class QRZ(APIQueryCallsignDataProvider):
session_key = str(session["Key"]) session_key = str(session["Key"])
else: else:
# Log this failure at debug level only, not our problem if user entered the wrong password. # Log this failure at debug level only, not our problem if user entered the wrong password.
logging.debug("QRZ.com login details incorrect, failed to look up with QRZ.") logger.debug("QRZ.com login details incorrect, failed to look up with QRZ.")
return None return None
except Exception: except Exception:
logging.error("Exception when getting QRZ.com session key") logger.error("Exception when getting QRZ.com session key")
return None return None
if not session_key: if not session_key:
@@ -69,7 +71,7 @@ class QRZ(APIQueryCallsignDataProvider):
if home_call != callsign: if home_call != callsign:
calls_to_try.append(home_call) calls_to_try.append(home_call)
except ValueError: except ValueError:
logging.debug(f"Could not look up home call for callsign {callsign}") logger.debug(f"Could not look up home call for callsign {callsign}")
# Try looking up each call using the API # Try looking up each call using the API
for lookup_call in calls_to_try: for lookup_call in calls_to_try:
@@ -93,25 +95,25 @@ class QRZ(APIQueryCallsignDataProvider):
elif "Session" in qrz_response and "Error" in qrz_response.get("Session"): elif "Session" in qrz_response and "Error" in qrz_response.get("Session"):
# Errors here are normally just "callsign not in database", no need to log that ourselves # Errors here are normally just "callsign not in database", no need to log that ourselves
# above debug level. # above debug level.
logging.debug( logger.debug(
f"QRZ returned an error looking up callsign {lookup_call}: {qrz_response.get('Session').get('Error')}" f"QRZ returned an error looking up callsign {lookup_call}: {qrz_response.get('Session').get('Error')}"
) )
elif not response.from_cache: elif not response.from_cache:
logging.warning(f"QRZ returned a malformed response looking up callsign {lookup_call}") logger.warning(f"QRZ returned a malformed response looking up callsign {lookup_call}")
elif not response.from_cache: elif not response.from_cache:
logging.warning(f"HTTP {response.status_code} looking up callsign {lookup_call} using QRZ") logger.warning(f"HTTP {response.status_code} looking up callsign {lookup_call} using QRZ")
except (KeyError, ValueError): except (KeyError, ValueError):
continue continue
except ConnectionError: except ConnectionError:
logging.warning(f"Connection error when looking up callsign {lookup_call} using QRZ") logger.warning(f"Connection error when looking up callsign {lookup_call} using QRZ")
continue continue
except (ConnectTimeout, ReadTimeout): except (ConnectTimeout, ReadTimeout):
logging.warning(f"Timeout when looking up callsign {lookup_call} using QRZ.") logger.warning(f"Timeout when looking up callsign {lookup_call} using QRZ.")
continue continue
except Exception: except Exception:
logging.exception(f"Exception when looking up callsign {lookup_call} using QRZ") logger.exception(f"Exception when looking up callsign {lookup_call} using QRZ")
continue continue
# Not found in QRZ; return a Callsign object with no data so we cache that and don't keep retrying # Not found in QRZ; return a Callsign object with no data so we cache that and don't keep retrying
@@ -119,7 +121,7 @@ class QRZ(APIQueryCallsignDataProvider):
except Exception: except Exception:
self.status = "Error" self.status = "Error"
logging.exception("Exception when looking up data from QRZ.com") logger.exception("Exception when looking up data from QRZ.com")
# Return None, this won't be cached so we will be asked to query data again for this call next time. # Return None, this won't be cached so we will be asked to query data again for this call next time.
return None return None
@@ -10,6 +10,8 @@ from core.constants import HTTP_HEADERS
from core.url_data_cache import URLDataCache from core.url_data_cache import URLDataCache
from providers.sigrefdata.sig_ref_data_provider import SIGRefDataProvider from providers.sigrefdata.sig_ref_data_provider import SIGRefDataProvider
logger = logging.getLogger(__name__)
class FileDownloadSIGRefDataProvider(SIGRefDataProvider): class FileDownloadSIGRefDataProvider(SIGRefDataProvider):
"""Generic SIG ref data provider class for providers that fetch their data from the web by downloading a file.""" """Generic SIG ref data provider class for providers that fetch their data from the web by downloading a file."""
@@ -26,7 +28,7 @@ class FileDownloadSIGRefDataProvider(SIGRefDataProvider):
def start(self): def start(self):
# Fire off the polling thread. It will poll immediately on startup, then sleep for poll_interval between # 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. # subsequent polls, so start() returns immediately and the application can continue starting.
logging.info(f"Set up query of {self.sig_name} SIG ref data every {self._poll_interval!s} days.") logger.info(f"Set up query of {self.sig_name} SIG ref data every {self._poll_interval!s} days.")
self._thread = Thread(target=self._run, name=f"FileDownloadSIGRefDataProvider-{self.sig_name}") self._thread = Thread(target=self._run, name=f"FileDownloadSIGRefDataProvider-{self.sig_name}")
self._thread.start() self._thread.start()
@@ -44,7 +46,7 @@ class FileDownloadSIGRefDataProvider(SIGRefDataProvider):
try: try:
# Request data from API. Use the data cache (with a TTL of 1 day) here, not as the main mechanism for # Request data from API. 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. # caching, but just so continual restarts of the software during testing don't hammer the servers.
logging.debug(f"Downloading {self.sig_name} SIG ref data...") logger.debug(f"Downloading {self.sig_name} SIG ref data...")
http_response = self._url_data_cache.get(self._url, headers=HTTP_HEADERS) http_response = self._url_data_cache.get(self._url, headers=HTTP_HEADERS)
# Check response code was good # Check response code was good
if http_response.ok: if http_response.ok:
@@ -56,20 +58,20 @@ class FileDownloadSIGRefDataProvider(SIGRefDataProvider):
self.status = "OK" self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC) self.last_update_time = datetime.now(pytz.UTC)
logging.debug(f"Received SIG ref data for {self.sig_name}") logger.debug(f"Received SIG ref data for {self.sig_name}")
else: else:
self.status = "Error" self.status = "Error"
logging.warning(f"HTTP {http_response.status_code} when downloading SIG ref data for {self.sig_name}.") logger.warning(f"HTTP {http_response.status_code} when downloading SIG ref data for {self.sig_name}.")
except ConnectionError: except ConnectionError:
self.status = "Error" self.status = "Error"
logging.warning(f"Connection error when downloading SIG ref data for {self.sig_name}.") logger.warning(f"Connection error when downloading SIG ref data for {self.sig_name}.")
except (ConnectTimeout, ReadTimeout): except (ConnectTimeout, ReadTimeout):
self.status = "Error" self.status = "Error"
logging.warning(f"Timeout when downloading SIG ref data for {self.sig_name}.") logger.warning(f"Timeout when downloading SIG ref data for {self.sig_name}.")
except Exception: except Exception:
self.status = "Error" self.status = "Error"
logging.exception(f"Exception in HTTP SIG Ref Data Provider ({self.sig_name})") logger.exception(f"Exception in HTTP SIG Ref Data Provider ({self.sig_name})")
self._stop_event.wait(timeout=1) self._stop_event.wait(timeout=1)
def _http_response_to_data(self, http_response): def _http_response_to_data(self, http_response):
+3 -1
View File
@@ -8,6 +8,8 @@ from providers.sigrefdata.file_download_sig_ref_data_provider import (
FileDownloadSIGRefDataProvider, FileDownloadSIGRefDataProvider,
) )
logger = logging.getLogger(__name__)
class IOTA(FileDownloadSIGRefDataProvider): class IOTA(FileDownloadSIGRefDataProvider):
"""SIG ref data provider for Islands on the Air""" """SIG ref data provider for Islands on the Air"""
@@ -31,7 +33,7 @@ class IOTA(FileDownloadSIGRefDataProvider):
try: try:
grid = latlong_to_locator(latitude, longitude, 6) grid = latlong_to_locator(latitude, longitude, 6)
except ValueError: except ValueError:
logging.debug( logger.debug(
"Error converting lat/lon to locator for an IOTA reference %f %f", "Error converting lat/lon to locator for an IOTA reference %f %f",
latitude, latitude,
longitude, longitude,
@@ -5,6 +5,8 @@ import pytz
from providers.sigrefdata.sig_ref_data_provider import SIGRefDataProvider from providers.sigrefdata.sig_ref_data_provider import SIGRefDataProvider
logger = logging.getLogger(__name__)
class LocalFileSIGRefDataProvider(SIGRefDataProvider): class LocalFileSIGRefDataProvider(SIGRefDataProvider):
"""Generic SIG ref data provider class for providers that fetch their data from a local file on startup.""" """Generic SIG ref data provider class for providers that fetch their data from a local file on startup."""
@@ -14,7 +16,7 @@ class LocalFileSIGRefDataProvider(SIGRefDataProvider):
self._path = path self._path = path
def start(self): def start(self):
logging.debug(f"Loading {self.sig_name} SIG ref data from file.") logger.debug(f"Loading {self.sig_name} SIG ref data from file.")
try: try:
new_data = self._file_to_data(self._path) new_data = self._file_to_data(self._path)
if new_data: if new_data:
@@ -23,10 +25,10 @@ class LocalFileSIGRefDataProvider(SIGRefDataProvider):
self.last_update_time = datetime.now(pytz.UTC) self.last_update_time = datetime.now(pytz.UTC)
else: else:
self.status = "Error" self.status = "Error"
logging.info(f"Failed to load SIG ref data for {self.sig_name}") logger.info(f"Failed to load SIG ref data for {self.sig_name}")
except Exception: except Exception:
self.status = "Error" self.status = "Error"
logging.exception(f"Exception in local file SIG Ref Data Provider ({self.sig_name})") logger.exception(f"Exception in local file SIG Ref Data Provider ({self.sig_name})")
def _file_to_data(self, path): def _file_to_data(self, path):
"""Load a file on the given path and turn it into SIG Ref data.""" """Load a file on the given path and turn it into SIG Ref data."""
@@ -5,6 +5,8 @@ import pytz
from core.data_store import DATA_STORE from core.data_store import DATA_STORE
logger = logging.getLogger(__name__)
class SIGRefDataProvider: class SIGRefDataProvider:
"""Generic SIG reference data provider class. Subclasses of this query the individual URLs or files for data.""" """Generic SIG reference data provider class. Subclasses of this query the individual URLs or files for data."""
@@ -45,4 +47,4 @@ class SIGRefDataProvider:
break break
self.reference_count = len(new_data) self.reference_count = len(new_data)
logging.info(f"Loaded {self.reference_count} references for {self.sig_name} into the data store.") logger.info(f"Loaded {self.reference_count} references for {self.sig_name} into the data store.")
+3 -1
View File
@@ -9,6 +9,8 @@ from providers.sigrefdata.file_download_sig_ref_data_provider import (
FileDownloadSIGRefDataProvider, FileDownloadSIGRefDataProvider,
) )
logger = logging.getLogger(__name__)
class WCA(FileDownloadSIGRefDataProvider): class WCA(FileDownloadSIGRefDataProvider):
"""SIG ref data provider for World Castles Award""" """SIG ref data provider for World Castles Award"""
@@ -36,7 +38,7 @@ class WCA(FileDownloadSIGRefDataProvider):
longitude = float(split[1]) longitude = float(split[1])
grid = latlong_to_locator(latitude, longitude) grid = latlong_to_locator(latitude, longitude)
except ValueError: except ValueError:
logging.debug(f"Encountered dodgy formatting in WCA CSV, skipping location data for {ref_id}") logger.debug(f"Encountered dodgy formatting in WCA CSV, skipping location data for {ref_id}")
new_data.append( new_data.append(
SIGRef( SIGRef(
+9 -7
View File
@@ -11,6 +11,8 @@ from core.constants import HTTP_HEADERS
from providers.solarconditions.ionosonde_utils import compute_band_states from providers.solarconditions.ionosonde_utils import compute_band_states
from providers.solarconditions.solar_conditions_provider import SolarConditionsProvider from providers.solarconditions.solar_conditions_provider import SolarConditionsProvider
logger = logging.getLogger(__name__)
# Each station gets polled roughly once every hour (3600 seconds). Note that to avoid a burst of requests to the server # Each station gets polled roughly once every hour (3600 seconds). Note that to avoid a burst of requests to the server
# every hour, the requests for data from each station are spaced out throughout the hour, leading to one request being # every hour, the requests for data from each station are spaced out throughout the hour, leading to one request being
# sent every 1-2 minutes. # sent every 1-2 minutes.
@@ -64,7 +66,7 @@ class GIROIonosonde(SolarConditionsProvider):
return stations return stations
def start(self): def start(self):
logging.info(f"Set up query of GIRO ionosonde data API every {POLL_INTERVAL} seconds.") logger.info(f"Set up query of GIRO ionosonde data API every {POLL_INTERVAL} seconds.")
self._thread = Thread(target=self._run, name="GIROIonosondeDataProvider") self._thread = Thread(target=self._run, name="GIROIonosondeDataProvider")
self._thread.start() self._thread.start()
@@ -86,7 +88,7 @@ class GIROIonosonde(SolarConditionsProvider):
ursi = station["ursi"] ursi = station["ursi"]
name = station["name"] name = station["name"]
try: try:
logging.debug(f"Polling GIRO ionosonde data for {ursi} ({name})...") logger.debug(f"Polling GIRO ionosonde data for {ursi} ({name})...")
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
from_time = now - timedelta(hours=HISTORY_HOURS) from_time = now - timedelta(hours=HISTORY_HOURS)
cutoff_ts = from_time.timestamp() cutoff_ts = from_time.timestamp()
@@ -127,11 +129,11 @@ class GIROIonosonde(SolarConditionsProvider):
self.update_data({"ionosonde_data": ionosonde_data}) self.update_data({"ionosonde_data": ionosonde_data})
self.status = "OK" self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC) self.last_update_time = datetime.now(pytz.UTC)
logging.debug(f"Updated ionosonde data for {ursi} ({name}).") logger.debug(f"Updated ionosonde data for {ursi} ({name}).")
except Exception: except Exception:
self.status = "Error" self.status = "Error"
logging.exception(f"Exception fetching GIRO ionosonde data for {ursi} ({name})") logger.exception(f"Exception fetching GIRO ionosonde data for {ursi} ({name})")
def _fetch_station_data(self, ursi, from_time, to_time): def _fetch_station_data(self, ursi, from_time, to_time):
"""Fetch foF2, MUF and LUF readings for a station. Returns (fof2_dict, muf_dict, luf_dict) keyed by UNIX timestamp.""" """Fetch foF2, MUF and LUF readings for a station. Returns (fof2_dict, muf_dict, luf_dict) keyed by UNIX timestamp."""
@@ -142,14 +144,14 @@ class GIROIonosonde(SolarConditionsProvider):
try: try:
http_response = requests.get(url, headers=HTTP_HEADERS, timeout=(5, 15)) http_response = requests.get(url, headers=HTTP_HEADERS, timeout=(5, 15))
if not http_response.ok: if not http_response.ok:
logging.warning(f"HTTP {http_response.status_code} when calling Giro ionosonde API.") logger.warning(f"HTTP {http_response.status_code} when calling Giro ionosonde API.")
return None, None, None return None, None, None
return self._parse_all(http_response.text) return self._parse_all(http_response.text)
except (ConnectTimeout, ReadTimeout): except (ConnectTimeout, ReadTimeout):
logging.warning("Timeout when accessing Giro ionosonde API.") logger.warning("Timeout when accessing Giro ionosonde API.")
return None, None, None return None, None, None
except ConnectionError: except ConnectionError:
logging.warning("Connection error when accessing Giro ionosonde API.") logger.warning("Connection error when accessing Giro ionosonde API.")
return None, None, None return None, None, None
@staticmethod @staticmethod
+5 -3
View File
@@ -9,6 +9,8 @@ from providers.solarconditions.http_solar_conditions_provider import (
HTTPSolarConditionsProvider, HTTPSolarConditionsProvider,
) )
logger = logging.getLogger(__name__)
POLL_INTERVAL = 3600 # 1 hour POLL_INTERVAL = 3600 # 1 hour
URL = "https://www.hamqsl.com/solarxml.php" URL = "https://www.hamqsl.com/solarxml.php"
@@ -24,14 +26,14 @@ class HamQSL(HTTPSolarConditionsProvider):
root = ElementTree.fromstring(http_response.text) root = ElementTree.fromstring(http_response.text)
sd = root.find("solardata") sd = root.find("solardata")
if sd is None: if sd is None:
logging.warning("HamQSL solar conditions API returned unexpected XML structure") logger.warning("HamQSL solar conditions API returned unexpected XML structure")
return None return None
# Some error checking functions in case the data is janky. # Some error checking functions in case the data is janky.
def text(tag, default=None): def text(tag, default=None):
if sd is None: if sd is None:
logging.warning("HamQSL solar conditions API returned unexpected XML structure") logger.warning("HamQSL solar conditions API returned unexpected XML structure")
return default return default
el = sd.find(tag) el = sd.find(tag)
return el.text.strip() if el is not None and el.text else default return el.text.strip() if el is not None and el.text else default
@@ -79,7 +81,7 @@ class HamQSL(HTTPSolarConditionsProvider):
dt = dateutil_parser.parse(updated_str, tzinfos={tz_abbr: timezone}) dt = dateutil_parser.parse(updated_str, tzinfos={tz_abbr: timezone})
updated = dt.astimezone(pytz.UTC).timestamp() updated = dt.astimezone(pytz.UTC).timestamp()
except (ValueError, IndexError): except (ValueError, IndexError):
logging.warning(f"HamQSL solar conditions API returned unrecognised timestamp format: {updated_str}") logger.warning(f"HamQSL solar conditions API returned unrecognised timestamp format: {updated_str}")
# Return the data ready to be put into the solar conditions object. # Return the data ready to be put into the solar conditions object.
return { return {
@@ -9,6 +9,8 @@ from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout
from core.constants import HTTP_HEADERS from core.constants import HTTP_HEADERS
from providers.solarconditions.solar_conditions_provider import SolarConditionsProvider from providers.solarconditions.solar_conditions_provider import SolarConditionsProvider
logger = logging.getLogger(__name__)
class HTTPSolarConditionsProvider(SolarConditionsProvider): class HTTPSolarConditionsProvider(SolarConditionsProvider):
"""Generic solar conditions provider for providers that request data via HTTP(S). Subclasses implement """Generic solar conditions provider for providers that request data via HTTP(S). Subclasses implement
@@ -22,7 +24,7 @@ class HTTPSolarConditionsProvider(SolarConditionsProvider):
self._stop_event = Event() self._stop_event = Event()
def start(self): def start(self):
logging.info(f"Set up query of {self.name} solar conditions API every {self._poll_interval!s} seconds.") logger.info(f"Set up query of {self.name} solar conditions API every {self._poll_interval!s} seconds.")
self._thread = Thread(target=self._run, name=f"HTTPSolarConditionsProvider-{self.name}") self._thread = Thread(target=self._run, name=f"HTTPSolarConditionsProvider-{self.name}")
self._thread.start() self._thread.start()
@@ -37,7 +39,7 @@ class HTTPSolarConditionsProvider(SolarConditionsProvider):
def _poll(self): def _poll(self):
try: try:
logging.debug(f"Polling {self.name} solar conditions API...") logger.debug(f"Polling {self.name} solar conditions API...")
http_response = requests.get(self._url, headers=HTTP_HEADERS, timeout=(5, 30)) http_response = requests.get(self._url, headers=HTTP_HEADERS, timeout=(5, 30))
# Check response code was good # Check response code was good
if http_response.ok: if http_response.ok:
@@ -46,18 +48,18 @@ class HTTPSolarConditionsProvider(SolarConditionsProvider):
self.status = "OK" self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC) self.last_update_time = datetime.now(pytz.UTC)
logging.debug(f"Received data from {self.name} solar conditions API.") logger.debug(f"Received data from {self.name} solar conditions API.")
else: else:
self.status = "Error" self.status = "Error"
logging.warning(f"HTTP {http_response.status_code} when calling {self.name} solar conditions API.") logger.warning(f"HTTP {http_response.status_code} when calling {self.name} solar conditions API.")
except ConnectionError: except ConnectionError:
logging.warning(f"Connection error when accessing {self.name} solar conditions API.") logger.warning(f"Connection error when accessing {self.name} solar conditions API.")
except (ConnectTimeout, ReadTimeout): except (ConnectTimeout, ReadTimeout):
logging.warning(f"Timeout when accessing {self.name} solar conditions API.") logger.warning(f"Timeout when accessing {self.name} solar conditions API.")
except Exception: except Exception:
self.status = "Error" self.status = "Error"
logging.exception(f"Exception in HTTP Solar Conditions Provider ({self.name})") logger.exception(f"Exception in HTTP Solar Conditions Provider ({self.name})")
self._stop_event.wait(timeout=1) self._stop_event.wait(timeout=1)
def _http_response_to_solar_conditions(self, http_response): def _http_response_to_solar_conditions(self, http_response):
+9 -7
View File
@@ -10,6 +10,8 @@ from core.constants import HTTP_HEADERS
from providers.solarconditions.ionosonde_utils import compute_band_states from providers.solarconditions.ionosonde_utils import compute_band_states
from providers.solarconditions.solar_conditions_provider import SolarConditionsProvider from providers.solarconditions.solar_conditions_provider import SolarConditionsProvider
logger = logging.getLogger(__name__)
POLL_INTERVAL = 900 # 15 minutes POLL_INTERVAL = 900 # 15 minutes
KC2G_URL = "https://prop.kc2g.com/api/stations.json" KC2G_URL = "https://prop.kc2g.com/api/stations.json"
HISTORY_HOURS = 24 HISTORY_HOURS = 24
@@ -29,7 +31,7 @@ class KC2GProp(SolarConditionsProvider):
self._stop_event = Event() self._stop_event = Event()
def start(self): def start(self):
logging.info(f"Set up query of KC2G ionosonde data API every {POLL_INTERVAL} seconds.") logger.info(f"Set up query of KC2G ionosonde data API every {POLL_INTERVAL} seconds.")
self._thread = Thread(target=self._run, name="KC2GPropProvider") self._thread = Thread(target=self._run, name="KC2GPropProvider")
self._thread.start() self._thread.start()
@@ -44,10 +46,10 @@ class KC2GProp(SolarConditionsProvider):
def _poll(self): def _poll(self):
try: try:
logging.debug("Polling KC2G ionosonde data...") logger.debug("Polling KC2G ionosonde data...")
http_response = requests.get(KC2G_URL, headers=HTTP_HEADERS, timeout=(5, 30)) http_response = requests.get(KC2G_URL, headers=HTTP_HEADERS, timeout=(5, 30))
if not http_response.ok: if not http_response.ok:
logging.warning(f"HTTP {http_response.status_code} when calling KG2G ionosonde API.") logger.warning(f"HTTP {http_response.status_code} when calling KG2G ionosonde API.")
return return
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
@@ -114,13 +116,13 @@ class KC2GProp(SolarConditionsProvider):
self.update_data({"ionosonde_data": ionosonde_data}) self.update_data({"ionosonde_data": ionosonde_data})
self.status = "OK" self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC) self.last_update_time = datetime.now(pytz.UTC)
logging.debug(f"Updated KC2G ionosonde data for {updated_count} stations.") logger.debug(f"Updated KC2G ionosonde data for {updated_count} stations.")
except ConnectionError: except ConnectionError:
logging.warning("Connection error when accessing KC2G ionosonde API.") logger.warning("Connection error when accessing KC2G ionosonde API.")
except (ConnectTimeout, ReadTimeout): except (ConnectTimeout, ReadTimeout):
logging.warning("Timeout when accessing KC2G ionosonde API.") logger.warning("Timeout when accessing KC2G ionosonde API.")
except Exception: except Exception:
self.status = "Error" self.status = "Error"
logging.exception("Exception in KC2G ionosonde data provider") logger.exception("Exception in KC2G ionosonde data provider")
self._stop_event.wait(timeout=1) self._stop_event.wait(timeout=1)
+12 -10
View File
@@ -6,6 +6,8 @@ from providers.solarconditions.http_solar_conditions_provider import (
HTTPSolarConditionsProvider, HTTPSolarConditionsProvider,
) )
logger = logging.getLogger(__name__)
POLL_INTERVAL = 10800 # Every 3 hours POLL_INTERVAL = 10800 # Every 3 hours
URL = "https://services.swpc.noaa.gov/text/3-day-forecast.txt" URL = "https://services.swpc.noaa.gov/text/3-day-forecast.txt"
@@ -28,7 +30,7 @@ class NOAA3dayForecast(HTTPSolarConditionsProvider):
start_idx = i start_idx = i
break break
if start_idx is None: if start_idx is None:
logging.warning(f"NOAA 3-day forecast: could not find '{section_header}' section") logger.warning(f"NOAA 3-day forecast: could not find '{section_header}' section")
return None return None
# Find the date header line by scanning the next few lines for month & day patterns # Find the date header line by scanning the next few lines for month & day patterns
@@ -38,11 +40,11 @@ class NOAA3dayForecast(HTTPSolarConditionsProvider):
date_header_idx = j date_header_idx = j
break break
if date_header_idx is None: if date_header_idx is None:
logging.warning(f"NOAA 3-day forecast: could not find date header after '{section_header}'") logger.warning(f"NOAA 3-day forecast: could not find date header after '{section_header}'")
return None return None
date_matches = re.findall(r"([A-Za-z]{3})\s+(\d{2})", lines[date_header_idx]) date_matches = re.findall(r"([A-Za-z]{3})\s+(\d{2})", lines[date_header_idx])
if not date_matches: if not date_matches:
logging.warning(f"NOAA 3-day forecast: no dates in header: {lines[date_header_idx]}") logger.warning(f"NOAA 3-day forecast: no dates in header: {lines[date_header_idx]}")
return None return None
# Figure out the date based on the line found # Figure out the date based on the line found
@@ -52,7 +54,7 @@ class NOAA3dayForecast(HTTPSolarConditionsProvider):
dt = datetime.strptime(f"{day_str} {month_str} {year}", "%d %b %Y").replace(tzinfo=timezone.utc) dt = datetime.strptime(f"{day_str} {month_str} {year}", "%d %b %Y").replace(tzinfo=timezone.utc)
column_timestamps.append(dt.timestamp()) column_timestamps.append(dt.timestamp())
except ValueError: except ValueError:
logging.warning(f"NOAA 3-day forecast: could not parse date: {month_str} {day_str} {year}") logger.warning(f"NOAA 3-day forecast: could not parse date: {month_str} {day_str} {year}")
return None return None
# Parse data rows. Each non-empty line should have a text label followed by percentage values # Parse data rows. Each non-empty line should have a text label followed by percentage values
@@ -91,26 +93,26 @@ class NOAA3dayForecast(HTTPSolarConditionsProvider):
start_idx = i start_idx = i
break break
if start_idx is None: if start_idx is None:
logging.warning("NOAA K-index forecast: could not find 'NOAA Kp index breakdown' section") logger.warning("NOAA K-index forecast: could not find 'NOAA Kp index breakdown' section")
return None return None
# Extract the year from the header line, e.g. "NOAA Kp index breakdown Apr 2-Apr 4, 2026" # Extract the year from the header line, e.g. "NOAA Kp index breakdown Apr 2-Apr 4, 2026"
header_line = lines[start_idx] header_line = lines[start_idx]
year_match = re.search(r"\b(\d{4})\b", header_line) year_match = re.search(r"\b(\d{4})\b", header_line)
if not year_match: if not year_match:
logging.warning(f"NOAA K-index forecast: could not extract year from: {header_line}") logger.warning(f"NOAA K-index forecast: could not extract year from: {header_line}")
return None return None
year = int(year_match.group(1)) year = int(year_match.group(1))
# Parse the column date headers on the next line, e.g. " Apr 02 Apr 03 Apr 04" # Parse the column date headers on the next line, e.g. " Apr 02 Apr 03 Apr 04"
if start_idx + 1 >= len(lines): if start_idx + 1 >= len(lines):
logging.warning("NOAA K-index forecast: missing date header line") logger.warning("NOAA K-index forecast: missing date header line")
return None return None
date_header_line = lines[start_idx + 2] date_header_line = lines[start_idx + 2]
date_matches = re.findall(r"([A-Za-z]{3})\s+(\d{2})", date_header_line) date_matches = re.findall(r"([A-Za-z]{3})\s+(\d{2})", date_header_line)
if not date_matches: if not date_matches:
logging.warning(f"NOAA K-index forecast: could not parse date headers from: {date_header_line}") logger.warning(f"NOAA K-index forecast: could not parse date headers from: {date_header_line}")
return None return None
column_dates = [] column_dates = []
@@ -118,7 +120,7 @@ class NOAA3dayForecast(HTTPSolarConditionsProvider):
try: try:
column_dates.append(datetime.strptime(f"{day_str} {month_str} {year}", "%d %b %Y").date()) column_dates.append(datetime.strptime(f"{day_str} {month_str} {year}", "%d %b %Y").date())
except ValueError: except ValueError:
logging.warning(f"NOAA K-index forecast: could not parse date: {month_str} {day_str} {year}") logger.warning(f"NOAA K-index forecast: could not parse date: {month_str} {day_str} {year}")
return None return None
# Parse each data row, e.g. "00-03UT 2.00 3.00 2.00" # Parse each data row, e.g. "00-03UT 2.00 3.00 2.00"
@@ -159,7 +161,7 @@ class NOAA3dayForecast(HTTPSolarConditionsProvider):
k_index_forecast[key] = kp k_index_forecast[key] = kp
if not k_index_forecast: if not k_index_forecast:
logging.warning("NOAA K-index forecast: no data rows parsed") logger.warning("NOAA K-index forecast: no data rows parsed")
return None return None
# Parse Solar Radiation Storm Forecast (single row: "S1 or greater") # Parse Solar Radiation Storm Forecast (single row: "S1 or greater")
+5 -3
View File
@@ -9,6 +9,8 @@ from core.config import SERVER_OWNER_CALLSIGN
from data.spot import Spot from data.spot import Spot
from providers.spot.spot_provider import SpotProvider from providers.spot.spot_provider import SpotProvider
logger = logging.getLogger(__name__)
class APRSIS(SpotProvider): class APRSIS(SpotProvider):
"""Spot provider for the APRS-IS.""" """Spot provider for the APRS-IS."""
@@ -25,10 +27,10 @@ class APRSIS(SpotProvider):
def _connect(self): def _connect(self):
self._aprsis = aprslib.IS(SERVER_OWNER_CALLSIGN) self._aprsis = aprslib.IS(SERVER_OWNER_CALLSIGN)
self.status = "Connecting" self.status = "Connecting"
logging.info("APRS-IS connecting...") logger.info("APRS-IS connecting...")
self._aprsis.connect() self._aprsis.connect()
self._aprsis.consumer(self._handle) self._aprsis.consumer(self._handle)
logging.info("APRS-IS connected.") logger.info("APRS-IS connected.")
def stop(self): def stop(self):
self.status = "Shutting down" self.status = "Shutting down"
@@ -60,4 +62,4 @@ class APRSIS(SpotProvider):
self.status = "OK" self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC) self.last_update_time = datetime.now(pytz.UTC)
logging.debug("Data received from APRS-IS.") logger.debug("Data received from APRS-IS.")
+11 -9
View File
@@ -11,6 +11,8 @@ from core.config import SERVER_OWNER_CALLSIGN
from data.spot import Spot from data.spot import Spot
from providers.spot.spot_provider import SpotProvider from providers.spot.spot_provider import SpotProvider
logger = logging.getLogger(__name__)
class DXCluster(SpotProvider): class DXCluster(SpotProvider):
"""Spot provider for a DX Cluster. Hostname, port, login_prompt, login_callsign and allow_rbn_spots are provided in config. """Spot provider for a DX Cluster. Hostname, port, login_prompt, login_callsign and allow_rbn_spots are provided in config.
@@ -59,19 +61,19 @@ class DXCluster(SpotProvider):
while not connected and self._running: while not connected and self._running:
try: try:
self.status = "Connecting" self.status = "Connecting"
logging.info(f"DX Cluster {self._hostname} connecting...") logger.info(f"DX Cluster {self._hostname} connecting...")
self._telnet = telnetlib3.Telnet(self._hostname, self._port) self._telnet = telnetlib3.Telnet(self._hostname, self._port)
self._telnet.read_until(self._login_prompt.encode("latin-1")) self._telnet.read_until(self._login_prompt.encode("latin-1"))
self._telnet.write(f"{self._login_callsign}\n".encode("latin-1")) self._telnet.write(f"{self._login_callsign}\n".encode("latin-1"))
connected = True connected = True
logging.info(f"DX Cluster {self._hostname} connected.") logger.info(f"DX Cluster {self._hostname} connected.")
except ConnectionRefusedError: except ConnectionRefusedError:
self.status = "Error" self.status = "Error"
logging.warning(f"Connection refused to DX cluster {self._hostname}") logger.warning(f"Connection refused to DX cluster {self._hostname}")
sleep(300) sleep(300)
except Exception: except Exception:
self.status = "Error" self.status = "Error"
logging.exception(f"Exception while connecting to DX Cluster Provider ({self._hostname}).") logger.exception(f"Exception while connecting to DX Cluster Provider ({self._hostname}).")
sleep(5) sleep(5)
self.status = "Waiting for Data" self.status = "Waiting for Data"
@@ -101,25 +103,25 @@ class DXCluster(SpotProvider):
self.status = "OK" self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC) self.last_update_time = datetime.now(pytz.UTC)
logging.debug(f"Data received from DX Cluster {self._hostname}.") logger.debug(f"Data received from DX Cluster {self._hostname}.")
except EOFError: except EOFError:
connected = False connected = False
if self._running: if self._running:
self.status = "Restarting" self.status = "Restarting"
logging.warning(f"Disconnected from DX Cluster {self._hostname}. Reconnecting...") logger.warning(f"Disconnected from DX Cluster {self._hostname}. Reconnecting...")
sleep(5) sleep(5)
else: else:
logging.info(f"DX Cluster {self._hostname} shutting down...") logger.info(f"DX Cluster {self._hostname} shutting down...")
self.status = "Shutting down" self.status = "Shutting down"
except Exception: except Exception:
connected = False connected = False
if self._running: if self._running:
self.status = "Error" self.status = "Error"
logging.exception(f"Exception in DX Cluster Provider ({self._hostname})") logger.exception(f"Exception in DX Cluster Provider ({self._hostname})")
sleep(5) sleep(5)
else: else:
logging.info(f"DX Cluster {self._hostname} shutting down...") logger.info(f"DX Cluster {self._hostname} shutting down...")
self.status = "Shutting down" self.status = "Shutting down"
self.status = "Disconnected" self.status = "Disconnected"
+8 -6
View File
@@ -9,6 +9,8 @@ from data.sig_ref import SIGRef
from data.spot import Spot from data.spot import Spot
from providers.spot.http_spot_provider import HTTPSpotProvider from providers.spot.http_spot_provider import HTTPSpotProvider
logger = logging.getLogger(__name__)
class GMA(HTTPSpotProvider): class GMA(HTTPSpotProvider):
"""Spot provider for General Mountain Activity""" """Spot provider for General Mountain Activity"""
@@ -24,7 +26,7 @@ class GMA(HTTPSpotProvider):
self._api_key = provider_config.get("api_key", "") self._api_key = provider_config.get("api_key", "")
if self._api_key == "": if self._api_key == "":
provider_config["enabled"] = False provider_config["enabled"] = False
logging.warning("GMA spot provider configured but no api key was provided, this API will not be queried.") logger.warning("GMA spot provider configured but no api key was provided, this API will not be queried.")
self._url_data_cache = URLDataCache("GMA") self._url_data_cache = URLDataCache("GMA")
super().__init__( super().__init__(
@@ -127,7 +129,7 @@ class GMA(HTTPSpotProvider):
spot.sig_refs[0].sig = "MOTA" spot.sig_refs[0].sig = "MOTA"
spot.sig = "MOTA" spot.sig = "MOTA"
case _: case _:
logging.warning( logger.warning(
f"GMA spot found with ref type {ref_info['reftype']}, developer needs to add support for this!" f"GMA spot found with ref type {ref_info['reftype']}, developer needs to add support for this!"
) )
spot.sig_refs[0].sig = ref_info["reftype"] spot.sig_refs[0].sig = ref_info["reftype"]
@@ -139,19 +141,19 @@ class GMA(HTTPSpotProvider):
elif not ref_response.from_cache: elif not ref_response.from_cache:
if not ref_response.ok: if not ref_response.ok:
logging.warning( logger.warning(
f"HTTP {ref_response.status_code} when looking up GMA ref {source_spot['REF']}" f"HTTP {ref_response.status_code} when looking up GMA ref {source_spot['REF']}"
) )
else: else:
logging.warning( logger.warning(
f"GMA API returned a malformed response when looking up ref {source_spot['REF']}" f"GMA API returned a malformed response when looking up ref {source_spot['REF']}"
) )
except: except:
logging.exception( logger.exception(
f"Exception when looking up {self.REF_INFO_URL_ROOT}{source_spot['REF']}, ignoring this spot for now" f"Exception when looking up {self.REF_INFO_URL_ROOT}{source_spot['REF']}, ignoring this spot for now"
) )
else: else:
logging.warning(f"The GMA API returned an unexpected response (HTTP {http_response.status_code}).") logger.warning(f"The GMA API returned an unexpected response (HTTP {http_response.status_code}).")
return new_spots return new_spots
+4 -2
View File
@@ -11,6 +11,8 @@ from data.sig_ref import SIGRef
from data.spot import Spot from data.spot import Spot
from providers.spot.http_spot_provider import HTTPSpotProvider from providers.spot.http_spot_provider import HTTPSpotProvider
logger = logging.getLogger(__name__)
class HEMA(HTTPSpotProvider): class HEMA(HTTPSpotProvider):
"""Spot provider for HuMPs Excluding Marilyns Award""" """Spot provider for HuMPs Excluding Marilyns Award"""
@@ -80,9 +82,9 @@ class HEMA(HTTPSpotProvider):
# code will do that for us. # code will do that for us.
new_spots.append(spot) new_spots.append(spot)
except (ConnectTimeout, ReadTimeout): except (ConnectTimeout, ReadTimeout):
logging.warning("Timeout when accessing HEMA spots API.") logger.warning("Timeout when accessing HEMA spots API.")
except ConnectionError: except ConnectionError:
logging.warning("Connection error when accessing HEMA spots API.") logger.warning("Connection error when accessing HEMA spots API.")
return new_spots return new_spots
def can_submit_spot(self, sig): def can_submit_spot(self, sig):
+9 -7
View File
@@ -10,6 +10,8 @@ from requests.exceptions import ConnectionError, ConnectTimeout
from core.constants import HTTP_HEADERS from core.constants import HTTP_HEADERS
from providers.spot.spot_provider import SpotProvider from providers.spot.spot_provider import SpotProvider
logger = logging.getLogger(__name__)
class HTTPSpotProvider(SpotProvider): class HTTPSpotProvider(SpotProvider):
"""Generic spot provider class for providers that request data via HTTP(S). Just for convenience to avoid code """Generic spot provider class for providers that request data via HTTP(S). Just for convenience to avoid code
@@ -26,7 +28,7 @@ class HTTPSpotProvider(SpotProvider):
def start(self): def start(self):
# Fire off the polling thread. It will poll immediately on startup, then sleep for poll_interval between # 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. # subsequent polls, so start() returns immediately and the application can continue starting.
logging.info(f"Set up query of {self.name} spot API every {self._poll_interval!s} seconds.") logger.info(f"Set up query of {self.name} spot API every {self._poll_interval!s} seconds.")
self._thread = Thread(target=self._run, name=f"HTTPSpotProvider-{self.name}") self._thread = Thread(target=self._run, name=f"HTTPSpotProvider-{self.name}")
self._thread.start() self._thread.start()
@@ -50,7 +52,7 @@ class HTTPSpotProvider(SpotProvider):
def _poll(self): def _poll(self):
try: try:
# Request data from API # Request data from API
logging.debug(f"Polling {self.name} spot API...") logger.debug(f"Polling {self.name} spot API...")
http_response = requests.get(self._url, headers=HTTP_HEADERS, timeout=(5, 30)) http_response = requests.get(self._url, headers=HTTP_HEADERS, timeout=(5, 30))
# Check response code was good # Check response code was good
if http_response.ok: if http_response.ok:
@@ -62,18 +64,18 @@ class HTTPSpotProvider(SpotProvider):
self.status = "OK" self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC) self.last_update_time = datetime.now(pytz.UTC)
logging.debug(f"Received data from {self.name} spot API.") logger.debug(f"Received data from {self.name} spot API.")
else: else:
self.status = "Error" self.status = "Error"
logging.warning(f"HTTP {http_response.status_code} when calling {self.name} spot API.") logger.warning(f"HTTP {http_response.status_code} when calling {self.name} spot API.")
except ConnectionError: except ConnectionError:
logging.warning(f"Connection error when accessing {self.name} spots API.") logger.warning(f"Connection error when accessing {self.name} spots API.")
except (ConnectTimeout, ReadTimeout): except (ConnectTimeout, ReadTimeout):
logging.warning(f"Timeout when accessing {self.name} spots API.") logger.warning(f"Timeout when accessing {self.name} spots API.")
except Exception: except Exception:
self.status = "Error" self.status = "Error"
logging.exception(f"Exception in HTTP Spot Provider ({self.name})") logger.exception(f"Exception in HTTP Spot Provider ({self.name})")
self._stop_event.wait(timeout=1) self._stop_event.wait(timeout=1)
def _http_response_to_spots(self, http_response): def _http_response_to_spots(self, http_response):
+3 -1
View File
@@ -10,6 +10,8 @@ from data.sig_ref import SIGRef
from data.spot import Spot from data.spot import Spot
from providers.spot.http_spot_provider import HTTPSpotProvider from providers.spot.http_spot_provider import HTTPSpotProvider
logger = logging.getLogger(__name__)
class ParksNPeaks(HTTPSpotProvider): class ParksNPeaks(HTTPSpotProvider):
"""Spot provider for Parks n Peaks""" """Spot provider for Parks n Peaks"""
@@ -91,7 +93,7 @@ class ParksNPeaks(HTTPSpotProvider):
"SANPCPA", "SANPCPA",
"LLOTA", "LLOTA",
]: ]:
logging.warning(f"PNP spot found with sig {sig}, developer needs to add support for this!") logger.warning(f"PNP spot found with sig {sig}, developer needs to add support for this!")
# Add new spot to the list # Add new spot to the list
new_spots.append(spot) new_spots.append(spot)
+10 -8
View File
@@ -11,6 +11,8 @@ from core.config import SERVER_OWNER_CALLSIGN
from data.spot import Spot from data.spot import Spot
from providers.spot.spot_provider import SpotProvider from providers.spot.spot_provider import SpotProvider
logger = logging.getLogger(__name__)
class RBN(SpotProvider): class RBN(SpotProvider):
"""Spot provider for the Reverse Beacon Network. Connects to a single port, if you want both CW/RTTY (port 7000) and FT8 """Spot provider for the Reverse Beacon Network. Connects to a single port, if you want both CW/RTTY (port 7000) and FT8
@@ -46,15 +48,15 @@ class RBN(SpotProvider):
while not connected and self._running: while not connected and self._running:
try: try:
self.status = "Connecting" self.status = "Connecting"
logging.info(f"RBN port {self._port!s} connecting...") logger.info(f"RBN port {self._port!s} connecting...")
self._telnet = telnetlib3.Telnet("telnet.reversebeacon.net", self._port) self._telnet = telnetlib3.Telnet("telnet.reversebeacon.net", self._port)
self._telnet.read_until("Please enter your call: ".encode("latin-1")) self._telnet.read_until("Please enter your call: ".encode("latin-1"))
self._telnet.write(f"{SERVER_OWNER_CALLSIGN}\n".encode("latin-1")) self._telnet.write(f"{SERVER_OWNER_CALLSIGN}\n".encode("latin-1"))
connected = True connected = True
logging.info(f"RBN port {self._port!s} connected.") logger.info(f"RBN port {self._port!s} connected.")
except Exception: except Exception:
self.status = "Error" self.status = "Error"
logging.exception(f"Exception while connecting to RBN (port {self._port!s}).") logger.exception(f"Exception while connecting to RBN (port {self._port!s}).")
sleep(5) sleep(5)
self.status = "Waiting for Data" self.status = "Waiting for Data"
@@ -84,25 +86,25 @@ class RBN(SpotProvider):
self.status = "OK" self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC) self.last_update_time = datetime.now(pytz.UTC)
logging.debug(f"Data received from RBN on port {self._port!s}.") logger.debug(f"Data received from RBN on port {self._port!s}.")
except EOFError: except EOFError:
connected = False connected = False
if self._running: if self._running:
self.status = "Restarting" self.status = "Restarting"
logging.warning(f"Disconnected from RBN provider (port {self._port!s}). Reconnecting...") logger.warning(f"Disconnected from RBN provider (port {self._port!s}). Reconnecting...")
sleep(5) sleep(5)
else: else:
logging.info(f"RBN provider (port {self._port!s}) shutting down...") logger.info(f"RBN provider (port {self._port!s}) shutting down...")
self.status = "Shutting down" self.status = "Shutting down"
except Exception: except Exception:
connected = False connected = False
if self._running: if self._running:
self.status = "Error" self.status = "Error"
logging.exception(f"Exception in RBN provider (port {self._port!s})") logger.exception(f"Exception in RBN provider (port {self._port!s})")
sleep(5) sleep(5)
else: else:
logging.info(f"RBN provider (port {self._port!s}) shutting down...") logger.info(f"RBN provider (port {self._port!s}) shutting down...")
self.status = "Shutting down" self.status = "Shutting down"
self.status = "Disconnected" self.status = "Disconnected"
+4 -2
View File
@@ -9,6 +9,8 @@ from data.sig_ref import SIGRef
from data.spot import Spot from data.spot import Spot
from providers.spot.http_spot_provider import HTTPSpotProvider from providers.spot.http_spot_provider import HTTPSpotProvider
logger = logging.getLogger(__name__)
class SOTA(HTTPSpotProvider): class SOTA(HTTPSpotProvider):
"""Spot provider for Summits on the Air""" """Spot provider for Summits on the Air"""
@@ -73,9 +75,9 @@ class SOTA(HTTPSpotProvider):
# that for us. # that for us.
new_spots.append(spot) new_spots.append(spot)
except ConnectionError: except ConnectionError:
logging.warning("Connection error when accessing SOTA spots API") logger.warning("Connection error when accessing SOTA spots API")
except (ConnectTimeout, ReadTimeout): except (ConnectTimeout, ReadTimeout):
logging.warning("Timeout when accessing SOTA spots API.") logger.warning("Timeout when accessing SOTA spots API.")
return new_spots return new_spots
def can_submit_spot(self, sig): def can_submit_spot(self, sig):
+9 -7
View File
@@ -8,6 +8,8 @@ from requests_sse import EventSource
from core.constants import HTTP_HEADERS from core.constants import HTTP_HEADERS
from providers.spot.spot_provider import SpotProvider from providers.spot.spot_provider import SpotProvider
logger = logging.getLogger(__name__)
class SSESpotProvider(SpotProvider): class SSESpotProvider(SpotProvider):
"""Spot provider using Server-Sent Events.""" """Spot provider using Server-Sent Events."""
@@ -22,7 +24,7 @@ class SSESpotProvider(SpotProvider):
self._event_source = None self._event_source = None
def start(self): def start(self):
logging.info(f"Set up SSE connection to {self.name} spot API.") logger.info(f"Set up SSE connection to {self.name} spot API.")
self._stop_event.clear() self._stop_event.clear()
self._thread = Thread(target=self._run, name=f"SSESpotProvider-{self.name}") self._thread = Thread(target=self._run, name=f"SSESpotProvider-{self.name}")
self._thread.daemon = True self._thread.daemon = True
@@ -37,12 +39,12 @@ class SSESpotProvider(SpotProvider):
try: try:
event_source.close() event_source.close()
except Exception: except Exception:
logging.exception(f"Exception closing SSE connection for {self.name} during stop()") logger.exception(f"Exception closing SSE connection for {self.name} during stop()")
if self._thread: if self._thread:
self._thread.join(timeout=15) self._thread.join(timeout=15)
if self._thread.is_alive(): if self._thread.is_alive():
logging.warning(f"{self.name} SSE worker thread did not exit on time and will be killed.") logger.warning(f"{self.name} SSE worker thread did not exit on time and will be killed.")
def _on_open(self): def _on_open(self):
self.status = "Waiting for Data" self.status = "Waiting for Data"
@@ -57,7 +59,7 @@ class SSESpotProvider(SpotProvider):
def _run(self): def _run(self):
while not self._stop_event.is_set(): while not self._stop_event.is_set():
try: try:
logging.debug(f"Connecting to {self.name} spot API...") logger.debug(f"Connecting to {self.name} spot API...")
self.status = "Connecting" self.status = "Connecting"
with EventSource( with EventSource(
self._url, self._url,
@@ -81,10 +83,10 @@ class SSESpotProvider(SpotProvider):
self.status = "OK" self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC) self.last_update_time = datetime.now(pytz.UTC)
logging.debug(f"Received data from {self.name} spot API.") logger.debug(f"Received data from {self.name} spot API.")
except Exception: except Exception:
logging.exception( logger.exception(
f"Exception processing message from SSE Spot Provider ({self.name})" f"Exception processing message from SSE Spot Provider ({self.name})"
) )
finally: finally:
@@ -92,7 +94,7 @@ class SSESpotProvider(SpotProvider):
except Exception: except Exception:
self.status = "Error" self.status = "Error"
logging.exception(f"Exception in SSE Spot Provider ({self.name})") logger.exception(f"Exception in SSE Spot Provider ({self.name})")
else: else:
self.status = "Disconnected" self.status = "Disconnected"
self._stop_event.wait(timeout=5) # Wait before trying to reconnect self._stop_event.wait(timeout=5) # Wait before trying to reconnect
+7 -5
View File
@@ -9,6 +9,8 @@ from websocket import create_connection
from core.constants import HTTP_HEADERS from core.constants import HTTP_HEADERS
from providers.spot.spot_provider import SpotProvider from providers.spot.spot_provider import SpotProvider
logger = logging.getLogger(__name__)
class WebsocketSpotProvider(SpotProvider): class WebsocketSpotProvider(SpotProvider):
"""Spot provider using websockets.""" """Spot provider using websockets."""
@@ -22,7 +24,7 @@ class WebsocketSpotProvider(SpotProvider):
self._last_event_id = None self._last_event_id = None
def start(self): def start(self):
logging.info(f"Set up websocket connection to {self.name} spot API.") logger.info(f"Set up websocket connection to {self.name} spot API.")
self._stopped = False self._stopped = False
self._thread = Thread(target=self._run, name=f"WebsocketSpotProvider-{self.name}") self._thread = Thread(target=self._run, name=f"WebsocketSpotProvider-{self.name}")
self._thread.daemon = True self._thread.daemon = True
@@ -44,7 +46,7 @@ class WebsocketSpotProvider(SpotProvider):
def _run(self): def _run(self):
while not self._stopped: while not self._stopped:
try: try:
logging.debug(f"Connecting to {self.name} spot API...") logger.debug(f"Connecting to {self.name} spot API...")
self.status = "Connecting" self.status = "Connecting"
self._ws = create_connection(self._url, header=HTTP_HEADERS) self._ws = create_connection(self._url, header=HTTP_HEADERS)
self.status = "Connected" self.status = "Connected"
@@ -57,14 +59,14 @@ class WebsocketSpotProvider(SpotProvider):
self.status = "OK" self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC) self.last_update_time = datetime.now(pytz.UTC)
logging.debug(f"Received data from {self.name} spot API.") logger.debug(f"Received data from {self.name} spot API.")
except Exception: except Exception:
logging.exception(f"Exception processing message from Websocket Spot Provider ({self.name})") logger.exception(f"Exception processing message from Websocket Spot Provider ({self.name})")
except Exception as e: except Exception as e:
self.status = "Error" self.status = "Error"
logging.exception(f"Exception in Websocket Spot Provider ({self.name})", e) logger.exception(f"Exception in Websocket Spot Provider ({self.name})", e)
else: else:
self.status = "Disconnected" self.status = "Disconnected"
sleep(5) # Wait before trying to reconnect sleep(5) # Wait before trying to reconnect
+3 -1
View File
@@ -11,6 +11,8 @@ from data.sig_ref import SIGRef
from data.spot import Spot from data.spot import Spot
from providers.spot.http_spot_provider import HTTPSpotProvider from providers.spot.http_spot_provider import HTTPSpotProvider
logger = logging.getLogger(__name__)
class WOTA(HTTPSpotProvider): class WOTA(HTTPSpotProvider):
"""Spot provider for Wainwrights on the Air""" """Spot provider for Wainwrights on the Air"""
@@ -82,7 +84,7 @@ class WOTA(HTTPSpotProvider):
new_spots.append(spot) new_spots.append(spot)
except Exception as e: except Exception as e:
logging.error("Exception parsing WOTA spot", e) logger.error("Exception parsing WOTA spot", e)
return new_spots return new_spots
def can_submit_spot(self, sig): def can_submit_spot(self, sig):
+3 -1
View File
@@ -9,6 +9,8 @@ from providers.staticdata.local_file_static_data_provider import (
LocalFileStaticDataProvider, LocalFileStaticDataProvider,
) )
logger = logging.getLogger(__name__)
class CQZoneData(LocalFileStaticDataProvider): class CQZoneData(LocalFileStaticDataProvider):
"""Static data provider for CQ zone geodata.""" """Static data provider for CQ zone geodata."""
@@ -34,5 +36,5 @@ class CQZoneData(LocalFileStaticDataProvider):
return True return True
except Exception: except Exception:
logging.exception("Exception when loading CQ zone data.") logger.exception("Exception when loading CQ zone data.")
return False return False
@@ -10,6 +10,8 @@ from core.constants import HTTP_HEADERS
from core.url_data_cache import URLDataCache from core.url_data_cache import URLDataCache
from providers.staticdata.static_data_provider import StaticDataProvider from providers.staticdata.static_data_provider import StaticDataProvider
logger = logging.getLogger(__name__)
class FileDownloadStaticDataProvider(StaticDataProvider): class FileDownloadStaticDataProvider(StaticDataProvider):
"""Generic static reference data provider class for providers that fetch their data from the web by downloading a """Generic static reference data provider class for providers that fetch their data from the web by downloading a
@@ -27,7 +29,7 @@ class FileDownloadStaticDataProvider(StaticDataProvider):
def start(self): def start(self):
# Fire off the polling thread. It will poll immediately on startup, then sleep for poll_interval between # 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. # subsequent polls, so start() returns immediately and the application can continue starting.
logging.info(f"Set up query of {self.name} static reference data every {self._poll_interval!s} days.") logger.info(f"Set up query of {self.name} static reference data every {self._poll_interval!s} days.")
self._thread = Thread(target=self._run, name=f"FileDownloadStaticDataProvider-{self.name}") self._thread = Thread(target=self._run, name=f"FileDownloadStaticDataProvider-{self.name}")
self._thread.start() self._thread.start()
@@ -44,7 +46,7 @@ class FileDownloadStaticDataProvider(StaticDataProvider):
try: try:
# Request data from API. Use the data cache (with a TTL of 1 day) here, not as the main mechanism for # Request data from API. 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. # caching, but just so continual restarts of the software during testing don't hammer the servers.
logging.debug(f"Downloading {self.name} static reference data...") logger.debug(f"Downloading {self.name} static reference data...")
http_response = self._url_data_cache.get(self._url, headers=HTTP_HEADERS) http_response = self._url_data_cache.get(self._url, headers=HTTP_HEADERS)
# Check response code was good # Check response code was good
if http_response.ok: if http_response.ok:
@@ -53,22 +55,22 @@ class FileDownloadStaticDataProvider(StaticDataProvider):
if ok: if ok:
self.status = "OK" self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC) self.last_update_time = datetime.now(pytz.UTC)
logging.info(f"Updated static reference data for {self.name}") logger.info(f"Updated static reference data for {self.name}")
else: else:
self.status = "Error" self.status = "Error"
logging.warning( logger.warning(
f"HTTP {http_response.status_code} when downloading static reference data for {self.name}." f"HTTP {http_response.status_code} when downloading static reference data for {self.name}."
) )
except ConnectionError: except ConnectionError:
self.status = "Error" self.status = "Error"
logging.warning(f"Connection error when downloading static reference data for {self.name}.") logger.warning(f"Connection error when downloading static reference data for {self.name}.")
except (ConnectTimeout, ReadTimeout): except (ConnectTimeout, ReadTimeout):
self.status = "Error" self.status = "Error"
logging.warning(f"Timeout when downloading static reference data for {self.name}.") logger.warning(f"Timeout when downloading static reference data for {self.name}.")
except Exception: except Exception:
self.status = "Error" self.status = "Error"
logging.exception(f"Exception in HTTP static reference data provider ({self.name})") logger.exception(f"Exception in HTTP static reference data provider ({self.name})")
self._stop_event.wait(timeout=1) self._stop_event.wait(timeout=1)
def _handle_http_response(self, http_response): def _handle_http_response(self, http_response):
+3 -1
View File
@@ -9,6 +9,8 @@ from providers.staticdata.local_file_static_data_provider import (
LocalFileStaticDataProvider, LocalFileStaticDataProvider,
) )
logger = logging.getLogger(__name__)
class ITUZoneData(LocalFileStaticDataProvider): class ITUZoneData(LocalFileStaticDataProvider):
"""Static data provider for ITU zone geodata.""" """Static data provider for ITU zone geodata."""
@@ -34,5 +36,5 @@ class ITUZoneData(LocalFileStaticDataProvider):
return True return True
except Exception: except Exception:
logging.exception("Exception when loading ITU zone data.") logger.exception("Exception when loading ITU zone data.")
return False return False
+3 -1
View File
@@ -5,6 +5,8 @@ from providers.staticdata.file_download_static_data_provider import (
FileDownloadStaticDataProvider, FileDownloadStaticDataProvider,
) )
logger = logging.getLogger(__name__)
class K0SWE(FileDownloadStaticDataProvider): class K0SWE(FileDownloadStaticDataProvider):
"""Static data provider for K0SWE's dxcc.json, which provides callsign regex to DXCC entity mapping, plus DXCC to """Static data provider for K0SWE's dxcc.json, which provides callsign regex to DXCC entity mapping, plus DXCC to
@@ -44,5 +46,5 @@ class K0SWE(FileDownloadStaticDataProvider):
return True return True
except Exception: except Exception:
logging.exception("Exception when loading K0SWE dxcc.json.") logger.exception("Exception when loading K0SWE dxcc.json.")
return False return False
@@ -5,6 +5,8 @@ import pytz
from providers.staticdata.static_data_provider import StaticDataProvider from providers.staticdata.static_data_provider import StaticDataProvider
logger = logging.getLogger(__name__)
class LocalFileStaticDataProvider(StaticDataProvider): class LocalFileStaticDataProvider(StaticDataProvider):
"""Generic static reference data provider class for providers that fetch their data from a local file on startup.""" """Generic static reference data provider class for providers that fetch their data from a local file on startup."""
@@ -15,19 +17,19 @@ class LocalFileStaticDataProvider(StaticDataProvider):
self._stop = False self._stop = False
def start(self): def start(self):
logging.debug(f"Loading {self.name} static reference data from file.") logger.debug(f"Loading {self.name} static reference data from file.")
try: try:
ok = self._load_data(self._path) ok = self._load_data(self._path)
if ok: if ok:
self.status = "OK" self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC) self.last_update_time = datetime.now(pytz.UTC)
logging.info(f"Updated static reference data for {self.name}") logger.info(f"Updated static reference data for {self.name}")
else: else:
self.status = "Error" self.status = "Error"
logging.error(f"Failed to load data for {self.name}") logger.error(f"Failed to load data for {self.name}")
except Exception: except Exception:
self.status = "Error" self.status = "Error"
logging.exception(f"Exception in local file Static Data Provider ({self.name})") logger.exception(f"Exception in local file Static Data Provider ({self.name})")
def stop(self): def stop(self):
self._stop = True self._stop = True
+5 -3
View File
@@ -18,6 +18,8 @@ from core.utils import infer_band_from_freq, safe_json_dumps
from data.spot import Spot from data.spot import Spot
from providers.spot.spot_provider import SpotProvider from providers.spot.spot_provider import SpotProvider
logger = logging.getLogger(__name__)
RECAPTCHA_VERIFY_URL = "https://www.google.com/recaptcha/api/siteverify" RECAPTCHA_VERIFY_URL = "https://www.google.com/recaptcha/api/siteverify"
@@ -220,7 +222,7 @@ class APISpotHandler(tornado.web.RequestHandler):
except NotImplementedError as e: except NotImplementedError as e:
upstream_warning = str(e) upstream_warning = str(e)
except Exception: except Exception:
logging.exception(f"Failed to submit spot upstream to {upstream_provider_name}") logger.exception(f"Failed to submit spot upstream to {upstream_provider_name}")
upstream_warning = ( upstream_warning = (
f"Spot was saved locally but upstream submission to {upstream_provider_name} failed." f"Spot was saved locally but upstream submission to {upstream_provider_name} failed."
) )
@@ -244,7 +246,7 @@ class APISpotHandler(tornado.web.RequestHandler):
self.set_header("Content-Type", "application/json") self.set_header("Content-Type", "application/json")
except Exception: except Exception:
logging.exception("Exception when handling client request to add spot API") logger.exception("Exception when handling client request to add spot API")
self.write(safe_json_dumps("Error - an internal server error occurred.")) self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500) self.set_status(500)
self.set_header("Cache-Control", "no-store") self.set_header("Cache-Control", "no-store")
@@ -270,5 +272,5 @@ class APISpotHandler(tornado.web.RequestHandler):
) )
return response.ok and response.json().get("success", False) return response.ok and response.json().get("success", False)
except Exception: except Exception:
logging.exception("reCAPTCHA verification request failed") logger.exception("reCAPTCHA verification request failed")
return False return False
+5 -3
View File
@@ -13,6 +13,8 @@ from core.prometheus_metrics_handler import api_requests_counter
from core.utils import safe_json_dumps from core.utils import safe_json_dumps
from data.lookup_credentials import extract_credentials from data.lookup_credentials import extract_credentials
logger = logging.getLogger(__name__)
class APIAlertsHandler(tornado.web.RequestHandler): class APIAlertsHandler(tornado.web.RequestHandler):
"""API request handler for /api/v2/alerts""" """API request handler for /api/v2/alerts"""
@@ -63,7 +65,7 @@ class APIAlertsHandler(tornado.web.RequestHandler):
self.write(safe_json_dumps(f"Bad request - {e!s}")) self.write(safe_json_dumps(f"Bad request - {e!s}"))
self.set_status(400) self.set_status(400)
except Exception: except Exception:
logging.exception("Exception when handling client request to alerts API") logger.exception("Exception when handling client request to alerts API")
self.write(safe_json_dumps("Error - an internal server error occurred.")) self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500) self.set_status(500)
self.set_header("Cache-Control", "no-store") self.set_header("Cache-Control", "no-store")
@@ -110,7 +112,7 @@ class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
self._sse_alert_broadcaster.register(self) self._sse_alert_broadcaster.register(self)
except Exception: except Exception:
logging.exception("Exception when serving SSE socket") logger.exception("Exception when serving SSE socket")
self.close() self.close()
def close(self): def close(self):
@@ -129,7 +131,7 @@ class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
alert.infer_missing(self._credentials) alert.infer_missing(self._credentials)
self.write_message(msg=safe_json_dumps(alert)) self.write_message(msg=safe_json_dumps(alert))
except Exception: except Exception:
logging.exception("Exception in SSE callback, connection will be closed") logger.exception("Exception in SSE callback, connection will be closed")
self.close() self.close()
+3 -1
View File
@@ -12,6 +12,8 @@ from tornado.web import Application
from core.prometheus_metrics_handler import api_requests_counter from core.prometheus_metrics_handler import api_requests_counter
from core.utils import safe_json_dumps from core.utils import safe_json_dumps
logger = logging.getLogger(__name__)
CONTINENTS = ["EU", "NA", "SA", "AS", "AF", "OC", "AN"] CONTINENTS = ["EU", "NA", "SA", "AS", "AF", "OC", "AN"]
BANDS = ["160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m", "6m"] BANDS = ["160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m", "6m"]
CONTINENTS_SET = frozenset(CONTINENTS) CONTINENTS_SET = frozenset(CONTINENTS)
@@ -68,6 +70,6 @@ class APIDxStatsHandler(tornado.web.RequestHandler):
self.set_header("Content-Type", "application/json") self.set_header("Content-Type", "application/json")
except Exception: except Exception:
logging.exception("Exception when handling client request to dx stats API") logger.exception("Exception when handling client request to dx stats API")
self.write(safe_json_dumps("Error - an internal server error occurred.")) self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500) self.set_status(500)
+5 -3
View File
@@ -22,6 +22,8 @@ from core.utils import safe_json_dumps
from data.lookup_credentials import extract_credentials from data.lookup_credentials import extract_credentials
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
logger = logging.getLogger(__name__)
class APILookupCallHandler(tornado.web.RequestHandler): class APILookupCallHandler(tornado.web.RequestHandler):
"""API request handler for /api/v2/lookup/call""" """API request handler for /api/v2/lookup/call"""
@@ -66,7 +68,7 @@ class APILookupCallHandler(tornado.web.RequestHandler):
self.set_status(422) self.set_status(422)
except Exception: except Exception:
logging.exception("Exception when handling client request to call lookup API") logger.exception("Exception when handling client request to call lookup API")
self.write(safe_json_dumps("Error - an internal server error occurred.")) self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500) self.set_status(500)
@@ -124,7 +126,7 @@ class APILookupSIGRefHandler(tornado.web.RequestHandler):
self.set_status(422) self.set_status(422)
except Exception: except Exception:
logging.exception("Exception when handling client request to sig ref lookup API") logger.exception("Exception when handling client request to sig ref lookup API")
self.write(safe_json_dumps("Error - an internal server error occurred.")) self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500) self.set_status(500)
@@ -192,7 +194,7 @@ class APILookupGridHandler(tornado.web.RequestHandler):
self.set_status(422) self.set_status(422)
except Exception: except Exception:
logging.exception("Exception when handling client request to grid ref lookup API") logger.exception("Exception when handling client request to grid ref lookup API")
self.write(safe_json_dumps("Error - an internal server error occurred.")) self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500) self.set_status(500)
+3 -1
View File
@@ -19,6 +19,8 @@ from core.constants import (
from core.prometheus_metrics_handler import api_requests_counter from core.prometheus_metrics_handler import api_requests_counter
from core.utils import safe_json_dumps from core.utils import safe_json_dumps
logger = logging.getLogger(__name__)
class APIOptionsHandler(tornado.web.RequestHandler): class APIOptionsHandler(tornado.web.RequestHandler):
"""API request handler for /api/v2/options""" """API request handler for /api/v2/options"""
@@ -119,6 +121,6 @@ class APIOptionsHandler(tornado.web.RequestHandler):
self.set_header("Content-Type", "application/json") self.set_header("Content-Type", "application/json")
except Exception: except Exception:
logging.exception("Exception when handling client request to options API") logger.exception("Exception when handling client request to options API")
self.write(safe_json_dumps("Error - an internal server error occurred.")) self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500) self.set_status(500)
+3 -1
View File
@@ -10,6 +10,8 @@ from tornado.web import Application
from core.prometheus_metrics_handler import api_requests_counter from core.prometheus_metrics_handler import api_requests_counter
from core.utils import safe_json_dumps from core.utils import safe_json_dumps
logger = logging.getLogger(__name__)
class APISolarConditionsHandler(tornado.web.RequestHandler): class APISolarConditionsHandler(tornado.web.RequestHandler):
"""API request handler for /api/v2/solar""" """API request handler for /api/v2/solar"""
@@ -42,6 +44,6 @@ class APISolarConditionsHandler(tornado.web.RequestHandler):
self.set_header("Content-Type", "application/json") self.set_header("Content-Type", "application/json")
except Exception: except Exception:
logging.exception("Exception when handling client request to solar conditions API") logger.exception("Exception when handling client request to solar conditions API")
self.write(safe_json_dumps("Error - an internal server error occurred.")) self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500) self.set_status(500)
+5 -3
View File
@@ -13,6 +13,8 @@ from core.prometheus_metrics_handler import api_requests_counter
from core.utils import safe_json_dumps from core.utils import safe_json_dumps
from data.lookup_credentials import extract_credentials from data.lookup_credentials import extract_credentials
logger = logging.getLogger(__name__)
class APISpotsHandler(tornado.web.RequestHandler): class APISpotsHandler(tornado.web.RequestHandler):
"""API request handler for /api/v2/spots""" """API request handler for /api/v2/spots"""
@@ -63,7 +65,7 @@ class APISpotsHandler(tornado.web.RequestHandler):
self.write(safe_json_dumps(f"Bad request - {e!s}")) self.write(safe_json_dumps(f"Bad request - {e!s}"))
self.set_status(400) self.set_status(400)
except Exception: except Exception:
logging.exception("Excedption when handling client request to spots API") logger.exception("Excedption when handling client request to spots API")
self.write(safe_json_dumps("Error - an internal server error occurred.")) self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500) self.set_status(500)
self.set_header("Cache-Control", "no-store") self.set_header("Cache-Control", "no-store")
@@ -112,7 +114,7 @@ class APISpotsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
self._sse_spot_broadcaster.register(self) self._sse_spot_broadcaster.register(self)
except Exception: except Exception:
logging.exception("Exception when serving SSE socket") logger.exception("Exception when serving SSE socket")
self.close() self.close()
def close(self): def close(self):
@@ -132,7 +134,7 @@ class APISpotsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
spot.infer_missing(self._credentials) spot.infer_missing(self._credentials)
self.write_message(msg=safe_json_dumps(spot)) self.write_message(msg=safe_json_dumps(spot))
except Exception: except Exception:
logging.exception("Exception in SSE callback, connection will be closed") logger.exception("Exception in SSE callback, connection will be closed")
self.close() self.close()
+3 -1
View File
@@ -10,6 +10,8 @@ from tornado.web import Application
from core.prometheus_metrics_handler import api_requests_counter from core.prometheus_metrics_handler import api_requests_counter
from core.utils import safe_json_dumps from core.utils import safe_json_dumps
logger = logging.getLogger(__name__)
class APIStatusHandler(tornado.web.RequestHandler): class APIStatusHandler(tornado.web.RequestHandler):
"""API request handler for /api/v2/status""" """API request handler for /api/v2/status"""
@@ -42,6 +44,6 @@ class APIStatusHandler(tornado.web.RequestHandler):
self.set_header("Content-Type", "application/json") self.set_header("Content-Type", "application/json")
except Exception: except Exception:
logging.exception("Exception when handling client request to status API") logger.exception("Exception when handling client request to status API")
self.write(safe_json_dumps("Error - an internal server error occurred.")) self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500) self.set_status(500)
+3 -1
View File
@@ -15,6 +15,8 @@ from core.sig_utils import get_ref_regex_for_sig
from core.utils import infer_band_from_freq, safe_json_dumps from core.utils import infer_band_from_freq, safe_json_dumps
from data.spot import Spot from data.spot import Spot
logger = logging.getLogger(__name__)
class V1APISpotHandler(tornado.web.RequestHandler): class V1APISpotHandler(tornado.web.RequestHandler):
"""API request handler for /api/v1/spot (POST). Included in early Spothole v2 for backwards compatibility.""" """API request handler for /api/v1/spot (POST). Included in early Spothole v2 for backwards compatibility."""
@@ -143,7 +145,7 @@ class V1APISpotHandler(tornado.web.RequestHandler):
self.set_header("Content-Type", "application/json") self.set_header("Content-Type", "application/json")
except Exception: except Exception:
logging.exception("Exception when handling client request to add spot API") logger.exception("Exception when handling client request to add spot API")
self.write(safe_json_dumps("Error - an internal server error occurred.")) self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500) self.set_status(500)
self.set_header("Cache-Control", "no-store") self.set_header("Cache-Control", "no-store")
+3 -1
View File
@@ -3,6 +3,8 @@ import threading
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
logger = logging.getLogger(__name__)
class SSEBroadcaster: class SSEBroadcaster:
"""Bridge between DataStore listener callbacks (which fire on provider threads) to Tornado's async SSE handlers """Bridge between DataStore listener callbacks (which fire on provider threads) to Tornado's async SSE handlers
@@ -35,5 +37,5 @@ class SSEBroadcaster:
handler.callback(value) handler.callback(value)
except Exception: except Exception:
# Connection probably dropped, ignore and de-register the handler to stop getting future items. # Connection probably dropped, ignore and de-register the handler to stop getting future items.
logging.debug("Failed to push to an SSE client; dropping it") logger.debug("Failed to push to an SSE client; dropping it")
self.unregister(handler) self.unregister(handler)
+7 -5
View File
@@ -34,6 +34,8 @@ from server.handlers.metrics import PrometheusMetricsHandler
from server.handlers.pagetemplate import PageTemplateHandler from server.handlers.pagetemplate import PageTemplateHandler
from server.sse_broadcaster import SSEBroadcaster from server.sse_broadcaster import SSEBroadcaster
logger = logging.getLogger(__name__)
_HERE = os.path.dirname(__file__ or "") _HERE = os.path.dirname(__file__ or "")
@@ -159,7 +161,7 @@ class WebServer:
# If in API-only mode, serve a basic homepage; in normal mode, serve the usual UI routes # If in API-only mode, serve a basic homepage; in normal mode, serve the usual UI routes
if self._api_only_mode: if self._api_only_mode:
logging.info("API-only mode is enabled. Web UI will not be served.") logger.info("API-only mode is enabled. Web UI will not be served.")
ui_routes = [ ui_routes = [
( (
r"/", r"/",
@@ -238,8 +240,8 @@ class WebServer:
debug=False, debug=False,
) )
app.listen(self._port, xheaders=True) app.listen(self._port, xheaders=True)
logging.info(f"Web server running on port {WEB_SERVER_PORT!s}") logger.info(f"Web server running on port {WEB_SERVER_PORT!s}")
logging.info(f"You can access your copy of Spothole at {BASE_URL}") logger.info(f"You can access your copy of Spothole at {BASE_URL}")
await self._shutdown_event.wait() await self._shutdown_event.wait()
@@ -249,9 +251,9 @@ def request_log(handler):
if LOG_WEB_REQUESTS: if LOG_WEB_REQUESTS:
if handler.get_status() < 500: if handler.get_status() < 500:
log_method = logging.info log_method = logger.info
else: else:
log_method = logging.warning log_method = logger.warning
request = handler.request request = handler.request
client_ip = request.remote_ip client_ip = request.remote_ip
+5 -3
View File
@@ -12,6 +12,8 @@ from core.data_store import DATA_STORE
from core.status_reporter import StatusReporter from core.status_reporter import StatusReporter
from server.webserver import WEB_SERVER from server.webserver import WEB_SERVER
logger = logging.getLogger(__name__)
# Globals # Globals
run = True run = True
@@ -21,7 +23,7 @@ def shutdown(_signum=None, _frame=None):
global run global run
logging.info("Stopping program...") logger.info("Stopping program...")
WEB_SERVER.stop() WEB_SERVER.stop()
DATA_PROVIDERS.stop() DATA_PROVIDERS.stop()
CLEANUP_TIMER.stop() CLEANUP_TIMER.stop()
@@ -41,8 +43,8 @@ if __name__ == "__main__":
root.handlers.clear() root.handlers.clear()
root.addHandler(handler) root.addHandler(handler)
logging.info("Starting...") logger.info("Starting...")
logging.info(f"This is Spothole version {SOFTWARE_VERSION}. This instance is run by {SERVER_OWNER_CALLSIGN}.") logger.info(f"This is Spothole version {SOFTWARE_VERSION}. This instance is run by {SERVER_OWNER_CALLSIGN}.")
# Shut down gracefully on SIGINT # Shut down gracefully on SIGINT
signal.signal(signal.SIGINT, shutdown) signal.signal(signal.SIGINT, shutdown)
+1 -1
View File
@@ -76,7 +76,7 @@
</div> </div>
<script src="/static/js/add-spot.js?v=1786779098"></script> <script src="/static/js/add-spot.js?v=1786779306"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-add-spot").addClass("active"); $("#nav-link-add-spot").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+1 -1
View File
@@ -82,7 +82,7 @@
</div> </div>
<script src="/static/js/alerts.js?v=1786779098"></script> <script src="/static/js/alerts.js?v=1786779307"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-alerts").addClass("active"); $("#nav-link-alerts").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -79,8 +79,8 @@
</div> </div>
<script src="/static/js/spotsbandsandmap.js?v=1786779098"></script> <script src="/static/js/spotsbandsandmap.js?v=1786779307"></script>
<script src="/static/js/bands.js?v=1786779098"></script> <script src="/static/js/bands.js?v=1786779307"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-bands").addClass("active"); $("#nav-link-bands").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+5 -5
View File
@@ -1,6 +1,6 @@
{% extends "skeleton.html" %} {% extends "skeleton.html" %}
{% block head_extra %} {% block head_extra %}
<link rel="stylesheet" href="/static/css/style.css?v=1786779098" type="text/css"> <link rel="stylesheet" href="/static/css/style.css?v=1786779306" type="text/css">
<link href="/static/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet"> <link href="/static/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet">
<link href="/static/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet"> <link href="/static/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet">
<link href="/static/vendor/css/solid-6.7.2.min.css" rel="stylesheet"> <link href="/static/vendor/css/solid-6.7.2.min.css" rel="stylesheet">
@@ -15,10 +15,10 @@
window.fetchEventSource = fetchEventSource; window.fetchEventSource = fetchEventSource;
</script> </script>
<script src="/static/js/utils.js?v=1786779098"></script> <script src="/static/js/utils.js?v=1786779306"></script>
<script src="/static/js/ui-ham.js?v=1786779098"></script> <script src="/static/js/ui-ham.js?v=1786779306"></script>
<script src="/static/js/geo.js?v=1786779098"></script> <script src="/static/js/geo.js?v=1786779306"></script>
<script src="/static/js/common.js?v=1786779098"></script> <script src="/static/js/common.js?v=1786779306"></script>
{% end %} {% end %}
{% block body %} {% block body %}
<div class="container"> <div class="container">
+1 -1
View File
@@ -284,7 +284,7 @@
</div> </div>
<script src="/static/vendor/js/chart-4.4.9.umd.min.js"></script> <script src="/static/vendor/js/chart-4.4.9.umd.min.js"></script>
<script src="/static/js/conditions.js?v=1786779098"></script> <script src="/static/js/conditions.js?v=1786779307"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-conditions").addClass("active"); $("#nav-link-conditions").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -112,8 +112,8 @@
<script src="/static/vendor/js/leaflet-cqzones.js"></script> <script src="/static/vendor/js/leaflet-cqzones.js"></script>
<script src="/static/vendor/js/leaflet-workedallbritainireland.js" type="module"></script> <script src="/static/vendor/js/leaflet-workedallbritainireland.js" type="module"></script>
<script src="/static/js/spotsbandsandmap.js?v=1786779098"></script> <script src="/static/js/spotsbandsandmap.js?v=1786779307"></script>
<script src="/static/js/map.js?v=1786779098"></script> <script src="/static/js/map.js?v=1786779307"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-map").addClass("active"); $("#nav-link-map").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -118,8 +118,8 @@
</div> </div>
<script src="/static/js/spotsbandsandmap.js?v=1786779098"></script> <script src="/static/js/spotsbandsandmap.js?v=1786779306"></script>
<script src="/static/js/spots.js?v=1786779098"></script> <script src="/static/js/spots.js?v=1786779306"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-spots").addClass("active"); $("#nav-link-spots").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+1 -1
View File
@@ -86,7 +86,7 @@
</div> </div>
</div> </div>
<script src="/static/js/status.js?v=1786779098"></script> <script src="/static/js/status.js?v=1786779307"></script>
<script> <script>
$(document).ready(function () { $(document).ready(function () {
$("#nav-link-status").addClass("active"); $("#nav-link-status").addClass("active");