From 606cf1be68ba08cb10249ec0c59f8c27e6718cbf Mon Sep 17 00:00:00 2001
From: Ian Renton
Date: Wed, 2 Sep 2026 08:37:13 +0100
Subject: [PATCH] Start converting some enum-like strings to proper enums, plus
global reformat
---
core/constants.py | 1 -
core/enums.py | 7 +++++++
core/utils.py | 8 ++++----
data/callsign.py | 6 +++---
data/spot.py | 8 ++++----
docs/docker.md | 23 ++++++++++++-----------
docs/modifying.md | 4 ++--
docs/nginx.md | 25 ++++++++++---------------
providers/callsigndata/qrz.py | 6 ++----
providers/sigrefdata/dtmba.py | 9 +--------
providers/spot/wota.py | 4 +++-
server/handlers/api/options.py | 6 +++---
server/handlers/api/v1_compatability.py | 4 ++--
server/handlers/api/v1_spots.py | 1 +
static/apidocs/openapi.yml | 2 ++
templates/about.html | 8 +++++---
templates/add_spot.html | 5 +++--
templates/alerts.html | 2 +-
templates/bands.html | 4 ++--
templates/base.html | 15 ++++++++-------
templates/conditions.html | 2 +-
templates/map.html | 4 ++--
templates/spots.html | 4 ++--
templates/status.html | 2 +-
24 files changed, 81 insertions(+), 79 deletions(-)
diff --git a/core/constants.py b/core/constants.py
index c2045c3..2bcfe8f 100644
--- a/core/constants.py
+++ b/core/constants.py
@@ -276,7 +276,6 @@ DATA_MODES = [
"MSK144",
]
ALL_MODES = CW_MODES + PHONE_MODES + DATA_MODES
-MODE_TYPES = ["CW", "PHONE", "DATA"]
# Mode aliases. Sometimes we get spots with a mode described in a different way that is effectively the same as a mode
# we already know, or we want to normalise things for consistency. The lookup table for this is here. Incoming spots
diff --git a/core/enums.py b/core/enums.py
index 2c55506..ca31870 100644
--- a/core/enums.py
+++ b/core/enums.py
@@ -11,6 +11,13 @@ class Continent(str, Enum):
AN = "AN"
+class ModeType(str, Enum):
+ PHONE = "PHONE"
+ CW = "CW"
+ DATA = "DATA"
+ UNKNOWN = "UNKNOWN"
+
+
class ModeSource(str, Enum):
"""Where the mode data came from in a spot."""
diff --git a/core/utils.py b/core/utils.py
index 44be09c..ecbc61a 100644
--- a/core/utils.py
+++ b/core/utils.py
@@ -15,7 +15,7 @@ from core.constants import (
UNKNOWN_BAND,
)
from core.data_store import DATA_STORE
-from core.enums import Continent
+from core.enums import Continent, ModeType
from data.callsign import Callsign, LocationSourceForCallsign
logger = logging.getLogger(__name__)
@@ -44,11 +44,11 @@ def infer_mode_type_from_mode(mode):
"""Infer a "mode family" from a mode."""
if mode.upper() in CW_MODES:
- return "CW"
+ return ModeType.CW
elif mode.upper() in PHONE_MODES:
- return "PHONE"
+ return ModeType.PHONE
elif mode.upper() in DATA_MODES:
- return "DATA"
+ return ModeType.DATA
else:
if mode.upper() != "OTHER" and mode != "?":
logger.warning(f"Found an unrecognised mode: {mode}. Developer should categorise this.")
diff --git a/data/callsign.py b/data/callsign.py
index 946a84d..b7f6a44 100644
--- a/data/callsign.py
+++ b/data/callsign.py
@@ -1,6 +1,6 @@
from dataclasses import dataclass
-from core.enums import LocationSourceForCallsign
+from core.enums import Continent, LocationSourceForCallsign
@dataclass
@@ -30,14 +30,14 @@ class Callsign:
# Country in which the callsign indicates they are operating
country: str | None = None
# Continent in which the callsign indicates they are operating
- continent: str | None = None
+ continent: Continent | None = None
# DXCC ID in which the callsign indicates they are operating
dxcc_id: int | None = None
# CQ zone in which the callsign indicates they are operating
cq_zone: int | None = None
# ITU zone in which the callsign indicates they are operating
itu_zone: int | None = None
- # Location source. This can be "HOME QTH" or "DXCC" depending on which provider gave us a location
+ # Location source
location_source: LocationSourceForCallsign = LocationSourceForCallsign.NONE
def fully_populated(self):
diff --git a/data/spot.py b/data/spot.py
index e063866..b101d38 100644
--- a/data/spot.py
+++ b/data/spot.py
@@ -12,7 +12,7 @@ from pyhamtools.locator import latlong_to_locator, locator_to_latlong
from core.call_lookup_helper import get_call_info
from core.config import MAX_SPOT_AGE
from core.constants import MODE_ALIASES, PROPAGATION_MODES, SIGS
-from core.enums import Continent, LocationSourceForSpot, ModeSource
+from core.enums import Continent, LocationSourceForSpot, 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 (
@@ -104,9 +104,9 @@ class Spot:
# Reported mode, such as SSB, PHONE, CW, FT8...
mode: str | None = None
- # Inferred mode "family". One of "CW", "PHONE" or "DIGI".
- mode_type: str | None = None
- # Source of the mode information. "SPOT", "COMMENT", "BANDPLAN" or "NONE"
+ # Inferred mode "family".
+ mode_type: ModeType = ModeType.UNKNOWN
+ # Source of the mode information.
mode_source: ModeSource = ModeSource.NONE
# Frequency, in Hz
freq: float | None = None
diff --git a/docs/docker.md b/docs/docker.md
index b490984..db81bdb 100644
--- a/docs/docker.md
+++ b/docs/docker.md
@@ -23,15 +23,16 @@ You can replace `#main` with any other branch or tag reference, for example `#1.
Save the file. You will still need to create a copy of `config-example.yml` and name it `config.yml`, though with the
Docker setup nothing has actually been downloaded yet, so you will have to copy the example from the repository some
-other way, e.g. [from the repo in a web browser](https://git.ianrenton.com/ian/spothole/src/branch/main/config-example.yml).
+other way,
+e.g. [from the repo in a web browser](https://git.ianrenton.com/ian/spothole/src/branch/main/config-example.yml).
With that in place, run `docker compose up` and you should be good to go. To detach, press `d` or run the command with
the `-d` flag.
### nginx Reverse Proxy with Docker
-In a containerised setup, it's typical to run an nginx reverse proxy in one container, alongside certbot for renewal
-of HTTPS certificates, and then applications like Spothole in a separate container. In this case, there are a couple of
+In a containerised setup, it's typical to run an nginx reverse proxy in one container, alongside certbot for renewal of
+HTTPS certificates, and then applications like Spothole in a separate container. In this case, there are a couple of
variations of the docker compose file above, and the nginx reverse proxy configuration covered [here](./nginx.md), that
you will want to make.
@@ -60,8 +61,8 @@ networks:
```
In your nginx site configuration, you'll want to refer to the Spothole container directly, and drop the block that
-allows nginx to access static files directly, as these will be inaccessible in another container. So you may end up
-with something like:
+allows nginx to access static files directly, as these will be inaccessible in another container. So you may end up with
+something like:
```nginx
server {
@@ -146,13 +147,13 @@ server {
```
If desired, you could even change the port on which Spothole runs from 8080 to a plain 80, in which case your
-`proxy_pass` statements could drop the `:8080` suffix. Since Spothole is in a container, it can serve HTTP on port 80
-if desired, because it doesn't conflict with the host system.
+`proxy_pass` statements could drop the `:8080` suffix. Since Spothole is in a container, it can serve HTTP on port 80 if
+desired, because it doesn't conflict with the host system.
### Restoring the static files bypass
-If you would still like to bypass Spothole's web server for the static files, and serve them with nginx, you can
-do. The easiest way is to run another nginx container to serve the files, so your Spothole `compose.yaml` becomes:
+If you would still like to bypass Spothole's web server for the static files, and serve them with nginx, you can do. The
+easiest way is to run another nginx container to serve the files, so your Spothole `compose.yaml` becomes:
```yaml
services:
@@ -183,8 +184,8 @@ networks:
external: true
```
-Then you can re-add the block that handles the `/static` path in your nginx reverse proxy config, but this time
-point it at the new container rather than at a filesystem path:
+Then you can re-add the block that handles the `/static` path in your nginx reverse proxy config, but this time point it
+at the new container rather than at a filesystem path:
```nginx
# Load static assets from the spothole-static-nginx container
diff --git a/docs/modifying.md b/docs/modifying.md
index f754970..e00cb97 100644
--- a/docs/modifying.md
+++ b/docs/modifying.md
@@ -48,8 +48,8 @@ To navigate your way around the source code, this list may help.
### Extending the server
Spothole is designed to be easily extensible. If you want to write your own spot provider, for example, simply add a
-module to the `providers.spot` package containing your class. (Currently, in order to be loaded correctly, the module (
-file) name should be the same as the class name, but lower case.)
+module to the `providers.spot` package containing your class. (Currently, in order to be loaded correctly, the module
+(file) name should be the same as the class name, but lower case.)
Your class should extend "SpotProvider"; if it operates by polling an HTTP Server on a timer, it can instead extend "
HTTPSpotProvider" where some of the work is done for you.
diff --git a/docs/nginx.md b/docs/nginx.md
index 1d39cc3..cef4c0d 100644
--- a/docs/nginx.md
+++ b/docs/nginx.md
@@ -2,8 +2,8 @@
Web servers generally serve their pages from port 80. However, it's best not to serve Spothole's web interface directly
on port 80, as that requires root privileges on a Linux system. It also and prevents us using HTTPS to serve a secure
-site, since Spothole itself doesn't directly support acting as an HTTPS server. The normal solution to this is to use
-a "reverse proxy" setup, where a general web server handles HTTP and HTTP requests (to port 80 & 443 respectively), then
+site, since Spothole itself doesn't directly support acting as an HTTPS server. The normal solution to this is to use a
+"reverse proxy" setup, where a general web server handles HTTP and HTTP requests (to port 80 & 443 respectively), then
passes on the request to the back-end application (in this case Spothole). nginx is a common choice for this general web
server.
@@ -89,19 +89,14 @@ server {
```
One further change you might want to make to the file above is the `add_header Access-Control-Allow-Origin` statements.
-These are what's used on
-my own Spothole server to make sure that other third-party web-based software can get the data from my instance, and
-applies to any endpoint underneath `/api`. If you want
-*your* Spothole instance to be set up the same way, so that others can write software in JavaScript that can access it,
-leave this intact. But if you want your Spothole instance to only be usable by scripts running on the web server you
-write,
-you can remove these lines. (Note that this doesn't stop other people writing *non-web-based* software that accesses
-your
-Spothole API—the enforcement of cross-origin headers only happens within the user's browser. If you need to lock
-your
-instance down so that no-one else can access it with *any* software, that's an aspect of nginx or firewall config that
-you will need
-to find help with elsewhere.)
+These are what's used on my own Spothole server to make sure that other third-party web-based software can get the data
+from my instance, and applies to any endpoint underneath `/api`. If you want *your* Spothole instance to be set up the
+same way, so that others can write software in JavaScript that can access it, leave this intact. But if you want your
+Spothole instance to only be usable by scripts running on the web server you write, you can remove these lines. (Note
+that this doesn't stop other people writing *non-web-based* software that accesses your Spothole API—the
+enforcement of cross-origin headers only happens within the user's browser. If you need to lock your instance down so
+that no-one else can access it with *any* software, that's an aspect of nginx or firewall config that you will need to
+find help with elsewhere.)
Now, make a symbolic link to enable the site:
diff --git a/providers/callsigndata/qrz.py b/providers/callsigndata/qrz.py
index 1604c5e..c413bed 100644
--- a/providers/callsigndata/qrz.py
+++ b/providers/callsigndata/qrz.py
@@ -10,12 +10,10 @@ from requests_cache import CachedSession
from core.constants import HTTP_HEADERS
from core.data_store import CACHE_DIR, DATA_STORE
-from core.enums import LocationSourceForCallsign, Continent
+from core.enums import Continent, LocationSourceForCallsign
from core.url_data_cache import URLDataCache
from data.callsign import Callsign
-from providers.callsigndata.api_query_callsign_data_provider import (
- APIQueryCallsignDataProvider,
-)
+from providers.callsigndata.api_query_callsign_data_provider import APIQueryCallsignDataProvider
logger = logging.getLogger(__name__)
diff --git a/providers/sigrefdata/dtmba.py b/providers/sigrefdata/dtmba.py
index 801ebb4..d20d52a 100644
--- a/providers/sigrefdata/dtmba.py
+++ b/providers/sigrefdata/dtmba.py
@@ -20,14 +20,7 @@ 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="Building"
- )
- )
+ new_data.append(SIGRef(sig=self.SIG, id=ref_id, name=ref_name, ref_type="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
diff --git a/providers/spot/wota.py b/providers/spot/wota.py
index 717d325..87ce14c 100644
--- a/providers/spot/wota.py
+++ b/providers/spot/wota.py
@@ -76,7 +76,9 @@ class WOTA(HTTPSpotProvider):
except Exception:
logger.warning(f"Could not parse WOTA spot description: {source_spot.description}")
- time = datetime.strptime(source_spot.pub_date.content, self.RSS_DATE_TIME_FORMAT).astimezone(pytz.UTC)
+ time = datetime.strptime(source_spot.pub_date.content, self.RSS_DATE_TIME_FORMAT).astimezone(
+ pytz.UTC
+ )
# Convert to our spot format
spot = Spot(
diff --git a/server/handlers/api/options.py b/server/handlers/api/options.py
index 86dc834..6aa9928 100644
--- a/server/handlers/api/options.py
+++ b/server/handlers/api/options.py
@@ -8,8 +8,8 @@ from tornado import httputil
from tornado.web import Application
from core.config import ALLOW_SPOTTING, MAX_SPOT_AGE
-from core.constants import ALL_MODES, BANDS, MODE_TYPES, PROPAGATION_MODES, SIGS
-from core.enums import Continent
+from core.constants import ALL_MODES, BANDS, PROPAGATION_MODES, SIGS
+from core.enums import Continent, ModeType
from core.prometheus_metrics_handler import api_requests_counter
from core.utils import safe_json_dumps
@@ -84,7 +84,7 @@ class APIOptionsHandler(tornado.web.RequestHandler):
options = {
"bands": BANDS,
"modes": ALL_MODES,
- "mode_types": MODE_TYPES,
+ "mode_types": [t.value for t in ModeType],
"sigs": SIGS,
"spot_providers": spot_providers,
"spot_providers_enabled_by_default": spot_providers_enabled_by_default,
diff --git a/server/handlers/api/v1_compatability.py b/server/handlers/api/v1_compatability.py
index fa6bcbb..53350bc 100644
--- a/server/handlers/api/v1_compatability.py
+++ b/server/handlers/api/v1_compatability.py
@@ -2,7 +2,6 @@ import tornado
from tornado.httpclient import AsyncHTTPClient
from tornado.httputil import HTTPHeaders
-
_LEGACY_PARAM_TO_HEADER_MAP = {
"qrz_username": "X-QRZ-Username",
"qrz_password": "X-QRZ-Password",
@@ -12,6 +11,7 @@ _LEGACY_PARAM_TO_HEADER_MAP = {
"hamqth_session_id": "X-HamQTH-Session-ID",
}
+
class V1RedirectHandler(tornado.web.RequestHandler):
"""Transparently proxies requests from the old API to the new one,
returning whatever the v2 endpoint returns, for endpoints with no breaking changes."""
@@ -60,4 +60,4 @@ class V1RedirectHandler(tornado.web.RequestHandler):
await self._proxy(path)
async def post(self, path):
- await self._proxy(path)
\ No newline at end of file
+ await self._proxy(path)
diff --git a/server/handlers/api/v1_spots.py b/server/handlers/api/v1_spots.py
index fc05ffb..3e1dab3 100644
--- a/server/handlers/api/v1_spots.py
+++ b/server/handlers/api/v1_spots.py
@@ -23,6 +23,7 @@ def _handle_legacy_params(handler):
if value:
handler.request.headers[header] = value
+
class V1APISpotsHandler(APISpotsHandler):
"""API request handler for /api/v1/spots (GET). Included in early Spothole v2 for backwards compatibility."""
diff --git a/static/apidocs/openapi.yml b/static/apidocs/openapi.yml
index 47c2dc1..5ec9a10 100644
--- a/static/apidocs/openapi.yml
+++ b/static/apidocs/openapi.yml
@@ -20,6 +20,7 @@ info:
* Added DTMBA, FEA, BIWOTA, COTA & PGA SIGs
* Removed the distinction between LSB & USB (both will now show as SSB) and between the various digital voice modes, which will now show as DV.
* Added `icon`, `region_flag` and `refs_globally_unique` to SIG information
+ * Unknown mode types now return "UNKNOWN" not null
### 2.0
@@ -945,6 +946,7 @@ components:
- CW
- PHONE
- DATA
+ - UNKNOWN
example: CW
ModeSource:
diff --git a/templates/about.html b/templates/about.html
index 3f2f346..4af1230 100644
--- a/templates/about.html
+++ b/templates/about.html
@@ -179,13 +179,15 @@
filters. They are also stored in your browser's local storage, so that your preferences are remembered between
sessions.
The data you provide can optionally include your login credentials for QRZ.com and HamQTH. You can provide these
- in the "Your Data" menu of most pages. If you do, Spothole will augment the data it produces with lookups from these
+ in the "Your Data" menu of most pages. If you do, Spothole will augment the data it produces with lookups from
+ these
services, which can for example provide more accurate markers on the map tab, and operator names when you mouse
over a DX callsign. Spothole will still work fine if you don't provide these. The values you enter are sent to
Spothole via HTTPS so are protected in transit, though of course you do have to trust Spothole with this
sensitive data in order to use this feature.
-
Any data you send as part of a query, such as your QRZ or HamQTH credentials, is used only for the lifetime of that
- query and is not saved anywhere apart from your own device.
+
Any data you send as part of a query, such as your QRZ or HamQTH credentials, is used only for the lifetime of
+ that
+ query and is not saved anywhere apart from your own device.
Spothole uses no trackers, no ads, and no cookies.
{% if len(web_ui_options["support_button_html"]) > 0 %}
Caveat: The owner of this server has chosen to inject their own content into the "spots" page.
diff --git a/templates/add_spot.html b/templates/add_spot.html
index 6f76b99..ec30cd1 100644
--- a/templates/add_spot.html
+++ b/templates/add_spot.html
@@ -2,7 +2,8 @@
{% block content %}
-
+
+
Adding spots to Spothole This page is implemented
as a proof of concept for adding spots to the Spothole system. Currently, spots added in this way are only
visible within Spothole and are not sent "upstream" to DX clusters or xOTA spotting sites. The functionality
@@ -76,7 +77,7 @@