mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-09-20 14:27:42 +00:00
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:
@@ -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.")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user