From 81cd686a000b8b957c2dcb6b0c9e1bee08b97eb2 Mon Sep 17 00:00:00 2001 From: Ian Renton Date: Fri, 18 Sep 2026 14:54:11 +0100 Subject: [PATCH] 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 --- config-example.yml | 12 +- core/activity_lookup_helper.py | 156 ++++++++++++ core/activity_utils.py | 26 ++ core/constants.py | 226 +++++++++--------- core/data_providers.py | 4 +- core/data_store.py | 26 +- core/enums.py | 8 +- core/sig_lookup_helper.py | 153 ------------ core/sig_utils.py | 24 -- core/single_object_data_cache.py | 8 +- data/activity.py | 41 ++++ data/{sig_ref.py => activity_ref.py} | 12 +- data/alert.py | 25 +- data/callsign.py | 4 +- data/sig.py | 39 --- data/spot.py | 165 +++++++------ docs/embedding.md | 4 +- docs/modifying.md | 4 +- .../activity_ref_data_provider.py} | 12 +- .../{sigrefdata => activityrefdata}/arlhs.py | 24 +- .../{sigrefdata => activityrefdata}/cota.py | 22 +- .../{sigrefdata => activityrefdata}/dce.py | 18 +- .../{sigrefdata => activityrefdata}/defe.py | 18 +- .../{sigrefdata => activityrefdata}/dme.py | 26 +- .../{sigrefdata => activityrefdata}/dmue.py | 18 +- .../{sigrefdata => activityrefdata}/dmve.py | 18 +- .../{sigrefdata => activityrefdata}/dtmba.py | 18 +- .../{sigrefdata => activityrefdata}/fea.py | 20 +- ...le_download_activity_ref_data_provider.py} | 35 +-- .../{sigrefdata => activityrefdata}/gma.py | 24 +- .../{sigrefdata => activityrefdata}/illw.py | 24 +- .../{sigrefdata => activityrefdata}/iota.py | 24 +- providers/activityrefdata/krmnpa.py | 14 ++ .../{sigrefdata => activityrefdata}/llota.py | 24 +- .../local_file_activity_ref_data_provider.py | 36 +++ .../{sigrefdata => activityrefdata}/mota.py | 24 +- .../{sigrefdata => activityrefdata}/pga.py | 22 +- .../pnp_kml_activity_ref_data_provider.py} | 20 +- .../{sigrefdata => activityrefdata}/pota.py | 24 +- providers/activityrefdata/sanpcpa.py | 14 ++ .../{sigrefdata => activityrefdata}/siota.py | 24 +- .../{sigrefdata => activityrefdata}/sota.py | 24 +- .../toilets.py | 22 +- .../{sigrefdata => activityrefdata}/towers.py | 24 +- .../{sigrefdata => activityrefdata}/wca.py | 24 +- .../{sigrefdata => activityrefdata}/wota.py | 24 +- .../{sigrefdata => activityrefdata}/wwbota.py | 24 +- .../{sigrefdata => activityrefdata}/wwff.py | 24 +- .../{sigrefdata => activityrefdata}/zlota.py | 24 +- providers/alert/bota.py | 4 +- providers/alert/hamsat.py | 6 +- providers/alert/parksnpeaks.py | 30 +-- providers/alert/pota.py | 4 +- providers/alert/sota.py | 4 +- providers/alert/wota.py | 4 +- providers/alert/wwff.py | 4 +- providers/sigrefdata/krmnpa.py | 14 -- .../local_file_sig_ref_data_provider.py | 36 --- providers/sigrefdata/sanpcpa.py | 14 -- providers/spot/gma.py | 28 +-- providers/spot/hema.py | 12 +- providers/spot/llota.py | 8 +- providers/spot/parksnpeaks.py | 36 +-- providers/spot/pota.py | 12 +- providers/spot/sota.py | 12 +- providers/spot/spot_provider.py | 4 +- providers/spot/tiles.py | 14 +- providers/spot/towers.py | 6 +- providers/spot/wota.py | 10 +- providers/spot/wwbota.py | 14 +- providers/spot/wwff.py | 12 +- providers/spot/xota.py | 26 +- providers/spot/zlota.py | 8 +- static/apidocs/openapi.yml | 193 +++++++-------- static/css/style.css | 2 +- static/js/add-spot.js | 14 +- static/js/alerts.js | 28 +-- static/js/bands.js | 2 +- static/js/map.js | 20 +- static/js/spots.js | 16 +- static/js/spotsbandsandmap.js | 10 +- static/js/ui-ham.js | 2 +- templates/about.html | 15 +- templates/add_spot.html | 4 +- templates/bands.html | 2 +- .../cards/{sigs.html => activities.html} | 2 +- templates/map.html | 2 +- templates/spots.html | 2 +- templates/status.html | 2 +- webserver/handlers/api/addspot.py | 18 +- webserver/handlers/api/alerts.py | 12 +- webserver/handlers/api/lookups.py | 32 +-- webserver/handlers/api/options.py | 12 +- webserver/handlers/api/spots.py | 24 +- webserver/handlers/api/v1_addspot.py | 8 +- webserver/webserver.py | 4 +- 96 files changed, 1208 insertions(+), 1170 deletions(-) create mode 100644 core/activity_lookup_helper.py create mode 100644 core/activity_utils.py delete mode 100644 core/sig_lookup_helper.py delete mode 100644 core/sig_utils.py create mode 100644 data/activity.py rename data/{sig_ref.py => activity_ref.py} (66%) delete mode 100644 data/sig.py rename providers/{sigrefdata/sig_ref_data_provider.py => activityrefdata/activity_ref_data_provider.py} (79%) rename providers/{sigrefdata => activityrefdata}/arlhs.py (67%) rename providers/{sigrefdata => activityrefdata}/cota.py (67%) rename providers/{sigrefdata => activityrefdata}/dce.py (60%) rename providers/{sigrefdata => activityrefdata}/defe.py (62%) rename providers/{sigrefdata => activityrefdata}/dme.py (75%) rename providers/{sigrefdata => activityrefdata}/dmue.py (57%) rename providers/{sigrefdata => activityrefdata}/dmve.py (65%) rename providers/{sigrefdata => activityrefdata}/dtmba.py (56%) rename providers/{sigrefdata => activityrefdata}/fea.py (68%) rename providers/{sigrefdata/file_download_sig_ref_data_provider.py => activityrefdata/file_download_activity_ref_data_provider.py} (64%) rename providers/{sigrefdata => activityrefdata}/gma.py (70%) rename providers/{sigrefdata => activityrefdata}/illw.py (67%) rename providers/{sigrefdata => activityrefdata}/iota.py (73%) create mode 100644 providers/activityrefdata/krmnpa.py rename providers/{sigrefdata => activityrefdata}/llota.py (66%) create mode 100644 providers/activityrefdata/local_file_activity_ref_data_provider.py rename providers/{sigrefdata => activityrefdata}/mota.py (67%) rename providers/{sigrefdata => activityrefdata}/pga.py (72%) rename providers/{sigrefdata/pnp_kml_sig_ref_data_provider.py => activityrefdata/pnp_kml_activity_ref_data_provider.py} (76%) rename providers/{sigrefdata => activityrefdata}/pota.py (67%) create mode 100644 providers/activityrefdata/sanpcpa.py rename providers/{sigrefdata => activityrefdata}/siota.py (64%) rename providers/{sigrefdata => activityrefdata}/sota.py (72%) rename providers/{sigrefdata => activityrefdata}/toilets.py (60%) rename providers/{sigrefdata => activityrefdata}/towers.py (68%) rename providers/{sigrefdata => activityrefdata}/wca.py (74%) rename providers/{sigrefdata => activityrefdata}/wota.py (71%) rename providers/{sigrefdata => activityrefdata}/wwbota.py (67%) rename providers/{sigrefdata => activityrefdata}/wwff.py (70%) rename providers/{sigrefdata => activityrefdata}/zlota.py (72%) delete mode 100644 providers/sigrefdata/krmnpa.py delete mode 100644 providers/sigrefdata/local_file_sig_ref_data_provider.py delete mode 100644 providers/sigrefdata/sanpcpa.py rename templates/cards/{sigs.html => activities.html} (74%) diff --git a/config-example.yml b/config-example.yml index 589c8ad..0cd0072 100644 --- a/config-example.yml +++ b/config-example.yml @@ -128,12 +128,12 @@ spot_providers: name: "C3 TOTA" enabled: false url: "wss://39c3.totawatch.de/api/spot/live" - # For the "XOTA" provider, a SIG must be set menually here because xOTA is a generic backend for xOTA + # For the "XOTA" provider, an activity must be set manually here because xOTA is a generic backend for xOTA # programmes and so different URLs potentially provide different programmes. sig: "Toilets" - # For Toilets on the Air, we prefix the SIG references (T-01 etc) with some characters that define the conference: - # C3, EH or HOPE - so we can look up the correct locations in our database, because each conference starts from T-01 - # but refers to a toilet in a different building (or continent!) + # For Toilets on the Air, we prefix the activity references (T-01 etc) with some characters that define the + # conference: C3, EH or HOPE - so we can look up the correct locations in our database, because each conference + # starts from T-01 but refers to a toilet in a different building (or continent!) sig_ref_prefix: "C3" - class: "XOTA" @@ -216,8 +216,8 @@ static_data_providers: enabled: true -# SIG reference data providers to use. These allow Spothole to download, for example, the WWFF directory that maps WWFF -# park IDs to their name and location. +# Activity reference data providers to use. These allow Spothole to download, for example, the WWFF directory that +# maps WWFF park IDs to their name and location. sig_ref_data_providers: - class: "POTA" enabled: true diff --git a/core/activity_lookup_helper.py b/core/activity_lookup_helper.py new file mode 100644 index 0000000..c90d934 --- /dev/null +++ b/core/activity_lookup_helper.py @@ -0,0 +1,156 @@ +import logging +import re + +from pyhamtools.locator import latlong_to_locator, locator_to_latlong + +from core.constants import ACTIVITIES +from core.data_store import DATA_STORE +from core.enums import ActivityRefType +from core.geo_utils import wab_wai_square_to_lat_lon +from data.activity_ref import ActivityRef + +logger = logging.getLogger(__name__) + + +def get_activity_ref_info(activity_name, ref_id): + """Look up details of an activity reference (e.g. POTA park) such as name, lat/lon, and grid. Takes in an + activity name and a reference ID (both strings) and returns an ActivityRef object populated with as much data + as we can find. This makes use of activity ref data in the data store, live lookups from the web, or just + automatic calculation depending on which activity we are getting data for.""" + + if activity_name is None or activity_name == "" or ref_id is None or ref_id == "": + logger.debug("Failed to look up activity ref info, activity or ref were not set.") + return None + + # Sometimes we allow spaces instead of dashes in references due to common usage that way, but official reference + # lists never do, so convert them here. + ref_id = ref_id.replace(" ", "-") + + # Prepare the object to be returned + activity_ref = ActivityRef(sig=activity_name, id=ref_id) + + # We can always get the reference type and the icon from the activity itself + for activity in ACTIVITIES: + if activity.name.upper() == activity_name.upper(): + activity_ref.ref_type = activity.ref_type + activity_ref.icon = activity.icon + + try: + ### FUDGES ### + # + # DME fudge. Our database has leading zeros padding to 5 digits which is the expected format, but not all + # activators add leading zeros. We also need to normalise "DME 01234" to "DME-01234" to match what's in our + # database. + if activity_name.upper() == "DME": + match = re.match(r"DME[\- ](\d{3,5})", ref_id, re.IGNORECASE) + if match: + number = match.group(1) + ref_id = f"DME-{number.zfill(5)}" + + # DTMBA spotters sometimes include spaces and dashes, our regex allows them but they must be removed here so we + # can look up against the official list which doesn't have them + if activity_name.upper() == "DTMBA": + ref_id = ref_id.replace("-", "").replace(" ", "") + + ### NO DATA ACTIVITIES ### + # + # If the activity is HEMA or BIWOTA, we have no way to either generate useful data or look it up on a + # reference list, so just skip the lookup here. + if activity_name.upper() == "HEMA" or activity_name.upper() == "BIWOTA": + return activity_ref + + ### PROGRAMMATIC DATA GENERATION INSTEAD OF LOOKUPS ### + # + # If the activity is Tiles, WAB, WAI or BOTA (Beaches), we don't have anything to look up from the data + # store, we can calculate all the information we are going to get directly. + if activity_name.upper() == "TILES": + # Tiles on the Air just uses Maidenhead 6-digit squares, so ID, Name and Grid are all the same + if not activity_ref.name: + activity_ref.name = activity_ref.id + if not activity_ref.grid: + activity_ref.grid = activity_ref.id + if activity_ref.grid and (not activity_ref.latitude or not activity_ref.longitude): + ll = locator_to_latlong(str(activity_ref.grid)) + activity_ref.latitude = ll[0] + activity_ref.longitude = ll[1] + return activity_ref + + elif activity_name.upper() == "WAB" or activity_name.upper() == "WAI": + ll = wab_wai_square_to_lat_lon(ref_id) + if ll: + activity_ref.name = ref_id + try: + activity_ref.grid = latlong_to_locator(ll[0], ll[1], 6) + activity_ref.latitude = ll[0] + activity_ref.longitude = ll[1] + except Exception: + logger.warning("Invalid lat/lon received for WAB/WAI reference") + return activity_ref + + elif activity_name.upper() == "BOTA": + # For BOTA all we can ever generate is the URL, there is no data file or lookup for lat/longs + if not activity_ref.name: + activity_ref.name = activity_ref.id + if activity_ref.name: + activity_ref.url = ( + f"https://www.beachesontheair.com/beaches/{activity_ref.name.lower().replace(' ', '-')}" + ) + return activity_ref + + elif activity_name.upper() == "GMA Islands": + # GMA Islands is a bit of a mess of GMA and IOTA references. Try looking them both up and see what returns + # the best result. + iota_lookup = get_activity_ref_info("IOTA", ref_id) + gma_lookup = get_activity_ref_info("GMA", ref_id) + for key, value in iota_lookup.__dict__.items(): + if value is not None and activity_ref.__dict__.get(key) is None: + activity_ref.__dict__[key] = value + for key, value in gma_lookup.__dict__.items(): + if value is not None and activity_ref.__dict__.get(key) is None: + activity_ref.__dict__[key] = value + activity_ref.ref_type = ActivityRefType.ISLAND + return activity_ref + + ### ACTUAL LOOKUP ### + # + # OK, this is something we have to look up. Now check to see if our data store contains reference data and if + # so, copy the data into the activity_ref object + key = f"{activity_name}:{ref_id}" + try: + lookup_data = DATA_STORE.activity_refs.get(key) if key in DATA_STORE.activity_refs else None + if lookup_data: + for attr, value in lookup_data.__dict__.items(): + if value is not None and activity_ref.__dict__.get(attr) is None: + activity_ref.__dict__[attr] = value + + else: + # Maybe a super new reference we don't know about yet, but more likely a typo or a test reference, + # just silently ignore it. + logger.debug(f"{activity_name} database did not contain data for ref {ref_id}") + + except (ValueError, KeyError): + # Catch exceptions due to e.g. old versions of objects in the cache that are no longer compatible, + # and remove them from the cache. + del DATA_STORE.activity_refs[key] + return None + + except Exception: + logger.exception(f"Exception when looking up activity ref info for {activity_name} ref {ref_id}") + return activity_ref + + +def populate_missing_activity_ref_info(activity_ref): + """Look up details of an activity reference (e.g. POTA park) such as name, lat/lon, and grid. Takes in an + activity_ref object which must at minimum have a "sig" and an "id". The rest of the object will be populated + and returned. Any data currently in the object will be kept, only missing data in the object will be populated + if it can be determined.""" + + lookup_data = get_activity_ref_info(activity_ref.sig, activity_ref.id) + + if lookup_data: + # Copy new activity ref data into existing object where data was previously missing + for key, value in lookup_data.__dict__.items(): + if value is not None and activity_ref.__dict__.get(key) is None: + activity_ref.__dict__[key] = value + + return activity_ref diff --git a/core/activity_utils.py b/core/activity_utils.py new file mode 100644 index 0000000..1b5cfaf --- /dev/null +++ b/core/activity_utils.py @@ -0,0 +1,26 @@ +from core.constants import ACTIVITIES + + +def get_ref_regex_for_activity(activity): + """Utility function to get the regex string for an activity reference for a named activity. If no match is + found, None will be returned.""" + + for a in ACTIVITIES: + if a.name.upper() == activity.upper(): + return a.ref_regex + return None + + +def get_activity_name_from_comment_name(activity): + """Utility function to get the name of an activity from its "comment name". Generally these will be the same + but there are some cases (e.g. is "TOTA" Towers, Tiles or Toilets?) where we need to transform one to the + other.""" + + for a in ACTIVITIES: + if any(n.upper() == activity.upper() for n in a.comment_names): + return a.name + return None + + +# Regex matching any activity's "comment name", i.e. how it may be referred to in spot comments +ANY_ACTIVITY_REGEX = rf"({'|'.join(n for a in ACTIVITIES for n in a.comment_names)})" diff --git a/core/constants.py b/core/constants.py index ea76e43..0a8ce8a 100644 --- a/core/constants.py +++ b/core/constants.py @@ -1,7 +1,7 @@ from core.config import SERVER_OWNER_CALLSIGN -from core.enums import SIGRefType, SIGType +from core.enums import ActivityRefType, ActivityType +from data.activity import Activity from data.band import Band -from data.sig import SIG # General software SOFTWARE_VERSION = "2.1" @@ -10,387 +10,387 @@ SOFTWARE_VERSION = "2.1" HTTP_HEADERS = {"User-Agent": f"Spothole v{SOFTWARE_VERSION} (operated by {SERVER_OWNER_CALLSIGN})"} HAMQTH_PRG = f"Spothole v{SOFTWARE_VERSION} operated by {SERVER_OWNER_CALLSIGN}".replace(" ", "_") -# Special Interest Groups -SIGS = [ - SIG( +# Activities +ACTIVITIES = [ + Activity( name="AMSAT", comment_names=[], description="Amateur Radio Satellites", - sig_type=SIGType.WORLDWIDE, + sig_type=ActivityType.WORLDWIDE, icon="fa-satellite", refs_globally_unique=False, ), - SIG( + Activity( name="EME", comment_names=[], description="Moonbounce", - sig_type=SIGType.WORLDWIDE, + sig_type=ActivityType.WORLDWIDE, icon="fa-moon", refs_globally_unique=False, ), - SIG( + Activity( name="POTA", comment_names=["POTA"], description="Parks on the Air", - sig_type=SIGType.WORLDWIDE, - ref_type=SIGRefType.PARK, + sig_type=ActivityType.WORLDWIDE, + ref_type=ActivityRefType.PARK, ref_regex=r"[A-Z]{2}\-\d{4,5}|K\-TEST", icon="fa-tree", refs_globally_unique=False, ), - SIG( + Activity( name="SOTA", comment_names=["SOTA"], description="Summits on the Air", - sig_type=SIGType.WORLDWIDE, - ref_type=SIGRefType.SUMMIT, + sig_type=ActivityType.WORLDWIDE, + ref_type=ActivityRefType.SUMMIT, ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}", icon="fa-mountain-sun", refs_globally_unique=False, ), - SIG( + Activity( name="WWFF", comment_names=["WWFF"], description="World Wide Flora & Fauna", - sig_type=SIGType.WORLDWIDE, - ref_type=SIGRefType.PARK, + sig_type=ActivityType.WORLDWIDE, + ref_type=ActivityRefType.PARK, ref_regex=r"[A-Z0-9]{1,3}FF\-\d{4}", icon="fa-seedling", refs_globally_unique=True, ), - SIG( + Activity( name="GMA", comment_names=["GMA"], description="Global Mountain Activity", - sig_type=SIGType.WORLDWIDE, - ref_type=SIGRefType.SUMMIT, + sig_type=ActivityType.WORLDWIDE, + ref_type=ActivityRefType.SUMMIT, ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}", icon="fa-person-hiking", refs_globally_unique=False, ), - SIG( + Activity( name="WWBOTA", comment_names=["WWBOTA", "BOTA"], description="Worldwide Bunkers on the Air", - sig_type=SIGType.WORLDWIDE, - ref_type=SIGRefType.BUNKER, + sig_type=ActivityType.WORLDWIDE, + ref_type=ActivityRefType.BUNKER, ref_regex=r"B\/[A-Z0-9]{1,3}\-\d{3,4}", icon="fa-radiation", refs_globally_unique=True, ), - SIG( + Activity( name="HEMA", comment_names=["HEMA"], description="HuMPs Excluding Marilyns Award", - sig_type=SIGType.WORLDWIDE, - ref_type=SIGRefType.SUMMIT, + sig_type=ActivityType.WORLDWIDE, + ref_type=ActivityRefType.SUMMIT, ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{3}\-\d{3}", icon="fa-mound", refs_globally_unique=False, ), - SIG( + Activity( name="IOTA", comment_names=["IOTA"], description="Islands on the Air", - sig_type=SIGType.WORLDWIDE, - ref_type=SIGRefType.ISLAND, + sig_type=ActivityType.WORLDWIDE, + ref_type=ActivityRefType.ISLAND, ref_regex=r"[A-Z]{2}\-\d{3}", icon="fa-book-atlas", refs_globally_unique=False, ), - SIG( + Activity( name="GMA Islands", comment_names=[], description="Global Mountain Activity - Islands", - sig_type=SIGType.WORLDWIDE, - ref_type=SIGRefType.ISLAND, + sig_type=ActivityType.WORLDWIDE, + ref_type=ActivityRefType.ISLAND, ref_regex=r"(([A-Z]{2}\-\d{3})|([A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}))", icon="fa-person-hiking", refs_globally_unique=False, ), - SIG( + Activity( name="ARLHS", comment_names=["ARLHS"], description="Amateur Radio Lighthouse Society", - sig_type=SIGType.WORLDWIDE, - ref_type=SIGRefType.LIGHTHOUSE, + sig_type=ActivityType.WORLDWIDE, + ref_type=ActivityRefType.LIGHTHOUSE, ref_regex=r"[A-Z]{3}[\- ]\d{3,4}", icon="fa-house-flood-water", refs_globally_unique=False, ), - SIG( + Activity( name="ILLW", comment_names=["ILLW"], description="International Lighthouse & Lightship Weekend", - sig_type=SIGType.EVENT, - ref_type=SIGRefType.LIGHTHOUSE, + sig_type=ActivityType.EVENT, + ref_type=ActivityRefType.LIGHTHOUSE, ref_regex=r"[A-Z]{2}\d{4}", icon="fa-house-flood-water", refs_globally_unique=False, ), - SIG( + Activity( name="MOTA", comment_names=["MOTA"], description="Mills on the Air", - sig_type=SIGType.EVENT, - ref_type=SIGRefType.MILL, + sig_type=ActivityType.EVENT, + ref_type=ActivityRefType.MILL, ref_regex=r"X\d{4,6}", icon="fa-fan", refs_globally_unique=True, ), - SIG( + Activity( name="SIOTA", comment_names=["SIOTA"], description="Silos on the Air", - sig_type=SIGType.WORLDWIDE, - ref_type=SIGRefType.SILO, + sig_type=ActivityType.WORLDWIDE, + ref_type=ActivityRefType.SILO, ref_regex=r"[A-Z]{2}\-[A-Z]{3}\d", icon="fa-wheat-awn", refs_globally_unique=False, ), - SIG( + Activity( name="WCA", comment_names=["WCA"], description="World Castles Award", - sig_type=SIGType.WORLDWIDE, - ref_type=SIGRefType.CASTLE, + sig_type=ActivityType.WORLDWIDE, + ref_type=ActivityRefType.CASTLE, ref_regex=r"[A-Z0-9]{1,3}\-\d{5}", icon="fa-chess-rook", refs_globally_unique=False, ), - SIG( + Activity( name="ZLOTA", comment_names=["ZLOTA"], description="New Zealand on the Air", - sig_type=SIGType.REGIONAL, + sig_type=ActivityType.REGIONAL, ref_type=None, ref_regex=r"ZL[A-Z]/[A-Z]{2}\-\d{3,4}", icon="fa-kiwi-bird", region_flag="🇳🇿", refs_globally_unique=True, ), - SIG( + Activity( name="WOTA", comment_names=["WOTA"], description="Wainwrights on the Air", - sig_type=SIGType.REGIONAL, - ref_type=SIGRefType.SUMMIT, + sig_type=ActivityType.REGIONAL, + ref_type=ActivityRefType.SUMMIT, ref_regex=r"[A-Z]{3}-[0-9]{2}", icon="fa-w", region_flag="🇬🇧", refs_globally_unique=False, ), - SIG( + Activity( name="BOTA", comment_names=[], description="Beaches on the Air", - sig_type=SIGType.WORLDWIDE, - ref_type=SIGRefType.BEACH, + sig_type=ActivityType.WORLDWIDE, + ref_type=ActivityRefType.BEACH, icon="fa-umbrella-beach", refs_globally_unique=False, ), - SIG( + Activity( name="KRMNPA", comment_names=["KRMNPA"], description="Keith Roget Memorial National Parks Award", - sig_type=SIGType.REGIONAL, - ref_type=SIGRefType.PARK, + sig_type=ActivityType.REGIONAL, + ref_type=ActivityRefType.PARK, ref_regex=r"VKFF\-\d{4}", icon="fa-earth-oceania", region_flag="🇦🇺", refs_globally_unique=False, ), - SIG( + Activity( name="SANPCPA", comment_names=["SANPCPA"], description="South Australian National Parks and Conservation Parks Award", - sig_type=SIGType.REGIONAL, - ref_type=SIGRefType.PARK, + sig_type=ActivityType.REGIONAL, + ref_type=ActivityRefType.PARK, ref_regex=r"VKFF\-\d{4}", icon="fa-earth-oceania", region_flag="🇦🇺", refs_globally_unique=False, ), - SIG( + Activity( name="LLOTA", comment_names=["LLOTA"], description="Lagos y Lagunas on the Air", - sig_type=SIGType.WORLDWIDE, - ref_type=SIGRefType.LAKE, + sig_type=ActivityType.WORLDWIDE, + ref_type=ActivityRefType.LAKE, ref_regex=r"LL[A-Z]{2}\-\d{4}", icon="fa-water", refs_globally_unique=True, ), - SIG( + Activity( name="Towers", comment_names=["TOTA"], description="Towers on the Air", - sig_type=SIGType.WORLDWIDE, - ref_type=SIGRefType.TOWER, + sig_type=ActivityType.WORLDWIDE, + ref_type=ActivityRefType.TOWER, ref_regex=r"[A-Z]{2,3}R\-\d{4}", icon="fa-tower-observation", refs_globally_unique=False, ), - SIG( + Activity( name="Tiles", comment_names=[], description="Tiles on the Air", - sig_type=SIGType.WORLDWIDE, - ref_type=SIGRefType.GRID, + sig_type=ActivityType.WORLDWIDE, + ref_type=ActivityRefType.GRID, ref_regex=r"[A-Za-z]{2}[0-9]{2}[A-Za-z]{2}", icon="fa-square", refs_globally_unique=False, ), - SIG( + Activity( name="WAB", comment_names=["WAB"], description="Worked All Britain", - sig_type=SIGType.REGIONAL, - ref_type=SIGRefType.GRID, + sig_type=ActivityType.REGIONAL, + ref_type=ActivityRefType.GRID, ref_regex=r"[A-Z]{1,2}[0-9]{2}", icon="fa-table-cells-large", region_flag="🇬🇧", refs_globally_unique=False, ), - SIG( + Activity( name="WAI", comment_names=["WAI"], description="Worked All Ireland", - sig_type=SIGType.REGIONAL, - ref_type=SIGRefType.GRID, + sig_type=ActivityType.REGIONAL, + ref_type=ActivityRefType.GRID, ref_regex=r"[A-Z][0-9]{2}", icon="fa-table-cells-large", region_flag="🇮🇪", refs_globally_unique=False, ), - SIG( + Activity( name="DMF", comment_names=["DMF"], description="Diplôme des Moulins de France", - sig_type=SIGType.REGIONAL, - ref_type=SIGRefType.MILL, + sig_type=ActivityType.REGIONAL, + ref_type=ActivityRefType.MILL, icon="fa-fan", region_flag="🇫🇷", refs_globally_unique=False, ), - SIG( + Activity( name="DME", comment_names=["DME"], description="Diploma Municipios de España", - sig_type=SIGType.REGIONAL, - ref_type=SIGRefType.TOWN, + sig_type=ActivityType.REGIONAL, + ref_type=ActivityRefType.TOWN, ref_regex=r"DME[\- ]\d{3,5}", icon="fa-building", region_flag="🇪🇸", refs_globally_unique=True, ), - SIG( + Activity( name="FEA", comment_names=["FEA"], description="Diploma Faros de España", - sig_type=SIGType.REGIONAL, - ref_type=SIGRefType.LIGHTHOUSE, + sig_type=ActivityType.REGIONAL, + ref_type=ActivityRefType.LIGHTHOUSE, # 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 allow for that. The FEA sigref data provider adds both + # prefix and just use FEA-1234 or FEA 1234, so allow for that. The FEA activity ref data provider adds both # forms to the database. ref_regex=r"([DE]|FEA)[\- ]\d{4}(\.\d)?", icon="fa-house-flood-water", region_flag="🇪🇸", refs_globally_unique=True, ), - SIG( + Activity( name="DMUE", comment_names=["DMUE"], description="Diploma Museos de España", - sig_type=SIGType.REGIONAL, - ref_type=SIGRefType.BUILDING, + sig_type=ActivityType.REGIONAL, + ref_type=ActivityRefType.BUILDING, ref_regex=r"MUE[A-Z]{2}-\d{3}", icon="fa-landmark", region_flag="🇪🇸", refs_globally_unique=True, ), - SIG( + Activity( name="DMVE", comment_names=["DMVE"], description="Diploma Monumentos y Vestigios de España", - sig_type=SIGType.REGIONAL, - ref_type=SIGRefType.BUILDING, + sig_type=ActivityType.REGIONAL, + ref_type=ActivityRefType.BUILDING, ref_regex=r"MV[A-Z]{1,2}-\d{4}", icon="fa-monument", region_flag="🇪🇸", refs_globally_unique=True, ), - SIG( + Activity( name="DCE", comment_names=["DCE"], description="Diploma Castillos de España", - sig_type=SIGType.REGIONAL, - ref_type=SIGRefType.CASTLE, + sig_type=ActivityType.REGIONAL, + ref_type=ActivityRefType.CASTLE, ref_regex=r"C[A-Z]{1,2}-\d{3}", icon="fa-chess-rook", region_flag="🇪🇸", refs_globally_unique=False, ), - SIG( + Activity( name="DEFE", comment_names=["DEFE"], description="Diploma Estaciones de Ferrocarril de España", - sig_type=SIGType.REGIONAL, - ref_type=SIGRefType.BUILDING, + sig_type=ActivityType.REGIONAL, + ref_type=ActivityRefType.BUILDING, ref_regex=r"EF[A-Z]{1,2}-\d{3}", icon="fa-train", region_flag="🇪🇸", refs_globally_unique=True, ), - SIG( + Activity( name="DTMBA", comment_names=["DTMBA"], description="Diploma Teatri Musei e Belle Arti", - sig_type=SIGType.REGIONAL, - ref_type=SIGRefType.BUILDING, + sig_type=ActivityType.REGIONAL, + ref_type=ActivityRefType.BUILDING, ref_regex=r"I-?[0-9]{3,4}\s?[A-Z]{2}", icon="fa-landmark", region_flag="🇮🇹", refs_globally_unique=True, ), - SIG( + Activity( name="BIWOTA", comment_names=["BIWOTA"], description="British Inland Waterways on the Air", - sig_type=SIGType.EVENT, - ref_type=SIGRefType.WATERWAY, + sig_type=ActivityType.EVENT, + ref_type=ActivityRefType.WATERWAY, icon="fa-ship", region_flag="🇬🇧", refs_globally_unique=False, ), - SIG( + Activity( name="COTA", comment_names=["COTA"], description="Castles on the Air", - sig_type=SIGType.REGIONAL, - ref_type=SIGRefType.CASTLE, + sig_type=ActivityType.REGIONAL, + ref_type=ActivityRefType.CASTLE, ref_regex=r"[A-Z]{3}\-[0-9]{3,5}", icon="fa-chess-rook", region_flag="🇩🇪", refs_globally_unique=False, ), - SIG( + Activity( name="PGA", comment_names=["PGA"], description="Polish Gmina Award", - sig_type=SIGType.REGIONAL, - ref_type=SIGRefType.REGION, + sig_type=ActivityType.REGIONAL, + ref_type=ActivityRefType.REGION, ref_regex=r"[A-Z]{2}[0-9]{2}", icon="fa-g", region_flag="🇵🇱", refs_globally_unique=False, ), - SIG( + Activity( name="Toilets", comment_names=[], description="Toilets on the Air", - sig_type=SIGType.EVENT, - ref_type=SIGRefType.TOILET, + sig_type=ActivityType.EVENT, + ref_type=ActivityRefType.TOILET, ref_regex=r"T\-[0-9]{2}", icon="fa-toilet", region_flag="🏴‍☠️", diff --git a/core/data_providers.py b/core/data_providers.py index 06423ee..71a5343 100644 --- a/core/data_providers.py +++ b/core/data_providers.py @@ -29,7 +29,7 @@ class DataProviders: for entry in config.get("static_data_providers", []): self.static_data_providers.append(create_provider_from_config("providers.staticdata", entry)) for entry in config.get("sig_ref_data_providers", []): - self.sig_ref_data_providers.append(create_provider_from_config("providers.sigrefdata", entry)) + self.sig_ref_data_providers.append(create_provider_from_config("providers.activityrefdata", entry)) for entry in config.get("callsign_data_providers", []): self.callsign_data_providers.append(create_provider_from_config("providers.callsigndata", entry)) @@ -54,7 +54,7 @@ class DataProviders: 25.0, lambda: self.start_providers(self.solar_condition_providers, "solar condition"), ), - threading.Timer(30.0, lambda: self.start_providers(self.sig_ref_data_providers, "SIG ref data")), + threading.Timer(30.0, lambda: self.start_providers(self.sig_ref_data_providers, "activity ref data")), ] for t in self._startup_timers: t.daemon = True diff --git a/core/data_store.py b/core/data_store.py index 954cd30..87006e6 100644 --- a/core/data_store.py +++ b/core/data_store.py @@ -15,8 +15,8 @@ CACHE_DIR = "./cache/" class DataStore: - """Data caching/storage object. Handles storage of spots, alerts, solar conditions, SIG reference data, and callsign - lookup data using different caching strategies for each.""" + """Data caching/storage object. Handles storage of spots, alerts, solar conditions, activity reference data, and + callsign lookup data using different caching strategies for each.""" def __init__(self): # Constants @@ -34,7 +34,7 @@ class DataStore: self.callsign_data_hamqth = None self.dxcc_data = None self.dxcc_lookup_by_call_regex = [] - self.sigrefs = None + self.activity_refs = None self.status = None self.solar_conditions = None # ITU/CQ zone GeoJSON data is only ever loaded statically from a local file so these don't even need to be @@ -51,16 +51,16 @@ class DataStore: self.solar_conditions = SingleObjectDataCache(f"{CACHE_DIR}solar", SolarConditions()) self.status = SingleObjectDataCache(f"{CACHE_DIR}status", {}) - # Standard disk cache for static reference and SIG ref data. Separate provider threads will repopulate these on - # a regular basis but there's no need for a TTL since old data is better than no data. + # Standard disk cache for static reference and activity ref data. Separate provider threads will repopulate + # these on a regular basis but there's no need for a TTL since old data is better than no data. self.dxcc_data = diskcache.Cache(f"{CACHE_DIR}dxcc_data") self.regenerate_call_regex_to_dxcc_entity_map() - # For SIG reference data specifically, we need to key on both SIG *and* reference, and trying to do two layers - # of dict in diskcache absolutely destroys performance with unpickling huge dicts, so we have an ugly "SIG:ref" - # syntax for keys to keep it a single level. - self.sigrefs = diskcache.Cache(f"{CACHE_DIR}sigrefs") - logger.info(f"Loaded data for {len(self.sigrefs)} SIG references.") + # For activity reference data specifically, we need to key on both activity *and* reference, and trying to do + # two layers of dict in diskcache absolutely destroys performance with unpickling huge dicts, so we have an + # ugly "activity:ref" syntax for keys to keep it a single level. + self.activity_refs = diskcache.Cache(f"{CACHE_DIR}activity_refs") + logger.info(f"Loaded data for {len(self.activity_refs)} activity references.") # Standard disk cache for callsign data. This data does have a TTL to trigger an occasional re-lookup. # Old data *is* better than no data, but we can't have a background thread re-looking-up every callsign @@ -82,8 +82,8 @@ class DataStore: logger.info(f"Loaded data for {len(unique_keys)} callsigns.") # Special caches for spots and alerts, which have TTL and write snapshots to disk at an interval. We - # specifically load these caches *last* so that any sigref and callsign data is already loaded from disk cache - # before the spots and alerts are live in the system. + # specifically load these caches *last* so that any activity ref and callsign data is already loaded from disk + # cache before the spots and alerts are live in the system. self.spots = LiveDataCache( maxsize=self._MAX_SPOT_COUNT, ttl=MAX_SPOT_AGE, @@ -116,7 +116,7 @@ class DataStore: self.solar_conditions.close() self.status.close() self.dxcc_data.close() - self.sigrefs.close() + self.activity_refs.close() self.callsign_data_countryfiles.close() self.callsign_data_clublogxml.close() self.callsign_data_clublogapi.close() diff --git a/core/enums.py b/core/enums.py index c3e1b9a..5eda1d5 100644 --- a/core/enums.py +++ b/core/enums.py @@ -90,8 +90,8 @@ class LocationSourceForCallsign(str, Enum): DXCC = "DXCC" -class SIGRefType(str, Enum): - """Type of a Special Interest Group reference.""" +class ActivityRefType(str, Enum): + """Type of an activity reference.""" PARK = "PARK" SUMMIT = "SUMMIT" @@ -121,8 +121,8 @@ class AlertType(str, Enum): CONTEST = "CONTEST" -class SIGType(str, Enum): - """Type of a Special Interest Group. Used to group them in the web UI.""" +class ActivityType(str, Enum): + """Type of an activity. Used to group them in the web UI.""" WORLDWIDE = "WORLDWIDE" REGIONAL = "REGIONAL" diff --git a/core/sig_lookup_helper.py b/core/sig_lookup_helper.py deleted file mode 100644 index 189ed6e..0000000 --- a/core/sig_lookup_helper.py +++ /dev/null @@ -1,153 +0,0 @@ -import logging -import re - -from pyhamtools.locator import latlong_to_locator, locator_to_latlong - -from core.constants import SIGS -from core.data_store import DATA_STORE -from core.enums import SIGRefType -from core.geo_utils import wab_wai_square_to_lat_lon -from data.sig_ref import SIGRef - -logger = logging.getLogger(__name__) - - -def get_sig_ref_info(sig_name, ref_id): - """Look up details of a SIG reference (e.g. POTA park) such as name, lat/lon, and grid. Takes in a sig name and - a reference ID (both strings) and returns a SigRef object populated with as much data as we can find. This makes - use of SIG ref data in the data store, live lookups from the web, or just automatic calculation depending on which - SIG we are getting data for.""" - - if sig_name is None or sig_name == "" or ref_id is None or ref_id == "": - logger.debug("Failed to look up sig_ref info, sig or ref were not set.") - return None - - # Sometimes we allow spaces instead of dashes in references due to common usage that way, but official reference - # lists never do, so convert them here. - ref_id = ref_id.replace(" ", "-") - - # Prepare the object to be returned - sig_ref = SIGRef(sig=sig_name, id=ref_id) - - # We can always get the reference type and the icon from the SIG itself - for sig in SIGS: - if sig.name.upper() == sig_name.upper(): - sig_ref.ref_type = sig.ref_type - sig_ref.icon = sig.icon - - try: - ### FUDGES ### - # - # DME fudge. Our database has leading zeros padding to 5 digits which is the expected format, but not all - # activators add leading zeros. We also need to normalise "DME 01234" to "DME-01234" to match what's in our - # database. - if sig_name.upper() == "DME": - match = re.match(r"DME[\- ](\d{3,5})", ref_id, re.IGNORECASE) - if match: - number = match.group(1) - ref_id = f"DME-{number.zfill(5)}" - - # DTMBA spotters sometimes include spaces and dashes, our regex allows them but they must be removed here so we - # can look up against the official list which doesn't have them - if sig_name.upper() == "DTMBA": - ref_id = ref_id.replace("-", "").replace(" ", "") - - ### NO DATA SIGS ### - # - # If the SIG is HEMA or BIWOTA, we have no way to either generate useful data or look it up on a reference list, - # so just skip the lookup here. - if sig_name.upper() == "HEMA" or sig_name.upper() == "BIWOTA": - return sig_ref - - ### PROGRAMMATIC DATA GENERATION INSTEAD OF LOOKUPS ### - # - # If the SIG is Tiles, WAB, WAI or BOTA (Beaches), we don't have anything to look up from the data store, we can - # calculate all the information we are going to get directly. - if sig_name.upper() == "TILES": - # Tiles on the Air just uses Maidenhead 6-digit squares, so ID, Name and Grid are all the same - if not sig_ref.name: - sig_ref.name = sig_ref.id - if not sig_ref.grid: - sig_ref.grid = sig_ref.id - if sig_ref.grid and (not sig_ref.latitude or not sig_ref.longitude): - ll = locator_to_latlong(str(sig_ref.grid)) - sig_ref.latitude = ll[0] - sig_ref.longitude = ll[1] - return sig_ref - - elif sig_name.upper() == "WAB" or sig_name.upper() == "WAI": - ll = wab_wai_square_to_lat_lon(ref_id) - if ll: - sig_ref.name = ref_id - try: - sig_ref.grid = latlong_to_locator(ll[0], ll[1], 6) - sig_ref.latitude = ll[0] - sig_ref.longitude = ll[1] - except Exception: - logger.warning("Invalid lat/lon received for WAB/WAI reference") - return sig_ref - - elif sig_name.upper() == "BOTA": - # For BOTA all we can ever generate is the URL, there is no data file or lookup for lat/longs - if not sig_ref.name: - sig_ref.name = sig_ref.id - if sig_ref.name: - sig_ref.url = f"https://www.beachesontheair.com/beaches/{sig_ref.name.lower().replace(' ', '-')}" - return sig_ref - - elif sig_name.upper() == "GMA Islands": - # GMA Islands is a bit of a mess of GMA and IOTA references. Try looking them both up and see what returns - # the best result. - iota_lookup = get_sig_ref_info("IOTA", ref_id) - gma_lookup = get_sig_ref_info("GMA", ref_id) - for key, value in iota_lookup.__dict__.items(): - if value is not None and sig_ref.__dict__.get(key) is None: - sig_ref.__dict__[key] = value - for key, value in gma_lookup.__dict__.items(): - if value is not None and sig_ref.__dict__.get(key) is None: - sig_ref.__dict__[key] = value - sig_ref.ref_type = SIGRefType.ISLAND - return sig_ref - - ### ACTUAL LOOKUP ### - # - # OK, this is something we have to look up. Now check to see if our data store contains reference data and if - # so, copy the data into the sig_ref object - key = f"{sig_name}:{ref_id}" - try: - lookup_data = DATA_STORE.sigrefs.get(key) if key in DATA_STORE.sigrefs else None - if lookup_data: - for attr, value in lookup_data.__dict__.items(): - if value is not None and sig_ref.__dict__.get(attr) is None: - sig_ref.__dict__[attr] = value - - else: - # Maybe a super new reference we don't know about yet, but more likely a typo or a test reference, - # just silently ignore it. - logger.debug(f"{sig_name} database did not contain data for ref {ref_id}") - - except (ValueError, KeyError): - # Catch exceptions due to e.g. old versions of objects in the cache that are no longer compatible, - # and remove them from the cache. - del DATA_STORE.sigrefs[key] - return None - - except Exception: - logger.exception(f"Exception when looking up sig_ref info for {sig_name} ref {ref_id}") - return sig_ref - - -def populate_missing_sig_ref_info(sig_ref): - """Look up details of a SIG reference (e.g. POTA park) such as name, lat/lon, and grid. Takes in a sig_ref object - which must at minimum have a "sig" and an "id". The rest of the object will be populated and returned. Any data - currently in the object will be kept, only missing data in the object will be populated if it can be determined.""" - - lookup_data = get_sig_ref_info(sig_ref.sig, sig_ref.id) - - if lookup_data: - # Copy new sig ref data into existing object where data was previously missing - for key, value in lookup_data.__dict__.items(): - if value is not None and sig_ref.__dict__.get(key) is None: - sig_ref.__dict__[key] = value - - return sig_ref diff --git a/core/sig_utils.py b/core/sig_utils.py deleted file mode 100644 index 46f2897..0000000 --- a/core/sig_utils.py +++ /dev/null @@ -1,24 +0,0 @@ -from core.constants import SIGS - - -def get_ref_regex_for_sig(sig): - """Utility function to get the regex string for a SIG reference for a named SIG. If no match is found, None will be returned.""" - - for s in SIGS: - if s.name.upper() == sig.upper(): - return s.ref_regex - return None - - -def get_sig_name_from_comment_name(sig): - """Utility function to get the name of a SIG from its "comment name". Generally these will be the same but there are - some cases (e.g. is "TOTA" Towers, Tiles or Toilets?) where we need to transform one to the other.""" - - for s in SIGS: - if any(n.upper() == sig.upper() for n in s.comment_names): - return s.name - return None - - -# Regex matching any SIG's "comment name", i.e. how it may be referred to in spot comments -ANY_SIG_REGEX = rf"({'|'.join(n for s in SIGS for n in s.comment_names)})" diff --git a/core/single_object_data_cache.py b/core/single_object_data_cache.py index 66b1d7f..5993060 100644 --- a/core/single_object_data_cache.py +++ b/core/single_object_data_cache.py @@ -7,10 +7,10 @@ logger = logging.getLogger(__name__) class SingleObjectDataCache: - """Cache for status and solar conditions. This uses DiskCache, but unlike the standard DiskCache users like SIG and - callsign lookup handlers, status and solar conditions are persisted as a single object. If we just load the object - from DiskCache and modify it, DiskCache doesn't know that it's been updated and needs re-caching, so we provide a - store() method that any functions updating the object can call afterwards.""" + """Cache for status and solar conditions. This uses DiskCache, but unlike the standard DiskCache users like + activity ref and callsign lookup handlers, status and solar conditions are persisted as a single object. If we + just load the object from DiskCache and modify it, DiskCache doesn't know that it's been updated and needs + re-caching, so we provide a store() method that any functions updating the object can call afterwards.""" def __init__(self, cache_dir, object_if_empty): """Initialize a SingleObjectDataCache. Provide the directory to load the cache from and save it to. If the cache diff --git a/data/activity.py b/data/activity.py new file mode 100644 index 0000000..4fd0ca0 --- /dev/null +++ b/data/activity.py @@ -0,0 +1,41 @@ +from dataclasses import dataclass, field + +from core.enums import ActivityRefType, ActivityType + + +@dataclass +class Activity: + """Data class that defines an Activity (formerly referred to as a "Special Interest Group" or "SIG", a term + which is still used for the `sig` field name in the API for backwards compatibility). Each contains a name and + a longer form description. They also contain comment_names which attempts to separate out the way people might + refer to it in cluster comments from how it is referred to in the UI & API. (For example, "TOTA" in cluster + spot comments almost always means Towers on the Air, but no single programme is referred to in the UI as "TOTA" + as it's ambiguous between Towers, Toilets and Tiles. And while Beaches got the name "BOTA" first, "BOTA" spots + are much more likely to be bunkers.) Finally, there is a ref_regex which provides a regular expression to + match what references (such as parks and summits) look like for that programme.""" + + # Activity name as used in the UI and API, e.g. "Towers" + name: str + # Description, e.g. "Towers on the Air" + description: str + # Type, either Worldwide, Regional or Event. Used for sorting in the web UI. + # Note: this field is still named "sig_type" in the API for backwards compatibility. + sig_type: ActivityType + # Identifies that the activity's reference ID structure defined by its regex is unique across all programmes and + # anything else we expect a user to put in a spot comment, and therefore we can pull references out of + # spot comments without also needing to see the activity name first. For example, "OHFF-1234" or "B/G-1234" are + # obviously WWFF and WWBOTA, nothing else looks like those. But "SZ09" could be WAB or Tiles, "GB1234" could + # conceivably be POTA or ILLW, etc. + refs_globally_unique: bool + # Activity names as they might appear in cluster spot comments, e.g. ["TOTA"] + comment_names: list[str] = field(default_factory=list) + # Reference type, what gets activated e.g. Park, Summit. May be None if the activity is for multiple types of + # things, in which case the spot data will have to provide this instead. + ref_type: ActivityRefType | None = None + # Regex matcher for references, e.g. for POTA r"[A-Z]{2}\-\d+". + ref_regex: str | None = None + # Icon to use in the UI when referencing this activity. Chosen from the Font Awesome set. + icon: str | None = None + # Emoji flag for the country or region where this activity is relevant, if any. If None, this implies the + # activity is in worldwide usage. + region_flag: str | None = None diff --git a/data/sig_ref.py b/data/activity_ref.py similarity index 66% rename from data/sig_ref.py rename to data/activity_ref.py index d5fcb00..d766c8d 100644 --- a/data/sig_ref.py +++ b/data/activity_ref.py @@ -1,24 +1,24 @@ from dataclasses import dataclass -from core.enums import SIGRefType +from core.enums import ActivityRefType @dataclass -class SIGRef: - """Data class that defines a Special Interest Group "info" or reference. As well as the basic reference ID we include a +class ActivityRef: + """Data class that defines an Activity "info" or reference. As well as the basic reference ID we include a name and a lookup URL.""" - # SIG that this reference is in, e.g. "POTA". + # Activity that this reference is in, e.g. "POTA". Still named "sig" for backwards compatibility with the API. sig: str # Reference ID, e.g. "GB-0001". id: str | None = None # Name of the reference, e.g. "Null Country Park", if known. name: str | None = None # Type of the reference, e.g. "Park", if known. - ref_type: SIGRefType | None = None + ref_type: ActivityRefType | None = None # URL to look up more information about the reference, if known. url: str | None = None - # Icon to use for the reference, derived from the SIG. Chosen from the Font Awesome set. + # Icon to use for the reference, derived from the activity. Chosen from the Font Awesome set. icon: str | None = None # Latitude of the reference, in degrees, if known. latitude: float | None = None diff --git a/data/alert.py b/data/alert.py index 76df2b6..4086df2 100644 --- a/data/alert.py +++ b/data/alert.py @@ -6,9 +6,9 @@ from datetime import datetime, timedelta import pytz +from core.activity_lookup_helper import populate_missing_activity_ref_info from core.call_lookup_helper import get_call_info from core.enums import AlertType, Continent -from core.sig_lookup_helper import populate_missing_sig_ref_info from core.utils import get_flag_for_dxcc logger = logging.getLogger(__name__) @@ -59,11 +59,12 @@ class Alert: # A URL link to more information, if any url: str | None = None - # Special Interest Group info + # Activity info - # Special Interest Group (SIG), e.g. outdoor activity programme such as POTA + # Activity (e.g. outdoor activity programme such as POTA). Still named "sig" for API backwards compatibility. sig: str | None = None - # SIG references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO + # Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO. Still named + # "sig_refs" for API backwards compatibility. sig_refs: list = field(default_factory=list) # Timing info @@ -125,15 +126,15 @@ class Alert: if self.dx_dxcc_id and not self.dx_flag: self.dx_flag = get_flag_for_dxcc(self.dx_dxcc_id) - # Fetch SIG data. In case a particular API doesn't provide a full set of name, lat, lon & grid for a reference - # in its initial call, we use this code to populate the rest of the data. This includes working out grid refs - # from WAB and WAI, which count as a SIG even though there's no real lookup, just maths + # Fetch activity data. In case a particular API doesn't provide a full set of name, lat, lon & grid for a + # reference in its initial call, we use this code to populate the rest of the data. This includes working + # out grid refs from WAB and WAI, which count as an activity even though there's no real lookup, just maths if self.sig_refs: - for sig_ref in self.sig_refs: - populate_missing_sig_ref_info(sig_ref) + for activity_ref in self.sig_refs: + populate_missing_activity_ref_info(activity_ref) - # If the spot itself doesn't have a SIG yet, but we have at least one SIG reference, take that reference's SIG - # and apply it to the whole spot. + # If the spot itself doesn't have an activity yet, but we have at least one activity reference, take that + # reference's activity and apply it to the whole spot. if self.sig_refs and self.sig_refs[0] and not self.sig: self.sig = self.sig_refs[0].sig @@ -152,7 +153,7 @@ class Alert: if self.dx_calls and not self.dx_names: self.dx_names = [get_call_info(c, credentials).name for c in self.dx_calls] - # Icon for the spot should be the icon of the first SIG ref if present, otherwise a radio tower + # Icon for the spot should be the icon of the first activity ref if present, otherwise a radio tower self.icon = "fa-tower-cell" if self.alert_type == AlertType.DXPEDITION: self.icon = "fa-globe-africa" diff --git a/data/callsign.py b/data/callsign.py index 83b4950..6bfac66 100644 --- a/data/callsign.py +++ b/data/callsign.py @@ -7,8 +7,8 @@ from core.enums import Continent, LocationSourceForCallsign class Callsign: """Data class that defines a callsign and the data associated with it. This will have been retrieved by a callsign lookup provider using data files or online lookup. This can be used to infer missing data for a spot, though if the - spot has a SIG (e.g. POTA) reference this data for their home location (or even just their country) will be less - accurate and should not be used in preference to that.""" + spot has an activity (e.g. POTA) reference this data for their home location (or even just their country) will be + less accurate and should not be used in preference to that.""" # Callsign as spotted call: str diff --git a/data/sig.py b/data/sig.py deleted file mode 100644 index 743578f..0000000 --- a/data/sig.py +++ /dev/null @@ -1,39 +0,0 @@ -from dataclasses import dataclass, field - -from core.enums import SIGRefType, SIGType - - -@dataclass -class SIG: - """Data class that defines a Special Interest Group. Each contains a name and a longer form description. - They also contain comment_names which attempts to separate out the way people might refer to it in - cluster comments from how it is referred to in the UI & API. (For example, "TOTA" in cluster spot comments - almost always means Towers on the Air, but no single programme is referred to in the UI as "TOTA" as - it's ambiguous between Towers, Toilets and Tiles. And while Beaches got the name "BOTA" first, "BOTA" spots - are much more likely to be bunkers.) Finally, there is a ref_regex which provides a regular expression to - match what references (such as parks and summits) look like for that programme.""" - - # SIG name as used in the UI and API, e.g. "Towers" - name: str - # Description, e.g. "Towers on the Air" - description: str - # Type, either Worldwide, Regional or Event. Used for sorting in the web UI. - sig_type: SIGType - # Identifies that the SIG's reference ID structure defined by its regex is unique across all programmes and - # anything else we expect a user to put in a spot comment, and therefore we can pull references out of - # spot comments without also needing to see the SIG name first. For example, "OHFF-1234" or "B/G-1234" are - # obviously WWFF and WWBOTA, nothing else looks like those. But "SZ09" could be WAB or Tiles, "GB1234" could - # conceivably be POTA or ILLW, etc. - refs_globally_unique: bool - # SIG names as they might appear in cluster spot comments, e.g. ["TOTA"] - comment_names: list[str] = field(default_factory=list) - # Reference type, what gets activated e.g. Park, Summit. May be None if the SIG is for multiple types of things, in - # which case the spot data will have to provide this instead. - ref_type: SIGRefType | None = None - # Regex matcher for references, e.g. for POTA r"[A-Z]{2}\-\d+". - ref_regex: str | None = None - # Icon to use in the UI when referencing this SIG. Chosen from the Font Awesome set. - icon: str | None = None - # Emoji flag for the country or region where this SIG is relevant, if any. If None, this implies the SIG is in - # worldwide usage. - region_flag: str | None = None diff --git a/data/spot.py b/data/spot.py index c4009ec..0a27103 100644 --- a/data/spot.py +++ b/data/spot.py @@ -9,18 +9,18 @@ from math import isnan import pytz from pyhamtools.locator import latlong_to_locator, locator_to_latlong +from core.activity_lookup_helper import populate_missing_activity_ref_info +from core.activity_utils import ( + ANY_ACTIVITY_REGEX, + get_activity_name_from_comment_name, + get_ref_regex_for_activity, +) from core.call_lookup_helper import get_call_info from core.config import MAX_SPOT_AGE -from core.constants import PROPAGATION_MODES, SIGS +from core.constants import ACTIVITIES, PROPAGATION_MODES from core.data_store import DATA_STORE from core.enums import Continent, LocationSourceForSpot, Mode, ModeSource, ModeType from core.geo_utils import lat_lon_to_cq_zone, lat_lon_to_itu_zone -from core.sig_lookup_helper import populate_missing_sig_ref_info -from core.sig_utils import ( - ANY_SIG_REGEX, - get_ref_regex_for_sig, - get_sig_name_from_comment_name, -) from core.utils import ( get_flag_for_dxcc, infer_band_from_freq, @@ -28,7 +28,7 @@ from core.utils import ( infer_mode_from_frequency, infer_mode_type_from_mode, ) -from data.sig_ref import SIGRef +from data.activity_ref import ActivityRef logger = logging.getLogger(__name__) @@ -46,8 +46,8 @@ class Spot: dx_call: str | None = None # Name of the operator that has been spotted dx_name: str | None = None - # QTH of the operator that has been spotted. This could be from any SIG refs or could be from online lookup of their - # home QTH. + # QTH of the operator that has been spotted. This could be from any activity refs or could be from online lookup of + # their home QTH. dx_qth: str | None = None # Country of the DX operator dx_country: str | None = None @@ -73,8 +73,8 @@ class Spot: # DX Location source. Indicates how accurate the location might be. dx_location_source: LocationSourceForSpot | None = None # DX Location good. Indicates that the software thinks the location data is good enough to plot on a map. This is - # true if the location source is "SPOT", "SIG REF LOOKUP" or "GRID", or if the location source is "HOME QTH" and the - # DX callsign doesn't have a suffix like /P. + # true if the location source is "SPOT", "SIG REF LOOKUP" or "GRID", or if the location source is "HOME QTH" and + # the DX callsign doesn't have a suffix like /P. (Location source retains "SIG" wording for API compatibility.) dx_location_good: bool = False # DE (Spotter) info @@ -120,11 +120,12 @@ class Spot: # QRT state. Some APIs return spots marked as QRT. Otherwise we can check the comments. qrt: bool = False - # Special Interest Group info + # Activity info - # Special Interest Group (SIG), e.g. outdoor activity programme such as POTA + # Activity (e.g. outdoor activity programme such as POTA). Still named "sig" for API backwards compatibility. sig: str | None = None - # SIG references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO + # Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO. Still named + # "sig_refs" for API backwards compatibility. sig_refs: list = field(default_factory=list) # Timing info @@ -158,7 +159,10 @@ class Spot: objects such as the sig_refs list..""" if self.sig_refs: - self.sig_refs = [sig_ref if isinstance(sig_ref, SIGRef) else SIGRef(**sig_ref) for sig_ref in self.sig_refs] + self.sig_refs = [ + activity_ref if isinstance(activity_ref, ActivityRef) else ActivityRef(**activity_ref) + for activity_ref in self.sig_refs + ] def infer_missing(self, credentials=None): """Infer missing parameters where possible""" @@ -262,76 +266,89 @@ class Spot: if self.dx_latitude or self.dx_grid: self.dx_location_source = LocationSourceForSpot.SPOT - # Set the top-level "SIG" if it is missing but we have at least one SIG ref. + # Set the top-level activity if it is missing but we have at least one activity ref. if not self.sig and self.sig_refs: self.sig = self.sig_refs[0].sig.upper() - # See if we already have a SIG reference, but the comment looks like it contains more for the same SIG. This - # should catch e.g. POTA comments like "2-fer: GB-0001 GB-0002". + # See if we already have an activity reference, but the comment looks like it contains more for the same + # activity. This should catch e.g. POTA comments like "2-fer: GB-0001 GB-0002". if self.comment and self.sig_refs and self.sig_refs[0].sig: - sig = self.sig_refs[0].sig.upper() - regex = get_ref_regex_for_sig(sig) + activity = self.sig_refs[0].sig.upper() + regex = get_ref_regex_for_activity(activity) if regex: all_comment_ref_matches = re.finditer(r"(^|\W)(" + regex + r")($|\W)", self.comment, re.IGNORECASE) for ref_match in all_comment_ref_matches: - self._append_sig_ref_if_missing(SIGRef(id=ref_match.group(2).upper(), sig=sig)) + self._append_activity_ref_if_missing(ActivityRef(id=ref_match.group(2).upper(), sig=activity)) - # See if the comment looks like it contains any SIGs (and optionally SIG references) that we can - # add to the spot. This should catch cluster spot comments like "POTA GB-0001 WWFF GFF-0001" and e.g. POTA - # comments like "also WWFF GFF-0001". + # See if the comment looks like it contains any activities (and optionally activity references) that we + # can add to the spot. This should catch cluster spot comments like "POTA GB-0001 WWFF GFF-0001" and e.g. + # POTA comments like "also WWFF GFF-0001". if self.comment: - sig_matches = re.finditer(r"(^|\W)" + ANY_SIG_REGEX + r"($|\W)", self.comment, re.IGNORECASE) - for sig_match in sig_matches: - # First of all, if we haven't got a SIG for this spot set yet, now we have. This covers things like cluster - # spots where the comment is just "POTA". - found_sig = get_sig_name_from_comment_name(sig_match.group(2)) + activity_matches = re.finditer(r"(^|\W)" + ANY_ACTIVITY_REGEX + r"($|\W)", self.comment, re.IGNORECASE) + for activity_match in activity_matches: + # First of all, if we haven't got an activity for this spot set yet, now we have. This covers + # things like cluster spots where the comment is just "POTA". + found_activity = get_activity_name_from_comment_name(activity_match.group(2)) if not self.sig: - self.sig = found_sig + self.sig = found_activity - # Now look to see if that SIG name was followed by something that looks like a reference ID for that SIG. - # If so, add that to the sig_refs list for this spot. - ref_regex = get_ref_regex_for_sig(found_sig) + # Now look to see if that activity name was followed by something that looks like a reference ID + # for that activity. If so, add that to the sig_refs list for this spot. + ref_regex = get_ref_regex_for_activity(found_activity) if ref_regex: ref_matches = re.finditer( - r"(^|\W)" + found_sig + r"([ -])(" + ref_regex + r")($|\W)", + r"(^|\W)" + found_activity + r"([ -])(" + ref_regex + r")($|\W)", self.comment, re.IGNORECASE, ) for ref_match in ref_matches: - self._append_sig_ref_if_missing(SIGRef(id=ref_match.group(3).upper(), sig=found_sig)) + self._append_activity_ref_if_missing( + ActivityRef(id=ref_match.group(3).upper(), sig=found_activity) + ) - # See if the comment looks like it contains any SIG references *without* the corresponding SIG name, but - # where the SIG reference is unique-looking enough that we can't confuse it with any other SIG. + # See if the comment looks like it contains any activity references *without* the corresponding activity + # name, but where the activity reference is unique-looking enough that we can't confuse it with any other + # activity. if self.comment: - for sig in SIGS: - if sig.refs_globally_unique and sig.ref_regex: - ref_matches = re.finditer(r"(^|\W)(" + sig.ref_regex + r")($|\W)", self.comment, re.IGNORECASE) + for activity in ACTIVITIES: + if activity.refs_globally_unique and activity.ref_regex: + ref_matches = re.finditer( + r"(^|\W)(" + activity.ref_regex + r")($|\W)", self.comment, re.IGNORECASE + ) for ref_match in ref_matches: - # First of all, if we haven't got a SIG for this spot set yet, now we have. This covers things - # like cluster spots where the comment is just "OHFF-1234", now we know it's WWFF. + # First of all, if we haven't got an activity for this spot set yet, now we have. This + # covers things like cluster spots where the comment is just "OHFF-1234", now we know + # it's WWFF. if not self.sig: - self.sig = sig.name - self._append_sig_ref_if_missing(SIGRef(id=ref_match.group(2).upper(), sig=sig.name)) + self.sig = activity.name + self._append_activity_ref_if_missing( + ActivityRef(id=ref_match.group(2).upper(), sig=activity.name) + ) - # Fetch SIG data. In case a particular API doesn't provide a full set of name, lat, lon & grid for a reference - # in its initial call, we use this code to populate the rest of the data. This includes working out grid refs - # from WAB and WAI, which count as a SIG even though there's no real lookup, just maths + # Fetch activity data. In case a particular API doesn't provide a full set of name, lat, lon & grid for a + # reference in its initial call, we use this code to populate the rest of the data. This includes working + # out grid refs from WAB and WAI, which count as an activity even though there's no real lookup, just maths if self.sig_refs: - for sig_ref in self.sig_refs: - sig_ref = populate_missing_sig_ref_info(sig_ref) - # If the spot itself doesn't have location yet, but the SIG ref does, extract it - if sig_ref.grid and not self.dx_grid: - self.dx_grid = sig_ref.grid - if sig_ref.latitude and not self.dx_latitude and sig_ref.longitude and not self.dx_longitude: - self.dx_latitude = sig_ref.latitude - self.dx_longitude = sig_ref.longitude + for activity_ref in self.sig_refs: + activity_ref = populate_missing_activity_ref_info(activity_ref) + # If the spot itself doesn't have location yet, but the activity ref does, extract it + if activity_ref.grid and not self.dx_grid: + self.dx_grid = activity_ref.grid + if ( + activity_ref.latitude + and not self.dx_latitude + and activity_ref.longitude + and not self.dx_longitude + ): + self.dx_latitude = activity_ref.latitude + self.dx_longitude = activity_ref.longitude if self.sig == "WAB" or self.sig == "WAI" or self.sig == "Tiles": self.dx_location_source = LocationSourceForSpot.GRID else: self.dx_location_source = LocationSourceForSpot.SIG_REF_LOOKUP - # If the spot itself doesn't have a SIG yet, but we have at least one SIG reference, take that reference's SIG - # and apply it to the whole spot. + # If the spot itself doesn't have an activity yet, but we have at least one activity reference, take that + # reference's activity and apply it to the whole spot. if self.sig_refs and not self.sig: self.sig = self.sig_refs[0].sig @@ -360,17 +377,17 @@ class Spot: self.propagation_mode = mode_tag logger.info(f"Seen a new propagation mode tag not yet in the system: {mode_tag}") - # Set SIGs based on propagation mode + # Set activities based on propagation mode if self.propagation_mode == "Satellite": if not self.sig: self.sig = "AMSAT" - if not any(sig_ref.sig == "AMSAT" for sig_ref in self.sig_refs): - self.sig_refs.append(SIGRef(sig="AMSAT")) + if not any(activity_ref.sig == "AMSAT" for activity_ref in self.sig_refs): + self.sig_refs.append(ActivityRef(sig="AMSAT")) if self.propagation_mode == "Earth-Moon-Earth": if not self.sig: self.sig = "EME" - if not any(sig_ref.sig == "EME" for sig_ref in self.sig_refs): - self.sig_refs.append(SIGRef(sig="EME")) + if not any(activity_ref.sig == "EME" for activity_ref in self.sig_refs): + self.sig_refs.append(ActivityRef(sig="EME")) # Parse "de_grid -> dx_grid" structures from the comment if self.comment: @@ -426,8 +443,8 @@ class Spot: self.dx_grid = dx_call_info.grid self.dx_location_source = dx_call_info.location_source - # Determine a "QTH" string. If we have a SIG ref, pick the first one and turn it into a suitable string, - # otherwise see what they have set on an online lookup service. + # Determine a "QTH" string. If we have an activity ref, pick the first one and turn it into a suitable + # string, otherwise see what they have set on an online lookup service. if self.sig_refs: qth = self.sig_refs[0].id if self.sig_refs[0].name: @@ -482,7 +499,7 @@ class Spot: self.de_longitude = de_call_info.longitude self.de_grid = de_call_info.grid - # Icon for the spot should be the icon of the first SIG ref if present, otherwise a radio tower + # Icon for the spot should be the icon of the first activity ref if present, otherwise a radio tower self.icon = "fa-tower-cell" if self.sig_refs and self.sig_refs[0].icon: self.icon = self.sig_refs[0].icon @@ -495,17 +512,17 @@ class Spot: return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True) - def _append_sig_ref_if_missing(self, new_sig_ref): - """Append a sig_ref to the list, so long as it's not already there.""" + def _append_activity_ref_if_missing(self, new_activity_ref): + """Append an activity ref to the list, so long as it's not already there.""" - new_sig_ref.id = new_sig_ref.id.strip().upper() - new_sig_ref.sig = new_sig_ref.sig.strip().upper() - if new_sig_ref.id == "": + new_activity_ref.id = new_activity_ref.id.strip().upper() + new_activity_ref.sig = new_activity_ref.sig.strip().upper() + if new_activity_ref.id == "": return - for sig_ref in self.sig_refs: - if sig_ref.id == new_sig_ref.id and sig_ref.sig == new_sig_ref.sig: + for activity_ref in self.sig_refs: + if activity_ref.id == new_activity_ref.id and activity_ref.sig == new_activity_ref.sig: return - self.sig_refs.append(new_sig_ref) + self.sig_refs.append(new_activity_ref) def expired(self): """Decide if this spot has expired (in which case it should not be added to the system in the first place, and not diff --git a/docs/embedding.md b/docs/embedding.md index 506adf2..9278820 100644 --- a/docs/embedding.md +++ b/docs/embedding.md @@ -5,7 +5,7 @@ You can embed Spothole's web interface in another website, e.g. for use as part URL parameters can be used to trigger an "embedded" mode which hides the headers, footers and settings. In this mode, you provide configuration for the various filter and display options via additional URL parameters. Any settings that the user has set for Spothole are ignored. This is so that the embedding site can select, for example, their choice of -dark mode or SIG filters, which will not impact how Spothole appears when the user accesses it directly. Effectively, it +dark mode or activity filters, which will not impact how Spothole appears when the user accesses it directly. Effectively, it becomes separate to their normal Spothole settings. Setting `embedded` to true is important for the rest of the settings to be applied; otherwise, the user's defaults will @@ -29,7 +29,7 @@ a mapping exists. | `limit` | 25, 50, 100, 200, 500 | 100 | `?limit=100` | Sets the number of alerts that will be displayed on the alerts page | | `max_age` | 300, 600, 1800, 3600 | 1800 | `?max_age=1800` | Sets the maximum age of spots displayed on the map and bands pages, in seconds. | | `band` | Comma-separated list | (all) | `?band=20m,40m` | Sets the list of bands that will be shown on the spots, bands and map pages. Available options match the labels of the buttons in the standard web interface. | -| `sig` | Comma-separated list | (all) | `?sig=POTA,SOTA,NO_SIG` | Sets the list of SIGs that will be shown on the spots, bands and map pages. Available options match the labels of the buttons in the standard web interface. | +| `sig` | Comma-separated list | (all) | `?sig=POTA,SOTA,NO_SIG` | Sets the list of activities that will be shown on the spots, bands and map pages. Available options match the labels of the buttons in the standard web interface. | | `source` | Comma-separated list | (all) | `?source=Cluster` | Sets the list of sources that will be shown on any spot or alert pages. Available options match the labels of the buttons in the standard web interface. | | `mode_type` | Comma-separated list | (all) | `?mode_type=PHONE,CW` | Sets the list of mode types that will be shown on the spots, bands and map pages. Available options match the labels of the buttons in the standard web interface. | | `dx_continent` | Comma-separated list | (all) | `?dx_continent=NA,SA` | Sets the list of DX Continents that will be shown on any spot or alert pages. Available options match the labels of the buttons in the standard web interface. | diff --git a/docs/modifying.md b/docs/modifying.md index 62326e9..8336215 100644 --- a/docs/modifying.md +++ b/docs/modifying.md @@ -18,8 +18,8 @@ To navigate your way around the source code, this list may help. services * `/providers/callsign` - Classes providing callsign lookup data by accessing bundled data files or the APIs of other services -* `/providers/sigrefdata` - Classes providing SIG reference lookup data by accessing bundled data files or the APIs of - other services +* `/providers/activityrefdata` - Classes providing activity reference lookup data by accessing bundled data files or + the APIs of other services * `/webserver` - Classes for running Spothole's own web server * `/telnetserver` - Classes for running Spothole's telnet server * `spothole.py` - Main application script diff --git a/providers/sigrefdata/sig_ref_data_provider.py b/providers/activityrefdata/activity_ref_data_provider.py similarity index 79% rename from providers/sigrefdata/sig_ref_data_provider.py rename to providers/activityrefdata/activity_ref_data_provider.py index bf128a9..ec9550a 100644 --- a/providers/sigrefdata/sig_ref_data_provider.py +++ b/providers/activityrefdata/activity_ref_data_provider.py @@ -9,11 +9,13 @@ from core.data_store import DATA_STORE logger = logging.getLogger(__name__) -class SIGRefDataProvider: - """Generic SIG reference data provider class. Subclasses of this query the individual URLs or files for data.""" +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""" + """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"] @@ -37,9 +39,9 @@ class SIGRefDataProvider: """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.sigrefs.transact(): + with DATA_STORE.activity_refs.transact(): for d in new_data: - DATA_STORE.sigrefs.set(f"{self.sig_name}:{d.id}", d) + 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 diff --git a/providers/sigrefdata/arlhs.py b/providers/activityrefdata/arlhs.py similarity index 67% rename from providers/sigrefdata/arlhs.py rename to providers/activityrefdata/arlhs.py index 972a018..c7e52c0 100644 --- a/providers/sigrefdata/arlhs.py +++ b/providers/activityrefdata/arlhs.py @@ -1,22 +1,22 @@ import csv from time import sleep -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.file_download_sig_ref_data_provider import ( - FileDownloadSIGRefDataProvider, +from core.enums import ActivityRefType +from data.activity_ref import ActivityRef +from providers.activityrefdata.file_download_activity_ref_data_provider import ( + FileDownloadActivityRefDataProvider, ) -class ARLHS(FileDownloadSIGRefDataProvider): - """SIG ref data provider for Amateur Radio Light House Society""" +class ARLHS(FileDownloadActivityRefDataProvider): + """Activity ref data provider for Amateur Radio Light House Society""" POLL_INTERVAL_DAYS = 30 - SIG = "ARLHS" + ACTIVITY = "ARLHS" DATA_URL = "https://www.gma.rocks/download/lighthouse.csv" def __init__(self, provider_config): - super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) + super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) def _http_response_to_data(self, http_response): new_data = [] @@ -24,11 +24,11 @@ class ARLHS(FileDownloadSIGRefDataProvider): if "ARLHS" in row and row["ARLHS"] != "": ref_id = row["ARLHS"] new_data.append( - SIGRef( - sig=self.SIG, + ActivityRef( + sig=self.ACTIVITY, id=ref_id, name=row.get("Name", None), - ref_type=SIGRefType.LIGHTHOUSE, + 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, @@ -41,7 +41,7 @@ class ARLHS(FileDownloadSIGRefDataProvider): if self._stop_event.is_set(): break - # Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time + # 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) diff --git a/providers/sigrefdata/cota.py b/providers/activityrefdata/cota.py similarity index 67% rename from providers/sigrefdata/cota.py rename to providers/activityrefdata/cota.py index 37c0ecc..b41cb92 100644 --- a/providers/sigrefdata/cota.py +++ b/providers/activityrefdata/cota.py @@ -2,20 +2,20 @@ from time import sleep from pyhamtools.locator import latlong_to_locator -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider +from core.enums import ActivityRefType +from data.activity_ref import ActivityRef +from providers.activityrefdata.file_download_activity_ref_data_provider import FileDownloadActivityRefDataProvider -class COTA(FileDownloadSIGRefDataProvider): - """SIG ref data provider for Castles on the Air""" +class COTA(FileDownloadActivityRefDataProvider): + """Activity ref data provider for Castles on the Air""" POLL_INTERVAL_DAYS = 30 - SIG = "COTA" + ACTIVITY = "COTA" DATA_URL = "https://www.cotagroup.org/cotagroup/map/data/castles-all-7d90ee2a5e1175e5dece1bbf9dc87504.json" def __init__(self, provider_config): - super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) + super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) def _http_response_to_data(self, http_response): new_data = [] @@ -29,11 +29,11 @@ class COTA(FileDownloadSIGRefDataProvider): grid = latlong_to_locator(lat, lon) new_data.append( - SIGRef( - sig=self.SIG, + ActivityRef( + sig=self.ACTIVITY, id=ref_id, name=name, - ref_type=SIGRefType.CASTLE, + ref_type=ActivityRefType.CASTLE, grid=grid, latitude=lat, longitude=lon, @@ -45,7 +45,7 @@ class COTA(FileDownloadSIGRefDataProvider): if self._stop_event.is_set(): break - # Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time + # 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) diff --git a/providers/sigrefdata/dce.py b/providers/activityrefdata/dce.py similarity index 60% rename from providers/sigrefdata/dce.py rename to providers/activityrefdata/dce.py index 8046687..53c817b 100644 --- a/providers/sigrefdata/dce.py +++ b/providers/activityrefdata/dce.py @@ -3,20 +3,20 @@ from time import sleep import pandas as pd -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider +from core.enums import ActivityRefType +from data.activity_ref import ActivityRef +from providers.activityrefdata.file_download_activity_ref_data_provider import FileDownloadActivityRefDataProvider -class DCE(FileDownloadSIGRefDataProvider): - """SIG ref data provider for Diploma Castillos de España""" +class DCE(FileDownloadActivityRefDataProvider): + """Activity ref data provider for Diploma Castillos de España""" POLL_INTERVAL_DAYS = 365 - SIG = "DCE" + ACTIVITY = "DCE" DATA_URL = "https://www.acracb.org/dce/descargas/General/directorio_referencias_dce.xls" def __init__(self, provider_config): - super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) + super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) def _http_response_to_data(self, http_response): new_data = [] @@ -27,7 +27,7 @@ class DCE(FileDownloadSIGRefDataProvider): for index, row in df.iterrows(): if row.iloc[0] and row.iloc[2]: new_data.append( - SIGRef(sig=self.SIG, id=row.iloc[0].strip(), name=row.iloc[2].strip(), ref_type=SIGRefType.CASTLE) + 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 @@ -35,7 +35,7 @@ class DCE(FileDownloadSIGRefDataProvider): if self._stop_event.is_set(): break - # Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time + # 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) diff --git a/providers/sigrefdata/defe.py b/providers/activityrefdata/defe.py similarity index 62% rename from providers/sigrefdata/defe.py rename to providers/activityrefdata/defe.py index afeb05f..1ab9ad7 100644 --- a/providers/sigrefdata/defe.py +++ b/providers/activityrefdata/defe.py @@ -3,20 +3,20 @@ from time import sleep import pandas as pd -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider +from core.enums import ActivityRefType +from data.activity_ref import ActivityRef +from providers.activityrefdata.file_download_activity_ref_data_provider import FileDownloadActivityRefDataProvider -class DEFE(FileDownloadSIGRefDataProvider): - """SIG ref data provider for Diploma Estationes de Ferrocarril de España""" +class DEFE(FileDownloadActivityRefDataProvider): + """Activity ref data provider for Diploma Estationes de Ferrocarril de España""" POLL_INTERVAL_DAYS = 365 - SIG = "DEFE" + ACTIVITY = "DEFE" DATA_URL = "https://www.acracb.org/defe/descargas/General/directorio_referencias_defe.xls" def __init__(self, provider_config): - super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) + super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) def _http_response_to_data(self, http_response): new_data = [] @@ -31,7 +31,7 @@ class DEFE(FileDownloadSIGRefDataProvider): if row.iloc[0] and row.iloc[1]: new_data.append( - SIGRef(sig=self.SIG, id=row.iloc[0].strip(), name=row.iloc[1].strip(), ref_type=SIGRefType.BUILDING) + 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 @@ -39,7 +39,7 @@ class DEFE(FileDownloadSIGRefDataProvider): if self._stop_event.is_set(): break - # Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time + # 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) diff --git a/providers/sigrefdata/dme.py b/providers/activityrefdata/dme.py similarity index 75% rename from providers/sigrefdata/dme.py rename to providers/activityrefdata/dme.py index c6a8350..d551eca 100644 --- a/providers/sigrefdata/dme.py +++ b/providers/activityrefdata/dme.py @@ -3,21 +3,21 @@ from time import sleep from pyhamtools.locator import latlong_to_locator -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.local_file_sig_ref_data_provider import ( - LocalFileSIGRefDataProvider, +from core.enums import ActivityRefType +from data.activity_ref import ActivityRef +from providers.activityrefdata.local_file_activity_ref_data_provider import ( + LocalFileActivityRefDataProvider, ) -class DME(LocalFileSIGRefDataProvider): - """SIG ref data provider for Diploma Municipios de Espana""" +class DME(LocalFileActivityRefDataProvider): + """Activity ref data provider for Diploma Municipios de Espana""" - SIG = "DME" + ACTIVITY = "DME" PATH = "datafiles/MUNICIPIOS.csv" def __init__(self, provider_config): - super().__init__(self.SIG, provider_config, self.PATH) + super().__init__(self.ACTIVITY, provider_config, self.PATH) def _file_to_data(self, path): new_data = [] @@ -26,7 +26,7 @@ class DME(LocalFileSIGRefDataProvider): # 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 - # sig_lookup_helper.py. + # activity_lookup_helper.py. ref_id = "DME-" + row["COD_INE"][:5] latitude = ( float(row["LATITUD_ETRS89_REGCAN95"].replace(",", ".")) @@ -39,10 +39,10 @@ class DME(LocalFileSIGRefDataProvider): else None ) - ref = SIGRef( - sig=self.SIG, + ref = ActivityRef( + sig=self.ACTIVITY, id=ref_id, - ref_type=SIGRefType.TOWN, + ref_type=ActivityRefType.TOWN, name=f"{row['NOMBRE_ACTUAL']}, {row['PROVINCIA']}", latitude=latitude, longitude=longitude, @@ -56,7 +56,7 @@ class DME(LocalFileSIGRefDataProvider): if self._stop_event.is_set(): break - # Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time + # 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) diff --git a/providers/sigrefdata/dmue.py b/providers/activityrefdata/dmue.py similarity index 57% rename from providers/sigrefdata/dmue.py rename to providers/activityrefdata/dmue.py index 2acf4f7..65e09c8 100644 --- a/providers/sigrefdata/dmue.py +++ b/providers/activityrefdata/dmue.py @@ -1,20 +1,20 @@ import csv from time import sleep -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider +from core.enums import ActivityRefType +from data.activity_ref import ActivityRef +from providers.activityrefdata.file_download_activity_ref_data_provider import FileDownloadActivityRefDataProvider -class DMUE(FileDownloadSIGRefDataProvider): - """SIG ref data provider for Diploma Museos de España""" +class DMUE(FileDownloadActivityRefDataProvider): + """Activity ref data provider for Diploma Museos de España""" POLL_INTERVAL_DAYS = 365 - SIG = "DMUE" + ACTIVITY = "DMUE" DATA_URL = "https://dmue.radiogalena.es/nom_dmue.csv" def __init__(self, provider_config): - super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) + super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) def _http_response_to_data(self, http_response): new_data = [] @@ -22,7 +22,7 @@ class DMUE(FileDownloadSIGRefDataProvider): 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( - SIGRef(sig=self.SIG, id=row[0].strip(), name=row[1].strip(), ref_type=SIGRefType.BUILDING) + 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 @@ -30,7 +30,7 @@ class DMUE(FileDownloadSIGRefDataProvider): if self._stop_event.is_set(): break - # Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time + # 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) diff --git a/providers/sigrefdata/dmve.py b/providers/activityrefdata/dmve.py similarity index 65% rename from providers/sigrefdata/dmve.py rename to providers/activityrefdata/dmve.py index c92fbf3..e4e85a7 100644 --- a/providers/sigrefdata/dmve.py +++ b/providers/activityrefdata/dmve.py @@ -3,20 +3,20 @@ from time import sleep import pandas as pd -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider +from core.enums import ActivityRefType +from data.activity_ref import ActivityRef +from providers.activityrefdata.file_download_activity_ref_data_provider import FileDownloadActivityRefDataProvider -class DMVE(FileDownloadSIGRefDataProvider): - """SIG ref data provider for Diploma Monumentos y Vestigios de España""" +class DMVE(FileDownloadActivityRefDataProvider): + """Activity ref data provider for Diploma Monumentos y Vestigios de España""" POLL_INTERVAL_DAYS = 365 - SIG = "DMVE" + ACTIVITY = "DMVE" DATA_URL = "https://www.acracb.org/dmve/descargas/General/directorio_referencias_dmve.xls" def __init__(self, provider_config): - super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) + super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) def _http_response_to_data(self, http_response): new_data = [] @@ -36,14 +36,14 @@ class DMVE(FileDownloadSIGRefDataProvider): continue if ref and name: - new_data.append(SIGRef(sig=self.SIG, id=ref.strip(), name=name.strip(), ref_type=SIGRefType.BUILDING)) + 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 sig refs by a few seconds but will ensure some time + # 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) diff --git a/providers/sigrefdata/dtmba.py b/providers/activityrefdata/dtmba.py similarity index 56% rename from providers/sigrefdata/dtmba.py rename to providers/activityrefdata/dtmba.py index 8e52ba9..5ea2d1d 100644 --- a/providers/sigrefdata/dtmba.py +++ b/providers/activityrefdata/dtmba.py @@ -1,19 +1,19 @@ from time import sleep -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider +from core.enums import ActivityRefType +from data.activity_ref import ActivityRef +from providers.activityrefdata.file_download_activity_ref_data_provider import FileDownloadActivityRefDataProvider -class DTMBA(FileDownloadSIGRefDataProvider): - """SIG ref data provider for Diploma Teatri Musei Belle Arti""" +class DTMBA(FileDownloadActivityRefDataProvider): + """Activity ref data provider for Diploma Teatri Musei Belle Arti""" POLL_INTERVAL_DAYS = 30 - SIG = "DTMBA" + ACTIVITY = "DTMBA" DATA_URL = "https://www.iu1fig.com/share/iz0eik/dtmba/export.php" def __init__(self, provider_config): - super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) + super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) def _http_response_to_data(self, http_response): new_data = [] @@ -21,14 +21,14 @@ class DTMBA(FileDownloadSIGRefDataProvider): split = row.split(";") ref_id = split[0] ref_name = split[1] - new_data.append(SIGRef(sig=self.SIG, id=ref_id, name=ref_name, ref_type=SIGRefType.BUILDING)) + 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 sig refs by a few seconds but will ensure some time + # 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) diff --git a/providers/sigrefdata/fea.py b/providers/activityrefdata/fea.py similarity index 68% rename from providers/sigrefdata/fea.py rename to providers/activityrefdata/fea.py index bdf82c3..5bbd407 100644 --- a/providers/sigrefdata/fea.py +++ b/providers/activityrefdata/fea.py @@ -3,20 +3,20 @@ from time import sleep import pdfplumber -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider +from core.enums import ActivityRefType +from data.activity_ref import ActivityRef +from providers.activityrefdata.file_download_activity_ref_data_provider import FileDownloadActivityRefDataProvider -class FEA(FileDownloadSIGRefDataProvider): - """SIG ref data provider for Diploma Faros de España""" +class FEA(FileDownloadActivityRefDataProvider): + """Activity ref data provider for Diploma Faros de España""" POLL_INTERVAL_DAYS = 30 - SIG = "FEA" + ACTIVITY = "FEA" DATA_URL = "http://ea5ol.net/Lista%20Faros.pdf" def __init__(self, provider_config): - super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) + super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) def _http_response_to_data(self, http_response): new_data = [] @@ -41,15 +41,15 @@ class FEA(FileDownloadSIGRefDataProvider): # 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(SIGRef(sig=self.SIG, id=ref_id_1, name=row[1].strip(), ref_type=SIGRefType.LIGHTHOUSE)) - new_data.append(SIGRef(sig=self.SIG, id=ref_id_2, name=row[1].strip(), ref_type=SIGRefType.LIGHTHOUSE)) + 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 sig refs by a few seconds but will ensure some time + # 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) diff --git a/providers/sigrefdata/file_download_sig_ref_data_provider.py b/providers/activityrefdata/file_download_activity_ref_data_provider.py similarity index 64% rename from providers/sigrefdata/file_download_sig_ref_data_provider.py rename to providers/activityrefdata/file_download_activity_ref_data_provider.py index d1e243b..9e21027 100644 --- a/providers/sigrefdata/file_download_sig_ref_data_provider.py +++ b/providers/activityrefdata/file_download_activity_ref_data_provider.py @@ -7,13 +7,14 @@ from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout from core.constants import HTTP_HEADERS from core.url_data_cache import URLDataCache -from providers.sigrefdata.sig_ref_data_provider import SIGRefDataProvider +from providers.activityrefdata.activity_ref_data_provider import ActivityRefDataProvider logger = logging.getLogger(__name__) -class FileDownloadSIGRefDataProvider(SIGRefDataProvider): - """Generic SIG ref data provider class for providers that fetch their data from the web by downloading a file.""" +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*.""" @@ -21,13 +22,13 @@ class FileDownloadSIGRefDataProvider(SIGRefDataProvider): self._url = url self._poll_interval = poll_interval self._thread = None - self._url_data_cache = URLDataCache(f"sigrefdata_{sig_name}") + 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} SIG ref data every {self._poll_interval!s} days.") - self._thread = Thread(target=self._run, name=f"FileDownloadSIGRefDataProvider-{self.sig_name}", daemon=True) + 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): @@ -35,7 +36,7 @@ class FileDownloadSIGRefDataProvider(SIGRefDataProvider): if self._thread: self._thread.join(timeout=35) if self._thread.is_alive(): - logger.warning(f"{self.sig_name} SIG ref data worker thread did not exit on time and will be killed.") + 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: @@ -47,37 +48,37 @@ class FileDownloadSIGRefDataProvider(SIGRefDataProvider): 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} SIG ref data...") + 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 SIG Ref data store + # 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 SIG ref data for {self.sig_name}") + 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 SIG ref data for {self.sig_name}.") + 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 SIG ref data for {self.sig_name}.") + 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 SIG ref data for {self.sig_name}.") + logger.warning(f"Timeout when downloading activity ref data for {self.sig_name}.") except Exception: self.status = "Error" - logger.exception(f"Exception in HTTP SIG Ref Data Provider ({self.sig_name})") + 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 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.""" + """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") diff --git a/providers/sigrefdata/gma.py b/providers/activityrefdata/gma.py similarity index 70% rename from providers/sigrefdata/gma.py rename to providers/activityrefdata/gma.py index 36ce616..23a350b 100644 --- a/providers/sigrefdata/gma.py +++ b/providers/activityrefdata/gma.py @@ -1,33 +1,33 @@ import csv from time import sleep -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.file_download_sig_ref_data_provider import ( - FileDownloadSIGRefDataProvider, +from core.enums import ActivityRefType +from data.activity_ref import ActivityRef +from providers.activityrefdata.file_download_activity_ref_data_provider import ( + FileDownloadActivityRefDataProvider, ) -class GMA(FileDownloadSIGRefDataProvider): - """SIG ref data provider for Global Mountain Activity""" +class GMA(FileDownloadActivityRefDataProvider): + """Activity ref data provider for Global Mountain Activity""" POLL_INTERVAL_DAYS = 30 - SIG = "GMA" + ACTIVITY = "GMA" DATA_URL = "https://www.gma.rocks/download/summits.csv" def __init__(self, provider_config): - super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) + 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( - SIGRef( - sig=self.SIG, + ActivityRef( + sig=self.ACTIVITY, id=ref_id, name=row.get("Name", None), - ref_type=SIGRefType.SUMMIT, + 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, @@ -43,7 +43,7 @@ class GMA(FileDownloadSIGRefDataProvider): if self._stop_event.is_set(): break - # Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time + # 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) diff --git a/providers/sigrefdata/illw.py b/providers/activityrefdata/illw.py similarity index 67% rename from providers/sigrefdata/illw.py rename to providers/activityrefdata/illw.py index a5b6a91..1a87167 100644 --- a/providers/sigrefdata/illw.py +++ b/providers/activityrefdata/illw.py @@ -1,22 +1,22 @@ import csv from time import sleep -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.file_download_sig_ref_data_provider import ( - FileDownloadSIGRefDataProvider, +from core.enums import ActivityRefType +from data.activity_ref import ActivityRef +from providers.activityrefdata.file_download_activity_ref_data_provider import ( + FileDownloadActivityRefDataProvider, ) -class ILLW(FileDownloadSIGRefDataProvider): - """SIG ref data provider for International Lighthouse & Lightship Weekend""" +class ILLW(FileDownloadActivityRefDataProvider): + """Activity ref data provider for International Lighthouse & Lightship Weekend""" POLL_INTERVAL_DAYS = 30 - SIG = "ILLW" + ACTIVITY = "ILLW" DATA_URL = "https://www.gma.rocks/download/lighthouse.csv" def __init__(self, provider_config): - super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) + super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) def _http_response_to_data(self, http_response): new_data = [] @@ -24,11 +24,11 @@ class ILLW(FileDownloadSIGRefDataProvider): if "ILLW" in row and row["ILLW"] != "": ref_id = row["ILLW"] new_data.append( - SIGRef( - sig=self.SIG, + ActivityRef( + sig=self.ACTIVITY, id=ref_id, name=row.get("Name", None), - ref_type=SIGRefType.LIGHTHOUSE, + 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, @@ -41,7 +41,7 @@ class ILLW(FileDownloadSIGRefDataProvider): if self._stop_event.is_set(): break - # Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time + # 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) diff --git a/providers/sigrefdata/iota.py b/providers/activityrefdata/iota.py similarity index 73% rename from providers/sigrefdata/iota.py rename to providers/activityrefdata/iota.py index 744b057..47d9f50 100644 --- a/providers/sigrefdata/iota.py +++ b/providers/activityrefdata/iota.py @@ -3,24 +3,24 @@ from time import sleep from pyhamtools.locator import latlong_to_locator -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.file_download_sig_ref_data_provider import ( - FileDownloadSIGRefDataProvider, +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(FileDownloadSIGRefDataProvider): - """SIG ref data provider for Islands on the Air""" +class IOTA(FileDownloadActivityRefDataProvider): + """Activity ref data provider for Islands on the Air""" POLL_INTERVAL_DAYS = 365 - SIG = "IOTA" + 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.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) + super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) def _http_response_to_data(self, http_response): new_data = [] @@ -41,11 +41,11 @@ class IOTA(FileDownloadSIGRefDataProvider): ) new_data.append( - SIGRef( - sig=self.SIG, + ActivityRef( + sig=self.ACTIVITY, id=ref_id, name=ref["name"], - ref_type=SIGRefType.ISLAND, + ref_type=ActivityRefType.ISLAND, grid=grid, latitude=latitude, longitude=longitude, @@ -57,7 +57,7 @@ class IOTA(FileDownloadSIGRefDataProvider): if self._stop_event.is_set(): break - # Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time + # 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) diff --git a/providers/activityrefdata/krmnpa.py b/providers/activityrefdata/krmnpa.py new file mode 100644 index 0000000..5ff6f6b --- /dev/null +++ b/providers/activityrefdata/krmnpa.py @@ -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) diff --git a/providers/sigrefdata/llota.py b/providers/activityrefdata/llota.py similarity index 66% rename from providers/sigrefdata/llota.py rename to providers/activityrefdata/llota.py index 208b34c..556b74e 100644 --- a/providers/sigrefdata/llota.py +++ b/providers/activityrefdata/llota.py @@ -2,22 +2,22 @@ from time import sleep from pyhamtools.locator import locator_to_latlong -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.file_download_sig_ref_data_provider import ( - FileDownloadSIGRefDataProvider, +from core.enums import ActivityRefType +from data.activity_ref import ActivityRef +from providers.activityrefdata.file_download_activity_ref_data_provider import ( + FileDownloadActivityRefDataProvider, ) -class LLOTA(FileDownloadSIGRefDataProvider): - """SIG ref data provider for Lagos y Lagunas on the Air""" +class LLOTA(FileDownloadActivityRefDataProvider): + """Activity ref data provider for Lagos y Lagunas on the Air""" POLL_INTERVAL_DAYS = 7 - SIG = "LLOTA" + ACTIVITY = "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_DAYS) + super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) def _http_response_to_data(self, http_response): new_data = [] @@ -29,11 +29,11 @@ class LLOTA(FileDownloadSIGRefDataProvider): ll = locator_to_latlong(grid) new_data.append( - SIGRef( - sig=self.SIG, + ActivityRef( + sig=self.ACTIVITY, id=ref_id, name=str(ref["name"]), - ref_type=SIGRefType.LAKE, + ref_type=ActivityRefType.LAKE, url=f"https://llota.app/list/ref/{ref_id}", grid=grid, latitude=ll[0], @@ -46,7 +46,7 @@ class LLOTA(FileDownloadSIGRefDataProvider): if self._stop_event.is_set(): break - # Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time + # 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) diff --git a/providers/activityrefdata/local_file_activity_ref_data_provider.py b/providers/activityrefdata/local_file_activity_ref_data_provider.py new file mode 100644 index 0000000..e03c7c9 --- /dev/null +++ b/providers/activityrefdata/local_file_activity_ref_data_provider.py @@ -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") diff --git a/providers/sigrefdata/mota.py b/providers/activityrefdata/mota.py similarity index 67% rename from providers/sigrefdata/mota.py rename to providers/activityrefdata/mota.py index aaa2a16..30994ce 100644 --- a/providers/sigrefdata/mota.py +++ b/providers/activityrefdata/mota.py @@ -1,33 +1,33 @@ import csv from time import sleep -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.file_download_sig_ref_data_provider import ( - FileDownloadSIGRefDataProvider, +from core.enums import ActivityRefType +from data.activity_ref import ActivityRef +from providers.activityrefdata.file_download_activity_ref_data_provider import ( + FileDownloadActivityRefDataProvider, ) -class MOTA(FileDownloadSIGRefDataProvider): - """SIG ref data provider for Mills on the Air""" +class MOTA(FileDownloadActivityRefDataProvider): + """Activity ref data provider for Mills on the Air""" POLL_INTERVAL_DAYS = 30 - SIG = "MOTA" + ACTIVITY = "MOTA" DATA_URL = "https://www.gma.rocks/download/mills.csv" def __init__(self, provider_config): - super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) + 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( - SIGRef( - sig=self.SIG, + ActivityRef( + sig=self.ACTIVITY, id=ref_id, name=row.get("Name", None), - ref_type=SIGRefType.MILL, + 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, @@ -40,7 +40,7 @@ class MOTA(FileDownloadSIGRefDataProvider): if self._stop_event.is_set(): break - # Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time + # 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) diff --git a/providers/sigrefdata/pga.py b/providers/activityrefdata/pga.py similarity index 72% rename from providers/sigrefdata/pga.py rename to providers/activityrefdata/pga.py index 2c20c67..8029b6e 100644 --- a/providers/sigrefdata/pga.py +++ b/providers/activityrefdata/pga.py @@ -2,20 +2,20 @@ from time import sleep from bs4 import BeautifulSoup -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider +from core.enums import ActivityRefType +from data.activity_ref import ActivityRef +from providers.activityrefdata.file_download_activity_ref_data_provider import FileDownloadActivityRefDataProvider -class PGA(FileDownloadSIGRefDataProvider): - """SIG ref data provider for Polish Gmina Award""" +class PGA(FileDownloadActivityRefDataProvider): + """Activity ref data provider for Polish Gmina Award""" POLL_INTERVAL_DAYS = 30 - SIG = "PGA" + ACTIVITY = "PGA" DATA_URL = "http://www.spga.pl/lista_pga2.php" def __init__(self, provider_config): - super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) + super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) def _http_response_to_data(self, http_response): new_data = [] @@ -38,11 +38,11 @@ class PGA(FileDownloadSIGRefDataProvider): continue new_data.append( - SIGRef( - sig=self.SIG, + ActivityRef( + sig=self.ACTIVITY, id=ref_id, name=name, - ref_type=SIGRefType.REGION, + ref_type=ActivityRefType.REGION, ) ) @@ -51,7 +51,7 @@ class PGA(FileDownloadSIGRefDataProvider): if self._stop_event.is_set(): break - # Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time + # 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) diff --git a/providers/sigrefdata/pnp_kml_sig_ref_data_provider.py b/providers/activityrefdata/pnp_kml_activity_ref_data_provider.py similarity index 76% rename from providers/sigrefdata/pnp_kml_sig_ref_data_provider.py rename to providers/activityrefdata/pnp_kml_activity_ref_data_provider.py index d13868b..119ce37 100644 --- a/providers/sigrefdata/pnp_kml_sig_ref_data_provider.py +++ b/providers/activityrefdata/pnp_kml_activity_ref_data_provider.py @@ -4,16 +4,16 @@ from time import sleep from fastkml import kml from pyhamtools.locator import latlong_to_locator -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.file_download_sig_ref_data_provider import ( - FileDownloadSIGRefDataProvider, +from core.enums import ActivityRefType +from data.activity_ref import ActivityRef +from providers.activityrefdata.file_download_activity_ref_data_provider import ( + FileDownloadActivityRefDataProvider, ) -class ParksNPeaksKMLSIGRefDataProvider(FileDownloadSIGRefDataProvider): - """Base class for SIG 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).""" +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+") @@ -40,11 +40,11 @@ class ParksNPeaksKMLSIGRefDataProvider(FileDownloadSIGRefDataProvider): longitude, latitude = placemark.geometry.x, placemark.geometry.y - ref = SIGRef( + ref = ActivityRef( sig=self.sig_name, id=ref_id, name=placemark.name, - ref_type=SIGRefType.PARK, + ref_type=ActivityRefType.PARK, url=f"https://parksnpeaks.org/getPark.php?actPark={ref_id}", latitude=latitude, longitude=longitude, @@ -58,7 +58,7 @@ class ParksNPeaksKMLSIGRefDataProvider(FileDownloadSIGRefDataProvider): if self._stop_event.is_set(): return new_data - # Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time + # 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) diff --git a/providers/sigrefdata/pota.py b/providers/activityrefdata/pota.py similarity index 67% rename from providers/sigrefdata/pota.py rename to providers/activityrefdata/pota.py index 7b6db68..f5b700a 100644 --- a/providers/sigrefdata/pota.py +++ b/providers/activityrefdata/pota.py @@ -1,33 +1,33 @@ import csv from time import sleep -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.file_download_sig_ref_data_provider import ( - FileDownloadSIGRefDataProvider, +from core.enums import ActivityRefType +from data.activity_ref import ActivityRef +from providers.activityrefdata.file_download_activity_ref_data_provider import ( + FileDownloadActivityRefDataProvider, ) -class POTA(FileDownloadSIGRefDataProvider): - """SIG ref data provider for Parks on the Air""" +class POTA(FileDownloadActivityRefDataProvider): + """Activity ref data provider for Parks on the Air""" POLL_INTERVAL_DAYS = 7 - SIG = "POTA" + ACTIVITY = "POTA" DATA_URL = "https://pota.app/all_parks_ext.csv" def __init__(self, provider_config): - super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) + 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( - SIGRef( - sig=self.SIG, + ActivityRef( + sig=self.ACTIVITY, id=ref_id, name=row.get("name", None), - ref_type=SIGRefType.PARK, + 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, @@ -40,7 +40,7 @@ class POTA(FileDownloadSIGRefDataProvider): if self._stop_event.is_set(): break - # Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time + # 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) diff --git a/providers/activityrefdata/sanpcpa.py b/providers/activityrefdata/sanpcpa.py new file mode 100644 index 0000000..2186777 --- /dev/null +++ b/providers/activityrefdata/sanpcpa.py @@ -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) diff --git a/providers/sigrefdata/siota.py b/providers/activityrefdata/siota.py similarity index 64% rename from providers/sigrefdata/siota.py rename to providers/activityrefdata/siota.py index 91f863a..5ccb0e9 100644 --- a/providers/sigrefdata/siota.py +++ b/providers/activityrefdata/siota.py @@ -1,33 +1,33 @@ import csv from time import sleep -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.file_download_sig_ref_data_provider import ( - FileDownloadSIGRefDataProvider, +from core.enums import ActivityRefType +from data.activity_ref import ActivityRef +from providers.activityrefdata.file_download_activity_ref_data_provider import ( + FileDownloadActivityRefDataProvider, ) -class SIOTA(FileDownloadSIGRefDataProvider): - """SIG ref data provider for Silos on the Air""" +class SIOTA(FileDownloadActivityRefDataProvider): + """Activity ref data provider for Silos on the Air""" POLL_INTERVAL_DAYS = 30 - SIG = "SIOTA" + ACTIVITY = "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_DAYS) + 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( - SIGRef( - sig=self.SIG, + ActivityRef( + sig=self.ACTIVITY, id=ref_id, name=row.get("NAME", None), - ref_type=SIGRefType.SILO, + 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, @@ -39,7 +39,7 @@ class SIOTA(FileDownloadSIGRefDataProvider): if self._stop_event.is_set(): break - # Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time + # 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) diff --git a/providers/sigrefdata/sota.py b/providers/activityrefdata/sota.py similarity index 72% rename from providers/sigrefdata/sota.py rename to providers/activityrefdata/sota.py index 07462ba..013350a 100644 --- a/providers/sigrefdata/sota.py +++ b/providers/activityrefdata/sota.py @@ -3,22 +3,22 @@ from time import sleep from pyhamtools.locator import latlong_to_locator -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.file_download_sig_ref_data_provider import ( - FileDownloadSIGRefDataProvider, +from core.enums import ActivityRefType +from data.activity_ref import ActivityRef +from providers.activityrefdata.file_download_activity_ref_data_provider import ( + FileDownloadActivityRefDataProvider, ) -class SOTA(FileDownloadSIGRefDataProvider): - """SIG ref data provider for Summits on the Air""" +class SOTA(FileDownloadActivityRefDataProvider): + """Activity ref data provider for Summits on the Air""" POLL_INTERVAL_DAYS = 30 - SIG = "SOTA" + ACTIVITY = "SOTA" DATA_URL = "https://storage.sota.org.uk/summitslist.csv" def __init__(self, provider_config): - super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) + super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) def _http_response_to_data(self, http_response): new_data = [] @@ -27,11 +27,11 @@ class SOTA(FileDownloadSIGRefDataProvider): 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 = SIGRef( - sig=self.SIG, + ref = ActivityRef( + sig=self.ACTIVITY, id=ref_id, name=row.get("SummitName", None), - ref_type=SIGRefType.SUMMIT, + ref_type=ActivityRefType.SUMMIT, url=f"https://www.sotadata.org.uk/en/summit/{ref_id}", latitude=latitude, longitude=longitude, @@ -47,7 +47,7 @@ class SOTA(FileDownloadSIGRefDataProvider): if self._stop_event.is_set(): break - # Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time + # 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) diff --git a/providers/sigrefdata/toilets.py b/providers/activityrefdata/toilets.py similarity index 60% rename from providers/sigrefdata/toilets.py rename to providers/activityrefdata/toilets.py index 7542ad5..e9829df 100644 --- a/providers/sigrefdata/toilets.py +++ b/providers/activityrefdata/toilets.py @@ -1,20 +1,20 @@ import csv -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.local_file_sig_ref_data_provider import ( - LocalFileSIGRefDataProvider, +from core.enums import ActivityRefType +from data.activity_ref import ActivityRef +from providers.activityrefdata.local_file_activity_ref_data_provider import ( + LocalFileActivityRefDataProvider, ) -class Toilets(LocalFileSIGRefDataProvider): - """SIG ref data provider for Toilets on the Air""" +class Toilets(LocalFileActivityRefDataProvider): + """Activity ref data provider for Toilets on the Air""" - SIG = "Toilets" + ACTIVITY = "Toilets" PATH = "datafiles/toilets.csv" def __init__(self, provider_config): - super().__init__(self.SIG, provider_config, self.PATH) + super().__init__(self.ACTIVITY, provider_config, self.PATH) def _file_to_data(self, path): new_data = [] @@ -23,11 +23,11 @@ class Toilets(LocalFileSIGRefDataProvider): dr = csv.DictReader(csv_data.splitlines()) for row in dr: new_data.append( - SIGRef( - sig=self.SIG, + ActivityRef( + sig=self.ACTIVITY, id=row["ref"], name=row["ref"], - ref_type=SIGRefType.TOILET, + ref_type=ActivityRefType.TOILET, latitude=float(row["lat"]), longitude=float(row["lon"]), ) diff --git a/providers/sigrefdata/towers.py b/providers/activityrefdata/towers.py similarity index 68% rename from providers/sigrefdata/towers.py rename to providers/activityrefdata/towers.py index 8449ca7..56d5d13 100644 --- a/providers/sigrefdata/towers.py +++ b/providers/activityrefdata/towers.py @@ -1,33 +1,33 @@ import csv from time import sleep -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.file_download_sig_ref_data_provider import ( - FileDownloadSIGRefDataProvider, +from core.enums import ActivityRefType +from data.activity_ref import ActivityRef +from providers.activityrefdata.file_download_activity_ref_data_provider import ( + FileDownloadActivityRefDataProvider, ) -class Towers(FileDownloadSIGRefDataProvider): - """SIG ref data provider for Towers on the Air""" +class Towers(FileDownloadActivityRefDataProvider): + """Activity ref data provider for Towers on the Air""" POLL_INTERVAL_DAYS = 30 - SIG = "Towers" + ACTIVITY = "Towers" DATA_URL = "https://wwtota.com/servis/generate_csv.php?ref=&filter=all" def __init__(self, provider_config): - super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) + 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( - SIGRef( - sig=self.SIG, + ActivityRef( + sig=self.ACTIVITY, id=ref_id, name=row.get("Nazev", None), - ref_type=SIGRefType.TOWER, + 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, @@ -40,7 +40,7 @@ class Towers(FileDownloadSIGRefDataProvider): if self._stop_event.is_set(): break - # Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time + # 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) diff --git a/providers/sigrefdata/wca.py b/providers/activityrefdata/wca.py similarity index 74% rename from providers/sigrefdata/wca.py rename to providers/activityrefdata/wca.py index 778481c..9145c25 100644 --- a/providers/sigrefdata/wca.py +++ b/providers/activityrefdata/wca.py @@ -4,24 +4,24 @@ from time import sleep from pyhamtools.locator import latlong_to_locator -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.file_download_sig_ref_data_provider import ( - FileDownloadSIGRefDataProvider, +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(FileDownloadSIGRefDataProvider): - """SIG ref data provider for World Castles Award""" +class WCA(FileDownloadActivityRefDataProvider): + """Activity ref data provider for World Castles Award""" POLL_INTERVAL_DAYS = 30 - SIG = "WCA" + ACTIVITY = "WCA" DATA_URL = "https://polo.ham2k.com/data/activities/wca/all-castles.csv" def __init__(self, provider_config): - super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) + super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) def _http_response_to_data(self, http_response): new_data = [] @@ -42,11 +42,11 @@ class WCA(FileDownloadSIGRefDataProvider): logger.debug(f"Encountered dodgy formatting in WCA CSV, skipping location data for {ref_id}") new_data.append( - SIGRef( - sig=self.SIG, + ActivityRef( + sig=self.ACTIVITY, id=ref_id, name=row.get("CLEAN NAME", None), - ref_type=SIGRefType.CASTLE, + ref_type=ActivityRefType.CASTLE, url=f"https://www.cqgma.org/zinfo.php?ref={ref_id}", latitude=latitude, longitude=longitude, @@ -59,7 +59,7 @@ class WCA(FileDownloadSIGRefDataProvider): if self._stop_event.is_set(): break - # Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time + # 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) diff --git a/providers/sigrefdata/wota.py b/providers/activityrefdata/wota.py similarity index 71% rename from providers/sigrefdata/wota.py rename to providers/activityrefdata/wota.py index 10e476c..544c8ad 100644 --- a/providers/sigrefdata/wota.py +++ b/providers/activityrefdata/wota.py @@ -1,21 +1,21 @@ from time import sleep -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.file_download_sig_ref_data_provider import ( - FileDownloadSIGRefDataProvider, +from core.enums import ActivityRefType +from data.activity_ref import ActivityRef +from providers.activityrefdata.file_download_activity_ref_data_provider import ( + FileDownloadActivityRefDataProvider, ) -class WOTA(FileDownloadSIGRefDataProvider): - """SIG ref data provider for Wainwrights on the Air""" +class WOTA(FileDownloadActivityRefDataProvider): + """Activity ref data provider for Wainwrights on the Air""" POLL_INTERVAL_DAYS = 365 - SIG = "WOTA" + ACTIVITY = "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_DAYS) + super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) def _http_response_to_data(self, http_response): new_data = [] @@ -29,12 +29,12 @@ class WOTA(FileDownloadSIGRefDataProvider): url = f"https://www.wota.org.uk/MM_LDO-{number + 214!s}" new_data.append( - SIGRef( - sig=self.SIG, + ActivityRef( + sig=self.ACTIVITY, id=ref_id, name=feature["properties"]["title"], url=url, - ref_type=SIGRefType.SUMMIT, + ref_type=ActivityRefType.SUMMIT, grid=feature["properties"]["qthLocator"], latitude=feature["geometry"]["coordinates"][1], longitude=feature["geometry"]["coordinates"][0], @@ -47,7 +47,7 @@ class WOTA(FileDownloadSIGRefDataProvider): if self._stop_event.is_set(): break - # Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time + # 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) diff --git a/providers/sigrefdata/wwbota.py b/providers/activityrefdata/wwbota.py similarity index 67% rename from providers/sigrefdata/wwbota.py rename to providers/activityrefdata/wwbota.py index 94e43be..e4edf1f 100644 --- a/providers/sigrefdata/wwbota.py +++ b/providers/activityrefdata/wwbota.py @@ -1,33 +1,33 @@ import csv from time import sleep -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.file_download_sig_ref_data_provider import ( - FileDownloadSIGRefDataProvider, +from core.enums import ActivityRefType +from data.activity_ref import ActivityRef +from providers.activityrefdata.file_download_activity_ref_data_provider import ( + FileDownloadActivityRefDataProvider, ) -class WWBOTA(FileDownloadSIGRefDataProvider): - """SIG ref data provider for Worldwide Bunkers on the Air""" +class WWBOTA(FileDownloadActivityRefDataProvider): + """Activity ref data provider for Worldwide Bunkers on the Air""" POLL_INTERVAL_DAYS = 30 - SIG = "WWBOTA" + ACTIVITY = "WWBOTA" DATA_URL = "https://api.wwbota.org/bunkers/?format=CSV" def __init__(self, provider_config): - super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) + 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( - SIGRef( - sig=self.SIG, + ActivityRef( + sig=self.ACTIVITY, id=ref_id, name=row.get("Name", None), - ref_type=SIGRefType.BUNKER, + 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, @@ -40,7 +40,7 @@ class WWBOTA(FileDownloadSIGRefDataProvider): if self._stop_event.is_set(): break - # Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time + # 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) diff --git a/providers/sigrefdata/wwff.py b/providers/activityrefdata/wwff.py similarity index 70% rename from providers/sigrefdata/wwff.py rename to providers/activityrefdata/wwff.py index 768de70..36f24ec 100644 --- a/providers/sigrefdata/wwff.py +++ b/providers/activityrefdata/wwff.py @@ -1,33 +1,33 @@ import csv from time import sleep -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.file_download_sig_ref_data_provider import ( - FileDownloadSIGRefDataProvider, +from core.enums import ActivityRefType +from data.activity_ref import ActivityRef +from providers.activityrefdata.file_download_activity_ref_data_provider import ( + FileDownloadActivityRefDataProvider, ) -class WWFF(FileDownloadSIGRefDataProvider): - """SIG ref data provider for Worldwide Flora & Fauna""" +class WWFF(FileDownloadActivityRefDataProvider): + """Activity ref data provider for Worldwide Flora & Fauna""" POLL_INTERVAL_DAYS = 30 - SIG = "WWFF" + ACTIVITY = "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_DAYS) + 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( - SIGRef( - sig=self.SIG, + ActivityRef( + sig=self.ACTIVITY, id=ref_id, name=row.get("name", None), - ref_type=SIGRefType.PARK, + 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"]) @@ -44,7 +44,7 @@ class WWFF(FileDownloadSIGRefDataProvider): if self._stop_event.is_set(): break - # Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time + # 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) diff --git a/providers/sigrefdata/zlota.py b/providers/activityrefdata/zlota.py similarity index 72% rename from providers/sigrefdata/zlota.py rename to providers/activityrefdata/zlota.py index ee47964..50c5f66 100644 --- a/providers/sigrefdata/zlota.py +++ b/providers/activityrefdata/zlota.py @@ -2,22 +2,22 @@ from time import sleep from pyhamtools.locator import latlong_to_locator -from core.enums import SIGRefType -from data.sig_ref import SIGRef -from providers.sigrefdata.file_download_sig_ref_data_provider import ( - FileDownloadSIGRefDataProvider, +from core.enums import ActivityRefType +from data.activity_ref import ActivityRef +from providers.activityrefdata.file_download_activity_ref_data_provider import ( + FileDownloadActivityRefDataProvider, ) -class ZLOTA(FileDownloadSIGRefDataProvider): - """SIG ref data provider for New Zealand on the Air""" +class ZLOTA(FileDownloadActivityRefDataProvider): + """Activity ref data provider for New Zealand on the Air""" POLL_INTERVAL_DAYS = 30 - SIG = "ZLOTA" + ACTIVITY = "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_DAYS) + super().__init__(self.ACTIVITY, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) def _http_response_to_data(self, http_response): new_data = [] @@ -28,12 +28,12 @@ class ZLOTA(FileDownloadSIGRefDataProvider): latitude = ref["latitude"] longitude = ref["longitude"] try: - ref_type = SIGRefType(ref["asset_type"].title().upper()) + ref_type = ActivityRefType(ref["asset_type"].title().upper()) except ValueError: ref_type = None - new_ref = SIGRef( - sig=self.SIG, + new_ref = ActivityRef( + sig=self.ACTIVITY, id=ref_id, name=ref["name"], ref_type=ref_type, @@ -58,7 +58,7 @@ class ZLOTA(FileDownloadSIGRefDataProvider): if self._stop_event.is_set(): break - # Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time + # 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) diff --git a/providers/alert/bota.py b/providers/alert/bota.py index 798770a..c278801 100644 --- a/providers/alert/bota.py +++ b/providers/alert/bota.py @@ -4,8 +4,8 @@ import pytz from bs4 import BeautifulSoup from core.enums import AlertType +from data.activity_ref import ActivityRef from data.alert import Alert -from data.sig_ref import SIGRef from providers.alert.http_alert_provider import HTTPAlertProvider @@ -56,7 +56,7 @@ class BOTA(HTTPAlertProvider): alert = Alert( source=self.name, dx_calls=[dx_call], - sig_refs=[SIGRef(id=ref_name, sig="BOTA")], + sig_refs=[ActivityRef(id=ref_name, sig="BOTA")], start_time=date_time.timestamp(), alert_type=AlertType.XOTA, ) diff --git a/providers/alert/hamsat.py b/providers/alert/hamsat.py index 6390e2a..6c297c3 100644 --- a/providers/alert/hamsat.py +++ b/providers/alert/hamsat.py @@ -3,8 +3,8 @@ from datetime import datetime import pytz from core.enums import AlertType +from data.activity_ref import ActivityRef from data.alert import Alert -from data.sig_ref import SIGRef from providers.alert.http_alert_provider import HTTPAlertProvider @@ -35,9 +35,9 @@ class Hamsat(HTTPAlertProvider): dx_calls=[source_alert["callsign"].upper()], freqs_modes=freqs_modes, comment=source_alert["comment"], - # Fudge a SIG ref to provide the remaining bits of data we need: the satellite and the operator's grid + # Fudge an activity ref to provide the remaining bits of data we need: the satellite and the operator's grid sig_refs=[ - SIGRef( + ActivityRef( sig="AMSAT", id=f"{source_alert['satellite']['name']} from {source_alert['grids'][0]}", ) diff --git a/providers/alert/parksnpeaks.py b/providers/alert/parksnpeaks.py index 614837c..cb326c1 100644 --- a/providers/alert/parksnpeaks.py +++ b/providers/alert/parksnpeaks.py @@ -4,8 +4,8 @@ from datetime import datetime import pytz from core.enums import AlertType +from data.activity_ref import ActivityRef from data.alert import Alert -from data.sig_ref import SIGRef from providers.alert.http_alert_provider import HTTPAlertProvider logger = logging.getLogger(__name__) @@ -25,23 +25,23 @@ class ParksNPeaks(HTTPAlertProvider): # Iterate through source data for source_alert in http_response.json(): # Calculate some things - sig = source_alert["Class"].upper() + activity = source_alert["Class"].upper() if " - " in source_alert["Location"]: split = source_alert["Location"].split(" - ") - sig_ref = split[0] - sig_ref_name = split[1] + ref_id = split[0] + ref_name = split[1] else: - sig_ref = source_alert["WWFFID"] - sig_ref_name = source_alert["Location"] + ref_id = source_alert["WWFFID"] + ref_name = source_alert["Location"] start_time = ( datetime.strptime(source_alert["alTime"], "%Y-%m-%d %H:%M:%S").replace(tzinfo=pytz.UTC).timestamp() ) - sigrefs = [] - # PnP can give us an alert of class "QRP" which is the only one that's not a real SIG in Spothole's list, - # so mask this out if we got it. - if sig != "QRP": - sigrefs = [SIGRef(id=sig_ref, sig=sig, name=sig_ref_name)] + activity_refs = [] + # PnP can give us an alert of class "QRP" which is the only one that's not a real activity in Spothole's + # list, so mask this out if we got it. + if activity != "QRP": + activity_refs = [ActivityRef(id=ref_id, sig=activity, name=ref_name)] # Convert to our alert format alert = Alert( @@ -50,13 +50,13 @@ class ParksNPeaks(HTTPAlertProvider): dx_calls=[source_alert["CallSign"].upper()], freqs_modes=f"{source_alert['Freq']} {source_alert['MODE']}", comment=source_alert["Comments"], - sig_refs=sigrefs, + sig_refs=activity_refs, start_time=start_time, alert_type=AlertType.XOTA, ) # Log a warning for the developer if PnP gives us an unknown programme we've never seen before - if sig and sig not in [ + if activity and activity not in [ "POTA", "SOTA", "WWFF", @@ -68,11 +68,11 @@ class ParksNPeaks(HTTPAlertProvider): "LLOTA", "QRP", ]: - logger.warning(f"PNP alert found with sig {sig}, developer needs to add support for this!") + logger.warning(f"PNP alert found with activity {activity}, developer needs to add support for this!") # If this is POTA, SOTA or WWFF data we already have it through other means, so ignore. Otherwise, add to # the alert list. Note that while ZLOTA has its own spots API, it doesn't have its own alerts API. So that # means the PnP *spot* provider rejects ZLOTA spots here, but the PnP *alerts* provider here allows ZLOTA. - if sig not in ["POTA", "SOTA", "WWFF"]: + if activity not in ["POTA", "SOTA", "WWFF"]: new_alerts.append(alert) return new_alerts diff --git a/providers/alert/pota.py b/providers/alert/pota.py index aceee0e..aebbcde 100644 --- a/providers/alert/pota.py +++ b/providers/alert/pota.py @@ -3,8 +3,8 @@ from datetime import datetime import pytz from core.enums import AlertType +from data.activity_ref import ActivityRef from data.alert import Alert -from data.sig_ref import SIGRef from providers.alert.http_alert_provider import HTTPAlertProvider @@ -29,7 +29,7 @@ class POTA(HTTPAlertProvider): freqs_modes=source_alert["frequencies"], comment=source_alert["comments"], sig_refs=[ - SIGRef( + ActivityRef( id=source_alert["reference"], sig="POTA", name=source_alert["name"], diff --git a/providers/alert/sota.py b/providers/alert/sota.py index b803b61..f260634 100644 --- a/providers/alert/sota.py +++ b/providers/alert/sota.py @@ -3,8 +3,8 @@ from datetime import datetime import pytz from core.enums import AlertType +from data.activity_ref import ActivityRef from data.alert import Alert -from data.sig_ref import SIGRef from providers.alert.http_alert_provider import HTTPAlertProvider @@ -35,7 +35,7 @@ class SOTA(HTTPAlertProvider): freqs_modes=source_alert["frequency"], comment=source_alert["comments"], sig_refs=[ - SIGRef( + ActivityRef( id=f"{source_alert['associationCode']}/{source_alert['summitCode']}", sig="SOTA", name=summit_name, diff --git a/providers/alert/wota.py b/providers/alert/wota.py index 4dd1522..8ecd56c 100644 --- a/providers/alert/wota.py +++ b/providers/alert/wota.py @@ -7,8 +7,8 @@ import pytz from rss_parser import Parser as RSSParser from rss_parser.models.rss import RSS +from data.activity_ref import ActivityRef from data.alert import Alert -from data.sig_ref import SIGRef from providers.alert.http_alert_provider import HTTPAlertProvider logger = logging.getLogger(__name__) @@ -74,7 +74,7 @@ class WOTA(HTTPAlertProvider): dx_calls=[dx_call], freqs_modes=freqs_modes, comment=comment, - sig_refs=[SIGRef(id=ref, sig="WOTA", name=ref_name)] if ref else [], + sig_refs=[ActivityRef(id=ref, sig="WOTA", name=ref_name)] if ref else [], start_time=time.timestamp(), ) diff --git a/providers/alert/wwff.py b/providers/alert/wwff.py index 5d4f359..d3b32f0 100644 --- a/providers/alert/wwff.py +++ b/providers/alert/wwff.py @@ -3,8 +3,8 @@ from datetime import datetime import pytz from core.enums import AlertType +from data.activity_ref import ActivityRef from data.alert import Alert -from data.sig_ref import SIGRef from providers.alert.http_alert_provider import HTTPAlertProvider @@ -28,7 +28,7 @@ class WWFF(HTTPAlertProvider): dx_calls=[source_alert["activator_call"].upper()], freqs_modes=f"{source_alert['band']} {source_alert['mode']}", comment=source_alert["remarks"], - sig_refs=[SIGRef(id=source_alert["reference"], sig="WWFF")], + sig_refs=[ActivityRef(id=source_alert["reference"], sig="WWFF")], start_time=datetime.strptime(source_alert["utc_start"], "%Y-%m-%d %H:%M:%S") .replace(tzinfo=pytz.UTC) .timestamp(), diff --git a/providers/sigrefdata/krmnpa.py b/providers/sigrefdata/krmnpa.py deleted file mode 100644 index 92cc9dc..0000000 --- a/providers/sigrefdata/krmnpa.py +++ /dev/null @@ -1,14 +0,0 @@ -from providers.sigrefdata.pnp_kml_sig_ref_data_provider import ( - ParksNPeaksKMLSIGRefDataProvider, -) - - -class KRMNPA(ParksNPeaksKMLSIGRefDataProvider): - """SIG ref data provider for the Keith Roget Memorrial National Parks Award (KRMNPA).""" - - POLL_INTERVAL_DAYS = 365 - SIG = "KRMNPA" - DATA_URL = "https://parksnpeaks.org/getPOI.php?poiClass=KRMNPA&poiFormat=4" - - def __init__(self, provider_config): - super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) diff --git a/providers/sigrefdata/local_file_sig_ref_data_provider.py b/providers/sigrefdata/local_file_sig_ref_data_provider.py deleted file mode 100644 index c810779..0000000 --- a/providers/sigrefdata/local_file_sig_ref_data_provider.py +++ /dev/null @@ -1,36 +0,0 @@ -import logging -from datetime import datetime - -import pytz - -from providers.sigrefdata.sig_ref_data_provider import SIGRefDataProvider - -logger = logging.getLogger(__name__) - - -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): - logger.debug(f"Loading {self.sig_name} SIG 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 SIG ref data for {self.sig_name}") - except Exception: - self.status = "Error" - logger.exception(f"Exception in local file SIG Ref Data Provider ({self.sig_name})") - - 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") diff --git a/providers/sigrefdata/sanpcpa.py b/providers/sigrefdata/sanpcpa.py deleted file mode 100644 index a57e141..0000000 --- a/providers/sigrefdata/sanpcpa.py +++ /dev/null @@ -1,14 +0,0 @@ -from providers.sigrefdata.pnp_kml_sig_ref_data_provider import ( - ParksNPeaksKMLSIGRefDataProvider, -) - - -class SANPCPA(ParksNPeaksKMLSIGRefDataProvider): - """SIG ref data provider for the South Australia National Parks and Conservation Parks Award (SANPCPA).""" - - POLL_INTERVAL_DAYS = 365 - SIG = "SANPCPA" - DATA_URL = "https://parksnpeaks.org/getPOI.php?poiClass=SANPCPA&poiFormat=4" - - def __init__(self, provider_config): - super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) diff --git a/providers/spot/gma.py b/providers/spot/gma.py index 688a2e9..b1b8356 100644 --- a/providers/spot/gma.py +++ b/providers/spot/gma.py @@ -4,9 +4,9 @@ from datetime import datetime import pytz from core.constants import HTTP_HEADERS -from core.enums import Mode, SIGRefType +from core.enums import ActivityRefType, Mode from core.url_data_cache import URLDataCache -from data.sig_ref import SIGRef +from data.activity_ref import ActivityRef from data.spot import Spot from providers.spot.http_spot_provider import HTTPSpotProvider @@ -69,7 +69,7 @@ class GMA(HTTPSpotProvider): mode=Mode.from_name(source_spot["MODE"].upper()) if "<>" not in source_spot["MODE"] else None, comment=source_spot["TEXT"], sig_refs=[ - SIGRef( + ActivityRef( id=source_spot["REF"], sig="", name=source_spot["NAME"], @@ -83,7 +83,7 @@ class GMA(HTTPSpotProvider): qrt=source_spot["QRG"] == "QRT", ) - # GMA doesn't give what programme (SIG) the reference is for until we separately look it up. + # GMA doesn't give what programme (activity) the reference is for until we separately look it up. if "REF" in source_spot: try: ref_response = self._url_data_cache.get( @@ -114,31 +114,31 @@ class GMA(HTTPSpotProvider): match ref_info["reftype"]: case "Summit": spot.sig_refs[0].sig = "GMA" - spot.sig_refs[0].ref_type = SIGRefType.SUMMIT + spot.sig_refs[0].ref_type = ActivityRefType.SUMMIT spot.sig = "GMA" case "IOTA Island": spot.sig_refs[0].sig = "IOTA" - spot.sig_refs[0].ref_type = SIGRefType.ISLAND + spot.sig_refs[0].ref_type = ActivityRefType.ISLAND spot.sig = "IOTA" case "GMA Island": spot.sig_refs[0].sig = "GMA Islands" - spot.sig_refs[0].ref_type = SIGRefType.ISLAND + spot.sig_refs[0].ref_type = ActivityRefType.ISLAND spot.sig = "GMA Islands" case "Lighthouse (ILLW)": spot.sig_refs[0].sig = "ILLW" - spot.sig_refs[0].ref_type = SIGRefType.LIGHTHOUSE + spot.sig_refs[0].ref_type = ActivityRefType.LIGHTHOUSE spot.sig = "ILLW" case "Lighthouse (ARLHS)": spot.sig_refs[0].sig = "ARLHS" - spot.sig_refs[0].ref_type = SIGRefType.LIGHTHOUSE + spot.sig_refs[0].ref_type = ActivityRefType.LIGHTHOUSE spot.sig = "ARLHS" case "Castle": spot.sig_refs[0].sig = "WCA" - spot.sig_refs[0].ref_type = SIGRefType.CASTLE + spot.sig_refs[0].ref_type = ActivityRefType.CASTLE spot.sig = "WCA" case "Mill": spot.sig_refs[0].sig = "MOTA" - spot.sig_refs[0].ref_type = SIGRefType.MILL + spot.sig_refs[0].ref_type = ActivityRefType.MILL spot.sig = "MOTA" case _: logger.warning( @@ -158,7 +158,7 @@ class GMA(HTTPSpotProvider): ) except Exception: logger.exception( - f"Exception when looking up {self.REF_INFO_URL_ROOT}{source_spot['REF']}, SIG data will not be populated for the spot." + f"Exception when looking up {self.REF_INFO_URL_ROOT}{source_spot['REF']}, activity data will not be populated for the spot." ) # Add to our list. Don't worry about de-duping, removing old spots etc. at this point; @@ -169,8 +169,8 @@ class GMA(HTTPSpotProvider): return new_spots - def can_submit_spot(self, sig): - return sig == "GMA" + def can_submit_spot(self, activity): + return activity == "GMA" def submit_spot(self, spot, credentials): # TODO: Implement. diff --git a/providers/spot/hema.py b/providers/spot/hema.py index 6fed404..d9e2d62 100644 --- a/providers/spot/hema.py +++ b/providers/spot/hema.py @@ -7,8 +7,8 @@ import requests from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout from core.constants import HTTP_HEADERS -from core.enums import Mode, SIGRefType -from data.sig_ref import SIGRef +from core.enums import ActivityRefType, Mode +from data.activity_ref import ActivityRef from data.spot import Spot from providers.spot.http_spot_provider import HTTPSpotProvider @@ -64,13 +64,13 @@ class HEMA(HTTPSpotProvider): comment=spotter_comment_match.group(2), sig="HEMA", sig_refs=[ - SIGRef( + ActivityRef( id=spot_items[3].upper(), sig="HEMA", name=spot_items[4], latitude=float(spot_items[7]), longitude=float(spot_items[8]), - ref_type=SIGRefType.SUMMIT, + ref_type=ActivityRefType.SUMMIT, ) ], time=datetime.strptime(spot_items[0], "%d/%m/%Y %H:%M") @@ -89,8 +89,8 @@ class HEMA(HTTPSpotProvider): logger.warning("Connection error when accessing HEMA spots API.") return new_spots - def can_submit_spot(self, sig): - return sig == "HEMA" + def can_submit_spot(self, activity): + return activity == "HEMA" def submit_spot(self, spot, credentials): # TODO: Implement. Currently blocked awaiting their API team to make a change to allow us to spot with a diff --git a/providers/spot/llota.py b/providers/spot/llota.py index 738ff28..bfbc7a2 100644 --- a/providers/spot/llota.py +++ b/providers/spot/llota.py @@ -1,7 +1,7 @@ from datetime import datetime -from core.enums import Mode, SIGRefType -from data.sig_ref import SIGRef +from core.enums import ActivityRefType, Mode +from data.activity_ref import ActivityRef from data.spot import Spot from providers.spot.http_spot_provider import HTTPSpotProvider @@ -36,11 +36,11 @@ class LLOTA(HTTPSpotProvider): comment=comment, sig="LLOTA", sig_refs=[ - SIGRef( + ActivityRef( id=source_spot["reference"], sig="LLOTA", name=source_spot["reference_name"], - ref_type=SIGRefType.LAKE, + ref_type=ActivityRefType.LAKE, ) ], time=datetime.fromisoformat(source_spot["updated_at"].replace("Z", "+00:00")).timestamp(), diff --git a/providers/spot/parksnpeaks.py b/providers/spot/parksnpeaks.py index 2426142..dcf75bb 100644 --- a/providers/spot/parksnpeaks.py +++ b/providers/spot/parksnpeaks.py @@ -7,7 +7,7 @@ import requests from core.constants import HTTP_HEADERS from core.enums import Mode -from data.sig_ref import SIGRef +from data.activity_ref import ActivityRef from data.spot import Spot from providers.spot.http_spot_provider import HTTPSpotProvider @@ -20,7 +20,7 @@ class ParksNPeaks(HTTPSpotProvider): POLL_INTERVAL_SEC = 120 SPOTS_URL = "https://www.parksnpeaks.org/api/ALL" SUBMIT_URL = "https://www.parksnpeaks.org/api/SPOT/" - SUBMITTABLE_SIGS = [ + SUBMITTABLE_ACTIVITIES = [ "POTA", "SOTA", "WWFF", @@ -64,26 +64,26 @@ class ParksNPeaks(HTTPSpotProvider): if not spot.de_call and m: spot.de_call = str(m.group(1)) - # Record SIG information. Sometimes we get a "SIG" of "QRP", which we ignore as it's not a programme with a - # defined set of references - sig = source_spot["actClass"].upper() - sig_ref = source_spot["actSiteID"] - if sig and sig != "" and sig != "QRP" and sig_ref and sig_ref != "": - spot.sig = sig - sig_refs = [ - SIGRef( + # Record activity information. Sometimes we get an activity of "QRP", which we ignore as it's not a + # programme with a defined set of references + activity = source_spot["actClass"].upper() + ref_id = source_spot["actSiteID"] + if activity and activity != "" and activity != "QRP" and ref_id and ref_id != "": + spot.sig = activity + activity_refs = [ + ActivityRef( id=source_spot["actSiteID"], sig=source_spot["actClass"].upper(), ) ] - spot.sig_refs = sig_refs + spot.sig_refs = activity_refs # Free text location is not present in all spots, so only add it if it's set if "actLocation" in source_spot and source_spot["actLocation"] != "": - sig_refs[0].name = source_spot["actLocation"] + activity_refs[0].name = source_spot["actLocation"] # Log a warning for the developer if PnP gives us an unknown programme we've never seen before - if sig not in [ + if activity not in [ "POTA", "SOTA", "WWFF", @@ -94,14 +94,14 @@ class ParksNPeaks(HTTPSpotProvider): "SANPCPA", "LLOTA", ]: - logger.warning(f"PNP spot found with sig {sig}, developer needs to add support for this!") + logger.warning(f"PNP spot found with activity {activity}, developer needs to add support for this!") # Add new spot to the list new_spots.append(spot) return new_spots - def can_submit_spot(self, sig): - return sig in self.SUBMITTABLE_SIGS + def can_submit_spot(self, activity): + return activity in self.SUBMITTABLE_ACTIVITIES def submit_spot(self, spot, credentials): # TODO test this works @@ -111,11 +111,11 @@ class ParksNPeaks(HTTPSpotProvider): raise ValueError( "Parks N Peaks user ID and API key are required. Get yours from your Parks N Peaks account." ) - sig_ref = spot.sig_refs[0].id if spot.sig_refs else "" + ref_id = spot.sig_refs[0].id if spot.sig_refs else "" body = { "actClass": spot.sig or "", "actCallsign": spot.dx_call, - "actSite": sig_ref, + "actSite": ref_id, "mode": spot.mode or "", "freq": str(spot.freq / 1000000.0), "comments": spot.comment or "", diff --git a/providers/spot/pota.py b/providers/spot/pota.py index 92faabe..662b182 100644 --- a/providers/spot/pota.py +++ b/providers/spot/pota.py @@ -4,8 +4,8 @@ import pytz import requests from core.constants import HTTP_HEADERS -from core.enums import Mode, SIGRefType -from data.sig_ref import SIGRef +from core.enums import ActivityRefType, Mode +from data.activity_ref import ActivityRef from data.spot import Spot from providers.spot.http_spot_provider import HTTPSpotProvider @@ -35,13 +35,13 @@ class POTA(HTTPSpotProvider): comment=source_spot["comments"], sig="POTA", sig_refs=[ - SIGRef( + ActivityRef( id=source_spot["reference"], sig="POTA", name=source_spot["name"], latitude=source_spot["latitude"], longitude=source_spot["longitude"], - ref_type=SIGRefType.PARK + ref_type=ActivityRefType.PARK ) ], time=datetime.strptime(source_spot["spotTime"], "%Y-%m-%dT%H:%M:%S") @@ -57,8 +57,8 @@ class POTA(HTTPSpotProvider): new_spots.append(spot) return new_spots - def can_submit_spot(self, sig): - return sig == "POTA" + def can_submit_spot(self, activity): + return activity == "POTA" def submit_spot(self, spot, credentials): sig_ref = spot.sig_refs[0].id if spot.sig_refs else None diff --git a/providers/spot/sota.py b/providers/spot/sota.py index c2f153f..897184f 100644 --- a/providers/spot/sota.py +++ b/providers/spot/sota.py @@ -5,8 +5,8 @@ import requests from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout from core.constants import HTTP_HEADERS -from core.enums import Mode, SIGRefType -from data.sig_ref import SIGRef +from core.enums import ActivityRefType, Mode +from data.activity_ref import ActivityRef from data.spot import Spot from providers.spot.http_spot_provider import HTTPSpotProvider @@ -58,14 +58,14 @@ class SOTA(HTTPSpotProvider): comment=source_spot["comments"], sig="SOTA", sig_refs=[ - SIGRef( + ActivityRef( id=source_spot["summitCode"], sig="SOTA", name=source_spot["summitName"], latitude=source_spot["latitude"], longitude=source_spot["longitude"], activation_score=source_spot["points"], - ref_type=SIGRefType.SUMMIT, + ref_type=ActivityRefType.SUMMIT, ) ], dx_latitude=source_spot["latitude"], @@ -82,8 +82,8 @@ class SOTA(HTTPSpotProvider): logger.warning("Timeout when accessing SOTA spots API.") return new_spots - def can_submit_spot(self, sig): - return sig == "SOTA" + def can_submit_spot(self, activity): + return activity == "SOTA" def submit_spot(self, spot, credentials): # TODO test this method works diff --git a/providers/spot/spot_provider.py b/providers/spot/spot_provider.py index 5cc5685..88df0a1 100644 --- a/providers/spot/spot_provider.py +++ b/providers/spot/spot_provider.py @@ -60,8 +60,8 @@ class SpotProvider: raise NotImplementedError("Subclasses must implement this method") - def can_submit_spot(self, sig): - """Return True if this provider supports submitting spots upstream for the given SIG.""" + def can_submit_spot(self, activity): + """Return True if this provider supports submitting spots upstream for the given activity.""" return False diff --git a/providers/spot/tiles.py b/providers/spot/tiles.py index 2af03fa..0dfd7d4 100644 --- a/providers/spot/tiles.py +++ b/providers/spot/tiles.py @@ -4,8 +4,8 @@ from datetime import datetime import requests from core.constants import HTTP_HEADERS -from core.enums import LocationSourceForSpot, Mode, SIGRefType -from data.sig_ref import SIGRef +from core.enums import ActivityRefType, LocationSourceForSpot, Mode +from data.activity_ref import ActivityRef from data.spot import Spot from providers.spot.http_spot_provider import HTTPSpotProvider @@ -60,15 +60,15 @@ class Tiles(HTTPSpotProvider): comment=source_spot["notes"], sig="Tiles", # Tiles spots can include POTA & SOTA references, but ignore those on the basis that we will get them separately from the POTA/SOTA providers anyway. - # Just take the grid reference itself as the single Tiles SIG reference. + # Just take the grid reference itself as the single Tiles activity reference. sig_refs=[ - SIGRef( + ActivityRef( id=source_spot["maidenhead_grid"], sig="Tiles", name=source_spot["maidenhead_grid"], latitude=source_spot["latitude"], longitude=source_spot["longitude"], - ref_type=SIGRefType.GRID + ref_type=ActivityRefType.GRID ) ], time=datetime.fromisoformat(source_spot["created_at"].replace("Z", "+00:00")).timestamp(), @@ -83,8 +83,8 @@ class Tiles(HTTPSpotProvider): new_spots.append(spot) return new_spots - def can_submit_spot(self, sig): - return sig == "Tiles" + def can_submit_spot(self, activity): + return activity == "Tiles" def submit_spot(self, spot, credentials): # Tiles on the air currently only supports *self* spots diff --git a/providers/spot/towers.py b/providers/spot/towers.py index 995410f..1b6128c 100644 --- a/providers/spot/towers.py +++ b/providers/spot/towers.py @@ -3,8 +3,8 @@ from datetime import datetime import pytz -from core.enums import SIGRefType -from data.sig_ref import SIGRef +from core.enums import ActivityRefType +from data.activity_ref import ActivityRef from data.spot import Spot from providers.spot.http_spot_provider import HTTPSpotProvider @@ -35,7 +35,7 @@ class Towers(HTTPSpotProvider): freq=likely_freq, comment=source_spot["comment"], sig="Towers", - sig_refs=[SIGRef(id=source_spot["ref"], sig="Towers", ref_type=SIGRefType.TOWER)], + sig_refs=[ActivityRef(id=source_spot["ref"], sig="Towers", ref_type=ActivityRefType.TOWER)], time=datetime.strptime(response_json["updated"][:10] + source_spot["time"], "%Y-%m-%d%H:%M") .replace(tzinfo=pytz.utc) .timestamp(), diff --git a/providers/spot/wota.py b/providers/spot/wota.py index ceb08ac..138f94e 100644 --- a/providers/spot/wota.py +++ b/providers/spot/wota.py @@ -8,8 +8,8 @@ import pytz from rss_parser import Parser from rss_parser.models.rss import RSS -from core.enums import Mode, SIGRefType -from data.sig_ref import SIGRef +from core.enums import ActivityRefType, Mode +from data.activity_ref import ActivityRef from data.spot import Spot from providers.spot.http_spot_provider import HTTPSpotProvider @@ -91,7 +91,7 @@ class WOTA(HTTPSpotProvider): mode=Mode.from_name(mode), comment=comment, sig="WOTA", - sig_refs=[SIGRef(id=ref, sig="WOTA", name=ref_name, ref_type=SIGRefType.SUMMIT)] if ref else [], + sig_refs=[ActivityRef(id=ref, sig="WOTA", name=ref_name, ref_type=ActivityRefType.SUMMIT)] if ref else [], time=time.timestamp(), ) @@ -104,8 +104,8 @@ class WOTA(HTTPSpotProvider): return new_spots - def can_submit_spot(self, sig): - return sig == "WOTA" + def can_submit_spot(self, activity): + return activity == "WOTA" def submit_spot(self, spot, credentials): # TODO Ask M5TEA if he's happy to share how this is done from his app diff --git a/providers/spot/wwbota.py b/providers/spot/wwbota.py index 3525312..f168ff5 100644 --- a/providers/spot/wwbota.py +++ b/providers/spot/wwbota.py @@ -1,8 +1,8 @@ import json from datetime import datetime -from core.enums import Mode, SIGRefType -from data.sig_ref import SIGRef +from core.enums import ActivityRefType, Mode +from data.activity_ref import ActivityRef from data.spot import Spot from providers.spot.sse_spot_provider import SSESpotProvider @@ -21,15 +21,15 @@ class WWBOTA(SSESpotProvider): # n-fer activations. refs = [] for ref in source_spot["references"]: - sigref = SIGRef( + activity_ref = ActivityRef( id=ref["reference"], sig="WWBOTA", name=ref["name"], latitude=ref["lat"], longitude=ref["long"], - ref_type=SIGRefType.BUNKER, + ref_type=ActivityRefType.BUNKER, ) - refs.append(sigref) + refs.append(activity_ref) spot = Spot( source=self.name, @@ -52,8 +52,8 @@ class WWBOTA(SSESpotProvider): # WWBOTA does support a special "Test" spot type, we need to avoid adding that. return spot if source_spot["type"] != "Test" else None - def can_submit_spot(self, sig): - return sig == "WWBOTA" + def can_submit_spot(self, activity): + return activity == "WWBOTA" def submit_spot(self, spot, credentials): # TODO: Implement. WWBOTA API docs cover this: https://api.wwbota.org/#tag/Spots/operation/create_spot_spots__post diff --git a/providers/spot/wwff.py b/providers/spot/wwff.py index 6623ec1..ab6667e 100644 --- a/providers/spot/wwff.py +++ b/providers/spot/wwff.py @@ -2,8 +2,8 @@ from datetime import datetime import pytz -from core.enums import Mode, SIGRefType -from data.sig_ref import SIGRef +from core.enums import ActivityRefType, Mode +from data.activity_ref import ActivityRef from data.spot import Spot from providers.spot.http_spot_provider import HTTPSpotProvider @@ -32,13 +32,13 @@ class WWFF(HTTPSpotProvider): comment=source_spot["remarks"], sig="WWFF", sig_refs=[ - SIGRef( + ActivityRef( id=source_spot["reference"], sig="WWFF", name=source_spot["reference_name"], latitude=source_spot["latitude"], longitude=source_spot["longitude"], - ref_type=SIGRefType.PARK + ref_type=ActivityRefType.PARK ) ], time=datetime.fromtimestamp(source_spot["spot_time"], tz=pytz.UTC).timestamp(), @@ -51,8 +51,8 @@ class WWFF(HTTPSpotProvider): new_spots.append(spot) return new_spots - def can_submit_spot(self, sig): - return sig == "WWFF" + def can_submit_spot(self, activity): + return activity == "WWFF" def submit_spot(self, spot, credentials): # TODO: Implement. Spotting to WWFF should be possible, need to look up the Spotline docs or copy approach from diff --git a/providers/spot/xota.py b/providers/spot/xota.py index a1c1ca4..a8544aa 100644 --- a/providers/spot/xota.py +++ b/providers/spot/xota.py @@ -4,43 +4,43 @@ from datetime import datetime import pytz from core.enums import Mode -from data.sig_ref import SIGRef +from data.activity_ref import ActivityRef from data.spot import Spot from providers.spot.websocket_spot_provider import WebsocketSpotProvider class XOTA(WebsocketSpotProvider): """Spot provider for servers based on the "xOTA" software at https://github.com/nischu/xOTA/ - The provider typically doesn't give us a lat/lon or SIG explicitly, so our own config provides a SIG which we can - then use for lookups. This functionality is implemented for Toilets on the Air events, of which there are - several - so a plain lookup of a "TOTA reference" doesn't make sense, it depends on which TOTA, which is why we also - provide a sig_ref_prefix in our config. This is applied to the reference ID, so e.g. "T-01" at C3 might become - "C3 T-01". This allows us to provide location lookups for TOTA at several conferences.""" + The provider typically doesn't give us a lat/lon or activity explicitly, so our own config provides an activity + which we can then use for lookups. This functionality is implemented for Toilets on the Air events, of which + there are several - so a plain lookup of a "TOTA reference" doesn't make sense, it depends on which TOTA, which + is why we also provide a sig_ref_prefix in our config. This is applied to the reference ID, so e.g. "T-01" at C3 + might become "C3 T-01". This allows us to provide location lookups for TOTA at several conferences.""" LOCATION_DATA = {} - SIG = None + ACTIVITY = None def __init__(self, provider_config): name = provider_config.get("name", "xOTA") super().__init__(name, provider_config, provider_config["url"]) - self.SIG = str(provider_config["sig"]) if "sig" in provider_config else None - self._sig_ref_prefix = str(provider_config["sig_ref_prefix"]) if "sig_ref_prefix" in provider_config else "" + self.ACTIVITY = str(provider_config["sig"]) if "sig" in provider_config else None + self._activity_ref_prefix = str(provider_config["sig_ref_prefix"]) if "sig_ref_prefix" in provider_config else "" def _ws_message_to_spot(self, b): string = b.decode("utf-8") source_spot = json.loads(string) - ref_id = f"{self._sig_ref_prefix} {source_spot['reference']['title']}" + ref_id = f"{self._activity_ref_prefix} {source_spot['reference']['title']}" spot = Spot( source=self.name, source_id=source_spot["id"], dx_call=source_spot["stationCallSign"].upper(), freq=float(source_spot["freq"]) * 1000, mode=Mode.from_name(source_spot["mode"].upper()), - sig=self.SIG, + sig=self.ACTIVITY, sig_refs=[ - SIGRef( + ActivityRef( id=ref_id, - sig=self.SIG or "", + sig=self.ACTIVITY or "", url=source_spot["reference"]["website"], ) ], diff --git a/providers/spot/zlota.py b/providers/spot/zlota.py index 0439750..0ca93c0 100644 --- a/providers/spot/zlota.py +++ b/providers/spot/zlota.py @@ -3,7 +3,7 @@ from datetime import datetime import pytz from core.enums import Mode -from data.sig_ref import SIGRef +from data.activity_ref import ActivityRef from data.spot import Spot from providers.spot.http_spot_provider import HTTPSpotProvider @@ -37,7 +37,7 @@ class ZLOTA(HTTPSpotProvider): comment=source_spot["comments"], sig="ZLOTA", sig_refs=[ - SIGRef( + ActivityRef( id=source_spot["reference"], sig="ZLOTA", name=source_spot["name"], @@ -51,8 +51,8 @@ class ZLOTA(HTTPSpotProvider): new_spots.append(spot) return new_spots - def can_submit_spot(self, sig): - return sig == "ZLOTA" + def can_submit_spot(self, activity): + return activity == "ZLOTA" def submit_spot(self, spot, credentials): # TODO: Implement. Spotting to ZLOTA is supported via POST, see https://ontheair.nz/api diff --git a/static/apidocs/openapi.yml b/static/apidocs/openapi.yml index 3c9aae6..f0ecbcc 100644 --- a/static/apidocs/openapi.yml +++ b/static/apidocs/openapi.yml @@ -109,7 +109,7 @@ tags: - name: General description: Server status and enumeration options. - name: Utilities - description: Utility lookups for callsigns, SIG references, and Maidenhead grids. + description: Utility lookups for callsigns, activity references, and Maidenhead grids. paths: /spots: @@ -129,9 +129,9 @@ paths: - $ref: '#/components/parameters/SpotMaxAge' - $ref: '#/components/parameters/SpotReceivedSince' - $ref: '#/components/parameters/SpotSource' - - $ref: '#/components/parameters/SpotSig' - - $ref: '#/components/parameters/SpotNeedsSig' - - $ref: '#/components/parameters/SpotNeedsSigRef' + - $ref: '#/components/parameters/SpotActivity' + - $ref: '#/components/parameters/SpotNeedsActivity' + - $ref: '#/components/parameters/SpotNeedsActivityRef' - $ref: '#/components/parameters/SpotBand' - $ref: '#/components/parameters/SpotMode' - $ref: '#/components/parameters/SpotModeType' @@ -171,9 +171,9 @@ paths: operationId: spots-stream parameters: - $ref: '#/components/parameters/SpotSource' - - $ref: '#/components/parameters/SpotSig' - - $ref: '#/components/parameters/SpotNeedsSig' - - $ref: '#/components/parameters/SpotNeedsSigRef' + - $ref: '#/components/parameters/SpotActivity' + - $ref: '#/components/parameters/SpotNeedsActivity' + - $ref: '#/components/parameters/SpotNeedsActivityRef' - $ref: '#/components/parameters/SpotBand' - $ref: '#/components/parameters/SpotMode' - $ref: '#/components/parameters/SpotModeType' @@ -218,7 +218,7 @@ paths: - $ref: '#/components/parameters/AlertDxpeditionsSkipMaxDurationCheck' - $ref: '#/components/parameters/AlertContestsSkipMaxDurationCheck' - $ref: '#/components/parameters/AlertSource' - - $ref: '#/components/parameters/AlertSig' + - $ref: '#/components/parameters/AlertActivity' - $ref: '#/components/parameters/AlertDxContinent' - $ref: '#/components/parameters/AlertDxCallIncludes' - $ref: '#/components/parameters/AlertTextIncludes' @@ -254,7 +254,7 @@ paths: - $ref: '#/components/parameters/AlertDxpeditionsSkipMaxDurationCheck' - $ref: '#/components/parameters/AlertContestsSkipMaxDurationCheck' - $ref: '#/components/parameters/AlertSource' - - $ref: '#/components/parameters/AlertSig' + - $ref: '#/components/parameters/AlertActivity' - $ref: '#/components/parameters/AlertDxContinent' - $ref: '#/components/parameters/AlertDxCallIncludes' - $ref: '#/components/parameters/AlertTextIncludes' @@ -392,24 +392,24 @@ paths: get: tags: - Utilities - summary: Look up SIG ref details + summary: Look up activity ref details description: > - Perform a lookup of data about a certain reference, providing the SIG and the ID of the - reference. A SIGRef structure will be returned containing the SIG and ID, plus any other + Perform a lookup of data about a certain reference, providing the activity and the ID of the + reference. An ActivityRef structure will be returned containing the activity and ID, plus any other information Spothole could find about it. operationId: sigref parameters: - - $ref: '#/components/parameters/SigRefSig' - - $ref: '#/components/parameters/SigRefId' + - $ref: '#/components/parameters/ActivityRefLookupActivity' + - $ref: '#/components/parameters/ActivityRefLookupId' responses: '200': description: Success content: application/json: schema: - $ref: '#/components/schemas/SIGRef' + $ref: '#/components/schemas/ActivityRef' '422': - description: Validation error e.g. SIG not supported or reference format incorrect + description: Validation error e.g. activity not supported or reference format incorrect content: application/json: schema: @@ -452,7 +452,7 @@ paths: description: > Supply a JSON object containing a `spot` sub-object (the spot data) and an optional `handling` sub-object containing server-side instructions such as upstream submission). Check `spot_submit_providers` in the - `/options` response to see which SIGs and providers support upstream submission. cURL example: + `/options` response to see which activities and providers support upstream submission. cURL example: `curl --request POST --header \"Content-Type: application/json\" --data '{\"spot\":{\"dx_call\":\"M0TRT\",\"time\":1760019539,\"freq\":14200000,\"comment\":\"Test spot please ignore\",\"de_call\":\"M0TRT\"}}' https://spothole.app/api/v2/spot`" operationId: spot requestBody: @@ -550,37 +550,37 @@ components: named some sources like clusters differently, and provided multiple options. schema: $ref: "#/components/schemas/Source" - SpotSig: + SpotActivity: name: sig in: query description: > - Limit the spots to only ones from one or more Special Interest Groups provided as an argument. - To select more than one SIG, supply a comma-separated list. The special `sig` name `NO_SIG` - matches spots with no sig set. You can use `sig=NO_SIG` to specifically only return generic - spots with no associated SIG. You can also use combinations to request for example POTA + no - SIG, but reject other SIGs. If you want to request 'every SIG and not No SIG', see the + Limit the spots to only ones from one or more activities provided as an argument. + To select more than one activity, supply a comma-separated list. The special `sig` name `NO_SIG` + matches spots with no activity set. You can use `sig=NO_SIG` to specifically only return generic + spots with no associated activity. You can also use combinations to request for example POTA + no + activity, but reject other activities. If you want to request 'every activity and not No Activity', see the `needs_sig` query parameter for a shortcut. schema: - $ref: "#/components/schemas/SIGNameIncludingNoSIG" - SpotNeedsSig: + $ref: "#/components/schemas/ActivityNameIncludingNoSig" + SpotNeedsActivity: name: needs_sig in: query description: > - Limit the spots to only ones with a Special Interest Group such as POTA. Because supplying all - known SIGs as a `sigs` parameter is unwieldy, and leaving `sigs` blank will also return spots - with *no* SIG, this parameter can be set true to return only spots with a SIG, regardless of + Limit the spots to only ones with an activity such as POTA. Because supplying all + known activities as a `sigs` parameter is unwieldy, and leaving `sigs` blank will also return spots + with *no* activity, this parameter can be set true to return only spots with an activity, regardless of what it is, so long as it's not blank. This is the equivalent of supplying the `sig` query - param with a list of every known SIG apart from the special `NO_SIG` value. This is what Field + param with a list of every known activity apart from the special `NO_SIG` value. This is what Field Spotter uses to exclude generic cluster spots and only retrieve xOTA things. schema: type: boolean default: false - SpotNeedsSigRef: + SpotNeedsActivityRef: name: needs_sig_ref in: query description: > Limit the spots to only ones which have at least one reference (e.g. a park reference) for - Special Interest Groups such as POTA. + activities such as POTA. schema: type: boolean default: false @@ -714,15 +714,15 @@ components: comma-separated list. schema: $ref: "#/components/schemas/Source" - AlertSig: + AlertActivity: name: sig in: query description: > - Limit the alerts to only ones from one or more Special Interest Groups. To select more than one - SIG, supply a comma-separated list. The special value 'NO_SIG' can be included to return alerts - specifically without an associated SIG (i.e. general DXpeditions). + Limit the alerts to only ones from one or more activities. To select more than one + activity, supply a comma-separated list. The special value 'NO_SIG' can be included to return alerts + specifically without an associated activity (i.e. general DXpeditions). schema: - $ref: "#/components/schemas/SIGNameIncludingNoSIG" + $ref: "#/components/schemas/ActivityNameIncludingNoSig" AlertDxContinent: name: dx_continent in: query @@ -830,17 +830,17 @@ components: schema: type: string example: M0TRT - SigRefSig: + ActivityRefLookupActivity: name: sig in: query - description: Special Interest Group (SIG), e.g. outdoor activity programme such as POTA + description: Activity, e.g. outdoor activity programme such as POTA (still named "sig" in the API for backwards compatibility) required: true schema: - $ref: "#/components/schemas/SIGName" - SigRefId: + $ref: "#/components/schemas/ActivityName" + ActivityRefLookupId: name: id in: query - description: ID of a reference in that SIG + description: ID of a reference in that activity required: true schema: type: string @@ -876,7 +876,7 @@ components: - UKPacketNet example: POTA - SIGName: + ActivityName: type: string enum: - POTA @@ -914,7 +914,7 @@ components: - Toilets example: POTA - SIGType: + ActivityType: type: string enum: - WORLDWIDE @@ -922,14 +922,14 @@ components: - EVENT example: WORLDWIDE - SIGNameIncludingNoSIG: + ActivityNameIncludingNoSig: oneOf: - - $ref: "#/components/schemas/SIGName" + - $ref: "#/components/schemas/ActivityName" - type: string enum: [ NO_SIG ] example: POTA - SIGRefType: + ActivityRefType: type: string enum: - PARK @@ -1075,26 +1075,26 @@ components: - NONE example: "HOME QTH" - SIGRef: + ActivityRef: type: object properties: id: type: string - description: SIG reference ID. + description: Activity reference ID. example: GB-0001 sig: - description: SIG that this reference is in. - $ref: "#/components/schemas/SIGName" + description: Activity that this reference is in. Still named "sig" in the API for backwards compatibility. + $ref: "#/components/schemas/ActivityName" name: type: string - description: SIG reference name + description: Activity reference name example: Null Country Park ref_type: - description: SIG reference type - $ref: "#/components/schemas/SIGRefType" + description: Activity reference type + $ref: "#/components/schemas/ActivityRefType" url: type: string - description: SIG reference URL, which the user can look up for more information + description: Activity reference URL, which the user can look up for more information example: "https://pota.app/#/park/GB-0001" grid: type: string @@ -1136,7 +1136,7 @@ components: dx_qth: type: string description: > - QTH of the operator that has been spotted. This could be from any SIG refs or could be + QTH of the operator that has been spotted. This could be from any activity refs or could be from online lookup of their home QTH. example: Dorset dx_country: @@ -1194,7 +1194,7 @@ components: dx_location_source: description: > Where we got the DX location (grid/latitude/longitude) from. If this was from the spot - itself, or from a lookup of the SIG ref (e.g. park) it's likely quite accurate, but if + itself, or from a lookup of the activity ref (e.g. park) it's likely quite accurate, but if we had to fall back to QRZ lookup, or even a location based on the DXCC itself, it will be a lot less accurate. "SPOT" indicates the location source was the spot itself from the spotting service. "SIG REF LOOKUP" indicates that the spot didn't provide a location, @@ -1308,13 +1308,13 @@ components: description: Comment left by the spotter, if any example: "59 in NY 73" sig: - description: Special Interest Group (SIG), e.g. outdoor activity programme such as POTA - $ref: "#/components/schemas/SIGName" + description: Activity, e.g. outdoor activity programme such as POTA (still named "sig" in the API for backwards compatibility) + $ref: "#/components/schemas/ActivityName" sig_refs: type: array items: - $ref: '#/components/schemas/SIGRef' - description: SIG references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO + $ref: '#/components/schemas/ActivityRef' + description: Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO. Still named "sig_refs" in the API for backwards compatibility. qrt: type: boolean description: QRT state. Some APIs return spots marked as QRT. Otherwise we can check the comments. @@ -1358,13 +1358,13 @@ components: If true, forward the spot to an external upstream provider (e.g. POTA, SOTA) rather than only adding it to this Spothole server. Requires `sig`, at least one `sig_refs` entry, and `upstream_provider` to be set. Check `spot_submit_providers` in the - /options response to see which SIGs and providers support this. + /options response to see which activities and providers support this. default: false upstream_provider: type: string description: > Name of the upstream provider to submit the spot to, e.g. "POTA" or "SOTA". Must - match one of the provider names returned in `spot_submit_providers` for the chosen SIG. + match one of the provider names returned in `spot_submit_providers` for the chosen activity. example: POTA upstream_credentials: type: object @@ -1481,13 +1481,13 @@ components: description: Comment made by the activator, if any example: "2025 DXpedition to null island" sig: - description: Special Interest Group (SIG), e.g. outdoor activity programme such as POTA - $ref: "#/components/schemas/SIGName" + description: Activity, e.g. outdoor activity programme such as POTA (still named "sig" in the API for backwards compatibility) + $ref: "#/components/schemas/ActivityName" sig_refs: type: array items: - $ref: '#/components/schemas/SIGRef' - description: SIG references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO + $ref: '#/components/schemas/ActivityRef' + description: Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO. Still named "sig_refs" in the API for backwards compatibility. alert_type: description: "The type of alert this is: xOTA, DXpedition, or Contest." $ref: "#/components/schemas/AlertType" @@ -1581,28 +1581,32 @@ components: description: The end frequency of this band, in Hz. example: 7200000 - SIG: + Activity: type: object + description: > + Represents an activity (a term which replaces the older "Special Interest Group" or "SIG" terminology, + though `sig`-prefixed field names remain for API backwards compatibility). properties: name: - description: The abbreviated name of the SIG - $ref: "#/components/schemas/SIGName" + description: The abbreviated name of the activity + $ref: "#/components/schemas/ActivityName" description: type: string - description: The full name of the SIG + description: The full name of the activity example: Parks on the Air sig_type: type: boolean description: > - Whether the SIG is worldwide, regional, or for a specific event. Generally for Spothole's own internal use, - clients probably won't need this. Used to group them in the web UI. - $ref: "#/components/schemas/SIGType" + Whether the activity is worldwide, regional, or for a specific event. Generally for Spothole's own + internal use, clients probably won't need this. Used to group them in the web UI. Still named + "sig_type" in the API for backwards compatibility. + $ref: "#/components/schemas/ActivityType" comment_names: type: array description: > - Names by which this SIG may be referred to in cluster spot comments. Most SIGs have a + Names by which this activity may be referred to in cluster spot comments. Most activities have a single entry matching their programme name (e.g. ["POTA"]), but some have none (where - the name is ambiguous with other SIGs, such as Tiles and Toilets on the Air) or multiple + the name is ambiguous with other activities, such as Tiles and Toilets on the Air) or multiple entries (e.g. WWBOTA accepts both "WWBOTA" and "BOTA" since the latter is often used). items: type: string @@ -1610,26 +1614,27 @@ components: ref_regex: type: string description: > - Regex that matches this SIG's reference IDs. Generally for Spothole's own internal use, + Regex that matches this activity's reference IDs. Generally for Spothole's own internal use, clients probably won't need this. example: "[A-Z]{2}\\-\\d+" refs_globally_unique: type: boolean description: > - Identifies that the SIG's reference ID structure defined by its regex is unique across all programmes and - anything else we expect a user to put in a spot comment, and therefore we can pull references out of - spot comments without also needing to see the SIG name first. For example, "OHFF-1234" or "B/G-1234" are - obviously WWFF and WWBOTA, nothing else looks like those. But "SZ09" could be WAB or Tiles, "GB1234" could - conceivably be POTA or ILLW, etc. Generally for Spothole's own internal use, clients probably won't need - this. + Identifies that the activity's reference ID structure defined by its regex is unique across all + programmes and anything else we expect a user to put in a spot comment, and therefore we can pull + references out of spot comments without also needing to see the activity name first. For example, + "OHFF-1234" or "B/G-1234" are obviously WWFF and WWBOTA, nothing else looks like those. But "SZ09" + could be WAB or Tiles, "GB1234" could conceivably be POTA or ILLW, etc. Generally for Spothole's own + internal use, clients probably won't need this. icon: type: string - description: Icon from the Font Awesome set that represents this SIG, for use in the front end. + description: Icon from the Font Awesome set that represents this activity, for use in the front end. example: "fa-tree" region_flag: type: string description: > - Flag emoji, if this SIG is specific to a country or region. If null, this SIG is treated as worldwide. + Flag emoji, if this activity is specific to a country or region. If null, this activity is treated as + worldwide. example: "🇺🇳" SolarConditions: @@ -1978,12 +1983,12 @@ components: is zero, the provider has never updated. example: 1759579508 - SIGRefDataProviderStatus: + ActivityRefDataProviderStatus: type: object properties: sig_name: type: string - description: The name of the SIG. + description: The name of the activity. Still named "sig_name" in the API for backwards compatibility. example: WWFF enabled: type: boolean @@ -2148,9 +2153,9 @@ components: $ref: '#/components/schemas/StaticDataProviderStatus' sig_ref_data_providers: type: array - description: An array of all the SIG reference data providers. + description: An array of all the activity reference data providers. items: - $ref: '#/components/schemas/SIGRefDataProviderStatus' + $ref: '#/components/schemas/ActivityRefDataProviderStatus' callsign_data_providers: type: array description: An array of all the callsign data providers. @@ -2179,9 +2184,9 @@ components: example: "PHONE" sigs: type: array - description: An array of all the supported Special Interest Groups. + description: An array of all the supported activities. items: - $ref: '#/components/schemas/SIG' + $ref: '#/components/schemas/Activity' spot_providers: type: array description: An array of all the supported spot data sources. @@ -2228,9 +2233,9 @@ components: spot_submit_providers: type: object description: > - A map of SIG name to a list of provider names that support upstream spot submission for that SIG. - If a SIG appears as a key here, the POST /spot endpoint accepts `submit_upstream: true` for - spots with that SIG, and will forward the spot to one of the listed providers. Omitted if no + A map of activity name to a list of provider names that support upstream spot submission for that + activity. If an activity appears as a key here, the POST /spot endpoint accepts `submit_upstream: true` + for spots with that activity, and will forward the spot to one of the listed providers. Omitted if no providers support upstream submission. additionalProperties: type: array @@ -2258,7 +2263,7 @@ components: qth: type: string description: > - QTH of the operator. This could be from any SIG refs or could be from online lookup of + QTH of the operator. This could be from any activity refs or could be from online lookup of their home QTH. example: Dorset country: diff --git a/static/css/style.css b/static/css/style.css index d3fdc97..ec2351f 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -165,7 +165,7 @@ a.dx-link { font-weight: bold; } -a.sig-ref-link { +a.activity-ref-link { color: var(--bs-emphasis-color); text-decoration: none; } diff --git a/static/js/add-spot.js b/static/js/add-spot.js index 1dbe594..1d0130f 100644 --- a/static/js/add-spot.js +++ b/static/js/add-spot.js @@ -41,11 +41,11 @@ function loadOptions() { })); }); - // Populate SIG drop-down - $.each(options["sigs"], function (i, sig) { + // Populate activity drop-down + $.each(options["sigs"], function (i, activity) { $('#sig').append($('