Compare commits

..

No commits in common. "master" and "v0.9.0" have entirely different histories.

20 changed files with 140 additions and 363 deletions

2
.github/CODEOWNERS vendored
View file

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

View file

@ -3,17 +3,61 @@ name: Linux
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
test_linux: test_linux:
runs-on: "ubuntu-24.04" # Ubuntu 20.04 is still required for python 3.6; this doesn't work on Ubuntu 22.04 anymore
name: "Ubuntu 24.04 - Python ${{ matrix.python-version }}" 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 }}"
env: env:
USING_COVERAGE: '3.11' USING_COVERAGE: '3.11'
strategy: strategy:
matrix: matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "pypy3.8", "pypy3.9", "pypy3.10"] python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.7", "pypy3.8", "pypy3.9", "pypy3.10"]
redis-version: [7] redis-version: [6]
steps: steps:
- uses: "actions/checkout@v3" - uses: "actions/checkout@v3"
@ -49,7 +93,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 % 60 ) + 1 ]s sleep $[ ( $RANDOM % 10 ) + 1 ]s
if [[ $PYTHON_VERSION == 3.11 ]] if [[ $PYTHON_VERSION == 3.11 ]]
then then
pytest --cov=test/ pytest --cov=test/
@ -72,12 +116,13 @@ jobs:
test_macos: test_macos:
runs-on: "macos-15" # Ubuntu 20.04 is still required for python 3.6; this doesn't work on Ubuntu 22.04 anymore
name: "MacOS 15 - Python ${{ matrix.python-version }}" runs-on: "macos-12"
name: "MacOS 12 - Python ${{ matrix.python-version }}"
strategy: strategy:
matrix: matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "pypy3.8", "pypy3.9", "pypy3.10"] python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.8", "pypy3.9", "pypy3.10"]
redis-version: [7.2] redis-version: [6]
steps: steps:
- uses: "actions/checkout@v3" - uses: "actions/checkout@v3"
@ -98,7 +143,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 uses: shogo82148/actions-setup-redis@v1.31.1
with: with:
redis-version: ${{ matrix.redis-version }} redis-version: ${{ matrix.redis-version }}
@ -111,16 +156,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 % 60 ) + 1 ] sleep $[ ( $RANDOM % 10 ) + 1 ]
pytest ./test pytest ./test
test_windows: test_windows:
runs-on: "windows-2022" runs-on: "windows-latest"
name: "Windows latest - Python ${{ matrix.python-version }}" name: "Windows latest - Python ${{ matrix.python-version }}"
strategy: strategy:
matrix: matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
steps: steps:
- uses: "actions/checkout@v3" - uses: "actions/checkout@v3"
@ -145,13 +190,10 @@ 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-msys2-with-Service.zip 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\pacman.exe -S --noconfirm unzip C:\msys64\usr\bin\tar.exe -xvzf Redis-7.0.14-Windows-x64-with-Service.tar.gz
C:\msys64\usr\bin\unzip.exe Redis-7.0.14-Windows-x64-msys2-with-Service.zip sc.exe create Redis binpath=D:\a\pyhamtools\pyhamtools\Redis-7.0.14-Windows-x64-with-Service\RedisService.exe start= auto
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:
@ -164,5 +206,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..60 | get-random) start-sleep -Seconds (5..20 | get-random)
pytest pytest

View file

