Refactor of caching & data storage part 7 #118

This commit is contained in:
Ian Renton
2026-08-01 10:34:20 +01:00
parent b7d2cf0fdb
commit 41aecc6007
10 changed files with 121 additions and 105 deletions
@@ -329,7 +329,7 @@ class LookupHelper:
def get_flag_for_dxcc(self, dxcc):
"""Get an emoji flag for a given DXCC entity ID"""
dxcc_data = DATA_STORE.dxcc_data[dxcc]
dxcc_data = DATA_STORE.dxcc_data[dxcc] if dxcc in DATA_STORE.dxcc_data else None
return dxcc_data["flag"] if dxcc_data else None
def infer_name_from_callsign_online_lookup(self, call, credentials=None):
@@ -621,9 +621,9 @@ class LookupHelper:
def _get_dxcc_data_for_callsign(self, call) -> dict | None:
"""Utility method to get generic DXCC data from our lookup table, if we can find it"""
for entry in [DATA_STORE.dxcc_data[key] for key in DATA_STORE.dxcc_data]:
if entry["_prefixRegexCompiled"].match(call):
return entry
for pattern, entity_code in DATA_STORE.dxcc_lookup_by_call_regex:
if pattern.match(call):
return DATA_STORE.dxcc_data[entity_code]
return None
def stop(self):
+12
View File
@@ -1,4 +1,5 @@
import logging
import re
from pathlib import Path
import diskcache
@@ -23,6 +24,7 @@ class DataStore:
self.spots = None
self.callsigns = None
self.dxcc_data = None
self.dxcc_lookup_by_call_regex = []
self.sigrefs = None
self.status_data = None
self._status = None
@@ -46,6 +48,7 @@ class DataStore:
# Standard disk cache for static reference and SIG ref data. Separate provider threads will repopulate these on
# a regular basis but there's no need for a TTL since old data is better than no data.
self.dxcc_data = diskcache.Cache(CACHE_DIR + "dxcc_data")
self.regenerateCallRegexToDXCCEntityMap()
# For SIG reference data specifically, we need to key on both SIG *and* reference, and trying to do two layers
# of dict in diskcache absolutely destroys performance with unpickling huge dicts, so we have an ugly "SIG:ref"
@@ -72,6 +75,15 @@ class DataStore:
snapshot_interval_sec=self._SPOT_ALERT_SNAPSHOT_INTERVAL_SEC)
logging.info(f"Loaded %d alerts from a previous run.", len(self.alerts.keys()))
def regenerateCallRegexToDXCCEntityMap(self):
"""DXCC entity data from K0SWE includes a regex which we can use to match a callsign, and determine which DXCC
entity it belongs to. But getting every DXCC entity data object out of DiskCache, iterating, compiling its regex
and testing the callsign every time is expensive. So instead we build a separate in-memory lookup of compiled
regex against DXCC entity code, as a list of tuples we can iterate through."""
for entry in [DATA_STORE.dxcc_data[key] for key in DATA_STORE.dxcc_data]:
self.dxcc_lookup_by_call_regex.append((re.compile(entry["prefixRegex"]), entry["entityCode"]))
def close(self):
self.spots.close()
self.alerts.close()
+93
View File
@@ -0,0 +1,93 @@
import logging
from pyhamtools.locator import locator_to_latlong, latlong_to_locator
from core.data_store import DATA_STORE
from core.geo_utils import wab_wai_square_to_lat_lon
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. This makes
use of SIG ref data in the data store, live lookups from the web, or just automatic calculation depending on which
SIG we are getting data for.
Note there is currently no support for KRMNPA location lookup, see issue #61."""
if sig_ref.sig is None or sig_ref.sig == "" or sig_ref.id is None or sig_ref.id == "":
logging.debug("Failed to look up sig_ref info, sig or id were not set.")
return sig_ref
sig = sig_ref.sig
ref_id = sig_ref.id
try:
### FUDGES ###
#
# DME fudge. Our database has leading zeros padding to 5 digits which is the expected format, but not all
# activators add leading zeros.
if sig.upper() == "DME":
ref_id = ref_id.zfill(5)
# KRMNPA & SANPCPA fudge. These don't have their own reference system, they just use VKFF references, so pretend
# the sig is WWFF and carry on
if sig.upper() == "KRMNPA" or sig.upper() == "SANPCPA":
sig = "WWFF"
### SKIPS ###
#
# If the SIG is HEMA, we have no current lookup for this so just skip the lookup here.
if sig.upper() == "HEMA":
return sig_ref
### PROGRAMMATIC DATA GENERATION INSTEAD OF LOOKUPS ###
#
# If the SIG is Tiles, WAB, WAI or BOTA (Beaches), we don't have anything to look up from the data store, we can
# calculate all the information we are going to get directly.
if sig.upper() == "TILES":
# Tiles on the Air just uses Maidenhead 6-digit squares, so ID, Name and Grid are all the same
if not sig_ref.name:
sig_ref.name = sig_ref.id
if not sig_ref.grid:
sig_ref.grid = sig_ref.id
if sig_ref.grid and not sig_ref.latitude:
ll = locator_to_latlong(str(sig_ref.grid))
sig_ref.latitude = ll[0]
sig_ref.longitude = ll[1]
return sig_ref
elif sig.upper() == "WAB" or sig.upper() == "WAI":
ll = wab_wai_square_to_lat_lon(ref_id)
if ll:
sig_ref.name = ref_id
try:
sig_ref.grid = latlong_to_locator(ll[0], ll[1], 6)
sig_ref.latitude = ll[0]
sig_ref.longitude = ll[1]
except:
logging.warning("Invalid lat/lon received for WAB/WAI reference")
return sig_ref
elif sig.upper() == "BOTA":
# For BOTA all we can ever generate is the URL, there is no data file or lookup for lat/longs
if not sig_ref.name:
sig_ref.name = sig_ref.id
sig_ref.url = "https://www.beachesontheair.com/beaches/" + sig_ref.name.lower().replace(" ", "-")
return sig_ref
### ACTUAL LOOKUP ###
#
# OK, this is something we have to look up. Now check to see if our data store contains SIG ref information for
# this SIG. If so, check for the reference data and use that.
key = sig + ":" + ref_id
lookup_data = DATA_STORE.sigrefs.get(key) if key in DATA_STORE.sigrefs else None
if lookup_data:
# Copy new sig ref data into existing object where data was previously missing
for key, value in lookup_data.__dict__.items():
if value is not None and sig_ref.__dict__.get(key) is None:
sig_ref.__dict__[key] = value
else:
logging.warning("%s database did not contain data for ref %s", sig, ref_id)
except Exception:
logging.error("Exception when looking up sig_ref info for " + sig + " ref " + ref_id, exc_info=True)
return sig_ref
-90
View File
@@ -1,10 +1,4 @@
import logging
from pyhamtools.locator import latlong_to_locator, locator_to_latlong
from core.constants import SIGS
from core.data_store import DATA_STORE
from core.geo_utils import wab_wai_square_to_lat_lon
def get_ref_regex_for_sig(sig):
@@ -26,89 +20,5 @@ def get_sig_name_from_comment_name(sig):
return None
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. This makes
use of SIG ref data in the data store, live lookups from the web, or just automatic calculation depending on which
SIG we are getting data for.
Note there is currently no support for KRMNPA location lookup, see issue #61."""
if sig_ref.sig is None or sig_ref.sig == "" or sig_ref.id is None or sig_ref.id == "":
logging.debug("Failed to look up sig_ref info, sig or id were not set.")
return sig_ref
sig = sig_ref.sig
ref_id = sig_ref.id
try:
### FUDGES ###
#
# DME fudge. Our database has leading zeros padding to 5 digits which is the expected format, but not all
# activators add leading zeros.
if sig.upper() == "DME":
ref_id = ref_id.zfill(5)
# KRMNPA & SANPCPA fudge. These don't have their own reference system, they just use VKFF references, so pretend
# the sig is WWFF and carry on
if sig.upper() == "KRMNPA" or sig.upper() == "SANPCPA":
sig = "WWFF"
### SKIPS ###
#
# If the SIG is HEMA, we have no current lookup for this so just skip the lookup here.
if sig.upper() == "HEMA":
return sig_ref
### PROGRAMMATIC DATA GENERATION INSTEAD OF LOOKUPS ###
#
# If the SIG is Tiles, WAB, WAI or BOTA (Beaches), we don't have anything to look up from the data store, we can
# calculate all the information we are going to get directly.
if sig.upper() == "TILES":
# Tiles on the Air just uses Maidenhead 6-digit squares, so ID, Name and Grid are all the same
if not sig_ref.name:
sig_ref.name = sig_ref.id
if not sig_ref.grid:
sig_ref.grid = sig_ref.id
if sig_ref.grid and not sig_ref.latitude:
ll = locator_to_latlong(str(sig_ref.grid))
sig_ref.latitude = ll[0]
sig_ref.longitude = ll[1]
elif sig.upper() == "WAB" or sig.upper() == "WAI":
ll = wab_wai_square_to_lat_lon(ref_id)
if ll:
sig_ref.name = ref_id
try:
sig_ref.grid = latlong_to_locator(ll[0], ll[1], 6)
sig_ref.latitude = ll[0]
sig_ref.longitude = ll[1]
except:
logging.warning("Invalid lat/lon received for WAB/WAI reference")
elif sig.upper() == "BOTA":
# For BOTA all we can ever generate is the URL, there is no data file or lookup for lat/longs
if not sig_ref.name:
sig_ref.name = sig_ref.id
sig_ref.url = "https://www.beachesontheair.com/beaches/" + sig_ref.name.lower().replace(" ", "-")
### ACTUAL LOOKUP ###
#
# OK, this is something we have to look up. Now check to see if our data store contains SIG ref information for
# this SIG. If so, check for the reference data and use that.
key = sig + ":" + ref_id
lookup_data = DATA_STORE.sigrefs.get(key) if key in DATA_STORE.sigrefs else None
if lookup_data:
# Copy new sig ref data into existing object where data was previously missing
for key, value in lookup_data.__dict__.items():
if value is not None and sig_ref.__dict__.get(key) is None:
sig_ref.__dict__[key] = value
else:
logging.warning("%s database did not contain data for ref %s", sig, ref_id)
except Exception:
logging.error("Exception when looking up sig_ref info for " + sig + " ref " + ref_id, exc_info=True)
return sig_ref
# Regex matching any SIG's "comment name", i.e. how it may be referred to in spot comments
ANY_SIG_REGEX = r"(" + r"|".join(n for s in SIGS for n in s.comment_names) + r")"
+2 -2
View File
@@ -7,8 +7,8 @@ from datetime import datetime, timedelta
import pytz
from core.lookup_helper import lookup_helper
from core.sig_utils import populate_sig_ref_info
from core.call_lookup_helper import lookup_helper
from core.sig_lookup_helper import populate_sig_ref_info
@dataclass
+3 -2
View File
@@ -12,8 +12,9 @@ from pyhamtools.locator import locator_to_latlong, latlong_to_locator
from core.config import MAX_SPOT_AGE
from core.constants import MODE_ALIASES, PROPAGATION_MODES
from core.geo_utils import lat_lon_to_cq_zone, lat_lon_to_itu_zone
from core.lookup_helper import lookup_helper
from core.sig_utils import populate_sig_ref_info, ANY_SIG_REGEX, get_ref_regex_for_sig, get_sig_name_from_comment_name
from core.call_lookup_helper import lookup_helper
from core.sig_utils import ANY_SIG_REGEX, get_ref_regex_for_sig, get_sig_name_from_comment_name
from core.sig_lookup_helper import populate_sig_ref_info
from core.utils import infer_band_from_freq, infer_mode_from_comment, \
infer_mode_from_frequency, infer_mode_type_from_mode
from data.sig_ref import SIGRef
+2 -1
View File
@@ -11,7 +11,8 @@ from tornado.web import Application
from core.constants import SIGS
from core.geo_utils import lat_lon_for_grid_sw_corner_plus_size, lat_lon_to_cq_zone, lat_lon_to_itu_zone
from core.prometheus_metrics_handler import api_requests_counter
from core.sig_utils import get_ref_regex_for_sig, populate_sig_ref_info
from core.sig_utils import get_ref_regex_for_sig
from core.sig_lookup_helper import populate_sig_ref_info
from core.utils import safe_json_dumps
from data.lookup_credentials import extract_credentials
from data.sig_ref import SIGRef
+1 -1
View File
@@ -9,7 +9,7 @@ from core.config import config, SERVER_OWNER_CALLSIGN, LOG_LEVEL, get_sig_ref_da
get_static_data_provider_from_config
from core.constants import SOFTWARE_VERSION
from core.data_store import DATA_STORE
from core.lookup_helper import lookup_helper
from core.call_lookup_helper import lookup_helper
from core.status_reporter import StatusReporter
from server.webserver import WebServer
+1 -1
View File
@@ -43,7 +43,7 @@ function loadStatus() {
jsonData["static_data_providers"].forEach(p => {
$("#static-data-providers-status-container").append(`
<div class="row row-cols-1 row-cols-md-4 g-4 mb-2">
<div class="col"><strong>${p["sig_name"]}</strong></div>
<div class="col"><strong>${p["name"]}</strong></div>
<div class="col">Status: ${p["status"]}</div>
<div class="col">Last updated: ${(p["enabled"] && p["last_updated"] > 0) ? moment.unix(p["last_updated"]).utc().fromNow() : "N/A"}</div>
</div>`);
+3 -4
View File
@@ -23,14 +23,13 @@ class K0SWE(FileDownloadStaticDataProvider):
for dxcc in dxcc_list:
dxcc_map[dxcc["entityCode"]] = dxcc
# Precompile regex matches for DXCCs to improve efficiency when iterating through them
for dxcc in dxcc_map.values():
dxcc["_prefixRegexCompiled"] = re.compile(dxcc["prefixRegex"])
# Add to data store
for k, v in dxcc_map.items():
DATA_STORE.dxcc_data[k] = v
# Regenerate in-memory regex-to-DXCC-entity-code map.
DATA_STORE.regenerateCallRegexToDXCCEntityMap()
return True
except Exception as e: