Compare commits

..

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

44 changed files with 18917 additions and 34467 deletions

37
.appveyor.yml Normal file
View file

@ -0,0 +1,37 @@
build: false
environment:
matrix:
- PYTHON: "C:\\Python27"
PYTHON_VERSION: "2.7.8"
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"
- PYTHON: "C:\\Python37"
PYTHON_VERSION: "3.7.5"
PYTHON_ARCH: "32"
- PYTHON: "C:\\Python38"
PYTHON_VERSION: "3.8.0"
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
View file

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

View file

@ -1,168 +0,0 @@
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

8
.gitignore vendored
View file

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

View file

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

34
.travis.yml Normal file
View file

@ -0,0 +1,34 @@
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"
- "3.8-dev" # 3.8 development branch
- "nightly"
- "pypy"
jobs:
allow_failures:
- python: "nightly"
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.8 ]]; 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.8 ]]; then make html; fi

View file

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2025 Tobias Wellnitz Copyright (c) 2014 Tobias Wellnitz
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

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

View file

@ -1,168 +1,6 @@
Changelog Changelog
--------- ---------
PyHamtools 0.12.0
================
09. June 2025
* deprecated support for Python 3.6
* deprecated support for Python 3.7
* added support for higher Microwave bands (tnx @sq6emm)
* added support for 10 characters Maidenhead locators (tnx @sq6emm)
* updated CI pipeline
PyHamtools 0.11.0
================
02. March 2025
* added support for Python 3.13
PyHamtools 0.10.0
================
01. June 2024
* full support for 4, 6, 8 characters Maidenhead locator conversions
PyHamtools 0.9.1
================
17. March 2024
* switched from distutils to setuptools. No impact for endusers.
PyHamtools 0.9.0
================
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 PyHamTools 0.7.5
================ ================
@ -305,7 +143,7 @@ PyHamTools 0.5.0
* corrected Longitude to General Standard (-180...0° West, 0...180° East) [LookupLib] * corrected Longitude to General Standard (-180...0° West, 0...180° East) [LookupLib]
* improved callsign decoding algorithm [CallInfo] * improved callsign decoding alogrithm [CallInfo]
* added special case to decode location of VK9 callsigns [CallInfo] * added special case to decode location of VK9 callsigns [CallInfo]

View file

@ -12,8 +12,19 @@
# All configuration values have a default; values that are commented out # All configuration values have a default; values that are commented out
# serve to show the default. # serve to show the default.
import sys
import os
from pyhamtools.version import __version__, __release__ from pyhamtools.version import __version__, __release__
sys.path.insert(0,"/Users/user/projects/pyhamtools/pyhamtools")
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------ # -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here. # If your documentation needs a minimal Sphinx version, state it here.
@ -24,8 +35,7 @@ from pyhamtools.version import __version__, __release__
# ones. # ones.
extensions = [ extensions = [
'sphinx.ext.autodoc', 'sphinx.ext.autodoc',
'sphinx.ext.napoleon', 'sphinxcontrib.napoleon',
'sphinx_rtd_dark_mode',
] ]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
@ -42,7 +52,7 @@ master_doc = 'index'
# General information about the project. # General information about the project.
project = u'pyhamtools' project = u'pyhamtools'
copyright = u'2024, Tobias Wellnitz, DH1TW' copyright = u'2019, Tobias Wellnitz, DH1TW'
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
@ -96,9 +106,7 @@ pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # a list of builtin themes.
# html_theme = 'default' html_theme = 'default'
html_theme = 'sphinx_rtd_theme'
# html_theme = 'sphinx_material'
# Theme options are theme-specific and customize the look and feel of a theme # Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the # further. For a list of options available for each theme, see the

View file

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

View file

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

View file