@ -1,33 +0,0 @@
# 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) 2025 Tobias Wellnitz Copyright (c) 2014 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,22 +34,20 @@ 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
* Python 3.13 * [pypy3.7](https://pypy.org/) (will be deprecated in 2024)
* [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:
@ -64,7 +62,9 @@ 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.
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: ## Installation
Install the dependencies (e.g. on Debian/Ubuntu):
```bash ```bash
@ -72,12 +72,6 @@ $ 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,40 +1,6 @@
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,7 +25,6 @@ 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.
@ -96,9 +95,7 @@ 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 >= 2300000) and (freq <= 2450000)): elif ((freq >= 2390000) 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,18 +200,6 @@ 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,13 +3,12 @@ from datetime import datetime, timezone
import ephem import ephem
def latlong_to_locator (latitude, longitude, precision=6): def latlong_to_locator (latitude, longitude):
"""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
@ -33,54 +32,35 @@ def latlong_to_locator (latitude, longitude, precision=6):
""" """
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;
# copied & adapted from github.com/space-physics/maidenhead locator = chr(ord('A') + int(longitude / 20))
A = ord('A') locator += chr(ord('A') + int(latitude / 10))
a = divmod(longitude, 20) locator += chr(ord('0') + int((longitude % 20) / 2))
b = divmod(latitude, 10) locator += chr(ord('0') + int(latitude % 10))
locator = chr(A + int(a[0])) + chr(A + int(b[0])) locator += chr(ord('A') + int((longitude - int(longitude / 2) * 2) / (2 / 24)))
lon = a[1] / 2.0 locator += chr(ord('A') + int((latitude - int(latitude / 1) * 1 ) / (1 / 24)))
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, center=True): def locator_to_latlong (locator):
"""converts Maidenhead locator in the corresponding WGS84 coordinates """converts Maidenhead locator in the corresponding WGS84 coordinates
Args: Args:
locator (string): Locator, either 4, 6 or 8 characters locator (string): Locator, either 4 or 6 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 Maidenhead locator string ValueError: When called with wrong or invalid input arg
TypeError: When arg is not a string TypeError: When arg is not a string
Example: Example:
@ -99,7 +79,7 @@ def locator_to_latlong (locator, center=True):
locator = locator.upper() locator = locator.upper()
if len(locator) < 4 or len(locator) == 5 or len(locator) == 7 or len(locator) == 9: if len(locator) == 5 or len(locator) < 4:
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'):
@ -120,64 +100,23 @@ def locator_to_latlong (locator, center=True):
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')) * 1 latitude += (ord(locator[3]) - ord('0'))
if len(locator) == 4: if len(locator) == 6:
longitude += ((ord(locator[4])) - ord('A')) * (2 / 24)
latitude += ((ord(locator[5])) - ord('A')) * (1 / 24)
if center: # move to center of subsquare
longitude += 2 / 2 longitude += 1 / 24
latitude += 1.0 / 2 latitude += 0.5 / 24
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:
raise ValueError # move to center of square
longitude += 1;
latitude += 0.5;
return latitude, longitude return latitude, longitude
@ -186,14 +125,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, 6 or 8 characters locator1 (string): Locator, either 4 or 6 characters
locator2 (string): Locator, either 4, 6 or 8 characters locator2 (string): Locator, either 4 or 6 characters
Returns: Returns:
float: Distance in km float: Distance in km
Raises: Raises:
ValueError: When called with wrong or invalid maidenhead locator strings ValueError: When called with wrong or invalid input arg
AttributeError: When args are not a string AttributeError: When args are not a string
Example: Example:
@ -203,9 +142,6 @@ 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
@ -224,15 +160,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, 6 or 8 characters locator1 (string): Locator, either 4 or 6 characters
locator2 (string): Locator, either 4, 6 or 8 characters locator2 (string): Locator, either 4 or 6 characters
Returns: Returns:
float: Distance in km float: Distance in km
@ -248,8 +184,6 @@ 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
@ -262,8 +196,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, 6 or 8 characters locator1 (string): Locator, either 4 or 6 characters
locator2 (string): Locator, either 4, 6 or 6 characters locator2 (string): Locator, either 4 or 6 characters
Returns: Returns:
float: Heading in deg float: Heading in deg
@ -279,9 +213,6 @@ 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)
@ -305,8 +236,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, 6 or 8 characters locator1 (string): Locator, either 4 or 6 characters
locator2 (string): Locator, either 4, 6 or 8 characters locator2 (string): Locator, either 4 or 6 characters
Returns: Returns:
float: Long path heading in deg float: Long path heading in deg
@ -322,9 +253,6 @@ 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)
@ -337,7 +265,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, 6 or 8 characters locator1 (string): Maidenhead Locator, either 4 or 6 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, 12, 0) VERSION = (0, 9, 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,5 +1,3 @@
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,9 +1,5 @@
pytest>=7.0.0 pytest>=7.0.0; python_version>='3.7'
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 setuptools import setup from distutils.core 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>=5.0.0", "lxml>=4.8.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': 189, u'adif': 147,
u'continent': u'OC', u'continent': u'OC',
u'country': u'NORFOLK ISLAND', u'country': u'LORD HOWE ISLAND',
u'cqz': 32, u'cqz': 30,
u'latitude': -29.0, u'latitude': -31.6,
u'longitude': 168.0 u'longitude': 159.1
} }
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': 189, u'adif': 147,
u'continent': u'OC', u'continent': u'OC',
u'country': u'Norfolk Island', u'country': u'Lord Howe Island',
u'cqz': 32, u'cqz': 30,
u'ituz': 60, u'ituz': 60,
u'latitude': -29.03, u'latitude': -31.55,
u'longitude': 167.93 u'longitude': 159.08
} }
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.62, 'longitude': 105.71,
'cqz': 29 'cqz': 29
} }

View file

@ -14,12 +14,7 @@ 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,24 +8,10 @@ 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_4chars_precision(self): def test_latlong_to_locator_normal_case(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,12 +1,10 @@
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_min_max_cases(self): def test_locator_to_latlong_edge_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
@ -15,79 +13,23 @@ 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_4chars_precision(self): def test_locator_to_latlong_normal_case(self):
latitude, longitude = locator_to_latlong("JN48")
assert abs(latitude - 48.5) < 0.1
assert abs(longitude - 9.0) < 0.1
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") latitude, longitude = locator_to_latlong("JN48QM")
assert abs(latitude - 48.52083) < 0.00001 assert abs(latitude - 48.52083) < 0.00001
assert abs(longitude - 9.37500) < 0.00001 assert abs(longitude - 9.3750000) < 0.0001
def test_locator_to_latlong_8chars_precision(self): latitude, longitude = locator_to_latlong("JN48")
latitude, longitude = locator_to_latlong("JN48QM84") assert abs(latitude - 48.5) < 0.001
assert abs(latitude - 48.51875) < 0.00001 assert abs(longitude - 9.000) < 0.001
assert abs(longitude - 9.40416) < 0.00001
latitude, longitude = locator_to_latlong("EM69SF53") def test_locator_to_latlong_mixed_signs(self):
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):
@ -102,29 +44,11 @@ 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.52, 'latitude': 9.50,
'longitude': 138.21, 'longitude': 138.20,
'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.52, 'latitude': 9.50,
'longitude': 138.21, 'longitude': 138.20,
'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.62, 'longitude': 105.71,
'cqz': 29, 'cqz': 29,
} }

View file

@ -29,7 +29,6 @@ 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,7 +65,6 @@ 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}