Files
spothole/providers/spot/spot_provider.py
T
Ian RentonandClaude Sonnet 5 e33eecbc4e Fix mojibake in DX cluster and RBN spot comments
Both providers always decoded telnet bytes as Latin-1, which silently
mangles nodes that send UTF-8 (Latin-1 decode never raises, so mixed
encodings across cluster nodes went unnoticed). Added a shared
decode_telnet_bytes() helper that tries UTF-8 first and falls back to
Latin-1, since a byte stream that happens to be valid multi-byte UTF-8
is essentially never accidental Latin-1 text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 19:08:05 +01:00

91 lines
4.0 KiB
Python

from datetime import datetime
import pytz
from core.data_store import DATA_STORE
def decode_telnet_bytes(data: bytes) -> str:
"""Decode a line of text received from a telnet connection. DX cluster and RBN nodes are inconsistent about the
character encoding they use for spot comments: most send UTF-8, but some older ones send Latin-1/CP1252. Try
UTF-8 first, since valid multi-byte UTF-8 sequences are very unlikely to occur by chance in Latin-1 text, then
fall back to Latin-1, which can decode any byte sequence without raising."""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("latin-1")
class SpotProvider:
"""Generic spot provider class. Subclasses of this query the individual APIs for data."""
def __init__(self, name, provider_config):
"""Constructor"""
self.name = name
self.enabled = provider_config.get("enabled", True)
self.enabled_by_default_in_web_ui = provider_config.get("enabled_by_default_in_web_ui", True)
self.last_update_time = datetime.min.replace(tzinfo=pytz.UTC)
self.last_spot_time = datetime.min.replace(tzinfo=pytz.UTC)
self.status = "Not Started" if self.enabled else "Disabled"
self._spots = DATA_STORE.spots
def start(self):
"""Start the provider. This should return immediately after spawning threads to access the remote resources"""
raise NotImplementedError("Subclasses must implement this method")
def _submit_batch(self, spots):
"""Submit a batch of spots retrieved from the provider. Only spots that are newer than the last spot retrieved
by this provider will be added to the spot list, to prevent duplications. Spots passing the check will also have
their infer_missing() method called to complete their data set. This is called by the API-querying
subclasses on receiving spots."""
# Sort the batch so that earliest ones go in first. This helps keep the ordering correct when spots are fired
# off to SSE listeners.
spots = sorted(spots, key=lambda s: s.time if s and s.time else 0)
for spot in spots:
if datetime.fromtimestamp(spot.time, pytz.UTC) > self.last_spot_time:
# Fill in any blanks and add to the list
spot.infer_missing()
self._add_spot(spot)
if spots:
self.last_spot_time = datetime.fromtimestamp(max(s.time for s in spots), pytz.UTC)
def _submit(self, spot):
"""Submit a single spot retrieved from the provider. This will be added to the list regardless of its age. Spots
passing the check will also have their infer_missing() method called to complete their data set. This is called by
the data streaming subclasses, which can be relied upon not to re-provide old spots."""
# Fill in any blanks and add to the list
spot.infer_missing()
self._add_spot(spot)
self.last_spot_time = datetime.fromtimestamp(spot.time, pytz.UTC)
def _add_spot(self, spot):
if not spot.expired():
self._spots.set(spot.id, spot)
def stop(self):
"""Stop any threads and prepare for application shutdown"""
raise NotImplementedError("Subclasses must implement this method")
def can_submit_spot(self, activity):
"""Return True if this provider supports submitting spots upstream for the given activity."""
return False
def submit_spot(self, spot, credentials):
"""Submit a spot upstream to this provider's API. credentials is a dict with provider-specific keys.
Raises an exception with a descriptive message on failure."""
raise NotImplementedError("This provider does not support spot submission")
def force_poll(self):
"""Trigger an immediate poll without waiting for the normal interval. Default implementation here does nothing
because not all spot providers have a polling mechanism. Providers that do should override this method."""
return