@ -1,11 +1,22 @@
import re import re
import logging import logging
from datetime import datetime, timezone from datetime import datetime
import sys
import pytz
from pyhamtools.consts import LookupConventions as const from pyhamtools.consts import LookupConventions as const
from pyhamtools.callsign_exceptions import callsign_exceptions from pyhamtools.callsign_exceptions import callsign_exceptions
UTC = pytz.UTC
if sys.version_info < (2, 7, ):
class NullHandler(logging.Handler):
def emit(self, record):
pass
class Callinfo(object): class Callinfo(object):
""" """
The purpose of this class is to return data (country, latitude, longitude, CQ Zone...etc) for an The purpose of this class is to return data (country, latitude, longitude, CQ Zone...etc) for an
@ -26,6 +37,9 @@ class Callinfo(object):
self._logger = logger self._logger = logger
else: else:
self._logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
if sys.version_info[:2] == (2, 6):
self._logger.addHandler(NullHandler())
else:
self._logger.addHandler(logging.NullHandler()) self._logger.addHandler(logging.NullHandler())
self._lookuplib = lookuplib self._lookuplib = lookuplib
@ -56,7 +70,7 @@ class Callinfo(object):
""" """
callsign = callsign.upper() callsign = callsign.upper()
homecall = re.search('[\\d]{0,1}[A-Z]{1,2}\\d([A-Z]{1,4}|\\d{3,3}|\\d{1,3}[A-Z])[A-Z]{0,5}', callsign) homecall = re.search('[\d]{0,1}[A-Z]{1,2}\d([A-Z]{1,4}|\d{3,3}|\d{1,3}[A-Z])[A-Z]{0,5}', callsign)
if homecall: if homecall:
homecall = homecall.group(0) homecall = homecall.group(0)
return homecall return homecall
@ -67,10 +81,10 @@ class Callinfo(object):
"""truncate call until it corresponds to a Prefix in the database""" """truncate call until it corresponds to a Prefix in the database"""
prefix = callsign prefix = callsign
if timestamp is None: if timestamp is None:
timestamp = datetime.now(timezone.utc) timestamp = datetime.utcnow().replace(tzinfo=UTC)
if re.search('(VK|AX|VI)9[A-Z]{3}', callsign): #special rule for VK9 calls if re.search('(VK|AX|VI)9[A-Z]{3}', callsign): #special rule for VK9 calls
if timestamp > datetime(2006,1,1, tzinfo=timezone.utc): if timestamp > datetime(2006,1,1, tzinfo=UTC):
prefix = callsign[0:3]+callsign[4:5] prefix = callsign[0:3]+callsign[4:5]
while len(prefix) > 0: while len(prefix) > 0:
@ -101,7 +115,7 @@ class Callinfo(object):
Args: Args:
callsign (str): Amateur Radio callsign callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc) timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC)
Raises: Raises:
KeyError: Callsign could not be identified KeyError: Callsign could not be identified
@ -110,12 +124,12 @@ class Callinfo(object):
""" """
entire_callsign = callsign.upper() entire_callsign = callsign.upper()
if timestamp is None: if timestamp is None:
timestamp = datetime.now(timezone.utc) timestamp = datetime.utcnow().replace(tzinfo=UTC)
if re.search('[/A-Z0-9\\-]{3,15}', entire_callsign): # make sure the call has at least 3 characters if re.search('[/A-Z0-9\-]{3,15}', entire_callsign): # make sure the call has at least 3 characters
if re.search('\\-\\d{1,3}$', entire_callsign): # cut off any -10 / -02 appendixes if re.search('\-\d{1,3}$', entire_callsign): # cut off any -10 / -02 appendixes
callsign = re.sub('\\-\\d{1,3}$', '', entire_callsign) callsign = re.sub('\-\d{1,3}$', '', entire_callsign)
if re.search('/[A-Z0-9]{1,4}/[A-Z0-9]{1,4}$', callsign): if re.search('/[A-Z0-9]{1,4}/[A-Z0-9]{1,4}$', callsign):
callsign = re.sub('/[A-Z0-9]{1,4}$', '', callsign) # cut off 2. appendix DH1TW/HC2/P -> DH1TW/HC2 callsign = re.sub('/[A-Z0-9]{1,4}$', '', callsign) # cut off 2. appendix DH1TW/HC2/P -> DH1TW/HC2
@ -178,21 +192,20 @@ class Callinfo(object):
data[const.BEACON] = True data[const.BEACON] = True
return data return data
elif re.search('\\d$', appendix): elif re.search('\d$', appendix):
area_nr = re.search('\\d$', appendix).group(0) area_nr = re.search('\d$', appendix).group(0)
callsign = re.sub('/\\d$', '', callsign) #remove /number callsign = re.sub('/\d$', '', callsign) #remove /number
if len(re.findall(r'\\d+', callsign)) == 1: #call has just on digit e.g. DH1TW if len(re.findall(r'\d+', callsign)) == 1: #call has just on digit e.g. DH1TW
callsign = re.sub('[\\d]+', area_nr, callsign) callsign = re.sub('[\d]+', area_nr, callsign)
else: # call has several digits e.g. 7N4AAL else: # call has several digits e.g. 7N4AAL
pass # no (two) digit prefix countries known where appendix would change entity pass # no (two) digit prefix contries known where appendix would change entitiy
return self._iterate_prefix(callsign, timestamp) return self._iterate_prefix(callsign, timestamp)
else: else:
return self._iterate_prefix(callsign, timestamp) return self._iterate_prefix(callsign, timestamp)
# regular callsigns, without prefix or appendix # regular callsigns, without prefix or appendix
# elif re.match('^[\\d]{0,1}[A-Z]{1,2}\\d{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([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,4}([A-Z]{1,4}|[A-Z]{1,2}\\d{0,3})[A-Z]{0,5}$', callsign):
return self._iterate_prefix(callsign, timestamp) return self._iterate_prefix(callsign, timestamp)
# callsigns with prefixes (xxx/callsign) # callsigns with prefixes (xxx/callsign)
@ -201,11 +214,8 @@ class Callinfo(object):
pfx = re.sub('/', '', pfx.group(0)) pfx = re.sub('/', '', pfx.group(0))
#make sure that the remaining part is actually a callsign (avoid: OZ/JO81) #make sure that the remaining part is actually a callsign (avoid: OZ/JO81)
rest = re.search('/[A-Z0-9]+', entire_callsign) rest = re.search('/[A-Z0-9]+', entire_callsign)
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)) 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 re.match('^[\d]{0,1}[A-Z]{1,2}\d([A-Z]{1,4}|\d{3,3}|\d{1,3}[A-Z])[A-Z]{0,5}$', rest):
return self._iterate_prefix(pfx) return self._iterate_prefix(pfx)
if entire_callsign in callsign_exceptions: if entire_callsign in callsign_exceptions:
@ -216,7 +226,7 @@ class Callinfo(object):
def _lookup_callsign(self, callsign, timestamp=None): def _lookup_callsign(self, callsign, timestamp=None):
if timestamp is None: if timestamp is None:
timestamp = datetime.now(timezone.utc) timestamp = datetime.utcnow().replace(tzinfo=UTC)
# Check if operation is invalid # Check if operation is invalid
invalid = False invalid = False
@ -264,7 +274,7 @@ class Callinfo(object):
Args: Args:
callsign (str): Amateur Radio callsign callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc) timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC)
Returns: Returns:
dict: Dictionary containing the callsign specific data dict: Dictionary containing the callsign specific data
@ -301,7 +311,7 @@ class Callinfo(object):
callsign = callsign.upper() callsign = callsign.upper()
if timestamp is None: if timestamp is None:
timestamp = datetime.now(timezone.utc) timestamp = datetime.utcnow().replace(tzinfo=UTC)
callsign_data = self._lookup_callsign(callsign, timestamp) callsign_data = self._lookup_callsign(callsign, timestamp)
@ -318,7 +328,7 @@ class Callinfo(object):
Args: Args:
callsign (str): Amateur Radio callsign callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc) timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC)
Returns: Returns:
bool: True / False bool: True / False
@ -334,7 +344,7 @@ class Callinfo(object):
""" """
if timestamp is None: if timestamp is None:
timestamp = datetime.now(timezone.utc) timestamp = datetime.utcnow().replace(tzinfo=UTC)
try: try:
if self.get_all(callsign, timestamp): if self.get_all(callsign, timestamp):
@ -347,7 +357,7 @@ class Callinfo(object):
Args: Args:
callsign (str): Amateur Radio callsign callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc) timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC)
Returns: Returns:
dict: Containing Latitude and Longitude dict: Containing Latitude and Longitude
@ -374,7 +384,7 @@ class Callinfo(object):
""" """
if timestamp is None: if timestamp is None:
timestamp = datetime.now(timezone.utc) timestamp = datetime.utcnow().replace(tzinfo=UTC)
callsign_data = self.get_all(callsign, timestamp=timestamp) callsign_data = self.get_all(callsign, timestamp=timestamp)
return { return {
@ -387,7 +397,7 @@ class Callinfo(object):
Args: Args:
callsign (str): Amateur Radio callsign callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc) timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC)
Returns: Returns:
int: containing the callsign's CQ Zone int: containing the callsign's CQ Zone
@ -397,7 +407,7 @@ class Callinfo(object):
""" """
if timestamp is None: if timestamp is None:
timestamp = datetime.now(timezone.utc) timestamp = datetime.utcnow().replace(tzinfo=UTC)
return self.get_all(callsign, timestamp)[const.CQZ] return self.get_all(callsign, timestamp)[const.CQZ]
@ -406,7 +416,7 @@ class Callinfo(object):
Args: Args:
callsign (str): Amateur Radio callsign callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc) timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC)
Returns: Returns:
int: containing the callsign's CQ Zone int: containing the callsign's CQ Zone
@ -419,7 +429,7 @@ class Callinfo(object):
""" """
if timestamp is None: if timestamp is None:
timestamp = datetime.now(timezone.utc) timestamp = datetime.utcnow().replace(tzinfo=UTC)
return self.get_all(callsign, timestamp)[const.ITUZ] return self.get_all(callsign, timestamp)[const.ITUZ]
@ -428,7 +438,7 @@ class Callinfo(object):
Args: Args:
callsign (str): Amateur Radio callsign callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc) timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC)
Returns: Returns:
str: name of the Country str: name of the Country
@ -446,7 +456,7 @@ class Callinfo(object):
""" """
if timestamp is None: if timestamp is None:
timestamp = datetime.now(timezone.utc) timestamp = datetime.utcnow().replace(tzinfo=UTC)
return self.get_all(callsign, timestamp)[const.COUNTRY] return self.get_all(callsign, timestamp)[const.COUNTRY]
@ -455,7 +465,7 @@ class Callinfo(object):
Args: Args:
callsign (str): Amateur Radio callsign callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc) timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC)
Returns: Returns:
int: containing the country ADIF id int: containing the country ADIF id
@ -465,7 +475,7 @@ class Callinfo(object):
""" """
if timestamp is None: if timestamp is None:
timestamp = datetime.now(timezone.utc) timestamp = datetime.utcnow().replace(tzinfo=UTC)
return self.get_all(callsign, timestamp)[const.ADIF] return self.get_all(callsign, timestamp)[const.ADIF]
@ -474,7 +484,7 @@ class Callinfo(object):
Args: Args:
callsign (str): Amateur Radio callsign callsign (str): Amateur Radio callsign
timestamp (datetime, optional): datetime in UTC (tzinfo=timezone.utc) timestamp (datetime, optional): datetime in UTC (tzinfo=pytz.UTC)
Returns: Returns:
str: continent identified str: continent identified
@ -494,6 +504,6 @@ class Callinfo(object):
- AN: Antarctica - AN: Antarctica
""" """
if timestamp is None: if timestamp is None:
timestamp = datetime.now(timezone.utc) timestamp = datetime.utcnow().replace(tzinfo=UTC)
return self.get_all(callsign, timestamp)[const.CONTINENT] return self.get_all(callsign, timestamp)[const.CONTINENT]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,17 +0,0 @@
# 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)

