Compare commits

...

82 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
Tobias Wellnitz, DH1TW 6a152760c5 added support for python 3.12 2023-12-28 21:02:41 +01:00
Tobias Wellnitz, DH1TW e549f416e8 updated clublog url to cdn.clublog.org in multiple places 2022-12-31 02:50:24 +01:00
Tobias Wellnitz, DH1TW 1dcc724e7b conftest: Accidentally removed os import 2022-12-31 02:49:49 +01:00
Tobias Wellnitz, DH1TW c5893dcc7a fixed two country IDs in Countryfilemapping
- Brazl and Domenican Republic
2022-12-31 02:31:39 +01:00
Tobias Wellnitz, DH1TW 098307308d create fixture for countryfileMapping as dict 2022-12-31 02:30:51 +01:00
Tobias Wellnitz, DH1TW 0cfea2ec89 Callinfo: added unit test for 1-by-3 callsigns (e.g. R2UHD) 2022-12-31 02:28:14 +01:00
Tobias Wellnitz, DH1TW c49ac97dd1 updated cty.plist test fixture 2022-12-31 02:26:59 +01:00
Tobias Wellnitz, DH1TW b49c549d48 callinfo: fixed regession for 2-by-1 calls 2022-12-27 00:01:06 +01:00
Tobias Wellnitz, DH1TW 01284a5ae8 callinfo: recognize callsigns with digits in the suffix 2022-12-26 21:13:13 +01:00
Tobias Wellnitz, DH1TW 19f00eac5b bumped version to 0.8.4 2022-12-18 14:47:07 +01:00
Tobias Wellnitz, DH1TW 8d1bb624de CI: remove python 3.6 on Windows due to lxml issues 2022-12-18 14:42:06 +01:00
Tobias Wellnitz, DH1TW 7b40e6c7ed callinfo: handle specific unicode strings 2022-12-18 14:40:20 +01:00
Tobias Wellnitz, DH1TW 1b0073fb0c marking strings with cyrillic characters as unicode 2022-12-18 14:04:13 +01:00
Tobias Wellnitz, DH1TW 077b645efe rais KeyError when callsign contains non latin characters
for example, cyrillic letter
2022-12-18 14:01:18 +01:00
Tobias Wellnitz, DH1TW bfbd0776bb CI: fixed test runner on Ubuntu 20.04 2022-12-06 01:31:30 +01:00
Tobias Wellnitz, DH1TW 4e8c90da1d fixed XML parsing error in QRZ.com session key renewal 2022-12-06 01:15:21 +01:00
Tobias Wellnitz, DH1TW b8bd89b2f2 timezone field of qrz.com must be str and not int 2022-12-05 02:25:03 +01:00
Tobias Wellnitz, DH1TW bf785f45f5 updated to v.0.8.1 2022-12-05 01:53:20 +01:00
Tobias Wellnitz, DH1TW adbd605fbd removed debug print statement 2022-12-05 01:51:46 +01:00
Tobias Wellnitz, DH1TW ad6ba5b418 readme: corrected link to df7cb profile 2022-12-05 01:12:52 +01:00
Tobias Wellnitz, DH1TW b1ef2a6d93 merging v0.8.0 branch
- finally using libxml2 parser in beautifulsoup/lxml
- minor bug in parsing the CCC field (qrz.com)
- fixed VK9XX fixture (lat/long)
- added support for python 3.10 and 3.11, pypy3.7-3.9
- removed support for python 3.4
- fixed escapings in regular expressions
- replaced exefile from past.buildins with custom function
2022-12-05 01:01:45 +01:00
Tobias Wellnitz, DH1TW ebdf0c9707 added python 3.10 and 3.11 to CI 2022-12-03 23:25:41 +01:00
Tobias Wellnitz, DH1TW 094db34159 Merge branch 'master' of github.com:dh1tw/pyhamtools 2022-12-03 23:18:22 +01:00
Tobias Wellnitz, DH1TW 755c74917c bumped version to 0.7.11 2022-12-03 23:16:16 +01:00
Tobias Wellnitz, DH1TW 696bfaca31 changed to XML parser for QRZ.com 2022-12-03 23:15:45 +01:00
Tobias Wellnitz, DH1TW 2dde0d8ca8 removed unneeded import 2022-12-03 23:14:30 +01:00
Tobias Wellnitz 13efbc11d6
Fix spelling errors PR #23 from dforsi/fix/typos
Fix spelling errors
2022-06-07 16:08:50 +02:00
Daniele Forsi 36caec81e7 Fix spelling errors
Fixde with:
codespell --skip test --write-changes --interactive=2
2022-06-03 23:45:50 +02:00
Tobias Wellnitz, DH1TW 44319becff Added Python 3.10 to CI Matrix 2022-05-12 01:45:40 +02:00
Tobias Wellnitz, DH1TW 82e2b93cd5 using lxml to parse qrz.com xml responses instead of html.parser 2022-05-12 01:43:23 +02:00
Tobias Wellnitz, DH1TW cb9b23c96f bumped ephem to >=4.1.3 to fix sunset / sunrise bug 2021-12-16 00:09:06 +01:00
Tobias Wellnitz, DH1TW 6cba84edf4 updated .gitignore with pyenv variables 2021-12-16 00:07:45 +01:00
Tobias Wellnitz, DH1TW 21ef09b3e4 updated urls of some links in readme.md 2021-12-08 23:57:40 +01:00
Tobias Wellnitz, DH1TW d5f3f1be84 pyhamtools version v0.7.8 2021-12-05 13:12:20 +01:00
Tobias Wellnitz, DH1TW c073ddf048 removed tests with invalid Clublog API key to avoid CDN IP blockage 2021-12-05 13:02:14 +01:00
Tobias Wellnitz, DH1TW 2790e07403 updated URL to Clublogs prefix & exeptions XML 2021-12-05 13:01:14 +01:00
Tobias Wellnitz, DH1TW 526a4f8fe9 added python3.9 support 2021-06-01 03:27:32 +02:00
Tobias Wellnitz, DH1TW fe478fae36 bumped travis and appveyor CI to python 3.9 2021-05-31 13:46:49 +02:00
Tobias Wellnitz, DH1TW b2a76a8695 bumped release to 0.7.6 2020-09-29 00:44:54 +02:00
Tobias Wellnitz, DH1TW 87d35c7d02 updated latitude of VK9XX test fixture 2020-09-29 00:13:55 +02:00
Tobias Wellnitz 629ac1c878
Merge pull request #18 from devnulling/master
Docs: fix calculate_distance example
2020-09-28 23:52:23 +02:00
Tobias Wellnitz, DH1TW baf1d5ab27 updated cty.plist test fixture to latest version
from http://www.country-files.com/cty
2020-09-28 23:47:46 +02:00
Tobias Wellnitz ed9b0be087
Merge pull request #19 from threeio/patch-1
Updating eSwatini to  Eswatini
2020-09-28 23:40:05 +02:00
Tobias Wellnitz 5fa8733fee
Merge pull request #20 from kholia/fix-redis-lookup-example
Docs: Fix redis related example in docstring
2020-09-28 23:37:15 +02:00
kstrishock-omnivore fa24721b54 Update test as well 2020-09-13 20:23:22 -07:00
Dhiru Kholia 905262cdcb Docs: Fix redis related example in docstring
This docstring demonstrates how to use Redis to query the data. However,
Redis is NOT actually getting used due to `lookuptype="countryfile"`
argument instead of the required `lookuptype="redis" argument.
2020-09-12 11:27:15 +05:30
threeio 5034fba267
Updating eSwatini to Eswatini
Based on current country-files.com data its now listed as Kingdom of Eswantini

This matches to what Wikipedia is saying as well https://en.wikipedia.org/wiki/Eswatini
2020-09-10 10:32:53 -07:00
devnulling 02ecb27648
Docs: fix calculate_distance example 2020-08-28 23:50:19 -07:00
Tobias Wellnitz, DH1TW 0fa24e9741 bumped version to 0.7.5 and added changelog 2020-03-03 22:25:10 +01:00
Tobias Wellnitz, DH1TW f940473245 travis-ci: allow tests with the python nightly build to fail 2020-03-03 21:54:03 +01:00
Tobias Wellnitz, DH1TW b3d919f99a store lookup data faster in redis by using pipelines 2020-03-03 21:45:03 +01:00
Tobias Wellnitz, DH1TW 1891c88e38 delete download artifact after import 2020-03-03 17:22:23 +01:00
Tobias Wellnitz, DH1TW 01869462df Bugfix for badly escaped json data after reading from redis (#16)
Apostrophes and quotation marks where escaped after retrieving the JSON formatted
data from redis.
2020-03-03 02:21:40 +01:00
Tobias Wellnitz, DH1TW bcb03b13b5 removed support for Python 3.4 on Windows
The first dependencies and sub-dependencies (e.g. Colorama) stopped supporting
 python 3.4. Hence support was removed in pyhamtools as well.
2020-03-03 02:02:27 +01:00
Tobias Wellnitz, DH1TW f51976a9f5 callinfo get_all() convert callsign to upper case 2020-03-03 01:22:46 +01:00
Tobias Wellnitz, DH1TW 5db094e1a2 changed zone exception test from dp0gvn to ci8aw 2020-03-03 01:03:50 +01:00
Tobias Wellnitz 7862663d70
Merge pull request #15 from df7cb/parameter_defaults
Don't use timestamps as parameter defaults
2019-12-16 01:52:59 +01:00
Christoph Berg 6fdb61e531 Don't use timestamps as parameter defaults
Constructs like

    def calculate_sunrise_sunset(locator, calc_date=datetime.utcnow()):

are actually wrong because they initialize the parameter default at
module load time, not at function call time. If the program is running
over some time, the result will be wrong.

As a side-effect, this fix makes the docs (and the whole project) build
reproducibly because previously the build time was embedded in the
sphinx docs:

    lookup_prefix(prefix, timestamp=datetime.datetime(2019, 11, 27, 3, 4, 36, 93157, tzinfo=<UTC>))
2019-12-15 22:21:46 +01:00
Tobias Wellnitz, DH1TW c8fb844e2d renamed Swaziland into Kingdom of eSwatini 2019-11-27 03:10:08 +01:00
Tobias Wellnitz, DH1TW 21f624c4c7 included python 3.7 and 3.8 in the CI build matrices 2019-11-14 17:45:09 +01:00
Tobias Wellnitz, DH1TW 202bd163cb migrated from travis-ci.org to travis-ci.com 2019-11-14 16:40:04 +01:00
45 changed files with 95159 additions and 71990 deletions

View file

@ -1,34 +0,0 @@
build: false
environment:
matrix:
- PYTHON: "C:\\Python27"
PYTHON_VERSION: "2.7.8"
PYTHON_ARCH: "32"
- PYTHON: "C:\\Python34"
PYTHON_VERSION: "3.4.1"
PYTHON_ARCH: "32"
- PYTHON: "C:\\Python35"
PYTHON_VERSION: "3.5.4"
PYTHON_ARCH: "32"
- PYTHON: "C:\\Python36"
PYTHON_VERSION: "3.6.4"
PYTHON_ARCH: "32"
init:
- "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%"
install:
- nuget install redis-64 -excludeversion
- redis-64\tools\redis-server.exe --service-install
- redis-64\tools\redis-server.exe --service-start
- "%PYTHON%/Scripts/pip.exe install -e ."
- "%PYTHON%/Scripts/pip.exe install -r requirements-docs.txt"
- "%PYTHON%/Scripts/pip.exe install -r requirements-pytest.txt"
test_script:
- "%PYTHON%/Scripts/pytest"

2
.github/CODEOWNERS vendored Normal file
View file

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

168
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,168 @@
name: Linux
on: [push, pull_request]
jobs:
test_linux:
runs-on: "ubuntu-24.04"
name: "Ubuntu 24.04 - Python ${{ matrix.python-version }}"
env:
USING_COVERAGE: '3.11'
strategy:
matrix:
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"
- 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 wheel codecov
python -m pip install -e .
python -m pip install -r requirements-pytest.txt
python -m pip install -r requirements-docs.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 % 60 ) + 1 ]s
if [[ $PYTHON_VERSION == 3.11 ]]
then
pytest --cov=test/
codecov
else
pytest test/
fi
cd docs && make html
# publish_package:
# runs-on: "ubuntu-latest"
# needs: ["test_linux"]
# if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
# steps:
# - name: Publish package
# uses: pypa/gh-action-pypi-publish@release/v1
# with:
# user: __token__
# password: ${{ secrets.PYPI_API_TOKEN }}
test_macos:
runs-on: "macos-15"
name: "MacOS 15 - Python ${{ matrix.python-version }}"
strategy:
matrix:
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"
- 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
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: shogo82148/actions-setup-redis@v1
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 % 60 ) + 1 ]
pytest ./test
test_windows:
runs-on: "windows-2022"
name: "Windows latest - Python ${{ matrix.python-version }}"
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
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: |
python -VV
python -m pip install --upgrade pip setuptools wheel codecov
python -m pip install -e .
python -m pip install -r requirements-pytest.txt
python -m pip install -r requirements-docs.txt
- name: Setup redis
# There are no github-actions supporting redis on windows.
# Github Actions Container services are also not available for windows.
# We have to download and install a non-official redis windows port
# 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-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:
CLUBLOG_APIKEY: ${{ secrets.CLUBLOG_APIKEY }}
QRZ_USERNAME: ${{ secrets.QRZ_USERNAME }}
QRZ_PWD: ${{ secrets.QRZ_PWD }}
PYTHON_VERSION: ${{ matrix.python-version }}
# give redis service time to startup and
# delay the execution randomly by 5-20sec to reduce the
# amount of concurrent API calls on Clublog and QRZ.com
# when all CI jobs execute simultaneously
run: |
start-sleep -Seconds (5..60 | get-random)
pytest

10
.gitignore vendored
View file

@ -1,7 +1,15 @@
docs/build docs/build
build/
dist/
settings.json settings.json
apikeysrc apikeysrc
coverage* coverage*
.coverage .coverage
.python-version
.cache/* .cache/*
MANIFEST MANIFEST
.DS_Store
__pycache__
*.pyc
*.tar.gz
*.egg-info

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,30 +0,0 @@
language: python
dist: xenial
python:
- "2.7"
- "3.4"
- "3.5"
- "3.5-dev" # 3.5 development branch
- "3.6"
- "3.6-dev" # 3.6 development branch
- "3.7"
- "3.7-dev" # 3.7 development branch
- "3.8-dev" # 3.7 development branch
- "nightly"
- "pypy"
services:
- redis-server
# install dependencies
install:
- pip install -e .
- pip install -r requirements-docs.txt
- pip install -r requirements-pytest.txt
- pip install codecov
# run tests
script:
- pytest --cov=./
- if [[ $TRAVIS_PYTHON_VERSION == 3.6 ]]; then codecov; fi
- cd docs
# build the docs on 2.7 and 3.6 (sphinx requires 2.7 or >=3.4)
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then make html; fi
- if [[ $TRAVIS_PYTHON_VERSION == 3.6 ]]; then make html; fi

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

@ -1,29 +1,28 @@
# pyhamtools # pyhamtools
[![Build Status](https://travis-ci.org/dh1tw/pyhamtools.svg?branch=master)](https://travis-ci.org/dh1tw/pyhamtools) ![Build Status](https://github.com/dh1tw/pyhamtools/actions/workflows/test.yml/badge.svg)
[![Build status](https://ci.appveyor.com/api/projects/status/8rfgr7x6w1arixrh?svg=true)](https://ci.appveyor.com/project/dh1tw/pyhamtools)
[![codecov](https://codecov.io/gh/dh1tw/pyhamtools/branch/master/graph/badge.svg)](https://codecov.io/gh/dh1tw/pyhamtools) [![codecov](https://codecov.io/gh/dh1tw/pyhamtools/branch/master/graph/badge.svg)](https://codecov.io/gh/dh1tw/pyhamtools)
[![PyPI version](https://badge.fury.io/py/pyhamtools.svg)](https://badge.fury.io/py/pyhamtools) [![PyPI version](https://badge.fury.io/py/pyhamtools.svg)](https://badge.fury.io/py/pyhamtools)
Pyhamtools is a set of functions and classes for Amateur Radio purpose. Pyhamtools is a set of functions and classes for Amateur Radio purposes.
Currently the core part is the Callsign Lookup which decodes any amateur radio Currently, the core part is the Callsign Lookup which decodes any amateur radio
callsign string and provides the corresponding information (Country, DXCC callsign string and provides the corresponding information (Country, DXCC
entity, CQ Zone...etc). This basic functionality is needed for Logbooks, entity, CQ Zone...etc). This basic functionality is needed for Logbooks,
DX-Clusters or Log Checking. This and additional convenience features are DX-Clusters or Log Checking. This and additional convenience features are
provided for the following sources: provided for the following sources:
Currently, Currently,
* [AD1C's Country-Files.org](http://country-files.org) * [AD1C's Amateur Radio Country Files](https://www.country-files.com)
* [Clublog Prefixes & Exceptions XML File](https://clublog.freshdesk.com/support/articles/54902-downloading-the-prefixes-and-exceptions-as) * [Clublog Prefixes & Exceptions XML File](https://clublog.freshdesk.com/support/articles/54902-downloading-the-prefixes-and-exceptions-as)
* [Clublog DXCC Query API](http://clublog.freshdesk.com/support/articles/54904-how-to-query-club-log-for-dxcc) * [Clublog DXCC Query API](http://clublog.freshdesk.com/support/articles/54904-how-to-query-club-log-for-dxcc)
* [QRZ.com XML API](http://www.qrz.com/XML/current_spec.html) * [QRZ.com XML API](http://www.qrz.com/XML/current_spec.html)
* [Redis.io](http://redis.io) * [Redis.io](http://redis.io)
* [ARRL Logbook of the World (LOTW)](http://https://lotw.arrl.org) * [ARRL Logbook of the World (LOTW)](https://lotw.arrl.org)
* [eQSL.cc user list](https://www.eqsl.cc) * [eQSL.cc user list](https://www.eqsl.cc)
* [Clublog & OQRS user list](http://clublog.freshdesk.com/support/solutions/articles/3000064883-list-of-club-log-and-lotw-users) * [Clublog & OQRS user list](http://clublog.freshdesk.com/support/solutions/articles/3000064883-list-of-club-log-and-lotw-users)
Other modules include location based calculations (e.g. distance, Other modules include location-based calculations (e.g. distance,
heading between Maidenhead locators) or frequency based calculations heading between Maidenhead locators) or frequency-based calculations
(e.g. frequency to band). (e.g. frequency to band).
## References ## References
@ -32,15 +31,24 @@ This Library is used in production at the [DXHeat.com DX Cluster](https://dxheat
## Compatibility ## Compatibility
Pyhamtools is since version 0.6.0 compatible with > Python 2.7 and > python 3.3. Pyhamtools is compatible with Python >=3.6.
We check compatibility on OSX, Windows and Linux with the following Python We check compatibility on OSX, Windows, and Linux with the following Python versions:
versions:
* Python 2.7 * Python 3.8
* Python 3.4 * Python 3.9
* Python 3.5 * Python 3.10
* Python 3.6 * Python 3.11
* [pypy](https://pypy.org/) (Python 2) * Python 3.12
* Python 3.13
* [pypy3.8](https://pypy.org/)
* [pypy3.9](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.
### 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
@ -52,9 +60,25 @@ Check out the full documentation including the changelog at:
Pyhamtools is published under the permissive [MIT License](http://choosealicense.com/licenses/mit/). You can find a good comparison of Pyhamtools is published under the permissive [MIT License](http://choosealicense.com/licenses/mit/). You can find a good comparison of
Open Source Software licenses, including the MIT license at [choosealicense.com](http://choosealicense.com/licenses/) Open Source Software licenses, including the MIT license at [choosealicense.com](http://choosealicense.com/licenses/)
## 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:
```bash
$ sudo apt-get install libxml2-dev libxslt-dev
```
You don't need to install these libraries manually on Windows / MacOS.
## Installation ## Installation
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
@ -62,6 +86,14 @@ $ pip install pyhamtools
``` ```
Christoph, [@df7cb](https://github.com/df7cb) is kindly maintaining a Debian package as an alternative way to install pyhamtools:
```bash
$ sudo apt-get install pyhamtools
```
## Example: How to use pyhamtools ## Example: How to use pyhamtools
``` python ``` python
@ -90,11 +122,11 @@ $ pip install pyhamtools
## Testing ## Testing
An extensive set of unit tests has been created for all Classes & Methods. An extensive set of unit tests has been created for all Classes & Methods.
In order to be able to perform all tests you need a QRZ.com account and a To be able to perform all tests, you need a QRZ.com account and a
[Clublog API key](http://clublog.freshdesk.com/support/solutions/articles/54910-api-keys). [Clublog API key](http://clublog.freshdesk.com/support/solutions/articles/54910-api-keys).
pyhamtools rely on the [pytest](https://docs.pytest.org/en/latest/) testing pyhamtools rely on the [pytest](https://docs.pytest.org/en/latest/) testing
framework. In order to install it with all the needed dependencies run: framework. To install it with all the needed dependencies run:
```bash ```bash
@ -102,7 +134,7 @@ $ pip install -r requirements-pytest.txt
``` ```
The QRZ.com credentials and the Clublog API key have to be set in environment The QRZ.com credentials and the Clublog API key have to be set in the environment
variables: variables:
```bash ```bash
@ -113,8 +145,8 @@ $ export QRZ_PWD="<your qrz.com password>"
``` ```
In order to perform the tests related to the [redis](https://redis.io/) key/value To perform the tests related to the [redis](https://redis.io/) key/value
store, a redis server has to be up & running. store, a Redis server has to be up & running.
```bash ```bash

View file

@ -1,6 +1,185 @@
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
================
28. December 2023
* Deprecated support for Python 2.7 and Python 3.5
* Added Support for Python 3.12
* Replaced pytz with datetime.timezone
* Added Continous Integration Jobs for MacOS (now supported by Github Actions)
PyHamtools 0.8.7
================
31. December 2022
* Lookuplib/Countryfiles: corrected Brazil to ADIF country id 108
* Lookuplib/Countryfiles: corrected Domenican Republic to ADIF country if 72
* Changed the remaining Clublog URLs to https://cdn.clublog.org
PyHamtools 0.8.6
================
26. December 2022
* fixed regex regression for detection two-by-one callsigns
PyHamtools 0.8.5
================
26. December 2022
* refined regex for decoding callsigns. In particular to better recognize callsigns with one or more digits in the suffix (e.g. TI5N5BEK, DP44N44T)
PyHamtools 0.8.4
================
18. December 2022
* raise KeyError when callsigns contain non-latin characters (e.g. cyrillic letters)
PyHamtools 0.8.3
================
06. December 2022
* fixed XML parsing error in QRZ.com session key renewal
PyHamtools 0.8.2
================
05. December 2022
* timezone field from QRZ.com casted to str instead of int
PyHamtools 0.8.1
================
05. December 2022
* removed debug print statement from QRZ.com queries
PyHamtools 0.8.0
================
05. December 2022
* Finally switched to XML parser in BeautifulSoup for qrz.com (requires libxml2-dev and libxslt-dev packages!)
* Fixed minor bug in parsing the CCC field of qrz.com XML messages
* Fixed VK9XX test fixture (Latitude & Longitude)
* Added support for CPython 3.10 and 3.11
* Added support for PyPy 3.7, 3.8, 3.9
* Dropped support for Python 3.4
* Fixed regular expression escapings which were marked as deprecated (since Python 3.6)
* Replaced legacy execfile function in test package to remove the deprecation warning about 'imp'
PyHamtools 0.7.10
================
12. May 2022
* Using lxml to parse XML messages returned from qrz.com
* Upgraded dependencies
PyHamtools 0.7.9
================
16. December 2021
* Calculating sunrise and sunset close to the artic region raised a ValueError due
to a bug in the underlying 3rd party library ephem. This release upgrades the
dependency to ephem > 4.1.3 which has the bug already fixed.
PyHamTools 0.7.8
================
04. December 2021
* Updated Clublog's (CDN based) URL for downloading the Prefixes and Exceptions XML
PyHamTools 0.7.7
================
01. June 2021
* Added support for Python 3.9
* Added deprecation warnings for Python 3.4 and 3.5
PyHamTools 0.7.6
================
29. September 2020
* Renamed "Kingdom of eSwatini" into "Kingdom of Eswatini" (#19 tnx @therrio)
* fixed the latitude in the VK9XX unit test fixture
* fixed docs - redis related example in docstring (#20 tnx @kholia)
* fixed docs - calculate distance example (#18 tnx @devnulling)
PyHamTools 0.7.5
================
3. March 2020
* fixed a bug related to badly escaped JSON data when using redis
* lookup data is now copied approx. 5x faster into redis
* download artifacts are now cleaned up from the temporary download directory
PyHamTools 0.7.4
================
27. November 2019
* Renamed "Swaziland" into "Kingdom of eSwatini"
PyHamTools 0.7.3 PyHamTools 0.7.3
================ ================
@ -126,7 +305,7 @@ PyHamTools 0.5.0
* corrected Longitude to General Standard (-180...0° West, 0...180° East) [LookupLib] * corrected Longitude to General Standard (-180...0° West, 0...180° East) [LookupLib]
* improved callsign decoding alogrithm [CallInfo] * improved callsign decoding algorithm [CallInfo]
* added special case to decode location of VK9 callsigns [CallInfo] * added special case to decode location of VK9 callsigns [CallInfo]

View file

@ -12,19 +12,8 @@
# All configuration values have a default; values that are commented out # All configuration values have a default; values that are commented out
# serve to show the default. # serve to show the default.
import sys
import os
from pyhamtools.version import __version__, __release__ from pyhamtools.version import __version__, __release__
sys.path.insert(0,"/Users/user/projects/pyhamtools/pyhamtools")
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------ # -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here. # If your documentation needs a minimal Sphinx version, state it here.
@ -35,7 +24,8 @@ sys.path.insert(0,"/Users/user/projects/pyhamtools/pyhamtools")
# ones. # ones.
extensions = [ extensions = [
'sphinx.ext.autodoc', 'sphinx.ext.autodoc',
'sphinxcontrib.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.
@ -52,7 +42,7 @@ master_doc = 'index'
# General information about the project. # General information about the project.
project = u'pyhamtools' project = u'pyhamtools'
copyright = u'2019, Tobias Wellnitz, DH1TW' copyright = u'2024, Tobias Wellnitz, DH1TW'
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
@ -106,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

@ -25,7 +25,7 @@ Calculate Distance between two WGS84 Coordinates
>>> from pyhamtools.locator import calculate_distance, latlong_to_locator >>> from pyhamtools.locator import calculate_distance, latlong_to_locator
>>> locator1 = latlong_to_locator(48.52, 9.375) >>> locator1 = latlong_to_locator(48.52, 9.375)
>>> locator2 = latlong_to_locator(-32.77, 152.125) >>> locator2 = latlong_to_locator(-32.77, 152.125)
>>> distance = calculate_heading(locator1, locator2) >>> distance = calculate_distance(locator1, locator2)
>>> print("%.1fkm" % distance) >>> print("%.1fkm" % distance)
16466.4km 16466.4km
@ -49,7 +49,7 @@ Decode a Callsign and get Country name, ADIF ID, Latitude & Longitude
In this example we will use AD1C's Country-files.com database to perform the lookup. In this example we will use AD1C's Country-files.com database to perform the lookup.
First we need to instanciate a LookupLib object for Country-files.com database. The latest database will be downloaded automatically. First we need to instantiate a LookupLib object for Country-files.com database. The latest database will be downloaded automatically.
.. code-block:: none .. code-block:: none
@ -57,7 +57,7 @@ First we need to instanciate a LookupLib object for Country-files.com database.
>>> my_lookuplib = LookupLib(lookuptype="countryfile") >>> my_lookuplib = LookupLib(lookuptype="countryfile")
Next, a Callinfo object needs to be instanciated. The lookuplib object will be injected on construction. Next, a Callinfo object needs to be instantiated. The lookuplib object will be injected on construction.
.. code-block:: none .. code-block:: none

View file

@ -25,7 +25,7 @@ with some modules and classes which are frequently needed:
PyHamTools is used in production at the DXHeat.com DXCluster_, performing several thousand lookups and PyHamTools is used in production at the DXHeat.com DXCluster_, performing several thousand lookups and
calculations per day. calculations per day.
.. _Clublog.org: https://secure.clublog.org/ .. _Clublog.org: https://clublog.org/
.. _Country-Files.com: http://www.country-files.com/ .. _Country-Files.com: http://www.country-files.com/
.. _QRZ.com: http://qrz.com .. _QRZ.com: http://qrz.com
.. _eQSL: http://eqsl.cc .. _eQSL: http://eqsl.cc

View file

@ -1,23 +1,11 @@
import re import re
import logging import logging
from datetime import datetime from datetime import datetime, timezone
import sys
import pytz
from pyhamtools.consts import LookupConventions as const from pyhamtools.consts import LookupConventions as const
from pyhamtools.callsign_exceptions import callsign_exceptions from pyhamtools.callsign_exceptions import callsign_exceptions
UTC = pytz.UTC
timestamp_now = datetime.utcnow().replace(tzinfo=UTC)
if sys.version_info < (2, 7, ):
class NullHandler(logging.Handler):
def emit(self, record):
pass
class Callinfo(object): class Callinfo(object):
""" """
The purpose of this class is to return data (country, latitude, longitude, CQ Zone...etc) for an The purpose of this class is to return data (country, latitude, longitude, CQ Zone...etc) for an
@ -38,10 +26,7 @@ class Callinfo(object):
self._logger = logger self._logger = logger
else: else:
self._logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
if sys.version_info[:2] == (2, 6): self._logger.addHandler(logging.NullHandler())
self._logger.addHandler(NullHandler())
else:
self._logger.addHandler(logging.NullHandler())
self._lookuplib = lookuplib self._lookuplib = lookuplib
self._callsign_info = None self._callsign_info = None
@ -71,19 +56,21 @@ class Callinfo(object):
""" """
callsign = callsign.upper() callsign = callsign.upper()
homecall = re.search('[\d]{0,1}[A-Z]{1,2}\d([A-Z]{1,4}|\d{3,3}|\d{1,3}[A-Z])[A-Z]{0,5}', callsign) homecall = re.search('[\\d]{0,1}[A-Z]{1,2}\\d([A-Z]{1,4}|\\d{3,3}|\\d{1,3}[A-Z])[A-Z]{0,5}', callsign)
if homecall: if homecall:
homecall = homecall.group(0) homecall = homecall.group(0)
return homecall return homecall
else: else:
raise ValueError raise ValueError
def _iterate_prefix(self, callsign, timestamp=timestamp_now): def _iterate_prefix(self, callsign, timestamp=None):
"""truncate call until it corresponds to a Prefix in the database""" """truncate call until it corresponds to a Prefix in the database"""
prefix = callsign prefix = callsign
if timestamp is None:
timestamp = datetime.now(timezone.utc)
if re.search('(VK|AX|VI)9[A-Z]{3}', callsign): #special rule for VK9 calls if re.search('(VK|AX|VI)9[A-Z]{3}', callsign): #special rule for VK9 calls
if timestamp > datetime(2006,1,1, tzinfo=UTC): if timestamp > datetime(2006,1,1, tzinfo=timezone.utc):
prefix = callsign[0:3]+callsign[4:5] prefix = callsign[0:3]+callsign[4:5]
while len(prefix) > 0: while len(prefix) > 0:
@ -109,12 +96,12 @@ class Callinfo(object):
check = callsign[-4:].upper() check = callsign[-4:].upper()
return "/B" in check or "/BCN" in check return "/B" in check or "/BCN" in check
def _dismantle_callsign(self, callsign, timestamp=timestamp_now): def _dismantle_callsign(self, callsign, timestamp=None):
""" try to identify the callsign's identity by analyzing it in the following order: """ try to identify the callsign's identity by analyzing it in the following order:
Args: Args:
callsign (str): Amateur Radio callsign callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC) timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc)
Raises: Raises:
KeyError: Callsign could not be identified KeyError: Callsign could not be identified
@ -122,11 +109,13 @@ class Callinfo(object):
""" """
entire_callsign = callsign.upper() entire_callsign = callsign.upper()
if timestamp is None:
timestamp = datetime.now(timezone.utc)
if re.search('[/A-Z0-9\-]{3,15}', entire_callsign): # make sure the call has at least 3 characters if re.search('[/A-Z0-9\\-]{3,15}', entire_callsign): # make sure the call has at least 3 characters
if re.search('\-\d{1,3}$', entire_callsign): # cut off any -10 / -02 appendixes if re.search('\\-\\d{1,3}$', entire_callsign): # cut off any -10 / -02 appendixes
callsign = re.sub('\-\d{1,3}$', '', entire_callsign) callsign = re.sub('\\-\\d{1,3}$', '', entire_callsign)
if re.search('/[A-Z0-9]{1,4}/[A-Z0-9]{1,4}$', callsign): if re.search('/[A-Z0-9]{1,4}/[A-Z0-9]{1,4}$', callsign):
callsign = re.sub('/[A-Z0-9]{1,4}$', '', callsign) # cut off 2. appendix DH1TW/HC2/P -> DH1TW/HC2 callsign = re.sub('/[A-Z0-9]{1,4}$', '', callsign) # cut off 2. appendix DH1TW/HC2/P -> DH1TW/HC2
@ -189,20 +178,21 @@ class Callinfo(object):
data[const.BEACON] = True data[const.BEACON] = True
return data return data
elif re.search('\d$', appendix): elif re.search('\\d$', appendix):
area_nr = re.search('\d$', appendix).group(0) area_nr = re.search('\\d$', appendix).group(0)
callsign = re.sub('/\d$', '', callsign) #remove /number callsign = re.sub('/\\d$', '', callsign) #remove /number
if len(re.findall(r'\d+', callsign)) == 1: #call has just on digit e.g. DH1TW if len(re.findall(r'\\d+', callsign)) == 1: #call has just on digit e.g. DH1TW
callsign = re.sub('[\d]+', area_nr, callsign) callsign = re.sub('[\\d]+', area_nr, callsign)
else: # call has several digits e.g. 7N4AAL else: # call has several digits e.g. 7N4AAL
pass # no (two) digit prefix contries known where appendix would change entitiy pass # no (two) digit prefix countries known where appendix would change entity
return self._iterate_prefix(callsign, timestamp) return self._iterate_prefix(callsign, timestamp)
else: else:
return self._iterate_prefix(callsign, timestamp) return self._iterate_prefix(callsign, timestamp)
# regular callsigns, without prefix or appendix # regular callsigns, without prefix or appendix
elif re.match('^[\d]{0,1}[A-Z]{1,2}\d([A-Z]{1,4}|\d{3,3}|\d{1,3}[A-Z])[A-Z]{0,5}$', callsign): # elif re.match('^[\\d]{0,1}[A-Z]{1,2}\\d{1,2}[A-Z]{1,2}([A-Z]{1,4}|\\d{1,3})[A-Z]{0,5}$', callsign):
elif re.match('^[\\d]{0,1}[A-Z]{1,2}\\d{1,4}([A-Z]{1,4}|[A-Z]{1,2}\\d{0,3})[A-Z]{0,5}$', callsign):
return self._iterate_prefix(callsign, timestamp) return self._iterate_prefix(callsign, timestamp)
# callsigns with prefixes (xxx/callsign) # callsigns with prefixes (xxx/callsign)
@ -211,8 +201,11 @@ class Callinfo(object):
pfx = re.sub('/', '', pfx.group(0)) pfx = re.sub('/', '', pfx.group(0))
#make sure that the remaining part is actually a callsign (avoid: OZ/JO81) #make sure that the remaining part is actually a callsign (avoid: OZ/JO81)
rest = re.search('/[A-Z0-9]+', entire_callsign) rest = re.search('/[A-Z0-9]+', entire_callsign)
rest = re.sub('/', '', rest.group(0)) if rest is None:
if re.match('^[\d]{0,1}[A-Z]{1,2}\d([A-Z]{1,4}|\d{3,3}|\d{1,3}[A-Z])[A-Z]{0,5}$', rest): self._logger.warning(u"non latin characters in callsign '{0}'".format(entire_callsign))
raise KeyError
rest = re.sub('/', '', rest.group(0))
if re.match('^[\\d]{0,1}[A-Z]{1,2}\\d([A-Z]{1,4}|\\d{3,3}|\\d{1,3}[A-Z])[A-Z]{0,5}$', rest):
return self._iterate_prefix(pfx) return self._iterate_prefix(pfx)
if entire_callsign in callsign_exceptions: if entire_callsign in callsign_exceptions:
@ -221,7 +214,9 @@ class Callinfo(object):
self._logger.debug("Could not decode " + callsign) self._logger.debug("Could not decode " + callsign)
raise KeyError("Callsign could not be decoded") raise KeyError("Callsign could not be decoded")
def _lookup_callsign(self, callsign, timestamp=timestamp_now): def _lookup_callsign(self, callsign, timestamp=None):
if timestamp is None:
timestamp = datetime.now(timezone.utc)
# Check if operation is invalid # Check if operation is invalid
invalid = False invalid = False
@ -264,12 +259,12 @@ class Callinfo(object):
# Dismantel the callsign and check if the prefix is known # Dismantel the callsign and check if the prefix is known
return self._dismantle_callsign(callsign, timestamp) return self._dismantle_callsign(callsign, timestamp)
def get_all(self, callsign, timestamp=timestamp_now): def get_all(self, callsign, timestamp=None):
""" Lookup a callsign and return all data available from the underlying database """ Lookup a callsign and return all data available from the underlying database
Args: Args:
callsign (str): Amateur Radio callsign callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC) timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc)
Returns: Returns:
dict: Dictionary containing the callsign specific data dict: Dictionary containing the callsign specific data
@ -302,6 +297,12 @@ class Callinfo(object):
would be missing with Clublog (API or XML) :py:class:`LookupLib`. would be missing with Clublog (API or XML) :py:class:`LookupLib`.
""" """
callsign = callsign.upper()
if timestamp is None:
timestamp = datetime.now(timezone.utc)
callsign_data = self._lookup_callsign(callsign, timestamp) callsign_data = self._lookup_callsign(callsign, timestamp)
try: try:
@ -312,12 +313,12 @@ class Callinfo(object):
return callsign_data return callsign_data
def is_valid_callsign(self, callsign, timestamp=timestamp_now): def is_valid_callsign(self, callsign, timestamp=None):
""" Checks if a callsign is valid """ Checks if a callsign is valid
Args: Args:
callsign (str): Amateur Radio callsign callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC) timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc)
Returns: Returns:
bool: True / False bool: True / False
@ -332,18 +333,21 @@ class Callinfo(object):
True True
""" """
if timestamp is None:
timestamp = datetime.now(timezone.utc)
try: try:
if self.get_all(callsign, timestamp): if self.get_all(callsign, timestamp):
return True return True
except KeyError: except KeyError:
return False return False
def get_lat_long(self, callsign, timestamp=timestamp_now): def get_lat_long(self, callsign, timestamp=None):
""" Returns Latitude and Longitude for a callsign """ Returns Latitude and Longitude for a callsign
Args: Args:
callsign (str): Amateur Radio callsign callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC) timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc)
Returns: Returns:
dict: Containing Latitude and Longitude dict: Containing Latitude and Longitude
@ -369,18 +373,21 @@ class Callinfo(object):
dedicated entry in the database exists. Best results will be retrieved with QRZ.com Lookup. dedicated entry in the database exists. Best results will be retrieved with QRZ.com Lookup.
""" """
if timestamp is None:
timestamp = datetime.now(timezone.utc)
callsign_data = self.get_all(callsign, timestamp=timestamp) callsign_data = self.get_all(callsign, timestamp=timestamp)
return { return {
const.LATITUDE: callsign_data[const.LATITUDE], const.LATITUDE: callsign_data[const.LATITUDE],
const.LONGITUDE: callsign_data[const.LONGITUDE] const.LONGITUDE: callsign_data[const.LONGITUDE]
} }
def get_cqz(self, callsign, timestamp=timestamp_now): def get_cqz(self, callsign, timestamp=None):
""" Returns CQ Zone of a callsign """ Returns CQ Zone of a callsign
Args: Args:
callsign (str): Amateur Radio callsign callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC) timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc)
Returns: Returns:
int: containing the callsign's CQ Zone int: containing the callsign's CQ Zone
@ -389,14 +396,17 @@ class Callinfo(object):
KeyError: no CQ Zone found for callsign KeyError: no CQ Zone found for callsign
""" """
if timestamp is None:
timestamp = datetime.now(timezone.utc)
return self.get_all(callsign, timestamp)[const.CQZ] return self.get_all(callsign, timestamp)[const.CQZ]
def get_ituz(self, callsign, timestamp=timestamp_now): def get_ituz(self, callsign, timestamp=None):
""" Returns ITU Zone of a callsign """ Returns ITU Zone of a callsign
Args: Args:
callsign (str): Amateur Radio callsign callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC) timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc)
Returns: Returns:
int: containing the callsign's CQ Zone int: containing the callsign's CQ Zone
@ -408,14 +418,17 @@ class Callinfo(object):
Currently, only Country-files.com lookup database contains ITU Zones Currently, only Country-files.com lookup database contains ITU Zones
""" """
if timestamp is None:
timestamp = datetime.now(timezone.utc)
return self.get_all(callsign, timestamp)[const.ITUZ] return self.get_all(callsign, timestamp)[const.ITUZ]
def get_country_name(self, callsign, timestamp=timestamp_now): def get_country_name(self, callsign, timestamp=None):
""" Returns the country name where the callsign is located """ Returns the country name where the callsign is located
Args: Args:
callsign (str): Amateur Radio callsign callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC) timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc)
Returns: Returns:
str: name of the Country str: name of the Country
@ -432,14 +445,17 @@ class Callinfo(object):
- Clublog: "FEDERAL REPUBLIC OF GERMANY" - Clublog: "FEDERAL REPUBLIC OF GERMANY"
""" """
if timestamp is None:
timestamp = datetime.now(timezone.utc)
return self.get_all(callsign, timestamp)[const.COUNTRY] return self.get_all(callsign, timestamp)[const.COUNTRY]
def get_adif_id(self, callsign, timestamp=timestamp_now): def get_adif_id(self, callsign, timestamp=None):
""" Returns ADIF id of a callsign's country """ Returns ADIF id of a callsign's country
Args: Args:
callsign (str): Amateur Radio callsign callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC) timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc)
Returns: Returns:
int: containing the country ADIF id int: containing the country ADIF id
@ -448,14 +464,17 @@ class Callinfo(object):
KeyError: No Country found for callsign KeyError: No Country found for callsign
""" """
if timestamp is None:
timestamp = datetime.now(timezone.utc)
return self.get_all(callsign, timestamp)[const.ADIF] return self.get_all(callsign, timestamp)[const.ADIF]
def get_continent(self, callsign, timestamp=timestamp_now): def get_continent(self, callsign, timestamp=None):
""" Returns the continent Identifier of a callsign """ Returns the continent Identifier of a callsign
Args: Args:
callsign (str): Amateur Radio callsign callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC) timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc)
Returns: Returns:
str: continent identified str: continent identified
@ -474,4 +493,7 @@ class Callinfo(object):
- OC: Oceania - OC: Oceania
- AN: Antarctica - AN: Antarctica
""" """
if timestamp is None:
timestamp = datetime.now(timezone.utc)
return self.get_all(callsign, timestamp)[const.CONTINENT] return self.get_all(callsign, timestamp)[const.CONTINENT]

View file

@ -1,7 +1,7 @@
{ {
"Canada": 1, "Canada": 1,
"Kingman Reef": 134, "Kingman Reef": 134,
"Swaziland": 468, "Kingdom of Eswatini": 468,
"Cameroon": 406, "Cameroon": 406,
"Burkina Faso": 480, "Burkina Faso": 480,
"Turkmenistan": 280, "Turkmenistan": 280,
@ -143,7 +143,7 @@
"Hungary": 239, "Hungary": 239,
"Sable Island": 211, "Sable Island": 211,
"Bosnia-Herzegovina": 501, "Bosnia-Herzegovina": 501,
"Brazil": 18, "Brazil": 108,
"Swains Island": 515, "Swains Island": 515,
"DPR of Korea": 344, "DPR of Korea": 344,
"Lakshadweep Islands": 142, "Lakshadweep Islands": 142,
@ -208,14 +208,13 @@
"Vanuatu": 158, "Vanuatu": 158,
"Malawi": 440, "Malawi": 440,
"Republic of the Congo": 412, "Republic of the Congo": 412,
"Dominican Republic": 95, "Dominican Republic": 72,
"St. Pierre & Miquelon": 277, "St. Pierre & Miquelon": 277,
"St. Helena": 250, "St. Helena": 250,
"St. Peter ": 253, "St. Peter ": 253,
"Baker & Howland Islands": 20, "Baker & Howland Islands": 20,
"Willis Island": 303, "Willis Island": 303,
"Balearic Islands": 21, "Balearic Islands": 21,
"i.name": 150,
"European Turkey": 390, "European Turkey": 390,
"Rodriguez Island": 207, "Rodriguez Island": 207,
"Guinea": 107, "Guinea": 107,
@ -362,4 +361,4 @@
"Amsterdam ": 10, "Amsterdam ": 10,
"Corsica": 214, "Corsica": 214,
"Shetland Islands": 279 "Shetland Islands": 279
} }

View file

@ -1,37 +1,30 @@
__author__ = 'dh1tw' __author__ = 'dh1tw'
from datetime import datetime from datetime import datetime, timezone
from time import strptime, mktime from time import strptime, mktime
import re import re
import pytz
from pyhamtools.consts import LookupConventions as const from pyhamtools.consts import LookupConventions as const
UTC = pytz.UTC
def decode_char_spot(raw_string): def decode_char_spot(raw_string):
"""Chop Line from DX-Cluster into pieces and return a dict with the spot data""" """Chop Line from DX-Cluster into pieces and return a dict with the spot data"""
data = {} data = {}
# Spotter callsign # Spotter callsign
if re.match('[A-Za-z0-9\/]+[:$]', raw_string[6:15]): if re.match(r'[A-Za-z0-9\/]+[:$]', raw_string[6:15]):
data[const.SPOTTER] = re.sub(':', '', re.match('[A-Za-z0-9\/]+[:$]', raw_string[6:15]).group(0)) data[const.SPOTTER] = re.sub(':', '', re.match(r'[A-Za-z0-9\/]+[:$]', raw_string[6:15]).group(0))
else: else:
raise ValueError raise ValueError
if re.search('[0-9\.]{5,12}', raw_string[10:25]): if re.search(r'[0-9\.]{5,12}', raw_string[10:25]):
data[const.FREQUENCY] = float(re.search('[0-9\.]{5,12}', raw_string[10:25]).group(0)) data[const.FREQUENCY] = float(re.search(r'[0-9\.]{5,12}', raw_string[10:25]).group(0))
else: else:
raise ValueError raise ValueError
data[const.DX] = re.sub('[^A-Za-z0-9\/]+', '', raw_string[26:38]) data[const.DX] = re.sub(r'[^A-Za-z0-9\/]+', '', raw_string[26:38])
data[const.COMMENT] = re.sub('[^\sA-Za-z0-9\.,;\#\+\-!\?\$\(\)@\/]+', ' ', raw_string[39:69]).strip() data[const.COMMENT] = re.sub(r'[^\sA-Za-z0-9\.,;\#\+\-!\?\$\(\)@\/]+', ' ', raw_string[39:69]).strip()
data[const.TIME] = datetime.now().replace(tzinfo=UTC) data[const.TIME] = datetime.now(timezone.utc)
return data return data

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,7 +200,19 @@ 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
return {"band": band, "mode": mode} return {"band": band, "mode": mode}

View file

@ -1,18 +1,15 @@
from __future__ import division
from math import pi, sin, cos, atan2, sqrt, radians, log, tan, degrees from math import pi, sin, cos, atan2, sqrt, radians, log, tan, degrees
from datetime import datetime from datetime import datetime, timezone
import pytz
import ephem import ephem
UTC = pytz.UTC 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
@ -36,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:
@ -83,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'):
@ -104,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
@ -129,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:
@ -146,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
@ -164,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
@ -188,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
@ -200,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
@ -217,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)
@ -240,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
@ -257,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,11 +333,11 @@ def calculate_heading_longpath(locator1, locator2):
return lp return lp
def calculate_sunrise_sunset(locator, calc_date=datetime.utcnow()): 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:
@ -283,16 +351,14 @@ def calculate_sunrise_sunset(locator, calc_date=datetime.utcnow()):
The following calculates the next sunrise & sunset for JN48QM on the 1./Jan/2014 The following calculates the next sunrise & sunset for JN48QM on the 1./Jan/2014
>>> from pyhamtools.locator import calculate_sunrise_sunset >>> from pyhamtools.locator import calculate_sunrise_sunset
>>> from datetime import datetime >>> from datetime import datetime, timezone
>>> import pytz >>> myDate = datetime(year=2014, month=1, day=1, tzinfo=timezone.utc)
>>> UTC = pytz.UTC
>>> myDate = datetime(year=2014, month=1, day=1, tzinfo=UTC)
>>> calculate_sunrise_sunset("JN48QM", myDate) >>> calculate_sunrise_sunset("JN48QM", myDate)
{ {
'morning_dawn': datetime.datetime(2014, 1, 1, 6, 36, 51, 710524, tzinfo=<UTC>), 'morning_dawn': datetime.datetime(2014, 1, 1, 6, 36, 51, 710524, tzinfo=datetime.timezone.utc),
'sunset': datetime.datetime(2014, 1, 1, 16, 15, 23, 31016, tzinfo=<UTC>), 'sunset': datetime.datetime(2014, 1, 1, 16, 15, 23, 31016, tzinfo=datetime.timezone.utc),
'evening_dawn': datetime.datetime(2014, 1, 1, 15, 38, 8, 355315, tzinfo=<UTC>), 'evening_dawn': datetime.datetime(2014, 1, 1, 15, 38, 8, 355315, tzinfo=datetime.timezone.utc),
'sunrise': datetime.datetime(2014, 1, 1, 7, 14, 6, 162063, tzinfo=<UTC>) 'sunrise': datetime.datetime(2014, 1, 1, 7, 14, 6, 162063, tzinfo=datetime.timezone.utc)
} }
""" """
@ -303,6 +369,8 @@ def calculate_sunrise_sunset(locator, calc_date=datetime.utcnow()):
latitude, longitude = locator_to_latlong(locator) latitude, longitude = locator_to_latlong(locator)
if calc_date is None:
calc_date = datetime.now(timezone.utc)
if type(calc_date) != datetime: if type(calc_date) != datetime:
raise ValueError raise ValueError
@ -348,11 +416,11 @@ def calculate_sunrise_sunset(locator, calc_date=datetime.utcnow()):
result['sunset'] = sunset result['sunset'] = sunset
if morning_dawn: if morning_dawn:
result['morning_dawn'] = morning_dawn.replace(tzinfo=UTC) result['morning_dawn'] = morning_dawn.replace(tzinfo=timezone.utc)
if sunrise: if sunrise:
result['sunrise'] = sunrise.replace(tzinfo=UTC) result['sunrise'] = sunrise.replace(tzinfo=timezone.utc)
if evening_dawn: if evening_dawn:
result['evening_dawn'] = evening_dawn.replace(tzinfo=UTC) result['evening_dawn'] = evening_dawn.replace(tzinfo=timezone.utc)
if sunset: if sunset:
result['sunset'] = sunset.replace(tzinfo=UTC) result['sunset'] = sunset.replace(tzinfo=timezone.utc)
return result return result

View file

@ -1,4 +1,3 @@
import os
import re import re
from datetime import datetime from datetime import datetime

View file

@ -1,36 +1,23 @@
from __future__ import unicode_literals
import os import os
import logging import logging
import logging.config import logging.config
import re import re
import random, string import random, string
from datetime import datetime from datetime import datetime, timezone
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import urllib import urllib
import json import json
import copy import copy
import sys
import unicodedata
import requests import requests
from requests.exceptions import ConnectionError, HTTPError, Timeout from requests.exceptions import ConnectionError, HTTPError, Timeout
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import pytz
from . import version from . import version
from .consts import LookupConventions as const from .consts import LookupConventions as const
from .exceptions import APIKeyMissingError from .exceptions import APIKeyMissingError
UTC = pytz.UTC REDIS_LUA_DEL_SCRIPT = "local keys = redis.call('keys', ARGV[1]) \n for i=1,#keys,20000 do \n redis.call('del', unpack(keys, i, math.min(i+19999, #keys))) \n end \n return keys"
timestamp_now = datetime.utcnow().replace(tzinfo=UTC)
if sys.version_info < (2, 7,):
class NullHandler(logging.Handler):
def emit(self, record):
pass
if sys.version_info.major == 3:
unicode = str
class LookupLib(object): class LookupLib(object):
""" """
@ -54,7 +41,7 @@ class LookupLib(object):
lookup against the Clublog API or QRZ.com. lookup against the Clublog API or QRZ.com.
The entire lookup data (where database files are downloaded) can also be copied into Redis, which an extremely The entire lookup data (where database files are downloaded) can also be copied into Redis, which an extremely
fast in-memory Key/Value store. A LookupLib object can be instanciated to perform then all lookups in Redis, fast in-memory Key/Value store. A LookupLib object can be instantiated to perform then all lookups in Redis,
instead processing and loading the data from Internet / File. This saves some time and allows several instances instead processing and loading the data from Internet / File. This saves some time and allows several instances
of :py:class:`LookupLib` to query the same data concurrently. of :py:class:`LookupLib` to query the same data concurrently.
@ -79,10 +66,7 @@ class LookupLib(object):
self._logger = logger self._logger = logger
else: else:
self._logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
if sys.version_info[:2] == (2, 6): self._logger.addHandler(logging.NullHandler())
self._logger.addHandler(NullHandler())
else:
self._logger.addHandler(logging.NullHandler())
self._apikey = apikey self._apikey = apikey
self._apiv = apiv self._apiv = apiv
@ -130,18 +114,15 @@ class LookupLib(object):
"agent" : agent "agent" : agent
} }
if sys.version_info.major == 3: encodeurl = url + "?" + urllib.parse.urlencode(params)
encodeurl = url + "?" + urllib.parse.urlencode(params)
else:
encodeurl = url + "?" + urllib.urlencode(params)
response = requests.get(encodeurl, timeout=10) response = requests.get(encodeurl, timeout=10)
doc = BeautifulSoup(response.text, "html.parser") doc = BeautifulSoup(response.text, "xml")
session_key = None session_key = None
if doc.session.key: if doc.QRZDatabase.Session.Key:
session_key = doc.session.key.text session_key = doc.QRZDatabase.Session.Key.text
else: else:
if doc.session.error: if doc.QRZDatabase.Session.Error:
raise ValueError(doc.session.error.text) raise ValueError(doc.QRZDatabase.Session.Error.text)
else: else:
raise ValueError("Could not retrieve Session Key from QRZ.com") raise ValueError("Could not retrieve Session Key from QRZ.com")
@ -175,7 +156,7 @@ class LookupLib(object):
>>> from pyhamtools import LookupLib >>> from pyhamtools import LookupLib
>>> import redis >>> import redis
>>> r = redis.Redis() >>> r = redis.Redis()
>>> my_lookuplib = LookupLib(lookuptype="countryfile", redis_instance=r, redis_prefix="CF") >>> my_lookuplib = LookupLib(lookuptype="redis", redis_instance=r, redis_prefix="CF")
>>> my_lookuplib.lookup_callsign("3D2RI") >>> my_lookuplib.lookup_callsign("3D2RI")
{ {
u'adif': 460, u'adif': 460,
@ -224,18 +205,26 @@ class LookupLib(object):
def _push_dict_to_redis(self, push_dict, redis_prefix, name): def _push_dict_to_redis(self, push_dict, redis_prefix, name):
r = self._redis r = self._redis
pipe = r.pipeline()
pipe.eval(REDIS_LUA_DEL_SCRIPT, 0, redis_prefix + name)
for i in push_dict: for i in push_dict:
json_data = self._serialize_data(push_dict[i]) json_data = self._serialize_data(push_dict[i])
r.delete(redis_prefix + name + str(i)) pipe.set(redis_prefix + name + str(i), json_data)
r.set(redis_prefix + name + str(i), json_data)
pipe.execute()
return True return True
def _push_dict_index_to_redis(self, index_dict, redis_prefix, name): def _push_dict_index_to_redis(self, index_dict, redis_prefix, name):
r = self._redis r = self._redis
pipe = r.pipeline()
pipe.eval(REDIS_LUA_DEL_SCRIPT, 0, redis_prefix + name)
for i in index_dict: for i in index_dict:
r.delete(redis_prefix + name + str(i))
for el in index_dict[i]: for el in index_dict[i]:
r.sadd(redis_prefix + name + str(i), el) pipe.sadd(redis_prefix + name + str(i), el)
pipe.execute()
return True return True
@ -319,13 +308,13 @@ class LookupLib(object):
return new_dict return new_dict
def lookup_callsign(self, callsign=None, timestamp=timestamp_now): def lookup_callsign(self, callsign=None, timestamp=None):
""" """
Returns lookup data if an exception exists for a callsign Returns lookup data if an exception exists for a callsign
Args: Args:
callsign (string): Amateur radio callsign callsign (string): Amateur radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC) timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc)
Returns: Returns:
dict: Dictionary containing the country specific data of the callsign dict: Dictionary containing the country specific data of the callsign
@ -338,10 +327,9 @@ class LookupLib(object):
The following code queries the the online Clublog API for the callsign "VK9XO" on a specific date. The following code queries the the online Clublog API for the callsign "VK9XO" on a specific date.
>>> from pyhamtools import LookupLib >>> from pyhamtools import LookupLib
>>> from datetime import datetime >>> from datetime import datetime, timezone
>>> import pytz
>>> my_lookuplib = LookupLib(lookuptype="clublogapi", apikey="myapikey") >>> my_lookuplib = LookupLib(lookuptype="clublogapi", apikey="myapikey")
>>> timestamp = datetime(year=1962, month=7, day=7, tzinfo=pytz.UTC) >>> timestamp = datetime(year=1962, month=7, day=7, tzinfo=timezone.utc)
>>> print my_lookuplib.lookup_callsign("VK9XO", timestamp) >>> print my_lookuplib.lookup_callsign("VK9XO", timestamp)
{ {
'country': u'CHRISTMAS ISLAND', 'country': u'CHRISTMAS ISLAND',
@ -364,6 +352,8 @@ class LookupLib(object):
""" """
callsign = callsign.strip().upper() callsign = callsign.strip().upper()
if timestamp is None:
timestamp = datetime.now(timezone.utc)
if self._lookuptype == "clublogapi": if self._lookuptype == "clublogapi":
callsign_data = self._lookup_clublogAPI(callsign=callsign, timestamp=timestamp, apikey=self._apikey) callsign_data = self._lookup_clublogAPI(callsign=callsign, timestamp=timestamp, apikey=self._apikey)
@ -482,13 +472,13 @@ class LookupLib(object):
raise KeyError raise KeyError
def lookup_prefix(self, prefix, timestamp=timestamp_now): def lookup_prefix(self, prefix, timestamp=None):
""" """
Returns lookup data of a Prefix Returns lookup data of a Prefix
Args: Args:
prefix (string): Prefix of a Amateur Radio callsign prefix (string): Prefix of a Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC) timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc)
Returns: Returns:
dict: Dictionary containing the country specific data of the Prefix dict: Dictionary containing the country specific data of the Prefix
@ -524,6 +514,8 @@ class LookupLib(object):
""" """
prefix = prefix.strip().upper() prefix = prefix.strip().upper()
if timestamp is None:
timestamp = datetime.now(timezone.utc)
if self._lookuptype == "clublogxml" or self._lookuptype == "countryfile": if self._lookuptype == "clublogxml" or self._lookuptype == "countryfile":
@ -537,13 +529,13 @@ class LookupLib(object):
# no matching case # no matching case
raise KeyError raise KeyError
def is_invalid_operation(self, callsign, timestamp=datetime.utcnow().replace(tzinfo=UTC)): def is_invalid_operation(self, callsign, timestamp=None):
""" """
Returns True if an operations is known as invalid Returns True if an operations is known as invalid
Args: Args:
callsign (string): Amateur Radio callsign callsign (string): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC) timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc)
Returns: Returns:
bool: True if a record exists for this callsign (at the given time) bool: True if a record exists for this callsign (at the given time)
@ -556,13 +548,12 @@ class LookupLib(object):
The following code checks the Clublog XML database if the operation is valid for two dates. The following code checks the Clublog XML database if the operation is valid for two dates.
>>> from pyhamtools import LookupLib >>> from pyhamtools import LookupLib
>>> from datetime import datetime >>> from datetime import datetime, timezone
>>> import pytz
>>> my_lookuplib = LookupLib(lookuptype="clublogxml", apikey="myapikey") >>> my_lookuplib = LookupLib(lookuptype="clublogxml", apikey="myapikey")
>>> print my_lookuplib.is_invalid_operation("5W1CFN") >>> print my_lookuplib.is_invalid_operation("5W1CFN")
True True
>>> try: >>> try:
>>> timestamp = datetime(year=2012, month=1, day=31).replace(tzinfo=pytz.UTC) >>> timestamp = datetime(year=2012, month=1, day=31, tzinfo=timezone.utc)
>>> my_lookuplib.is_invalid_operation("5W1CFN", timestamp) >>> my_lookuplib.is_invalid_operation("5W1CFN", timestamp)
>>> except KeyError: >>> except KeyError:
>>> print "Seems to be invalid operation before 31.1.2012" >>> print "Seems to be invalid operation before 31.1.2012"
@ -577,6 +568,8 @@ class LookupLib(object):
""" """
callsign = callsign.strip().upper() callsign = callsign.strip().upper()
if timestamp is None:
timestamp = datetime.now(timezone.utc)
if self._lookuptype == "clublogxml": if self._lookuptype == "clublogxml":
@ -624,13 +617,13 @@ class LookupLib(object):
raise KeyError raise KeyError
def lookup_zone_exception(self, callsign, timestamp=datetime.utcnow().replace(tzinfo=UTC)): def lookup_zone_exception(self, callsign, timestamp=None):
""" """
Returns a CQ Zone if an exception exists for the given callsign Returns a CQ Zone if an exception exists for the given callsign
Args: Args:
callsign (string): Amateur radio callsign callsign (string): Amateur radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC) timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc)
Returns: Returns:
int: Value of the the CQ Zone exception which exists for this callsign (at the given time) int: Value of the the CQ Zone exception which exists for this callsign (at the given time)
@ -659,6 +652,8 @@ class LookupLib(object):
""" """
callsign = callsign.strip().upper() callsign = callsign.strip().upper()
if timestamp is None:
timestamp = datetime.now(timezone.utc)
if self._lookuptype == "clublogxml": if self._lookuptype == "clublogxml":
@ -672,7 +667,7 @@ class LookupLib(object):
#no matching case #no matching case
raise KeyError raise KeyError
def _lookup_clublogAPI(self, callsign=None, timestamp=timestamp_now, url="https://secure.clublog.org/dxcc", apikey=None): def _lookup_clublogAPI(self, callsign=None, timestamp=None, url="https://cdn.clublog.org/dxcc", apikey=None):
""" Set up the Lookup object for Clublog Online API """ Set up the Lookup object for Clublog Online API
""" """
@ -686,10 +681,10 @@ class LookupLib(object):
"call" : callsign "call" : callsign
} }
if sys.version_info.major == 3: if timestamp is None:
encodeurl = url + "?" + urllib.parse.urlencode(params) timestamp = datetime.now(timezone.utc)
else:
encodeurl = url + "?" + urllib.urlencode(params) encodeurl = url + "?" + urllib.parse.urlencode(params)
response = requests.get(encodeurl, timeout=5) response = requests.get(encodeurl, timeout=5)
if not self._check_html_response(response): if not self._check_html_response(response):
@ -720,10 +715,7 @@ class LookupLib(object):
"callsign" : callsign, "callsign" : callsign,
} }
if sys.version_info.major == 3: encodeurl = url + "?" + urllib.parse.urlencode(params)
encodeurl = url + "?" + urllib.parse.urlencode(params)
else:
encodeurl = url + "?" + urllib.urlencode(params)
response = requests.get(encodeurl, timeout=5) response = requests.get(encodeurl, timeout=5)
return response return response
@ -736,10 +728,7 @@ class LookupLib(object):
"dxcc" : str(dxcc_or_callsign), "dxcc" : str(dxcc_or_callsign),
} }
if sys.version_info.major == 3: encodeurl = url + "?" + urllib.parse.urlencode(params)
encodeurl = url + "?" + urllib.parse.urlencode(params)
else:
encodeurl = url + "?" + urllib.urlencode(params)
response = requests.get(encodeurl, timeout=5) response = requests.get(encodeurl, timeout=5)
return response return response
@ -749,43 +738,43 @@ class LookupLib(object):
response = self._request_dxcc_info_from_qrz(dxcc_or_callsign, apikey, apiv=apiv) response = self._request_dxcc_info_from_qrz(dxcc_or_callsign, apikey, apiv=apiv)
root = BeautifulSoup(response.text, "html.parser") root = BeautifulSoup(response.text, "xml")
lookup = {} lookup = {}
if root.error: #try to get a new session key and try to request again if root.Error: #try to get a new session key and try to request again
if re.search('No DXCC Information for', root.error.text, re.I): #No data available for callsign if re.search('No DXCC Information for', root.Error.text, re.I): #No data available for callsign
raise KeyError(root.error.text) raise KeyError(root.Error.text)
elif re.search('Session Timeout', root.error.text, re.I): # Get new session key elif re.search('Session Timeout', root.Error.text, re.I): # Get new session key
self._apikey = apikey = self._get_qrz_session_key(self._username, self._pwd) self._apikey = apikey = self._get_qrz_session_key(self._username, self._pwd)
response = self._request_dxcc_info_from_qrz(dxcc_or_callsign, apikey) response = self._request_dxcc_info_from_qrz(dxcc_or_callsign, apikey)
root = BeautifulSoup(response.text, "html.parser") root = BeautifulSoup(response.text, "xml")
else: else:
raise AttributeError("Session Key Missing") #most likely session key missing or invalid raise AttributeError("Session Key Missing") #most likely session key missing or invalid
if root.dxcc is None: if root.DXCC is None:
raise ValueError raise ValueError
if root.dxcc.dxcc: if root.DXCC.dxcc:
lookup[const.ADIF] = int(root.dxcc.dxcc.text) lookup[const.ADIF] = int(root.DXCC.dxcc.text)
if root.dxcc.cc: if root.DXCC.cc:
lookup['cc'] = root.dxcc.cc.text lookup['cc'] = root.DXCC.cc.text
if root.dxcc.cc: if root.DXCC.ccc:
lookup['ccc'] = root.dxcc.ccc.text lookup['ccc'] = root.DXCC.ccc.text
if root.find('name'): if root.find('name'):
lookup[const.COUNTRY] = root.find('name').get_text() lookup[const.COUNTRY] = root.find('name').get_text()
if root.dxcc.continent: if root.DXCC.continent:
lookup[const.CONTINENT] = root.dxcc.continent.text lookup[const.CONTINENT] = root.DXCC.continent.text
if root.dxcc.ituzone: if root.DXCC.ituzone:
lookup[const.ITUZ] = int(root.dxcc.ituzone.text) lookup[const.ITUZ] = int(root.DXCC.ituzone.text)
if root.dxcc.cqzone: if root.DXCC.cqzone:
lookup[const.CQZ] = int(root.dxcc.cqzone.text) lookup[const.CQZ] = int(root.DXCC.cqzone.text)
if root.dxcc.timezone: if root.DXCC.timezone:
lookup['timezone'] = float(root.dxcc.timezone.text) lookup['timezone'] = float(root.DXCC.timezone.text)
if root.dxcc.lat: if root.DXCC.lat:
lookup[const.LATITUDE] = float(root.dxcc.lat.text) lookup[const.LATITUDE] = float(root.DXCC.lat.text)
if root.dxcc.lon: if root.DXCC.lon:
lookup[const.LONGITUDE] = float(root.dxcc.lon.text) lookup[const.LONGITUDE] = float(root.DXCC.lon.text)
return lookup return lookup
@ -801,164 +790,160 @@ class LookupLib(object):
response = self._request_callsign_info_from_qrz(callsign, apikey, apiv) response = self._request_callsign_info_from_qrz(callsign, apikey, apiv)
root = BeautifulSoup(response.text, "html.parser") root = BeautifulSoup(response.text, "xml")
lookup = {} lookup = {}
if root.error: if root.Error:
if re.search('Not found', root.error.text, re.I): #No data available for callsign if re.search('Not found', root.Error.text, re.I): #No data available for callsign
raise KeyError(root.error.text) raise KeyError(root.Error.text)
#try to get a new session key and try to request again #try to get a new session key and try to request again
elif re.search('Session Timeout', root.error.text, re.I) or re.search('Invalid session key', root.error.text, re.I): elif re.search('Session Timeout', root.Error.text, re.I) or re.search('Invalid session key', root.Error.text, re.I):
apikey = self._get_qrz_session_key(self._username, self._pwd) apikey = self._get_qrz_session_key(self._username, self._pwd)
response = self._request_callsign_info_from_qrz(callsign, apikey, apiv) response = self._request_callsign_info_from_qrz(callsign, apikey, apiv)
root = BeautifulSoup(response.text, "html.parser") root = BeautifulSoup(response.text, "xml")
#if this fails again, raise error #if this fails again, raise error
if root.error: if root.Error:
if re.search('Not found', root.error.text, re.I): #No data available for callsign if re.search('Not found', root.Error.text, re.I): #No data available for callsign
raise KeyError(root.error.text) raise KeyError(root.Error.text)
else: else:
raise AttributeError(root.error.text) #most likely session key invalid raise AttributeError(root.Error.text) #most likely session key invalid
else: else:
#update API Key ob Lookup object #update API Key ob Lookup object
self._apikey = apikey self._apikey = apikey
else: else:
raise AttributeError(root.error.text) #most likely session key missing raise AttributeError(root.Error.text) #most likely session key missing
if root.callsign is None: if root.Callsign is None:
raise ValueError raise ValueError
if root.callsign.call: if root.Callsign.call:
lookup[const.CALLSIGN] = root.callsign.call.text lookup[const.CALLSIGN] = root.Callsign.call.text
if root.callsign.xref: if root.Callsign.xref:
lookup[const.XREF] = root.callsign.xref.text lookup[const.XREF] = root.Callsign.xref.text
if root.callsign.aliases: if root.Callsign.aliases:
lookup[const.ALIASES] = root.callsign.aliases.text.split(',') lookup[const.ALIASES] = root.Callsign.aliases.text.split(',')
if root.callsign.dxcc: if root.Callsign.dxcc:
lookup[const.ADIF] = int(root.callsign.dxcc.text) lookup[const.ADIF] = int(root.Callsign.dxcc.text)
if root.callsign.fname: if root.Callsign.fname:
lookup[const.FNAME] = root.callsign.fname.text lookup[const.FNAME] = root.Callsign.fname.text
if root.callsign.find("name"): if root.Callsign.find("name"):
lookup[const.NAME] = root.callsign.find('name').get_text() lookup[const.NAME] = root.Callsign.find('name').get_text()
if root.callsign.addr1: if root.Callsign.addr1:
lookup[const.ADDR1] = root.callsign.addr1.text lookup[const.ADDR1] = root.Callsign.addr1.text
if root.callsign.addr2: if root.Callsign.addr2:
lookup[const.ADDR2] = root.callsign.addr2.text lookup[const.ADDR2] = root.Callsign.addr2.text
if root.callsign.state: if root.Callsign.state:
lookup[const.STATE] = root.callsign.state.text lookup[const.STATE] = root.Callsign.state.text
if root.callsign.zip: if root.Callsign.zip:
lookup[const.ZIPCODE] = root.callsign.zip.text lookup[const.ZIPCODE] = root.Callsign.zip.text
if root.callsign.country: if root.Callsign.country:
lookup[const.COUNTRY] = root.callsign.country.text lookup[const.COUNTRY] = root.Callsign.country.text
if root.callsign.ccode: if root.Callsign.ccode:
lookup[const.CCODE] = int(root.callsign.ccode.text) lookup[const.CCODE] = int(root.Callsign.ccode.text)
if root.callsign.lat: if root.Callsign.lat:
lookup[const.LATITUDE] = float(root.callsign.lat.text) lookup[const.LATITUDE] = float(root.Callsign.lat.text)
if root.callsign.lon: if root.Callsign.lon:
lookup[const.LONGITUDE] = float(root.callsign.lon.text) lookup[const.LONGITUDE] = float(root.Callsign.lon.text)
if root.callsign.grid: if root.Callsign.grid:
lookup[const.LOCATOR] = root.callsign.grid.text lookup[const.LOCATOR] = root.Callsign.grid.text
if root.callsign.county: if root.Callsign.county:
lookup[const.COUNTY] = root.callsign.county.text lookup[const.COUNTY] = root.Callsign.county.text
if root.callsign.fips: if root.Callsign.fips:
lookup[const.FIPS] = int(root.callsign.fips.text) # check type lookup[const.FIPS] = int(root.Callsign.fips.text) # check type
if root.callsign.land: if root.Callsign.land:
lookup[const.LAND] = root.callsign.land.text lookup[const.LAND] = root.Callsign.land.text
if root.callsign.efdate: if root.Callsign.efdate:
try: try:
lookup[const.EFDATE] = datetime.strptime(root.callsign.efdate.text, '%Y-%m-%d').replace(tzinfo=UTC) lookup[const.EFDATE] = datetime.strptime(root.Callsign.efdate.text, '%Y-%m-%d').replace(tzinfo=timezone.utc)
except ValueError: except ValueError:
self._logger.debug("[QRZ.com] efdate: Invalid DateTime; " + callsign + " " + root.callsign.efdate.text) self._logger.debug("[QRZ.com] efdate: Invalid DateTime; " + callsign + " " + root.Callsign.efdate.text)
if root.callsign.expdate: if root.Callsign.expdate:
try: try:
lookup[const.EXPDATE] = datetime.strptime(root.callsign.expdate.text, '%Y-%m-%d').replace(tzinfo=UTC) lookup[const.EXPDATE] = datetime.strptime(root.Callsign.expdate.text, '%Y-%m-%d').replace(tzinfo=timezone.utc)
except ValueError: except ValueError:
self._logger.debug("[QRZ.com] expdate: Invalid DateTime; " + callsign + " " + root.callsign.expdate.text) self._logger.debug("[QRZ.com] expdate: Invalid DateTime; " + callsign + " " + root.Callsign.expdate.text)
if root.callsign.p_call: if root.Callsign.p_call:
lookup[const.P_CALL] = root.callsign.p_call.text lookup[const.P_CALL] = root.Callsign.p_call.text
if root.callsign.find('class'): if root.Callsign.find('class'):
lookup[const.LICENSE_CLASS] = root.callsign.find('class').get_text() lookup[const.LICENSE_CLASS] = root.Callsign.find('class').get_text()
if root.callsign.codes: if root.Callsign.codes:
lookup[const.CODES] = root.callsign.codes.text lookup[const.CODES] = root.Callsign.codes.text
if root.callsign.qslmgr: if root.Callsign.qslmgr:
lookup[const.QSLMGR] = root.callsign.qslmgr.text lookup[const.QSLMGR] = root.Callsign.qslmgr.text
if root.callsign.email: if root.Callsign.email:
lookup[const.EMAIL] = root.callsign.email.text lookup[const.EMAIL] = root.Callsign.email.text
if root.callsign.url: if root.Callsign.url:
lookup[const.URL] = root.callsign.url.text lookup[const.URL] = root.Callsign.url.text
if root.callsign.u_views: if root.Callsign.u_views:
lookup[const.U_VIEWS] = int(root.callsign.u_views.text) lookup[const.U_VIEWS] = int(root.Callsign.u_views.text)
if root.callsign.bio: if root.Callsign.bio:
lookup[const.BIO] = root.callsign.bio.text lookup[const.BIO] = root.Callsign.bio.text
if root.callsign.biodate: if root.Callsign.biodate:
try: try:
lookup[const.BIODATE] = datetime.strptime(root.callsign.biodate.text, '%Y-%m-%d %H:%M:%S').replace(tzinfo=UTC) lookup[const.BIODATE] = datetime.strptime(root.Callsign.biodate.text, '%Y-%m-%d %H:%M:%S').replace(tzinfo=timezone.utc)
except ValueError: except ValueError:
self._logger.warning("[QRZ.com] biodate: Invalid DateTime; " + callsign) self._logger.warning("[QRZ.com] biodate: Invalid DateTime; " + callsign)
if root.callsign.image: if root.Callsign.image:
lookup[const.IMAGE] = root.callsign.image.text lookup[const.IMAGE] = root.Callsign.image.text
if root.callsign.imageinfo: if root.Callsign.imageinfo:
lookup[const.IMAGE_INFO] = root.callsign.imageinfo.text lookup[const.IMAGE_INFO] = root.Callsign.imageinfo.text
if root.callsign.serial: if root.Callsign.serial:
lookup[const.SERIAL] = long(root.callsign.serial.text) lookup[const.SERIAL] = long(root.Callsign.serial.text)
if root.callsign.moddate: if root.Callsign.moddate:
try: try:
lookup[const.MODDATE] = datetime.strptime(root.callsign.moddate.text, '%Y-%m-%d %H:%M:%S').replace(tzinfo=UTC) lookup[const.MODDATE] = datetime.strptime(root.Callsign.moddate.text, '%Y-%m-%d %H:%M:%S').replace(tzinfo=timezone.utc)
except ValueError: except ValueError:
self._logger.warning("[QRZ.com] moddate: Invalid DateTime; " + callsign) self._logger.warning("[QRZ.com] moddate: Invalid DateTime; " + callsign)
if root.callsign.MSA: if root.Callsign.MSA:
lookup[const.MSA] = int(root.callsign.MSA.text) lookup[const.MSA] = int(root.Callsign.MSA.text)
if root.callsign.AreaCode: if root.Callsign.AreaCode:
lookup[const.AREACODE] = int(root.callsign.AreaCode.text) lookup[const.AREACODE] = int(root.Callsign.AreaCode.text)
if root.callsign.TimeZone: if root.Callsign.TimeZone:
lookup[const.TIMEZONE] = int(root.callsign.TimeZone.text) lookup[const.TIMEZONE] = root.Callsign.TimeZone.text
if root.callsign.GMTOffset: if root.Callsign.GMTOffset:
lookup[const.GMTOFFSET] = float(root.callsign.GMTOffset.text) lookup[const.GMTOFFSET] = float(root.Callsign.GMTOffset.text)
if root.callsign.DST: if root.Callsign.DST:
if root.callsign.DST.text == "Y": if root.Callsign.DST.text == "Y":
lookup[const.DST] = True lookup[const.DST] = True
else: else:
lookup[const.DST] = False lookup[const.DST] = False
if root.callsign.eqsl: if root.Callsign.eqsl:
if root.callsign.eqsl.text == "1": if root.Callsign.eqsl.text == "1":
lookup[const.EQSL] = True lookup[const.EQSL] = True
else: else:
lookup[const.EQSL] = False lookup[const.EQSL] = False
if root.callsign.mqsl: if root.Callsign.mqsl:
if root.callsign.mqsl.text == "1": if root.Callsign.mqsl.text == "1":
lookup[const.MQSL] = True lookup[const.MQSL] = True
else: else:
lookup[const.MQSL] = False lookup[const.MQSL] = False
if root.callsign.cqzone: if root.Callsign.cqzone:
lookup[const.CQZ] = int(root.callsign.cqzone.text) lookup[const.CQZ] = int(root.Callsign.cqzone.text)
if root.callsign.ituzone: if root.Callsign.ituzone:
lookup[const.ITUZ] = int(root.callsign.ituzone.text) lookup[const.ITUZ] = int(root.Callsign.ituzone.text)
if root.callsign.born: if root.Callsign.born:
lookup[const.BORN] = int(root.callsign.born.text) lookup[const.BORN] = int(root.Callsign.born.text)
if root.callsign.user: if root.Callsign.user:
lookup[const.USER_MGR] = root.callsign.user.text lookup[const.USER_MGR] = root.Callsign.user.text
if root.callsign.lotw: if root.Callsign.lotw:
if root.callsign.lotw.text == "1": if root.Callsign.lotw.text == "1":
lookup[const.LOTW] = True lookup[const.LOTW] = True
else: else:
lookup[const.LOTW] = False lookup[const.LOTW] = False
if root.callsign.iota: if root.Callsign.iota:
lookup[const.IOTA] = root.callsign.iota.text lookup[const.IOTA] = root.Callsign.iota.text
if root.callsign.geoloc: if root.Callsign.geoloc:
lookup[const.GEOLOC] = root.callsign.geoloc.text lookup[const.GEOLOC] = root.Callsign.geoloc.text
# if sys.version_info >= (2,):
# for item in lookup:
# if isinstance(lookup[item], unicode):
# print item, repr(lookup[item])
return lookup return lookup
def _load_clublogXML(self, def _load_clublogXML(self,
url="https://secure.clublog.org/cty.php", url="https://cdn.clublog.org/cty.php",
apikey=None, apikey=None,
cty_file=None): cty_file=None):
""" Load and process the ClublogXML file either as a download or from file """ Load and process the ClublogXML file either as a download or from file
@ -986,6 +971,9 @@ class LookupLib(object):
self._invalid_operations_index = cty_dict["invalid_operations_index"] self._invalid_operations_index = cty_dict["invalid_operations_index"]
self._zone_exceptions_index = cty_dict["zone_exceptions_index"] self._zone_exceptions_index = cty_dict["zone_exceptions_index"]
if self._download:
self._cleanup_download_artifact(cty_file)
return True return True
def _load_countryfile(self, def _load_countryfile(self,
@ -1020,6 +1008,9 @@ class LookupLib(object):
self._callsign_exceptions_index = cty_dict["exceptions_index"] self._callsign_exceptions_index = cty_dict["exceptions_index"]
self._prefixes_index = cty_dict["prefixes_index"] self._prefixes_index = cty_dict["prefixes_index"]
if self._download:
self._cleanup_download_artifact(cty_file)
return True return True
def _download_file(self, url, apikey=None): def _download_file(self, url, apikey=None):
@ -1081,6 +1072,19 @@ class LookupLib(object):
return cty_file_path return cty_file_path
def _cleanup_download_artifact(self, filename):
"""
Delete the downloaded files which are not necessary anymore
Args:
filename (string): absolute path to the download artifact
"""
try:
os.remove(filename)
except:
self._logger.warning("unable delete the download artifact: %s", _download_file)
def _extract_clublog_header(self, cty_xml_filename): def _extract_clublog_header(self, cty_xml_filename):
""" """
Extract the header of the Clublog XML File Extract the header of the Clublog XML File
@ -1096,7 +1100,7 @@ class LookupLib(object):
if cty_date: if cty_date:
cty_date = cty_date.group(0).replace("date=", "").replace("'", "") cty_date = cty_date.group(0).replace("date=", "").replace("'", "")
cty_date = datetime.strptime(cty_date[:19], '%Y-%m-%dT%H:%M:%S') cty_date = datetime.strptime(cty_date[:19], '%Y-%m-%dT%H:%M:%S')
cty_date.replace(tzinfo=UTC) cty_date.replace(tzinfo=timezone.utc)
cty_header["Date"] = cty_date cty_header["Date"] = cty_date
cty_ns = re.search("xmlns='.+[']", raw_header) cty_ns = re.search("xmlns='.+[']", raw_header)
@ -1107,7 +1111,7 @@ class LookupLib(object):
if len(cty_header) == 2: if len(cty_header) == 2:
self._logger.debug("Header successfully retrieved from CTY File") self._logger.debug("Header successfully retrieved from CTY File")
elif len(cty_header) < 2: elif len(cty_header) < 2:
self._logger.warning("Header could only be partically retrieved from CTY File") self._logger.warning("Header could only be partially retrieved from CTY File")
self._logger.warning("Content of Header: ") self._logger.warning("Content of Header: ")
for key in cty_header: for key in cty_header:
self._logger.warning(str(key)+": "+str(cty_header[key])) self._logger.warning(str(key)+": "+str(cty_header[key]))
@ -1176,10 +1180,10 @@ class LookupLib(object):
entity = {} entity = {}
for item in cty_entity: for item in cty_entity:
if item.tag == "name": if item.tag == "name":
entity[const.COUNTRY] = unicode(item.text) entity[const.COUNTRY] = str(item.text)
self._logger.debug(unicode(item.text)) self._logger.debug(str(item.text))
elif item.tag == "prefix": elif item.tag == "prefix":
entity[const.PREFIX] = unicode(item.text) entity[const.PREFIX] = str(item.text)
elif item.tag == "deleted": elif item.tag == "deleted":
if item.text == "TRUE": if item.text == "TRUE":
entity[const.DELETED] = True entity[const.DELETED] = True
@ -1188,17 +1192,17 @@ class LookupLib(object):
elif item.tag == "cqz": elif item.tag == "cqz":
entity[const.CQZ] = int(item.text) entity[const.CQZ] = int(item.text)
elif item.tag == "cont": elif item.tag == "cont":
entity[const.CONTINENT] = unicode(item.text) entity[const.CONTINENT] = str(item.text)
elif item.tag == "long": elif item.tag == "long":
entity[const.LONGITUDE] = float(item.text) entity[const.LONGITUDE] = float(item.text)
elif item.tag == "lat": elif item.tag == "lat":
entity[const.LATITUDE] = float(item.text) entity[const.LATITUDE] = float(item.text)
elif item.tag == "start": elif item.tag == "start":
dt = datetime.strptime(item.text[:19], '%Y-%m-%dT%H:%M:%S') dt = datetime.strptime(item.text[:19], '%Y-%m-%dT%H:%M:%S')
entity[const.START] = dt.replace(tzinfo=UTC) entity[const.START] = dt.replace(tzinfo=timezone.utc)
elif item.tag == "end": elif item.tag == "end":
dt = datetime.strptime(item.text[:19], '%Y-%m-%dT%H:%M:%S') dt = datetime.strptime(item.text[:19], '%Y-%m-%dT%H:%M:%S')
entity[const.END] = dt.replace(tzinfo=UTC) entity[const.END] = dt.replace(tzinfo=timezone.utc)
elif item.tag == "whitelist": elif item.tag == "whitelist":
if item.text == "TRUE": if item.text == "TRUE":
entity[const.WHITELIST] = True entity[const.WHITELIST] = True
@ -1206,10 +1210,10 @@ class LookupLib(object):
entity[const.WHITELIST] = False entity[const.WHITELIST] = False
elif item.tag == "whitelist_start": elif item.tag == "whitelist_start":
dt = datetime.strptime(item.text[:19], '%Y-%m-%dT%H:%M:%S') dt = datetime.strptime(item.text[:19], '%Y-%m-%dT%H:%M:%S')
entity[const.WHITELIST_START] = dt.replace(tzinfo=UTC) entity[const.WHITELIST_START] = dt.replace(tzinfo=timezone.utc)
elif item.tag == "whitelist_end": elif item.tag == "whitelist_end":
dt = datetime.strptime(item.text[:19], '%Y-%m-%dT%H:%M:%S') dt = datetime.strptime(item.text[:19], '%Y-%m-%dT%H:%M:%S')
entity[const.WHITELIST_END] = dt.replace(tzinfo=UTC) entity[const.WHITELIST_END] = dt.replace(tzinfo=timezone.utc)
except AttributeError: except AttributeError:
self._logger.error("Error while processing: ") self._logger.error("Error while processing: ")
entities[int(cty_entity[0].text)] = entity entities[int(cty_entity[0].text)] = entity
@ -1230,23 +1234,23 @@ class LookupLib(object):
else: else:
call_exceptions_index[call] = [int(cty_exception.attrib["record"])] call_exceptions_index[call] = [int(cty_exception.attrib["record"])]
elif item.tag == "entity": elif item.tag == "entity":
call_exception[const.COUNTRY] = unicode(item.text) call_exception[const.COUNTRY] = str(item.text)
elif item.tag == "adif": elif item.tag == "adif":
call_exception[const.ADIF] = int(item.text) call_exception[const.ADIF] = int(item.text)
elif item.tag == "cqz": elif item.tag == "cqz":
call_exception[const.CQZ] = int(item.text) call_exception[const.CQZ] = int(item.text)
elif item.tag == "cont": elif item.tag == "cont":
call_exception[const.CONTINENT] = unicode(item.text) call_exception[const.CONTINENT] = str(item.text)
elif item.tag == "long": elif item.tag == "long":
call_exception[const.LONGITUDE] = float(item.text) call_exception[const.LONGITUDE] = float(item.text)
elif item.tag == "lat": elif item.tag == "lat":
call_exception[const.LATITUDE] = float(item.text) call_exception[const.LATITUDE] = float(item.text)
elif item.tag == "start": elif item.tag == "start":
dt = datetime.strptime(item.text[:19], '%Y-%m-%dT%H:%M:%S') dt = datetime.strptime(item.text[:19], '%Y-%m-%dT%H:%M:%S')
call_exception[const.START] = dt.replace(tzinfo=UTC) call_exception[const.START] = dt.replace(tzinfo=timezone.utc)
elif item.tag == "end": elif item.tag == "end":
dt = datetime.strptime(item.text[:19], '%Y-%m-%dT%H:%M:%S') dt = datetime.strptime(item.text[:19], '%Y-%m-%dT%H:%M:%S')
call_exception[const.END] = dt.replace(tzinfo=UTC) call_exception[const.END] = dt.replace(tzinfo=timezone.utc)
call_exceptions[int(cty_exception.attrib["record"])] = call_exception call_exceptions[int(cty_exception.attrib["record"])] = call_exception
self._logger.debug(str(len(call_exceptions))+" Exceptions added") self._logger.debug(str(len(call_exceptions))+" Exceptions added")
@ -1271,23 +1275,23 @@ class LookupLib(object):
else: else:
prefixes_index[call] = [int(cty_prefix.attrib["record"])] prefixes_index[call] = [int(cty_prefix.attrib["record"])]
if item.tag == "entity": if item.tag == "entity":
prefix[const.COUNTRY] = unicode(item.text) prefix[const.COUNTRY] = str(item.text)
elif item.tag == "adif": elif item.tag == "adif":
prefix[const.ADIF] = int(item.text) prefix[const.ADIF] = int(item.text)
elif item.tag == "cqz": elif item.tag == "cqz":
prefix[const.CQZ] = int(item.text) prefix[const.CQZ] = int(item.text)
elif item.tag == "cont": elif item.tag == "cont":
prefix[const.CONTINENT] = unicode(item.text) prefix[const.CONTINENT] = str(item.text)
elif item.tag == "long": elif item.tag == "long":
prefix[const.LONGITUDE] = float(item.text) prefix[const.LONGITUDE] = float(item.text)
elif item.tag == "lat": elif item.tag == "lat":
prefix[const.LATITUDE] = float(item.text) prefix[const.LATITUDE] = float(item.text)
elif item.tag == "start": elif item.tag == "start":
dt = datetime.strptime(item.text[:19], '%Y-%m-%dT%H:%M:%S') dt = datetime.strptime(item.text[:19], '%Y-%m-%dT%H:%M:%S')
prefix[const.START] = dt.replace(tzinfo=UTC) prefix[const.START] = dt.replace(tzinfo=timezone.utc)
elif item.tag == "end": elif item.tag == "end":
dt = datetime.strptime(item.text[:19], '%Y-%m-%dT%H:%M:%S') dt = datetime.strptime(item.text[:19], '%Y-%m-%dT%H:%M:%S')
prefix[const.END] = dt.replace(tzinfo=UTC) prefix[const.END] = dt.replace(tzinfo=timezone.utc)
prefixes[int(cty_prefix.attrib["record"])] = prefix prefixes[int(cty_prefix.attrib["record"])] = prefix
self._logger.debug(str(len(prefixes))+" Prefixes added") self._logger.debug(str(len(prefixes))+" Prefixes added")
@ -1310,10 +1314,10 @@ class LookupLib(object):
elif item.tag == "start": elif item.tag == "start":
dt = datetime.strptime(item.text[:19], '%Y-%m-%dT%H:%M:%S') dt = datetime.strptime(item.text[:19], '%Y-%m-%dT%H:%M:%S')
invalid_operation[const.START] = dt.replace(tzinfo=UTC) invalid_operation[const.START] = dt.replace(tzinfo=timezone.utc)
elif item.tag == "end": elif item.tag == "end":
dt = datetime.strptime(item.text[:19], '%Y-%m-%dT%H:%M:%S') dt = datetime.strptime(item.text[:19], '%Y-%m-%dT%H:%M:%S')
invalid_operation[const.END] = dt.replace(tzinfo=UTC) invalid_operation[const.END] = dt.replace(tzinfo=timezone.utc)
invalid_operations[int(cty_inv_operation.attrib["record"])] = invalid_operation invalid_operations[int(cty_inv_operation.attrib["record"])] = invalid_operation
self._logger.debug(str(len(invalid_operations))+" Invalid Operations added") self._logger.debug(str(len(invalid_operations))+" Invalid Operations added")
@ -1339,10 +1343,10 @@ class LookupLib(object):
zoneException[const.CQZ] = int(item.text) zoneException[const.CQZ] = int(item.text)
elif item.tag == "start": elif item.tag == "start":
dt = datetime.strptime(item.text[:19], '%Y-%m-%dT%H:%M:%S') dt = datetime.strptime(item.text[:19], '%Y-%m-%dT%H:%M:%S')
zoneException[const.START] = dt.replace(tzinfo=UTC) zoneException[const.START] = dt.replace(tzinfo=timezone.utc)
elif item.tag == "end": elif item.tag == "end":
dt = datetime.strptime(item.text[:19], '%Y-%m-%dT%H:%M:%S') dt = datetime.strptime(item.text[:19], '%Y-%m-%dT%H:%M:%S')
zoneException[const.END] = dt.replace(tzinfo=UTC) zoneException[const.END] = dt.replace(tzinfo=timezone.utc)
zone_exceptions[int(cty_zone_exception.attrib["record"])] = zoneException zone_exceptions[int(cty_zone_exception.attrib["record"])] = zoneException
self._logger.debug(str(len(zone_exceptions))+" Zone Exceptions added") self._logger.debug(str(len(zone_exceptions))+" Zone Exceptions added")
@ -1387,19 +1391,23 @@ class LookupLib(object):
mapping = None mapping = None
with open(country_mapping_filename, "r") as f: with open(country_mapping_filename, "r") as f:
mapping = json.loads(f.read(),encoding='UTF-8') mapping = json.loads(f.read())
cty_list = plistlib.readPlist(cty_file) with open(cty_file, 'rb') as f:
try:
cty_list = plistlib.load(f) #New API (Python >=3.4)
except AttributeError:
cty_list = plistlib.readPlist(cty_file) #Old API (Python >=2.7 && <=3.8)
for item in cty_list: for item in cty_list:
entry = {} entry = {}
call = str(item) call = str(item)
entry[const.COUNTRY] = unicode(cty_list[item]["Country"]) entry[const.COUNTRY] = str(cty_list[item]["Country"])
if mapping: if mapping:
entry[const.ADIF] = int(mapping[cty_list[item]["Country"]]) entry[const.ADIF] = int(mapping[cty_list[item]["Country"]])
entry[const.CQZ] = int(cty_list[item]["CQZone"]) entry[const.CQZ] = int(cty_list[item]["CQZone"])
entry[const.ITUZ] = int(cty_list[item]["ITUZone"]) entry[const.ITUZ] = int(cty_list[item]["ITUZone"])
entry[const.CONTINENT] = unicode(cty_list[item]["Continent"]) entry[const.CONTINENT] = str(cty_list[item]["Continent"])
entry[const.LATITUDE] = float(cty_list[item]["Latitude"]) entry[const.LATITUDE] = float(cty_list[item]["Latitude"])
entry[const.LONGITUDE] = float(cty_list[item]["Longitude"])*(-1) entry[const.LONGITUDE] = float(cty_list[item]["Longitude"])*(-1)
@ -1475,8 +1483,7 @@ class LookupLib(object):
Deserialize a JSON into a dictionary Deserialize a JSON into a dictionary
""" """
my_dict = json.loads(json_data.decode('utf8').replace("'", '"'), my_dict = json.loads(json_data.decode('utf8'))
encoding='UTF-8')
for item in my_dict: for item in my_dict:
if item == const.ADIF: if item == const.ADIF:
@ -1492,17 +1499,17 @@ class LookupLib(object):
elif item == const.LONGITUDE: elif item == const.LONGITUDE:
my_dict[item] = float(my_dict[item]) my_dict[item] = float(my_dict[item])
elif item == const.START: elif item == const.START:
my_dict[item] = datetime.strptime(my_dict[item], '%Y-%m-%d%H:%M:%S').replace(tzinfo=UTC) my_dict[item] = datetime.strptime(my_dict[item], '%Y-%m-%d%H:%M:%S').replace(tzinfo=timezone.utc)
elif item == const.END: elif item == const.END:
my_dict[item] = datetime.strptime(my_dict[item], '%Y-%m-%d%H:%M:%S').replace(tzinfo=UTC) my_dict[item] = datetime.strptime(my_dict[item], '%Y-%m-%d%H:%M:%S').replace(tzinfo=timezone.utc)
elif item == const.WHITELIST_START: elif item == const.WHITELIST_START:
my_dict[item] = datetime.strptime(my_dict[item], '%Y-%m-%d%H:%M:%S').replace(tzinfo=UTC) my_dict[item] = datetime.strptime(my_dict[item], '%Y-%m-%d%H:%M:%S').replace(tzinfo=timezone.utc)
elif item == const.WHITELIST_END: elif item == const.WHITELIST_END:
my_dict[item] = datetime.strptime(my_dict[item], '%Y-%m-%d%H:%M:%S').replace(tzinfo=UTC) my_dict[item] = datetime.strptime(my_dict[item], '%Y-%m-%d%H:%M:%S').replace(tzinfo=timezone.utc)
elif item == const.WHITELIST: elif item == const.WHITELIST:
my_dict[item] = self._str_to_bool(my_dict[item]) my_dict[item] = self._str_to_bool(my_dict[item])
else: else:
my_dict[item] = unicode(my_dict[item]) my_dict[item] = str(my_dict[item])
return my_dict return my_dict

View file

@ -1,4 +1,3 @@
from future.utils import iteritems
from datetime import datetime from datetime import datetime
import re import re
@ -10,7 +9,7 @@ from io import BytesIO
from requests.exceptions import ConnectionError, HTTPError, Timeout from requests.exceptions import ConnectionError, HTTPError, Timeout
def get_lotw_users(**kwargs): def get_lotw_users(**kwargs):
"""Download the latest offical list of `ARRL Logbook of the World (LOTW)`__ users. """Download the latest official list of `ARRL Logbook of the World (LOTW)`__ users.
Args: Args:
url (str, optional): Download URL url (str, optional): Download URL
@ -69,7 +68,7 @@ def get_lotw_users(**kwargs):
return lotw return lotw
def get_clublog_users(**kwargs): def get_clublog_users(**kwargs):
"""Download the latest offical list of `Clublog`__ users. """Download the latest official list of `Clublog`__ users.
Args: Args:
url (str, optional): Download URL url (str, optional): Download URL
@ -94,7 +93,7 @@ def get_clublog_users(**kwargs):
'lastupload': datetime.datetime(2013, 5, 8, 15, 0, 6), 'lastupload': datetime.datetime(2013, 5, 8, 15, 0, 6),
'oqrs': True} 'oqrs': True}
.. _CLUBLOG: https://secure.clublog.org .. _CLUBLOG: https://clublog.org
__ CLUBLOG_ __ CLUBLOG_
""" """
@ -106,7 +105,7 @@ def get_clublog_users(**kwargs):
try: try:
url = kwargs['url'] url = kwargs['url']
except KeyError: except KeyError:
url = "https://secure.clublog.org/clublog-users.json.zip" url = "https://cdn.clublog.org/clublog-users.json.zip"
try: try:
result = requests.get(url) result = requests.get(url)
@ -121,11 +120,11 @@ def get_clublog_users(**kwargs):
files = zip_file.namelist() files = zip_file.namelist()
cl_json_unzipped = zip_file.read(files[0]).decode('utf8').replace("'", '"') cl_json_unzipped = zip_file.read(files[0]).decode('utf8').replace("'", '"')
cl_data = json.loads(cl_json_unzipped, encoding='UTF-8') cl_data = json.loads(cl_json_unzipped)
error_count = 0 error_count = 0
for call, call_data in iteritems(cl_data): for call, call_data in cl_data.items():
try: try:
data = {} data = {}
if "firstqso" in call_data: if "firstqso" in call_data:
@ -150,7 +149,7 @@ def get_clublog_users(**kwargs):
except TypeError: #some date fields contain null instead of a valid datetime string - we ignore them except TypeError: #some date fields contain null instead of a valid datetime string - we ignore them
print("Ignoring invalid type in data:", call, call_data) print("Ignoring invalid type in data:", call, call_data)
pass pass
except ValueError: #some date fiels are invalid. we ignore them for the moment except ValueError: #some date fields are invalid. we ignore them for the moment
print("Ignoring invalid data:", call, call_data) print("Ignoring invalid data:", call, call_data)
pass pass
@ -203,4 +202,4 @@ def get_eqsl_users(**kwargs):
else: else:
raise IOError("HTTP Error: " + str(result.status_code)) raise IOError("HTTP Error: " + str(result.status_code))
return eqsl return eqsl

View file

@ -1,3 +1,3 @@
VERSION = (0, 7, 3) 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,2 +1 @@
sphinx>=1.8.5 sphinx>=1.8.5
sphinxcontrib-napoleon>=0.7

View file

@ -1,4 +1,9 @@
pytest>=3.3.2 pytest>=7.0.0
pytest-blockage>=0.2.2 pytest-blockage>=0.2.2
pytest-localserver>=0.5.0 pytest-localserver>=0.5
pytest-cov>=2.7.1 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,7 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
import sys
import os import os
from distutils.core import setup from setuptools import setup
kw = {} kw = {}
@ -16,11 +15,10 @@ setup(name='pyhamtools',
package_data={'': ['countryfilemapping.json']}, package_data={'': ['countryfilemapping.json']},
packages=['pyhamtools'], packages=['pyhamtools'],
install_requires=[ install_requires=[
"pytz>=2019.1",
"requests>=2.21.0", "requests>=2.21.0",
"pyephem>=3.7.6.0", "ephem>=4.1.3",
"beautifulsoup4>=4.7.1", "beautifulsoup4>=4.7.1",
"future>=0.17.1", "lxml>=5.0.0",
"redis>=2.10.6", "redis>=2.10.6",
], ],
**kw **kw

View file

@ -1,7 +1,7 @@
import pytest import pytest
import tempfile import pkgutil
import json
import os import os
import logging
from pyhamtools import LookupLib from pyhamtools import LookupLib
from pyhamtools import Callinfo from pyhamtools import Callinfo
@ -96,3 +96,7 @@ def fix_redis():
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def fix_qrz(): def fix_qrz():
return LookupLib(lookuptype="qrz", username=QRZ_USERNAME, pwd=QRZ_PWD) return LookupLib(lookuptype="qrz", username=QRZ_USERNAME, pwd=QRZ_PWD)
@pytest.fixture(scope="session")
def fixCountryMapping():
return json.loads(pkgutil.get_data("pyhamtools", "countryfilemapping.json"))

17
test/execfile.py Normal file
View file

@ -0,0 +1,17 @@
# In Python3 the function 'execfile' has been deprecated. The alternative is 'exec'.
# While the package 'past.builtins' provide a python2 / python3 compatible version of 'execfile',
# the import of 'past.builtins' keeps on throwing a deprecation warning about 'imp'.
# Therefore the version of 'execfile' from 'past/builtins' has been replaced by this alternative
# version, found on: https://stackoverflow.com/a/41658338/2292376
# When support of Python2 is finally dropped, this function can be removed
def execfile(filepath, globals=None, locals=None):
if globals is None:
globals = {}
globals.update({
"__file__": filepath,
"__name__": "__main__",
})
with open(filepath, 'rb') as file:
exec(compile(file.read(), filepath, 'exec'), globals, locals)

53981
test/fixtures/cty.plist vendored

File diff suppressed because it is too large Load diff

111216
test/fixtures/cty.xml vendored

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,10 @@
from datetime import datetime # -*- coding: utf-8 -*-
from datetime import datetime, timezone
import pytest import pytest
import pytz
from pyhamtools.consts import LookupConventions as const from pyhamtools.consts import LookupConventions as const
UTC = pytz.UTC
response_prefix_DH_clublog = { response_prefix_DH_clublog = {
'country': 'FEDERAL REPUBLIC OF GERMANY', 'country': 'FEDERAL REPUBLIC OF GERMANY',
'adif': 230, 'adif': 230,
@ -17,6 +14,24 @@ response_prefix_DH_clublog = {
'cqz': 14, 'cqz': 14,
} }
response_prefix_OE_clublog = {
'country': 'AUSTRIA',
'adif': 206,
'continent': 'EU',
'latitude': 47.3,
'longitude': 13.3,
'cqz': 15,
}
response_prefix_RU_clublog = {
'country': 'EUROPEAN RUSSIA',
'adif': 54,
'continent': 'EU',
'latitude': 55.8,
'longitude': 37.6,
'cqz': 16,
}
response_prefix_DH_countryfile = { response_prefix_DH_countryfile = {
'country': 'Fed. Rep. of Germany', 'country': 'Fed. Rep. of Germany',
'adif': 230, 'adif': 230,
@ -75,22 +90,49 @@ 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 = {
u'adif': 390,
u'continent': u'AS',
u'country': u'TURKEY',
u'cqz': 20,
u'latitude': 40.0,
u'longitude': 33.0
}
response_prefix_W2T_clublog = {
u'adif': 291,
u'continent': u'NA',
u'country': u'UNITED STATES OF AMERICA',
u'cqz': 5,
u'latitude': 43.0,
u'longitude': -87.9
}
response_prefix_V26K_clublog = {
u'adif': 94,
u'continent': u'NA',
u'country': u'ANTIGUA & BARBUDA',
u'cqz': 8,
u'latitude': 17.1,
u'longitude': -61.8
} }
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 = {
@ -102,6 +144,24 @@ response_prefix_VK9GMW_clublog = {
u'longitude': 155.8 u'longitude': 155.8
} }
response_prefix_8J1H90T_clublog = {
u'adif': 339,
u'continent': u'AS',
u'country': u'JAPAN',
u'cqz': 25,
u'latitude': 35.7,
u'longitude': 139.8
}
response_prefix_TI5N5BEK_clublog = {
u'adif': 308,
u'continent': u'NA',
u'country': u'COSTA RICA',
u'cqz': 7,
u'latitude': 9.9,
u'longitude': -84.0
}
response_callsign_exceptions_7N1PRD_0_clublog = { response_callsign_exceptions_7N1PRD_0_clublog = {
u'adif': 339, u'adif': 339,
u'continent': u'AS', u'continent': u'AS',
@ -134,18 +194,18 @@ response_Exception_VK9XO_with_start_date = {
'adif': 35, 'adif': 35,
'country': 'CHRISTMAS ISLAND', 'country': 'CHRISTMAS ISLAND',
'continent': 'OC', 'continent': 'OC',
'latitude': -10.50, 'latitude': -10.48,
'longitude': 105.70, 'longitude': 105.62,
'cqz': 29 'cqz': 29
} }
response_zone_exception_dp0gvn = { response_zone_exception_ci8aw = {
'country': 'ANTARCTICA', 'country': 'CANADA',
'adif': 13, 'adif': 1,
'cqz': 38, 'cqz': 1,
'latitude': -65.0, 'latitude': 45.0,
'longitude': -64.0, 'longitude': -80.0,
'continent': 'AN' 'continent': 'NA'
} }
response_lat_long_dh1tw = { response_lat_long_dh1tw = {
@ -230,12 +290,25 @@ class Test_callinfo_methods:
assert fix_callinfo._dismantle_callsign("DH1TW/M") == response_prefix_DH_clublog assert fix_callinfo._dismantle_callsign("DH1TW/M") == response_prefix_DH_clublog
assert fix_callinfo._dismantle_callsign("DH1TW/B")[const.BEACON] assert fix_callinfo._dismantle_callsign("DH1TW/B")[const.BEACON]
assert fix_callinfo._dismantle_callsign("DH1TW") == response_prefix_DH_clublog assert fix_callinfo._dismantle_callsign("DH1TW") == response_prefix_DH_clublog
assert fix_callinfo._dismantle_callsign("DA2X") == response_prefix_DH_clublog
assert fix_callinfo._dismantle_callsign("DN1BU") == response_prefix_DH_clublog
assert fix_callinfo._dismantle_callsign("OE50SPUTNIK") == response_prefix_OE_clublog
assert fix_callinfo._dismantle_callsign("DL60LINDAU") == response_prefix_DH_clublog
assert fix_callinfo._dismantle_callsign("DP75HILDE") == response_prefix_DH_clublog
assert fix_callinfo._dismantle_callsign("DL1640Y") == response_prefix_DH_clublog
assert fix_callinfo._dismantle_callsign("V26K") == response_prefix_V26K_clublog
assert fix_callinfo._dismantle_callsign("W2T") == response_prefix_W2T_clublog
assert fix_callinfo._dismantle_callsign("R2AQH") == response_prefix_RU_clublog
assert fix_callinfo._dismantle_callsign("TA7I") == response_prefix_TA7I_clublog
assert fix_callinfo._dismantle_callsign("DP44N44T") == response_prefix_DH_clublog
assert fix_callinfo._dismantle_callsign("DL/HC2AO") == response_prefix_DH_clublog assert fix_callinfo._dismantle_callsign("DL/HC2AO") == response_prefix_DH_clublog
assert fix_callinfo._dismantle_callsign("9H5A/C6A") == response_prefix_C6A_clublog assert fix_callinfo._dismantle_callsign("9H5A/C6A") == response_prefix_C6A_clublog
assert fix_callinfo._dismantle_callsign("C6A/9H5A") == response_prefix_C6A_clublog assert fix_callinfo._dismantle_callsign("C6A/9H5A") == response_prefix_C6A_clublog
assert fix_callinfo._dismantle_callsign("DH1TW/UNI") == response_prefix_DH_clublog assert fix_callinfo._dismantle_callsign("DH1TW/UNI") == response_prefix_DH_clublog
assert fix_callinfo._dismantle_callsign("DH1TW/BUX") == response_prefix_DH_clublog assert fix_callinfo._dismantle_callsign("DH1TW/BUX") == response_prefix_DH_clublog
assert fix_callinfo._dismantle_callsign("DH1TW/NOT") == response_prefix_DH_clublog assert fix_callinfo._dismantle_callsign("DH1TW/NOT") == response_prefix_DH_clublog
assert fix_callinfo._dismantle_callsign("8J1H90T") == response_prefix_8J1H90T_clublog
assert fix_callinfo._dismantle_callsign("TI5N5BEK") == response_prefix_TI5N5BEK_clublog
assert fix_callinfo._dismantle_callsign("VK9DLX/NOT") == response_prefix_VK9DLX_clublog assert fix_callinfo._dismantle_callsign("VK9DLX/NOT") == response_prefix_VK9DLX_clublog
assert fix_callinfo._dismantle_callsign("7QAA") == response_callsign_exceptions_7QAA_clublog assert fix_callinfo._dismantle_callsign("7QAA") == response_callsign_exceptions_7QAA_clublog
assert fix_callinfo._dismantle_callsign("7N1PRD/0") == response_callsign_exceptions_7N1PRD_0_clublog assert fix_callinfo._dismantle_callsign("7N1PRD/0") == response_callsign_exceptions_7N1PRD_0_clublog
@ -243,6 +316,8 @@ class Test_callinfo_methods:
with pytest.raises(KeyError): with pytest.raises(KeyError):
fix_callinfo._dismantle_callsign("OZ/JO85") fix_callinfo._dismantle_callsign("OZ/JO85")
with pytest.raises(KeyError):
fix_callinfo._dismantle_callsign("DL")
if fix_callinfo._lookuplib._lookuptype == "countryfile": if fix_callinfo._lookuplib._lookuptype == "countryfile":
assert fix_callinfo._dismantle_callsign("DH1TW/QRP") == response_prefix_DH_countryfile assert fix_callinfo._dismantle_callsign("DH1TW/QRP") == response_prefix_DH_countryfile
@ -265,6 +340,15 @@ class Test_callinfo_methods:
fix_callinfo._dismantle_callsign("OZ/JO85") fix_callinfo._dismantle_callsign("OZ/JO85")
def test_dismantle_callsign_with_cyrillic_characters(self, fix_callinfo):
with pytest.raises(KeyError):
fix_callinfo._dismantle_callsign(u"RД3MAS") #cyrillic letter 'Д' in call
with pytest.raises(KeyError):
fix_callinfo._dismantle_callsign(u"RД3/K9MAS") #cyrillic letter 'Д' in prefix
with pytest.raises(KeyError):
fix_callinfo._dismantle_callsign(u"R2EA/М") #cyrillic letter 'M' in appendix
def test_dismantle_callsign_with_VK9_special_suffixes(self, fix_callinfo): def test_dismantle_callsign_with_VK9_special_suffixes(self, fix_callinfo):
if fix_callinfo._lookuplib._lookuptype == "clublog": if fix_callinfo._lookuplib._lookuptype == "clublog":
@ -300,8 +384,8 @@ class Test_callinfo_methods:
if fix_callinfo._lookuplib._lookuptype == "clublogxml" or fix_callinfo._lookuplib._lookuptype == "clublogapi": if fix_callinfo._lookuplib._lookuptype == "clublogxml" or fix_callinfo._lookuplib._lookuptype == "clublogapi":
assert fix_callinfo.get_all("DH1TW") == response_prefix_DH_clublog assert fix_callinfo.get_all("DH1TW") == response_prefix_DH_clublog
assert fix_callinfo.get_all("dp0gvn") == response_zone_exception_dp0gvn assert fix_callinfo.get_all("ci8aw") == response_zone_exception_ci8aw
timestamp = datetime(year=2016, month=1, day=20, tzinfo=UTC) timestamp = datetime(year=2016, month=1, day=20, tzinfo=timezone.utc)
assert fix_callinfo.get_all("VP8STI", timestamp) == response_Exception_VP8STI_with_start_and_stop_date assert fix_callinfo.get_all("VP8STI", timestamp) == response_Exception_VP8STI_with_start_and_stop_date
elif fix_callinfo._lookuplib._lookuptype == "countryfile": elif fix_callinfo._lookuplib._lookuptype == "countryfile":

View file

@ -1,15 +1,9 @@
import os import os
import sys
import datetime
import pytest import pytest
from future.utils import iteritems
from pyhamtools.qsl import get_clublog_users from pyhamtools.qsl import get_clublog_users
if sys.version_info.major == 3:
unicode = str
test_dir = os.path.dirname(os.path.abspath(__file__)) test_dir = os.path.dirname(os.path.abspath(__file__))
fix_dir = os.path.join(test_dir, 'fixtures') fix_dir = os.path.join(test_dir, 'fixtures')
@ -26,8 +20,8 @@ class Test_clublog_methods:
data = get_clublog_users() data = get_clublog_users()
assert isinstance(data, dict) assert isinstance(data, dict)
for key, value in iteritems(data): for key, value in data.items():
assert isinstance(key, unicode) assert isinstance(key, str)
assert isinstance(value, dict) assert isinstance(value, dict)
def test_with_invalid_url(self): def test_with_invalid_url(self):

View file

@ -1,15 +1,9 @@
import pytest import pytest
from datetime import datetime from datetime import datetime, timezone
import pytz
from pyhamtools.consts import LookupConventions as const from pyhamtools.consts import LookupConventions as const
from pyhamtools.dxcluster import decode_char_spot, decode_pc11_message, decode_pc61_message from pyhamtools.dxcluster import decode_char_spot, decode_pc11_message, decode_pc61_message
UTC = pytz.UTC
fix_spot1 = "DX de CT3FW: 21004.8 HC2AO 599 TKS(CW)QSL READ,QRZ.COM 2132Z" fix_spot1 = "DX de CT3FW: 21004.8 HC2AO 599 TKS(CW)QSL READ,QRZ.COM 2132Z"
fix_spot1_broken_spotter_call = "DX de $QRM: 21004.8 HC2AO 599 TKS(CW)QSL READ,QRZ.COM 2132Z" fix_spot1_broken_spotter_call = "DX de $QRM: 21004.8 HC2AO 599 TKS(CW)QSL READ,QRZ.COM 2132Z"
@ -34,7 +28,7 @@ response_spot1 = {
const.BAND: 15, const.BAND: 15,
const.MODE: "CW", const.MODE: "CW",
const.COMMENT: "599 TKS(CW)QSL READ,QRZ.COM", const.COMMENT: "599 TKS(CW)QSL READ,QRZ.COM",
const.TIME: datetime.utcnow().replace( hour=21, minute=32, second=0, microsecond = 0, tzinfo=UTC) const.TIME: datetime.now(timezone.utc).replace(hour=21, minute=32, second=0, microsecond = 0)
} }

View file

@ -1,15 +1,10 @@
from past.builtins import execfile from .execfile import execfile
import os import os
import sys
import datetime
import pytest import pytest
from pyhamtools.qsl import get_eqsl_users from pyhamtools.qsl import get_eqsl_users
if sys.version_info.major == 3:
unicode = str
test_dir = os.path.dirname(os.path.abspath(__file__)) test_dir = os.path.dirname(os.path.abspath(__file__))
fix_dir = os.path.join(test_dir, 'fixtures') fix_dir = os.path.join(test_dir, 'fixtures')
class Test_eqsl_methods: class Test_eqsl_methods:
@ -26,7 +21,7 @@ class Test_eqsl_methods:
data = get_eqsl_users() data = get_eqsl_users()
assert isinstance(data, list) assert isinstance(data, list)
for el in data: for el in data:
assert isinstance(el, unicode) assert isinstance(el, str)
assert len(data) > 1000 assert len(data) > 1000
def test_with_invalid_url(self): def test_with_invalid_url(self):

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):
@ -43,12 +101,30 @@ 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

@ -1,12 +1,9 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
import pytest import pytest
import pytz
from pyhamtools.locator import calculate_sunrise_sunset from pyhamtools.locator import calculate_sunrise_sunset
UTC = pytz.UTC
class Test_calculate_sunrise_sunset_normal_case(): class Test_calculate_sunrise_sunset_normal_case():
def test_calculate_sunrise_sunset(self): def test_calculate_sunrise_sunset(self):
@ -14,28 +11,41 @@ class Test_calculate_sunrise_sunset_normal_case():
time_margin = timedelta(minutes=1) time_margin = timedelta(minutes=1)
locator = "JN48QM" locator = "JN48QM"
test_time = datetime(year=2014, month=1, day=1, tzinfo=UTC) test_time = datetime(year=2014, month=1, day=1, tzinfo=timezone.utc)
result_JN48QM_1_1_2014_evening_dawn = datetime(2014, 1, 1, 15, 38, tzinfo=UTC) result_JN48QM_1_1_2014_evening_dawn = datetime(2014, 1, 1, 15, 38, tzinfo=timezone.utc)
result_JN48QM_1_1_2014_morning_dawn = datetime(2014, 1, 1, 6, 36, tzinfo=UTC) result_JN48QM_1_1_2014_morning_dawn = datetime(2014, 1, 1, 6, 36, tzinfo=timezone.utc)
result_JN48QM_1_1_2014_sunrise = datetime(2014, 1, 1, 7, 14, tzinfo=UTC) result_JN48QM_1_1_2014_sunrise = datetime(2014, 1, 1, 7, 14, tzinfo=timezone.utc)
result_JN48QM_1_1_2014_sunset = datetime(2014, 1, 1, 16, 15, 23, 31016, tzinfo=UTC) result_JN48QM_1_1_2014_sunset = datetime(2014, 1, 1, 16, 15, 23, 31016, tzinfo=timezone.utc)
assert calculate_sunrise_sunset(locator, test_time)['morning_dawn'] - result_JN48QM_1_1_2014_morning_dawn < time_margin assert calculate_sunrise_sunset(locator, test_time)['morning_dawn'] - result_JN48QM_1_1_2014_morning_dawn < time_margin
assert calculate_sunrise_sunset(locator, test_time)['evening_dawn'] - result_JN48QM_1_1_2014_evening_dawn < time_margin assert calculate_sunrise_sunset(locator, test_time)['evening_dawn'] - result_JN48QM_1_1_2014_evening_dawn < time_margin
assert calculate_sunrise_sunset(locator, test_time)['sunset'] - result_JN48QM_1_1_2014_sunset < time_margin assert calculate_sunrise_sunset(locator, test_time)['sunset'] - result_JN48QM_1_1_2014_sunset < time_margin
assert calculate_sunrise_sunset(locator, test_time)['sunrise'] - result_JN48QM_1_1_2014_sunrise < time_margin assert calculate_sunrise_sunset(locator, test_time)['sunrise'] - result_JN48QM_1_1_2014_sunrise < time_margin
def test_calculate_distance_edge_case(self):
def test_calculate_sunrise_sunset_with_sun_never_rising(self):
time_margin = timedelta(minutes=1)
locator = "IQ50PW"
# The sun never rises in winter time close to the north pole (e.g. at Jan Mayen)
# Therefore we expect no sunrise or sunset.
test_time = datetime(year=2021, month=12, day=15, tzinfo=timezone.utc)
assert calculate_sunrise_sunset(locator, test_time)['morning_dawn'] == None
assert calculate_sunrise_sunset(locator, test_time)['evening_dawn'] == None
assert calculate_sunrise_sunset(locator, test_time)['sunset'] == None
assert calculate_sunrise_sunset(locator, test_time)['sunrise'] == None
def test_calculate_sunrise_sunset_with_sun_never_setting(self):
time_margin = timedelta(minutes=1) time_margin = timedelta(minutes=1)
locator = "AA00AA" locator = "AA00AA"
# no sunrise or sunset at southpol during arctic summer # The sun never sets at the south pole during arctic summer
# Therefore we expect no sunrise or sunset.
test_time = datetime(year=2014, month=1, day=1, tzinfo=UTC) test_time = datetime(year=2014, month=1, day=1, tzinfo=timezone.utc)
result_AA00AA_1_1_2014_evening_dawn = datetime(2014, 1, 1, 15, 38, tzinfo=UTC)
result_AA00AA_1_1_2014_morning_dawn = datetime(2014, 1, 1, 6, 36, tzinfo=UTC)
result_AA00AA_1_1_2014_sunrise = datetime(2014, 1, 1, 7, 14, tzinfo=UTC)
result_AA00AA_1_1_2014_sunset = datetime(2014, 1, 1, 16, 15, 23, 31016, tzinfo=UTC)
assert calculate_sunrise_sunset(locator, test_time)['morning_dawn'] == None assert calculate_sunrise_sunset(locator, test_time)['morning_dawn'] == None
assert calculate_sunrise_sunset(locator, test_time)['evening_dawn'] == None assert calculate_sunrise_sunset(locator, test_time)['evening_dawn'] == None

View file

@ -1,13 +1,8 @@
from __future__ import unicode_literals
import pytest import pytest
import sys
from pyhamtools.lookuplib import LookupLib from pyhamtools.lookuplib import LookupLib
from pyhamtools.exceptions import APIKeyMissingError from pyhamtools.exceptions import APIKeyMissingError
if sys.version_info.major == 3:
unicode = str
@pytest.fixture(scope="function", params=[5, -5, "", "foo bar", 11.5, {}, [], None, ("foo", "bar")]) @pytest.fixture(scope="function", params=[5, -5, "", "foo bar", 11.5, {}, [], None, ("foo", "bar")])
def fixAnyValue(request): def fixAnyValue(request):
return request.param return request.param
@ -41,5 +36,5 @@ class TestlookupLibHelper:
with pytest.raises(TypeError): with pytest.raises(TypeError):
fixClublogApi._generate_random_word() fixClublogApi._generate_random_word()
assert type(fixClublogApi._generate_random_word(5)) is unicode assert type(fixClublogApi._generate_random_word(5)) is str
assert len(fixClublogApi._generate_random_word(5)) is 5 assert len(fixClublogApi._generate_random_word(5)) == 5

View file

@ -1,5 +1,5 @@
import pytest import pytest
from datetime import datetime from datetime import datetime, timezone
from pyhamtools.lookuplib import LookupLib from pyhamtools.lookuplib import LookupLib
@ -84,7 +84,7 @@ class TestclublogApi_Getters:
def test_lookup_callsign(self, fixClublogApi): def test_lookup_callsign(self, fixClublogApi):
assert fixClublogApi.lookup_callsign("DH1TW") == response_Exception_DH1TW assert fixClublogApi.lookup_callsign("DH1TW") == response_Exception_DH1TW
assert fixClublogApi.lookup_callsign("VU9KV") == response_Exception_VU9KV assert fixClublogApi.lookup_callsign("VU9KV") == response_Exception_VU9KV
d = datetime.utcnow().replace(year=1971, month=4, day=14) d = datetime.now(timezone.utc).replace(year=1971, month=4, day=14)
assert fixClublogApi.lookup_callsign("VU9KV", d) == response_Exception_VU9KV_with_Date assert fixClublogApi.lookup_callsign("VU9KV", d) == response_Exception_VU9KV_with_Date
assert fixClublogApi.lookup_callsign("DH1TW/MM") == response_Exception_DH1TW_MM assert fixClublogApi.lookup_callsign("DH1TW/MM") == response_Exception_DH1TW_MM
assert fixClublogApi.lookup_callsign("DH1TW/AM") == response_Exception_DH1TW_AM assert fixClublogApi.lookup_callsign("DH1TW/AM") == response_Exception_DH1TW_AM

View file

@ -1,13 +1,10 @@
import pytest import pytest
from datetime import datetime from datetime import datetime, timezone
import pytz
import os import os
from pyhamtools.lookuplib import LookupLib from pyhamtools.lookuplib import LookupLib
from pyhamtools.exceptions import APIKeyMissingError from pyhamtools.exceptions import APIKeyMissingError
UTC = pytz.UTC
#Fixtures #Fixtures
#=========================================================== #===========================================================
@ -28,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,
} }
@ -37,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,
} }
@ -46,8 +43,8 @@ response_Exception_VK9XX_with_end_date = {
'adif': 35, 'adif': 35,
'country': u'CHRISTMAS ISLAND', 'country': u'CHRISTMAS ISLAND',
'continent': u'OC', 'continent': u'OC',
'latitude': -10.40, 'latitude': -10.52,
'longitude': 105.71, 'longitude': 105.54,
'cqz': 29, 'cqz': 29,
} }
@ -55,8 +52,8 @@ response_Exception_VK9XO_with_start_date = {
'adif': 35, 'adif': 35,
'country': u'CHRISTMAS ISLAND', 'country': u'CHRISTMAS ISLAND',
'continent': u'OC', 'continent': u'OC',
'latitude': -10.50, 'latitude': -10.48,
'longitude': 105.70, 'longitude': 105.62,
'cqz': 29, 'cqz': 29,
} }
@ -97,7 +94,7 @@ response_Prefix_VK9_starting_1976 = {
} }
response_Prefix_ZD5_1964_to_1971 = { response_Prefix_ZD5_1964_to_1971 = {
'country': u'SWAZILAND', 'country': u'KINGDOM OF ESWATINI',
'adif' : 468, 'adif' : 468,
'continent': u'AF', 'continent': u'AF',
'latitude': -26.30, 'latitude': -26.30,
@ -119,23 +116,6 @@ def fix_cty_xml_file(request):
#TESTS #TESTS
#=========================================================== #===========================================================
class TestClublogXML_Constructor:
def test_with_invalid_api_key(self):
with pytest.raises(APIKeyMissingError):
lib = LookupLib(lookuptype="clublogxml", apikey="foo")
lib.lookup_entity(230)
def test_with_no_api_key(self):
with pytest.raises(APIKeyMissingError):
lib = LookupLib(lookuptype="clublogxml")
lib.lookup_entity(230)
def test_with_file(self, fix_cty_xml_file):
lib = LookupLib(lookuptype="clublogxml", filename=fix_cty_xml_file)
assert lib.lookup_entity(230) == response_Entity_230
class TestclublogXML_Getters: class TestclublogXML_Getters:
#lookup_entity(callsign) #lookup_entity(callsign)
@ -159,40 +139,40 @@ class TestclublogXML_Getters:
#=============================== #===============================
def test_lookup_callsign_same_callsign_different_exceptions(self, fixClublogXML): def test_lookup_callsign_same_callsign_different_exceptions(self, fixClublogXML):
timestamp = datetime(year=1990, month=10, day=12, tzinfo=UTC) timestamp = datetime(year=1990, month=10, day=12, tzinfo=timezone.utc)
assert fixClublogXML.lookup_callsign("kc6mm", timestamp) == response_Exception_KC6MM_1990 assert fixClublogXML.lookup_callsign("kc6mm", timestamp) == response_Exception_KC6MM_1990
timestamp = datetime(year=1992, month=3, day=8, tzinfo=UTC) timestamp = datetime(year=1992, month=3, day=8, tzinfo=timezone.utc)
assert fixClublogXML.lookup_callsign("kc6mm", timestamp) == response_Exception_KC6MM_1992 assert fixClublogXML.lookup_callsign("kc6mm", timestamp) == response_Exception_KC6MM_1992
def test_lookup_callsign_exception_only_with_start_date(self, fixClublogXML): def test_lookup_callsign_exception_only_with_start_date(self, fixClublogXML):
#timestamp > startdate #timestamp > startdate
timestamp = datetime(year=1962, month=7, day=7, tzinfo=UTC) timestamp = datetime(year=1962, month=7, day=7, tzinfo=timezone.utc)
assert fixClublogXML.lookup_callsign("vk9xo", timestamp) == response_Exception_VK9XO_with_start_date assert fixClublogXML.lookup_callsign("vk9xo", timestamp) == response_Exception_VK9XO_with_start_date
assert fixClublogXML.lookup_callsign("vk9xo") == response_Exception_VK9XO_with_start_date assert fixClublogXML.lookup_callsign("vk9xo") == response_Exception_VK9XO_with_start_date
#timestamp < startdate #timestamp < startdate
timestamp = datetime(year=1962, month=7, day=5, tzinfo=UTC) timestamp = datetime(year=1962, month=7, day=5, tzinfo=timezone.utc)
with pytest.raises(KeyError): with pytest.raises(KeyError):
fixClublogXML.lookup_callsign("vk9xo", timestamp) fixClublogXML.lookup_callsign("vk9xo", timestamp)
def test_lookup_callsign_exception_only_with_end_date(self, fixClublogXML): def test_lookup_callsign_exception_only_with_end_date(self, fixClublogXML):
#timestamp < enddate #timestamp < enddate
timestamp = datetime(year=1975, month=9, day=14, tzinfo=UTC) timestamp = datetime(year=1975, month=9, day=14, tzinfo=timezone.utc)
assert fixClublogXML.lookup_callsign("vk9xx", timestamp) == response_Exception_VK9XX_with_end_date assert fixClublogXML.lookup_callsign("vk9xx", timestamp) == response_Exception_VK9XX_with_end_date
# timestamp > enddate # timestamp > enddate
with pytest.raises(KeyError): with pytest.raises(KeyError):
fixClublogXML.lookup_callsign("vk9xx") fixClublogXML.lookup_callsign("vk9xx")
timestamp = datetime(year=1975, month=9, day=16, tzinfo=UTC) timestamp = datetime(year=1975, month=9, day=16, tzinfo=timezone.utc)
with pytest.raises(KeyError): with pytest.raises(KeyError):
fixClublogXML.lookup_callsign("vk9xx", timestamp) fixClublogXML.lookup_callsign("vk9xx", timestamp)
def test_lookup_callsign_exception_no_start_nor_end_date(self, fixClublogXML): def test_lookup_callsign_exception_no_start_nor_end_date(self, fixClublogXML):
timestamp = datetime(year=1975, month=9, day=14, tzinfo=UTC) timestamp = datetime(year=1975, month=9, day=14, tzinfo=timezone.utc)
assert fixClublogXML.lookup_callsign("ax9nyg", timestamp) == response_Exception_AX9NYG assert fixClublogXML.lookup_callsign("ax9nyg", timestamp) == response_Exception_AX9NYG
assert fixClublogXML.lookup_callsign("ax9nyg" ) == response_Exception_AX9NYG assert fixClublogXML.lookup_callsign("ax9nyg" ) == response_Exception_AX9NYG
@ -213,29 +193,29 @@ class TestclublogXML_Getters:
def test_lookup_prefix_with_changing_entities(self, fixClublogXML): def test_lookup_prefix_with_changing_entities(self, fixClublogXML):
#return old entity (PAPUA TERR) #return old entity (PAPUA TERR)
timestamp = datetime(year=1975, month=9, day=14).replace(tzinfo=UTC) timestamp = datetime(year=1975, month=9, day=14, tzinfo=timezone.utc)
assert fixClublogXML.lookup_prefix("VK9", timestamp) == response_Prefix_VK9_until_1975 assert fixClublogXML.lookup_prefix("VK9", timestamp) == response_Prefix_VK9_until_1975
#return empty dict - Prefix was not assigned at that time #return empty dict - Prefix was not assigned at that time
timestamp = datetime(year=1975, month=9, day=16).replace(tzinfo=UTC) timestamp = datetime(year=1975, month=9, day=16, tzinfo=timezone.utc)
with pytest.raises(KeyError): with pytest.raises(KeyError):
fixClublogXML.lookup_prefix("VK9", timestamp) fixClublogXML.lookup_prefix("VK9", timestamp)
#return new entity (Norfolk Island) #return new entity (Norfolk Island)
timestamp = datetime.utcnow().replace(tzinfo=UTC) timestamp = datetime.now(timezone.utc)
assert fixClublogXML.lookup_prefix("VK9", timestamp ) == response_Prefix_VK9_starting_1976 assert fixClublogXML.lookup_prefix("VK9", timestamp ) == response_Prefix_VK9_starting_1976
def test_lookup_prefix_with_entities_having_start_and_stop(self, fixClublogXML): def test_lookup_prefix_with_entities_having_start_and_stop(self, fixClublogXML):
timestamp_before = datetime(year=1964, month=11, day=1).replace(tzinfo=UTC) timestamp_before = datetime(year=1964, month=11, day=1, tzinfo=timezone.utc)
with pytest.raises(KeyError): with pytest.raises(KeyError):
fixClublogXML.lookup_prefix("ZD5", timestamp_before) fixClublogXML.lookup_prefix("ZD5", timestamp_before)
timestamp_valid = datetime(year=1964, month=12, day=2).replace(tzinfo=UTC) timestamp_valid = datetime(year=1964, month=12, day=2, tzinfo=timezone.utc)
assert fixClublogXML.lookup_prefix("ZD5", timestamp_valid) == response_Prefix_ZD5_1964_to_1971 assert fixClublogXML.lookup_prefix("ZD5", timestamp_valid) == response_Prefix_ZD5_1964_to_1971
timestamp_after = datetime(year=1971, month=8, day=1).replace(tzinfo=UTC) timestamp_after = datetime(year=1971, month=8, day=1, tzinfo=timezone.utc)
with pytest.raises(KeyError): with pytest.raises(KeyError):
fixClublogXML.lookup_prefix("ZD5", timestamp_after) fixClublogXML.lookup_prefix("ZD5", timestamp_after)
@ -251,8 +231,8 @@ class TestclublogXML_Getters:
fixClublogXML.is_invalid_operation("dh1tw") fixClublogXML.is_invalid_operation("dh1tw")
#Invalid Operation with start and end date #Invalid Operation with start and end date
timestamp_before = datetime(year=1993, month=12, day=30).replace(tzinfo=UTC) timestamp_before = datetime(year=1993, month=12, day=30, tzinfo=timezone.utc)
timestamp = datetime(year=1994, month=12, day=30).replace(tzinfo=UTC) timestamp = datetime(year=1994, month=12, day=30, tzinfo=timezone.utc)
with pytest.raises(KeyError): with pytest.raises(KeyError):
fixClublogXML.is_invalid_operation("vk0mc") fixClublogXML.is_invalid_operation("vk0mc")
@ -263,18 +243,10 @@ class TestclublogXML_Getters:
#Invalid Operation with start date #Invalid Operation with start date
assert fixClublogXML.is_invalid_operation("5W1CFN") assert fixClublogXML.is_invalid_operation("5W1CFN")
timestamp_before = datetime(year=2012, month=1, day=31).replace(tzinfo=UTC) timestamp_before = datetime(year=2012, month=1, day=31, tzinfo=timezone.utc)
with pytest.raises(KeyError): with pytest.raises(KeyError):
fixClublogXML.is_invalid_operation("5W1CFN", timestamp_before) fixClublogXML.is_invalid_operation("5W1CFN", timestamp_before)
#Invalid Operation with end date
timestamp_before = datetime(year=2004, month=4, day=2).replace(tzinfo=UTC)
with pytest.raises(KeyError):
fixClublogXML.is_invalid_operation("T33C")
assert fixClublogXML.is_invalid_operation("T33C", timestamp_before)
#lookup_zone_exception(callsign, [date]) #lookup_zone_exception(callsign, [date])
#==================================== #====================================
@ -289,9 +261,9 @@ class TestclublogXML_Getters:
assert fixClublogXML.lookup_zone_exception("dp0gvn") == 38 assert fixClublogXML.lookup_zone_exception("dp0gvn") == 38
#zone exception with start and end date #zone exception with start and end date
timestamp = datetime(year=1992, month=10, day=2).replace(tzinfo=UTC) timestamp = datetime(year=1992, month=10, day=2, tzinfo=timezone.utc)
timestamp_before = datetime(year=1992, month=9, day=30).replace(tzinfo=UTC) timestamp_before = datetime(year=1992, month=9, day=30, tzinfo=timezone.utc)
timestamp_after = datetime(year=1993, month=3, day=1).replace(tzinfo=UTC) timestamp_after = datetime(year=1993, month=3, day=1, tzinfo=timezone.utc)
assert fixClublogXML.lookup_zone_exception("dl1kvc/p", timestamp) == 38 assert fixClublogXML.lookup_zone_exception("dl1kvc/p", timestamp) == 38
with pytest.raises(KeyError): with pytest.raises(KeyError):
@ -301,6 +273,6 @@ class TestclublogXML_Getters:
fixClublogXML.lookup_zone_exception("dl1kvc/p", timestamp_after) fixClublogXML.lookup_zone_exception("dl1kvc/p", timestamp_after)
#zone exception with start date #zone exception with start date
timestamp_before = datetime(year=2013, month=12, day=26).replace(tzinfo=UTC) timestamp_before = datetime(year=2013, month=12, day=26,tzinfo=timezone.utc)
with pytest.raises(KeyError): with pytest.raises(KeyError):
fixClublogXML.lookup_zone_exception("dh1hb/p", timestamp_before) fixClublogXML.lookup_zone_exception("dh1hb/p", timestamp_before)

View file

@ -1,11 +1,4 @@
from __future__ import unicode_literals
import pytest import pytest
import tempfile
import os
import sys
if sys.version_info.major == 3:
unicode = str
from datetime import datetime from datetime import datetime
@ -58,13 +51,13 @@ class Test_Getter_Setter_Api_Types_for_all_sources:
count = 0 count = 0
for attr in entity: for attr in entity:
if attr == "country": if attr == "country":
assert type(entity[attr] is unicode) assert type(entity[attr] is str)
count +=1 count +=1
if attr == "continent": if attr == "continent":
assert type(entity[attr] is unicode) assert type(entity[attr] is str)
count +=1 count +=1
if attr == "prefix": if attr == "prefix":
assert type(entity[attr] is unicode) assert type(entity[attr] is str)
count +=1 count +=1
if attr == "deleted": if attr == "deleted":
assert type(entity[attr] is bool) assert type(entity[attr] is bool)
@ -114,10 +107,10 @@ class Test_Getter_Setter_Api_Types_for_all_sources:
assert type(ex[attr]) is float assert type(ex[attr]) is float
count +=1 count +=1
elif attr == "country": elif attr == "country":
assert type(ex[attr]) is unicode assert type(ex[attr]) is str
count +=1 count +=1
elif attr == "continent": elif attr == "continent":
assert type(ex[attr]) is unicode assert type(ex[attr]) is str
count +=1 count +=1
elif attr == "cqz": elif attr == "cqz":
assert type(ex[attr]) is int assert type(ex[attr]) is int
@ -150,7 +143,7 @@ class Test_Getter_Setter_Api_Types_for_all_sources:
count = 0 count = 0
for attr in prefix: for attr in prefix:
if attr == "country": if attr == "country":
assert type(prefix[attr]) is unicode assert type(prefix[attr]) is str
count +=1 count +=1
elif attr == "adif": elif attr == "adif":
assert type(prefix[attr]) is int assert type(prefix[attr]) is int
@ -162,7 +155,7 @@ class Test_Getter_Setter_Api_Types_for_all_sources:
assert type(prefix[attr]) is int assert type(prefix[attr]) is int
count +=1 count +=1
elif attr == "continent": elif attr == "continent":
assert type(prefix[attr]) is unicode assert type(prefix[attr]) is str
count +=1 count +=1
elif attr == "latitude": elif attr == "latitude":
assert type(prefix[attr]) is float assert type(prefix[attr]) is float

View file

@ -1,16 +1,12 @@
import os import os
import pytest import pytest
from datetime import datetime from datetime import datetime, timezone
from pyhamtools.lookuplib import LookupLib from pyhamtools.lookuplib import LookupLib
from pyhamtools.exceptions import APIKeyMissingError from pyhamtools.exceptions import APIKeyMissingError
from pyhamtools.consts import LookupConventions as const from pyhamtools.consts import LookupConventions as const
import pytz
UTC = pytz.UTC
try: try:
QRZ_USERNAME = str(os.environ['QRZ_USERNAME']) QRZ_USERNAME = str(os.environ['QRZ_USERNAME'])
QRZ_PWD = str(os.environ['QRZ_PWD']) QRZ_PWD = str(os.environ['QRZ_PWD'])
@ -21,10 +17,10 @@ except Exception:
#=========================================================== #===========================================================
response_1A1AB = { response_1A1AB = {
u'biodate': datetime(2018, 9, 7, 21, 17, 7, tzinfo=UTC), u'biodate': datetime(2018, 9, 7, 21, 17, 7, tzinfo=timezone.utc),
u'bio': u'0', u'bio': u'0',
u'license_class': u'C', u'license_class': u'C',
u'moddate': datetime(2008, 11, 2, 15, 0, 38, tzinfo=UTC), u'moddate': datetime(2008, 11, 2, 15, 0, 38, tzinfo=timezone.utc),
u'locator': u'JN61fw', u'locator': u'JN61fw',
u'callsign': u'1A1AB', u'callsign': u'1A1AB',
u'addr2': u'00187 Rome', u'addr2': u'00187 Rome',

View file

@ -1,15 +1,11 @@
import pytest import pytest
import json import json
from datetime import datetime from datetime import datetime, timezone
import pytz
import redis import redis
from pyhamtools import LookupLib, Callinfo from pyhamtools import LookupLib, Callinfo
UTC = pytz.UTC
r = redis.Redis() r = redis.Redis()
@ -22,6 +18,14 @@ response_Exception_VP8STI_with_start_and_stop_date = {
'cqz': 13, 'cqz': 13,
} }
response_TU5PCT = {
'adif': 428,
'country': u"COTE D'IVOIRE",
'continent': u'AF',
'latitude': 5.3,
'longitude': -4.0,
'cqz': 35,
}
class TestStoreDataInRedis: class TestStoreDataInRedis:
@ -32,10 +36,11 @@ class TestStoreDataInRedis:
assert fix_redis.lookup_callsign("VK9XO") == fixClublogXML.lookup_callsign("VK9XO") assert fix_redis.lookup_callsign("VK9XO") == fixClublogXML.lookup_callsign("VK9XO")
assert fix_redis.lookup_prefix("DH") == fixClublogXML.lookup_prefix("DH") assert fix_redis.lookup_prefix("DH") == fixClublogXML.lookup_prefix("DH")
with pytest.raises(KeyError): with pytest.raises(KeyError):
fix_redis.is_invalid_operation("VK0MC") fix_redis.is_invalid_operation("VK0MC")
timestamp = datetime(year=1994, month=12, day=30).replace(tzinfo=UTC) timestamp = datetime(year=1994, month=12, day=30, tzinfo=timezone.utc)
assert fix_redis.is_invalid_operation("VK0MC", timestamp) assert fix_redis.is_invalid_operation("VK0MC", timestamp)
with pytest.raises(KeyError): with pytest.raises(KeyError):
@ -52,6 +57,7 @@ class TestStoreDataInRedis:
assert lib.lookup_prefix("DH") == fixCountryFile.lookup_prefix("DH") assert lib.lookup_prefix("DH") == fixCountryFile.lookup_prefix("DH")
def test_redis_lookup(self, fixClublogXML, fix_redis): def test_redis_lookup(self, fixClublogXML, fix_redis):
timestamp = datetime(year=2016, month=1, day=20, tzinfo=UTC) timestamp = datetime(year=2016, month=1, day=20, tzinfo=timezone.utc)
ci = Callinfo(fix_redis) ci = Callinfo(fix_redis)
assert ci.get_all("VP8STI", timestamp) == response_Exception_VP8STI_with_start_and_stop_date assert ci.get_all("VP8STI", timestamp) == response_Exception_VP8STI_with_start_and_stop_date
assert ci.get_all("tu5pct") == response_TU5PCT

View file

@ -1,15 +1,21 @@
import os import os
import sys
import datetime import datetime
from past.builtins import execfile from .execfile import execfile
from future.utils import iteritems
import pytest import pytest
from pyhamtools.qsl import get_lotw_users def execfile(filepath, globals=None, locals=None):
if globals is None:
globals = {}
globals.update({
"__file__": filepath,
"__name__": "__main__",
})
with open(filepath, 'rb') as file:
exec(compile(file.read(), filepath, 'exec'), globals, locals)
if sys.version_info.major == 3:
unicode = str from pyhamtools.qsl import get_lotw_users
test_dir = os.path.dirname(os.path.abspath(__file__)) test_dir = os.path.dirname(os.path.abspath(__file__))
fix_dir = os.path.join(test_dir, 'fixtures') fix_dir = os.path.join(test_dir, 'fixtures')
@ -23,12 +29,13 @@ 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()
assert isinstance(data, dict) assert isinstance(data, dict)
for key, value in iteritems(data): for key, value in data.items():
assert isinstance(key, unicode) assert isinstance(key, str)
assert isinstance(value, datetime.datetime ) assert isinstance(value, datetime.datetime )
assert len(data) > 1000 assert len(data) > 1000

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}