mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-09-24 16:24:32 +00:00
First attempt at converting "sig" to "activity" for v3
This commit is contained in:
@@ -13,11 +13,10 @@ class ActivityRefDataProvider:
|
||||
"""Generic activity reference data provider class. Subclasses of this query the individual URLs or files for
|
||||
data."""
|
||||
|
||||
def __init__(self, sig_name, provider_config):
|
||||
"""Constructor. Note the parameter and attribute are still named "sig_name" for consistency with the API's
|
||||
"sig" field name."""
|
||||
def __init__(self, activity_name, provider_config):
|
||||
"""Constructor"""
|
||||
|
||||
self.sig_name = sig_name
|
||||
self.activity_name = activity_name
|
||||
self.enabled = provider_config["enabled"]
|
||||
self.last_update_time = datetime.min.replace(tzinfo=pytz.UTC)
|
||||
self.status = "Not Started" if self.enabled else "Disabled"
|
||||
@@ -43,7 +42,7 @@ class ActivityRefDataProvider:
|
||||
# transact()s is to fail if they can't get the lock (?!). This behaviour is fixed by retry=True.
|
||||
with DATA_STORE.activity_refs.transact(retry=True):
|
||||
for d in new_data:
|
||||
DATA_STORE.activity_refs.set(f"{self.sig_name}:{d.id}", d)
|
||||
DATA_STORE.activity_refs.set(f"{self.activity_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
|
||||
@@ -52,4 +51,4 @@ class ActivityRefDataProvider:
|
||||
break
|
||||
|
||||
self.reference_count = len(new_data)
|
||||
logger.info(f"Loaded {self.reference_count} references for {self.sig_name} into the data store.")
|
||||
logger.info(f"Loaded {self.reference_count} references for {self.activity_name} into the data store.")
|
||||
|
||||
@@ -25,7 +25,7 @@ class ARLHS(FileDownloadActivityRefDataProvider):
|
||||
ref_id = row["ARLHS"]
|
||||
new_data.append(
|
||||
ActivityRef(
|
||||
sig=self.ACTIVITY,
|
||||
activity=self.ACTIVITY,
|
||||
id=ref_id,
|
||||
name=row.get("Name", None),
|
||||
ref_type=ActivityRefType.LIGHTHOUSE,
|
||||
|
||||
@@ -30,7 +30,7 @@ class COTA(FileDownloadActivityRefDataProvider):
|
||||
|
||||
new_data.append(
|
||||
ActivityRef(
|
||||
sig=self.ACTIVITY,
|
||||
activity=self.ACTIVITY,
|
||||
id=ref_id,
|
||||
name=name,
|
||||
ref_type=ActivityRefType.CASTLE,
|
||||
|
||||
@@ -27,7 +27,12 @@ class DCE(FileDownloadActivityRefDataProvider):
|
||||
for index, row in df.iterrows():
|
||||
if row.iloc[0] and row.iloc[2]:
|
||||
new_data.append(
|
||||
ActivityRef(sig=self.ACTIVITY, id=row.iloc[0].strip(), name=row.iloc[2].strip(), ref_type=ActivityRefType.CASTLE)
|
||||
ActivityRef(
|
||||
activity=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
|
||||
|
||||
@@ -31,7 +31,12 @@ class DEFE(FileDownloadActivityRefDataProvider):
|
||||
|
||||
if row.iloc[0] and row.iloc[1]:
|
||||
new_data.append(
|
||||
ActivityRef(sig=self.ACTIVITY, id=row.iloc[0].strip(), name=row.iloc[1].strip(), ref_type=ActivityRefType.BUILDING)
|
||||
ActivityRef(
|
||||
activity=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
|
||||
|
||||
@@ -40,7 +40,7 @@ class DME(LocalFileActivityRefDataProvider):
|
||||
)
|
||||
|
||||
ref = ActivityRef(
|
||||
sig=self.ACTIVITY,
|
||||
activity=self.ACTIVITY,
|
||||
id=ref_id,
|
||||
ref_type=ActivityRefType.TOWN,
|
||||
name=f"{row['NOMBRE_ACTUAL']}, {row['PROVINCIA']}",
|
||||
|
||||
@@ -22,7 +22,12 @@ class DMUE(FileDownloadActivityRefDataProvider):
|
||||
for row in csv.reader(http_response.content.decode("utf-8-sig").splitlines(), delimiter=";"):
|
||||
if len(row) > 1 and row[0] and row[1]:
|
||||
new_data.append(
|
||||
ActivityRef(sig=self.ACTIVITY, id=row[0].strip(), name=row[1].strip(), ref_type=ActivityRefType.BUILDING)
|
||||
ActivityRef(
|
||||
activity=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
|
||||
|
||||
@@ -36,7 +36,11 @@ class DMVE(FileDownloadActivityRefDataProvider):
|
||||
continue
|
||||
|
||||
if ref and name:
|
||||
new_data.append(ActivityRef(sig=self.ACTIVITY, id=ref.strip(), name=name.strip(), ref_type=ActivityRefType.BUILDING))
|
||||
new_data.append(
|
||||
ActivityRef(
|
||||
activity=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
|
||||
|
||||
@@ -21,7 +21,9 @@ class DTMBA(FileDownloadActivityRefDataProvider):
|
||||
split = row.split(";")
|
||||
ref_id = split[0]
|
||||
ref_name = split[1]
|
||||
new_data.append(ActivityRef(sig=self.ACTIVITY, id=ref_id, name=ref_name, ref_type=ActivityRefType.BUILDING))
|
||||
new_data.append(
|
||||
ActivityRef(activity=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
|
||||
|
||||
@@ -41,8 +41,16 @@ class FEA(FileDownloadActivityRefDataProvider):
|
||||
# prefix and just use FEA-1234 or FEA 1234, so we add both copies to the database.
|
||||
ref_id_1 = row[0].strip()
|
||||
ref_id_2 = ref_id_1.replace("D-", "FEA-").replace("E-", "FEA-")
|
||||
new_data.append(ActivityRef(sig=self.ACTIVITY, id=ref_id_1, name=row[1].strip(), ref_type=ActivityRefType.LIGHTHOUSE))
|
||||
new_data.append(ActivityRef(sig=self.ACTIVITY, id=ref_id_2, name=row[1].strip(), ref_type=ActivityRefType.LIGHTHOUSE))
|
||||
new_data.append(
|
||||
ActivityRef(
|
||||
activity=self.ACTIVITY, id=ref_id_1, name=row[1].strip(), ref_type=ActivityRefType.LIGHTHOUSE
|
||||
)
|
||||
)
|
||||
new_data.append(
|
||||
ActivityRef(
|
||||
activity=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
|
||||
|
||||
@@ -16,19 +16,21 @@ 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):
|
||||
def __init__(self, activity_name, provider_config, url, poll_interval):
|
||||
"""Set up the provider, note poll_interval is in *days*."""
|
||||
super().__init__(sig_name, provider_config)
|
||||
super().__init__(activity_name, provider_config)
|
||||
self._url = url
|
||||
self._poll_interval = poll_interval
|
||||
self._thread = None
|
||||
self._url_data_cache = URLDataCache(f"activity_ref_data_{sig_name}")
|
||||
self._url_data_cache = URLDataCache(f"activity_ref_data_{activity_name}")
|
||||
|
||||
def start(self):
|
||||
# Fire off the polling thread. It will poll immediately on startup, then sleep for poll_interval between
|
||||
# subsequent polls, so start() returns immediately and the application can continue starting.
|
||||
logger.info(f"Set up query of {self.sig_name} activity ref data every {self._poll_interval!s} days.")
|
||||
self._thread = Thread(target=self._run, name=f"FileDownloadActivityRefDataProvider-{self.sig_name}", daemon=True)
|
||||
logger.info(f"Set up query of {self.activity_name} activity ref data every {self._poll_interval!s} days.")
|
||||
self._thread = Thread(
|
||||
target=self._run, name=f"FileDownloadActivityRefDataProvider-{self.activity_name}", daemon=True
|
||||
)
|
||||
self._thread.start()
|
||||
|
||||
def stop(self):
|
||||
@@ -36,7 +38,9 @@ class FileDownloadActivityRefDataProvider(ActivityRefDataProvider):
|
||||
if self._thread:
|
||||
self._thread.join(timeout=12)
|
||||
if self._thread.is_alive():
|
||||
logger.warning(f"{self.sig_name} activity ref data worker thread did not exit on time and will be killed.")
|
||||
logger.warning(
|
||||
f"{self.activity_name} activity ref data worker thread did not exit on time and will be killed."
|
||||
)
|
||||
|
||||
def _run(self):
|
||||
while True:
|
||||
@@ -48,7 +52,7 @@ class FileDownloadActivityRefDataProvider(ActivityRefDataProvider):
|
||||
try:
|
||||
# Request data from API. Use the data cache (with a TTL of 1 day) here, not as the main mechanism for
|
||||
# caching, but just so continual restarts of the software during testing don't hammer the servers.
|
||||
logger.debug(f"Downloading {self.sig_name} activity ref data...")
|
||||
logger.debug(f"Downloading {self.activity_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:
|
||||
@@ -60,20 +64,22 @@ class FileDownloadActivityRefDataProvider(ActivityRefDataProvider):
|
||||
|
||||
self.status = "OK"
|
||||
self.last_update_time = datetime.now(pytz.UTC)
|
||||
logger.debug(f"Received activity ref data for {self.sig_name}")
|
||||
logger.debug(f"Received activity ref data for {self.activity_name}")
|
||||
else:
|
||||
self.status = "Error"
|
||||
logger.warning(f"HTTP {http_response.status_code} when downloading activity ref data for {self.sig_name}.")
|
||||
logger.warning(
|
||||
f"HTTP {http_response.status_code} when downloading activity ref data for {self.activity_name}."
|
||||
)
|
||||
|
||||
except ConnectionError:
|
||||
self.status = "Error"
|
||||
logger.warning(f"Connection error when downloading activity ref data for {self.sig_name}.")
|
||||
logger.warning(f"Connection error when downloading activity ref data for {self.activity_name}.")
|
||||
except (ConnectTimeout, ReadTimeout):
|
||||
self.status = "Error"
|
||||
logger.warning(f"Timeout when downloading activity ref data for {self.sig_name}.")
|
||||
logger.warning(f"Timeout when downloading activity ref data for {self.activity_name}.")
|
||||
except Exception:
|
||||
self.status = "Error"
|
||||
logger.exception(f"Exception in HTTP Activity Ref Data Provider ({self.sig_name})")
|
||||
logger.exception(f"Exception in HTTP Activity Ref Data Provider ({self.activity_name})")
|
||||
self._stop_event.wait(timeout=1)
|
||||
|
||||
def _http_response_to_data(self, http_response):
|
||||
|
||||
@@ -24,7 +24,7 @@ class GMA(FileDownloadActivityRefDataProvider):
|
||||
ref_id = row["Reference"]
|
||||
new_data.append(
|
||||
ActivityRef(
|
||||
sig=self.ACTIVITY,
|
||||
activity=self.ACTIVITY,
|
||||
id=ref_id,
|
||||
name=row.get("Name", None),
|
||||
ref_type=ActivityRefType.SUMMIT,
|
||||
|
||||
@@ -25,7 +25,7 @@ class ILLW(FileDownloadActivityRefDataProvider):
|
||||
ref_id = row["ILLW"]
|
||||
new_data.append(
|
||||
ActivityRef(
|
||||
sig=self.ACTIVITY,
|
||||
activity=self.ACTIVITY,
|
||||
id=ref_id,
|
||||
name=row.get("Name", None),
|
||||
ref_type=ActivityRefType.LIGHTHOUSE,
|
||||
|
||||
@@ -42,7 +42,7 @@ class IOTA(FileDownloadActivityRefDataProvider):
|
||||
|
||||
new_data.append(
|
||||
ActivityRef(
|
||||
sig=self.ACTIVITY,
|
||||
activity=self.ACTIVITY,
|
||||
id=ref_id,
|
||||
name=ref["name"],
|
||||
ref_type=ActivityRefType.ISLAND,
|
||||
|
||||
@@ -30,7 +30,7 @@ class LLOTA(FileDownloadActivityRefDataProvider):
|
||||
|
||||
new_data.append(
|
||||
ActivityRef(
|
||||
sig=self.ACTIVITY,
|
||||
activity=self.ACTIVITY,
|
||||
id=ref_id,
|
||||
name=str(ref["name"]),
|
||||
ref_type=ActivityRefType.LAKE,
|
||||
|
||||
@@ -11,12 +11,12 @@ 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)
|
||||
def __init__(self, activity_name, provider_config, path):
|
||||
super().__init__(activity_name, provider_config)
|
||||
self._path = path
|
||||
|
||||
def start(self):
|
||||
logger.debug(f"Loading {self.sig_name} activity ref data from file.")
|
||||
logger.debug(f"Loading {self.activity_name} activity ref data from file.")
|
||||
try:
|
||||
new_data = self._file_to_data(self._path)
|
||||
if new_data:
|
||||
@@ -25,10 +25,10 @@ class LocalFileActivityRefDataProvider(ActivityRefDataProvider):
|
||||
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}")
|
||||
logger.info(f"Failed to load activity ref data for {self.activity_name}")
|
||||
except Exception:
|
||||
self.status = "Error"
|
||||
logger.exception(f"Exception in local file Activity Ref Data Provider ({self.sig_name})")
|
||||
logger.exception(f"Exception in local file Activity Ref Data Provider ({self.activity_name})")
|
||||
|
||||
def _file_to_data(self, path):
|
||||
"""Load a file on the given path and turn it into activity ref data."""
|
||||
|
||||
@@ -24,7 +24,7 @@ class MOTA(FileDownloadActivityRefDataProvider):
|
||||
ref_id = row["Reference"]
|
||||
new_data.append(
|
||||
ActivityRef(
|
||||
sig=self.ACTIVITY,
|
||||
activity=self.ACTIVITY,
|
||||
id=ref_id,
|
||||
name=row.get("Name", None),
|
||||
ref_type=ActivityRefType.MILL,
|
||||
|
||||
@@ -39,7 +39,7 @@ class PGA(FileDownloadActivityRefDataProvider):
|
||||
|
||||
new_data.append(
|
||||
ActivityRef(
|
||||
sig=self.ACTIVITY,
|
||||
activity=self.ACTIVITY,
|
||||
id=ref_id,
|
||||
name=name,
|
||||
ref_type=ActivityRefType.REGION,
|
||||
|
||||
@@ -17,9 +17,9 @@ class ParksNPeaksKMLActivityRefDataProvider(FileDownloadActivityRefDataProvider)
|
||||
|
||||
REF_PATTERN = re.compile(r"VKFF-\d+")
|
||||
|
||||
def __init__(self, sig_name, provider_config, url, poll_interval):
|
||||
def __init__(self, activity_name, provider_config, url, poll_interval):
|
||||
"""Set up the provider, note poll_interval is in *days*."""
|
||||
super().__init__(sig_name, provider_config, url, poll_interval)
|
||||
super().__init__(activity_name, provider_config, url, poll_interval)
|
||||
|
||||
def _http_response_to_data(self, http_response):
|
||||
new_data = []
|
||||
@@ -41,7 +41,7 @@ class ParksNPeaksKMLActivityRefDataProvider(FileDownloadActivityRefDataProvider)
|
||||
longitude, latitude = placemark.geometry.x, placemark.geometry.y
|
||||
|
||||
ref = ActivityRef(
|
||||
sig=self.sig_name,
|
||||
activity=self.activity_name,
|
||||
id=ref_id,
|
||||
name=placemark.name,
|
||||
ref_type=ActivityRefType.PARK,
|
||||
|
||||
@@ -24,7 +24,7 @@ class POTA(FileDownloadActivityRefDataProvider):
|
||||
ref_id = row["reference"]
|
||||
new_data.append(
|
||||
ActivityRef(
|
||||
sig=self.ACTIVITY,
|
||||
activity=self.ACTIVITY,
|
||||
id=ref_id,
|
||||
name=row.get("name", None),
|
||||
ref_type=ActivityRefType.PARK,
|
||||
|
||||
@@ -24,7 +24,7 @@ class SIOTA(FileDownloadActivityRefDataProvider):
|
||||
ref_id = row["SILO_CODE"]
|
||||
new_data.append(
|
||||
ActivityRef(
|
||||
sig=self.ACTIVITY,
|
||||
activity=self.ACTIVITY,
|
||||
id=ref_id,
|
||||
name=row.get("NAME", None),
|
||||
ref_type=ActivityRefType.SILO,
|
||||
|
||||
@@ -28,7 +28,7 @@ class SOTA(FileDownloadActivityRefDataProvider):
|
||||
longitude = float(row["Longitude"]) if "Longitude" in row and row["Longitude"] != "" else None
|
||||
altitude = float(row["AltM"]) if "AltM" in row and row["AltM"] != "" else None
|
||||
ref = ActivityRef(
|
||||
sig=self.ACTIVITY,
|
||||
activity=self.ACTIVITY,
|
||||
id=ref_id,
|
||||
name=row.get("SummitName", None),
|
||||
ref_type=ActivityRefType.SUMMIT,
|
||||
|
||||
@@ -24,7 +24,7 @@ class Toilets(LocalFileActivityRefDataProvider):
|
||||
for row in dr:
|
||||
new_data.append(
|
||||
ActivityRef(
|
||||
sig=self.ACTIVITY,
|
||||
activity=self.ACTIVITY,
|
||||
id=row["ref"],
|
||||
name=row["ref"],
|
||||
ref_type=ActivityRefType.TOILET,
|
||||
|
||||
@@ -24,7 +24,7 @@ class Towers(FileDownloadActivityRefDataProvider):
|
||||
ref_id = row["Ref"]
|
||||
new_data.append(
|
||||
ActivityRef(
|
||||
sig=self.ACTIVITY,
|
||||
activity=self.ACTIVITY,
|
||||
id=ref_id,
|
||||
name=row.get("Nazev", None),
|
||||
ref_type=ActivityRefType.TOWER,
|
||||
|
||||
@@ -43,7 +43,7 @@ class WCA(FileDownloadActivityRefDataProvider):
|
||||
|
||||
new_data.append(
|
||||
ActivityRef(
|
||||
sig=self.ACTIVITY,
|
||||
activity=self.ACTIVITY,
|
||||
id=ref_id,
|
||||
name=row.get("CLEAN NAME", None),
|
||||
ref_type=ActivityRefType.CASTLE,
|
||||
|
||||
@@ -30,7 +30,7 @@ class WOTA(FileDownloadActivityRefDataProvider):
|
||||
|
||||
new_data.append(
|
||||
ActivityRef(
|
||||
sig=self.ACTIVITY,
|
||||
activity=self.ACTIVITY,
|
||||
id=ref_id,
|
||||
name=feature["properties"]["title"],
|
||||
url=url,
|
||||
|
||||
@@ -24,7 +24,7 @@ class WWBOTA(FileDownloadActivityRefDataProvider):
|
||||
ref_id = row["Reference"]
|
||||
new_data.append(
|
||||
ActivityRef(
|
||||
sig=self.ACTIVITY,
|
||||
activity=self.ACTIVITY,
|
||||
id=ref_id,
|
||||
name=row.get("Name", None),
|
||||
ref_type=ActivityRefType.BUNKER,
|
||||
|
||||
@@ -24,7 +24,7 @@ class WWFF(FileDownloadActivityRefDataProvider):
|
||||
ref_id = row["reference"]
|
||||
new_data.append(
|
||||
ActivityRef(
|
||||
sig=self.ACTIVITY,
|
||||
activity=self.ACTIVITY,
|
||||
id=ref_id,
|
||||
name=row.get("name", None),
|
||||
ref_type=ActivityRefType.PARK,
|
||||
|
||||
@@ -33,7 +33,7 @@ class ZLOTA(FileDownloadActivityRefDataProvider):
|
||||
ref_type = None
|
||||
|
||||
new_ref = ActivityRef(
|
||||
sig=self.ACTIVITY,
|
||||
activity=self.ACTIVITY,
|
||||
id=ref_id,
|
||||
name=ref["name"],
|
||||
ref_type=ref_type,
|
||||
|
||||
@@ -56,8 +56,8 @@ class BOTA(HTTPAlertProvider):
|
||||
alert = Alert(
|
||||
source=self.name,
|
||||
dx_calls=[dx_call],
|
||||
sig=ActivityName.BOTA,
|
||||
sig_refs=[ActivityRef(id=ref_name, sig=ActivityName.BOTA)],
|
||||
activity=ActivityName.BOTA,
|
||||
activity_refs=[ActivityRef(id=ref_name, activity=ActivityName.BOTA)],
|
||||
start_time=date_time.timestamp(),
|
||||
)
|
||||
|
||||
|
||||
@@ -38,11 +38,11 @@ class Hamsat(HTTPAlertProvider):
|
||||
dx_grid=source_alert["grids"][0],
|
||||
freqs_modes=freqs_modes,
|
||||
comment=source_alert["comment"],
|
||||
sig=ActivityName.SATELLITE,
|
||||
activity=ActivityName.SATELLITE,
|
||||
# Fudge an activity ref to provide the remaining bits of data we need: the satellite and the operator's grid
|
||||
sig_refs=[
|
||||
activity_refs=[
|
||||
ActivityRef(
|
||||
sig=ActivityName.SATELLITE,
|
||||
activity=ActivityName.SATELLITE,
|
||||
id=source_alert["satellite"]["name"],
|
||||
)
|
||||
],
|
||||
|
||||
@@ -89,7 +89,7 @@ class NG3K(HTTPAlertProvider):
|
||||
comment=f"{by}; {comment}; {qsl_info}",
|
||||
start_time=start_timestamp,
|
||||
end_time=end_timestamp,
|
||||
sig=ActivityName.DXPEDITION,
|
||||
activity=ActivityName.DXPEDITION,
|
||||
)
|
||||
|
||||
# Add to our list.
|
||||
|
||||
@@ -37,7 +37,7 @@ class ParksNPeaks(HTTPAlertProvider):
|
||||
datetime.strptime(source_alert["alTime"], "%Y-%m-%d %H:%M:%S").replace(tzinfo=pytz.UTC).timestamp()
|
||||
)
|
||||
|
||||
activity_refs = [ActivityRef(id=ref_id, sig=activity, name=ref_name)]
|
||||
activity_refs = [ActivityRef(id=ref_id, activity=activity, name=ref_name)]
|
||||
|
||||
# Convert to our alert format
|
||||
alert = Alert(
|
||||
@@ -46,8 +46,8 @@ class ParksNPeaks(HTTPAlertProvider):
|
||||
dx_calls=[source_alert["CallSign"].upper()],
|
||||
freqs_modes=f"{source_alert['Freq']} {source_alert['MODE']}",
|
||||
comment=source_alert["Comments"],
|
||||
sig=activity,
|
||||
sig_refs=activity_refs,
|
||||
activity=activity,
|
||||
activity_refs=activity_refs,
|
||||
start_time=start_time,
|
||||
)
|
||||
|
||||
|
||||
@@ -28,11 +28,11 @@ class POTA(HTTPAlertProvider):
|
||||
dx_calls=[source_alert["activator"].upper()],
|
||||
freqs_modes=source_alert["frequencies"],
|
||||
comment=source_alert["comments"],
|
||||
sig=ActivityName.POTA,
|
||||
sig_refs=[
|
||||
activity=ActivityName.POTA,
|
||||
activity_refs=[
|
||||
ActivityRef(
|
||||
id=source_alert["reference"],
|
||||
sig=ActivityName.POTA,
|
||||
activity=ActivityName.POTA,
|
||||
name=source_alert["name"],
|
||||
url=f"https://pota.app/#/park/{source_alert['reference']}",
|
||||
)
|
||||
|
||||
@@ -69,7 +69,7 @@ class RSGBICALAlertProvider(ICALAlertProvider):
|
||||
comment=summary,
|
||||
start_time=start_timestamp,
|
||||
end_time=end_timestamp,
|
||||
sig=ActivityName.CONTEST,
|
||||
activity=ActivityName.CONTEST,
|
||||
)
|
||||
|
||||
return alert
|
||||
|
||||
@@ -34,11 +34,11 @@ class SOTA(HTTPAlertProvider):
|
||||
dx_names=[source_alert["activatorName"].upper()],
|
||||
freqs_modes=source_alert["frequency"],
|
||||
comment=source_alert["comments"],
|
||||
sig=ActivityName.SOTA,
|
||||
sig_refs=[
|
||||
activity=ActivityName.SOTA,
|
||||
activity_refs=[
|
||||
ActivityRef(
|
||||
id=f"{source_alert['associationCode']}/{source_alert['summitCode']}",
|
||||
sig=ActivityName.SOTA,
|
||||
activity=ActivityName.SOTA,
|
||||
name=summit_name,
|
||||
activation_score=summit_points,
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@ class WA7BNM(ICALAlertProvider):
|
||||
url=url,
|
||||
start_time=start_timestamp,
|
||||
end_time=end_timestamp,
|
||||
sig=ActivityName.CONTEST,
|
||||
activity=ActivityName.CONTEST,
|
||||
)
|
||||
|
||||
return alert
|
||||
|
||||
@@ -75,7 +75,7 @@ class WOTA(HTTPAlertProvider):
|
||||
dx_calls=[dx_call],
|
||||
freqs_modes=freqs_modes,
|
||||
comment=comment,
|
||||
sig_refs=[ActivityRef(id=ref, sig=ActivityName.WOTA, name=ref_name)] if ref else [],
|
||||
activity_refs=[ActivityRef(id=ref, activity=ActivityName.WOTA, name=ref_name)] if ref else [],
|
||||
start_time=time.timestamp(),
|
||||
)
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ class WWFF(HTTPAlertProvider):
|
||||
dx_calls=[source_alert["activator_call"].upper()],
|
||||
freqs_modes=f"{source_alert['band']} {source_alert['mode']}",
|
||||
comment=source_alert["remarks"],
|
||||
sig=ActivityName.WWFF,
|
||||
sig_refs=[ActivityRef(id=source_alert["reference"], sig=ActivityName.WWFF)],
|
||||
activity=ActivityName.WWFF,
|
||||
activity_refs=[ActivityRef(id=source_alert["reference"], activity=ActivityName.WWFF)],
|
||||
start_time=datetime.strptime(source_alert["utc_start"], "%Y-%m-%d %H:%M:%S")
|
||||
.replace(tzinfo=pytz.UTC)
|
||||
.timestamp(),
|
||||
|
||||
+35
-35
@@ -68,10 +68,10 @@ class GMA(HTTPSpotProvider):
|
||||
# Filter out some weird mode strings
|
||||
mode=Mode.from_name(source_spot["MODE"].upper()) if "<>" not in source_spot["MODE"] else None,
|
||||
comment=source_spot["TEXT"],
|
||||
sig_refs=[
|
||||
activity_refs=[
|
||||
ActivityRef(
|
||||
id=source_spot["REF"],
|
||||
sig="",
|
||||
activity="",
|
||||
name=source_spot["NAME"],
|
||||
latitude=lat,
|
||||
longitude=lon,
|
||||
@@ -98,57 +98,57 @@ class GMA(HTTPSpotProvider):
|
||||
and ref_response.text != "\n"
|
||||
):
|
||||
ref_info = ref_response.json()
|
||||
if spot.sig_refs and ref_info and "reftype" in ref_info:
|
||||
if spot.activity_refs and ref_info and "reftype" in ref_info:
|
||||
match ref_info["reftype"]:
|
||||
case "Summit":
|
||||
# Summits are a bit complicated, they can be SOTA or GMA depending on the
|
||||
# separate "sota" field:
|
||||
if "sota" in ref_info and ref_info["sota"] != "":
|
||||
spot.sig_refs[0].sig = ActivityName.SOTA
|
||||
spot.sig_refs[0].ref_type = ActivityRefType.SUMMIT
|
||||
spot.sig = ActivityName.SOTA
|
||||
spot.activity_refs[0].activity = ActivityName.SOTA
|
||||
spot.activity_refs[0].ref_type = ActivityRefType.SUMMIT
|
||||
spot.activity = ActivityName.SOTA
|
||||
else:
|
||||
spot.sig_refs[0].sig = ActivityName.GMA
|
||||
spot.sig_refs[0].ref_type = ActivityRefType.SUMMIT
|
||||
spot.sig = ActivityName.GMA
|
||||
spot.activity_refs[0].activity = ActivityName.GMA
|
||||
spot.activity_refs[0].ref_type = ActivityRefType.SUMMIT
|
||||
spot.activity = ActivityName.GMA
|
||||
case "POTA":
|
||||
spot.sig_refs[0].sig = ActivityName.POTA
|
||||
spot.sig_refs[0].ref_type = ActivityRefType.PARK
|
||||
spot.sig = ActivityName.POTA
|
||||
spot.activity_refs[0].activity = ActivityName.POTA
|
||||
spot.activity_refs[0].ref_type = ActivityRefType.PARK
|
||||
spot.activity = ActivityName.POTA
|
||||
case "WWFF":
|
||||
spot.sig_refs[0].sig = ActivityName.WWFF
|
||||
spot.sig_refs[0].ref_type = ActivityRefType.PARK
|
||||
spot.sig = ActivityName.WWFF
|
||||
spot.activity_refs[0].activity = ActivityName.WWFF
|
||||
spot.activity_refs[0].ref_type = ActivityRefType.PARK
|
||||
spot.activity = ActivityName.WWFF
|
||||
case "IOTA Island":
|
||||
spot.sig_refs[0].sig = ActivityName.IOTA
|
||||
spot.sig_refs[0].ref_type = ActivityRefType.ISLAND
|
||||
spot.sig = ActivityName.IOTA
|
||||
spot.activity_refs[0].activity = ActivityName.IOTA
|
||||
spot.activity_refs[0].ref_type = ActivityRefType.ISLAND
|
||||
spot.activity = ActivityName.IOTA
|
||||
case "GMA Island":
|
||||
spot.sig_refs[0].sig = ActivityName.GMA_ISLANDS
|
||||
spot.sig_refs[0].ref_type = ActivityRefType.ISLAND
|
||||
spot.sig = ActivityName.GMA_ISLANDS
|
||||
spot.activity_refs[0].activity = ActivityName.GMA_ISLANDS
|
||||
spot.activity_refs[0].ref_type = ActivityRefType.ISLAND
|
||||
spot.activity = ActivityName.GMA_ISLANDS
|
||||
case "Lighthouse (ILLW)":
|
||||
spot.sig_refs[0].sig = ActivityName.ILLW
|
||||
spot.sig_refs[0].ref_type = ActivityRefType.LIGHTHOUSE
|
||||
spot.sig = ActivityName.ILLW
|
||||
spot.activity_refs[0].activity = ActivityName.ILLW
|
||||
spot.activity_refs[0].ref_type = ActivityRefType.LIGHTHOUSE
|
||||
spot.activity = ActivityName.ILLW
|
||||
case "Lighthouse (ARLHS)":
|
||||
spot.sig_refs[0].sig = ActivityName.ARLHS
|
||||
spot.sig_refs[0].ref_type = ActivityRefType.LIGHTHOUSE
|
||||
spot.sig = ActivityName.ARLHS
|
||||
spot.activity_refs[0].activity = ActivityName.ARLHS
|
||||
spot.activity_refs[0].ref_type = ActivityRefType.LIGHTHOUSE
|
||||
spot.activity = ActivityName.ARLHS
|
||||
case "Castle":
|
||||
spot.sig_refs[0].sig = ActivityName.WCA
|
||||
spot.sig_refs[0].ref_type = ActivityRefType.CASTLE
|
||||
spot.sig = ActivityName.WCA
|
||||
spot.activity_refs[0].activity = ActivityName.WCA
|
||||
spot.activity_refs[0].ref_type = ActivityRefType.CASTLE
|
||||
spot.activity = ActivityName.WCA
|
||||
case "Mill":
|
||||
spot.sig_refs[0].sig = ActivityName.MOTA
|
||||
spot.sig_refs[0].ref_type = ActivityRefType.MILL
|
||||
spot.sig = ActivityName.MOTA
|
||||
spot.activity_refs[0].activity = ActivityName.MOTA
|
||||
spot.activity_refs[0].ref_type = ActivityRefType.MILL
|
||||
spot.activity = ActivityName.MOTA
|
||||
case _:
|
||||
logger.warning(
|
||||
f"GMA spot found with ref type {ref_info['reftype']}, developer needs to add support for this!"
|
||||
)
|
||||
spot.sig_refs[0].sig = ref_info["reftype"]
|
||||
spot.sig = ref_info["reftype"]
|
||||
spot.activity_refs[0].activity = ref_info["reftype"]
|
||||
spot.activity = ref_info["reftype"]
|
||||
|
||||
elif not ref_response.from_cache:
|
||||
if not ref_response.ok:
|
||||
|
||||
@@ -62,11 +62,11 @@ class HEMA(HTTPSpotProvider):
|
||||
freq=float(freq_mode_match.group(1)) * 1000000,
|
||||
mode=Mode.from_name(freq_mode_match.group(2).upper()),
|
||||
comment=spotter_comment_match.group(2),
|
||||
sig=ActivityName.HEMA,
|
||||
sig_refs=[
|
||||
activity=ActivityName.HEMA,
|
||||
activity_refs=[
|
||||
ActivityRef(
|
||||
id=spot_items[3].upper(),
|
||||
sig=ActivityName.HEMA,
|
||||
activity=ActivityName.HEMA,
|
||||
name=spot_items[4],
|
||||
latitude=float(spot_items[7]),
|
||||
longitude=float(spot_items[8]),
|
||||
|
||||
@@ -34,11 +34,11 @@ class LLOTA(HTTPSpotProvider):
|
||||
freq=float(source_spot["frequency"]) * 1000000,
|
||||
mode=Mode.from_name(source_spot["mode"].upper()),
|
||||
comment=comment,
|
||||
sig=ActivityName.LLOTA,
|
||||
sig_refs=[
|
||||
activity=ActivityName.LLOTA,
|
||||
activity_refs=[
|
||||
ActivityRef(
|
||||
id=source_spot["reference"],
|
||||
sig=ActivityName.LLOTA,
|
||||
activity=ActivityName.LLOTA,
|
||||
name=source_spot["reference_name"],
|
||||
ref_type=ActivityRefType.LAKE,
|
||||
)
|
||||
|
||||
@@ -70,20 +70,20 @@ class ParksNPeaks(HTTPSpotProvider):
|
||||
ref_id = source_spot["actSiteID"]
|
||||
|
||||
if activity:
|
||||
spot.sig = activity
|
||||
spot.activity = activity
|
||||
|
||||
if ref_id:
|
||||
activity_refs = [
|
||||
ActivityRef(
|
||||
id=ref_id,
|
||||
sig=activity,
|
||||
activity=activity,
|
||||
# Free text location is not present in all spots, so only add it if it's set
|
||||
name=source_spot["actLocation"]
|
||||
if "actLocation" in source_spot and source_spot["actLocation"] != ""
|
||||
else None,
|
||||
)
|
||||
]
|
||||
spot.sig_refs = activity_refs
|
||||
spot.activity_refs = activity_refs
|
||||
|
||||
else:
|
||||
# If no actSiteID is set, e.g. because actClass is "QRP", sometimes we still have an actLocation
|
||||
@@ -128,9 +128,9 @@ class ParksNPeaks(HTTPSpotProvider):
|
||||
raise ValueError(
|
||||
"Parks N Peaks user ID and API key are required. Get yours from your Parks N Peaks account."
|
||||
)
|
||||
ref_id = spot.sig_refs[0].id if spot.sig_refs else ""
|
||||
ref_id = spot.activity_refs[0].id if spot.activity_refs else ""
|
||||
body = {
|
||||
"actClass": spot.sig or "",
|
||||
"actClass": spot.activity or "",
|
||||
"actCallsign": spot.dx_call,
|
||||
"actSite": ref_id,
|
||||
"mode": spot.mode or "",
|
||||
|
||||
@@ -33,11 +33,11 @@ class POTA(HTTPSpotProvider):
|
||||
freq=float(source_spot["frequency"]) * 1000 if source_spot["frequency"] != "INVALID" else None,
|
||||
mode=Mode.from_name(source_spot["mode"].upper()),
|
||||
comment=source_spot["comments"],
|
||||
sig=ActivityName.POTA,
|
||||
sig_refs=[
|
||||
activity=ActivityName.POTA,
|
||||
activity_refs=[
|
||||
ActivityRef(
|
||||
id=source_spot["reference"],
|
||||
sig=ActivityName.POTA,
|
||||
activity=ActivityName.POTA,
|
||||
name=source_spot["name"],
|
||||
latitude=source_spot["latitude"],
|
||||
longitude=source_spot["longitude"],
|
||||
@@ -61,14 +61,14 @@ class POTA(HTTPSpotProvider):
|
||||
return activity == ActivityName.POTA
|
||||
|
||||
def submit_spot(self, spot, credentials):
|
||||
sig_ref = spot.sig_refs[0].id if spot.sig_refs else None
|
||||
if sig_ref:
|
||||
ref_id = spot.activity_refs[0].id if spot.activity_refs else None
|
||||
if ref_id:
|
||||
body = {
|
||||
"activator": spot.dx_call,
|
||||
"spotter": spot.de_call,
|
||||
"frequency": str(spot.freq / 1000.0),
|
||||
"mode": spot.mode or "",
|
||||
"reference": sig_ref,
|
||||
"reference": ref_id,
|
||||
"comments": spot.comment or "",
|
||||
"source": "Spothole",
|
||||
}
|
||||
|
||||
@@ -57,11 +57,11 @@ class SOTA(HTTPSpotProvider):
|
||||
# Seen SOTA spots with no frequency!
|
||||
mode=Mode.from_name(source_spot["mode"].upper()),
|
||||
comment=source_spot["comments"],
|
||||
sig=ActivityName.SOTA,
|
||||
sig_refs=[
|
||||
activity=ActivityName.SOTA,
|
||||
activity_refs=[
|
||||
ActivityRef(
|
||||
id=source_spot["summitCode"],
|
||||
sig=ActivityName.SOTA,
|
||||
activity=ActivityName.SOTA,
|
||||
name=source_spot["summitName"],
|
||||
latitude=source_spot["latitude"],
|
||||
longitude=source_spot["longitude"],
|
||||
@@ -92,10 +92,10 @@ class SOTA(HTTPSpotProvider):
|
||||
id_token = credentials.get("id_token", "")
|
||||
if not access_token or not id_token:
|
||||
raise ValueError("SOTA API tokens are required. Please log into SOTA in order to spot to it.")
|
||||
sig_ref = spot.sig_refs[0].id if spot.sig_refs else ""
|
||||
if sig_ref:
|
||||
ref_id = spot.activity_refs[0].id if spot.activity_refs else ""
|
||||
if ref_id:
|
||||
# Split reference into association and summit codes
|
||||
ref_split = sig_ref.split("/")
|
||||
ref_split = ref_id.split("/")
|
||||
|
||||
# Figure out a valid mode. Borrowed this from PoLo :)
|
||||
# https://github.com/ham2k/app-polo/blob/main/src/extensions/activities/sota/SOTAPostSelfSpot.js
|
||||
|
||||
@@ -59,13 +59,13 @@ class Tiles(HTTPSpotProvider):
|
||||
freq=freq,
|
||||
mode=Mode.from_name(source_spot["mode"].upper()),
|
||||
comment=source_spot["notes"],
|
||||
sig=ActivityName.TILES,
|
||||
activity=ActivityName.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 activity reference.
|
||||
sig_refs=[
|
||||
activity_refs=[
|
||||
ActivityRef(
|
||||
id=source_spot["maidenhead_grid"],
|
||||
sig=ActivityName.TILES,
|
||||
activity=ActivityName.TILES,
|
||||
name=source_spot["maidenhead_grid"],
|
||||
latitude=source_spot["latitude"],
|
||||
longitude=source_spot["longitude"],
|
||||
|
||||
@@ -34,8 +34,10 @@ class Towers(HTTPSpotProvider):
|
||||
dx_call=source_spot["call"].upper(),
|
||||
freq=likely_freq,
|
||||
comment=source_spot["comment"],
|
||||
sig=ActivityName.TOWERS,
|
||||
sig_refs=[ActivityRef(id=source_spot["ref"], sig=ActivityName.TOWERS, ref_type=ActivityRefType.TOWER)],
|
||||
activity=ActivityName.TOWERS,
|
||||
activity_refs=[
|
||||
ActivityRef(id=source_spot["ref"], activity=ActivityName.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(),
|
||||
|
||||
@@ -92,9 +92,13 @@ class WOTA(HTTPSpotProvider):
|
||||
freq=freq_hz,
|
||||
mode=Mode.from_name(mode),
|
||||
comment=comment,
|
||||
sig=ActivityName.WOTA,
|
||||
sig_refs=(
|
||||
[ActivityRef(id=ref, sig=ActivityName.WOTA, name=ref_name, ref_type=ActivityRefType.SUMMIT)]
|
||||
activity=ActivityName.WOTA,
|
||||
activity_refs=(
|
||||
[
|
||||
ActivityRef(
|
||||
id=ref, activity=ActivityName.WOTA, name=ref_name, ref_type=ActivityRefType.SUMMIT
|
||||
)
|
||||
]
|
||||
if ref
|
||||
else []
|
||||
),
|
||||
|
||||
@@ -23,7 +23,7 @@ class WWBOTA(SSESpotProvider):
|
||||
for ref in source_spot["references"]:
|
||||
activity_ref = ActivityRef(
|
||||
id=ref["reference"],
|
||||
sig=ActivityName.WWBOTA,
|
||||
activity=ActivityName.WWBOTA,
|
||||
name=ref["name"],
|
||||
latitude=ref["lat"],
|
||||
longitude=ref["long"],
|
||||
@@ -38,8 +38,8 @@ class WWBOTA(SSESpotProvider):
|
||||
freq=float(source_spot["freq"]) * 1000000,
|
||||
mode=Mode.from_name(source_spot["mode"].upper()) if source_spot.get("mode") else None,
|
||||
comment=source_spot["comment"],
|
||||
sig=ActivityName.WWBOTA,
|
||||
sig_refs=refs,
|
||||
activity=ActivityName.WWBOTA,
|
||||
activity_refs=refs,
|
||||
time=datetime.fromisoformat(source_spot["time"].replace("Z", "+00:00")).timestamp(),
|
||||
# WWBOTA spots can contain multiple references for bunkers being activated simultaneously. For
|
||||
# now, we will just pick the first one to use as our grid, latitude and longitude.
|
||||
|
||||
@@ -30,11 +30,11 @@ class WWFF(HTTPSpotProvider):
|
||||
freq=float(source_spot["frequency_khz"]) * 1000,
|
||||
mode=Mode.from_name(source_spot["mode"].upper()),
|
||||
comment=source_spot["remarks"],
|
||||
sig=ActivityName.WWFF,
|
||||
sig_refs=[
|
||||
activity=ActivityName.WWFF,
|
||||
activity_refs=[
|
||||
ActivityRef(
|
||||
id=source_spot["reference"],
|
||||
sig=ActivityName.WWFF,
|
||||
activity=ActivityName.WWFF,
|
||||
name=source_spot["reference_name"],
|
||||
latitude=source_spot["latitude"],
|
||||
longitude=source_spot["longitude"],
|
||||
|
||||
@@ -14,16 +14,18 @@ class XOTA(WebsocketSpotProvider):
|
||||
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."""
|
||||
is why we also provide an activity_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."""
|
||||
|
||||
ACTIVITY = None
|
||||
|
||||
def __init__(self, provider_config):
|
||||
name = provider_config.get("name", "xOTA")
|
||||
super().__init__(name, provider_config, provider_config["url"])
|
||||
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 ""
|
||||
self.ACTIVITY = str(provider_config["activity"]) if "activity" in provider_config else None
|
||||
self._activity_ref_prefix = (
|
||||
str(provider_config["activity_ref_prefix"]) if "activity_ref_prefix" in provider_config else ""
|
||||
)
|
||||
|
||||
def _ws_message_to_spot(self, b):
|
||||
string = b.decode("utf-8")
|
||||
@@ -35,11 +37,11 @@ class XOTA(WebsocketSpotProvider):
|
||||
dx_call=source_spot["stationCallSign"].upper(),
|
||||
freq=float(source_spot["freq"]) * 1000,
|
||||
mode=Mode.from_name(source_spot["mode"].upper()),
|
||||
sig=self.ACTIVITY,
|
||||
sig_refs=[
|
||||
activity=self.ACTIVITY,
|
||||
activity_refs=[
|
||||
ActivityRef(
|
||||
id=ref_id,
|
||||
sig=self.ACTIVITY or "",
|
||||
activity=self.ACTIVITY or "",
|
||||
url=source_spot["reference"]["website"],
|
||||
)
|
||||
],
|
||||
|
||||
@@ -35,11 +35,11 @@ class ZLOTA(HTTPSpotProvider):
|
||||
freq=freq_hz,
|
||||
mode=Mode.from_name(source_spot["mode"].upper().strip()),
|
||||
comment=source_spot["comments"],
|
||||
sig=ActivityName.ZLOTA,
|
||||
sig_refs=[
|
||||
activity=ActivityName.ZLOTA,
|
||||
activity_refs=[
|
||||
ActivityRef(
|
||||
id=source_spot["reference"],
|
||||
sig=ActivityName.ZLOTA,
|
||||
activity=ActivityName.ZLOTA,
|
||||
name=source_spot["name"],
|
||||
)
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user