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] 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

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) 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:
@ -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. Starting with version 0.8.0, `libxml2-dev` and `libxslt-dev` are required dependencies.
## Installation 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:
Install the dependencies (e.g. on Debian/Ubuntu):
```bash ```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`: The easiest way to install pyhamtools is through the packet manager `pip`:
```bash ```bash

View file

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

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, 0) 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

@ -1,6 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
import os import os
from distutils.core import setup from setuptools import setup
kw = {} kw = {}
@ -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", "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}