Bulk convert comments above classes/functions/methods into proper docstrings

This commit is contained in:
Ian Renton
2026-02-27 14:21:35 +00:00
parent 068c732796
commit 6b18ec6f88
63 changed files with 540 additions and 349 deletions

View File

@@ -7,4 +7,4 @@ from requests_cache import CachedSession
# of time has passed. This is used throughout Spothole to cache data that does not change
# rapidly.
SEMI_STATIC_URL_DATA_CACHE = CachedSession("cache/semi_static_url_data_cache",
expire_after=timedelta(days=30))
expire_after=timedelta(days=30))

View File

@@ -6,11 +6,12 @@ from time import sleep
import pytz
# Provides a timed cleanup of the spot list.
class CleanupTimer:
"""Provides a timed cleanup of the spot list."""
# Constructor
def __init__(self, spots, alerts, web_server, cleanup_interval):
"""Constructor"""
self.spots = spots
self.alerts = alerts
self.web_server = web_server
@@ -20,21 +21,24 @@ class CleanupTimer:
self.status = "Starting"
self._stop_event = Event()
# Start the cleanup timer
def start(self):
"""Start the cleanup timer"""
self._thread = Thread(target=self._run, daemon=True)
self._thread.start()
# Stop any threads and prepare for application shutdown
def stop(self):
"""Stop any threads and prepare for application shutdown"""
self._stop_event.set()
def _run(self):
while not self._stop_event.wait(timeout=self.cleanup_interval):
self._cleanup()
# Perform cleanup and reschedule next timer
def _cleanup(self):
"""Perform cleanup and reschedule next timer"""
try:
# Perform cleanup via letting the data expire
self.spots.expire()

View File

@@ -23,7 +23,7 @@ WEB_UI_OPTIONS = config["web-ui-options"]
# For ease of config, each spot provider owns its own config about whether it should be enabled by default in the web UI
# but for consistency we provide this to the front-end in web-ui-options because it has no impact outside of the web UI.
WEB_UI_OPTIONS["spot-providers-enabled-by-default"] = [p["name"] for p in config["spot-providers"] if p["enabled"] and (
"enabled-by-default-in-web-ui" not in p or p["enabled-by-default-in-web-ui"] == True)]
"enabled-by-default-in-web-ui" not in p or p["enabled-by-default-in-web-ui"] == True)]
# If spotting to this server is enabled, "API" is another valid spot source even though it does not come from
# one of our proviers. We set that to also be enabled by default.
if ALLOW_SPOTTING:

View File

