Improve adherence to python coding standards and clear up IDE static analysis warnings

This commit is contained in:
Ian Renton
2026-02-27 19:17:04 +00:00
parent 6b18ec6f88
commit 6982354364
53 changed files with 633 additions and 626 deletions

View File

@@ -27,30 +27,30 @@ class LookupHelper:
lookup methods will fail if start() has not yet been called. This therefore needs starting before any spot or
alert handlers are created."""
self.CLUBLOG_CALLSIGN_DATA_CACHE = None
self.LOOKUP_LIB_CLUBLOG_XML = None
self.CLUBLOG_XML_AVAILABLE = None
self.LOOKUP_LIB_CLUBLOG_API = None
self.CLUBLOG_XML_DOWNLOAD_LOCATION = None
self.CLUBLOG_API_AVAILABLE = None
self.CLUBLOG_CTY_XML_CACHE = None
self.CLUBLOG_API_KEY = None
self.QRZ_CALLSIGN_DATA_CACHE = None
self.LOOKUP_LIB_QRZ = None
self.QRZ_AVAILABLE = None
self.HAMQTH_AVAILABLE = None
self.HAMQTH_CALLSIGN_DATA_CACHE = None
self.HAMQTH_BASE_URL = "https://www.hamqth.com/xml.php"
self._clublog_callsign_data_cache = None
self._lookup_lib_clublog_xml = None
self._clublog_xml_available = None
self._lookup_lib_clublog_api = None
self._clublog_xml_download_location = None
self._clublog_api_available = None
self._clublog_cty_xml_cache = None
self._clublog_api_key = None
self._qrz_callsign_data_cache = None
self._lookup_lib_qrz = None
self._qrz_available = None
self._hamqth_available = None
self._hamqth_callsign_data_cache = None
self._hamqth_base_url = "https://www.hamqth.com/xml.php"
# HamQTH session keys expire after an hour. Rather than working out how much time has passed manually, we cheat
# and cache the HTTP response for 55 minutes, so when the login URL is queried within 55 minutes of the previous
# time, you just get the cached response.
self.HAMQTH_SESSION_LOOKUP_CACHE = CachedSession("cache/hamqth_session_cache",
expire_after=timedelta(minutes=55))
self.CALL_INFO_BASIC = None
self.LOOKUP_LIB_BASIC = None
self.COUNTRY_FILES_CTY_PLIST_DOWNLOAD_LOCATION = None
self.DXCC_JSON_DOWNLOAD_LOCATION = None
self.DXCC_DATA = None
self._hamqth_session_lookup_cache = CachedSession("cache/hamqth_session_cache",
expire_after=timedelta(minutes=55))
self._call_info_basic = None
self._lookup_lib_basic = None
self._country_files_cty_plist_download_location = None
self._dxcc_json_download_location = None
self._dxcc_data = None
def start(self):
# Lookup helpers from pyhamtools. We use five (!) of these. The simplest is country-files.com, which downloads
@@ -58,55 +58,55 @@ class LookupHelper:
# If the user provides login details/API keys, we also set up helpers for QRZ.com, HamQTH, Clublog (live API
# request), and Clublog (XML download). The lookup functions iterate through these in a sensible order, looking
# for suitable data.
self.COUNTRY_FILES_CTY_PLIST_DOWNLOAD_LOCATION = "cache/cty.plist"
success = self.download_country_files_cty_plist()
self._country_files_cty_plist_download_location = "cache/cty.plist"
success = self._download_country_files_cty_plist()
if success:
self.LOOKUP_LIB_BASIC = LookupLib(lookuptype="countryfile",
filename=self.COUNTRY_FILES_CTY_PLIST_DOWNLOAD_LOCATION)
self._lookup_lib_basic = LookupLib(lookuptype="countryfile",
filename=self._country_files_cty_plist_download_location)
else:
self.LOOKUP_LIB_BASIC = LookupLib(lookuptype="countryfile")
self.CALL_INFO_BASIC = Callinfo(self.LOOKUP_LIB_BASIC)
self._lookup_lib_basic = LookupLib(lookuptype="countryfile")
self._call_info_basic = Callinfo(self._lookup_lib_basic)
self.QRZ_AVAILABLE = config["qrz-username"] != "" and config["qrz-password"] != ""
if self.QRZ_AVAILABLE:
self.LOOKUP_LIB_QRZ = LookupLib(lookuptype="qrz", username=config["qrz-username"],
pwd=config["qrz-password"])
self.QRZ_CALLSIGN_DATA_CACHE = Cache('cache/qrz_callsign_lookup_cache')
self._qrz_available = config["qrz-username"] != "" and config["qrz-password"] != ""
if self._qrz_available:
self._lookup_lib_qrz = LookupLib(lookuptype="qrz", username=config["qrz-username"],
pwd=config["qrz-password"])
self._qrz_callsign_data_cache = Cache('cache/qrz_callsign_lookup_cache')
self.HAMQTH_AVAILABLE = config["hamqth-username"] != "" and config["hamqth-password"] != ""
self.HAMQTH_CALLSIGN_DATA_CACHE = Cache('cache/hamqth_callsign_lookup_cache')
self._hamqth_available = config["hamqth-username"] != "" and config["hamqth-password"] != ""
self._hamqth_callsign_data_cache = Cache('cache/hamqth_callsign_lookup_cache')
self.CLUBLOG_API_KEY = config["clublog-api-key"]
self.CLUBLOG_CTY_XML_CACHE = CachedSession("cache/clublog_cty_xml_cache", expire_after=timedelta(days=10))
self.CLUBLOG_API_AVAILABLE = self.CLUBLOG_API_KEY != ""
self.CLUBLOG_XML_DOWNLOAD_LOCATION = "cache/cty.xml"
if self.CLUBLOG_API_AVAILABLE:
self.LOOKUP_LIB_CLUBLOG_API = LookupLib(lookuptype="clublogapi", apikey=self.CLUBLOG_API_KEY)
success = self.download_clublog_ctyxml()
self.CLUBLOG_XML_AVAILABLE = success
self._clublog_api_key = config["clublog-api-key"]
self._clublog_cty_xml_cache = CachedSession("cache/clublog_cty_xml_cache", expire_after=timedelta(days=10))
self._clublog_api_available = self._clublog_api_key != ""
self._clublog_xml_download_location = "cache/cty.xml"
if self._clublog_api_available:
self._lookup_lib_clublog_api = LookupLib(lookuptype="clublogapi", apikey=self._clublog_api_key)
success = self._download_clublog_ctyxml()
self._clublog_xml_available = success
if success:
self.LOOKUP_LIB_CLUBLOG_XML = LookupLib(lookuptype="clublogxml",
filename=self.CLUBLOG_XML_DOWNLOAD_LOCATION)
self.CLUBLOG_CALLSIGN_DATA_CACHE = Cache('cache/clublog_callsign_lookup_cache')
self._lookup_lib_clublog_xml = LookupLib(lookuptype="clublogxml",
filename=self._clublog_xml_download_location)
self._clublog_callsign_data_cache = Cache('cache/clublog_callsign_lookup_cache')
# We also get a lookup of DXCC data from K0SWE to use for additional lookups of e.g. flags.
self.DXCC_JSON_DOWNLOAD_LOCATION = "cache/dxcc.json"
success = self.download_dxcc_json()
self._dxcc_json_download_location = "cache/dxcc.json"
success = self._download_dxcc_json()
if success:
with open(self.DXCC_JSON_DOWNLOAD_LOCATION) as f:
with open(self._dxcc_json_download_location) as f:
tmp_dxcc_data = json.load(f)["dxcc"]
# Reformat as a map for faster lookup
self.DXCC_DATA = {}
self._dxcc_data = {}
for dxcc in tmp_dxcc_data:
self.DXCC_DATA[dxcc["entityCode"]] = dxcc
self._dxcc_data[dxcc["entityCode"]] = dxcc
else:
logging.error("Could not download DXCC data, flags and similar data may be missing!")
# Precompile regex matches for DXCCs to improve efficiency when iterating through them
for dxcc in self.DXCC_DATA.values():
for dxcc in self._dxcc_data.values():
dxcc["_prefixRegexCompiled"] = re.compile(dxcc["prefixRegex"])
def download_country_files_cty_plist(self):
def _download_country_files_cty_plist(self):
"""Download the cty.plist file from country-files.com on first startup. The pyhamtools lib can actually download and use
this itself, but it's occasionally offline which causes it to throw an error. By downloading it separately, we can
catch errors and handle them, falling back to a previous copy of the file in the cache, and we can use the
@@ -117,7 +117,7 @@ class LookupHelper:
response = SEMI_STATIC_URL_DATA_CACHE.get("https://www.country-files.com/cty/cty.plist",
headers=HTTP_HEADERS).text
with open(self.COUNTRY_FILES_CTY_PLIST_DOWNLOAD_LOCATION, "w") as f:
with open(self._country_files_cty_plist_download_location, "w") as f:
f.write(response)
f.flush()
return True
@@ -126,7 +126,7 @@ class LookupHelper:
logging.error("Exception when downloading Clublog cty.xml", e)
return False
def download_dxcc_json(self):
def _download_dxcc_json(self):
"""Download the dxcc.json file on first startup."""
try:
@@ -135,7 +135,7 @@ class LookupHelper:
"https://raw.githubusercontent.com/k0swe/dxcc-json/refs/heads/main/dxcc.json",
headers=HTTP_HEADERS).text
with open(self.DXCC_JSON_DOWNLOAD_LOCATION, "w") as f:
with open(self._dxcc_json_download_location, "w") as f:
f.write(response)
f.flush()
return True
@@ -144,20 +144,20 @@ class LookupHelper:
logging.error("Exception when downloading dxcc.json", e)
return False
def download_clublog_ctyxml(self):
def _download_clublog_ctyxml(self):
"""Download the cty.xml (gzipped) file from Clublog on first startup, so we can use it in preference to querying the
database live if possible."""
try:
logging.info("Downloading Clublog cty.xml.gz...")
response = self.CLUBLOG_CTY_XML_CACHE.get("https://cdn.clublog.org/cty.php?api=" + self.CLUBLOG_API_KEY,
headers=HTTP_HEADERS)
response = self._clublog_cty_xml_cache.get("https://cdn.clublog.org/cty.php?api=" + self._clublog_api_key,
headers=HTTP_HEADERS)
logging.info("Caching Clublog cty.xml.gz...")
open(self.CLUBLOG_XML_DOWNLOAD_LOCATION + ".gz", 'wb').write(response.content)
with gzip.open(self.CLUBLOG_XML_DOWNLOAD_LOCATION + ".gz", "rb") as uncompressed:
open(self._clublog_xml_download_location + ".gz", 'wb').write(response.content)
with gzip.open(self._clublog_xml_download_location + ".gz", "rb") as uncompressed:
file_content = uncompressed.read()
logging.info("Caching Clublog cty.xml...")
with open(self.CLUBLOG_XML_DOWNLOAD_LOCATION, "wb") as f:
with open(self._clublog_xml_download_location, "wb") as f:
f.write(file_content)
f.flush()
return True
@@ -166,69 +166,36 @@ class LookupHelper:
logging.error("Exception when downloading Clublog cty.xml", e)
return False
def infer_mode_from_comment(self, comment):
"""Infer a mode from the comment"""
for mode in ALL_MODES:
if mode in comment.upper():
return mode
for mode in MODE_ALIASES.keys():
if mode in comment.upper():
return MODE_ALIASES[mode]
return None
def infer_mode_type_from_mode(self, mode):
"""Infer a "mode family" from a mode."""
if mode.upper() in CW_MODES:
return "CW"
elif mode.upper() in PHONE_MODES:
return "PHONE"
elif mode.upper() in DATA_MODES:
return "DATA"
else:
if mode.upper() != "OTHER":
logging.warn("Found an unrecognised mode: " + mode + ". Developer should categorise this.")
return None
def infer_band_from_freq(self, freq):
"""Infer a band from a frequency in Hz"""
for b in BANDS:
if b.start_freq <= freq <= b.end_freq:
return b
return UNKNOWN_BAND
def infer_country_from_callsign(self, call):
"""Infer a country name from a callsign"""
try:
# Start with the basic country-files.com-based decoder.
country = self.CALL_INFO_BASIC.get_country_name(call)
except (KeyError, ValueError) as e:
country = self._call_info_basic.get_country_name(call)
except (KeyError, ValueError):
country = None
# Couldn't get anything from basic call info database, try QRZ.com
if not country:
qrz_data = self.get_qrz_data_for_callsign(call)
qrz_data = self._get_qrz_data_for_callsign(call)
if qrz_data and "country" in qrz_data:
country = qrz_data["country"]
# Couldn't get anything from QRZ.com database, try HamQTH
if not country:
hamqth_data = self.get_hamqth_data_for_callsign(call)
hamqth_data = self._get_hamqth_data_for_callsign(call)
if hamqth_data and "country" in hamqth_data:
country = hamqth_data["country"]
# Couldn't get anything from HamQTH database, try Clublog data
if not country:
clublog_data = self.get_clublog_xml_data_for_callsign(call)
clublog_data = self._get_clublog_xml_data_for_callsign(call)
if clublog_data and "Name" in clublog_data:
country = clublog_data["Name"]
if not country:
clublog_data = self.get_clublog_api_data_for_callsign(call)
clublog_data = self._get_clublog_api_data_for_callsign(call)
if clublog_data and "Name" in clublog_data:
country = clublog_data["Name"]
# Couldn't get anything from Clublog database, try DXCC data
if not country:
dxcc_data = self.get_dxcc_data_for_callsign(call)
dxcc_data = self._get_dxcc_data_for_callsign(call)
if dxcc_data and "name" in dxcc_data:
country = dxcc_data["name"]
return country
@@ -238,31 +205,31 @@ class LookupHelper:
try:
# Start with the basic country-files.com-based decoder.
dxcc = self.CALL_INFO_BASIC.get_adif_id(call)
except (KeyError, ValueError) as e:
dxcc = self._call_info_basic.get_adif_id(call)
except (KeyError, ValueError):
dxcc = None
# Couldn't get anything from basic call info database, try QRZ.com
if not dxcc:
qrz_data = self.get_qrz_data_for_callsign(call)
qrz_data = self._get_qrz_data_for_callsign(call)
if qrz_data and "adif" in qrz_data:
dxcc = qrz_data["adif"]
# Couldn't get anything from QRZ.com database, try HamQTH
if not dxcc:
hamqth_data = self.get_hamqth_data_for_callsign(call)
hamqth_data = self._get_hamqth_data_for_callsign(call)
if hamqth_data and "adif" in hamqth_data:
dxcc = hamqth_data["adif"]
# Couldn't get anything from HamQTH database, try Clublog data
if not dxcc:
clublog_data = self.get_clublog_xml_data_for_callsign(call)
clublog_data = self._get_clublog_xml_data_for_callsign(call)
if clublog_data and "DXCC" in clublog_data:
dxcc = clublog_data["DXCC"]
if not dxcc:
clublog_data = self.get_clublog_api_data_for_callsign(call)
clublog_data = self._get_clublog_api_data_for_callsign(call)
if clublog_data and "DXCC" in clublog_data:
dxcc = clublog_data["DXCC"]
# Couldn't get anything from Clublog database, try DXCC data
if not dxcc:
dxcc_data = self.get_dxcc_data_for_callsign(call)
dxcc_data = self._get_dxcc_data_for_callsign(call)
if dxcc_data and "entityCode" in dxcc_data:
dxcc = dxcc_data["entityCode"]
return dxcc
@@ -272,26 +239,26 @@ class LookupHelper:
try:
# Start with the basic country-files.com-based decoder.
continent = self.CALL_INFO_BASIC.get_continent(call)
except (KeyError, ValueError) as e:
continent = self._call_info_basic.get_continent(call)
except (KeyError, ValueError):
continent = None
# Couldn't get anything from basic call info database, try HamQTH
if not continent:
hamqth_data = self.get_hamqth_data_for_callsign(call)
hamqth_data = self._get_hamqth_data_for_callsign(call)
if hamqth_data and "continent" in hamqth_data:
country = hamqth_data["continent"]
continent = hamqth_data["continent"]
# Couldn't get anything from HamQTH database, try Clublog data
if not continent:
clublog_data = self.get_clublog_xml_data_for_callsign(call)
clublog_data = self._get_clublog_xml_data_for_callsign(call)
if clublog_data and "Continent" in clublog_data:
continent = clublog_data["Continent"]
if not continent:
clublog_data = self.get_clublog_api_data_for_callsign(call)
clublog_data = self._get_clublog_api_data_for_callsign(call)
if clublog_data and "Continent" in clublog_data:
continent = clublog_data["Continent"]
# Couldn't get anything from Clublog database, try DXCC data
if not continent:
dxcc_data = self.get_dxcc_data_for_callsign(call)
dxcc_data = self._get_dxcc_data_for_callsign(call)
# Some DXCCs are in two continents, if so don't use the continent data as we can't be sure
if dxcc_data and "continent" in dxcc_data and len(dxcc_data["continent"]) == 1:
continent = dxcc_data["continent"][0]
@@ -302,31 +269,31 @@ class LookupHelper:
try:
# Start with the basic country-files.com-based decoder.
cqz = self.CALL_INFO_BASIC.get_cqz(call)
except (KeyError, ValueError) as e:
cqz = self._call_info_basic.get_cqz(call)
except (KeyError, ValueError):
cqz = None
# Couldn't get anything from basic call info database, try QRZ.com
if not cqz:
qrz_data = self.get_qrz_data_for_callsign(call)
qrz_data = self._get_qrz_data_for_callsign(call)
if qrz_data and "cqz" in qrz_data:
cqz = qrz_data["cqz"]
# Couldn't get anything from QRZ.com database, try HamQTH
if not cqz:
hamqth_data = self.get_hamqth_data_for_callsign(call)
hamqth_data = self._get_hamqth_data_for_callsign(call)
if hamqth_data and "cq" in hamqth_data:
cqz = hamqth_data["cq"]
# Couldn't get anything from HamQTH database, try Clublog data
if not cqz:
clublog_data = self.get_clublog_xml_data_for_callsign(call)
clublog_data = self._get_clublog_xml_data_for_callsign(call)
if clublog_data and "CQZ" in clublog_data:
cqz = clublog_data["CQZ"]
if not cqz:
clublog_data = self.get_clublog_api_data_for_callsign(call)
clublog_data = self._get_clublog_api_data_for_callsign(call)
if clublog_data and "CQZ" in clublog_data:
cqz = clublog_data["CQZ"]
# Couldn't get anything from Clublog database, try DXCC data
if not cqz:
dxcc_data = self.get_dxcc_data_for_callsign(call)
dxcc_data = self._get_dxcc_data_for_callsign(call)
# Some DXCCs are in multiple zones, if so don't use the zone data as we can't be sure
if dxcc_data and "cq" in dxcc_data and len(dxcc_data["cq"]) == 1:
cqz = dxcc_data["cq"][0]
@@ -337,22 +304,22 @@ class LookupHelper:
try:
# Start with the basic country-files.com-based decoder.
ituz = self.CALL_INFO_BASIC.get_ituz(call)
except (KeyError, ValueError) as e:
ituz = self._call_info_basic.get_ituz(call)
except (KeyError, ValueError):
ituz = None
# Couldn't get anything from basic call info database, try QRZ.com
if not ituz:
qrz_data = self.get_qrz_data_for_callsign(call)
qrz_data = self._get_qrz_data_for_callsign(call)
if qrz_data and "ituz" in qrz_data:
ituz = qrz_data["ituz"]
# Couldn't get anything from QRZ.com database, try HamQTH
if not ituz:
hamqth_data = self.get_hamqth_data_for_callsign(call)
hamqth_data = self._get_hamqth_data_for_callsign(call)
if hamqth_data and "itu" in hamqth_data:
ituz = hamqth_data["itu"]
# Couldn't get anything from HamQTH database, Clublog doesn't provide this, so try DXCC data
if not ituz:
dxcc_data = self.get_dxcc_data_for_callsign(call)
dxcc_data = self._get_dxcc_data_for_callsign(call)
# Some DXCCs are in multiple zones, if so don't use the zone data as we can't be sure
if dxcc_data and "itu" in dxcc_data and len(dxcc_data["itu"]) == 1:
ituz = dxcc_data["itu"]
@@ -361,18 +328,18 @@ class LookupHelper:
def get_flag_for_dxcc(self, dxcc):
"""Get an emoji flag for a given DXCC entity ID"""
return self.DXCC_DATA[dxcc]["flag"] if dxcc in self.DXCC_DATA else None
return self._dxcc_data[dxcc]["flag"] if dxcc in self._dxcc_data else None
def infer_name_from_callsign_online_lookup(self, call):
"""Infer an operator name from a callsign (requires QRZ.com/HamQTH)"""
data = self.get_qrz_data_for_callsign(call)
data = self._get_qrz_data_for_callsign(call)
if data and "fname" in data:
name = data["fname"]
if "name" in data:
name = name + " " + data["name"]
return name
data = self.get_hamqth_data_for_callsign(call)
data = self._get_hamqth_data_for_callsign(call)
if data and "nick" in data:
return data["nick"]
else:
@@ -382,12 +349,12 @@ class LookupHelper:
"""Infer a latitude and longitude from a callsign (requires QRZ.com/HamQTH)
Coordinates that look default are rejected (apologies if your position really is 0,0, enjoy your voyage)"""
data = self.get_qrz_data_for_callsign(call)
data = self._get_qrz_data_for_callsign(call)
if data and "latitude" in data and "longitude" in data and (
float(data["latitude"]) != 0 or float(data["longitude"]) != 0) and -89.9 < float(
data["latitude"]) < 89.9:
return [float(data["latitude"]), float(data["longitude"])]
data = self.get_hamqth_data_for_callsign(call)
data = self._get_hamqth_data_for_callsign(call)
if data and "latitude" in data and "longitude" in data and (
float(data["latitude"]) != 0 or float(data["longitude"]) != 0) and -89.9 < float(
data["latitude"]) < 89.9:
@@ -399,11 +366,11 @@ class LookupHelper:
"""Infer a grid locator from a callsign (requires QRZ.com/HamQTH).
Grids that look default are rejected (apologies if your grid really is AA00aa, enjoy your research)"""
data = self.get_qrz_data_for_callsign(call)
data = self._get_qrz_data_for_callsign(call)
if data and "locator" in data and data["locator"].upper() != "AA00" and data["locator"].upper() != "AA00AA" and \
data["locator"].upper() != "AA00AA00":
return data["locator"]
data = self.get_hamqth_data_for_callsign(call)
data = self._get_hamqth_data_for_callsign(call)
if data and "grid" in data and data["grid"].upper() != "AA00" and data["grid"].upper() != "AA00AA" and data[
"grid"].upper() != "AA00AA00":
return data["grid"]
@@ -413,10 +380,10 @@ class LookupHelper:
def infer_qth_from_callsign_online_lookup(self, call):
"""Infer a textual QTH from a callsign (requires QRZ.com/HamQTH)"""
data = self.get_qrz_data_for_callsign(call)
data = self._get_qrz_data_for_callsign(call)
if data and "addr2" in data:
return data["addr2"]
data = self.get_hamqth_data_for_callsign(call)
data = self._get_hamqth_data_for_callsign(call)
if data and "qth" in data:
return data["qth"]
else:
@@ -426,7 +393,7 @@ class LookupHelper:
"""Infer a latitude and longitude from a callsign (using DXCC, probably very inaccurate)"""
try:
data = self.CALL_INFO_BASIC.get_lat_long(call)
data = self._call_info_basic.get_lat_long(call)
if data and "latitude" in data and "longitude" in data:
loc = [float(data["latitude"]), float(data["longitude"])]
else:
@@ -435,11 +402,11 @@ class LookupHelper:
loc = None
# Couldn't get anything from basic call info database, try Clublog data
if not loc:
data = self.get_clublog_xml_data_for_callsign(call)
data = self._get_clublog_xml_data_for_callsign(call)
if data and "Lat" in data and "Lon" in data:
loc = [float(data["Lat"]), float(data["Lon"])]
if not loc:
data = self.get_clublog_api_data_for_callsign(call)
data = self._get_clublog_api_data_for_callsign(call)
if data and "Lat" in data and "Lon" in data:
loc = [float(data["Lat"]), float(data["Lon"])]
return loc
@@ -455,49 +422,28 @@ class LookupHelper:
logging.debug("Invalid lat/lon received for DXCC")
return grid
def infer_mode_from_frequency(self, freq):
"""Infer a mode from the frequency (in Hz) according to the band plan. Just a guess really."""
try:
khz = freq / 1000.0
mode = freq_to_band(khz)["mode"]
# Some additional common digimode ranges in addition to what the 3rd-party freq_to_band function returns.
# This is mostly here just because freq_to_band is very specific about things like FT8 frequencies, and e.g.
# a spot at 7074.5 kHz will be indicated as LSB, even though it's clearly in the FT8 range. Future updates
# might include other common digimode centres of activity here, but this achieves the main goal of keeping
# large numbers of clearly-FT* spots off the list of people filtering out digimodes.
if (7074 <= khz < 7077) or (10136 <= khz < 10139) or (14074 <= khz < 14077) or (18100 <= khz < 18103) or (
21074 <= khz < 21077) or (24915 <= khz < 24918) or (28074 <= khz < 28077):
mode = "FT8"
if (7047.5 <= khz < 7050.5) or (10140 <= khz < 10143) or (14080 <= khz < 14083) or (
18104 <= khz < 18107) or (21140 <= khz < 21143) or (24919 <= khz < 24922) or (28180 <= khz < 28183):
mode = "FT4"
return mode
except KeyError:
return None
def get_qrz_data_for_callsign(self, call):
def _get_qrz_data_for_callsign(self, call):
"""Utility method to get QRZ.com data from cache if possible, if not get it from the API and cache it"""
# Fetch from cache if we can, otherwise fetch from the API and cache it
if call in self.QRZ_CALLSIGN_DATA_CACHE:
return self.QRZ_CALLSIGN_DATA_CACHE.get(call)
elif self.QRZ_AVAILABLE:
if call in self._qrz_callsign_data_cache:
return self._qrz_callsign_data_cache.get(call)
elif self._qrz_available:
try:
data = self.LOOKUP_LIB_QRZ.lookup_callsign(callsign=call)
self.QRZ_CALLSIGN_DATA_CACHE.add(call, data, expire=604800) # 1 week in seconds
data = self._lookup_lib_qrz.lookup_callsign(callsign=call)
self._qrz_callsign_data_cache.add(call, data, expire=604800) # 1 week in seconds
return data
except (KeyError, ValueError):
# QRZ had no info for the call, but maybe it had prefixes or suffixes. Try again with the base call.
try:
data = self.LOOKUP_LIB_QRZ.lookup_callsign(callsign=callinfo.Callinfo.get_homecall(call))
self.QRZ_CALLSIGN_DATA_CACHE.add(call, data, expire=604800) # 1 week in seconds
data = self._lookup_lib_qrz.lookup_callsign(callsign=callinfo.Callinfo.get_homecall(call))
self._qrz_callsign_data_cache.add(call, data, expire=604800) # 1 week in seconds
return data
except (KeyError, ValueError):
# QRZ had no info for the call, that's OK. Cache a None so we don't try to look this up again
self.QRZ_CALLSIGN_DATA_CACHE.add(call, None, expire=604800) # 1 week in seconds
self._qrz_callsign_data_cache.add(call, None, expire=604800) # 1 week in seconds
return None
except (Exception):
except Exception:
# General exception like a timeout when communicating with QRZ. Return None this time, but don't cache
# that, so we can try again next time.
logging.error("Exception when looking up QRZ data")
@@ -505,17 +451,17 @@ class LookupHelper:
else:
return None
def get_hamqth_data_for_callsign(self, call):
def _get_hamqth_data_for_callsign(self, call):
"""Utility method to get HamQTH data from cache if possible, if not get it from the API and cache it"""
# Fetch from cache if we can, otherwise fetch from the API and cache it
if call in self.HAMQTH_CALLSIGN_DATA_CACHE:
return self.HAMQTH_CALLSIGN_DATA_CACHE.get(call)
elif self.HAMQTH_AVAILABLE:
if call in self._hamqth_callsign_data_cache:
return self._hamqth_callsign_data_cache.get(call)
elif self._hamqth_available:
try:
# First we need to log in and get a session token.
session_data = self.HAMQTH_SESSION_LOOKUP_CACHE.get(
self.HAMQTH_BASE_URL + "?u=" + urllib.parse.quote_plus(config["hamqth-username"]) +
session_data = self._hamqth_session_lookup_cache.get(
self._hamqth_base_url + "?u=" + urllib.parse.quote_plus(config["hamqth-username"]) +
"&p=" + urllib.parse.quote_plus(config["hamqth-password"]), headers=HTTP_HEADERS).content
dict_data = xmltodict.parse(session_data)
if "session_id" in dict_data["HamQTH"]["session"]:
@@ -524,78 +470,79 @@ class LookupHelper:
# Now look up the actual data.
try:
lookup_data = SEMI_STATIC_URL_DATA_CACHE.get(
self.HAMQTH_BASE_URL + "?id=" + session_id + "&callsign=" + urllib.parse.quote_plus(
self._hamqth_base_url + "?id=" + session_id + "&callsign=" + urllib.parse.quote_plus(
call) + "&prg=" + HAMQTH_PRG, headers=HTTP_HEADERS).content
data = xmltodict.parse(lookup_data)["HamQTH"]["search"]
self.HAMQTH_CALLSIGN_DATA_CACHE.add(call, data, expire=604800) # 1 week in seconds
self._hamqth_callsign_data_cache.add(call, data, expire=604800) # 1 week in seconds
return data
except (KeyError, ValueError):
# HamQTH had no info for the call, but maybe it had prefixes or suffixes. Try again with the base call.
try:
lookup_data = SEMI_STATIC_URL_DATA_CACHE.get(
self.HAMQTH_BASE_URL + "?id=" + session_id + "&callsign=" + urllib.parse.quote_plus(
self._hamqth_base_url + "?id=" + session_id + "&callsign=" + urllib.parse.quote_plus(
callinfo.Callinfo.get_homecall(call)) + "&prg=" + HAMQTH_PRG,
headers=HTTP_HEADERS).content
data = xmltodict.parse(lookup_data)["HamQTH"]["search"]
self.HAMQTH_CALLSIGN_DATA_CACHE.add(call, data, expire=604800) # 1 week in seconds
self._hamqth_callsign_data_cache.add(call, data, expire=604800) # 1 week in seconds
return data
except (KeyError, ValueError):
# HamQTH had no info for the call, that's OK. Cache a None so we don't try to look this up again
self.HAMQTH_CALLSIGN_DATA_CACHE.add(call, None, expire=604800) # 1 week in seconds
self._hamqth_callsign_data_cache.add(call, None, expire=604800) # 1 week in seconds
return None
else:
logging.warn("HamQTH login details incorrect, failed to look up with HamQTH.")
logging.warning("HamQTH login details incorrect, failed to look up with HamQTH.")
except:
logging.error("Exception when looking up HamQTH data")
return None
return None
def get_clublog_api_data_for_callsign(self, call):
def _get_clublog_api_data_for_callsign(self, call):
"""Utility method to get Clublog API data from cache if possible, if not get it from the API and cache it"""
# Fetch from cache if we can, otherwise fetch from the API and cache it
if call in self.CLUBLOG_CALLSIGN_DATA_CACHE:
return self.CLUBLOG_CALLSIGN_DATA_CACHE.get(call)
elif self.CLUBLOG_API_AVAILABLE:
if call in self._clublog_callsign_data_cache:
return self._clublog_callsign_data_cache.get(call)
elif self._clublog_api_available:
try:
data = self.LOOKUP_LIB_CLUBLOG_API.lookup_callsign(callsign=call)
self.CLUBLOG_CALLSIGN_DATA_CACHE.add(call, data, expire=604800) # 1 week in seconds
data = self._lookup_lib_clublog_api.lookup_callsign(callsign=call)
self._clublog_callsign_data_cache.add(call, data, expire=604800) # 1 week in seconds
return data
except (KeyError, ValueError):
# Clublog had no info for the call, but maybe it had prefixes or suffixes. Try again with the base call.
try:
data = self.LOOKUP_LIB_CLUBLOG_API.lookup_callsign(callsign=callinfo.Callinfo.get_homecall(call))
self.CLUBLOG_CALLSIGN_DATA_CACHE.add(call, data, expire=604800) # 1 week in seconds
data = self._lookup_lib_clublog_api.lookup_callsign(callsign=callinfo.Callinfo.get_homecall(call))
self._clublog_callsign_data_cache.add(call, data, expire=604800) # 1 week in seconds
return data
except (KeyError, ValueError):
# Clublog had no info for the call, that's OK. Cache a None so we don't try to look this up again
self.CLUBLOG_CALLSIGN_DATA_CACHE.add(call, None, expire=604800) # 1 week in seconds
self._clublog_callsign_data_cache.add(call, None, expire=604800) # 1 week in seconds
return None
except APIKeyMissingError:
# User API key was wrong, warn
logging.error("Could not look up via Clublog API, key " + self.CLUBLOG_API_KEY + " was rejected.")
logging.error("Could not look up via Clublog API, key " + self._clublog_api_key + " was rejected.")
return None
else:
return None
def get_clublog_xml_data_for_callsign(self, call):
def _get_clublog_xml_data_for_callsign(self, call):
"""Utility method to get Clublog XML data from file"""
if self.CLUBLOG_XML_AVAILABLE:
if self._clublog_xml_available:
try:
data = self.LOOKUP_LIB_CLUBLOG_XML.lookup_callsign(callsign=call)
data = self._lookup_lib_clublog_xml.lookup_callsign(callsign=call)
return data
except (KeyError, ValueError):
# Clublog had no info for the call, that's OK. Cache a None so we don't try to look this up again
self.CLUBLOG_CALLSIGN_DATA_CACHE.add(call, None, expire=604800) # 1 week in seconds
self._clublog_callsign_data_cache.add(call, None, expire=604800) # 1 week in seconds
return None
else:
return None
def get_dxcc_data_for_callsign(self, call):
def _get_dxcc_data_for_callsign(self, call):
"""Utility method to get generic DXCC data from our lookup table, if we can find it"""
for entry in self.DXCC_DATA.values():
for entry in self._dxcc_data.values():
if entry["_prefixRegexCompiled"].match(call):
return entry
return None
@@ -603,9 +550,66 @@ class LookupHelper:
def stop(self):
"""Shutdown method to close down any caches neatly."""
self.QRZ_CALLSIGN_DATA_CACHE.close()
self.CLUBLOG_CALLSIGN_DATA_CACHE.close()
self._qrz_callsign_data_cache.close()
self._clublog_callsign_data_cache.close()
# Singleton object
lookup_helper = LookupHelper()
def infer_mode_from_comment(comment):
"""Infer a mode from the comment"""
for mode in ALL_MODES:
if mode in comment.upper():
return mode
for mode in MODE_ALIASES.keys():
if mode in comment.upper():
return MODE_ALIASES[mode]
return None
def infer_mode_type_from_mode(mode):
"""Infer a "mode family" from a mode."""
if mode.upper() in CW_MODES:
return "CW"
elif mode.upper() in PHONE_MODES:
return "PHONE"
elif mode.upper() in DATA_MODES:
return "DATA"
else:
if mode.upper() != "OTHER":
logging.warning("Found an unrecognised mode: " + mode + ". Developer should categorise this.")
return None
def infer_band_from_freq(freq):
"""Infer a band from a frequency in Hz"""
for b in BANDS:
if b.start_freq <= freq <= b.end_freq:
return b
return UNKNOWN_BAND
def infer_mode_from_frequency(freq):
"""Infer a mode from the frequency (in Hz) according to the band plan. Just a guess really."""
try:
khz = freq / 1000.0
mode = freq_to_band(khz)["mode"]
# Some additional common digimode ranges in addition to what the 3rd-party freq_to_band function returns.
# This is mostly here just because freq_to_band is very specific about things like FT8 frequencies, and e.g.
# a spot at 7074.5 kHz will be indicated as LSB, even though it's clearly in the FT8 range. Future updates
# might include other common digimode centres of activity here, but this achieves the main goal of keeping
# large numbers of clearly-FT* spots off the list of people filtering out digimodes.
if (7074 <= khz < 7077) or (10136 <= khz < 10139) or (14074 <= khz < 14077) or (18100 <= khz < 18103) or (
21074 <= khz < 21077) or (24915 <= khz < 24918) or (28074 <= khz < 28077):
mode = "FT8"
if (7047.5 <= khz < 7050.5) or (10140 <= khz < 10143) or (14080 <= khz < 14083) or (
18104 <= khz < 18107) or (21140 <= khz < 21143) or (24919 <= khz < 24922) or (28180 <= khz < 28183):
mode = "FT4"
return mode
except KeyError:
return None