Compare commits

..

21 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
Tobias Wellnitz, DH1TW 84d88faf69 created release 0.9.1 2024-03-17 23:11:25 +01:00
Tobias Wellnitz 5799c7337b
Merge pull request #28 from df7cb/setuptools
Switch from distutils to setuptools
2024-03-17 23:03:39 +01:00
Tobias Wellnitz, DH1TW cea30d761a limited lxml to version <5.0 for compatibility with pypy-3.7 2024-03-17 22:32:11 +01:00
Tobias Wellnitz, DH1TW 71b2a743db added default codeowner 2024-03-16 16:55:39 +01:00
tony mancill 8d765b9346 Switch from distutils to setuptools
https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1065919
2024-03-11 10:47:27 +01:00
Tobias Wellnitz, DH1TW f2b2f16806 readme: clarified neededed dependencies 2024-01-03 22:09:42 +01:00
Tobias Wellnitz, DH1TW 27d61089a7 configured readthedocs 2023-12-29 00:21:59 +01:00
Tobias Wellnitz, DH1TW b5ba291e0d updated docs config path 2023-12-28 21:12:52 +01:00
Tobias Wellnitz, DH1TW c447cdc6a8 added readthedocs config file 2023-12-28 21:11:16 +01:00
20 changed files with 365 additions and 142 deletions

2
.github/CODEOWNERS vendored Normal file
View file

@ -0,0 +1,2 @@
# global code owner
@DH1TW

View file

@ -3,61 +3,17 @@ name: Linux
on: [push, pull_request]
jobs:
test_linux:
# Ubuntu 20.04 is still required for python 3.6; this doesn't work on Ubuntu 22.04 anymore
runs-on: "ubuntu-20.04"
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 }}"
runs-on: "ubuntu-24.04"
name: "Ubuntu 24.04 - Python ${{ matrix.python-version }}"
env:
USING_COVERAGE: '3.11'
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.7", "pypy3.8", "pypy3.9", "pypy3.10"]
redis-version: [6]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "pypy3.8", "pypy3.9", "pypy3.10"]
redis-version: [7]
steps:
- uses: "actions/checkout@v3"
@ -93,7 +49,7 @@ jobs:
# 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
sleep $[ ( $RANDOM % 60 ) + 1 ]s
if [[ $PYTHON_VERSION == 3.11 ]]
then
pytest --cov=test/
@ -116,13 +72,12 @@ jobs:
test_macos:
# Ubuntu 20.04 is still required for python 3.6; this doesn't work on Ubuntu 22.04 anymore
runs-on: "macos-12"
name: "MacOS 12 - Python ${{ matrix.python-version }}"
runs-on: "macos-15"
name: "MacOS 15 - Python ${{ matrix.python-version }}"
strategy:
matrix:
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.8", "pypy3.9", "pypy3.10"]
redis-version: [6]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "pypy3.8", "pypy3.9", "pypy3.10"]
redis-version: [7.2]
steps:
- uses: "actions/checkout@v3"
@ -143,7 +98,7 @@ jobs:
python -m pip install -r requirements-pytest.txt
- name: Start Redis
uses: shogo82148/actions-setup-redis@v1.31.1
uses: shogo82148/actions-setup-redis@v1
with:
redis-version: ${{ matrix.redis-version }}
@ -156,16 +111,16 @@ jobs:
# 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 ]
sleep $[ ( $RANDOM % 60 ) + 1 ]
pytest ./test
test_windows:
runs-on: "windows-latest"
runs-on: "windows-2022"
name: "Windows latest - Python ${{ matrix.python-version }}"
strategy:
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:
- uses: "actions/checkout@v3"
@ -190,10 +145,13 @@ jobs:
# since there is no official redis version for windows.
# Redis is then installed an run as a service
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\tar.exe -xvzf Redis-7.0.14-Windows-x64-with-Service.tar.gz
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\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\pacman.exe -S --noconfirm unzip
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
echo "Redis service started"
- name: "Run tests for ${{ matrix.python-version }}"
env:
@ -206,5 +164,5 @@ jobs:
# amount of concurrent API calls on Clublog and QRZ.com
# when all CI jobs execute simultaneously
run: |
start-sleep -Seconds (5..20 | get-random)
start-sleep -Seconds (5..60 | get-random)
pytest

33
.readthedocs.yaml Normal file
View file

@ -0,0 +1,33 @@
# Read the Docs configuration file for Sphinx projects
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the OS, Python version and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.11"
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: docs/source/conf.py
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
# builder: "dirhtml"
# Fail on all warnings to avoid broken references
# fail_on_warning: true
# Optionally build your docs in additional formats such as PDF and ePub
# formats:
# - pdf
# - epub
# Optional but recommended, declare the Python requirements required
# to build your documentation
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- method: setuptools
path: .
- requirements: readthedocs-pip-requirements.txt

