mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-03-15 20:34:31 +00:00
Bulk convert comments above classes/functions/methods into proper docstrings
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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".
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user