51552
test/fixtures/cty.plist vendored

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

@ -14,12 +14,7 @@ class Test_calculate_distance():
assert abs(calculate_distance("JN48QM", "FN44AB") - 5965) < 1 assert abs(calculate_distance("JN48QM", "FN44AB") - 5965) < 1
assert abs(calculate_distance("FN44AB", "JN48QM") - 5965) < 1 assert abs(calculate_distance("FN44AB", "JN48QM") - 5965) < 1
assert abs(calculate_distance("JN48QM", "QF67BF") - 16467) < 1 assert abs(calculate_distance("JN48QM", "QF67bf") - 16467) < 1
assert abs(calculate_distance("JN48QM84", "QF67BF84") - 16467) < 1
assert abs(calculate_distance("JN48QM84", "QF67BF") - 16464) < 1
assert abs(calculate_distance("JN48QM84", "QF67") - 16506) < 1
assert abs(calculate_distance("JN48QM", "QF67") - 16508) < 1
assert abs(calculate_distance("JN48", "QF67") - 16535) < 1
def test_calculate_distance_invalid_inputs(self): def test_calculate_distance_invalid_inputs(self):
with pytest.raises(AttributeError): with pytest.raises(AttributeError):

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,11 +1,15 @@
import pytest import pytest
import json import json
from datetime import datetime, timezone from datetime import datetime
import pytz
import redis import redis
from pyhamtools import LookupLib, Callinfo from pyhamtools import LookupLib, Callinfo
UTC = pytz.UTC
r = redis.Redis() r = redis.Redis()
@ -40,7 +44,7 @@ class TestStoreDataInRedis:
with pytest.raises(KeyError): with pytest.raises(KeyError):
fix_redis.is_invalid_operation("VK0MC") fix_redis.is_invalid_operation("VK0MC")
timestamp = datetime(year=1994, month=12, day=30, tzinfo=timezone.utc) timestamp = datetime(year=1994, month=12, day=30).replace(tzinfo=UTC)
assert fix_redis.is_invalid_operation("VK0MC", timestamp) assert fix_redis.is_invalid_operation("VK0MC", timestamp)
with pytest.raises(KeyError): with pytest.raises(KeyError):
@ -57,7 +61,7 @@ class TestStoreDataInRedis:
assert lib.lookup_prefix("DH") == fixCountryFile.lookup_prefix("DH") assert lib.lookup_prefix("DH") == fixCountryFile.lookup_prefix("DH")
def test_redis_lookup(self, fixClublogXML, fix_redis): def test_redis_lookup(self, fixClublogXML, fix_redis):
timestamp = datetime(year=2016, month=1, day=20, tzinfo=timezone.utc) timestamp = datetime(year=2016, month=1, day=20, tzinfo=UTC)
ci = Callinfo(fix_redis) ci = Callinfo(fix_redis)
assert ci.get_all("VP8STI", timestamp) == response_Exception_VP8STI_with_start_and_stop_date assert ci.get_all("VP8STI", timestamp) == response_Exception_VP8STI_with_start_and_stop_date
assert ci.get_all("tu5pct") == response_TU5PCT assert ci.get_all("tu5pct") == response_TU5PCT

