From 2495d5e237e3709ddd63e8c7c5a54dc0550a456a Mon Sep 17 00:00:00 2001 From: Miroslaw Mowinski Date: Tue, 17 Feb 2026 17:11:14 +0100 Subject: [PATCH] Fixing dismantle callsign --- pyhamtools/callinfo.py | 27 ++++++++++++++++++++++----- test/conftest.py | 8 ++++++++ test/test_callinfo.py | 19 +++++++++++++++++-- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/pyhamtools/callinfo.py b/pyhamtools/callinfo.py index 681eb25..ff6e74f 100644 --- a/pyhamtools/callinfo.py +++ b/pyhamtools/callinfo.py @@ -56,12 +56,24 @@ class Callinfo(object): """ callsign = callsign.upper() - homecall = re.search('[\\d]{0,1}[A-Z]{1,2}\\d([A-Z]{1,4}|\\d{3,3}|\\d{1,3}[A-Z])[A-Z]{0,5}', callsign) + + # Prefer splitting on '/', as portable calls often appear as chains like DL/SQ5FOX/M/DL. + # Pick the first token that looks like a real callsign and let higher-level logic validate it. + token_candidates = [token.strip() for token in callsign.split('/') if token.strip()] + token_candidates = [re.sub(r'-\d{1,3}$', '', token) for token in token_candidates] + + # Accept typical callsigns and special patterns with digits in the suffix (e.g. 3Z3Z3Z). + token_pattern = re.compile(r'^[\d]{0,1}[A-Z]{1,2}\d{1,4}[A-Z0-9]{1,8}$') + for token in token_candidates: + if token_pattern.match(token): + return token + + # Fallback: search inside the string for a callsign-like pattern. + homecall = re.search(r'[\d]{0,1}[A-Z]{1,2}\d{1,4}[A-Z0-9]{1,8}', callsign) if homecall: - homecall = homecall.group(0) - return homecall - else: - raise ValueError + return homecall.group(0) + + raise ValueError def _iterate_prefix(self, callsign, timestamp=None): """truncate call until it corresponds to a Prefix in the database""" @@ -195,6 +207,11 @@ class Callinfo(object): elif re.match('^[\\d]{0,1}[A-Z]{1,2}\\d{1,4}([A-Z]{1,4}|[A-Z]{1,2}\\d{0,3})[A-Z]{0,5}$', callsign): return self._iterate_prefix(callsign, timestamp) + # Some special callsigns contain digits in the suffix (e.g. 3Z3Z3Z). + # Fall back to a more permissive pattern and let the prefix database decide. + elif re.match('^[\\d]{0,1}[A-Z]{1,2}\\d{1,4}[A-Z0-9]{1,8}$', callsign): + return self._iterate_prefix(callsign, timestamp) + # callsigns with prefixes (xxx/callsign) elif re.search('^[A-Z0-9]{1,4}/', entire_callsign): pfx = re.search('^[A-Z0-9]{1,4}/', entire_callsign) diff --git a/test/conftest.py b/test/conftest.py index 3105328..d9fac9d 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -57,17 +57,23 @@ def fixApiKey(request): @pytest.fixture(scope="module", params=["clublogapi", "clublogxml", "countryfile"]) def fixGeneralApi(request, fixApiKey): """Fixture returning all possible instances of LookupLib""" + if request.param in ("clublogapi", "clublogxml") and not fixApiKey: + pytest.skip("CLUBLOG_APIKEY not set; skipping Clublog-backed tests") Lib = LookupLib(request.param, fixApiKey) # pytest.skip("better later") return(Lib) @pytest.fixture(scope="module") def fixClublogApi(request, fixApiKey): + if not fixApiKey: + pytest.skip("CLUBLOG_APIKEY not set; skipping clublogapi tests") Lib = LookupLib("clublogapi", fixApiKey) return(Lib) @pytest.fixture(scope="module") def fixClublogXML(request, fixApiKey): + if not fixApiKey: + pytest.skip("CLUBLOG_APIKEY not set; skipping clublogxml tests") Lib = LookupLib("clublogxml", fixApiKey) return(Lib) @@ -78,6 +84,8 @@ def fixCountryFile(request): @pytest.fixture(scope="module", params=["clublogxml", "countryfile"]) def fix_callinfo(request, fixApiKey): + if request.param == "clublogxml" and not fixApiKey: + pytest.skip("CLUBLOG_APIKEY not set; skipping clublogxml-based callinfo tests") lib = LookupLib(request.param, fixApiKey) callinfo = Callinfo(lib) return(callinfo) diff --git a/test/test_callinfo.py b/test/test_callinfo.py index 3c7c672..984a05e 100644 --- a/test/test_callinfo.py +++ b/test/test_callinfo.py @@ -272,8 +272,22 @@ class Test_callinfo_methods: assert not fix_callinfo.check_if_beacon("DH1TW") def test_get_homecall(self, fix_callinfo): - assert fix_callinfo.get_homecall("HB9/DH1TW") == "DH1TW" - assert fix_callinfo.get_homecall("SM3/DH1TW/P") == "DH1TW" + cases = [ + ("HB9/DH1TW", "DH1TW"), + ("SM3/DH1TW/P", "DH1TW"), + ("SP5ABC", "SP5ABC"), + ("SP/SP5ABC", "SP5ABC"), + ("SP5ABC/W5", "SP5ABC"), + ("DL/SQ5FOX/M/DL", "SQ5FOX"), + ("3z3z3z", "3Z3Z3Z"), + ("DL/3z3z3z", "3Z3Z3Z"), + ("DL/3z3z3z/am/m/ok", "3Z3Z3Z"), + ("N0CALL/P", "N0CALL"), + ("W5/N0CALL", "N0CALL"), + ] + + for input_call, expected in cases: + assert fix_callinfo.get_homecall(input_call) == expected with pytest.raises(ValueError): fix_callinfo.get_homecall("QRM") @@ -395,6 +409,7 @@ class Test_callinfo_methods: def test_is_valid_callsign(self, fix_callinfo): assert fix_callinfo.is_valid_callsign("DH1TW") + assert fix_callinfo.is_valid_callsign("3Z3Z3Z") assert not fix_callinfo.is_valid_callsign("QRM") def test_get_lat_long(self, fix_callinfo):