Refactor of caching & data storage part 4 #118

This commit is contained in:
Ian Renton
2026-07-31 21:02:37 +01:00
parent 0f59af6f9e
commit 23edd4e02e
37 changed files with 822 additions and 528 deletions
+61 -56
View File
@@ -1,6 +1,7 @@
import copy
import hashlib
import json
import logging
from dataclasses import dataclass
from datetime import datetime, timedelta
@@ -64,69 +65,73 @@ class Alert:
def infer_missing(self, credentials=None):
"""Infer missing parameters where possible"""
# If we somehow don't have a start time, set it to zero so it sorts off the bottom of any list but
# clients can still reliably parse it as a number.
if not self.start_time:
self.start_time = 0
try:
# If we somehow don't have a start time, set it to zero so it sorts off the bottom of any list but
# clients can still reliably parse it as a number.
if not self.start_time:
self.start_time = 0
# If we don't have a received time, this has just been received so set that to "now"
if not self.received_time:
self.received_time = datetime.now(pytz.UTC).timestamp()
# If we don't have a received time, this has just been received so set that to "now"
if not self.received_time:
self.received_time = datetime.now(pytz.UTC).timestamp()
# Fill in ISO versions of times, in case the client prefers that
if self.start_time and not self.start_time_iso:
self.start_time_iso = datetime.fromtimestamp(self.start_time, pytz.UTC).isoformat()
if self.end_time and not self.end_time_iso:
self.end_time_iso = datetime.fromtimestamp(self.end_time, pytz.UTC).isoformat()
if self.received_time and not self.received_time_iso:
self.received_time_iso = datetime.fromtimestamp(self.received_time, pytz.UTC).isoformat()
# Fill in ISO versions of times, in case the client prefers that
if self.start_time and not self.start_time_iso:
self.start_time_iso = datetime.fromtimestamp(self.start_time, pytz.UTC).isoformat()
if self.end_time and not self.end_time_iso:
self.end_time_iso = datetime.fromtimestamp(self.end_time, pytz.UTC).isoformat()
if self.received_time and not self.received_time_iso:
self.received_time_iso = datetime.fromtimestamp(self.received_time, pytz.UTC).isoformat()
# DX country, continent, zones etc. from callsign. CQ/ITU zone are better looked up with a location but we don't
# have a real location for alerts.
if self.dx_calls and self.dx_calls[0] and not self.dx_country:
self.dx_country = lookup_helper.infer_country_from_callsign(self.dx_calls[0], credentials)
if self.dx_calls and self.dx_calls[0] and not self.dx_continent:
self.dx_continent = lookup_helper.infer_continent_from_callsign(self.dx_calls[0], credentials)
if self.dx_calls and self.dx_calls[0] and not self.dx_cq_zone:
self.dx_cq_zone = lookup_helper.infer_cq_zone_from_callsign(self.dx_calls[0], credentials)
if self.dx_calls and self.dx_calls[0] and not self.dx_itu_zone:
self.dx_itu_zone = lookup_helper.infer_itu_zone_from_callsign(self.dx_calls[0], credentials)
if self.dx_calls and self.dx_calls[0] and not self.dx_dxcc_id:
self.dx_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.dx_calls[0], credentials)
if self.dx_dxcc_id and not self.dx_flag:
self.dx_flag = lookup_helper.get_flag_for_dxcc(self.dx_dxcc_id)
# DX country, continent, zones etc. from callsign. CQ/ITU zone are better looked up with a location but we don't
# have a real location for alerts.
if self.dx_calls and self.dx_calls[0] and not self.dx_country:
self.dx_country = lookup_helper.infer_country_from_callsign(self.dx_calls[0], credentials)
if self.dx_calls and self.dx_calls[0] and not self.dx_continent:
self.dx_continent = lookup_helper.infer_continent_from_callsign(self.dx_calls[0], credentials)
if self.dx_calls and self.dx_calls[0] and not self.dx_cq_zone:
self.dx_cq_zone = lookup_helper.infer_cq_zone_from_callsign(self.dx_calls[0], credentials)
if self.dx_calls and self.dx_calls[0] and not self.dx_itu_zone:
self.dx_itu_zone = lookup_helper.infer_itu_zone_from_callsign(self.dx_calls[0], credentials)
if self.dx_calls and self.dx_calls[0] and not self.dx_dxcc_id:
self.dx_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.dx_calls[0], credentials)
if self.dx_dxcc_id and not self.dx_flag:
self.dx_flag = lookup_helper.get_flag_for_dxcc(self.dx_dxcc_id)
# Fetch SIG data. In case a particular API doesn't provide a full set of name, lat, lon & grid for a reference
# in its initial call, we use this code to populate the rest of the data. This includes working out grid refs
# from WAB and WAI, which count as a SIG even though there's no real lookup, just maths
if self.sig_refs and len(self.sig_refs) > 0:
for sig_ref in self.sig_refs:
populate_sig_ref_info(sig_ref)
# Fetch SIG data. In case a particular API doesn't provide a full set of name, lat, lon & grid for a reference
# in its initial call, we use this code to populate the rest of the data. This includes working out grid refs
# from WAB and WAI, which count as a SIG even though there's no real lookup, just maths
if self.sig_refs and len(self.sig_refs) > 0:
for sig_ref in self.sig_refs:
populate_sig_ref_info(sig_ref)
# If the spot itself doesn't have a SIG yet, but we have at least one SIG reference, take that reference's SIG
# and apply it to the whole spot.
if self.sig_refs and len(self.sig_refs) > 0 and self.sig_refs[0] and not self.sig:
self.sig = self.sig_refs[0].sig
# If the spot itself doesn't have a SIG yet, but we have at least one SIG reference, take that reference's SIG
# and apply it to the whole spot.
if self.sig_refs and len(self.sig_refs) > 0 and self.sig_refs[0] and not self.sig:
self.sig = self.sig_refs[0].sig
# Always create an ID based on a hash of every parameter *except* received_time. This is used as the index
# to a map, which as a byproduct avoids us having multiple duplicate copies of the object that are identical
# apart from that they were retrieved from the API at different times. Note that the simple Python hash()
# function includes a seed randomly generated at runtime; this is therefore not consistent between runs. But we
# use diskcache to store our data between runs, so we use SHA256 which does not include this random element.
# The ID is computed before the online lookups below so that it is stable regardless of whether credentials
# are provided, allowing the enriched API response to be matched to the stored alert by ID.
if not self.id:
self_copy = copy.deepcopy(self)
self_copy.received_time = 0
self_copy.received_time_iso = ""
self.id = hashlib.sha256(str(self_copy).encode("utf-8")).hexdigest()
# Always create an ID based on a hash of every parameter *except* received_time. This is used as the index
# to a map, which as a byproduct avoids us having multiple duplicate copies of the object that are identical
# apart from that they were retrieved from the API at different times. Note that the simple Python hash()
# function includes a seed randomly generated at runtime; this is therefore not consistent between runs. But we
# use diskcache to store our data between runs, so we use SHA256 which does not include this random element.
# The ID is computed before the online lookups below so that it is stable regardless of whether credentials
# are provided, allowing the enriched API response to be matched to the stored alert by ID.
if not self.id:
self_copy = copy.deepcopy(self)
self_copy.received_time = 0
self_copy.received_time_iso = ""
self.id = hashlib.sha256(str(self_copy).encode("utf-8")).hexdigest()
# DX operator details lookup, using QRZ.com/HamQTH. This should be the last resort compared to taking the data
# from the actual alerting service, e.g. we don't want to accidentally use a user's QRZ.com home lat/lon
# instead of the one from the park reference they're at.
if self.dx_calls and not self.dx_names:
self.dx_names = list(
map(lambda c: lookup_helper.infer_name_from_callsign_online_lookup(c, credentials), self.dx_calls))
# DX operator details lookup, using QRZ.com/HamQTH. This should be the last resort compared to taking the data
# from the actual alerting service, e.g. we don't want to accidentally use a user's QRZ.com home lat/lon
# instead of the one from the park reference they're at.
if self.dx_calls and not self.dx_names:
self.dx_names = list(
map(lambda c: lookup_helper.infer_name_from_callsign_online_lookup(c, credentials), self.dx_calls))
except Exception as e:
logging.error("Exception while inferring missing data from spot", e, exc_info=True)
def to_json(self):
"""JSON serialise"""