View file

@ -1,22 +1,16 @@
import os import os
import sys
import datetime import datetime
from .execfile import execfile from past.builtins import execfile
from future.utils import iteritems
import pytest import pytest
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)
from pyhamtools.qsl import get_lotw_users from pyhamtools.qsl import get_lotw_users
if sys.version_info.major == 3:
unicode = str
test_dir = os.path.dirname(os.path.abspath(__file__)) test_dir = os.path.dirname(os.path.abspath(__file__))
fix_dir = os.path.join(test_dir, 'fixtures') fix_dir = os.path.join(test_dir, 'fixtures')
@ -29,13 +23,12 @@ class Test_lotw_methods:
execfile(os.path.join(fix_dir,"lotw_fixture.py"), namespace) execfile(os.path.join(fix_dir,"lotw_fixture.py"), namespace)
assert get_lotw_users(url=httpserver.url) == namespace['lotw_fixture'] assert get_lotw_users(url=httpserver.url) == namespace['lotw_fixture']
@pytest.mark.skip("ARRL has been hacked in May 2024; skipping until LOTW is again up")
def test_download_lotw_list_and_check_types(self): def test_download_lotw_list_and_check_types(self):
data = get_lotw_users() data = get_lotw_users()
assert isinstance(data, dict) assert isinstance(data, dict)
for key, value in data.items(): for key, value in iteritems(data):
assert isinstance(key, str) assert isinstance(key, unicode)
assert isinstance(value, datetime.datetime ) assert isinstance(value, datetime.datetime )
assert len(data) > 1000 assert len(data) > 1000

View file

@ -65,7 +65,6 @@ class Test_utils_freq_to_band():
assert freq_to_band(1200000) == {"band" : 0.23, "mode":None} assert freq_to_band(1200000) == {"band" : 0.23, "mode":None}
def test_shf_frequencies(self): def test_shf_frequencies(self):
assert freq_to_band(2320200) == {"band" : 0.13, "mode":None}
assert freq_to_band(2390000) == {"band" : 0.13, "mode":None} assert freq_to_band(2390000) == {"band" : 0.13, "mode":None}
assert freq_to_band(3300000) == {"band" : 0.09, "mode":None} assert freq_to_band(3300000) == {"band" : 0.09, "mode":None}