View file

@ -1,6 +1,6 @@
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
of this software and associated documentation files (the "Software"), to deal

View file

@ -34,19 +34,21 @@ This Library is used in production at the [DXHeat.com DX Cluster](https://dxheat
Pyhamtools is compatible with Python >=3.6.
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.9
* Python 3.10
* Python 3.11
* Python 3.12
* [pypy3.7](https://pypy.org/) (will be deprecated in 2024)
* Python 3.13
* [pypy3.8](https://pypy.org/)
* [pypy3.9](https://pypy.org/)
* [pypy3.10](https://pypy.org/)
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.
### 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.
### 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
@ -62,9 +64,7 @@ Open Source Software licenses, including the MIT license at [choosealicense.com]
Starting with version 0.8.0, `libxml2-dev` and `libxslt-dev` are required dependencies.
## Installation
Install the dependencies (e.g. on Debian/Ubuntu):
There is a good change that the libraries are already installed on your system. If not, you can install them with the package manager of your distro. For example on Debian / Ubuntu based distros the corresponding command is:
```bash
@ -72,6 +72,12 @@ $ sudo apt-get install libxml2-dev libxslt-dev
```
You don't need to install these libraries manually on Windows / MacOS.
## Installation
The easiest way to install pyhamtools is through the packet manager `pip`:
```bash

View file

@ -1,6 +1,40 @@
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
================
17. March 2024
* switched from distutils to setuptools. No impact for endusers.
PyHamtools 0.9.0
================

View file

@ -25,6 +25,7 @@ from pyhamtools.version import __version__, __release__
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.napoleon',
'sphinx_rtd_dark_mode',
]
# 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
# 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
# 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)):
band = 0.23 #23cm
mode = None
elif ((freq >= 2390000) and (freq <= 2450000)):
elif ((freq >= 2300000) and (freq <= 2450000)):
band = 0.13 #13cm
mode = None
elif ((freq >= 3300000) and (freq <= 3500000)):
@ -200,7 +200,19 @@ def freq_to_band(freq):
elif ((freq >= 47000000) and (freq <= 47200000)):
band = 0.0063 #6,3mm
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:
raise KeyError
return {"band": band, "mode": mode}
return {"band": band, "mode": mode}

View file

@ -3,12 +3,13 @@ from datetime import datetime, timezone
import ephem
def latlong_to_locator (latitude, longitude):
def latlong_to_locator (latitude, longitude, precision=6):
"""converts WGS84 coordinates into the corresponding Maidenhead Locator
Args:
latitude (float): Latitude
longitude (float): Longitude
precision (int): 4,6,8,10 chars (default 6)
Returns:
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:
raise ValueError
if latitude >= 90 or latitude <= -90:
raise ValueError
longitude += 180;
latitude +=90;
longitude +=180
latitude +=90
locator = chr(ord('A') + int(longitude / 20))
locator += chr(ord('A') + int(latitude / 10))
locator += chr(ord('0') + int((longitude % 20) / 2))
locator += chr(ord('0') + int(latitude % 10))
locator += chr(ord('A') + int((longitude - int(longitude / 2) * 2) / (2 / 24)))
locator += chr(ord('A') + int((latitude - int(latitude / 1) * 1 ) / (1 / 24)))
# copied & adapted from github.com/space-physics/maidenhead
A = ord('A')
a = divmod(longitude, 20)
b = divmod(latitude, 10)
locator = chr(A + int(a[0])) + chr(A + int(b[0]))
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
def locator_to_latlong (locator):
def locator_to_latlong (locator, center=True):
"""converts Maidenhead locator in the corresponding WGS84 coordinates
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:
tuple (float, float): Latitude, Longitude
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
Example:
@ -79,7 +99,7 @@ def locator_to_latlong (locator):
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
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'):
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
latitude = (ord(locator[1]) - ord('A')) * 10 - 90
longitude += (ord(locator[2]) - ord('0')) * 2
latitude += (ord(locator[3]) - ord('0'))
latitude += (ord(locator[3]) - ord('0')) * 1
if len(locator) == 6:
longitude += ((ord(locator[4])) - ord('A')) * (2 / 24)
latitude += ((ord(locator[5])) - ord('A')) * (1 / 24)
if len(locator) == 4:
# move to center of subsquare
longitude += 1 / 24
latitude += 0.5 / 24
if center:
longitude += 2 / 2
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:
# move to center of square
longitude += 1;
latitude += 0.5;
raise ValueError
return latitude, longitude
@ -125,14 +186,14 @@ def calculate_distance(locator1, locator2):
"""calculates the (shortpath) distance between two Maidenhead locators
Args:
locator1 (string): Locator, either 4 or 6 characters
locator2 (string): Locator, either 4 or 6 characters
locator1 (string): Locator, either 4, 6 or 8 characters
locator2 (string): Locator, either 4, 6 or 8 characters
Returns:
float: Distance in km
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
Example:
@ -142,6 +203,9 @@ def calculate_distance(locator1, locator2):
>>> calculate_distance("JN48QM", "QF67bf")
16466.413
Note:
Distances is calculated between the centers of the (sub) squares
"""
R = 6371 #earh radius
@ -160,15 +224,15 @@ def calculate_distance(locator1, locator2):
c = 2 * atan2(sqrt(a), sqrt(1-a))
d = R * c #distance in km
return d;
return d
def calculate_distance_longpath(locator1, locator2):
"""calculates the (longpath) distance between two Maidenhead locators
Args:
locator1 (string): Locator, either 4 or 6 characters
locator2 (string): Locator, either 4 or 6 characters
locator1 (string): Locator, either 4, 6 or 8 characters
locator2 (string): Locator, either 4, 6 or 8 characters
Returns:
float: Distance in km
@ -184,6 +248,8 @@ def calculate_distance_longpath(locator1, locator2):
>>> calculate_distance_longpath("JN48QM", "QF67bf")
23541.5867
Note:
Distance is calculated between the centers of the (sub) squares
"""
c = 40008 #[km] earth circumference
@ -196,8 +262,8 @@ def calculate_heading(locator1, locator2):
"""calculates the heading from the first to the second locator
Args:
locator1 (string): Locator, either 4 or 6 characters
locator2 (string): Locator, either 4 or 6 characters
locator1 (string): Locator, either 4, 6 or 8 characters
locator2 (string): Locator, either 4, 6 or 6 characters
Returns:
float: Heading in deg
@ -213,6 +279,9 @@ def calculate_heading(locator1, locator2):
>>> calculate_heading("JN48QM", "QF67bf")
74.3136
Note:
Heading is calculated between the centers of the (sub) squares
"""
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)
Args:
locator1 (string): Locator, either 4 or 6 characters
locator2 (string): Locator, either 4 or 6 characters
locator1 (string): Locator, either 4, 6 or 8 characters
locator2 (string): Locator, either 4, 6 or 8 characters
Returns:
float: Long path heading in deg
@ -253,6 +322,9 @@ def calculate_heading_longpath(locator1, locator2):
>>> calculate_heading_longpath("JN48QM", "QF67bf")
254.3136
Note:
Distance is calculated between the centers of the (sub) squares
"""
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
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)
Returns:

View file

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

View file

@ -1,3 +1,5 @@
sphinx>=1.8.5
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==4.6.11; python_version=='3.6'
pytest>=7.0.0
pytest-blockage>=0.2.2
pytest-localserver>=0.5
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

@ -1,6 +1,6 @@
#!/usr/bin/env python
import os
from distutils.core import setup
from setuptools import setup
kw = {}
@ -18,7 +18,7 @@ setup(name='pyhamtools',
"requests>=2.21.0",
"ephem>=4.1.3",
"beautifulsoup4>=4.7.1",
"lxml>=4.8.0",
"lxml>=5.0.0",
"redis>=2.10.6",
],
**kw

View file

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

View file

@ -14,7 +14,12 @@ class Test_calculate_distance():
assert abs(calculate_distance("JN48QM", "FN44AB") - 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):
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.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.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):

View file

@ -1,10 +1,12 @@
import pytest
import maidenhead
from pyhamtools.locator import locator_to_latlong
from pyhamtools.consts import LookupConventions as const
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")
assert abs(latitude + 89.97916) < 0.00001
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(longitude - 179.9583) < 0.0001
def test_locator_to_latlong_normal_case(self):
latitude, longitude = locator_to_latlong("JN48QM")
assert abs(latitude - 48.52083) < 0.00001
assert abs(longitude - 9.3750000) < 0.0001
def test_locator_to_latlong_4chars_precision(self):
latitude, longitude = locator_to_latlong("JN48")
assert abs(latitude - 48.5) < 0.001
assert abs(longitude - 9.000) < 0.001
assert abs(latitude - 48.5) < 0.1
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")
assert abs(latitude - 48.52083) < 0.00001
assert abs(longitude - 9.3750000) < 0.0001
def test_locator_to_latlong_wrong_amount_of_characters(self):
with pytest.raises(ValueError):
@ -43,12 +101,30 @@ class Test_locator_to_latlong():
with pytest.raises(ValueError):
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):
with pytest.raises(ValueError):
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):
latitude, longitude = locator_to_latlong("****")

View file

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

View file

@ -29,6 +29,7 @@ class Test_lotw_methods:
execfile(os.path.join(fix_dir,"lotw_fixture.py"), namespace)
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):
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}
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(3300000) == {"band" : 0.09, "mode":None}