Giant refactor to rebrand "SIG" as "Activity" anywhere that doesn't touch API or config file (which is to be addressed in a future breaking change). #147

This commit is contained in:
Ian Renton
2026-09-18 14:54:11 +01:00
parent 556ea56378
commit 81cd686a00
96 changed files with 1208 additions and 1170 deletions
@@ -0,0 +1,53 @@
import logging
from datetime import datetime
from threading import Event
import pytz
from core.data_store import DATA_STORE
logger = logging.getLogger(__name__)
class ActivityRefDataProvider:
"""Generic activity reference data provider class. Subclasses of this query the individual URLs or files for
data."""
def __init__(self, sig_name, provider_config):
"""Constructor. Note the parameter and attribute are still named "sig_name" for consistency with the API's
"sig" field name."""
self.sig_name = sig_name
self.enabled = provider_config["enabled"]
self.last_update_time = datetime.min.replace(tzinfo=pytz.UTC)
self.status = "Not Started" if self.enabled else "Disabled"
self.reference_count = 0
self._stop_event = Event()
def start(self):
"""Start the provider. This should return immediately after spawning threads to access the remote resources"""
raise NotImplementedError("Subclasses must implement this method")
def stop(self):
"""Stop any threads and prepare for application shutdown. Subclasses should implement this method and call
super()."""
self._stop_event.set()
def _add_data(self, new_data):
"""Add all the provided reference data objects to the data store."""
# with transact() batches all writes together to save making thousands of individual sqlite writes
with DATA_STORE.activity_refs.transact():
for d in new_data:
DATA_STORE.activity_refs.set(f"{self.sig_name}:{d.id}", d)
# For the big data sources, loading will take a few minutes. If we want to shut down the software neatly
# within the first few minutes of startup, we need a way to abort this expensive process of filling up the
# disk cache.
if self._stop_event.is_set():
break
self.reference_count = len(new_data)
logger.info(f"Loaded {self.reference_count} references for {self.sig_name} into the data store.")
+48
View File
@@ -0,0 +1,48 @@
import csv
from time import sleep
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.file_download_activity_ref_data_provider import (
FileDownloadActivityRefDataProvider,
)
class ARLHS(FileDownloadActivityRefDataProvider):
"""Activity ref data provider for Amateur Radio Light House Society"""
POLL_INTERVAL_DAYS = 30
ACTIVITY = "ARLHS"
DATA_URL = "https://www.gma.rocks/download/lighthouse.csv"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()[1:]):
if "ARLHS" in row and row["ARLHS"] != "":
ref_id = row["ARLHS"]
new_data.append(
ActivityRef(
sig=self.ACTIVITY,
id=ref_id,
name=row.get("Name", None),
ref_type=ActivityRefType.LIGHTHOUSE,
url=f"https://www.cqgma.org/zinfo.php?ref={ref_id}",
latitude=float(row["Latitude"]) if "Latitude" in row and row["Latitude"] != "" else None,
longitude=float(row["Longitude"]) if "Longitude" in row and row["Longitude"] != "" else None,
grid=row["Maidenhead Locator"],
)
)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
# the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
+52
View File
@@ -0,0 +1,52 @@
from time import sleep
from pyhamtools.locator import latlong_to_locator
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.file_download_activity_ref_data_provider import FileDownloadActivityRefDataProvider
class COTA(FileDownloadActivityRefDataProvider):
"""Activity ref data provider for Castles on the Air"""
POLL_INTERVAL_DAYS = 30
ACTIVITY = "COTA"
DATA_URL = "https://www.cotagroup.org/cotagroup/map/data/castles-all-7d90ee2a5e1175e5dece1bbf9dc87504.json"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
data = http_response.json()
if isinstance(data, list):
for ref in data[2]:
ref_id = ref[3]
name = ref[4]
lat = float(ref[0])
lon = float(ref[1])
grid = latlong_to_locator(lat, lon)
new_data.append(
ActivityRef(
sig=self.ACTIVITY,
id=ref_id,
name=name,
ref_type=ActivityRefType.CASTLE,
grid=grid,
latitude=lat,
longitude=lon,
)
)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest
# of the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
+42
View File
@@ -0,0 +1,42 @@
import io
from time import sleep
import pandas as pd
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.file_download_activity_ref_data_provider import FileDownloadActivityRefDataProvider
class DCE(FileDownloadActivityRefDataProvider):
"""Activity ref data provider for Diploma Castillos de España"""
POLL_INTERVAL_DAYS = 365
ACTIVITY = "DCE"
DATA_URL = "https://www.acracb.org/dce/descargas/General/directorio_referencias_dce.xls"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
file_stream = io.BytesIO(http_response.content)
df = pd.read_excel(file_stream, engine="xlrd", header=None)
for index, row in df.iterrows():
if row.iloc[0] and row.iloc[2]:
new_data.append(
ActivityRef(sig=self.ACTIVITY, id=row.iloc[0].strip(), name=row.iloc[2].strip(), ref_type=ActivityRefType.CASTLE)
)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
# the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
+46
View File
@@ -0,0 +1,46 @@
import io
from time import sleep
import pandas as pd
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.file_download_activity_ref_data_provider import FileDownloadActivityRefDataProvider
class DEFE(FileDownloadActivityRefDataProvider):
"""Activity ref data provider for Diploma Estationes de Ferrocarril de España"""
POLL_INTERVAL_DAYS = 365
ACTIVITY = "DEFE"
DATA_URL = "https://www.acracb.org/defe/descargas/General/directorio_referencias_defe.xls"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
file_stream = io.BytesIO(http_response.content)
df = pd.read_excel(file_stream, engine="xlrd", header=None)
for index, row in df.iterrows():
# Skip the header row
if str(row.iloc[0]) == "NºDEFE":
continue
if row.iloc[0] and row.iloc[1]:
new_data.append(
ActivityRef(sig=self.ACTIVITY, id=row.iloc[0].strip(), name=row.iloc[1].strip(), ref_type=ActivityRefType.BUILDING)
)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
# the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
+63
View File
@@ -0,0 +1,63 @@
import csv
from time import sleep
from pyhamtools.locator import latlong_to_locator
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.local_file_activity_ref_data_provider import (
LocalFileActivityRefDataProvider,
)
class DME(LocalFileActivityRefDataProvider):
"""Activity ref data provider for Diploma Municipios de Espana"""
ACTIVITY = "DME"
PATH = "datafiles/MUNICIPIOS.csv"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.PATH)
def _file_to_data(self, path):
new_data = []
with open(path, encoding="latin-1") as _f:
for row in csv.DictReader(_f, delimiter=";"):
# Store reference IDs with the "DME-" prefix rather than just the number. This will prevent Spothole
# from agressively thinking every number in a spot comment is DME after it's seen "DME" once. The only
# numbers that count are straight after "DME " or "DME-". The dash versus space is normalised in
# activity_lookup_helper.py.
ref_id = "DME-" + row["COD_INE"][:5]
latitude = (
float(row["LATITUD_ETRS89_REGCAN95"].replace(",", "."))
if row.get("LATITUD_ETRS89_REGCAN95")
else None
)
longitude = (
float(row["LONGITUD_ETRS89_REGCAN95"].replace(",", "."))
if row.get("LONGITUD_ETRS89_REGCAN95")
else None
)
ref = ActivityRef(
sig=self.ACTIVITY,
id=ref_id,
ref_type=ActivityRefType.TOWN,
name=f"{row['NOMBRE_ACTUAL']}, {row['PROVINCIA']}",
latitude=latitude,
longitude=longitude,
)
if latitude and longitude:
ref.grid = latlong_to_locator(latitude, longitude, 6)
new_data.append(ref)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest
# of the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
+37
View File
@@ -0,0 +1,37 @@
import csv
from time import sleep
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.file_download_activity_ref_data_provider import FileDownloadActivityRefDataProvider
class DMUE(FileDownloadActivityRefDataProvider):
"""Activity ref data provider for Diploma Museos de España"""
POLL_INTERVAL_DAYS = 365
ACTIVITY = "DMUE"
DATA_URL = "https://dmue.radiogalena.es/nom_dmue.csv"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
for row in csv.reader(http_response.content.decode("utf-8-sig").splitlines(), delimiter=";"):
if len(row) > 1 and row[0] and row[1]:
new_data.append(
ActivityRef(sig=self.ACTIVITY, id=row[0].strip(), name=row[1].strip(), ref_type=ActivityRefType.BUILDING)
)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
# the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
+50
View File
@@ -0,0 +1,50 @@
import io
from time import sleep
import pandas as pd
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.file_download_activity_ref_data_provider import FileDownloadActivityRefDataProvider
class DMVE(FileDownloadActivityRefDataProvider):
"""Activity ref data provider for Diploma Monumentos y Vestigios de España"""
POLL_INTERVAL_DAYS = 365
ACTIVITY = "DMVE"
DATA_URL = "https://www.acracb.org/dmve/descargas/General/directorio_referencias_dmve.xls"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
file_stream = io.BytesIO(http_response.content)
# Despide the .xls extension this is actually an xlsx file, so we need openpyxl not xlrd
df = pd.read_excel(file_stream, engine="openpyxl", header=None)
for index, row in df.iterrows():
ref = row.iloc[0]
name = row.iloc[1]
# Skip the header row and blank rows
if str(ref) == "REF.":
continue
if pd.isna(ref) or pd.isna(name):
continue
if ref and name:
new_data.append(ActivityRef(sig=self.ACTIVITY, id=ref.strip(), name=name.strip(), ref_type=ActivityRefType.BUILDING))
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
# the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
+35
View File
@@ -0,0 +1,35 @@
from time import sleep
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.file_download_activity_ref_data_provider import FileDownloadActivityRefDataProvider
class DTMBA(FileDownloadActivityRefDataProvider):
"""Activity ref data provider for Diploma Teatri Musei Belle Arti"""
POLL_INTERVAL_DAYS = 30
ACTIVITY = "DTMBA"
DATA_URL = "https://www.iu1fig.com/share/iz0eik/dtmba/export.php"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
for row in http_response.content.decode("utf-8-sig").splitlines():
split = row.split(";")
ref_id = split[0]
ref_name = split[1]
new_data.append(ActivityRef(sig=self.ACTIVITY, id=ref_id, name=ref_name, ref_type=ActivityRefType.BUILDING))
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
# the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
+56
View File
@@ -0,0 +1,56 @@
from io import BytesIO
from time import sleep
import pdfplumber
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.file_download_activity_ref_data_provider import FileDownloadActivityRefDataProvider
class FEA(FileDownloadActivityRefDataProvider):
"""Activity ref data provider for Diploma Faros de España"""
POLL_INTERVAL_DAYS = 30
ACTIVITY = "FEA"
DATA_URL = "http://ea5ol.net/Lista%20Faros.pdf"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
# Use PDFPlumber to extract the tables in the PDF
with pdfplumber.open(BytesIO(http_response.content)) as pdf:
all_rows = []
for page_number, page in enumerate(pdf.pages, start=1):
tables = page.extract_tables()
for table_number, table in enumerate(tables, start=1):
if not table:
continue
rows = [row for row in table if any(cell and cell.strip() for cell in row)]
all_rows.extend(rows)
for row in all_rows:
if not "REF" in row[0] and not "\n" in row[0]:
# FEA references are technically [DE]\-\d{4}(\.\d)? but spotters always seem to miss out the D- or E-
# prefix and just use FEA-1234 or FEA 1234, so we add both copies to the database.
ref_id_1 = row[0].strip()
ref_id_2 = ref_id_1.replace("D-", "FEA-").replace("E-", "FEA-")
new_data.append(ActivityRef(sig=self.ACTIVITY, id=ref_id_1, name=row[1].strip(), ref_type=ActivityRefType.LIGHTHOUSE))
new_data.append(ActivityRef(sig=self.ACTIVITY, id=ref_id_2, name=row[1].strip(), ref_type=ActivityRefType.LIGHTHOUSE))
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
# the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
@@ -0,0 +1,84 @@
import logging
from datetime import datetime
from threading import Thread
import pytz
from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout
from core.constants import HTTP_HEADERS
from core.url_data_cache import URLDataCache
from providers.activityrefdata.activity_ref_data_provider import ActivityRefDataProvider
logger = logging.getLogger(__name__)
class FileDownloadActivityRefDataProvider(ActivityRefDataProvider):
"""Generic activity ref data provider class for providers that fetch their data from the web by downloading a
file."""
def __init__(self, sig_name, provider_config, url, poll_interval):
"""Set up the provider, note poll_interval is in *days*."""
super().__init__(sig_name, provider_config)
self._url = url
self._poll_interval = poll_interval
self._thread = None
self._url_data_cache = URLDataCache(f"sigrefdata_{sig_name}") # cache dir name kept for continuity
def start(self):
# Fire off the polling thread. It will poll immediately on startup, then sleep for poll_interval between
# subsequent polls, so start() returns immediately and the application can continue starting.
logger.info(f"Set up query of {self.sig_name} activity ref data every {self._poll_interval!s} days.")
self._thread = Thread(target=self._run, name=f"FileDownloadActivityRefDataProvider-{self.sig_name}", daemon=True)
self._thread.start()
def stop(self):
super().stop()
if self._thread:
self._thread.join(timeout=35)
if self._thread.is_alive():
logger.warning(f"{self.sig_name} activity ref data worker thread did not exit on time and will be killed.")
def _run(self):
while True:
self._poll()
if self._stop_event.wait(timeout=self._poll_interval * 60 * 60 * 24):
break
def _poll(self):
try:
# Request data from API. Use the data cache (with a TTL of 1 day) here, not as the main mechanism for
# caching, but just so continual restarts of the software during testing don't hammer the servers.
logger.debug(f"Downloading {self.sig_name} activity ref data...")
http_response = self._url_data_cache.get(self._url, headers=HTTP_HEADERS)
# Check response code was good
if http_response.ok:
# Pass off to the subclass for processing
new_data = self._http_response_to_data(http_response)
# Add the new data to the activity ref data store
if new_data:
self._add_data(new_data)
self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC)
logger.debug(f"Received activity ref data for {self.sig_name}")
else:
self.status = "Error"
logger.warning(f"HTTP {http_response.status_code} when downloading activity ref data for {self.sig_name}.")
except ConnectionError:
self.status = "Error"
logger.warning(f"Connection error when downloading activity ref data for {self.sig_name}.")
except (ConnectTimeout, ReadTimeout):
self.status = "Error"
logger.warning(f"Timeout when downloading activity ref data for {self.sig_name}.")
except Exception:
self.status = "Error"
logger.exception(f"Exception in HTTP Activity Ref Data Provider ({self.sig_name})")
self._stop_event.wait(timeout=1)
def _http_response_to_data(self, http_response):
"""Convert an HTTP response returned by the server into activity ref data. The whole response is provided here
so the subclass implementations can check for HTTP status codes if necessary, and handle the response as
JSON, CSV, whatever the remote file actually is."""
raise NotImplementedError("Subclasses must implement this method")
+50
View File
@@ -0,0 +1,50 @@
import csv
from time import sleep
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.file_download_activity_ref_data_provider import (
FileDownloadActivityRefDataProvider,
)
class GMA(FileDownloadActivityRefDataProvider):
"""Activity ref data provider for Global Mountain Activity"""
POLL_INTERVAL_DAYS = 30
ACTIVITY = "GMA"
DATA_URL = "https://www.gma.rocks/download/summits.csv"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()[1:]):
ref_id = row["Reference"]
new_data.append(
ActivityRef(
sig=self.ACTIVITY,
id=ref_id,
name=row.get("Name", None),
ref_type=ActivityRefType.SUMMIT,
url=f"https://www.cqgma.org/zinfo.php?ref={ref_id}",
latitude=float(row["Latitude"]) if "Latitude" in row and row["Latitude"] != "" else None,
longitude=float(row["Longitude"]) if "Longitude" in row and row["Longitude"] != "" else None,
altitude=float(row["Height (m)"].replace("m", ""))
if "Height (m)" in row and row["Height (m)"] != ""
else None,
grid=row["Maidenhead Locator"],
)
)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
# the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
+48
View File
@@ -0,0 +1,48 @@
import csv
from time import sleep
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.file_download_activity_ref_data_provider import (
FileDownloadActivityRefDataProvider,
)
class ILLW(FileDownloadActivityRefDataProvider):
"""Activity ref data provider for International Lighthouse & Lightship Weekend"""
POLL_INTERVAL_DAYS = 30
ACTIVITY = "ILLW"
DATA_URL = "https://www.gma.rocks/download/lighthouse.csv"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()[1:]):
if "ILLW" in row and row["ILLW"] != "":
ref_id = row["ILLW"]
new_data.append(
ActivityRef(
sig=self.ACTIVITY,
id=ref_id,
name=row.get("Name", None),
ref_type=ActivityRefType.LIGHTHOUSE,
url=f"https://www.cqgma.org/zinfo.php?ref={ref_id}",
latitude=float(row["Latitude"]) if "Latitude" in row and row["Latitude"] != "" else None,
longitude=float(row["Longitude"]) if "Longitude" in row and row["Longitude"] != "" else None,
grid=row["Maidenhead Locator"],
)
)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
# the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
+64
View File
@@ -0,0 +1,64 @@
import logging
from time import sleep
from pyhamtools.locator import latlong_to_locator
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.file_download_activity_ref_data_provider import (
FileDownloadActivityRefDataProvider,
)
logger = logging.getLogger(__name__)
class IOTA(FileDownloadActivityRefDataProvider):
"""Activity ref data provider for Islands on the Air"""
POLL_INTERVAL_DAYS = 365
ACTIVITY = "IOTA"
DATA_URL = "https://www.iota-world.org/islands-on-the-air/downloads/download-file.html?path=groups.json"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
data = http_response.json()
if isinstance(data, list):
for ref in data:
ref_id = ref["refno"]
latitude = (float(ref["latitude_min"]) + float(ref["latitude_max"])) / 2.0
longitude = (float(ref["longitude_min"]) + float(ref["longitude_max"])) / 2.0
grid = None
try:
grid = latlong_to_locator(latitude, longitude, 6)
except ValueError:
logger.debug(
"Error converting lat/lon to locator for an IOTA reference %f %f",
latitude,
longitude,
)
new_data.append(
ActivityRef(
sig=self.ACTIVITY,
id=ref_id,
name=ref["name"],
ref_type=ActivityRefType.ISLAND,
grid=grid,
latitude=latitude,
longitude=longitude,
)
)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest
# of the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
+14
View File
@@ -0,0 +1,14 @@
from providers.activityrefdata.pnp_kml_activity_ref_data_provider import (
ParksNPeaksKMLActivityRefDataProvider,
)
class KRMNPA(ParksNPeaksKMLActivityRefDataProvider):
"""Activity ref data provider for the Keith Roget Memorrial National Parks Award (KRMNPA)."""
POLL_INTERVAL_DAYS = 365
ACTIVITY = "KRMNPA"
DATA_URL = "https://parksnpeaks.org/getPOI.php?poiClass=KRMNPA&poiFormat=4"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
+53
View File
@@ -0,0 +1,53 @@
from time import sleep
from pyhamtools.locator import locator_to_latlong
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.file_download_activity_ref_data_provider import (
FileDownloadActivityRefDataProvider,
)
class LLOTA(FileDownloadActivityRefDataProvider):
"""Activity ref data provider for Lagos y Lagunas on the Air"""
POLL_INTERVAL_DAYS = 7
ACTIVITY = "LLOTA"
DATA_URL = "https://llota.app/api/public/references"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
data = http_response.json()
if isinstance(data, list):
for ref in data:
ref_id = ref["reference_code"]
grid = str(ref["grid_locator"])
ll = locator_to_latlong(grid)
new_data.append(
ActivityRef(
sig=self.ACTIVITY,
id=ref_id,
name=str(ref["name"]),
ref_type=ActivityRefType.LAKE,
url=f"https://llota.app/list/ref/{ref_id}",
grid=grid,
latitude=ll[0],
longitude=ll[1],
)
)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest
# of the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
@@ -0,0 +1,36 @@
import logging
from datetime import datetime
import pytz
from providers.activityrefdata.activity_ref_data_provider import ActivityRefDataProvider
logger = logging.getLogger(__name__)
class LocalFileActivityRefDataProvider(ActivityRefDataProvider):
"""Generic activity ref data provider class for providers that fetch their data from a local file on startup."""
def __init__(self, sig_name, provider_config, path):
super().__init__(sig_name, provider_config)
self._path = path
def start(self):
logger.debug(f"Loading {self.sig_name} activity ref data from file.")
try:
new_data = self._file_to_data(self._path)
if new_data:
self._add_data(new_data)
self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC)
else:
self.status = "Error"
logger.info(f"Failed to load activity ref data for {self.sig_name}")
except Exception:
self.status = "Error"
logger.exception(f"Exception in local file Activity Ref Data Provider ({self.sig_name})")
def _file_to_data(self, path):
"""Load a file on the given path and turn it into activity ref data."""
raise NotImplementedError("Subclasses must implement this method")
+47
View File
@@ -0,0 +1,47 @@
import csv
from time import sleep
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.file_download_activity_ref_data_provider import (
FileDownloadActivityRefDataProvider,
)
class MOTA(FileDownloadActivityRefDataProvider):
"""Activity ref data provider for Mills on the Air"""
POLL_INTERVAL_DAYS = 30
ACTIVITY = "MOTA"
DATA_URL = "https://www.gma.rocks/download/mills.csv"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()[1:]):
ref_id = row["Reference"]
new_data.append(
ActivityRef(
sig=self.ACTIVITY,
id=ref_id,
name=row.get("Name", None),
ref_type=ActivityRefType.MILL,
url=f"https://www.cqgma.org/zinfo.php?ref={ref_id}",
latitude=float(row["Latitude"]) if "Latitude" in row and row["Latitude"] != "" else None,
longitude=float(row["Longitude"]) if "Longitude" in row and row["Longitude"] != "" else None,
grid=row["Maidenhead Locator"],
)
)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
# the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
+58
View File
@@ -0,0 +1,58 @@
from time import sleep
from bs4 import BeautifulSoup
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.file_download_activity_ref_data_provider import FileDownloadActivityRefDataProvider
class PGA(FileDownloadActivityRefDataProvider):
"""Activity ref data provider for Polish Gmina Award"""
POLL_INTERVAL_DAYS = 30
ACTIVITY = "PGA"
DATA_URL = "http://www.spga.pl/lista_pga2.php"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
soup = BeautifulSoup(http_response.text, "html.parser")
# Iterate through tables in the page
for table in soup.find_all("table"):
header_cells = table.find_all(["th", "td"], limit=10)
header_texts = [c.get_text(strip=True) for c in header_cells]
# If it has "PGA" and "Nazwa" in the header, it's the main data table
if any("PGA" in t for t in header_texts) and any("Nazwa" in t for t in header_texts):
# Iterate through all rows except the first
rows = table.find_all("tr")
for row in rows[1:]:
cells = row.find_all("td")
ref_id = cells[0].get_text(strip=True)
name = cells[1].get_text(strip=True)
if not ref_id:
continue
new_data.append(
ActivityRef(
sig=self.ACTIVITY,
id=ref_id,
name=name,
ref_type=ActivityRefType.REGION,
)
)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest
# of the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
@@ -0,0 +1,65 @@
import re
from time import sleep
from fastkml import kml
from pyhamtools.locator import latlong_to_locator
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.file_download_activity_ref_data_provider import (
FileDownloadActivityRefDataProvider,
)
class ParksNPeaksKMLActivityRefDataProvider(FileDownloadActivityRefDataProvider):
"""Base class for activity ref data providers that use parksnpeaks.org KML POI feeds and have references that
use the VKFF refs rather than their own system (i.e. KRMNPA and SANPCPA)."""
REF_PATTERN = re.compile(r"VKFF-\d+")
def __init__(self, sig_name, provider_config, url, poll_interval):
"""Set up the provider, note poll_interval is in *days*."""
super().__init__(sig_name, provider_config, url, poll_interval)
def _http_response_to_data(self, http_response):
new_data = []
k = kml.KML.from_string(http_response.content)
for document in k.features:
# noinspection unresolved-references
for folder in document.features:
# noinspection unresolved-references
for placemark in folder.features:
description = placemark.description or ""
match = self.REF_PATTERN.search(description)
if not match:
# No VKFF reference found in this placemark - skip it (e.g. non-park waypoints)
continue
ref_id = match.group(0)
longitude, latitude = placemark.geometry.x, placemark.geometry.y
ref = ActivityRef(
sig=self.sig_name,
id=ref_id,
name=placemark.name,
ref_type=ActivityRefType.PARK,
url=f"https://parksnpeaks.org/getPark.php?actPark={ref_id}",
latitude=latitude,
longitude=longitude,
)
if latitude and longitude:
ref.grid = latlong_to_locator(latitude, longitude, 6)
new_data.append(ref)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to
# parse the rest of the data in this case
if self._stop_event.is_set():
return new_data
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
+47
View File
@@ -0,0 +1,47 @@
import csv
from time import sleep
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.file_download_activity_ref_data_provider import (
FileDownloadActivityRefDataProvider,
)
class POTA(FileDownloadActivityRefDataProvider):
"""Activity ref data provider for Parks on the Air"""
POLL_INTERVAL_DAYS = 7
ACTIVITY = "POTA"
DATA_URL = "https://pota.app/all_parks_ext.csv"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()):
ref_id = row["reference"]
new_data.append(
ActivityRef(
sig=self.ACTIVITY,
id=ref_id,
name=row.get("name", None),
ref_type=ActivityRefType.PARK,
url=f"https://pota.app/#/park/{ref_id}",
grid=row.get("grid", None),
latitude=float(row["latitude"]) if "latitude" in row and row["latitude"] != "" else None,
longitude=float(row["longitude"]) if "longitude" in row and row["longitude"] != "" else None,
)
)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
# the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
+14
View File
@@ -0,0 +1,14 @@
from providers.activityrefdata.pnp_kml_activity_ref_data_provider import (
ParksNPeaksKMLActivityRefDataProvider,
)
class SANPCPA(ParksNPeaksKMLActivityRefDataProvider):
"""Activity ref data provider for the South Australia National Parks and Conservation Parks Award (SANPCPA)."""
POLL_INTERVAL_DAYS = 365
ACTIVITY = "SANPCPA"
DATA_URL = "https://parksnpeaks.org/getPOI.php?poiClass=SANPCPA&poiFormat=4"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
+46
View File
@@ -0,0 +1,46 @@
import csv
from time import sleep
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.file_download_activity_ref_data_provider import (
FileDownloadActivityRefDataProvider,
)
class SIOTA(FileDownloadActivityRefDataProvider):
"""Activity ref data provider for Silos on the Air"""
POLL_INTERVAL_DAYS = 30
ACTIVITY = "SIOTA"
DATA_URL = "https://www.silosontheair.com/data/silos.csv"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()):
ref_id = row["SILO_CODE"]
new_data.append(
ActivityRef(
sig=self.ACTIVITY,
id=ref_id,
name=row.get("NAME", None),
ref_type=ActivityRefType.SILO,
grid=row.get("LOCATOR", None),
latitude=float(row["LAT"]) if "LAT" in row else None,
longitude=float(row["LNG"]) if "LNG" in row else None,
)
)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
# the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
+54
View File
@@ -0,0 +1,54 @@
import csv
from time import sleep
from pyhamtools.locator import latlong_to_locator
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.file_download_activity_ref_data_provider import (
FileDownloadActivityRefDataProvider,
)
class SOTA(FileDownloadActivityRefDataProvider):
"""Activity ref data provider for Summits on the Air"""
POLL_INTERVAL_DAYS = 30
ACTIVITY = "SOTA"
DATA_URL = "https://storage.sota.org.uk/summitslist.csv"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()[1:]):
ref_id = row["SummitCode"]
latitude = float(row["Latitude"]) if "Latitude" in row and row["Latitude"] != "" else None
longitude = float(row["Longitude"]) if "Longitude" in row and row["Longitude"] != "" else None
altitude = float(row["AltM"]) if "AltM" in row and row["AltM"] != "" else None
ref = ActivityRef(
sig=self.ACTIVITY,
id=ref_id,
name=row.get("SummitName", None),
ref_type=ActivityRefType.SUMMIT,
url=f"https://www.sotadata.org.uk/en/summit/{ref_id}",
latitude=latitude,
longitude=longitude,
altitude=altitude,
activation_score=int(row["Points"]) if "Points" in row else None,
)
if latitude and longitude:
ref.grid = latlong_to_locator(latitude, longitude, 6)
new_data.append(ref)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
# the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
+41
View File
@@ -0,0 +1,41 @@
import csv
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.local_file_activity_ref_data_provider import (
LocalFileActivityRefDataProvider,
)
class Toilets(LocalFileActivityRefDataProvider):
"""Activity ref data provider for Toilets on the Air"""
ACTIVITY = "Toilets"
PATH = "datafiles/toilets.csv"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.PATH)
def _file_to_data(self, path):
new_data = []
with open(path) as _f:
csv_data = _f.read()
dr = csv.DictReader(csv_data.splitlines())
for row in dr:
new_data.append(
ActivityRef(
sig=self.ACTIVITY,
id=row["ref"],
name=row["ref"],
ref_type=ActivityRefType.TOILET,
latitude=float(row["lat"]),
longitude=float(row["lon"]),
)
)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest
# of the data in this case
if self._stop_event.is_set():
break
return new_data
+47
View File
@@ -0,0 +1,47 @@
import csv
from time import sleep
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.file_download_activity_ref_data_provider import (
FileDownloadActivityRefDataProvider,
)
class Towers(FileDownloadActivityRefDataProvider):
"""Activity ref data provider for Towers on the Air"""
POLL_INTERVAL_DAYS = 30
ACTIVITY = "Towers"
DATA_URL = "https://wwtota.com/servis/generate_csv.php?ref=&filter=all"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines(), delimiter=";"):
ref_id = row["Ref"]
new_data.append(
ActivityRef(
sig=self.ACTIVITY,
id=ref_id,
name=row.get("Nazev", None),
ref_type=ActivityRefType.TOWER,
url=f"https://wwtota.com/seznam/karta_rozhledny.php?ref={ref_id}",
grid=row["Lokator"] if "Lokator" in row and row["Lokator"] != "" else None,
latitude=float(row["Lat"]) if "Lat" in row and row["Lat"] != "" else None,
longitude=float(row["Lon"]) if "Lon" in row and row["Lon"] != "" else None,
)
)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
# the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
+66
View File
@@ -0,0 +1,66 @@
import csv
import logging
from time import sleep
from pyhamtools.locator import latlong_to_locator
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.file_download_activity_ref_data_provider import (
FileDownloadActivityRefDataProvider,
)
logger = logging.getLogger(__name__)
class WCA(FileDownloadActivityRefDataProvider):
"""Activity ref data provider for World Castles Award"""
POLL_INTERVAL_DAYS = 30
ACTIVITY = "WCA"
DATA_URL = "https://polo.ham2k.com/data/activities/wca/all-castles.csv"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()):
ref_id = row["REF"]
coords_str = row["COORDINATES"]
latitude = None
longitude = None
grid = None
try:
if coords_str:
split = coords_str.split(", ")
latitude = float(split[0])
longitude = float(split[1])
grid = latlong_to_locator(latitude, longitude)
except ValueError:
logger.debug(f"Encountered dodgy formatting in WCA CSV, skipping location data for {ref_id}")
new_data.append(
ActivityRef(
sig=self.ACTIVITY,
id=ref_id,
name=row.get("CLEAN NAME", None),
ref_type=ActivityRefType.CASTLE,
url=f"https://www.cqgma.org/zinfo.php?ref={ref_id}",
latitude=latitude,
longitude=longitude,
grid=grid,
)
)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
# the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
+54
View File
@@ -0,0 +1,54 @@
from time import sleep
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.file_download_activity_ref_data_provider import (
FileDownloadActivityRefDataProvider,
)
class WOTA(FileDownloadActivityRefDataProvider):
"""Activity ref data provider for Wainwrights on the Air"""
POLL_INTERVAL_DAYS = 365
ACTIVITY = "WOTA"
DATA_URL = "https://www.wota.org.uk/mapping/data/summits.json"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
for feature in http_response.json().get("features", []):
ref_id = feature["properties"]["wotaId"]
# Fudge WOTA URLs. Outlying fell (LDO) URLs don't match their ID numbers but require 214 to be
# added to them
url = f"https://www.wota.org.uk/MM_{ref_id}"
if ref_id.upper().startswith("LDO-"):
number = int(ref_id.upper().replace("LDO-", ""))
url = f"https://www.wota.org.uk/MM_LDO-{number + 214!s}"
new_data.append(
ActivityRef(
sig=self.ACTIVITY,
id=ref_id,
name=feature["properties"]["title"],
url=url,
ref_type=ActivityRefType.SUMMIT,
grid=feature["properties"]["qthLocator"],
latitude=feature["geometry"]["coordinates"][1],
longitude=feature["geometry"]["coordinates"][0],
altitude=feature["properties"]["height"],
)
)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
# the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
+47
View File
@@ -0,0 +1,47 @@
import csv
from time import sleep
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.file_download_activity_ref_data_provider import (
FileDownloadActivityRefDataProvider,
)
class WWBOTA(FileDownloadActivityRefDataProvider):
"""Activity ref data provider for Worldwide Bunkers on the Air"""
POLL_INTERVAL_DAYS = 30
ACTIVITY = "WWBOTA"
DATA_URL = "https://api.wwbota.org/bunkers/?format=CSV"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()):
ref_id = row["Reference"]
new_data.append(
ActivityRef(
sig=self.ACTIVITY,
id=ref_id,
name=row.get("Name", None),
ref_type=ActivityRefType.BUNKER,
url=f"https://bunkerwiki.org/?s={ref_id}" if ref_id.startswith("B/G") else None,
grid=row["Locator"] if "Locator" in row and row["Locator"] != "" else None,
latitude=float(row["Lat"]) if "Lat" in row and row["Lat"] != "" else None,
longitude=float(row["Long"]) if "Long" in row and row["Long"] != "" else None,
)
)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
# the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
+51
View File
@@ -0,0 +1,51 @@
import csv
from time import sleep
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.file_download_activity_ref_data_provider import (
FileDownloadActivityRefDataProvider,
)
class WWFF(FileDownloadActivityRefDataProvider):
"""Activity ref data provider for Worldwide Flora & Fauna"""
POLL_INTERVAL_DAYS = 30
ACTIVITY = "WWFF"
DATA_URL = "https://wwff.co/wwff-data/wwff_directory.csv"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()):
ref_id = row["reference"]
new_data.append(
ActivityRef(
sig=self.ACTIVITY,
id=ref_id,
name=row.get("name", None),
ref_type=ActivityRefType.PARK,
url=f"https://wwff.co/directory/?showRef={ref_id}",
grid=row["iaruLocator"] if "iaruLocator" in row and row["iaruLocator"] != "-" else None,
latitude=float(row["latitude"])
if "latitude" in row and row["latitude"] != "" and row["latitude"] != "-"
else None,
longitude=float(row["longitude"])
if "longitude" in row and row["longitude"] != "" and row["longitude"] != "-"
else None,
)
)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
# the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data
+65
View File
@@ -0,0 +1,65 @@
from time import sleep
from pyhamtools.locator import latlong_to_locator
from core.enums import ActivityRefType
from data.activity_ref import ActivityRef
from providers.activityrefdata.file_download_activity_ref_data_provider import (
FileDownloadActivityRefDataProvider,
)
class ZLOTA(FileDownloadActivityRefDataProvider):
"""Activity ref data provider for New Zealand on the Air"""
POLL_INTERVAL_DAYS = 30
ACTIVITY = "ZLOTA"
DATA_URL = "https://ontheair.nz/assets/assets.json"
def __init__(self, provider_config):
super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
def _http_response_to_data(self, http_response):
new_data = []
data = http_response.json()
if isinstance(data, list):
for ref in data:
ref_id = ref["code"]
latitude = ref["latitude"]
longitude = ref["longitude"]
try:
ref_type = ActivityRefType(ref["asset_type"].title().upper())
except ValueError:
ref_type = None
new_ref = ActivityRef(
sig=self.ACTIVITY,
id=ref_id,
name=ref["name"],
ref_type=ref_type,
url=f"https://ontheair.nz/assets/{ref_id.replace('/', '_')}",
latitude=latitude,
longitude=longitude,
)
# Check lat/lon validity and update grid accordingly
if latitude and longitude:
try:
new_ref.grid = latlong_to_locator(latitude, longitude, 6)
except ValueError:
# Junk lat/lon, remove from the spot
new_ref.latitude = None
new_ref.longitude = None
new_data.append(new_ref)
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest
# of the data in this case
if self._stop_event.is_set():
break
# Very short pause. This will extend the time to handle activity refs by a few seconds but will ensure some time
# is available for other threads e.g. the web server.
sleep(0.001)
return new_data