From 6d3fe0ee87eb3c7f3cb4fc6fa6049f57b33712d7 Mon Sep 17 00:00:00 2001 From: Greg Albrecht <gba@onbeep.com> Date: Mon, 23 Sep 2013 23:18:52 -0700 Subject: [PATCH] updating for lint, coverage and style. --- LICENSE | 13 +++ MANIFEST.in | 1 + Makefile | 19 ++-- aprs/classes.py | 37 +++++++- aprs/decimaldegrees.py | 17 ++-- aprs/util.py | 129 ++++++++++++++++++-------- requirements.txt | 1 - setup.cfg | 12 ++- setup.py | 53 +++++++++-- tests/__init__.py | 1 + tests/context.py | 4 +- tests/test_aprs.py | 27 +++++- tests/test_util.py | 205 +++++++++++++++++++++++++++++++++++++---- 13 files changed, 423 insertions(+), 96 deletions(-) create mode 100644 LICENSE create mode 100644 MANIFEST.in diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..353e988 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2013 OnBeep, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..738616d --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include README.rst LICENSE requirements.txt \ No newline at end of file diff --git a/Makefile b/Makefile index c9d5620..bf5972d 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,13 @@ -# TODO: add boilerplate to Makefile. +# Makefile for APRS Python Module. +# +# Source:: https://github.com/ampledata/aprs +# Author:: Greg Albrecht W2GMD <gba@onbeep.com> +# Copyright:: Copyright 2013 OnBeep, Inc. +# License:: Apache License, Version 2.0 # -all: install_requirements develop todo +all: install_requirements develop develop: python setup.py develop @@ -16,13 +21,10 @@ install: uninstall: pip uninstall -y aprs -todo: - grep \#\ TODO Makefile - clean: rm -rf *.egg* build dist *.py[oc] */*.py[co] cover doctest_pypi.cfg \ nosetests.xml pylint.log *.egg output.xml flake8.log tests.log \ - test-result.xml htmlcov fab.log + test-result.xml htmlcov fab.log .coverage publish: python setup.py register sdist upload @@ -41,7 +43,6 @@ clonedigger: clonedigger --cpd-output aprs tests lint: - pylint -f parseable -i y -r y aprs/*.py tests/*.py *.py | \ - tee pylint.log + pylint -i y -r n aprs/*.py tests/*.py *.py # -f colorized -test: install_requirements lint clonedigger flake8 nosetests \ No newline at end of file +test: install_requirements lint clonedigger flake8 nosetests diff --git a/aprs/classes.py b/aprs/classes.py index ae2eaa1..7f7c9fa 100755 --- a/aprs/classes.py +++ b/aprs/classes.py @@ -1,9 +1,11 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +"""APRS Class Definitions""" + __author__ = 'Greg Albrecht W2GMD <gba@onbeep.com>' +__license__ = 'Apache License, Version 2.0' __copyright__ = 'Copyright 2013 OnBeep, Inc.' -__license__ = 'Apache 2.0' import logging @@ -14,18 +16,40 @@ import requests import kiss import aprs.constants +import aprs.util class APRS(object): - logger = logging.getLogger('aprs') - logger.addHandler(logging.StreamHandler()) + """APRS-IS Object.""" + + logger = logging.getLogger(__name__) + logger.setLevel(aprs.constants.LOG_LEVEL) + console_handler = logging.StreamHandler() + console_handler.setLevel(aprs.constants.LOG_LEVEL) + formatter = logging.Formatter(aprs.constants.LOG_FORMAT) + console_handler.setFormatter(formatter) + logger.addHandler(console_handler) + logger.propagate = False def __init__(self, user, password='-1', input_url=None): self._url = input_url or aprs.constants.APRSIS_URL self._auth = ' '.join(['user', user, 'pass', password]) def send(self, message, headers=None): + """ + Sends message to APRS-IS send-only interface. + + :param message: Message to send to APRS-IS. + :param headers: Optional headers to post. + :type message: str + :type headers: dict + + :return: True on 204 success, False otherwise. + :rtype: bool + """ + self.logger.debug('message=%s headers=%s', message, headers) + headers = headers or aprs.constants.APRSIS_HTTP_HEADERS content = "\n".join([self._auth, message]) @@ -36,6 +60,13 @@ class APRS(object): class APRSKISS(kiss.KISS): + """APRS interface for KISS serial devices.""" + def write(self, frame): + """Writes APRS-encoded frame to KISS device. + + :param frame: APRS frame to write to KISS device. + :type frame: str + """ encoded_frame = aprs.util.encode_frame(frame) super(APRSKISS, self).write(encoded_frame) diff --git a/aprs/decimaldegrees.py b/aprs/decimaldegrees.py index 304ed57..eb47ff4 100644 --- a/aprs/decimaldegrees.py +++ b/aprs/decimaldegrees.py @@ -123,14 +123,14 @@ def dms2decimal(degrees, minutes, seconds): """ decimal = D(0) - deg = D(str(degrees)) - min = libdecimal.getcontext().divide(D(str(minutes)), D(60)) - sec = libdecimal.getcontext().divide(D(str(seconds)), D(3600)) + degs = D(str(degrees)) + mins = libdecimal.getcontext().divide(D(str(minutes)), D(60)) + secs = libdecimal.getcontext().divide(D(str(seconds)), D(3600)) if degrees >= D(0): - decimal = deg + min + sec + decimal = degs + mins + secs else: - decimal = deg - min - sec + decimal = degs - mins - secs return libdecimal.getcontext().normalize(decimal) @@ -151,11 +151,12 @@ def dm2decimal(degrees, minutes): return dms2decimal(degrees, minutes, 0) -def run_doctest(): +def run_doctest(): # pragma: no cover + """Runs doctests for this module.""" import doctest - import decimaldegrees + import decimaldegrees # pylint: disable=W0406 return doctest.testmod(decimaldegrees) if __name__ == '__main__': - run_doctest() + run_doctest() # pragma: no cover diff --git a/aprs/util.py b/aprs/util.py index 64451bf..bd28723 100755 --- a/aprs/util.py +++ b/aprs/util.py @@ -12,7 +12,6 @@ import logging import aprs.constants import aprs.decimaldegrees -import aprs.util import kiss.constants @@ -20,26 +19,27 @@ logger = logging.getLogger(__name__) logger.setLevel(aprs.constants.LOG_LEVEL) console_handler = logging.StreamHandler() console_handler.setLevel(aprs.constants.LOG_LEVEL) -formatter = logging.Formatter(aprs.constants.LOG_FORMAT) -console_handler.setFormatter(formatter) +console_handler.setFormatter( + logging.Formatter(aprs.constants.LOG_FORMAT)) logger.addHandler(console_handler) logger.propagate = False -# http://stackoverflow.com/questions/2056750/lat-long-to-minutes-and-seconds def dec2dm_lat(dec): """Converts DecDeg to APRS Coord format. See: http://ember2ash.com/lat.htm + Source: http://stackoverflow.com/questions/2056750 + Example: >>> test_lat = 37.7418096 >>> aprs_lat = dec2dm_lat(test_lat) >>> aprs_lat '3744.51N' """ - dm = aprs.decimaldegrees.decimal2dm(dec) + dec_min = aprs.decimaldegrees.decimal2dm(dec) - deg = dm[0] + deg = dec_min[0] abs_deg = abs(deg) if not deg == abs_deg: @@ -47,7 +47,7 @@ def dec2dm_lat(dec): else: suffix = 'N' - return ''.join([str(abs_deg), "%.2f" % dm[1], suffix]) + return ''.join([str(abs_deg), "%.2f" % dec_min[1], suffix]) def dec2dm_lng(dec): @@ -60,9 +60,9 @@ def dec2dm_lng(dec): >>> aprs_lng '12223.30W' """ - dm = aprs.decimaldegrees.decimal2dm(dec) + dec_min = aprs.decimaldegrees.decimal2dm(dec) - deg = dm[0] + deg = dec_min[0] abs_deg = abs(deg) if not deg == abs_deg: @@ -70,22 +70,13 @@ def dec2dm_lng(dec): else: suffix = 'E' - return ''.join([str(abs_deg), "%.2f" % dm[1], suffix]) + return ''.join([str(abs_deg), "%.2f" % dec_min[1], suffix]) -# TODO: Convert doctest to unittest. def decode_aprs_ascii_frame(ascii_frame): """ Breaks an ASCII APRS Frame down to it's constituent parts. - Test & Example - ~~~~ - - >>> frame = 'W2GMD-9>APOTC1,WIDE1-1,WIDE2-1:!3745.94N/12228.05W>118/010/A=000269 38C=Temp http://w2gmd.org/ Twitter: @ampledata' - >>> decode_aprs_ascii_frame(frame) - {'source': 'W2GMD-9', 'destination': 'APOTC1', 'text': '!3745.94N/12228.05W>118/010/A=000269 38C=Temp http://w2gmd.org/ Twitter: @ampledata', 'path': 'APOTC1,WIDE1-1,WIDE2-1'} - - :param frame: ASCII APRS Frame. :type frame: str @@ -96,15 +87,15 @@ def decode_aprs_ascii_frame(ascii_frame): decoded_frame = {} frame_so_far = '' - for c in ascii_frame: - if '>' in c and not 'source' in decoded_frame: + for char in ascii_frame: + if '>' in char and not 'source' in decoded_frame: decoded_frame['source'] = frame_so_far frame_so_far = '' - elif ':' in c and not 'path' in decoded_frame: + elif ':' in char and not 'path' in decoded_frame: decoded_frame['path'] = frame_so_far frame_so_far = '' else: - frame_so_far = ''.join([frame_so_far, c]) + frame_so_far = ''.join([frame_so_far, char]) decoded_frame['text'] = frame_so_far decoded_frame['destination'] = decoded_frame['path'].split(',')[0] @@ -113,6 +104,15 @@ def decode_aprs_ascii_frame(ascii_frame): def format_aprs_frame(frame): + """ + Formats APRS frame-as-dict into APRS frame-as-string. + + :param frame: APRS frame-as-dict + :type frame: dict + + :return: APRS frame-as-string. + :rtype: str + """ formatted_frame = '>'.join([frame['source'], frame['destination']]) formatted_frame = ','.join([formatted_frame, frame['path']]) formatted_frame = ':'.join([formatted_frame, frame['text']]) @@ -120,6 +120,15 @@ def format_aprs_frame(frame): def create_callsign(raw_callsign): + """ + Creates callsign-as-dict from callsign-as-string. + + :param raw_callsign: Callsign-as-string (with or without ssid). + :type raw_callsign: str + + :return: Callsign-as-dict. + :rtype: dict + """ if '-' in raw_callsign: call_sign, ssid = raw_callsign.split('-') else: @@ -165,14 +174,12 @@ def valid_callsign(callsign): logger.debug('callsign=%s ssid=%s', callsign, ssid) - if len(callsign) < 2 or len(callsign) > 6: - return False - - if len(str(ssid)) < 1 or len(str(ssid)) > 2: + if (len(callsign) < 2 or len(callsign) > 6 or len(str(ssid)) < 1 or + len(str(ssid)) > 2): return False - for c in callsign: - if not (c.isalpha() or c.isdigit()): + for char in callsign: + if not (char.isalpha() or char.isdigit()): return False if not str(ssid).isdigit(): @@ -198,6 +205,14 @@ def extract_callsign(raw_frame): def extract_path(start, raw_frame): + """Extracts path from raw APRS KISS frame. + + :param start: + :param raw_frame: Raw APRS frame from a KISS device. + + :return: Full path from APRS frame. + :rtype: list + """ full_path = [] for i in range(2, start): @@ -207,30 +222,58 @@ def extract_path(start, raw_frame): full_path.append(''.join([path, '*'])) else: full_path.append(path) + return full_path def format_path(start, raw_frame): + """ + Formats path from raw APRS KISS frame. + + :param start: + :param raw_frame: Raw APRS KISS frame. + + :return: Formatted APRS path. + :rtype: str + """ return ','.join(extract_path(start, raw_frame)) def encode_callsign(callsign): + """ + Encodes a callsign-dict within a KISS frame. + + :param callsign: Callsign-dict to encode. + :type callsign: dict + + :return: KISS-encoded callsign. + :rtype: str + """ call_sign = callsign['callsign'] - ct = (callsign['ssid'] << 1) | 0x60 + enc_ssid = (callsign['ssid'] << 1) | 0x60 if '*' in call_sign: call_sign = call_sign.replace('*', '') - ct |= 0x80 + enc_ssid |= 0x80 while len(call_sign) < 6: call_sign = ''.join([call_sign, ' ']) encoded = ''.join([chr(ord(p) << 1) for p in call_sign]) - return ''.join([encoded, chr(ct)]) + return ''.join([encoded, chr(enc_ssid)]) def encode_frame(frame): + """ + Encodes an APRS frame-as-dict as a KISS frame. + + :param frame: APRS frame-as-dict to encode. + :type frame: dict + + :return: KISS-encoded APRS frame. + :rtype: str + """ enc_frame = ''.join([ encode_callsign(create_callsign(frame['destination'])), encode_callsign(create_callsign(frame['source'])), @@ -248,6 +291,15 @@ def encode_frame(frame): def decode_frame(raw_frame): + """ + Decodes a KISS-encoded APRS frame. + + :param raw_frame: KISS-encoded frame to decode. + :type raw_frame: str + + :return: APRS frame-as-dict. + :rtype: dict + """ logging.debug('raw_frame=%s', raw_frame) frame = {} frame_len = len(raw_frame) @@ -256,9 +308,9 @@ def decode_frame(raw_frame): for raw_slice in range(0, frame_len): # Is address field length correct? if ord(raw_frame[raw_slice]) & 0x01 and ((raw_slice + 1) % 7) == 0: - n = (raw_slice + 1) / 7 + i = (raw_slice + 1) / 7 # Less than 2 callsigns? - if 2 < n < 10: + if 2 < i < 10: if (ord(raw_frame[raw_slice + 1]) & 0x03 == 0x03 and ord(raw_frame[raw_slice + 2]) == 0xf0): frame['text'] = raw_frame[raw_slice + 3:] @@ -266,17 +318,18 @@ def decode_frame(raw_frame): extract_callsign(raw_frame)) frame['source'] = full_callsign( extract_callsign(raw_frame[7:])) - frame['path'] = format_path(n, raw_frame) + frame['path'] = format_path(i, raw_frame) logging.debug('frame=%s', frame) return frame -def run_doctest(): +def run_doctest(): # pragma: no cover + """Runs doctests for this module.""" import doctest - import aprs.util + import aprs.util # pylint: disable=W0406 return doctest.testmod(aprs.util) if __name__ == '__main__': - run_doctest() + run_doctest() # pragma: no cover diff --git a/requirements.txt b/requirements.txt index 60144c7..6794368 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,3 @@ clonedigger httpretty nose requests -pyserial \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index fe220ee..828204d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,9 +1,17 @@ +# Nosetests configuration for APRS. +# +# Source:: https://github.com/ampledata/aprs +# Author:: Greg Albrecht W2GMD <gba@onbeep.com> +# Copyright:: Copyright 2013 OnBeep, Inc. +# License:: Apache License, Version 2.0 +# + + [nosetests] with-xunit = 1 with-coverage = 1 cover-html = 1 with-doctest = 1 doctest-tests = 1 -cover-inclusive = 1 +cover-tests = 0 cover-package = aprs -cover-tests = 1 diff --git a/setup.py b/setup.py index dc01831..5593251 100644 --- a/setup.py +++ b/setup.py @@ -1,24 +1,57 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -__author__ = 'Greg Albrecht W2GMD <gba@gregalbrecht.com>' -__copyright__ = 'Copyright 2013 Greg Albrecht' -__license__ = 'Creative Commons Attribution 3.0 Unported License' +""" +Setup for the APRS Python Module. +Source:: https://github.com/ampledata/aprs +""" -import setuptools +__author__ = 'Greg Albrecht W2GMD <gba@onbeep.com>' +__copyright__ = 'Copyright 2013 OnBeep, Inc.' +__license__ = 'Apache License, Version 2.0' -setuptools.setup( - version='1.0.0', +import os +import sys + +import aprs + +try: + from setuptools import setup +except ImportError: + from distutils.core import setup # pylint: disable=F0401,E0611 + + +packages = ['aprs'] +requires = ['requests', 'kiss'] + + +def publish(): + """Function for publishing package to pypi.""" + if sys.argv[-1] == 'publish': + os.system('python setup.py sdist upload') + sys.exit() + + +publish() + + +setup( name='aprs', + version=aprs.__version__, description='Python Bindings for APRS-IS API.', author='Greg Albrecht', - author_email='gba@gregalbrecht.com', - license='Creative Commons Attribution 3.0 Unported License', + author_email='gba@onbeep.com', + packages=packages, + package_data={'': ['LICENSE']}, + license=open('LICENSE').read(), + long_description=open('README.rst').read(), url='https://github.com/ampledata/aprs', setup_requires=['nose'], tests_require=['coverage', 'httpretty', 'nose'], - install_requires=['requests'] - + install_requires=requires, + package_dir={'aprs': 'aprs'}, + zip_safe=False, + include_package_data=True ) diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..d50eb01 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for APRS Python Module.""" diff --git a/tests/context.py b/tests/context.py index 433ac01..a9ab68e 100644 --- a/tests/context.py +++ b/tests/context.py @@ -1,10 +1,12 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +"""Context for tests for APRS Python Module.""" + import os import sys sys.path.insert(0, os.path.abspath('..')) -import aprs +import aprs # pylint: disable=W0611 diff --git a/tests/test_aprs.py b/tests/test_aprs.py index 4e55f9d..33ff8e0 100644 --- a/tests/test_aprs.py +++ b/tests/test_aprs.py @@ -1,9 +1,11 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +"""Tests for Python APRS-IS Bindings.""" + __author__ = 'Greg Albrecht W2GMD <gba@onbeep.com>' +__license__ = 'Apache License, Version 2.0' __copyright__ = 'Copyright 2013 OnBeep, Inc.' -__license__ = 'Apache 2.0' import random @@ -22,8 +24,8 @@ POSITIVE_NUMBERS = NUMBERS[1:] ALPHANUM = ''.join([ALPHABET, NUMBERS]) -class APRSTest(unittest.TestCase): - """Tests for Python APRS Bindings.""" +class APRSTest(unittest.TestCase): # pylint: disable=R0904 + """Tests for Python APRS-IS Bindings.""" logger = logging.getLogger(__name__) logger.setLevel(aprs.constants.LOG_LEVEL) @@ -35,9 +37,17 @@ class APRSTest(unittest.TestCase): logger.propagate = False def random(self, length=8, alphabet=ALPHANUM): + """ + Generates a random string for test cases. + + :param length: Length of string to generate. + :param alphabet: Alphabet to use to create string. + :type length: int + :type alphabet: str + """ return ''.join(random.choice(alphabet) for _ in xrange(length)) - def setUp(self): + def setUp(self): # pylint: disable=C0103 self.fake_server = ''.join([ 'http://localhost:', self.random(4, POSITIVE_NUMBERS), @@ -60,6 +70,9 @@ class APRSTest(unittest.TestCase): @httpretty.httprettified def test_fake_good_auth(self): + """ + Tests authenticating against APRS-IS using a valid call+pass. + """ httpretty.HTTPretty.register_uri( httpretty.HTTPretty.POST, self.fake_server, @@ -83,6 +96,9 @@ class APRSTest(unittest.TestCase): @httpretty.httprettified def test_fake_bad_auth(self): + """ + Tests authenticating against APRS-IS using an invalid call+pass. + """ httpretty.HTTPretty.register_uri( httpretty.HTTPretty.POST, self.fake_server, @@ -106,6 +122,9 @@ class APRSTest(unittest.TestCase): @unittest.skip('Test only works with real server.') def test_more(self): + """ + Tests APRS-IS binding against a real APRS-IS server. + """ aprs_conn = aprs.APRS( user=self.real_callsign, input_url=self.real_server diff --git a/tests/test_util.py b/tests/test_util.py index 21f2a4a..8fba5ba 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,9 +1,11 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +"""Tests for Python APRS util methods.""" + __author__ = 'Greg Albrecht W2GMD <gba@onbeep.com>' __copyright__ = 'Copyright 2013 OnBeep, Inc.' -__license__ = 'Apache 2.0' +__license__ = 'Apache License, Version 2.0' import unittest @@ -25,7 +27,7 @@ INVALID_CALLSIGNS = ['xW2GMDx', 'W2GMD-16', 'W2GMD-A', 'W', 'W2GMD-1-0', 'W*GMD', 'W2GMD-123'] -class APRSUtilTestCase(unittest.TestCase): +class APRSUtilTestCase(unittest.TestCase): # pylint: disable=R0904 """Tests for Python APRS Utils.""" logger = logging.getLogger(__name__) @@ -37,16 +39,16 @@ class APRSUtilTestCase(unittest.TestCase): logger.addHandler(console_handler) logger.propagate = False - def setUp(self): + def setUp(self): # pylint: disable=C0103 """Setup.""" self.test_frames = open(constants.TEST_FRAMES, 'r') self.test_frame = self.test_frames.readlines()[0].strip() - def tearDown(self): + def tearDown(self): # pylint: disable=C0103 """Teardown.""" self.test_frames.close() - def test_latitude(self): + def test_latitude_north(self): """Test Decimal to APRS Latitude conversion. Spec per ftp://ftp.tapr.org/aprssig/aprsspec/spec/aprs101/APRS101.pdf @@ -70,14 +72,45 @@ class APRSUtilTestCase(unittest.TestCase): self.logger.debug('aprs_lat=%s', aprs_lat) lat_deg = int(aprs_lat.split('.')[0][:1]) - lat_hsec = aprs_lat.split('.')[1] + #lat_hsec = aprs_lat.split('.')[1] self.assertTrue(len(aprs_lat) == 8) self.assertTrue(lat_deg >= 00) self.assertTrue(lat_deg <= 90) self.assertTrue(aprs_lat.endswith('N')) - def test_longitude(self): + def test_latitude_south(self): + """Test Decimal to APRS Latitude conversion. + + Spec per ftp://ftp.tapr.org/aprssig/aprsspec/spec/aprs101/APRS101.pdf + -- + Latitude is expressed as a fixed 8-character field, in degrees and + decimal minutes (to two decimal places), followed by the letter N for + north or S for south. Latitude degrees are in the range 00 to 90. + Latitude minutes are expressed as whole minutes and hundredths of a + minute, separated by a decimal point. + + For example: + + 4903.50N is 49 degrees 3 minutes 30 seconds north. + + In generic format examples, the latitude is shown as the 8-character + string ddmm.hhN (i.e. degrees, minutes and hundredths of a minute + north). + """ + test_lat = -37.7418096 + aprs_lat = aprs.util.dec2dm_lat(test_lat) + self.logger.debug('aprs_lat=%s', aprs_lat) + + lat_deg = int(aprs_lat.split('.')[0][:1]) + #lat_hsec = aprs_lat.split('.')[1] + + self.assertTrue(len(aprs_lat) == 8) + self.assertTrue(lat_deg >= 00) + self.assertTrue(lat_deg <= 90) + self.assertTrue(aprs_lat.endswith('S')) + + def test_longitude_west(self): """Test Decimal to APRS Longitude conversion. Spec per ftp://ftp.tapr.org/aprssig/aprsspec/spec/aprs101/APRS101.pdf @@ -89,7 +122,7 @@ class APRSUtilTestCase(unittest.TestCase): Longitude degrees are in the range 000 to 180. Longitude minutes are expressed as whole minutes and hundredths of a minute, separated by a decimal point. - + For example: 07201.75W is 72 degrees 1 minute 45 seconds west. @@ -103,31 +136,84 @@ class APRSUtilTestCase(unittest.TestCase): self.logger.debug('aprs_lng=%s', aprs_lng) lng_deg = int(aprs_lng.split('.')[0][:2]) - lng_hsec = aprs_lng.split('.')[1] + #lng_hsec = aprs_lng.split('.')[1] self.assertTrue(len(aprs_lng) == 9) self.assertTrue(lng_deg >= 000) self.assertTrue(lng_deg <= 180) self.assertTrue(aprs_lng.endswith('W')) + def test_longitude_east(self): + """Test Decimal to APRS Longitude conversion. + + Spec per ftp://ftp.tapr.org/aprssig/aprsspec/spec/aprs101/APRS101.pdf + -- + Longitude is expressed as a fixed 9-character field, in degrees and + decimal minutes (to two decimal places), followed by the letter E for + east or W for west. + + Longitude degrees are in the range 000 to 180. Longitude minutes are + expressed as whole minutes and hundredths of a minute, separated by a + decimal point. + + For example: + + 07201.75W is 72 degrees 1 minute 45 seconds west. + + In generic format examples, the longitude is shown as the 9-character + string dddmm.hhW (i.e. degrees, minutes and hundredths of a minute + west). + """ + test_lng = 122.38833 + aprs_lng = aprs.util.dec2dm_lng(test_lng) + self.logger.debug('aprs_lng=%s', aprs_lng) + + lng_deg = int(aprs_lng.split('.')[0][:2]) + #lng_hsec = aprs_lng.split('.')[1] + + self.assertTrue(len(aprs_lng) == 9) + self.assertTrue(lng_deg >= 000) + self.assertTrue(lng_deg <= 180) + self.assertTrue(aprs_lng.endswith('E')) + def test_valid_callsign_valid(self): - for c in VALID_CALLSIGNS: - self.assertTrue(aprs.util.valid_callsign(c), "%s is a valid call" % c) + """ + Tests valid callsigns using `aprs.util.valid_callsign()`. + """ + for i in VALID_CALLSIGNS: + self.assertTrue( + aprs.util.valid_callsign(i), "%s is a valid call" % i) def test_valid_callsign_invalid(self): - for c in INVALID_CALLSIGNS: - self.assertFalse(aprs.util.valid_callsign(c), "%s is an invalid call" % c) + """ + Tests invalid callsigns using `aprs.util.valid_callsign()`. + """ + for i in INVALID_CALLSIGNS: + self.assertFalse( + aprs.util.valid_callsign(i), "%s is an invalid call" % i) def test_extract_callsign_source(self): + """ + Tests extracting the source callsign from a KISS-encoded APRS frame + using `aprs.util.extract_callsign()`. + """ callsign = {'callsign': 'W2GMD', 'ssid': 6} extracted_callsign = aprs.util.extract_callsign(self.test_frame[7:]) self.assertEqual(callsign, extracted_callsign) - def test_extract_callsign_destination(self): + def test_extract_callsign_dest(self): + """ + Tests extracting the destination callsign from a KISS-encoded APRS + frame using `aprs.util.extract_callsign()`. + """ extracted_callsign = aprs.util.extract_callsign(self.test_frame) self.assertEqual(extracted_callsign['callsign'], 'APRX24') def test_full_callsign_with_ssid(self): + """ + Tests creating a full callsign string from a callsign+ssid dict using + `aprs.util.full_callsign()`. + """ callsign = { 'callsign': 'W2GMD', 'ssid': 1 @@ -136,6 +222,10 @@ class APRSUtilTestCase(unittest.TestCase): self.assertEqual(full_callsign, 'W2GMD-1') def test_full_callsign_sans_ssid(self): + """ + Tests creating a full callsign string from a callsign dict using + `aprs.util.full_callsign()`. + """ callsign = { 'callsign': 'W2GMD', 'ssid': 0 @@ -144,6 +234,10 @@ class APRSUtilTestCase(unittest.TestCase): self.assertEqual(full_callsign, 'W2GMD') def test_format_aprs_frame(self): + """ + Tests formatting an APRS frame-as-string from an APRS frame-as-dict + using `aprs.util.format_aprs_frame()`. + """ frame = { 'source': 'W2GMD-1', 'destination': 'OMG', @@ -157,22 +251,46 @@ class APRSUtilTestCase(unittest.TestCase): ) def test_decode_aprs_ascii_frame(self): - ascii_frame = 'W2GMD-9>APOTC1,WIDE1-1,WIDE2-1:!3745.94N/12228.05W>118/010/A=000269 38C=Temp http://w2gmd.org/ Twitter: @ampledata' + """ + Tests creating an APRS frame-as-dict from an APRS frame-as-string + using `aprs.util.decode_aprs_ascii_frame()`. + """ + ascii_frame = ( + 'W2GMD-9>APOTC1,WIDE1-1,WIDE2-1:!3745.94N/12228.05W>118/010/' + 'A=000269 38C=Temp http://w2gmd.org/ Twitter: @ampledata') frame = aprs.util.decode_aprs_ascii_frame(ascii_frame) self.assertEqual( - frame, - {'source': 'W2GMD-9', 'destination': 'APOTC1', 'text': '!3745.94N/12228.05W>118/010/A=000269 38C=Temp http://w2gmd.org/ Twitter: @ampledata', 'path': 'APOTC1,WIDE1-1,WIDE2-1'} + { + 'source': 'W2GMD-9', + 'destination': 'APOTC1', + 'path': 'APOTC1,WIDE1-1,WIDE2-1', + 'text': ('!3745.94N/12228.05W>118/010/A=000269 38C=Temp ' + 'http://w2gmd.org/ Twitter: @ampledata'), + }, + frame ) def test_extract_path(self): + """ + Tests extracting the APRS path from a KISS-encoded frame + using `aprs.util.extract_path()`. + """ extracted_path = aprs.util.extract_path(3, self.test_frame) self.assertEqual('WIDE1-1', extracted_path[0]) def test_format_path(self): + """ + Tests formatting an APRS path from a KISS-encoded frame + using `aprs.util.format_path()`. + """ extracted_path = aprs.util.format_path(3, self.test_frame) self.assertEqual('WIDE1-1', extracted_path) def test_encode_frame(self): + """ + Tests KISS-encoding an APRS frame using + `aprs.util.encode_frame()`. + """ frame = { 'source': 'W2GMD-1', 'destination': 'OMG', @@ -180,19 +298,29 @@ class APRSUtilTestCase(unittest.TestCase): 'text': 'test_encode_frame' } encoded_frame = aprs.util.encode_frame(frame) - legit = '\x9e\x9a\x8e@@@`\xaed\x8e\x9a\x88@b\xae\x92\x88\x8ab@c\x03\xf0test_encode_frame' + legit = ('\x9e\x9a\x8e@@@`\xaed\x8e\x9a\x88@b' + '\xae\x92\x88\x8ab@c\x03\xf0test_encode_frame') self.assertEqual(legit, encoded_frame) def test_decode_frame_recorded(self): + """ + Tests decoding a KISS-encoded APRS frame using + `aprs.util.decode_frame()`. + """ frame = { - 'text': '!3745.75NI12228.05W#W2GMD-6 Inner Sunset, SF iGate/Digipeater http://w2gmd.org', 'path': 'WIDE1-1', 'destination': 'APRX24', - 'source': 'W2GMD-6' + 'source': 'W2GMD-6', + 'text': ('!3745.75NI12228.05W#W2GMD-6 Inner Sunset, ' + 'SF iGate/Digipeater http://w2gmd.org') } self.assertEqual(frame, aprs.util.decode_frame(self.test_frame)) def test_decode_frame(self): + """ + Tests decoding a KISS-encoded APRS frame using + `aprs.util.decode_frame()`. + """ frame = { 'source': 'W2GMD-1', 'destination': 'OMG', @@ -203,5 +331,42 @@ class APRSUtilTestCase(unittest.TestCase): decoded_frame = aprs.util.decode_frame(encoded_frame) self.assertEqual(frame, decoded_frame) + def test_create_callsign(self): + """ + Tests creating a callsign string from a callsign dict using + `aprs.util.create_callsign()`. + """ + full_callsign = 'W2GMD-1' + callsign = aprs.util.create_callsign(full_callsign) + self.assertEqual({'callsign': 'W2GMD', 'ssid': 1}, callsign) + + def test_full_callsign(self): + """ + Tests converting a callsign dict to a callsing string + (callsign-ssid) using `aprs.util.full_callsign()`. + """ + callsign = {'callsign': 'W2GMD', 'ssid': 1} + full_callsign = aprs.util.full_callsign(callsign) + self.assertEqual('W2GMD-1', full_callsign) + + def test_encode_callsign_digipeated(self): + """ + Tests encoding a digipeated callsign with + `aprs.util.encode_callsign()`. + """ + callsign = {'callsign': 'W2GMD*', 'ssid': 1} + encoded_callsign = aprs.util.encode_callsign(callsign) + self.assertEqual('\xaed\x8e\x9a\x88@\xe2', encoded_callsign) + + def test_encode_callsign(self): + """ + Tests encoding a non-digipeated callsign with + `aprs.util.encode_callsign()`. + """ + callsign = {'callsign': 'W2GMD', 'ssid': 1} + encoded_callsign = aprs.util.encode_callsign(callsign) + self.assertEqual('\xaed\x8e\x9a\x88@b', encoded_callsign) + + if __name__ == '__main__': unittest.main() -- GitLab