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
build/
dist/
settings.json
apikeysrc
coverage*
.coverage
.python-version
.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)
Copyright (c) 2014 Tobias Wellnitz
Copyright (c) 2025 Tobias Wellnitz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -1,29 +1,28 @@
# pyhamtools
[![Build Status](https://travis-ci.org/dh1tw/pyhamtools.svg?branch=master)](https://travis-ci.org/dh1tw/pyhamtools)
[![Build status](https://ci.appveyor.com/api/projects/status/8rfgr7x6w1arixrh?svg=true)](https://ci.appveyor.com/project/dh1tw/pyhamtools)
![Build Status](https://github.com/dh1tw/pyhamtools/actions/workflows/test.yml/badge.svg)
[![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)
Pyhamtools is a set of functions and classes for Amateur Radio purpose.
Currently the core part is the Callsign Lookup which decodes any amateur radio
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
callsign string and provides the corresponding information (Country, DXCC
entity, CQ Zone...etc). This basic functionality is needed for Logbooks,
DX-Clusters or Log Checking. This and additional convenience features are
provided for the following sources:
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 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)
* [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)
* [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,
heading between Maidenhead locators) or frequency based calculations
Other modules include location-based calculations (e.g. distance,
heading between Maidenhead locators) or frequency-based calculations
(e.g. frequency to band).
## References
@ -32,15 +31,24 @@ This Library is used in production at the [DXHeat.com DX Cluster](https://dxheat
## Compatibility
Pyhamtools is since version 0.6.0 compatible with > Python 2.7 and > python 3.3.
We check compatibility on OSX, Windows and Linux with the following Python
versions:
Pyhamtools is compatible with Python >=3.6.
We check compatibility on OSX, Windows, and Linux with the following Python versions:
* Python 2.7
* Python 3.4
* Python 3.5
* Python 3.6
* [pypy](https://pypy.org/) (Python 2)
* Python 3.8
* Python 3.9
* Python 3.10
* Python 3.11
* 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
@ -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
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
Easiest way to install pyhamtools is through the packet manager `pip`:
The easiest way to install pyhamtools is through the packet manager `pip`:
```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
``` python
@ -90,11 +122,11 @@ $ pip install pyhamtools
## Testing
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).
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
@ -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:
```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
store, a redis server has to be up & running.
To perform the tests related to the [redis](https://redis.io/) key/value
store, a Redis server has to be up & running.
```bash

View file

@ -1,6 +1,185 @@
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
================
@ -126,7 +305,7 @@ PyHamTools 0.5.0
* 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]

View file

@ -12,19 +12,8 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys
import os
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 ------------------------------------------------
# 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.
extensions = [
'sphinx.ext.autodoc',
'sphinxcontrib.napoleon',
'sphinx.ext.napoleon',
'sphinx_rtd_dark_mode',
]
# Add any paths that contain templates here, relative to this directory.
@ -52,7 +42,7 @@ master_doc = 'index'
# General information about the project.
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
# |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
# a list of builtin themes.
html_theme = 'default'
# html_theme = 'default'
html_theme = 'sphinx_rtd_theme'
# html_theme = 'sphinx_material'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the

View file

@ -25,7 +25,7 @@ Calculate Distance between two WGS84 Coordinates
>>> from pyhamtools.locator import calculate_distance, latlong_to_locator
>>> locator1 = latlong_to_locator(48.52, 9.375)
>>> locator2 = latlong_to_locator(-32.77, 152.125)
>>> distance = calculate_heading(locator1, locator2)
>>> distance = calculate_distance(locator1, locator2)
>>> print("%.1fkm" % distance)
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.
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
@ -57,7 +57,7 @@ First we need to instanciate a LookupLib object for Country-files.com database.
>>> 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

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
calculations per day.
.. _Clublog.org: https://secure.clublog.org/
.. _Clublog.org: https://clublog.org/
.. _Country-Files.com: http://www.country-files.com/
.. _QRZ.com: http://qrz.com
.. _eQSL: http://eqsl.cc

View file

@ -1,23 +1,11 @@
import re
import logging
from datetime import datetime
import sys
import pytz
from datetime import datetime, timezone
from pyhamtools.consts import LookupConventions as const
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):
"""
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
else:
self._logger = logging.getLogger(__name__)
if sys.version_info[:2] == (2, 6):
self._logger.addHandler(NullHandler())
else:
self._logger.addHandler(logging.NullHandler())
self._logger.addHandler(logging.NullHandler())
self._lookuplib = lookuplib
self._callsign_info = None
@ -71,19 +56,21 @@ class Callinfo(object):
"""
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:
homecall = homecall.group(0)
return homecall
else:
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"""
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 timestamp > datetime(2006,1,1, tzinfo=UTC):
if timestamp > datetime(2006,1,1, tzinfo=timezone.utc):
prefix = callsign[0:3]+callsign[4:5]
while len(prefix) > 0:
@ -109,12 +96,12 @@ class Callinfo(object):
check = callsign[-4:].upper()
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:
Args:
callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC)
timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc)
Raises:
KeyError: Callsign could not be identified
@ -122,11 +109,13 @@ class Callinfo(object):
"""
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
callsign = re.sub('\-\d{1,3}$', '', entire_callsign)
if re.search('\\-\\d{1,3}$', entire_callsign): # cut off any -10 / -02 appendixes
callsign = re.sub('\\-\\d{1,3}$', '', entire_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
@ -189,20 +178,21 @@ class Callinfo(object):
data[const.BEACON] = True
return data
elif re.search('\d$', appendix):
area_nr = re.search('\d$', appendix).group(0)
callsign = re.sub('/\d$', '', callsign) #remove /number
if len(re.findall(r'\d+', callsign)) == 1: #call has just on digit e.g. DH1TW
callsign = re.sub('[\d]+', area_nr, callsign)
elif re.search('\\d$', appendix):
area_nr = re.search('\\d$', appendix).group(0)
callsign = re.sub('/\\d$', '', callsign) #remove /number
if len(re.findall(r'\\d+', callsign)) == 1: #call has just on digit e.g. DH1TW
callsign = re.sub('[\\d]+', area_nr, callsign)
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)
else:
return self._iterate_prefix(callsign, timestamp)
# 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)
# callsigns with prefixes (xxx/callsign)
@ -211,8 +201,11 @@ class Callinfo(object):
pfx = re.sub('/', '', pfx.group(0))
#make sure that the remaining part is actually a callsign (avoid: OZ/JO81)
rest = re.search('/[A-Z0-9]+', entire_callsign)
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):
if rest is None:
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)
if entire_callsign in callsign_exceptions:
@ -221,7 +214,9 @@ class Callinfo(object):
self._logger.debug("Could not decode " + callsign)
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
invalid = False
@ -264,12 +259,12 @@ class Callinfo(object):
# Dismantel the callsign and check if the prefix is known
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
Args:
callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC)
timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc)
Returns:
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`.
"""
callsign = callsign.upper()
if timestamp is None:
timestamp = datetime.now(timezone.utc)
callsign_data = self._lookup_callsign(callsign, timestamp)
try:
@ -312,12 +313,12 @@ class Callinfo(object):
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
Args:
callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC)
timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc)
Returns:
bool: True / False
@ -332,18 +333,21 @@ class Callinfo(object):
True
"""
if timestamp is None:
timestamp = datetime.now(timezone.utc)
try:
if self.get_all(callsign, timestamp):
return True
except KeyError:
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
Args:
callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC)
timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc)
Returns:
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.
"""
if timestamp is None:
timestamp = datetime.now(timezone.utc)
callsign_data = self.get_all(callsign, timestamp=timestamp)
return {
const.LATITUDE: callsign_data[const.LATITUDE],
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
Args:
callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC)
timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc)
Returns:
int: containing the callsign's CQ Zone
@ -389,14 +396,17 @@ class Callinfo(object):
KeyError: no CQ Zone found for callsign
"""
if timestamp is None:
timestamp = datetime.now(timezone.utc)
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
Args:
callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC)
timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc)
Returns:
int: containing the callsign's CQ Zone
@ -408,14 +418,17 @@ class Callinfo(object):
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]
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
Args:
callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC)
timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc)
Returns:
str: name of the Country
@ -432,14 +445,17 @@ class Callinfo(object):
- Clublog: "FEDERAL REPUBLIC OF GERMANY"
"""
if timestamp is None:
timestamp = datetime.now(timezone.utc)
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
Args:
callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC)
timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc)
Returns:
int: containing the country ADIF id
@ -448,14 +464,17 @@ class Callinfo(object):
KeyError: No Country found for callsign
"""
if timestamp is None:
timestamp = datetime.now(timezone.utc)
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
Args:
callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC)
timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc)
Returns:
str: continent identified
@ -474,4 +493,7 @@ class Callinfo(object):
- OC: Oceania
- AN: Antarctica
"""
if timestamp is None:
timestamp = datetime.now(timezone.utc)
return self.get_all(callsign, timestamp)[const.CONTINENT]

View file

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

View file

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

View file

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

View file

@ -1,18 +1,15 @@
from __future__ import division
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
UTC = pytz.UTC
def latlong_to_locator (latitude, longitude):
def latlong_to_locator (latitude, longitude, precision=6):
"""converts WGS84 coordinates into the corresponding Maidenhead Locator
Args:
latitude (float): Latitude
longitude (float): Longitude
precision (int): 4,6,8,10 chars (default 6)
Returns:
string: Maidenhead locator
@ -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:
raise ValueError
if latitude >= 90 or latitude <= -90:
raise ValueError
longitude += 180;
latitude +=90;
longitude +=180
latitude +=90
locator = chr(ord('A') + int(longitude / 20))
locator += chr(ord('A') + int(latitude / 10))
locator += chr(ord('0') + int((longitude % 20) / 2))
locator += chr(ord('0') + int(latitude % 10))
locator += chr(ord('A') + int((longitude - int(longitude / 2) * 2) / (2 / 24)))
locator += chr(ord('A') + int((latitude - int(latitude / 1) * 1 ) / (1 / 24)))
# copied & adapted from github.com/space-physics/maidenhead
A = ord('A')
a = divmod(longitude, 20)
b = divmod(latitude, 10)
locator = chr(A + int(a[0])) + chr(A + int(b[0]))
lon = a[1] / 2.0
lat = b[1]
i = 1
while i < precision/2:
i += 1
a = divmod(lon, 1)
b = divmod(lat, 1)
if not (i % 2):
locator += str(int(a[0])) + str(int(b[0]))
lon = 24 * a[1]
lat = 24 * b[1]
else:
locator += chr(A + int(a[0])) + chr(A + int(b[0]))
lon = 10 * a[1]
lat = 10 * b[1]
return locator
def locator_to_latlong (locator):
def locator_to_latlong (locator, center=True):
"""converts Maidenhead locator in the corresponding WGS84 coordinates
Args:
locator (string): Locator, either 4 or 6 characters
locator (string): Locator, either 4, 6 or 8 characters
center (bool): Center of (sub)square. By default True. If False, the south/western corner will be returned
Returns:
tuple (float, float): Latitude, Longitude
Raises:
ValueError: When called with wrong or invalid input arg
ValueError: When called with wrong or invalid Maidenhead locator string
TypeError: When arg is not a string
Example:
@ -83,7 +99,7 @@ def locator_to_latlong (locator):
locator = locator.upper()
if len(locator) == 5 or len(locator) < 4:
if len(locator) < 4 or len(locator) == 5 or len(locator) == 7 or len(locator) == 9:
raise ValueError
if ord(locator[0]) > ord('R') or ord(locator[0]) < ord('A'):
@ -104,23 +120,64 @@ def locator_to_latlong (locator):
if ord (locator[5]) > ord('X') or ord(locator[5]) < ord('A'):
raise ValueError
if len(locator) == 8:
if ord(locator[6]) > ord('9') or ord(locator[6]) < ord('0'):
raise ValueError
if ord (locator[7]) > ord('9') or ord(locator[7]) < ord('0'):
raise ValueError
if len(locator) == 10:
if ord(locator[8]) > ord('X') or ord(locator[8]) < ord('A'):
raise ValueError
if ord (locator[9]) > ord('X') or ord(locator[9]) < ord('A'):
raise ValueError
longitude = (ord(locator[0]) - ord('A')) * 20 - 180
latitude = (ord(locator[1]) - ord('A')) * 10 - 90
longitude += (ord(locator[2]) - ord('0')) * 2
latitude += (ord(locator[3]) - ord('0'))
latitude += (ord(locator[3]) - ord('0')) * 1
if len(locator) == 6:
longitude += ((ord(locator[4])) - ord('A')) * (2 / 24)
latitude += ((ord(locator[5])) - ord('A')) * (1 / 24)
if len(locator) == 4:
# move to center of subsquare
longitude += 1 / 24
latitude += 0.5 / 24
if center:
longitude += 2 / 2
latitude += 1.0 / 2
elif len(locator) == 6:
longitude += (ord(locator[4]) - ord('A')) * 5.0 / 60
latitude += (ord(locator[5]) - ord('A')) * 2.5 / 60
if center:
longitude += 5.0 / 60 / 2
latitude += 2.5 / 60 / 2
elif len(locator) == 8:
longitude += (ord(locator[4]) - ord('A')) * 5.0 / 60
latitude += (ord(locator[5]) - ord('A')) * 2.5 / 60
longitude += int(locator[6]) * 5.0 / 600
latitude += int(locator[7]) * 2.5 / 600
if center:
longitude += 5.0 / 600 / 2
latitude += 2.5 / 600 / 2
elif len(locator) == 10:
longitude += (ord(locator[4]) - ord('A')) * 5.0 / 60
latitude += (ord(locator[5]) - ord('A')) * 2.5 / 60
longitude += int(locator[6]) * 5.0 / 600
latitude += int(locator[7]) * 2.5 / 600
longitude += (ord(locator[8]) - ord('A')) * 1.0 / 2880
latitude += (ord(locator[9]) - ord('A')) * 1.0 / 5760
if center:
longitude += 1.0 / 2880 / 2
latitude += 1.0 / 5760 / 2
else:
# move to center of square
longitude += 1;
latitude += 0.5;
raise ValueError
return latitude, longitude
@ -129,14 +186,14 @@ def calculate_distance(locator1, locator2):
"""calculates the (shortpath) distance between two Maidenhead locators
Args:
locator1 (string): Locator, either 4 or 6 characters
locator2 (string): Locator, either 4 or 6 characters
locator1 (string): Locator, either 4, 6 or 8 characters
locator2 (string): Locator, either 4, 6 or 8 characters
Returns:
float: Distance in km
Raises:
ValueError: When called with wrong or invalid input arg
ValueError: When called with wrong or invalid maidenhead locator strings
AttributeError: When args are not a string
Example:
@ -146,6 +203,9 @@ def calculate_distance(locator1, locator2):
>>> calculate_distance("JN48QM", "QF67bf")
16466.413
Note:
Distances is calculated between the centers of the (sub) squares
"""
R = 6371 #earh radius
@ -164,15 +224,15 @@ def calculate_distance(locator1, locator2):
c = 2 * atan2(sqrt(a), sqrt(1-a))
d = R * c #distance in km
return d;
return d
def calculate_distance_longpath(locator1, locator2):
"""calculates the (longpath) distance between two Maidenhead locators
Args:
locator1 (string): Locator, either 4 or 6 characters
locator2 (string): Locator, either 4 or 6 characters
locator1 (string): Locator, either 4, 6 or 8 characters
locator2 (string): Locator, either 4, 6 or 8 characters
Returns:
float: Distance in km
@ -188,6 +248,8 @@ def calculate_distance_longpath(locator1, locator2):
>>> calculate_distance_longpath("JN48QM", "QF67bf")
23541.5867
Note:
Distance is calculated between the centers of the (sub) squares
"""
c = 40008 #[km] earth circumference
@ -200,8 +262,8 @@ def calculate_heading(locator1, locator2):
"""calculates the heading from the first to the second locator
Args:
locator1 (string): Locator, either 4 or 6 characters
locator2 (string): Locator, either 4 or 6 characters
locator1 (string): Locator, either 4, 6 or 8 characters
locator2 (string): Locator, either 4, 6 or 6 characters
Returns:
float: Heading in deg
@ -217,6 +279,9 @@ def calculate_heading(locator1, locator2):
>>> calculate_heading("JN48QM", "QF67bf")
74.3136
Note:
Heading is calculated between the centers of the (sub) squares
"""
lat1, long1 = locator_to_latlong(locator1)
@ -240,8 +305,8 @@ def calculate_heading_longpath(locator1, locator2):
"""calculates the heading from the first to the second locator (long path)
Args:
locator1 (string): Locator, either 4 or 6 characters
locator2 (string): Locator, either 4 or 6 characters
locator1 (string): Locator, either 4, 6 or 8 characters
locator2 (string): Locator, either 4, 6 or 8 characters
Returns:
float: Long path heading in deg
@ -257,6 +322,9 @@ def calculate_heading_longpath(locator1, locator2):
>>> calculate_heading_longpath("JN48QM", "QF67bf")
254.3136
Note:
Distance is calculated between the centers of the (sub) squares
"""
heading = calculate_heading(locator1, locator2)
@ -265,11 +333,11 @@ def calculate_heading_longpath(locator1, locator2):
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
Args:
locator1 (string): Maidenhead Locator, either 4 or 6 characters
locator1 (string): Maidenhead Locator, either 4, 6 or 8 characters
calc_date (datetime, optional): Starting datetime for the calculations (UTC)
Returns:
@ -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
>>> from pyhamtools.locator import calculate_sunrise_sunset
>>> from datetime import datetime
>>> import pytz
>>> UTC = pytz.UTC
>>> myDate = datetime(year=2014, month=1, day=1, tzinfo=UTC)
>>> from datetime import datetime, timezone
>>> myDate = datetime(year=2014, month=1, day=1, tzinfo=timezone.utc)
>>> calculate_sunrise_sunset("JN48QM", myDate)
{
'morning_dawn': datetime.datetime(2014, 1, 1, 6, 36, 51, 710524, tzinfo=<UTC>),
'sunset': datetime.datetime(2014, 1, 1, 16, 15, 23, 31016, tzinfo=<UTC>),
'evening_dawn': datetime.datetime(2014, 1, 1, 15, 38, 8, 355315, tzinfo=<UTC>),
'sunrise': datetime.datetime(2014, 1, 1, 7, 14, 6, 162063, 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=datetime.timezone.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=datetime.timezone.utc)
}
"""
@ -303,6 +369,8 @@ def calculate_sunrise_sunset(locator, calc_date=datetime.utcnow()):
latitude, longitude = locator_to_latlong(locator)
if calc_date is None:
calc_date = datetime.now(timezone.utc)
if type(calc_date) != datetime:
raise ValueError
@ -348,11 +416,11 @@ def calculate_sunrise_sunset(locator, calc_date=datetime.utcnow()):
result['sunset'] = sunset
if morning_dawn:
result['morning_dawn'] = morning_dawn.replace(tzinfo=UTC)
result['morning_dawn'] = morning_dawn.replace(tzinfo=timezone.utc)
if sunrise:
result['sunrise'] = sunrise.replace(tzinfo=UTC)
result['sunrise'] = sunrise.replace(tzinfo=timezone.utc)
if evening_dawn:
result['evening_dawn'] = evening_dawn.replace(tzinfo=UTC)
result['evening_dawn'] = evening_dawn.replace(tzinfo=timezone.utc)
if sunset:
result['sunset'] = sunset.replace(tzinfo=UTC)
result['sunset'] = sunset.replace(tzinfo=timezone.utc)
return result

View file

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

View file

@ -1,36 +1,23 @@
from __future__ import unicode_literals
import os
import logging
import logging.config
import re
import random, string
from datetime import datetime
from datetime import datetime, timezone
import xml.etree.ElementTree as ET
import urllib
import json
import copy
import sys
import unicodedata
import requests
from requests.exceptions import ConnectionError, HTTPError, Timeout
from bs4 import BeautifulSoup
import pytz
from . import version
from .consts import LookupConventions as const
from .exceptions import APIKeyMissingError
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
if sys.version_info.major == 3:
unicode = str
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"
class LookupLib(object):
"""
@ -54,7 +41,7 @@ class LookupLib(object):
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
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
of :py:class:`LookupLib` to query the same data concurrently.
@ -79,10 +66,7 @@ class LookupLib(object):
self._logger = logger
else:
self._logger = logging.getLogger(__name__)
if sys.version_info[:2] == (2, 6):
self._logger.addHandler(NullHandler())
else:
self._logger.addHandler(logging.NullHandler())
self._logger.addHandler(logging.NullHandler())
self._apikey = apikey
self._apiv = apiv
@ -130,18 +114,15 @@ class LookupLib(object):
"agent" : agent
}
if sys.version_info.major == 3:
encodeurl = url + "?" + urllib.parse.urlencode(params)
else:
encodeurl = url + "?" + urllib.urlencode(params)
encodeurl = url + "?" + urllib.parse.urlencode(params)
response = requests.get(encodeurl, timeout=10)
doc = BeautifulSoup(response.text, "html.parser")
doc = BeautifulSoup(response.text, "xml")
session_key = None
if doc.session.key:
session_key = doc.session.key.text
if doc.QRZDatabase.Session.Key:
session_key = doc.QRZDatabase.Session.Key.text
else:
if doc.session.error:
raise ValueError(doc.session.error.text)
if doc.QRZDatabase.Session.Error:
raise ValueError(doc.QRZDatabase.Session.Error.text)
else:
raise ValueError("Could not retrieve Session Key from QRZ.com")
@ -175,7 +156,7 @@ class LookupLib(object):
>>> from pyhamtools import LookupLib
>>> import 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")
{
u'adif': 460,
@ -224,18 +205,26 @@ class LookupLib(object):
def _push_dict_to_redis(self, push_dict, redis_prefix, name):
r = self._redis
pipe = r.pipeline()
pipe.eval(REDIS_LUA_DEL_SCRIPT, 0, redis_prefix + name)
for i in push_dict:
json_data = self._serialize_data(push_dict[i])
r.delete(redis_prefix + name + str(i))
r.set(redis_prefix + name + str(i), json_data)
pipe.set(redis_prefix + name + str(i), json_data)
pipe.execute()
return True
def _push_dict_index_to_redis(self, index_dict, redis_prefix, name):
r = self._redis
pipe = r.pipeline()
pipe.eval(REDIS_LUA_DEL_SCRIPT, 0, redis_prefix + name)
for i in index_dict:
r.delete(redis_prefix + name + str(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
@ -319,13 +308,13 @@ class LookupLib(object):
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
Args:
callsign (string): Amateur radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC)
timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc)
Returns:
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.
>>> from pyhamtools import LookupLib
>>> from datetime import datetime
>>> import pytz
>>> from datetime import datetime, timezone
>>> 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)
{
'country': u'CHRISTMAS ISLAND',
@ -364,6 +352,8 @@ class LookupLib(object):
"""
callsign = callsign.strip().upper()
if timestamp is None:
timestamp = datetime.now(timezone.utc)
if self._lookuptype == "clublogapi":
callsign_data = self._lookup_clublogAPI(callsign=callsign, timestamp=timestamp, apikey=self._apikey)
@ -482,13 +472,13 @@ class LookupLib(object):
raise KeyError
def lookup_prefix(self, prefix, timestamp=timestamp_now):
def lookup_prefix(self, prefix, timestamp=None):
"""
Returns lookup data of a Prefix
Args:
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:
dict: Dictionary containing the country specific data of the Prefix
@ -524,6 +514,8 @@ class LookupLib(object):
"""
prefix = prefix.strip().upper()
if timestamp is None:
timestamp = datetime.now(timezone.utc)
if self._lookuptype == "clublogxml" or self._lookuptype == "countryfile":
@ -537,13 +529,13 @@ class LookupLib(object):
# no matching case
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
Args:
callsign (string): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC)
timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc)
Returns:
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.
>>> from pyhamtools import LookupLib
>>> from datetime import datetime
>>> import pytz
>>> from datetime import datetime, timezone
>>> my_lookuplib = LookupLib(lookuptype="clublogxml", apikey="myapikey")
>>> print my_lookuplib.is_invalid_operation("5W1CFN")
True
>>> 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)
>>> except KeyError:
>>> print "Seems to be invalid operation before 31.1.2012"
@ -577,6 +568,8 @@ class LookupLib(object):
"""
callsign = callsign.strip().upper()
if timestamp is None:
timestamp = datetime.now(timezone.utc)
if self._lookuptype == "clublogxml":
@ -624,13 +617,13 @@ class LookupLib(object):
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
Args:
callsign (string): Amateur radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC)
timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc)
Returns:
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()
if timestamp is None:
timestamp = datetime.now(timezone.utc)
if self._lookuptype == "clublogxml":
@ -672,7 +667,7 @@ class LookupLib(object):
#no matching case
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
"""
@ -686,10 +681,10 @@ class LookupLib(object):
"call" : callsign
}
if sys.version_info.major == 3:
encodeurl = url + "?" + urllib.parse.urlencode(params)
else:
encodeurl = url + "?" + urllib.urlencode(params)
if timestamp is None:
timestamp = datetime.now(timezone.utc)
encodeurl = url + "?" + urllib.parse.urlencode(params)
response = requests.get(encodeurl, timeout=5)
if not self._check_html_response(response):
@ -720,10 +715,7 @@ class LookupLib(object):
"callsign" : callsign,
}
if sys.version_info.major == 3:
encodeurl = url + "?" + urllib.parse.urlencode(params)
else:
encodeurl = url + "?" + urllib.urlencode(params)
encodeurl = url + "?" + urllib.parse.urlencode(params)
response = requests.get(encodeurl, timeout=5)
return response
@ -736,10 +728,7 @@ class LookupLib(object):
"dxcc" : str(dxcc_or_callsign),
}
if sys.version_info.major == 3:
encodeurl = url + "?" + urllib.parse.urlencode(params)
else:
encodeurl = url + "?" + urllib.urlencode(params)
encodeurl = url + "?" + urllib.parse.urlencode(params)
response = requests.get(encodeurl, timeout=5)
return response
@ -749,43 +738,43 @@ class LookupLib(object):
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 = {}
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
raise KeyError(root.error.text)
elif re.search('Session Timeout', root.error.text, re.I): # Get new session key
if re.search('No DXCC Information for', root.Error.text, re.I): #No data available for callsign
raise KeyError(root.Error.text)
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)
response = self._request_dxcc_info_from_qrz(dxcc_or_callsign, apikey)
root = BeautifulSoup(response.text, "html.parser")
root = BeautifulSoup(response.text, "xml")
else:
raise AttributeError("Session Key Missing") #most likely session key missing or invalid
if root.dxcc is None:
if root.DXCC is None:
raise ValueError
if root.dxcc.dxcc:
lookup[const.ADIF] = int(root.dxcc.dxcc.text)
if root.dxcc.cc:
lookup['cc'] = root.dxcc.cc.text
if root.dxcc.cc:
lookup['ccc'] = root.dxcc.ccc.text
if root.DXCC.dxcc:
lookup[const.ADIF] = int(root.DXCC.dxcc.text)
if root.DXCC.cc:
lookup['cc'] = root.DXCC.cc.text
if root.DXCC.ccc:
lookup['ccc'] = root.DXCC.ccc.text
if root.find('name'):
lookup[const.COUNTRY] = root.find('name').get_text()
if root.dxcc.continent:
lookup[const.CONTINENT] = root.dxcc.continent.text
if root.dxcc.ituzone:
lookup[const.ITUZ] = int(root.dxcc.ituzone.text)
if root.dxcc.cqzone:
lookup[const.CQZ] = int(root.dxcc.cqzone.text)
if root.dxcc.timezone:
lookup['timezone'] = float(root.dxcc.timezone.text)
if root.dxcc.lat:
lookup[const.LATITUDE] = float(root.dxcc.lat.text)
if root.dxcc.lon:
lookup[const.LONGITUDE] = float(root.dxcc.lon.text)
if root.DXCC.continent:
lookup[const.CONTINENT] = root.DXCC.continent.text
if root.DXCC.ituzone:
lookup[const.ITUZ] = int(root.DXCC.ituzone.text)
if root.DXCC.cqzone:
lookup[const.CQZ] = int(root.DXCC.cqzone.text)
if root.DXCC.timezone:
lookup['timezone'] = float(root.DXCC.timezone.text)
if root.DXCC.lat:
lookup[const.LATITUDE] = float(root.DXCC.lat.text)
if root.DXCC.lon:
lookup[const.LONGITUDE] = float(root.DXCC.lon.text)
return lookup
@ -801,164 +790,160 @@ class LookupLib(object):
response = self._request_callsign_info_from_qrz(callsign, apikey, apiv)
root = BeautifulSoup(response.text, "html.parser")
root = BeautifulSoup(response.text, "xml")
lookup = {}
if root.error:
if root.Error:
if re.search('Not found', root.error.text, re.I): #No data available for callsign
raise KeyError(root.error.text)
if re.search('Not found', root.Error.text, re.I): #No data available for callsign
raise KeyError(root.Error.text)
#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)
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 root.error:
if root.Error:
if re.search('Not found', root.error.text, re.I): #No data available for callsign
raise KeyError(root.error.text)
if re.search('Not found', root.Error.text, re.I): #No data available for callsign
raise KeyError(root.Error.text)
else:
raise AttributeError(root.error.text) #most likely session key invalid
raise AttributeError(root.Error.text) #most likely session key invalid
else:
#update API Key ob Lookup object
self._apikey = apikey
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
if root.callsign.call:
lookup[const.CALLSIGN] = root.callsign.call.text
if root.callsign.xref:
lookup[const.XREF] = root.callsign.xref.text
if root.callsign.aliases:
lookup[const.ALIASES] = root.callsign.aliases.text.split(',')
if root.callsign.dxcc:
lookup[const.ADIF] = int(root.callsign.dxcc.text)
if root.callsign.fname:
lookup[const.FNAME] = root.callsign.fname.text
if root.callsign.find("name"):
lookup[const.NAME] = root.callsign.find('name').get_text()
if root.callsign.addr1:
lookup[const.ADDR1] = root.callsign.addr1.text
if root.callsign.addr2:
lookup[const.ADDR2] = root.callsign.addr2.text
if root.callsign.state:
lookup[const.STATE] = root.callsign.state.text
if root.callsign.zip:
lookup[const.ZIPCODE] = root.callsign.zip.text
if root.callsign.country:
lookup[const.COUNTRY] = root.callsign.country.text
if root.callsign.ccode:
lookup[const.CCODE] = int(root.callsign.ccode.text)
if root.callsign.lat:
lookup[const.LATITUDE] = float(root.callsign.lat.text)
if root.callsign.lon:
lookup[const.LONGITUDE] = float(root.callsign.lon.text)
if root.callsign.grid:
lookup[const.LOCATOR] = root.callsign.grid.text
if root.callsign.county:
lookup[const.COUNTY] = root.callsign.county.text
if root.callsign.fips:
lookup[const.FIPS] = int(root.callsign.fips.text) # check type
if root.callsign.land:
lookup[const.LAND] = root.callsign.land.text
if root.callsign.efdate:
if root.Callsign.call:
lookup[const.CALLSIGN] = root.Callsign.call.text
if root.Callsign.xref:
lookup[const.XREF] = root.Callsign.xref.text
if root.Callsign.aliases:
lookup[const.ALIASES] = root.Callsign.aliases.text.split(',')
if root.Callsign.dxcc:
lookup[const.ADIF] = int(root.Callsign.dxcc.text)
if root.Callsign.fname:
lookup[const.FNAME] = root.Callsign.fname.text
if root.Callsign.find("name"):
lookup[const.NAME] = root.Callsign.find('name').get_text()
if root.Callsign.addr1:
lookup[const.ADDR1] = root.Callsign.addr1.text
if root.Callsign.addr2:
lookup[const.ADDR2] = root.Callsign.addr2.text
if root.Callsign.state:
lookup[const.STATE] = root.Callsign.state.text
if root.Callsign.zip:
lookup[const.ZIPCODE] = root.Callsign.zip.text
if root.Callsign.country:
lookup[const.COUNTRY] = root.Callsign.country.text
if root.Callsign.ccode:
lookup[const.CCODE] = int(root.Callsign.ccode.text)
if root.Callsign.lat:
lookup[const.LATITUDE] = float(root.Callsign.lat.text)
if root.Callsign.lon:
lookup[const.LONGITUDE] = float(root.Callsign.lon.text)
if root.Callsign.grid:
lookup[const.LOCATOR] = root.Callsign.grid.text
if root.Callsign.county:
lookup[const.COUNTY] = root.Callsign.county.text
if root.Callsign.fips:
lookup[const.FIPS] = int(root.Callsign.fips.text) # check type
if root.Callsign.land:
lookup[const.LAND] = root.Callsign.land.text
if root.Callsign.efdate:
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:
self._logger.debug("[QRZ.com] efdate: Invalid DateTime; " + callsign + " " + root.callsign.efdate.text)
if root.callsign.expdate:
self._logger.debug("[QRZ.com] efdate: Invalid DateTime; " + callsign + " " + root.Callsign.efdate.text)
if root.Callsign.expdate:
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:
self._logger.debug("[QRZ.com] expdate: Invalid DateTime; " + callsign + " " + root.callsign.expdate.text)
if root.callsign.p_call:
lookup[const.P_CALL] = root.callsign.p_call.text
if root.callsign.find('class'):
lookup[const.LICENSE_CLASS] = root.callsign.find('class').get_text()
if root.callsign.codes:
lookup[const.CODES] = root.callsign.codes.text
if root.callsign.qslmgr:
lookup[const.QSLMGR] = root.callsign.qslmgr.text
if root.callsign.email:
lookup[const.EMAIL] = root.callsign.email.text
if root.callsign.url:
lookup[const.URL] = root.callsign.url.text
if root.callsign.u_views:
lookup[const.U_VIEWS] = int(root.callsign.u_views.text)
if root.callsign.bio:
lookup[const.BIO] = root.callsign.bio.text
if root.callsign.biodate:
self._logger.debug("[QRZ.com] expdate: Invalid DateTime; " + callsign + " " + root.Callsign.expdate.text)
if root.Callsign.p_call:
lookup[const.P_CALL] = root.Callsign.p_call.text
if root.Callsign.find('class'):
lookup[const.LICENSE_CLASS] = root.Callsign.find('class').get_text()
if root.Callsign.codes:
lookup[const.CODES] = root.Callsign.codes.text
if root.Callsign.qslmgr:
lookup[const.QSLMGR] = root.Callsign.qslmgr.text
if root.Callsign.email:
lookup[const.EMAIL] = root.Callsign.email.text
if root.Callsign.url:
lookup[const.URL] = root.Callsign.url.text
if root.Callsign.u_views:
lookup[const.U_VIEWS] = int(root.Callsign.u_views.text)
if root.Callsign.bio:
lookup[const.BIO] = root.Callsign.bio.text
if root.Callsign.biodate:
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:
self._logger.warning("[QRZ.com] biodate: Invalid DateTime; " + callsign)
if root.callsign.image:
lookup[const.IMAGE] = root.callsign.image.text
if root.callsign.imageinfo:
lookup[const.IMAGE_INFO] = root.callsign.imageinfo.text
if root.callsign.serial:
lookup[const.SERIAL] = long(root.callsign.serial.text)
if root.callsign.moddate:
if root.Callsign.image:
lookup[const.IMAGE] = root.Callsign.image.text
if root.Callsign.imageinfo:
lookup[const.IMAGE_INFO] = root.Callsign.imageinfo.text
if root.Callsign.serial:
lookup[const.SERIAL] = long(root.Callsign.serial.text)
if root.Callsign.moddate:
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:
self._logger.warning("[QRZ.com] moddate: Invalid DateTime; " + callsign)
if root.callsign.MSA:
lookup[const.MSA] = int(root.callsign.MSA.text)
if root.callsign.AreaCode:
lookup[const.AREACODE] = int(root.callsign.AreaCode.text)
if root.callsign.TimeZone:
lookup[const.TIMEZONE] = int(root.callsign.TimeZone.text)
if root.callsign.GMTOffset:
lookup[const.GMTOFFSET] = float(root.callsign.GMTOffset.text)
if root.callsign.DST:
if root.callsign.DST.text == "Y":
if root.Callsign.MSA:
lookup[const.MSA] = int(root.Callsign.MSA.text)
if root.Callsign.AreaCode:
lookup[const.AREACODE] = int(root.Callsign.AreaCode.text)
if root.Callsign.TimeZone:
lookup[const.TIMEZONE] = root.Callsign.TimeZone.text
if root.Callsign.GMTOffset:
lookup[const.GMTOFFSET] = float(root.Callsign.GMTOffset.text)
if root.Callsign.DST:
if root.Callsign.DST.text == "Y":
lookup[const.DST] = True
else:
lookup[const.DST] = False
if root.callsign.eqsl:
if root.callsign.eqsl.text == "1":
if root.Callsign.eqsl:
if root.Callsign.eqsl.text == "1":
lookup[const.EQSL] = True
else:
lookup[const.EQSL] = False
if root.callsign.mqsl:
if root.callsign.mqsl.text == "1":
if root.Callsign.mqsl:
if root.Callsign.mqsl.text == "1":
lookup[const.MQSL] = True
else:
lookup[const.MQSL] = False
if root.callsign.cqzone:
lookup[const.CQZ] = int(root.callsign.cqzone.text)
if root.callsign.ituzone:
lookup[const.ITUZ] = int(root.callsign.ituzone.text)
if root.callsign.born:
lookup[const.BORN] = int(root.callsign.born.text)
if root.callsign.user:
lookup[const.USER_MGR] = root.callsign.user.text
if root.callsign.lotw:
if root.callsign.lotw.text == "1":
if root.Callsign.cqzone:
lookup[const.CQZ] = int(root.Callsign.cqzone.text)
if root.Callsign.ituzone:
lookup[const.ITUZ] = int(root.Callsign.ituzone.text)
if root.Callsign.born:
lookup[const.BORN] = int(root.Callsign.born.text)
if root.Callsign.user:
lookup[const.USER_MGR] = root.Callsign.user.text
if root.Callsign.lotw:
if root.Callsign.lotw.text == "1":
lookup[const.LOTW] = True
else:
lookup[const.LOTW] = False
if root.callsign.iota:
lookup[const.IOTA] = root.callsign.iota.text
if root.callsign.geoloc:
lookup[const.GEOLOC] = root.callsign.geoloc.text
if root.Callsign.iota:
lookup[const.IOTA] = root.Callsign.iota.text
if root.Callsign.geoloc:
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
def _load_clublogXML(self,
url="https://secure.clublog.org/cty.php",
url="https://cdn.clublog.org/cty.php",
apikey=None,
cty_file=None):
""" 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._zone_exceptions_index = cty_dict["zone_exceptions_index"]
if self._download:
self._cleanup_download_artifact(cty_file)
return True
def _load_countryfile(self,
@ -1020,6 +1008,9 @@ class LookupLib(object):
self._callsign_exceptions_index = cty_dict["exceptions_index"]
self._prefixes_index = cty_dict["prefixes_index"]
if self._download:
self._cleanup_download_artifact(cty_file)
return True
def _download_file(self, url, apikey=None):
@ -1081,6 +1072,19 @@ class LookupLib(object):
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):
"""
Extract the header of the Clublog XML File
@ -1096,7 +1100,7 @@ class LookupLib(object):
if cty_date:
cty_date = cty_date.group(0).replace("date=", "").replace("'", "")
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_ns = re.search("xmlns='.+[']", raw_header)
@ -1107,7 +1111,7 @@ class LookupLib(object):
if len(cty_header) == 2:
self._logger.debug("Header successfully retrieved from CTY File")
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: ")
for key in cty_header:
self._logger.warning(str(key)+": "+str(cty_header[key]))
@ -1176,10 +1180,10 @@ class LookupLib(object):
entity = {}
for item in cty_entity:
if item.tag == "name":
entity[const.COUNTRY] = unicode(item.text)
self._logger.debug(unicode(item.text))
entity[const.COUNTRY] = str(item.text)
self._logger.debug(str(item.text))
elif item.tag == "prefix":
entity[const.PREFIX] = unicode(item.text)
entity[const.PREFIX] = str(item.text)
elif item.tag == "deleted":
if item.text == "TRUE":
entity[const.DELETED] = True
@ -1188,17 +1192,17 @@ class LookupLib(object):
elif item.tag == "cqz":
entity[const.CQZ] = int(item.text)
elif item.tag == "cont":
entity[const.CONTINENT] = unicode(item.text)
entity[const.CONTINENT] = str(item.text)
elif item.tag == "long":
entity[const.LONGITUDE] = float(item.text)
elif item.tag == "lat":
entity[const.LATITUDE] = float(item.text)
elif item.tag == "start":
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":
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":
if item.text == "TRUE":
entity[const.WHITELIST] = True
@ -1206,10 +1210,10 @@ class LookupLib(object):
entity[const.WHITELIST] = False
elif item.tag == "whitelist_start":
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":
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:
self._logger.error("Error while processing: ")
entities[int(cty_entity[0].text)] = entity
@ -1230,23 +1234,23 @@ class LookupLib(object):
else:
call_exceptions_index[call] = [int(cty_exception.attrib["record"])]
elif item.tag == "entity":
call_exception[const.COUNTRY] = unicode(item.text)
call_exception[const.COUNTRY] = str(item.text)
elif item.tag == "adif":
call_exception[const.ADIF] = int(item.text)
elif item.tag == "cqz":
call_exception[const.CQZ] = int(item.text)
elif item.tag == "cont":
call_exception[const.CONTINENT] = unicode(item.text)
call_exception[const.CONTINENT] = str(item.text)
elif item.tag == "long":
call_exception[const.LONGITUDE] = float(item.text)
elif item.tag == "lat":
call_exception[const.LATITUDE] = float(item.text)
elif item.tag == "start":
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":
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
self._logger.debug(str(len(call_exceptions))+" Exceptions added")
@ -1271,23 +1275,23 @@ class LookupLib(object):
else:
prefixes_index[call] = [int(cty_prefix.attrib["record"])]
if item.tag == "entity":
prefix[const.COUNTRY] = unicode(item.text)
prefix[const.COUNTRY] = str(item.text)
elif item.tag == "adif":
prefix[const.ADIF] = int(item.text)
elif item.tag == "cqz":
prefix[const.CQZ] = int(item.text)
elif item.tag == "cont":
prefix[const.CONTINENT] = unicode(item.text)
prefix[const.CONTINENT] = str(item.text)
elif item.tag == "long":
prefix[const.LONGITUDE] = float(item.text)
elif item.tag == "lat":
prefix[const.LATITUDE] = float(item.text)
elif item.tag == "start":
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":
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
self._logger.debug(str(len(prefixes))+" Prefixes added")
@ -1310,10 +1314,10 @@ class LookupLib(object):
elif item.tag == "start":
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":
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
self._logger.debug(str(len(invalid_operations))+" Invalid Operations added")
@ -1339,10 +1343,10 @@ class LookupLib(object):
zoneException[const.CQZ] = int(item.text)
elif item.tag == "start":
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":
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
self._logger.debug(str(len(zone_exceptions))+" Zone Exceptions added")
@ -1387,19 +1391,23 @@ class LookupLib(object):
mapping = None
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:
entry = {}
call = str(item)
entry[const.COUNTRY] = unicode(cty_list[item]["Country"])
entry[const.COUNTRY] = str(cty_list[item]["Country"])
if mapping:
entry[const.ADIF] = int(mapping[cty_list[item]["Country"]])
entry[const.CQZ] = int(cty_list[item]["CQZone"])
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.LONGITUDE] = float(cty_list[item]["Longitude"])*(-1)
@ -1475,8 +1483,7 @@ class LookupLib(object):
Deserialize a JSON into a dictionary
"""
my_dict = json.loads(json_data.decode('utf8').replace("'", '"'),
encoding='UTF-8')
my_dict = json.loads(json_data.decode('utf8'))
for item in my_dict:
if item == const.ADIF:
@ -1492,17 +1499,17 @@ class LookupLib(object):
elif item == const.LONGITUDE:
my_dict[item] = float(my_dict[item])
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:
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:
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:
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:
my_dict[item] = self._str_to_bool(my_dict[item])
else:
my_dict[item] = unicode(my_dict[item])
my_dict[item] = str(my_dict[item])
return my_dict

View file

@ -1,4 +1,3 @@
from future.utils import iteritems
from datetime import datetime
import re
@ -10,7 +9,7 @@ from io import BytesIO
from requests.exceptions import ConnectionError, HTTPError, Timeout
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:
url (str, optional): Download URL
@ -69,7 +68,7 @@ def get_lotw_users(**kwargs):
return lotw
def get_clublog_users(**kwargs):
"""Download the latest offical list of `Clublog`__ users.
"""Download the latest official list of `Clublog`__ users.
Args:
url (str, optional): Download URL
@ -94,7 +93,7 @@ def get_clublog_users(**kwargs):
'lastupload': datetime.datetime(2013, 5, 8, 15, 0, 6),
'oqrs': True}
.. _CLUBLOG: https://secure.clublog.org
.. _CLUBLOG: https://clublog.org
__ CLUBLOG_
"""
@ -106,7 +105,7 @@ def get_clublog_users(**kwargs):
try:
url = kwargs['url']
except KeyError:
url = "https://secure.clublog.org/clublog-users.json.zip"
url = "https://cdn.clublog.org/clublog-users.json.zip"
try:
result = requests.get(url)
@ -121,11 +120,11 @@ def get_clublog_users(**kwargs):
files = zip_file.namelist()
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
for call, call_data in iteritems(cl_data):
for call, call_data in cl_data.items():
try:
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
print("Ignoring invalid type in data:", call, call_data)
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)
pass
@ -203,4 +202,4 @@ def get_eqsl_users(**kwargs):
else:
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:]
__version__ = '.'.join((str(VERSION[0]), str(VERSION[1])))

View file

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

View file

@ -1,2 +1 @@
sphinx>=1.8.5
sphinxcontrib-napoleon>=0.7
sphinx>=1.8.5

View file

@ -1,4 +1,9 @@
pytest>=3.3.2
pytest>=7.0.0
pytest-blockage>=0.2.2
pytest-localserver>=0.5.0
pytest-cov>=2.7.1
pytest-localserver>=0.5
pytest-cov>=2.12
maidenhead==1.6.0
requests>=2.32.4
beautifulsoup4==4.13.4
redis==5.2.1
ephem==4.2

View file

@ -1,7 +1,6 @@
#!/usr/bin/env python
import sys
import os
from distutils.core import setup
from setuptools import setup
kw = {}
@ -16,11 +15,10 @@ setup(name='pyhamtools',
package_data={'': ['countryfilemapping.json']},
packages=['pyhamtools'],
install_requires=[
"pytz>=2019.1",
"requests>=2.21.0",
"pyephem>=3.7.6.0",
"ephem>=4.1.3",
"beautifulsoup4>=4.7.1",
"future>=0.17.1",
"lxml>=5.0.0",
"redis>=2.10.6",
],
**kw

View file

@ -1,7 +1,7 @@
import pytest
import tempfile
import pkgutil
import json
import os
import logging
from pyhamtools import LookupLib
from pyhamtools import Callinfo
@ -96,3 +96,7 @@ def fix_redis():
@pytest.fixture(scope="module")
def fix_qrz():
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 pytz
from pyhamtools.consts import LookupConventions as const
UTC = pytz.UTC
response_prefix_DH_clublog = {
'country': 'FEDERAL REPUBLIC OF GERMANY',
'adif': 230,
@ -17,6 +14,24 @@ response_prefix_DH_clublog = {
'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 = {
'country': 'Fed. Rep. of Germany',
'adif': 230,
@ -75,22 +90,49 @@ response_prefix_VK9DWX_clublog = {
}
response_prefix_VK9DLX_clublog = {
u'adif': 147,
u'adif': 189,
u'continent': u'OC',
u'country': u'LORD HOWE ISLAND',
u'cqz': 30,
u'latitude': -31.6,
u'longitude': 159.1
u'country': u'NORFOLK ISLAND',
u'cqz': 32,
u'latitude': -29.0,
u'longitude': 168.0
}
response_prefix_TA7I_clublog = {
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 = {
u'adif': 147,
u'adif': 189,
u'continent': u'OC',
u'country': u'Lord Howe Island',
u'cqz': 30,
u'country': u'Norfolk Island',
u'cqz': 32,
u'ituz': 60,
u'latitude': -31.55,
u'longitude': 159.08
u'latitude': -29.03,
u'longitude': 167.93
}
response_prefix_VK9GMW_clublog = {
@ -102,6 +144,24 @@ response_prefix_VK9GMW_clublog = {
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 = {
u'adif': 339,
u'continent': u'AS',
@ -134,18 +194,18 @@ response_Exception_VK9XO_with_start_date = {
'adif': 35,
'country': 'CHRISTMAS ISLAND',
'continent': 'OC',
'latitude': -10.50,
'longitude': 105.70,
'latitude': -10.48,
'longitude': 105.62,
'cqz': 29
}
response_zone_exception_dp0gvn = {
'country': 'ANTARCTICA',
'adif': 13,
'cqz': 38,
'latitude': -65.0,
'longitude': -64.0,
'continent': 'AN'
response_zone_exception_ci8aw = {
'country': 'CANADA',
'adif': 1,
'cqz': 1,
'latitude': 45.0,
'longitude': -80.0,
'continent': 'NA'
}
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/B")[const.BEACON]
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("9H5A/C6A") == 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/BUX") == 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("7QAA") == response_callsign_exceptions_7QAA_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):
fix_callinfo._dismantle_callsign("OZ/JO85")
with pytest.raises(KeyError):
fix_callinfo._dismantle_callsign("DL")
if fix_callinfo._lookuplib._lookuptype == "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")
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):
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":
assert fix_callinfo.get_all("DH1TW") == response_prefix_DH_clublog
assert fix_callinfo.get_all("dp0gvn") == response_zone_exception_dp0gvn
timestamp = datetime(year=2016, month=1, day=20, tzinfo=UTC)
assert fix_callinfo.get_all("ci8aw") == response_zone_exception_ci8aw
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
elif fix_callinfo._lookuplib._lookuptype == "countryfile":

View file

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

View file

@ -1,15 +1,9 @@
import pytest
from datetime import datetime
import pytz
from datetime import datetime, timezone
from pyhamtools.consts import LookupConventions as const
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_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.MODE: "CW",
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 sys
import datetime
import pytest
from pyhamtools.qsl import get_eqsl_users
if sys.version_info.major == 3:
unicode = str
test_dir = os.path.dirname(os.path.abspath(__file__))
fix_dir = os.path.join(test_dir, 'fixtures')
class Test_eqsl_methods:
@ -26,7 +21,7 @@ class Test_eqsl_methods:
data = get_eqsl_users()
assert isinstance(data, list)
for el in data:
assert isinstance(el, unicode)
assert isinstance(el, str)
assert len(data) > 1000
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("FN44AB", "JN48QM") - 5965) < 1
assert abs(calculate_distance("JN48QM", "QF67bf") - 16467) < 1
assert abs(calculate_distance("JN48QM", "QF67BF") - 16467) < 1
assert abs(calculate_distance("JN48QM84", "QF67BF84") - 16467) < 1
assert abs(calculate_distance("JN48QM84", "QF67BF") - 16464) < 1
assert abs(calculate_distance("JN48QM84", "QF67") - 16506) < 1
assert abs(calculate_distance("JN48QM", "QF67") - 16508) < 1
assert abs(calculate_distance("JN48", "QF67") - 16535) < 1
def test_calculate_distance_invalid_inputs(self):
with pytest.raises(AttributeError):

View file

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

View file

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

View file

@ -1,12 +1,9 @@
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
import pytest
import pytz
from pyhamtools.locator import calculate_sunrise_sunset
UTC = pytz.UTC
class Test_calculate_sunrise_sunset_normal_case():
def test_calculate_sunrise_sunset(self):
@ -14,28 +11,41 @@ class Test_calculate_sunrise_sunset_normal_case():
time_margin = timedelta(minutes=1)
locator = "JN48QM"
test_time = datetime(year=2014, month=1, day=1, tzinfo=UTC)
result_JN48QM_1_1_2014_evening_dawn = datetime(2014, 1, 1, 15, 38, tzinfo=UTC)
result_JN48QM_1_1_2014_morning_dawn = datetime(2014, 1, 1, 6, 36, tzinfo=UTC)
result_JN48QM_1_1_2014_sunrise = datetime(2014, 1, 1, 7, 14, tzinfo=UTC)
result_JN48QM_1_1_2014_sunset = datetime(2014, 1, 1, 16, 15, 23, 31016, 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=timezone.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=timezone.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)['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)['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)
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)
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)
test_time = datetime(year=2014, month=1, day=1, tzinfo=timezone.utc)
assert calculate_sunrise_sunset(locator, test_time)['morning_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 sys
from pyhamtools.lookuplib import LookupLib
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")])
def fixAnyValue(request):
return request.param
@ -41,5 +36,5 @@ class TestlookupLibHelper:
with pytest.raises(TypeError):
fixClublogApi._generate_random_word()
assert type(fixClublogApi._generate_random_word(5)) is unicode
assert len(fixClublogApi._generate_random_word(5)) is 5
assert type(fixClublogApi._generate_random_word(5)) is str
assert len(fixClublogApi._generate_random_word(5)) == 5

View file

@ -1,5 +1,5 @@
import pytest
from datetime import datetime
from datetime import datetime, timezone
from pyhamtools.lookuplib import LookupLib
@ -84,7 +84,7 @@ class TestclublogApi_Getters:
def test_lookup_callsign(self, fixClublogApi):
assert fixClublogApi.lookup_callsign("DH1TW") == response_Exception_DH1TW
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("DH1TW/MM") == response_Exception_DH1TW_MM
assert fixClublogApi.lookup_callsign("DH1TW/AM") == response_Exception_DH1TW_AM

View file

@ -1,13 +1,10 @@
import pytest
from datetime import datetime
import pytz
from datetime import datetime, timezone
import os
from pyhamtools.lookuplib import LookupLib
from pyhamtools.exceptions import APIKeyMissingError
UTC = pytz.UTC
#Fixtures
#===========================================================
@ -28,8 +25,8 @@ response_Exception_KC6MM_1990 = {
'adif': 22,
'country': u'PALAU',
'continent': u'OC',
'latitude': 9.50,
'longitude': 138.20,
'latitude': 9.52,
'longitude': 138.21,
'cqz': 27,
}
@ -37,8 +34,8 @@ response_Exception_KC6MM_1992 = {
'adif': 22,
'country': u'PALAU',
'continent': u'OC',
'latitude': 9.50,
'longitude': 138.20,
'latitude': 9.52,
'longitude': 138.21,
'cqz': 27,
}
@ -46,8 +43,8 @@ response_Exception_VK9XX_with_end_date = {
'adif': 35,
'country': u'CHRISTMAS ISLAND',
'continent': u'OC',
'latitude': -10.40,
'longitude': 105.71,
'latitude': -10.52,
'longitude': 105.54,
'cqz': 29,
}
@ -55,8 +52,8 @@ response_Exception_VK9XO_with_start_date = {
'adif': 35,
'country': u'CHRISTMAS ISLAND',
'continent': u'OC',
'latitude': -10.50,
'longitude': 105.70,
'latitude': -10.48,
'longitude': 105.62,
'cqz': 29,
}
@ -97,7 +94,7 @@ response_Prefix_VK9_starting_1976 = {
}
response_Prefix_ZD5_1964_to_1971 = {
'country': u'SWAZILAND',
'country': u'KINGDOM OF ESWATINI',
'adif' : 468,
'continent': u'AF',
'latitude': -26.30,
@ -119,23 +116,6 @@ def fix_cty_xml_file(request):
#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:
#lookup_entity(callsign)
@ -159,40 +139,40 @@ class TestclublogXML_Getters:
#===============================
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
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
def test_lookup_callsign_exception_only_with_start_date(self, fixClublogXML):
#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") == response_Exception_VK9XO_with_start_date
#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):
fixClublogXML.lookup_callsign("vk9xo", timestamp)
def test_lookup_callsign_exception_only_with_end_date(self, fixClublogXML):
#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
# timestamp > enddate
with pytest.raises(KeyError):
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):
fixClublogXML.lookup_callsign("vk9xx", timestamp)
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" ) == response_Exception_AX9NYG
@ -213,29 +193,29 @@ class TestclublogXML_Getters:
def test_lookup_prefix_with_changing_entities(self, fixClublogXML):
#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
#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):
fixClublogXML.lookup_prefix("VK9", timestamp)
#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
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):
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
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):
fixClublogXML.lookup_prefix("ZD5", timestamp_after)
@ -251,8 +231,8 @@ class TestclublogXML_Getters:
fixClublogXML.is_invalid_operation("dh1tw")
#Invalid Operation with start and end date
timestamp_before = datetime(year=1993, month=12, day=30).replace(tzinfo=UTC)
timestamp = datetime(year=1994, 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, tzinfo=timezone.utc)
with pytest.raises(KeyError):
fixClublogXML.is_invalid_operation("vk0mc")
@ -263,18 +243,10 @@ class TestclublogXML_Getters:
#Invalid Operation with start date
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):
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])
#====================================
@ -289,9 +261,9 @@ class TestclublogXML_Getters:
assert fixClublogXML.lookup_zone_exception("dp0gvn") == 38
#zone exception with start and end date
timestamp = datetime(year=1992, month=10, day=2).replace(tzinfo=UTC)
timestamp_before = datetime(year=1992, month=9, day=30).replace(tzinfo=UTC)
timestamp_after = datetime(year=1993, month=3, day=1).replace(tzinfo=UTC)
timestamp = datetime(year=1992, month=10, day=2, tzinfo=timezone.utc)
timestamp_before = datetime(year=1992, month=9, day=30, tzinfo=timezone.utc)
timestamp_after = datetime(year=1993, month=3, day=1, tzinfo=timezone.utc)
assert fixClublogXML.lookup_zone_exception("dl1kvc/p", timestamp) == 38
with pytest.raises(KeyError):
@ -301,6 +273,6 @@ class TestclublogXML_Getters:
fixClublogXML.lookup_zone_exception("dl1kvc/p", timestamp_after)
#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):
fixClublogXML.lookup_zone_exception("dh1hb/p", timestamp_before)

View file

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

View file

@ -1,16 +1,12 @@
import os
import pytest
from datetime import datetime
from datetime import datetime, timezone
from pyhamtools.lookuplib import LookupLib
from pyhamtools.exceptions import APIKeyMissingError
from pyhamtools.consts import LookupConventions as const
import pytz
UTC = pytz.UTC
try:
QRZ_USERNAME = str(os.environ['QRZ_USERNAME'])
QRZ_PWD = str(os.environ['QRZ_PWD'])
@ -21,10 +17,10 @@ except Exception:
#===========================================================
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'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'callsign': u'1A1AB',
u'addr2': u'00187 Rome',

View file

@ -1,15 +1,11 @@
import pytest
import json
from datetime import datetime
from datetime import datetime, timezone
import pytz
import redis
from pyhamtools import LookupLib, Callinfo
UTC = pytz.UTC
r = redis.Redis()
@ -22,6 +18,14 @@ response_Exception_VP8STI_with_start_and_stop_date = {
'cqz': 13,
}
response_TU5PCT = {
'adif': 428,
'country': u"COTE D'IVOIRE",
'continent': u'AF',
'latitude': 5.3,
'longitude': -4.0,
'cqz': 35,
}
class TestStoreDataInRedis:
@ -32,10 +36,11 @@ class TestStoreDataInRedis:
assert fix_redis.lookup_callsign("VK9XO") == fixClublogXML.lookup_callsign("VK9XO")
assert fix_redis.lookup_prefix("DH") == fixClublogXML.lookup_prefix("DH")
with pytest.raises(KeyError):
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)
with pytest.raises(KeyError):
@ -52,6 +57,7 @@ class TestStoreDataInRedis:
assert lib.lookup_prefix("DH") == fixCountryFile.lookup_prefix("DH")
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)
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 sys
import datetime
from past.builtins import execfile
from future.utils import iteritems
from .execfile import execfile
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__))
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)
assert get_lotw_users(url=httpserver.url) == namespace['lotw_fixture']
@pytest.mark.skip("ARRL has been hacked in May 2024; skipping until LOTW is again up")
def test_download_lotw_list_and_check_types(self):
data = get_lotw_users()
assert isinstance(data, dict)
for key, value in iteritems(data):
assert isinstance(key, unicode)
for key, value in data.items():
assert isinstance(key, str)
assert isinstance(value, datetime.datetime )
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}
def test_shf_frequencies(self):
assert freq_to_band(2320200) == {"band" : 0.13, "mode":None}
assert freq_to_band(2390000) == {"band" : 0.13, "mode":None}
assert freq_to_band(3300000) == {"band" : 0.09, "mode":None}