Compare commits

...

12 commits

Author SHA1 Message Date
Tobias Wellnitz, DH1TW 8c15ab8c2c changed documents theme to readthedocs
Some checks failed
Linux / Ubuntu 24.04 - Python ${{ matrix.python-version }} (3.10, 7) (push) Has been cancelled
Linux / Ubuntu 24.04 - Python ${{ matrix.python-version }} (3.11, 7) (push) Has been cancelled
Linux / Ubuntu 24.04 - Python ${{ matrix.python-version }} (3.12, 7) (push) Has been cancelled
Linux / Ubuntu 24.04 - Python ${{ matrix.python-version }} (3.13, 7) (push) Has been cancelled
Linux / Ubuntu 24.04 - Python ${{ matrix.python-version }} (3.8, 7) (push) Has been cancelled
Linux / Ubuntu 24.04 - Python ${{ matrix.python-version }} (3.9, 7) (push) Has been cancelled
Linux / Ubuntu 24.04 - Python ${{ matrix.python-version }} (pypy3.10, 7) (push) Has been cancelled
Linux / Ubuntu 24.04 - Python ${{ matrix.python-version }} (pypy3.8, 7) (push) Has been cancelled
Linux / Ubuntu 24.04 - Python ${{ matrix.python-version }} (pypy3.9, 7) (push) Has been cancelled
Linux / MacOS 15 - Python ${{ matrix.python-version }} (3.10, 7.2) (push) Has been cancelled
Linux / MacOS 15 - Python ${{ matrix.python-version }} (3.11, 7.2) (push) Has been cancelled
Linux / MacOS 15 - Python ${{ matrix.python-version }} (3.12, 7.2) (push) Has been cancelled
Linux / MacOS 15 - Python ${{ matrix.python-version }} (3.13, 7.2) (push) Has been cancelled
Linux / MacOS 15 - Python ${{ matrix.python-version }} (3.8, 7.2) (push) Has been cancelled
Linux / MacOS 15 - Python ${{ matrix.python-version }} (3.9, 7.2) (push) Has been cancelled
Linux / MacOS 15 - Python ${{ matrix.python-version }} (pypy3.10, 7.2) (push) Has been cancelled
Linux / MacOS 15 - Python ${{ matrix.python-version }} (pypy3.8, 7.2) (push) Has been cancelled
Linux / MacOS 15 - Python ${{ matrix.python-version }} (pypy3.9, 7.2) (push) Has been cancelled
Linux / Windows latest - Python ${{ matrix.python-version }} (3.10) (push) Has been cancelled
Linux / Windows latest - Python ${{ matrix.python-version }} (3.11) (push) Has been cancelled
Linux / Windows latest - Python ${{ matrix.python-version }} (3.12) (push) Has been cancelled
Linux / Windows latest - Python ${{ matrix.python-version }} (3.13) (push) Has been cancelled
Linux / Windows latest - Python ${{ matrix.python-version }} (3.8) (push) Has been cancelled
Linux / Windows latest - Python ${{ matrix.python-version }} (3.9) (push) Has been cancelled
2025-09-10 23:21:34 +02:00
Tobias Wellnitz, DH1TW 8ef752e3ad added unittest for frequency conversion (2.4GHz - 13cm) 2025-07-30 00:49:38 +02:00
Tobias Wellnitz, DH1TW 9c61f75c28 release version 0.12.0 2025-06-10 01:34:16 +02:00
Tobias Wellnitz, DH1TW b51b32e575 updated CI build system 2025-06-10 01:32:11 +02:00
Tobias Wellnitz, DH1TW a12616ceca added missing packages for requirements-pytest 2025-06-09 23:29:42 +02:00
Dawid 1a79467db1
Add higher uWave bands and 10 char locators support (#32) 2025-06-09 23:21:27 +02:00
Tobias Wellnitz, DH1TW 4bdaf8d335 updated readme & license 2025-03-02 00:10:44 +01:00
Tobias Wellnitz, DH1TW 940c0f072c added support for python 3.13 2025-03-02 00:07:00 +01:00
Tobias Wellnitz, DH1TW ac444fa36b CI: fixed redis dependency on windows 2024-06-01 01:30:20 +02:00
Tobias Wellnitz, DH1TW 17117b1c20 fixed lat/long unittest 2024-06-01 00:45:57 +02:00
Tobias Wellnitz, DH1TW 1c3536396d skip LOTW download test until LOTW is again recovered 2024-06-01 00:39:04 +02:00
Tobias Wellnitz, DH1TW 5ec3461d03 support for 4, 6, 8 char precision maidenhead locator conversions
# fixes bug report #30
2024-06-01 00:33:10 +02:00
18 changed files with 315 additions and 138 deletions

View file

@ -3,61 +3,17 @@ name: Linux
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
test_linux: test_linux:
# Ubuntu 20.04 is still required for python 3.6; this doesn't work on Ubuntu 22.04 anymore runs-on: "ubuntu-24.04"
runs-on: "ubuntu-20.04" name: "Ubuntu 24.04 - Python ${{ matrix.python-version }}"
name: "Ubuntu 20.04 - Python ${{ matrix.python-version }}"
strategy:
matrix:
python-version: ["3.6"]
redis-version: [6]
steps:
- uses: "actions/checkout@v3"
- uses: "actions/setup-python@v4"
with:
python-version: "${{ matrix.python-version }}"
cache: "pip"
cache-dependency-path: |
**/setup.py
**/requirements*.txt
- name: "Install dependencies"
run: |
set -xe
sudo apt-get install -y libxml2-dev libxslt-dev
python -VV
python -m pip install --upgrade pip setuptools
python -m pip install -e .
python -m pip install -r requirements-pytest.txt
- name: Start Redis
uses: supercharge/redis-github-action@1.2.0
with:
redis-version: ${{ matrix.redis-version }}
- name: "Run tests for ${{ matrix.python-version }}"
env:
CLUBLOG_APIKEY: ${{ secrets.CLUBLOG_APIKEY }}
QRZ_USERNAME: ${{ secrets.QRZ_USERNAME }}
QRZ_PWD: ${{ secrets.QRZ_PWD }}
PYTHON_VERSION: ${{ matrix.python-version }}
# delay the execution randomly by a couple of seconds to reduce the amount
# of concurrent API calls on Clublog and QRZ.com when all CI jobs execute simultaneously
run: |
sleep $[ ( $RANDOM % 10 ) + 1 ]s
pytest ./test
test_linux_ubuntu_22:
runs-on: "ubuntu-22.04"
name: "Ubuntu 22.04 - Python ${{ matrix.python-version }}"
env: env:
USING_COVERAGE: '3.11' USING_COVERAGE: '3.11'
strategy: strategy:
matrix: matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.7", "pypy3.8", "pypy3.9", "pypy3.10"] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "pypy3.8", "pypy3.9", "pypy3.10"]
redis-version: [6] redis-version: [7]
steps: steps:
- uses: "actions/checkout@v3" - uses: "actions/checkout@v3"
@ -93,7 +49,7 @@ jobs:
# delay the execution randomly by a couple of seconds to reduce the amount # delay the execution randomly by a couple of seconds to reduce the amount
# of concurrent API calls on Clublog and QRZ.com when all CI jobs execute simultaneously # of concurrent API calls on Clublog and QRZ.com when all CI jobs execute simultaneously
run: | run: |
sleep $[ ( $RANDOM % 10 ) + 1 ]s sleep $[ ( $RANDOM % 60 ) + 1 ]s
if [[ $PYTHON_VERSION == 3.11 ]] if [[ $PYTHON_VERSION == 3.11 ]]
then then
pytest --cov=test/ pytest --cov=test/
@ -116,13 +72,12 @@ jobs:
test_macos: test_macos:
# Ubuntu 20.04 is still required for python 3.6; this doesn't work on Ubuntu 22.04 anymore runs-on: "macos-15"
runs-on: "macos-12" name: "MacOS 15 - Python ${{ matrix.python-version }}"
name: "MacOS 12 - Python ${{ matrix.python-version }}"
strategy: strategy:
matrix: matrix:
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.8", "pypy3.9", "pypy3.10"] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "pypy3.8", "pypy3.9", "pypy3.10"]
redis-version: [6] redis-version: [7.2]
steps: steps:
- uses: "actions/checkout@v3" - uses: "actions/checkout@v3"
@ -143,7 +98,7 @@ jobs:
python -m pip install -r requirements-pytest.txt python -m pip install -r requirements-pytest.txt
- name: Start Redis - name: Start Redis
uses: shogo82148/actions-setup-redis@v1.31.1 uses: shogo82148/actions-setup-redis@v1
with: with:
redis-version: ${{ matrix.redis-version }} redis-version: ${{ matrix.redis-version }}
@ -156,16 +111,16 @@ jobs:
# delay the execution randomly by a couple of seconds to reduce the amount # delay the execution randomly by a couple of seconds to reduce the amount
# of concurrent API calls on Clublog and QRZ.com when all CI jobs execute simultaneously # of concurrent API calls on Clublog and QRZ.com when all CI jobs execute simultaneously
run: | run: |
sleep $[ ( $RANDOM % 10 ) + 1 ] sleep $[ ( $RANDOM % 60 ) + 1 ]
pytest ./test pytest ./test
test_windows: test_windows:
runs-on: "windows-latest" runs-on: "windows-2022"
name: "Windows latest - Python ${{ matrix.python-version }}" name: "Windows latest - Python ${{ matrix.python-version }}"
strategy: strategy:
matrix: matrix:
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
steps: steps:
- uses: "actions/checkout@v3" - uses: "actions/checkout@v3"
@ -190,10 +145,13 @@ jobs:
# since there is no official redis version for windows. # since there is no official redis version for windows.
# Redis is then installed an run as a service # Redis is then installed an run as a service
run: | run: |
C:\msys64\usr\bin\wget.exe https://github.com/redis-windows/redis-windows/releases/download/7.0.14/Redis-7.0.14-Windows-x64-with-Service.tar.gz C:\msys64\usr\bin\wget.exe https://github.com/redis-windows/redis-windows/releases/download/7.0.14/Redis-7.0.14-Windows-x64-msys2-with-Service.zip
C:\msys64\usr\bin\tar.exe -xvzf Redis-7.0.14-Windows-x64-with-Service.tar.gz C:\msys64\usr\bin\pacman.exe -S --noconfirm unzip
sc.exe create Redis binpath=D:\a\pyhamtools\pyhamtools\Redis-7.0.14-Windows-x64-with-Service\RedisService.exe start= auto C:\msys64\usr\bin\unzip.exe Redis-7.0.14-Windows-x64-msys2-with-Service.zip
sc.exe create Redis binpath=${{ github.workspace }}\Redis-7.0.14-Windows-x64-msys2-with-Service\RedisService.exe start= auto
echo "Redis service created, now starting it"
net start Redis net start Redis
echo "Redis service started"
- name: "Run tests for ${{ matrix.python-version }}" - name: "Run tests for ${{ matrix.python-version }}"
env: env:
@ -206,5 +164,5 @@ jobs:
# amount of concurrent API calls on Clublog and QRZ.com # amount of concurrent API calls on Clublog and QRZ.com
# when all CI jobs execute simultaneously # when all CI jobs execute simultaneously
run: | run: |
start-sleep -Seconds (5..20 | get-random) start-sleep -Seconds (5..60 | get-random)
pytest pytest

View file

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2014 Tobias Wellnitz Copyright (c) 2025 Tobias Wellnitz
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -34,20 +34,22 @@ This Library is used in production at the [DXHeat.com DX Cluster](https://dxheat
Pyhamtools is compatible with Python >=3.6. Pyhamtools is compatible with Python >=3.6.
We check compatibility on OSX, Windows, and Linux with the following Python versions: We check compatibility on OSX, Windows, and Linux with the following Python versions:
* Python 3.6 (will be deprecated in 2024)
* Python 3.7 (will be deprecated in 2024)
* Python 3.8 * Python 3.8
* Python 3.9 * Python 3.9
* Python 3.10 * Python 3.10
* Python 3.11 * Python 3.11
* Python 3.12 * Python 3.12
* [pypy3.7](https://pypy.org/) (will be deprecated in 2024) * Python 3.13
* [pypy3.8](https://pypy.org/) * [pypy3.8](https://pypy.org/)
* [pypy3.9](https://pypy.org/) * [pypy3.9](https://pypy.org/)
* [pypy3.10](https://pypy.org/) * [pypy3.10](https://pypy.org/)
### depreciated: Python 2.7 & Python 3.5
The support for Python 2.7 and 3.5 has been deprecated at the end of 2023. The last version which supports Python 2.7 and Python 3.5 is 0.8.7. The support for Python 2.7 and 3.5 has been deprecated at the end of 2023. The last version which supports Python 2.7 and Python 3.5 is 0.8.7.
### depricated: Python 3.6 & Python 3.7
Support for Python 3.6 and Python 3.7 has been deprecated in June 2025. The last version which support Python 3.6 and Python 3.7 is 0.11.0.
## Documentation ## Documentation
Check out the full documentation including the changelog at: Check out the full documentation including the changelog at:

View file

@ -1,6 +1,33 @@
Changelog Changelog
--------- ---------
PyHamtools 0.12.0
================
09. June 2025
* deprecated support for Python 3.6
* deprecated support for Python 3.7
* added support for higher Microwave bands (tnx @sq6emm)
* added support for 10 characters Maidenhead locators (tnx @sq6emm)
* updated CI pipeline
PyHamtools 0.11.0
================
02. March 2025
* added support for Python 3.13
PyHamtools 0.10.0
================
01. June 2024
* full support for 4, 6, 8 characters Maidenhead locator conversions
PyHamtools 0.9.1 PyHamtools 0.9.1
================ ================

View file

@ -25,6 +25,7 @@ from pyhamtools.version import __version__, __release__
extensions = [ extensions = [
'sphinx.ext.autodoc', 'sphinx.ext.autodoc',
'sphinx.ext.napoleon', 'sphinx.ext.napoleon',
'sphinx_rtd_dark_mode',
] ]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
@ -95,7 +96,9 @@ pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # a list of builtin themes.
html_theme = 'default' # html_theme = 'default'
html_theme = 'sphinx_rtd_theme'
# html_theme = 'sphinx_material'
# Theme options are theme-specific and customize the look and feel of a theme # Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the # further. For a list of options available for each theme, see the

View file

@ -182,7 +182,7 @@ def freq_to_band(freq):
elif ((freq >= 1200000) and (freq <= 1300000)): elif ((freq >= 1200000) and (freq <= 1300000)):
band = 0.23 #23cm band = 0.23 #23cm
mode = None mode = None
elif ((freq >= 2390000) and (freq <= 2450000)): elif ((freq >= 2300000) and (freq <= 2450000)):
band = 0.13 #13cm band = 0.13 #13cm
mode = None mode = None
elif ((freq >= 3300000) and (freq <= 3500000)): elif ((freq >= 3300000) and (freq <= 3500000)):
@ -200,6 +200,18 @@ def freq_to_band(freq):
elif ((freq >= 47000000) and (freq <= 47200000)): elif ((freq >= 47000000) and (freq <= 47200000)):
band = 0.0063 #6,3mm band = 0.0063 #6,3mm
mode = None mode = None
elif ((freq >= 75500000) and (freq <= 81500000)):
band = 0.004 #4mm
mode = None
elif ((freq >= 122250000) and (freq <= 123000000)):
band = 0.0025 #2.5mm
mode = None
elif ((freq >= 134000000) and (freq <= 141000000)):
band = 0.002 #2mm
mode = None
elif ((freq >= 241000000) and (freq <= 250000000)):
band = 0.001 #1mm
mode = None
else: else:
raise KeyError raise KeyError

View file

@ -3,12 +3,13 @@ from datetime import datetime, timezone
import ephem import ephem
def latlong_to_locator (latitude, longitude): def latlong_to_locator (latitude, longitude, precision=6):
"""converts WGS84 coordinates into the corresponding Maidenhead Locator """converts WGS84 coordinates into the corresponding Maidenhead Locator
Args: Args:
latitude (float): Latitude latitude (float): Latitude
longitude (float): Longitude longitude (float): Longitude
precision (int): 4,6,8,10 chars (default 6)
Returns: Returns:
string: Maidenhead locator string: Maidenhead locator
@ -32,35 +33,54 @@ def latlong_to_locator (latitude, longitude):
""" """
if precision < 4 or precision == 5 or precision == 7 or precision == 9 or precision > 10:
return ValueError
if longitude >= 180 or longitude <= -180: if longitude >= 180 or longitude <= -180:
raise ValueError raise ValueError
if latitude >= 90 or latitude <= -90: if latitude >= 90 or latitude <= -90:
raise ValueError raise ValueError
longitude += 180; longitude +=180
latitude +=90; latitude +=90
locator = chr(ord('A') + int(longitude / 20)) # copied & adapted from github.com/space-physics/maidenhead
locator += chr(ord('A') + int(latitude / 10)) A = ord('A')
locator += chr(ord('0') + int((longitude % 20) / 2)) a = divmod(longitude, 20)
locator += chr(ord('0') + int(latitude % 10)) b = divmod(latitude, 10)
locator += chr(ord('A') + int((longitude - int(longitude / 2) * 2) / (2 / 24))) locator = chr(A + int(a[0])) + chr(A + int(b[0]))
locator += chr(ord('A') + int((latitude - int(latitude / 1) * 1 ) / (1 / 24))) lon = a[1] / 2.0
lat = b[1]
i = 1
while i < precision/2:
i += 1
a = divmod(lon, 1)
b = divmod(lat, 1)
if not (i % 2):
locator += str(int(a[0])) + str(int(b[0]))
lon = 24 * a[1]
lat = 24 * b[1]
else:
locator += chr(A + int(a[0])) + chr(A + int(b[0]))
lon = 10 * a[1]
lat = 10 * b[1]
return locator return locator
def locator_to_latlong (locator): def locator_to_latlong (locator, center=True):
"""converts Maidenhead locator in the corresponding WGS84 coordinates """converts Maidenhead locator in the corresponding WGS84 coordinates
Args: Args:
locator (string): Locator, either 4 or 6 characters locator (string): Locator, either 4, 6 or 8 characters
center (bool): Center of (sub)square. By default True. If False, the south/western corner will be returned
Returns: Returns:
tuple (float, float): Latitude, Longitude tuple (float, float): Latitude, Longitude
Raises: Raises:
ValueError: When called with wrong or invalid input arg ValueError: When called with wrong or invalid Maidenhead locator string
TypeError: When arg is not a string TypeError: When arg is not a string
Example: Example:
@ -79,7 +99,7 @@ def locator_to_latlong (locator):
locator = locator.upper() locator = locator.upper()
if len(locator) == 5 or len(locator) < 4: if len(locator) < 4 or len(locator) == 5 or len(locator) == 7 or len(locator) == 9:
raise ValueError raise ValueError
if ord(locator[0]) > ord('R') or ord(locator[0]) < ord('A'): if ord(locator[0]) > ord('R') or ord(locator[0]) < ord('A'):
@ -100,23 +120,64 @@ def locator_to_latlong (locator):
if ord (locator[5]) > ord('X') or ord(locator[5]) < ord('A'): if ord (locator[5]) > ord('X') or ord(locator[5]) < ord('A'):
raise ValueError raise ValueError
if len(locator) == 8:
if ord(locator[6]) > ord('9') or ord(locator[6]) < ord('0'):
raise ValueError
if ord (locator[7]) > ord('9') or ord(locator[7]) < ord('0'):
raise ValueError
if len(locator) == 10:
if ord(locator[8]) > ord('X') or ord(locator[8]) < ord('A'):
raise ValueError
if ord (locator[9]) > ord('X') or ord(locator[9]) < ord('A'):
raise ValueError
longitude = (ord(locator[0]) - ord('A')) * 20 - 180 longitude = (ord(locator[0]) - ord('A')) * 20 - 180
latitude = (ord(locator[1]) - ord('A')) * 10 - 90 latitude = (ord(locator[1]) - ord('A')) * 10 - 90
longitude += (ord(locator[2]) - ord('0')) * 2 longitude += (ord(locator[2]) - ord('0')) * 2
latitude += (ord(locator[3]) - ord('0')) latitude += (ord(locator[3]) - ord('0')) * 1
if len(locator) == 6: if len(locator) == 4:
longitude += ((ord(locator[4])) - ord('A')) * (2 / 24)
latitude += ((ord(locator[5])) - ord('A')) * (1 / 24)
# move to center of subsquare if center:
longitude += 1 / 24 longitude += 2 / 2
latitude += 0.5 / 24 latitude += 1.0 / 2
elif len(locator) == 6:
longitude += (ord(locator[4]) - ord('A')) * 5.0 / 60
latitude += (ord(locator[5]) - ord('A')) * 2.5 / 60
if center:
longitude += 5.0 / 60 / 2
latitude += 2.5 / 60 / 2
elif len(locator) == 8:
longitude += (ord(locator[4]) - ord('A')) * 5.0 / 60
latitude += (ord(locator[5]) - ord('A')) * 2.5 / 60
longitude += int(locator[6]) * 5.0 / 600
latitude += int(locator[7]) * 2.5 / 600
if center:
longitude += 5.0 / 600 / 2
latitude += 2.5 / 600 / 2
elif len(locator) == 10:
longitude += (ord(locator[4]) - ord('A')) * 5.0 / 60
latitude += (ord(locator[5]) - ord('A')) * 2.5 / 60
longitude += int(locator[6]) * 5.0 / 600
latitude += int(locator[7]) * 2.5 / 600
longitude += (ord(locator[8]) - ord('A')) * 1.0 / 2880
latitude += (ord(locator[9]) - ord('A')) * 1.0 / 5760
if center:
longitude += 1.0 / 2880 / 2
latitude += 1.0 / 5760 / 2
else: else:
# move to center of square raise ValueError
longitude += 1;
latitude += 0.5;
return latitude, longitude return latitude, longitude
@ -125,14 +186,14 @@ def calculate_distance(locator1, locator2):
"""calculates the (shortpath) distance between two Maidenhead locators """calculates the (shortpath) distance between two Maidenhead locators
Args: Args:
locator1 (string): Locator, either 4 or 6 characters locator1 (string): Locator, either 4, 6 or 8 characters
locator2 (string): Locator, either 4 or 6 characters locator2 (string): Locator, either 4, 6 or 8 characters
Returns: Returns:
float: Distance in km float: Distance in km
Raises: Raises:
ValueError: When called with wrong or invalid input arg ValueError: When called with wrong or invalid maidenhead locator strings
AttributeError: When args are not a string AttributeError: When args are not a string
Example: Example:
@ -142,6 +203,9 @@ def calculate_distance(locator1, locator2):
>>> calculate_distance("JN48QM", "QF67bf") >>> calculate_distance("JN48QM", "QF67bf")
16466.413 16466.413
Note:
Distances is calculated between the centers of the (sub) squares
""" """
R = 6371 #earh radius R = 6371 #earh radius
@ -160,15 +224,15 @@ def calculate_distance(locator1, locator2):
c = 2 * atan2(sqrt(a), sqrt(1-a)) c = 2 * atan2(sqrt(a), sqrt(1-a))
d = R * c #distance in km d = R * c #distance in km
return d; return d
def calculate_distance_longpath(locator1, locator2): def calculate_distance_longpath(locator1, locator2):
"""calculates the (longpath) distance between two Maidenhead locators """calculates the (longpath) distance between two Maidenhead locators
Args: Args:
locator1 (string): Locator, either 4 or 6 characters locator1 (string): Locator, either 4, 6 or 8 characters
locator2 (string): Locator, either 4 or 6 characters locator2 (string): Locator, either 4, 6 or 8 characters
Returns: Returns:
float: Distance in km float: Distance in km
@ -184,6 +248,8 @@ def calculate_distance_longpath(locator1, locator2):
>>> calculate_distance_longpath("JN48QM", "QF67bf") >>> calculate_distance_longpath("JN48QM", "QF67bf")
23541.5867 23541.5867
Note:
Distance is calculated between the centers of the (sub) squares
""" """
c = 40008 #[km] earth circumference c = 40008 #[km] earth circumference
@ -196,8 +262,8 @@ def calculate_heading(locator1, locator2):
"""calculates the heading from the first to the second locator """calculates the heading from the first to the second locator
Args: Args:
locator1 (string): Locator, either 4 or 6 characters locator1 (string): Locator, either 4, 6 or 8 characters
locator2 (string): Locator, either 4 or 6 characters locator2 (string): Locator, either 4, 6 or 6 characters
Returns: Returns:
float: Heading in deg float: Heading in deg
@ -213,6 +279,9 @@ def calculate_heading(locator1, locator2):
>>> calculate_heading("JN48QM", "QF67bf") >>> calculate_heading("JN48QM", "QF67bf")
74.3136 74.3136
Note:
Heading is calculated between the centers of the (sub) squares
""" """
lat1, long1 = locator_to_latlong(locator1) lat1, long1 = locator_to_latlong(locator1)
@ -236,8 +305,8 @@ def calculate_heading_longpath(locator1, locator2):
"""calculates the heading from the first to the second locator (long path) """calculates the heading from the first to the second locator (long path)
Args: Args:
locator1 (string): Locator, either 4 or 6 characters locator1 (string): Locator, either 4, 6 or 8 characters
locator2 (string): Locator, either 4 or 6 characters locator2 (string): Locator, either 4, 6 or 8 characters
Returns: Returns:
float: Long path heading in deg float: Long path heading in deg
@ -253,6 +322,9 @@ def calculate_heading_longpath(locator1, locator2):
>>> calculate_heading_longpath("JN48QM", "QF67bf") >>> calculate_heading_longpath("JN48QM", "QF67bf")
254.3136 254.3136
Note:
Distance is calculated between the centers of the (sub) squares
""" """
heading = calculate_heading(locator1, locator2) heading = calculate_heading(locator1, locator2)
@ -265,7 +337,7 @@ def calculate_sunrise_sunset(locator, calc_date=None):
"""calculates the next sunset and sunrise for a Maidenhead locator at a give date & time """calculates the next sunset and sunrise for a Maidenhead locator at a give date & time
Args: Args:
locator1 (string): Maidenhead Locator, either 4 or 6 characters locator1 (string): Maidenhead Locator, either 4, 6 or 8 characters
calc_date (datetime, optional): Starting datetime for the calculations (UTC) calc_date (datetime, optional): Starting datetime for the calculations (UTC)
Returns: Returns:

View file

@ -1,3 +1,3 @@
VERSION = (0, 9, 1) VERSION = (0, 12, 0)
__release__ = ''.join(['-.'[type(x) == int]+str(x) for x in VERSION])[1:] __release__ = ''.join(['-.'[type(x) == int]+str(x) for x in VERSION])[1:]
__version__ = '.'.join((str(VERSION[0]), str(VERSION[1]))) __version__ = '.'.join((str(VERSION[0]), str(VERSION[1])))

View file

@ -1,3 +1,5 @@
sphinx>=1.8.5 sphinx>=1.8.5
sphinxcontrib-napoleon>=0.7 sphinxcontrib-napoleon>=0.7
beautifulsoup4>=4.7.1 beautifulsoup4>=4.7.1
sphinx_rtd_theme>=0.5.2
sphinx_rtd_dark_mode>=0.1.2

View file

@ -1,5 +1,9 @@
pytest>=7.0.0; python_version>='3.7' pytest>=7.0.0
pytest==4.6.11; python_version=='3.6'
pytest-blockage>=0.2.2 pytest-blockage>=0.2.2
pytest-localserver>=0.5 pytest-localserver>=0.5
pytest-cov>=2.12 pytest-cov>=2.12
maidenhead==1.6.0
requests>=2.32.4
beautifulsoup4==4.13.4
redis==5.2.1
ephem==4.2

View file

@ -18,7 +18,7 @@ setup(name='pyhamtools',
"requests>=2.21.0", "requests>=2.21.0",
"ephem>=4.1.3", "ephem>=4.1.3",
"beautifulsoup4>=4.7.1", "beautifulsoup4>=4.7.1",
"lxml>=4.8.0,<5.0.0", "lxml>=5.0.0",
"redis>=2.10.6", "redis>=2.10.6",
], ],
**kw **kw

View file

@ -90,12 +90,12 @@ response_prefix_VK9DWX_clublog = {
} }
response_prefix_VK9DLX_clublog = { response_prefix_VK9DLX_clublog = {
u'adif': 147, u'adif': 189,
u'continent': u'OC', u'continent': u'OC',
u'country': u'LORD HOWE ISLAND', u'country': u'NORFOLK ISLAND',
u'cqz': 30, u'cqz': 32,
u'latitude': -31.6, u'latitude': -29.0,
u'longitude': 159.1 u'longitude': 168.0
} }
response_prefix_TA7I_clublog = { response_prefix_TA7I_clublog = {
@ -126,13 +126,13 @@ response_prefix_V26K_clublog = {
} }
response_prefix_VK9DLX_countryfile = { response_prefix_VK9DLX_countryfile = {
u'adif': 147, u'adif': 189,
u'continent': u'OC', u'continent': u'OC',
u'country': u'Lord Howe Island', u'country': u'Norfolk Island',
u'cqz': 30, u'cqz': 32,
u'ituz': 60, u'ituz': 60,
u'latitude': -31.55, u'latitude': -29.03,
u'longitude': 159.08 u'longitude': 167.93
} }
response_prefix_VK9GMW_clublog = { response_prefix_VK9GMW_clublog = {
@ -195,7 +195,7 @@ response_Exception_VK9XO_with_start_date = {
'country': 'CHRISTMAS ISLAND', 'country': 'CHRISTMAS ISLAND',
'continent': 'OC', 'continent': 'OC',
'latitude': -10.48, 'latitude': -10.48,
'longitude': 105.71, 'longitude': 105.62,
'cqz': 29 'cqz': 29
} }

View file

@ -14,7 +14,12 @@ class Test_calculate_distance():
assert abs(calculate_distance("JN48QM", "FN44AB") - 5965) < 1 assert abs(calculate_distance("JN48QM", "FN44AB") - 5965) < 1
assert abs(calculate_distance("FN44AB", "JN48QM") - 5965) < 1 assert abs(calculate_distance("FN44AB", "JN48QM") - 5965) < 1
assert abs(calculate_distance("JN48QM", "QF67bf") - 16467) < 1 assert abs(calculate_distance("JN48QM", "QF67BF") - 16467) < 1
assert abs(calculate_distance("JN48QM84", "QF67BF84") - 16467) < 1
assert abs(calculate_distance("JN48QM84", "QF67BF") - 16464) < 1
assert abs(calculate_distance("JN48QM84", "QF67") - 16506) < 1
assert abs(calculate_distance("JN48QM", "QF67") - 16508) < 1
assert abs(calculate_distance("JN48", "QF67") - 16535) < 1
def test_calculate_distance_invalid_inputs(self): def test_calculate_distance_invalid_inputs(self):
with pytest.raises(AttributeError): with pytest.raises(AttributeError):

View file

@ -8,10 +8,24 @@ class Test_latlong_to_locator():
assert latlong_to_locator(-89.97916, -179.95833) == "AA00AA" assert latlong_to_locator(-89.97916, -179.95833) == "AA00AA"
assert latlong_to_locator(89.97916, 179.9583) == "RR99XX" assert latlong_to_locator(89.97916, 179.9583) == "RR99XX"
def test_latlong_to_locator_normal_case(self): def test_latlong_to_locator_4chars_precision(self):
assert latlong_to_locator(48.52083, 9.3750000, precision=4) == "JN48"
assert latlong_to_locator(39.222916, -86.45416, 4) == "EM69"
def test_latlong_to_locator_6chars_precision(self):
assert latlong_to_locator(48.52083, 9.3750000) == "JN48QM" assert latlong_to_locator(48.52083, 9.3750000) == "JN48QM"
assert latlong_to_locator(48.5, 9.0) == "JN48MM" #center of the square assert latlong_to_locator(48.5, 9.0) == "JN48MM" #center of the square
assert latlong_to_locator(39.222916, -86.45416, 6) == "EM69SF"
def test_latlong_to_locator_8chars_precision(self):
assert latlong_to_locator(48.51760, 9.40345, precision=8) == "JN48QM84"
assert latlong_to_locator(39.222916, -86.45416, 8) == "EM69SF53"
def test_latlong_to_locator_10chars_precision(self):
assert latlong_to_locator(45.835677, 68.525173, precision=10) == "MN45GU30AN"
assert latlong_to_locator(51.124913, 16.941840, 10) == "JO81LC39AX"
def test_latlong_to_locator_invalid_characters(self): def test_latlong_to_locator_invalid_characters(self):

View file

@ -1,10 +1,12 @@
import pytest import pytest
import maidenhead
from pyhamtools.locator import locator_to_latlong from pyhamtools.locator import locator_to_latlong
from pyhamtools.consts import LookupConventions as const from pyhamtools.consts import LookupConventions as const
class Test_locator_to_latlong(): class Test_locator_to_latlong():
def test_locator_to_latlong_edge_cases(self): def test_locator_to_latlong_min_max_cases(self):
latitude, longitude = locator_to_latlong("AA00AA") latitude, longitude = locator_to_latlong("AA00AA")
assert abs(latitude + 89.97916) < 0.00001 assert abs(latitude + 89.97916) < 0.00001
assert abs(longitude +179.95833) < 0.0001 assert abs(longitude +179.95833) < 0.0001
@ -13,23 +15,79 @@ class Test_locator_to_latlong():
assert abs(latitude - 89.97916) < 0.00001 assert abs(latitude - 89.97916) < 0.00001
assert abs(longitude - 179.9583) < 0.0001 assert abs(longitude - 179.9583) < 0.0001
def test_locator_to_latlong_normal_case(self): def test_locator_to_latlong_4chars_precision(self):
latitude, longitude = locator_to_latlong("JN48QM")
assert abs(latitude - 48.52083) < 0.00001
assert abs(longitude - 9.3750000) < 0.0001
latitude, longitude = locator_to_latlong("JN48") latitude, longitude = locator_to_latlong("JN48")
assert abs(latitude - 48.5) < 0.001 assert abs(latitude - 48.5) < 0.1
assert abs(longitude - 9.000) < 0.001 assert abs(longitude - 9.0) < 0.1
def test_locator_to_latlong_mixed_signs(self): latitude, longitude = locator_to_latlong("JN48", center=False)
assert abs(latitude - 48) < 0.1
assert abs(longitude - 8) < 0.1
def test_locator_to_latlong_6chars_precision(self):
latitude, longitude = locator_to_latlong("JN48QM")
assert abs(latitude - 48.52083) < 0.00001
assert abs(longitude - 9.37500) < 0.00001
def test_locator_to_latlong_8chars_precision(self):
latitude, longitude = locator_to_latlong("JN48QM84")
assert abs(latitude - 48.51875) < 0.00001
assert abs(longitude - 9.40416) < 0.00001
latitude, longitude = locator_to_latlong("EM69SF53")
assert abs(latitude - 39.222916) < 0.00001
assert abs(longitude + 86.45416) < 0.00001
def test_locator_to_latlong_10chars_precision(self):
latitude, longitude = locator_to_latlong("JO81LC39AX")
assert abs(latitude - 51.124913) < 0.000001
assert abs(longitude - 16.941840) < 0.000001
latitude, longitude = locator_to_latlong("MN45GU30AN")
assert abs(latitude - 45.835677) < 0.000001
assert abs(longitude - 68.525173) < 0.000001
def test_locator_to_latlong_consistency_checks_6chars_lower_left_corner(self):
latitude_4, longitude_4 = locator_to_latlong("JN48", center=False)
latitude_6, longitude_6 = locator_to_latlong("JN48AA", center=False)
assert latitude_4 == latitude_6
assert longitude_4 == longitude_6
def test_locator_to_latlong_consistency_checks_8chars_lower_left_corner(self):
latitude_6, longitude_6 = locator_to_latlong("JN48AA", center=False)
latitude_8, longitude_8 = locator_to_latlong("JN48AA00", center=False)
assert latitude_6 == latitude_8
assert longitude_6 == longitude_8
def test_locator_to_latlong_consistency_checks_against_maidenhead(self):
locs = ["JN48", "EM69", "JN48QM", "EM69SF", "AA00AA", "RR99XX", "JN48QM84", "EM69SF53"]
# lower left (south/east) corner
for loc in locs:
lat, lon = locator_to_latlong(loc, center=False)
lat_m, lon_m = maidenhead.to_location(loc)
assert abs(lat - lat_m) < 0.00001
assert abs(lon - lon_m) < 0.00001
# center of square
for loc in locs:
lat, lon = locator_to_latlong(loc) # default: center=True
lat_m, lon_m = maidenhead.to_location(loc, center=True)
assert abs(lat - lat_m) < 0.1
assert abs(lon - lon_m) < 0.1
def test_locator_to_latlong_upper_lower_chars(self):
latitude, longitude = locator_to_latlong("Jn48qM") latitude, longitude = locator_to_latlong("Jn48qM")
assert abs(latitude - 48.52083) < 0.00001 assert abs(latitude - 48.52083) < 0.00001
assert abs(longitude - 9.3750000) < 0.0001 assert abs(longitude - 9.3750000) < 0.0001
def test_locator_to_latlong_wrong_amount_of_characters(self): def test_locator_to_latlong_wrong_amount_of_characters(self):
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -44,11 +102,29 @@ class Test_locator_to_latlong():
with pytest.raises(ValueError): with pytest.raises(ValueError):
latitude, longitude = locator_to_latlong("JN8Q") latitude, longitude = locator_to_latlong("JN8Q")
with pytest.raises(ValueError):
latitude, longitude = locator_to_latlong("JN8QM1")
with pytest.raises(ValueError):
latitude, longitude = locator_to_latlong("JN8QM1AA")
def test_locator_to_latlong_invalid_characters(self): def test_locator_to_latlong_invalid_characters(self):
with pytest.raises(ValueError): with pytest.raises(ValueError):
latitude, longitude = locator_to_latlong("21XM99") latitude, longitude = locator_to_latlong("21XM99")
with pytest.raises(ValueError):
latitude, longitude = locator_to_latlong("48")
with pytest.raises(ValueError):
latitude, longitude = locator_to_latlong("JNJN")
with pytest.raises(ValueError):
latitude, longitude = locator_to_latlong("JN4848")
with pytest.raises(ValueError):
latitude, longitude = locator_to_latlong("JN48QMaa")
with pytest.raises(ValueError): with pytest.raises(ValueError):
latitude, longitude = locator_to_latlong("****") latitude, longitude = locator_to_latlong("****")

View file

@ -25,8 +25,8 @@ response_Exception_KC6MM_1990 = {
'adif': 22, 'adif': 22,
'country': u'PALAU', 'country': u'PALAU',
'continent': u'OC', 'continent': u'OC',
'latitude': 9.50, 'latitude': 9.52,
'longitude': 138.20, 'longitude': 138.21,
'cqz': 27, 'cqz': 27,
} }
@ -34,8 +34,8 @@ response_Exception_KC6MM_1992 = {
'adif': 22, 'adif': 22,
'country': u'PALAU', 'country': u'PALAU',
'continent': u'OC', 'continent': u'OC',
'latitude': 9.50, 'latitude': 9.52,
'longitude': 138.20, 'longitude': 138.21,
'cqz': 27, 'cqz': 27,
} }
@ -53,7 +53,7 @@ response_Exception_VK9XO_with_start_date = {
'country': u'CHRISTMAS ISLAND', 'country': u'CHRISTMAS ISLAND',
'continent': u'OC', 'continent': u'OC',
'latitude': -10.48, 'latitude': -10.48,
'longitude': 105.71, 'longitude': 105.62,
'cqz': 29, 'cqz': 29,
} }

View file

@ -29,6 +29,7 @@ class Test_lotw_methods:
execfile(os.path.join(fix_dir,"lotw_fixture.py"), namespace) execfile(os.path.join(fix_dir,"lotw_fixture.py"), namespace)
assert get_lotw_users(url=httpserver.url) == namespace['lotw_fixture'] assert get_lotw_users(url=httpserver.url) == namespace['lotw_fixture']
@pytest.mark.skip("ARRL has been hacked in May 2024; skipping until LOTW is again up")
def test_download_lotw_list_and_check_types(self): def test_download_lotw_list_and_check_types(self):
data = get_lotw_users() data = get_lotw_users()

View file

@ -65,6 +65,7 @@ class Test_utils_freq_to_band():
assert freq_to_band(1200000) == {"band" : 0.23, "mode":None} assert freq_to_band(1200000) == {"band" : 0.23, "mode":None}
def test_shf_frequencies(self): def test_shf_frequencies(self):
assert freq_to_band(2320200) == {"band" : 0.13, "mode":None}
assert freq_to_band(2390000) == {"band" : 0.13, "mode":None} assert freq_to_band(2390000) == {"band" : 0.13, "mode":None}
assert freq_to_band(3300000) == {"band" : 0.09, "mode":None} assert freq_to_band(3300000) == {"band" : 0.09, "mode":None}