From d1a50c655978c51c5d591755826e72b912cdb8c2 Mon Sep 17 00:00:00 2001 From: "Walter A. Boring IV" Date: Wed, 15 May 2019 20:12:59 +0000 Subject: [PATCH] Added tox support This patch adds support for tox. Tox is used to run various python compliance tests. This enables pep8 tests, as well as python2 and python3 compatibility as well as coverage and documentation building. --- aprsd/fuzzyclock.py | 159 ++++++++++--------- aprsd/main.py | 353 +++++++++++++++++++++++++----------------- aprsd/utils.py | 7 +- test-requirements.txt | 1 + tools/fast8.sh | 25 +++ tox.ini | 39 +++++ 6 files changed, 364 insertions(+), 220 deletions(-) create mode 100644 test-requirements.txt create mode 100755 tools/fast8.sh create mode 100644 tox.ini diff --git a/aprsd/fuzzyclock.py b/aprsd/fuzzyclock.py index aae0e2c..6f95f94 100644 --- a/aprsd/fuzzyclock.py +++ b/aprsd/fuzzyclock.py @@ -17,98 +17,105 @@ import sys import time + def fuzzy(hour, minute, degree=1): - '''Implements the fuzzy clock. - returns the the string that spells out the time - hour:minute - Supports two degrees of fuzziness. Set with degree = 1 or degree = 2 - When degree = 1, time is in quantum of 5 minutes. - When degree = 2, time is in quantum of 15 minutes.''' + '''Implements the fuzzy clock. + returns the the string that spells out the time - hour:minute + Supports two degrees of fuzziness. Set with degree = 1 or degree = 2 + When degree = 1, time is in quantum of 5 minutes. + When degree = 2, time is in quantum of 15 minutes.''' - if degree<=0 or degree>2: - print 'Please use a degree of 1 or 2. Using fuzziness degree=1' - degree = 1 + if degree <= 0 or degree > 2: + print('Please use a degree of 1 or 2. Using fuzziness degree=1') + degree = 1 - begin = 'It\'s ' + begin = 'It\'s ' - f0 = 'almost ' - f1 = 'exactly ' - f2 = 'around ' + f0 = 'almost ' + f1 = 'exactly ' + f2 = 'around ' - b0 = ' past ' - b1 = ' to ' + b0 = ' past ' + b1 = ' to ' - hourList = ('One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten', 'Eleven', 'Twelve') + hourList = ('One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', + 'Nine', 'Ten', 'Eleven', 'Twelve') - s1 = s2 = s3 = s4 = '' - base = 5 + s1 = s2 = s3 = s4 = '' + base = 5 - if degree == 1: - base = 5 - val = ('Five', 'Ten', 'Quarter', 'Twenty', 'Twenty-Five', 'Half') - elif degree == 2: - base = 15 - val = ('Quarter', 'Half') + if degree == 1: + base = 5 + val = ('Five', 'Ten', 'Quarter', 'Twenty', 'Twenty-Five', 'Half') + elif degree == 2: + base = 15 + val = ('Quarter', 'Half') - dmin = minute % base # to find whether we have to use 'almost', 'exactly' or 'around' - if minute > 30: - pos = int((60 - minute) / base) #position in the tuple 'val' - else: - pos = int(minute / base) + # to find whether we have to use 'almost', 'exactly' or 'around' + dmin = minute % base + if minute > 30: + pos = int((60 - minute) / base) # position in the tuple 'val' + else: + pos = int(minute / base) - if dmin == 0: - s1 = f1 - pos = pos - 1 - elif dmin <= base/2: - s1 = f2 - if minute < 30: - pos = pos-1 - else: - s1 = f0 - if minute > 30: - pos = pos -1 + if dmin == 0: + s1 = f1 + pos = pos - 1 + elif dmin <= base/2: + s1 = f2 + if minute < 30: + pos = pos-1 + else: + s1 = f0 + if minute > 30: + pos = pos - 1 - s2 = val[pos] + s2 = val[pos] - if minute <= base/2: # Case like "It's around/exactly Ten" - s2 = s3 = '' - s4 = hourList[hour - 12 - 1] - elif minute >= 60-base/2: # Case like "It's almost Ten" - s2 = s3 = '' - s4 = hourList[hour - 12] - else: # Other cases with all words, like "It's around Quarter past One" - if minute>30: - s3 = b1 # to - s4 = hourList[hour - 12] - else: - s3 = b0 # past - s4 = hourList[hour - 12 - 1] + if minute <= base / 2: + # Case like "It's around/exactly Ten" + s2 = s3 = '' + s4 = hourList[hour - 12 - 1] + elif minute >= 60 - base / 2: + # Case like "It's almost Ten" + s2 = s3 = '' + s4 = hourList[hour - 12] + else: + # Other cases with all words, like "It's around Quarter past One" + if minute > 30: + s3 = b1 # to + s4 = hourList[hour - 12] + else: + s3 = b0 # past + s4 = hourList[hour - 12 - 1] + + return begin + s1 + s2 + s3 + s4 - return begin + s1 + s2 + s3 + s4 def main(): - deg = 1 - stm = time.localtime() - h = stm.tm_hour - m = stm.tm_min + deg = 1 + stm = time.localtime() + h = stm.tm_hour + m = stm.tm_min - if len(sys.argv)>=2: - try: - deg = int(sys.argv[1]) - except: - print 'Please use a degree of 1 or 2. Using fuzziness degree=1' + if len(sys.argv) >= 2: + try: + deg = int(sys.argv[1]) + except Exception: + print('Please use a degree of 1 or 2. Using fuzziness degree=1') - if len(sys.argv)>=3: - tm = sys.argv[2].split(':') - try: - h = int(tm[0]) - m = int(tm[1]) - if h<0 or h>23 or m<0 or m>59: - raise Exception - except: - print 'Bad time entered. Using the system time.' - h = stm.tm_hour - m = stm.tm_min - print fuzzy(h, m, deg) - return + if len(sys.argv) >= 3: + tm = sys.argv[2].split(':') + try: + h = int(tm[0]) + m = int(tm[1]) + if h < 0 or h > 23 or m < 0 or m > 59: + raise Exception + except Exception: + print('Bad time entered. Using the system time.') + h = stm.tm_hour + m = stm.tm_min + print(fuzzy(h, m, deg)) + return main() diff --git a/aprsd/main.py b/aprsd/main.py index 29b6cc5..47a3f7a 100644 --- a/aprsd/main.py +++ b/aprsd/main.py @@ -46,7 +46,7 @@ from imapclient import IMAPClient, SEEN # local imports here from aprsd.fuzzyclock import fuzzy -import utils +from aprsd import utils # setup the global logger LOG = logging.getLogger('APRSD') @@ -58,7 +58,7 @@ CONFIG = None # HOST = "noam.aprs2.net" # north america tier2 servers round robin # USER = "KM6XXX-9" # callsign of this aprs client with SSID # PASS = "99999" # google how to generate this -# BASECALLSIGN = "KM6XXX" # callsign of radio in the field to which we send email +# BASECALLSIGN = "KM6XXX" # callsign of radio in the field to send email # shortcuts = { # "aa" : "5551239999@vtext.com", # "cl" : "craiglamparter@somedomain.org", @@ -66,9 +66,18 @@ CONFIG = None # } # globals - tell me a better way to update data being used by threads -email_sent_dict = {} # message_number:time combos so we don't resend the same email in five mins {int:int} -ack_dict = {} # message_nubmer:ack combos so we stop sending a message after an ack from radio {int:int} -message_number = 0 # current aprs radio message number, increments for each message we send over rf {int} + +# message_number:time combos so we don't resend the same email in +# five mins {int:int} +email_sent_dict = {} + +# message_nubmer:ack combos so we stop sending a message after an +# ack from radio {int:int} +ack_dict = {} + +# current aprs radio message number, increments for each message we +# send over rf {int} +message_number = 0 # global telnet connection object tn = None @@ -93,69 +102,82 @@ def setup_connection(): LOG.debug("Setting up telnet connection to '%s:%s'" % (host, port)) try: tn = telnetlib.Telnet(host, port) - except Exception, e: + except Exception: LOG.exception("Telnet session failed.") sys.exit(-1) def signal_handler(signal, frame): LOG.info("Ctrl+C, exiting.") - #sys.exit(0) # thread ignores this + # sys.exit(0) # thread ignores this os._exit(0) -### end signal_handler +# end signal_handler + def parse_email(msgid, data, server): - envelope = data[b'ENVELOPE'] - #print('ID:%d "%s" (%s)' % (msgid, envelope.subject.decode(), envelope.date )) - f = re.search('([\.\w_-]+@[\.\w_-]+)', str(envelope.from_[0]) ) # email address match - if f is not None: - from_addr = f.group(1) - else: - from_addr = "noaddr" - m = server.fetch([msgid], ['RFC822']) - msg = email.message_from_string(m[msgid]['RFC822']) - if msg.is_multipart(): - text = "" - html = None - body = "* unreadable msg received" # default in case body somehow isn't set below - happened once + envelope = data[b'ENVELOPE'] + # print('ID:%d "%s" (%s)' % + # (msgid, envelope.subject.decode(), envelope.date)) + # email address match + f = re.search('([\.\w_-]+@[\.\w_-]+)', str(envelope.from_[0])) + if f is not None: + from_addr = f.group(1) + else: + from_addr = "noaddr" + m = server.fetch([msgid], ['RFC822']) + msg = email.message_from_string(m[msgid]['RFC822']) + if msg.is_multipart(): + text = "" + html = None + + # default in case body somehow isn't set below - happened once + body = "* unreadable msg received" for part in msg.get_payload(): - if part.get_content_charset() is None: - # We cannot know the character set, so return decoded "something" - text = part.get_payload(decode=True) - continue + if part.get_content_charset() is None: + # We cannot know the character set, so return decoded "something" + text = part.get_payload(decode=True) + continue - charset = part.get_content_charset() + charset = part.get_content_charset() - if part.get_content_type() == 'text/plain': - text = unicode(part.get_payload(decode=True), str(charset), "ignore").encode('utf8', 'replace') + if part.get_content_type() == 'text/plain': + text = unicode(part.get_payload(decode=True), str(charset), + "ignore").encode('utf8', 'replace') - if part.get_content_type() == 'text/html': - html = unicode(part.get_payload(decode=True), str(charset), "ignore").encode('utf8', 'replace') + if part.get_content_type() == 'text/html': + html = unicode(part.get_payload(decode=True), str(charset), + "ignore").encode('utf8', 'replace') - if text is not None: - body = text.strip() # strip removes white space fore and aft of string - else: - body = html.strip() - else: - text = unicode(msg.get_payload(decode=True), msg.get_content_charset(), 'ignore').encode('utf8', 'replace') - body = text.strip() + if text is not None: + # strip removes white space fore and aft of string + body = text.strip() + else: + body = html.strip() + else: + text = unicode(msg.get_payload(decode=True), msg.get_content_charset(), + 'ignore').encode('utf8', 'replace') + body = text.strip() - body = re.sub('<[^<]+?>', '', body) # strip all html tags - body = body.replace("\n", " ").replace("\r", " ") # strip CR/LF, make it one line, .rstrip fails at this - return(body, from_addr) -## end parse_email + # strip all html tags + body = re.sub('<[^<]+?>', '', body) + + # strip CR/LF, make it one line, .rstrip fails at this + body = body.replace("\n", " ").replace("\r", " ") + return(body, from_addr) + # end parse_email def resend_email(count): - date = datetime.datetime.now() + date = datetime.datetime.now() month = date.strftime("%B")[:3] # Nov, Mar, Apr - day = date.day - year = date.year - today = str(day) + "-" + month + "-" + str(year) + day = date.day + year = date.year + today = "%s-%s-%s" % (day, month, year) shortcuts = CONFIG['shortcuts'] - shortcuts_inverted = dict([[v,k] for k,v in shortcuts.items()]) # swap key/value + # swap key/value + shortcuts_inverted = dict([[v, k] for k, v in shortcuts.items()]) LOG.debug("resend_email: Connect to IMAP host '%s' with user '%s'" % (CONFIG['imap']['host'], @@ -172,12 +194,16 @@ def resend_email(count): messages.sort(reverse=True) del messages[int(count):] # only the latest "count" messages for message in messages: - for msgid, data in list(server.fetch(message, ['ENVELOPE']).items()): # one at a time, otherwise order is random + for msgid, data in list(server.fetch(message, ['ENVELOPE']).items()): + # one at a time, otherwise order is random (body, from_addr) = parse_email(msgid, data, server) - server.remove_flags(msgid, [SEEN]) # unset seen flag, will stay bold in email client - if from_addr in shortcuts_inverted: # reverse lookup of a shortcut + # unset seen flag, will stay bold in email client + server.remove_flags(msgid, [SEEN]) + if from_addr in shortcuts_inverted: + # reverse lookup of a shortcut from_addr = shortcuts_inverted[from_addr] - reply = "-" + from_addr + " * " + body # asterisk indicates a resend + # asterisk indicates a resend + reply = "-" + from_addr + " * " + body send_message(fromcall, reply) msgexists = True @@ -186,29 +212,36 @@ def resend_email(count): h = stm.tm_hour m = stm.tm_min s = stm.tm_sec - # append time as a kind of serial number to prevent FT1XDR from thinking this is a duplicate message. - # The FT1XDR pretty much ignores the aprs message number in this regard. The FTM400 gets it right. - reply = "No new msg " + str(h).zfill(2) + ":" + str(m).zfill(2) + ":" + str(s).zfill(2) + # append time as a kind of serial number to prevent FT1XDR from + # thinking this is a duplicate message. + # The FT1XDR pretty much ignores the aprs message number in this + # regard. The FTM400 gets it right. + reply = "No new msg %s:%s:%s" % (str(h).zfill(2), + str(m).zfill(2), + str(s).zfill(2)) send_message(fromcall, reply) server.logout() -### end resend_email() + # end resend_email() + def check_email_thread(): # print "Email thread disabled." # return LOG.debug("Starting Email thread") - threading.Timer(55, check_email_thread).start() # how do we skip first run? + # how do we skip first run? + threading.Timer(55, check_email_thread).start() shortcuts = CONFIG['shortcuts'] - shortcuts_inverted = dict([[v,k] for k,v in shortcuts.items()]) # swap key/value + # swap key/value + shortcuts_inverted = dict([[v, k] for k, v in shortcuts.items()]) date = datetime.datetime.now() month = date.strftime("%B")[:3] # Nov, Mar, Apr - day = date.day - year = date.year - today = str(day) + "-" + month + "-" + str(year) + day = date.day + year = date.year + today = "%s-%s-%s" % (day, month, year) LOG.debug("Connect to IMAP host '%s' with user '%s'" % (CONFIG['imap']['host'], @@ -232,72 +265,82 @@ def check_email_thread(): LOG.debug('ID:%d "%s" (%s)' % (msgid, envelope.subject.decode(), envelope.date)) f = re.search('([[A-a][0-9]_-]+@[[A-a][0-9]_-\.]+)', - str(envelope.from_[0]) ) + str(envelope.from_[0])) if f is not None: from_addr = f.group(1) else: from_addr = "noaddr" - if "APRS" not in server.get_flags(msgid)[msgid]: #if msg not flagged as sent via aprs - m = server.fetch([msgid], ['RFC822']) + if "APRS" not in server.get_flags(msgid)[msgid]: + # if msg not flagged as sent via aprs + server.fetch([msgid], ['RFC822']) (body, from_addr) = parse_email(msgid, data, server) - server.remove_flags(msgid, [SEEN]) # unset seen flag, will stay bold in email client + # unset seen flag, will stay bold in email client + server.remove_flags(msgid, [SEEN]) - if from_addr in shortcuts_inverted: # reverse lookup of a shortcut + if from_addr in shortcuts_inverted: + # reverse lookup of a shortcut from_addr = shortcuts_inverted[from_addr] reply = "-" + from_addr + " " + body - #print "Sending message via aprs: " + reply - send_message(CONFIG['ham']['callsign'], reply) #radio - server.add_flags(msgid, ['APRS']) #flag message as sent via aprs - server.remove_flags(msgid, [SEEN]) #unset seen flag, will stay bold in email client + # print "Sending message via aprs: " + reply + # radio + send_message(CONFIG['ham']['callsign'], reply) + # flag message as sent via aprs + server.add_flags(msgid, ['APRS']) + # unset seen flag, will stay bold in email client + server.remove_flags(msgid, [SEEN]) server.logout() -### end check_email() + # end check_email() def send_ack_thread(tocall, ack, retry_count): - tocall = tocall.ljust(9) # pad to nine chars - line = CONFIG['aprs']['login'] + ">APRS::" + tocall + ":ack" + str(ack) + "\n" - for i in range(retry_count, 0, -1): - LOG.info("Sending ack __________________ Tx(" + str(i) + ")") - LOG.info("Raw : " + line) - LOG.info("To : " + tocall) - LOG.info("Ack number : " + str(ack)) - tn.write(line) - time.sleep(31) # aprs duplicate detection is 30 secs? (21 only sends first, 28 skips middle) - return() -### end_send_ack_thread + tocall = tocall.ljust(9) # pad to nine chars + line = "%s>APRS::%s:ack%s\n" % (CONFIG['aprs']['login'], tocall, ack) + for i in range(retry_count, 0, -1): + LOG.info("Sending ack __________________ Tx(%s)" % i) + LOG.info("Raw : %s" % line) + LOG.info("To : %s" % tocall) + LOG.info("Ack number : %s" % ack) + tn.write(line) + # aprs duplicate detection is 30 secs? + # (21 only sends first, 28 skips middle) + time.sleep(31) + return() + # end_send_ack_thread def send_ack(tocall, ack): - retry_count = 3 - thread = threading.Thread(target = send_ack_thread, args = (tocall, ack, retry_count)) - thread.start() - return() -### end send_ack() + retry_count = 3 + thread = threading.Thread(target=send_ack_thread, + args=(tocall, ack, retry_count)) + thread.start() + return() + # end send_ack() def send_message_thread(tocall, message, this_message_number, retry_count): - global ack_dict - line = (CONFIG['aprs']['login'] + ">APRS::" + tocall + ":" + message + - "{" + str(this_message_number) + "\n") - for i in range(retry_count, 0, -1): - LOG.debug("DEBUG: send_message_thread msg:ack combos are: ") - LOG.debug(pprint.pformat(ack_dict)) - if ack_dict[this_message_number] != 1: - LOG.info("Sending message_______________ " + - str(this_message_number) + "(Tx" + str(i) + ")") - LOG.info("Raw : " + line) - LOG.info("To : " + tocall) - LOG.info("Message : " + message) - tn.write(line) - sleeptime = (retry_count - i + 1) * 31 # decaying repeats, 31 to 93 second intervals - time.sleep(sleeptime) - else: - break - return -### end send_message_thread + global ack_dict + line = (CONFIG['aprs']['login'] + ">APRS::" + tocall + ":" + message + + "{" + str(this_message_number) + "\n") + for i in range(retry_count, 0, -1): + LOG.debug("DEBUG: send_message_thread msg:ack combos are: ") + LOG.debug(pprint.pformat(ack_dict)) + if ack_dict[this_message_number] != 1: + LOG.info("Sending message_______________ " + + str(this_message_number) + "(Tx" + str(i) + ")") + LOG.info("Raw : " + line) + LOG.info("To : " + tocall) + LOG.info("Message : " + message) + tn.write(line) + # decaying repeats, 31 to 93 second intervals + sleeptime = (retry_count - i + 1) * 31 + time.sleep(sleeptime) + else: + break + return + # end send_message_thread def send_message(tocall, message): @@ -307,39 +350,50 @@ def send_message(tocall, message): if message_number > 98: # global message_number = 0 message_number += 1 - if len(ack_dict) > 90: # empty ack dict if it's really big, could result in key error later - LOG.debug("DEBUG: Length of ack dictionary is big at " + str(len(ack_dict)) + " clearing.") + if len(ack_dict) > 90: + # empty ack dict if it's really big, could result in key error later + LOG.debug("DEBUG: Length of ack dictionary is big at %s clearing." % + len(ack_dict)) ack_dict.clear() LOG.debug(pprint.pformat(ack_dict)) - LOG.debug("DEBUG: Cleared ack dictionary, ack_dict length is now " + str(len(ack_dict)) + ".") + LOG.debug("DEBUG: Cleared ack dictionary, ack_dict length is now %s." % + len(ack_dict)) ack_dict[message_number] = 0 # clear ack for this message number - tocall = tocall.ljust(9) # pad to nine chars - message = message[:67] # max? ftm400 displays 64, raw msg shows 74 - # and ftm400-send is max 64. setting this to - # 67 displays 64 on the ftm400. (+3 {01 suffix) - # feature req: break long ones into two msgs + tocall = tocall.ljust(9) # pad to nine chars + + # max? ftm400 displays 64, raw msg shows 74 + # and ftm400-send is max 64. setting this to + # 67 displays 64 on the ftm400. (+3 {01 suffix) + # feature req: break long ones into two msgs + message = message[:67] thread = threading.Thread( - target = send_message_thread, - args = (tocall, message, message_number, retry_count)) + target=send_message_thread, + args=(tocall, message, message_number, retry_count)) thread.start() return() -### end send_message() + # end send_message() def process_message(line): f = re.search('^(.*)>', line) fromcall = f.group(1) - searchstring = '::' + CONFIG['aprs']['login'] + '[ ]*:(.*)' # verify this, callsign is padded out with spaces to colon + searchstring = '::%s[ ]*:(.*)' % CONFIG['aprs']['login'] + # verify this, callsign is padded out with spaces to colon m = re.search(searchstring, line) fullmessage = m.group(1) - ack_attached = re.search('(.*){([0-9A-Z]+)', fullmessage) # ack formats include: {1, {AB}, {12 - if ack_attached: # "{##" suffix means radio wants an ack back - message = ack_attached.group(1) # message content - ack_num = ack_attached.group(2) # suffix number to use in ack + ack_attached = re.search('(.*){([0-9A-Z]+)', fullmessage) + # ack formats include: {1, {AB}, {12 + if ack_attached: + # "{##" suffix means radio wants an ack back + # message content + message = ack_attached.group(1) + # suffix number to use in ack + ack_num = ack_attached.group(2) else: message = fullmessage - ack_num = "0" # ack not requested, but lets send one as 0 + # ack not requested, but lets send one as 0 + ack_num = "0" LOG.info("Received message______________") LOG.info("Raw : " + line) @@ -348,7 +402,7 @@ def process_message(line): LOG.info("Msg number : " + str(ack_num)) return (fromcall, message, ack_num) -### end process_message() + # end process_message() def send_email(to_addr, content): @@ -379,7 +433,7 @@ def send_email(to_addr, content): return(-1) s.quit() return(0) -### end send_email + # end send_email # Setup the logging faciility @@ -413,7 +467,7 @@ def setup_logging(args): LOG.addHandler(sh) -### main() ### +# main() ### def main(args=args): global CONFIG @@ -429,7 +483,7 @@ def main(args=args): user = CONFIG['aprs']['login'] password = CONFIG['aprs']['password'] LOG.info("LOGIN to APRSD with user '%s'" % user) - tn.write("user %s pass %s vers aprsd 0.99\n" % (user, password) ) + tn.write("user %s pass %s vers aprsd 0.99\n" % (user, password)) time.sleep(2) check_email_thread() # start email reader thread @@ -438,11 +492,11 @@ def main(args=args): while True: line = "" try: - for char in tn.read_until("\n",100): + for char in tn.read_until("\n", 100): line = line + char line = line.replace('\n', '') LOG.info(line) - searchstring = '::' + user + searchstring = '::%s' % user # is aprs message to us, not beacon, status, etc if re.search(searchstring, line): (fromcall, message, ack) = process_message(line) @@ -454,7 +508,7 @@ def main(args=args): if re.search('^ack[0-9]+', message): # put message_number:1 in dict to record the ack a = re.search('^ack([0-9]+)', message) - ack_dict.update({int(a.group(1)):1}) + ack_dict.update({int(a.group(1)): 1}) continue # EMAIL (-) @@ -539,7 +593,7 @@ def main(args=args): response2 = urllib.urlopen(url2) wx_data = json.loads(response2.read()) reply = wx_data['location']['areaDescription'] + " " + str(altfeet) + "' " + str(lat) + "," + str(lon) + " " + str("%.1f" % round(delta_hours,1)) + "h ago" - reply = reply.encode('ascii',errors='ignore') # unicode to ascii + reply = reply.encode('ascii', errors='ignore') # unicode to ascii send_message(fromcall, reply.rstrip()) except: reply = "Unable to find you (send beacon?)" @@ -547,36 +601,51 @@ def main(args=args): # WEATHER (w) "42F(68F/48F) Haze. Tonight, Haze then Chance Rain." elif re.search('^w', message): - # get my last location from aprsis then get weather from weather service + # get my last location from aprsis then get weather from + # weather service try: - url = "http://api.aprs.fi/api/get?name=" + fromcall + "&what=loc&apikey=104070.f9lE8qg34L8MZF&format=json" + url = ("http://api.aprs.fi/api/get?" + "&what=loc&apikey=104070.f9lE8qg34L8MZF&format=json" + "&name=%s" % fromcall) response = urllib.urlopen(url) aprs_data = json.loads(response.read()) - lat = aprs_data['entries'][0]['lat'] - lon = aprs_data['entries'][0]['lng'] - url2 = "https://forecast.weather.gov/MapClick.php?lat=" + str(lat) + "&lon=" + str(lon) + "&FcstType=json" + lat = aprs_data['entries'][0]['lat'] + lon = aprs_data['entries'][0]['lng'] + url2 = ("https://forecast.weather.gov/MapClick.php?lat=%s" + "&lon=%s&FcstType=json" % (lat, lon)) response2 = urllib.urlopen(url2) wx_data = json.loads(response2.read()) - reply = wx_data['currentobservation']['Temp'] + "F(" + wx_data['data']['temperature'][0] + "F/" + wx_data['data']['temperature'][1] + "F) " + wx_data['data']['weather'][0] + ". " + wx_data['time']['startPeriodName'][1] + ", " + wx_data['data']['weather'][1] + "." - reply = reply.encode('ascii',errors='ignore') # unicode to ascii + reply = "%sF(%sF/%sF) %s. %s, %s." % ( + wx_data['currentobservation']['Temp'], + wx_data['data']['temperature'][0], + wx_data['data']['temperature'][1], + wx_data['data']['weather'][0], + wx_data['time']['startPeriodName'][1], + wx_data['data']['weather'][1]) + + # unicode to ascii + reply = reply.encode('ascii', errors='ignore') send_message(fromcall, reply.rstrip()) - except: + except Exception: reply = "Unable to find you (send beacon?)" send_message(fromcall, reply) # USAGE else: - reply = "usage: time, fortune, loc, weath, -emailaddr emailbody, -#(resend)" + reply = ("usage: time, fortune, loc, weath, -emailaddr " + "emailbody, -#(resend)") send_message(fromcall, reply) - time.sleep(1) # let any threads do their thing, then ack - send_ack(fromcall, ack) # send an ack last + # let any threads do their thing, then ack + time.sleep(1) + # send an ack last + send_ack(fromcall, ack) - except Exception, e: + except Exception as e: LOG.error("Error in mainline loop:") LOG.error("%s" % str(e)) LOG.error("Exiting.") - #sys.exit(1) # merely a suggestion + # sys.exit(1) # merely a suggestion os._exit(1) # end while True diff --git a/aprsd/utils.py b/aprsd/utils.py index e15d23b..894ae2f 100644 --- a/aprsd/utils.py +++ b/aprsd/utils.py @@ -36,6 +36,7 @@ imap: log = logging.getLogger('APRSD') + def env(*vars, **kwargs): """This returns the first environment variable set. if none are non-empty, defaults to '' or keyword arg default @@ -55,10 +56,12 @@ def get_config(): config = yaml.load(stream) return config else: - log.critical("%s is missing, please create a config file" % config_file) - print("\nCopy to ~/.aprsd/config.yml and edit\n\nSample config:\n %s" % example_config) + log.critical("%s is missing, please create config file" % config_file) + print("\nCopy to ~/.aprsd/config.yml and edit\n\nSample config:\n %s" + % example_config) sys.exit(-1) + # This method tries to parse the config yaml file # and consume the settings. # If the required params don't exist, diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..3930480 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1 @@ +flake8 diff --git a/tools/fast8.sh b/tools/fast8.sh new file mode 100755 index 0000000..22976ce --- /dev/null +++ b/tools/fast8.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +NUM_COMMITS=${FAST8_NUM_COMMITS:-1} + +if [[ $NUM_COMMITS = "smart" ]]; then + # Run on all commits not submitted yet + # (sort of -- only checks vs. "master" since this is easy) + NUM_COMMITS=$(git cherry master | wc -l) +fi + +echo "Checking last $NUM_COMMITS commits." + +cd $(dirname "$0")/.. +CHANGED=$(git diff --name-only HEAD~${NUM_COMMITS} | tr '\n' ' ') + +# Skip files that don't exist +# (have been git rm'd) +CHECK="" +for FILE in $CHANGED; do + if [ -f "$FILE" ]; then + CHECK="$CHECK $FILE" + fi +done + +diff -u --from-file /dev/null $CHECK | flake8 --diff diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..55448f1 --- /dev/null +++ b/tox.ini @@ -0,0 +1,39 @@ +[tox] +minversion = 1.6 +skipdist = True +envlist = py27,py36,py37,fast8,pep8,cover,docs + +[testenv] +setenv = VIRTUAL_ENV={envdir} +usedevelop = True +install_command = pip install {opts} {packages} + +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt +commands = + pytest {posargs} + +[testenv:cover] +commands = + pytest --cov=aprsd + +[testenv:docs] +deps = -r{toxinidir}/test-requirements.txt +commands = sphinx-build -b html docs/source docs/html + +[testenv:pep8] +commands = + flake8 {posargs} aprsd test + +[testenv:fast8] +basepython = python3 +# Use same environment directory as pep8 env to save space and install time +envdir = {toxworkdir}/pep8 +commands = + {toxinidir}/tools/fast8.sh +passenv = FAST8_NUM_COMMITS + +[flake8] +show-source = True +ignore = E713 +exclude = .venv,.git,.tox,dist,doc,.ropeproject