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 %}

- - + diff --git a/templates/bands.html b/templates/bands.html index c5bfb6c..ffc56ea 100644 --- a/templates/bands.html +++ b/templates/bands.html @@ -76,8 +76,8 @@
- - + + diff --git a/templates/base.html b/templates/base.html index e22274e..09b02d3 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,6 +1,6 @@ {% extends "skeleton.html" %} {% block head_extra %} - + @@ -11,14 +11,15 @@ - - - - + + + + {% end %} {% block body %}
diff --git a/templates/conditions.html b/templates/conditions.html index b37f3c7..6bd7e1a 100644 --- a/templates/conditions.html +++ b/templates/conditions.html @@ -284,7 +284,7 @@
- + diff --git a/templates/map.html b/templates/map.html index e8c97bf..ffbfd4a 100644 --- a/templates/map.html +++ b/templates/map.html @@ -113,8 +113,8 @@ const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}"; - - + + diff --git a/templates/spots.html b/templates/spots.html index 625fe74..76cad44 100644 --- a/templates/spots.html +++ b/templates/spots.html @@ -113,8 +113,8 @@ - - + + diff --git a/templates/status.html b/templates/status.html index 64199d6..52e6918 100644 --- a/templates/status.html +++ b/templates/status.html @@ -86,7 +86,7 @@ - +