@@ -12,27 +12,27 @@ HAMQTH_PRG = (SOFTWARE_NAME + " v" + SOFTWARE_VERSION + " operated by " + SERVER
# Special Interest Groups
SIGS = [
SIG(name="POTA", description="Parks on the Air", ref_regex=r"[A-Z]{2}\-\d{4,5}"),
SIG(name="SOTA", description="Summits on the Air", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}"),
SIG(name="WWFF", description="World Wide Flora & Fauna", ref_regex=r"[A-Z0-9]{1,3}FF\-\d{4}"),
SIG(name="GMA", description="Global Mountain Activity", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}"),
SIG(name="WWBOTA", description="Worldwide Bunkers on the Air", ref_regex=r"B\/[A-Z0-9]{1,3}\-\d{3,4}"),
SIG(name="HEMA", description="HuMPs Excluding Marilyns Award", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{3}\-\d{3}"),
SIG(name="IOTA", description="Islands on the Air", ref_regex=r"[A-Z]{2}\-\d{3}"),
SIG(name="MOTA", description="Mills on the Air", ref_regex=r"X\d{4-6}"),
SIG(name="ARLHS", description="Amateur Radio Lighthouse Society", ref_regex=r"[A-Z]{3}\-\d{3,4}"),
SIG(name="ILLW", description="International Lighthouse & Lightship Weekend", ref_regex=r"[A-Z]{2}\d{4}"),
SIG(name="SIOTA", description="Silos on the Air", ref_regex=r"[A-Z]{2}\-[A-Z]{3}\d"),
SIG(name="WCA", description="World Castles Award", ref_regex=r"[A-Z0-9]{1,3}\-\d{5}"),
SIG(name="ZLOTA", description="New Zealand on the Air", ref_regex=r"ZL[A-Z]/[A-Z]{2}\-\d{3,4}"),
SIG(name="WOTA", description="Wainwrights on the Air", ref_regex=r"[A-Z]{3}-[0-9]{2}"),
SIG(name="BOTA", description="Beaches on the Air"),
SIG(name="KRMNPA", description="Keith Roget Memorial National Parks Award"),
SIG(name="LLOTA", description="Lagos y Lagunas on the Air", ref_regex=r"[A-Z]{2}\-\d{4}"),
SIG(name="WWTOTA", description="Towers on the Air", ref_regex=r"[A-Z]{2}R\-\d{4}"),
SIG(name="WAB", description="Worked All Britain", ref_regex=r"[A-Z]{1,2}[0-9]{2}"),
SIG(name="WAI", description="Worked All Ireland", ref_regex=r"[A-Z][0-9]{2}"),
SIG(name="TOTA", description="Toilets on the Air", ref_regex=r"T\-[0-9]{2}")
SIG(name="POTA", description="Parks on the Air", ref_regex=r"[A-Z]{2}\-\d{4,5}"),
SIG(name="SOTA", description="Summits on the Air", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}"),
SIG(name="WWFF", description="World Wide Flora & Fauna", ref_regex=r"[A-Z0-9]{1,3}FF\-\d{4}"),
SIG(name="GMA", description="Global Mountain Activity", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}"),
SIG(name="WWBOTA", description="Worldwide Bunkers on the Air", ref_regex=r"B\/[A-Z0-9]{1,3}\-\d{3,4}"),
SIG(name="HEMA", description="HuMPs Excluding Marilyns Award", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{3}\-\d{3}"),
SIG(name="IOTA", description="Islands on the Air", ref_regex=r"[A-Z]{2}\-\d{3}"),
SIG(name="MOTA", description="Mills on the Air", ref_regex=r"X\d{4-6}"),
SIG(name="ARLHS", description="Amateur Radio Lighthouse Society", ref_regex=r"[A-Z]{3}\-\d{3,4}"),
SIG(name="ILLW", description="International Lighthouse & Lightship Weekend", ref_regex=r"[A-Z]{2}\d{4}"),
SIG(name="SIOTA", description="Silos on the Air", ref_regex=r"[A-Z]{2}\-[A-Z]{3}\d"),
SIG(name="WCA", description="World Castles Award", ref_regex=r"[A-Z0-9]{1,3}\-\d{5}"),
SIG(name="ZLOTA", description="New Zealand on the Air", ref_regex=r"ZL[A-Z]/[A-Z]{2}\-\d{3,4}"),
SIG(name="WOTA", description="Wainwrights on the Air", ref_regex=r"[A-Z]{3}-[0-9]{2}"),
SIG(name="BOTA", description="Beaches on the Air"),
SIG(name="KRMNPA", description="Keith Roget Memorial National Parks Award"),
SIG(name="LLOTA", description="Lagos y Lagunas on the Air", ref_regex=r"[A-Z]{2}\-\d{4}"),
SIG(name="WWTOTA", description="Towers on the Air", ref_regex=r"[A-Z]{2}R\-\d{4}"),
SIG(name="WAB", description="Worked All Britain", ref_regex=r"[A-Z]{1,2}[0-9]{2}"),
SIG(name="WAI", description="Worked All Ireland", ref_regex=r"[A-Z][0-9]{2}"),
SIG(name="TOTA", description="Toilets on the Air", ref_regex=r"T\-[0-9]{2}")
]
# Modes. Note "DIGI" and "DIGITAL" are also supported but are normalised into "DATA".

View File

@@ -18,8 +18,10 @@ for idx in cq_zone_data.index:
for idx in itu_zone_data.index:
prepare(itu_zone_data.at[idx, 'geometry'])
# Finds out which CQ zone a lat/lon point is in.
def lat_lon_to_cq_zone(lat, lon):
"""Finds out which CQ zone a lat/lon point is in."""
lon = ((lon + 180) % 360) - 180
for index, row in cq_zone_data.iterrows():
polygon = Polygon(row["geometry"])
@@ -38,8 +40,9 @@ def lat_lon_to_cq_zone(lat, lon):
return None
# Finds out which ITU zone a lat/lon point is in.
def lat_lon_to_itu_zone(lat, lon):
"""Finds out which ITU zone a lat/lon point is in."""
lon = ((lon + 180) % 360) - 180
for index, row in itu_zone_data.iterrows():
polygon = Polygon(row["geometry"])
@@ -58,9 +61,10 @@ def lat_lon_to_itu_zone(lat, lon):
return None
# Convert a Maidenhead grid reference of arbitrary precision to the lat/long of the centre point of the square.
# Returns None if the grid format is invalid.
def lat_lon_for_grid_centre(grid):
"""Convert a Maidenhead grid reference of arbitrary precision to the lat/long of the centre point of the square.
Returns None if the grid format is invalid."""
lat, lon, lat_cell_size, lon_cell_size = lat_lon_for_grid_sw_corner_plus_size(grid)
if lat is not None and lon is not None and lat_cell_size is not None and lon_cell_size is not None:
return [lat + lat_cell_size / 2.0, lon + lon_cell_size / 2.0]
@@ -68,18 +72,21 @@ def lat_lon_for_grid_centre(grid):
return None
# Convert a Maidenhead grid reference of arbitrary precision to the lat/long of the southwest corner of the square.
# Returns None if the grid format is invalid.
def lat_lon_for_grid_sw_corner(grid):
"""Convert a Maidenhead grid reference of arbitrary precision to the lat/long of the southwest corner of the square.
Returns None if the grid format is invalid."""
lat, lon, lat_cell_size, lon_cell_size = lat_lon_for_grid_sw_corner_plus_size(grid)
if lat is not None and lon is not None:
return [lat, lon]
else:
return None
# Convert a Maidenhead grid reference of arbitrary precision to the lat/long of the northeast corner of the square.
# Returns None if the grid format is invalid.
def lat_lon_for_grid_ne_corner(grid):
"""Convert a Maidenhead grid reference of arbitrary precision to the lat/long of the northeast corner of the square.
Returns None if the grid format is invalid."""
lat, lon, lat_cell_size, lon_cell_size = lat_lon_for_grid_sw_corner_plus_size(grid)
if lat is not None and lon is not None and lat_cell_size is not None and lon_cell_size is not None:
return [lat + lat_cell_size, lon + lon_cell_size]
@@ -87,11 +94,12 @@ def lat_lon_for_grid_ne_corner(grid):
return None
# Convert a Maidenhead grid reference of arbitrary precision to lat/long, including in the result the size of the
# lowest grid square. This is a utility method used by the main methods that return the centre, southwest, and
# northeast coordinates of a grid square.
# The return type is always a tuple of size 4. The elements in it are None if the grid format is invalid.
def lat_lon_for_grid_sw_corner_plus_size(grid):
"""Convert a Maidenhead grid reference of arbitrary precision to lat/long, including in the result the size of the
lowest grid square. This is a utility method used by the main methods that return the centre, southwest, and
northeast coordinates of a grid square.
The return type is always a tuple of size 4. The elements in it are None if the grid format is invalid."""
# Make sure we are in upper case so our maths works. Case is arbitrary for Maidenhead references
grid = grid.upper()
@@ -157,8 +165,9 @@ def lat_lon_for_grid_sw_corner_plus_size(grid):
return lat, lon, lat_cell_size, lon_cell_size
# Convert a Worked All Britain or Worked All Ireland reference to a lat/lon point.
def wab_wai_square_to_lat_lon(ref):
"""Convert a Worked All Britain or Worked All Ireland reference to a lat/lon point."""
# First check we have a valid grid square, and based on what it looks like, use either the Ordnance Survey, Irish,
# or UTM grid systems to perform the conversion.
if re.match(r"^[HNOST][ABCDEFGHJKLMNOPQRSTUVWXYZ][0-9]{2}$", ref):
@@ -172,8 +181,9 @@ def wab_wai_square_to_lat_lon(ref):
return None
# Get a lat/lon point for the centre of an Ordnance Survey grid square
def os_grid_square_to_lat_lon(ref):
"""Get a lat/lon point for the centre of an Ordnance Survey grid square"""
# Convert the letters into multipliers for the 500km squares and 100km squares
offset_500km_multiplier = ord(ref[0]) - 65
offset_100km_multiplier = ord(ref[1]) - 65
@@ -202,8 +212,9 @@ def os_grid_square_to_lat_lon(ref):
return lat, lon
# Get a lat/lon point for the centre of an Irish Grid square.
def irish_grid_square_to_lat_lon(ref):
"""Get a lat/lon point for the centre of an Irish Grid square."""
# Convert the letters into multipliers for the 100km squares
offset_100km_multiplier = ord(ref[0]) - 65
@@ -229,8 +240,9 @@ def irish_grid_square_to_lat_lon(ref):
return lat, lon
# Get a lat/lon point for the centre of a UTM grid square (supports only squares WA & WV for the Channel Islands, nothing else implemented)
def utm_grid_square_to_lat_lon(ref):
"""Get a lat/lon point for the centre of a UTM grid square (supports only squares WA & WV for the Channel Islands, nothing else implemented)"""
# Take the numeric parts of the grid square and multiply by 10000 to get metres from the corner of the letter-based grid square
easting = int(ref[2]) * 10000
northing = int(ref[3]) * 10000

View File

@@ -19,13 +19,14 @@ from core.constants import BANDS, UNKNOWN_BAND, CW_MODES, PHONE_MODES, DATA_MODE
HTTP_HEADERS, HAMQTH_PRG, MODE_ALIASES
# Singleton class that provides lookup functionality.
class LookupHelper:
"""Singleton class that provides lookup functionality."""
# Create the lookup helper. Note that nothing actually happens until the start() method is called, and that all
# lookup methods will fail if start() has not yet been called. This therefore needs starting before any spot or
# alert handlers are created.
def __init__(self):
"""Create the lookup helper. Note that nothing actually happens until the start() method is called, and that all
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
@@ -105,11 +106,12 @@ class LookupHelper:
for dxcc in self.DXCC_DATA.values():
dxcc["_prefixRegexCompiled"] = re.compile(dxcc["prefixRegex"])
# 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
# requests_cache library to prevent re-downloading too quickly if the software keeps restarting.
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
requests_cache library to prevent re-downloading too quickly if the software keeps restarting."""
try:
logging.info("Downloading Country-files.com cty.plist...")
response = SEMI_STATIC_URL_DATA_CACHE.get("https://www.country-files.com/cty/cty.plist",
@@ -124,12 +126,14 @@ class LookupHelper:
logging.error("Exception when downloading Clublog cty.xml", e)
return False
# Download the dxcc.json file on first startup.
def download_dxcc_json(self):
"""Download the dxcc.json file on first startup."""
try:
logging.info("Downloading dxcc.json...")
response = SEMI_STATIC_URL_DATA_CACHE.get("https://raw.githubusercontent.com/k0swe/dxcc-json/refs/heads/main/dxcc.json",
headers=HTTP_HEADERS).text
response = SEMI_STATIC_URL_DATA_CACHE.get(
"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:
f.write(response)
@@ -140,9 +144,10 @@ class LookupHelper:
logging.error("Exception when downloading dxcc.json", e)
return False
# 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.
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,
@@ -161,8 +166,9 @@ class LookupHelper:
logging.error("Exception when downloading Clublog cty.xml", e)
return False
# Infer a mode from the comment
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
@@ -171,8 +177,9 @@ class LookupHelper:
return MODE_ALIASES[mode]
return None
# Infer a "mode family" from a mode.
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:
@@ -184,15 +191,17 @@ class LookupHelper:
logging.warn("Found an unrecognised mode: " + mode + ". Developer should categorise this.")
return None
# Infer a band from a frequency in Hz
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
# Infer a country name from a callsign
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)
@@ -224,8 +233,9 @@ class LookupHelper:
country = dxcc_data["name"]
return country
# Infer a DXCC ID from a callsign
def infer_dxcc_id_from_callsign(self, call):
"""Infer a DXCC ID from a callsign"""
try:
# Start with the basic country-files.com-based decoder.
dxcc = self.CALL_INFO_BASIC.get_adif_id(call)
@@ -257,8 +267,9 @@ class LookupHelper:
dxcc = dxcc_data["entityCode"]
return dxcc
# Infer a continent shortcode from a callsign
def infer_continent_from_callsign(self, call):
"""Infer a continent shortcode from a callsign"""
try:
# Start with the basic country-files.com-based decoder.
continent = self.CALL_INFO_BASIC.get_continent(call)
@@ -286,8 +297,9 @@ class LookupHelper:
continent = dxcc_data["continent"][0]
return continent
# Infer a CQ zone from a callsign
def infer_cq_zone_from_callsign(self, call):
"""Infer a CQ zone from a callsign"""
try:
# Start with the basic country-files.com-based decoder.
cqz = self.CALL_INFO_BASIC.get_cqz(call)
@@ -320,8 +332,9 @@ class LookupHelper:
cqz = dxcc_data["cq"][0]
return cqz
# Infer a ITU zone from a callsign
def infer_itu_zone_from_callsign(self, call):
"""Infer a ITU zone from a callsign"""
try:
# Start with the basic country-files.com-based decoder.
ituz = self.CALL_INFO_BASIC.get_ituz(call)
@@ -345,12 +358,14 @@ class LookupHelper:
ituz = dxcc_data["itu"]
return ituz
# Get an emoji flag for a given DXCC entity ID
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
# Infer an operator name from a callsign (requires QRZ.com/HamQTH)
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)
if data and "fname" in data:
name = data["fname"]
@@ -363,32 +378,41 @@ class LookupHelper:
else:
return None
# 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)
def infer_latlon_from_callsign_online_lookup(self, call):
"""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)
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:
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)
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:
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"])]
else:
return None
# 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)
def infer_grid_from_callsign_online_lookup(self, call):
"""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)
if data and "locator" in data and data["locator"].upper() != "AA00" and data["locator"].upper() != "AA00AA" and data["locator"].upper() != "AA00AA00":
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)
if data and "grid" in data and data["grid"].upper() != "AA00" and data["grid"].upper() != "AA00AA" and data["grid"].upper() != "AA00AA00":
if data and "grid" in data and data["grid"].upper() != "AA00" and data["grid"].upper() != "AA00AA" and data[
"grid"].upper() != "AA00AA00":
return data["grid"]
else:
return None
# Infer a textual QTH from a callsign (requires QRZ.com/HamQTH)
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)
if data and "addr2" in data:
return data["addr2"]
@@ -398,8 +422,9 @@ class LookupHelper:
else:
return None
# Infer a latitude and longitude from a callsign (using DXCC, probably very inaccurate)
def infer_latlon_from_callsign_dxcc(self, call):
"""Infer a latitude and longitude from a callsign (using DXCC, probably very inaccurate)"""
try:
data = self.CALL_INFO_BASIC.get_lat_long(call)
if data and "latitude" in data and "longitude" in data:
@@ -419,8 +444,9 @@ class LookupHelper:
loc = [float(data["Lat"]), float(data["Lon"])]
return loc
# Infer a grid locator from a callsign (using DXCC, probably very inaccurate)
def infer_grid_from_callsign_dxcc(self, call):
"""Infer a grid locator from a callsign (using DXCC, probably very inaccurate)"""
latlon = self.infer_latlon_from_callsign_dxcc(call)
grid = None
try:
@@ -429,8 +455,9 @@ class LookupHelper:
logging.debug("Invalid lat/lon received for DXCC")
return grid
# Infer a mode from the frequency (in Hz) according to the band plan. Just a guess really.
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"]
@@ -449,8 +476,9 @@ class LookupHelper:
except KeyError:
return None
# Utility method to get QRZ.com data from cache if possible, if not get it from the API and cache it
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)
@@ -477,8 +505,9 @@ class LookupHelper:
else:
return None
# Utility method to get HamQTH data from cache if possible, if not get it from the API and cache it
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)
@@ -505,7 +534,8 @@ class LookupHelper:
try:
lookup_data = SEMI_STATIC_URL_DATA_CACHE.get(
self.HAMQTH_BASE_URL + "?id=" + session_id + "&callsign=" + urllib.parse.quote_plus(
callinfo.Callinfo.get_homecall(call)) + "&prg=" + HAMQTH_PRG, headers=HTTP_HEADERS).content
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
return data
@@ -520,8 +550,9 @@ class LookupHelper:
logging.error("Exception when looking up HamQTH data")
return None
# Utility method to get Clublog API data from cache if possible, if not get it from the API and cache it
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)
@@ -547,8 +578,9 @@ class LookupHelper:
else:
return None
# Utility method to get Clublog XML data from file
def get_clublog_xml_data_for_callsign(self, call):
"""Utility method to get Clublog XML data from file"""
if self.CLUBLOG_XML_AVAILABLE:
try:
data = self.LOOKUP_LIB_CLUBLOG_XML.lookup_callsign(callsign=call)
@@ -560,15 +592,17 @@ class LookupHelper:
else:
return None
# Utility method to get generic DXCC data from our lookup table, if we can find it
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():
if entry["_prefixRegexCompiled"].match(call):
return entry
return None
# Shutdown method to close down any caches neatly.
def stop(self):
"""Shutdown method to close down any caches neatly."""
self.QRZ_CALLSIGN_DATA_CACHE.close()
self.CLUBLOG_CALLSIGN_DATA_CACHE.close()

