mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-08-06 02:21:42 +00:00
Refactor of caching & data storage part 3 #118
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import csv
|
||||
|
||||
from pyhamtools.locator import latlong_to_locator
|
||||
|
||||
from data.sig_ref import SIGRef
|
||||
from sigrefdataproviders.local_file_sig_ref_data_provider import LocalFileSIGRefDataProvider
|
||||
|
||||
|
||||
class DME(LocalFileSIGRefDataProvider):
|
||||
"""SIG ref data provider for Diploma Municipios de Espana"""
|
||||
|
||||
SIG = "DME"
|
||||
PATH = "datafiles/MUNICIPIOS.csv"
|
||||
|
||||
def __init__(self, provider_config):
|
||||
super().__init__(self.SIG, 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=";"):
|
||||
ref_id = 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
|
||||
|
||||
new_data[ref_id] = SIGRef(sig=self.SIG, id=ref_id,
|
||||
name=row["NOMBRE_ACTUAL"] + ", " + row["PROVINCIA"],
|
||||
latitude=latitude,
|
||||
longitude=longitude)
|
||||
if latitude and longitude:
|
||||
new_data[ref_id].grid = latlong_to_locator(latitude, longitude, 6)
|
||||
return new_data
|
||||
@@ -0,0 +1,75 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from threading import Thread, Event
|
||||
|
||||
import pytz
|
||||
import requests
|
||||
from requests import ReadTimeout
|
||||
from requests.exceptions import ConnectionError, ConnectTimeout
|
||||
|
||||
from core.constants import HTTP_HEADERS
|
||||
from sigrefdataproviders.sig_ref_data_provider import SIGRefDataProvider
|
||||
|
||||
|
||||
class FileDownloadSIGRefDataProvider(SIGRefDataProvider):
|
||||
"""Generic SIG 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):
|
||||
super().__init__(sig_name, provider_config)
|
||||
self._url = url
|
||||
self._poll_interval = poll_interval
|
||||
self._thread = None
|
||||
self._stop_event = Event()
|
||||
|
||||
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.
|
||||
logging.info(
|
||||
"Set up query of " + self.sig_name + " SIG ref data every " + str(self._poll_interval) + " seconds.")
|
||||
self._thread = Thread(target=self._run, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def stop(self):
|
||||
self._stop_event.set()
|
||||
|
||||
def _run(self):
|
||||
while True:
|
||||
self._poll()
|
||||
if self._stop_event.wait(timeout=self._poll_interval):
|
||||
break
|
||||
|
||||
def _poll(self):
|
||||
try:
|
||||
# Request data from API
|
||||
logging.debug("Downloading " + self.sig_name + " SIG ref data...")
|
||||
http_response = requests.get(self._url, headers=HTTP_HEADERS, timeout=(5, 30))
|
||||
# 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)
|
||||
# Submit the new spots for processing. There might not be any spots for the less popular programs.
|
||||
if new_data:
|
||||
self._replace_data(new_data)
|
||||
|
||||
self.status = "OK"
|
||||
self.last_update_time = datetime.now(pytz.UTC)
|
||||
logging.debug("Received SIG ref data for " + self.sig_name)
|
||||
else:
|
||||
self.status = "Error"
|
||||
logging.warning(f"HTTP {http_response.status_code} when downloading SIG ref data for {self.sig_name}.")
|
||||
|
||||
except ConnectionError:
|
||||
logging.warning(f"Connection error when downloading SIG ref data for {self.sig_name}.")
|
||||
except (ConnectTimeout, ReadTimeout):
|
||||
logging.warning(f"Timeout when downloading SIG ref data for {self.sig_name}.")
|
||||
except Exception:
|
||||
self.status = "Error"
|
||||
logging.exception("Exception in HTTP SIG 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 SIG 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,32 @@
|
||||
from pyhamtools.locator import locator_to_latlong
|
||||
|
||||
from data.sig_ref import SIGRef
|
||||
from sigrefdataproviders.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
||||
|
||||
|
||||
class LLOTA(FileDownloadSIGRefDataProvider):
|
||||
"""SIG ref data provider for Lagos y Lagunas on the Air"""
|
||||
|
||||
POLL_INTERVAL_SEC = 7 * 24 * 60 * 60 # 7 days
|
||||
SIG = "LLOTA"
|
||||
DATA_URL = "https://llota.app/api/public/references"
|
||||
|
||||
def __init__(self, provider_config):
|
||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_SEC)
|
||||
|
||||
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[ref_id] = SIGRef(sig=self.SIG, id=ref_id, name=str(ref["name"]),
|
||||
url="https://llota.app/list/ref/" + ref_id,
|
||||
grid=grid,
|
||||
latitude=ll[0],
|
||||
longitude=ll[1])
|
||||
|
||||
return new_data
|
||||
@@ -0,0 +1,36 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import pytz
|
||||
|
||||
from sigrefdataproviders.sig_ref_data_provider import SIGRefDataProvider
|
||||
|
||||
|
||||
class LocalFileSIGRefDataProvider(SIGRefDataProvider):
|
||||
"""Generic SIG ref data provider class for providers that fetch their data from a local file on startup."""
|
||||
|
||||
def __init__(self, sig, provider_config, path):
|
||||
super().__init__(sig, provider_config)
|
||||
self._path = path
|
||||
|
||||
def start(self):
|
||||
logging.info("Loading " + self.sig_name + " SIG ref data from file.")
|
||||
try:
|
||||
new_data = self._file_to_data(self._path)
|
||||
if new_data:
|
||||
self._replace_data(new_data)
|
||||
self.status = "OK"
|
||||
self.last_update_time = datetime.now(pytz.UTC)
|
||||
else:
|
||||
logging.info("No new SIG ref data found for " + self.sig_name)
|
||||
except Exception as e:
|
||||
self.status = "Error"
|
||||
logging.exception("Exception in local file SIG Ref Data Provider (" + self.sig_name + ")")
|
||||
|
||||
def stop(self):
|
||||
pass
|
||||
|
||||
def _file_to_data(self, path):
|
||||
"""Load a file on the given path and turn it into SIG Ref data."""
|
||||
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
@@ -2,19 +2,25 @@ from datetime import datetime
|
||||
|
||||
import pytz
|
||||
|
||||
from core.data_store import DATA_STORE
|
||||
|
||||
|
||||
class SIGRefDataProvider:
|
||||
"""Generic SIG reference data provider class. Subclasses of this query the individual URLs or files for data."""
|
||||
|
||||
def __init__(self, name, provider_config):
|
||||
def __init__(self, sig_name, provider_config):
|
||||
"""Constructor"""
|
||||
|
||||
self.name = name
|
||||
self.sig_name = sig_name
|
||||
self.enabled = provider_config["enabled"]
|
||||
self.last_update_time = datetime.min.replace(tzinfo=pytz.UTC)
|
||||
self.last_spot_time = datetime.min.replace(tzinfo=pytz.UTC)
|
||||
self.status = "Not Started" if self.enabled else "Disabled"
|
||||
|
||||
# Create an empty dict to store data if one doesn't already exist
|
||||
if not sig_name in DATA_STORE.sigrefs:
|
||||
DATA_STORE.sigrefs[sig_name] = {}
|
||||
|
||||
|
||||
def start(self):
|
||||
"""Start the provider. This should return immediately after spawning threads to access the remote resources"""
|
||||
@@ -26,3 +32,9 @@ class SIGRefDataProvider:
|
||||
"""Stop any threads and prepare for application shutdown"""
|
||||
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def _replace_data(self, new_data):
|
||||
"""Replace all data for the named sig with the new data. new_data should be a map of reference ID to SIGRef
|
||||
objects."""
|
||||
|
||||
DATA_STORE.sigrefs[self.sig_name] = new_data
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import csv
|
||||
|
||||
from data.sig_ref import SIGRef
|
||||
from sigrefdataproviders.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
||||
|
||||
|
||||
class SIOTA(FileDownloadSIGRefDataProvider):
|
||||
"""SIG ref data provider for Silos on the Air"""
|
||||
|
||||
POLL_INTERVAL_SEC = 30 * 24 * 60 * 60 # 30 days
|
||||
SIG = "SIOTA"
|
||||
DATA_URL = "https://www.silosontheair.com/data/silos.csv"
|
||||
|
||||
def __init__(self, provider_config):
|
||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_SEC)
|
||||
|
||||
def _http_response_to_data(self, http_response):
|
||||
new_data = {}
|
||||
for row in csv.DictReader(http_response.content.decode().splitlines()):
|
||||
ref_id = row["SILO_CODE"]
|
||||
new_data[ref_id] = SIGRef(sig=self.SIG, id=ref_id, name=row["NAME"] if "NAME" in row else None,
|
||||
grid=row["LOCATOR"] if "LOCATOR" in row else None,
|
||||
latitude=float(row["LAT"]) if "LAT" in row else None,
|
||||
longitude=float(row["LNG"]) if "LNG" in row else None)
|
||||
|
||||
return new_data
|
||||
@@ -0,0 +1,24 @@
|
||||
import csv
|
||||
|
||||
from data.sig_ref import SIGRef
|
||||
from sigrefdataproviders.local_file_sig_ref_data_provider import LocalFileSIGRefDataProvider
|
||||
|
||||
|
||||
class TOTA(LocalFileSIGRefDataProvider):
|
||||
"""SIG ref data provider for Toilets on the Air"""
|
||||
|
||||
SIG = "TOTA"
|
||||
PATH = "datafiles/tota.csv"
|
||||
|
||||
def __init__(self, provider_config):
|
||||
super().__init__(self.SIG, provider_config, self.PATH)
|
||||
|
||||
def _file_to_data(self, path):
|
||||
new_data = {}
|
||||
f = open(path)
|
||||
csv_data = f.read()
|
||||
dr = csv.DictReader(csv_data.splitlines())
|
||||
for row in dr:
|
||||
new_data[row["ref"]] = SIGRef(sig=self.SIG, id=row["ref"], name=row["ref"], latitude=float(row["lat"]),
|
||||
longitude=float(row["lon"]))
|
||||
return new_data
|
||||
@@ -0,0 +1,31 @@
|
||||
from data.sig_ref import SIGRef
|
||||
from sigrefdataproviders.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
||||
|
||||
|
||||
class WOTA(FileDownloadSIGRefDataProvider):
|
||||
"""SIG ref data provider for Wainwrights on the Air"""
|
||||
|
||||
POLL_INTERVAL_SEC = 365 * 24 * 60 * 60 # 365 days
|
||||
SIG = "WOTA"
|
||||
DATA_URL = "https://www.wota.org.uk/mapping/data/summits.json"
|
||||
|
||||
def __init__(self, provider_config):
|
||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_SEC)
|
||||
|
||||
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 = "https://www.wota.org.uk/MM_" + ref_id
|
||||
if ref_id.upper().startswith("LDO-"):
|
||||
number = int(ref_id.upper().replace("LDO-", ""))
|
||||
url = "https://www.wota.org.uk/MM_LDO-" + str(number + 214)
|
||||
|
||||
new_data[ref_id] = SIGRef(sig=self.SIG, id=ref_id, name=feature["properties"]["title"], url=url,
|
||||
grid=feature["properties"]["qthLocator"],
|
||||
latitude=feature["geometry"]["coordinates"][1],
|
||||
longitude=feature["geometry"]["coordinates"][0])
|
||||
|
||||
return new_data
|
||||
@@ -0,0 +1,30 @@
|
||||
import csv
|
||||
|
||||
from data.sig_ref import SIGRef
|
||||
from sigrefdataproviders.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
||||
|
||||
|
||||
class WWFF(FileDownloadSIGRefDataProvider):
|
||||
"""SIG ref data provider for Worldwide Flora & Fauna"""
|
||||
|
||||
POLL_INTERVAL_SEC = 30 * 24 * 60 * 60 # 30 days
|
||||
SIG = "WWFF"
|
||||
DATA_URL = "https://wwff.co/wwff-data/wwff_directory.csv"
|
||||
|
||||
def __init__(self, provider_config):
|
||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_SEC)
|
||||
|
||||
def _http_response_to_data(self, http_response):
|
||||
new_data = {}
|
||||
for row in csv.DictReader(http_response.content.decode().splitlines()):
|
||||
ref_id = row["reference"]
|
||||
new_data[ref_id] = SIGRef(sig=self.SIG, id=ref_id, name=row["name"] if "name" in row else None,
|
||||
url="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)
|
||||
|
||||
return new_data
|
||||
@@ -0,0 +1,33 @@
|
||||
from pyhamtools.locator import latlong_to_locator
|
||||
|
||||
from data.sig_ref import SIGRef
|
||||
from sigrefdataproviders.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
||||
|
||||
|
||||
class ZLOTA(FileDownloadSIGRefDataProvider):
|
||||
"""SIG ref data provider for New Zealand on the Air"""
|
||||
|
||||
POLL_INTERVAL_SEC = 30 * 24 * 60 * 60 # 30 days
|
||||
SIG = "ZLOTA"
|
||||
DATA_URL = "https://ontheair.nz/assets/assets.json"
|
||||
|
||||
def __init__(self, provider_config):
|
||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_SEC)
|
||||
|
||||
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["y"]
|
||||
longitude = ref["x"]
|
||||
|
||||
new_data[ref_id] = SIGRef(sig=self.SIG, id=ref_id, name=ref["name"],
|
||||
url="https://ontheair.nz/assets/" + ref_id.replace("/", "_"),
|
||||
latitude=latitude,
|
||||
longitude=longitude)
|
||||
if latitude and longitude:
|
||||
new_data[ref_id].grid = latlong_to_locator(latitude, longitude, 6)
|
||||
|
||||
return new_data
|
||||
Reference in New Issue
Block a user