View File

@@ -31,6 +31,7 @@ memory_use_gauge = Gauge(
)
# Get a Prometheus metrics response for the web server
def get_metrics():
"""Get a Prometheus metrics response for the web server"""
return generate_latest(registry)

View File

@@ -8,18 +8,20 @@ from core.constants import SIGS, HTTP_HEADERS
from core.geo_utils import wab_wai_square_to_lat_lon
# Utility function to get the regex string for a SIG reference for a named SIG. If no match is found, None will be returned.
def get_ref_regex_for_sig(sig):
"""Utility function to get the regex string for a SIG reference for a named SIG. If no match is found, None will be returned."""
for s in SIGS:
if s.name.upper() == sig.upper():
return s.ref_regex
return None
# Look up details of a SIG reference (e.g. POTA park) such as name, lat/lon, and grid. Takes in a sig_ref object which
# must at minimum have a "sig" and an "id". The rest of the object will be populated and returned.
# Note there is currently no support for KRMNPA location lookup, see issue #61.
def populate_sig_ref_info(sig_ref):
"""Look up details of a SIG reference (e.g. POTA park) such as name, lat/lon, and grid. Takes in a sig_ref object which
must at minimum have a "sig" and an "id". The rest of the object will be populated and returned.
Note there is currently no support for KRMNPA location lookup, see issue #61."""
if sig_ref.sig is None or sig_ref.id is None:
logging.warning("Failed to look up sig_ref info, sig or id were not set.")
@@ -67,7 +69,7 @@ def populate_sig_ref_info(sig_ref):
sig_ref.longitude = data["longitude"] if "longitude" in data else None
elif sig.upper() == "WWFF":
wwff_csv_data = SEMI_STATIC_URL_DATA_CACHE.get("https://wwff.co/wwff-data/wwff_directory.csv",
headers=HTTP_HEADERS)
headers=HTTP_HEADERS)
wwff_dr = csv.DictReader(wwff_csv_data.content.decode().splitlines())
for row in wwff_dr:
if row["reference"] == ref_id:
@@ -75,7 +77,8 @@ def populate_sig_ref_info(sig_ref):
sig_ref.url = "https://wwff.co/directory/?showRef=" + ref_id
sig_ref.grid = row["iaruLocator"] if "iaruLocator" in row and row["iaruLocator"] != "-" else None
sig_ref.latitude = float(row["latitude"]) if "latitude" in row and row["latitude"] != "-" else None
sig_ref.longitude = float(row["longitude"]) if "longitude" in row and row["longitude"] != "-" else None
sig_ref.longitude = float(row["longitude"]) if "longitude" in row and row[
"longitude"] != "-" else None
break
elif sig.upper() == "SIOTA":
siota_csv_data = SEMI_STATIC_URL_DATA_CACHE.get("https://www.silosontheair.com/data/silos.csv",
@@ -124,7 +127,8 @@ def populate_sig_ref_info(sig_ref):
sig_ref.name = sig_ref.id
sig_ref.url = "https://www.beachesontheair.com/beaches/" + sig_ref.name.lower().replace(" ", "-")
elif sig.upper() == "LLOTA":
data = SEMI_STATIC_URL_DATA_CACHE.get("https://llota.app/api/public/references", headers=HTTP_HEADERS).json()
data = SEMI_STATIC_URL_DATA_CACHE.get("https://llota.app/api/public/references",
headers=HTTP_HEADERS).json()
if data:
for ref in data:
if ref["reference_code"] == ref_id:

View File

@@ -10,12 +10,13 @@ from core.constants import SOFTWARE_VERSION
from core.prometheus_metrics_handler import memory_use_gauge, spots_gauge, alerts_gauge
# Provides a timed update of the application's status data.
class StatusReporter:
"""Provides a timed update of the application's status data."""
# Constructor
def __init__(self, status_data, run_interval, web_server, cleanup_timer, spots, spot_providers, alerts,
alert_providers):
"""Constructor"""
self.status_data = status_data
self.run_interval = run_interval
self.web_server = web_server
@@ -30,24 +31,28 @@ class StatusReporter:
self.status_data["software-version"] = SOFTWARE_VERSION
self.status_data["server-owner-callsign"] = SERVER_OWNER_CALLSIGN
# Start the reporter thread
def start(self):
"""Start the reporter thread"""
self._thread = Thread(target=self._run, daemon=True)
self._thread.start()
# Stop any threads and prepare for application shutdown
def stop(self):
"""Stop any threads and prepare for application shutdown"""
self._stop_event.set()
# Thread entry point: report immediately on startup, then on each interval until stopped
def _run(self):
"""Thread entry point: report immediately on startup, then on each interval until stopped"""
while True:
self._report()
if self._stop_event.wait(timeout=self.run_interval):
break
# Write status information
def _report(self):
"""Write status information"""
self.status_data["uptime"] = (datetime.now(pytz.UTC) - self.startup_time).total_seconds()
self.status_data["mem_use_mb"] = round(psutil.Process(os.getpid()).memory_info().rss / (1024 * 1024), 3)
self.status_data["num_spots"] = len(self.spots)
@@ -57,7 +62,8 @@ class StatusReporter:
"last_updated": p.last_update_time.replace(
tzinfo=pytz.UTC).timestamp() if p.last_update_time.year > 2000 else 0,
"last_spot": p.last_spot_time.replace(
tzinfo=pytz.UTC).timestamp() if p.last_spot_time.year > 2000 else 0}, self.spot_providers))
tzinfo=pytz.UTC).timestamp() if p.last_spot_time.year > 2000 else 0},
self.spot_providers))
self.status_data["alert_providers"] = list(
map(lambda p: {"name": p.name, "enabled": p.enabled, "status": p.status,
"last_updated": p.last_update_time.replace(
@@ -81,4 +87,4 @@ class StatusReporter:
# Update Prometheus metrics
memory_use_gauge.set(psutil.Process(os.getpid()).memory_info().rss * 1024)
spots_gauge.set(len(self.spots))
alerts_gauge.set(len(self.alerts))
alerts_gauge.set(len(self.alerts))

View File

@@ -1,14 +1,15 @@
# Convert objects to serialisable things. Used by JSON serialiser as a default when it encounters unserializable things.
# Just converts objects to dict. Try to avoid doing anything clever here when serialising spots, because we also need
# to receive spots without complex handling.
def serialize_everything(obj):
"""Convert objects to serialisable things. Used by JSON serialiser as a default when it encounters unserializable things.
Just converts objects to dict. Try to avoid doing anything clever here when serialising spots, because we also need
to receive spots without complex handling."""
return obj.__dict__
# Empty a queue
def empty_queue(q):
"""Empty a queue"""
while not q.empty():
try:
q.get_nowait()
except:
break
break