From www.AA6E.net
Jump to: navigation, search
#!/bin/env python
#
# File: qrz.py
# Version: 0.12
#
# Report generator for QRZ.com XML database, featuring label printing.
# Copyright (c) 2008 Martin Ewing
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  
# 02110-1301, USA.
#
# Contact ewing @@ alum.mit.edu or c/o San Pasqual Consulting, 28
# Wood Road, Branford CT 06405, USA.

# qrz.py is a Python program for examining and printing data from
# the QRZ.com amateur radio callsign database.  Various output options
# are provided, including printing labels, 30/page.
# This program operates from the command line only.  Some aspects
# are Unix/Linux - specific.

# Future:   implement bio/photo output methods; 
#           nicely formatted full-record output.

# Developed using Python 2.5.1 on Fedora 8 Linux

# Changes:
# v 0.11:  correct -s processing (print with .rstrip(), not .strip())
# v 0.12:  provide sitecustomize.py to allow for some non-ASCII characters:
#       import sys
#       sys.setdefaultencoding('iso-8859-1')
#    This requires distributing a zip or tgz file.

import xml.dom.minidom as mdom, urllib, sys, getopt, getpass, os

# Sign up for XML access account at http://online.qrz.com

# API info: http://online.qrz.com/specifications.html

IDENT = 'qrz.py v 0.11'
AGENT = 'qrzpy01'       # QRZ.com agent code (helps identify the software client
                        # for QRZ's info.  Do not change unless you substantially
                        # modify this code.
LOGIN_URL1 = 'http://online.qrz.com/bin/xml?username='
LOGIN_URL2 = ';password='
LOGIN_URL3 = ';'+AGENT+'\n'
QUERY_URL  = 'http://online.qrz.com/bin/xml?'
MAIL_MODE_MAX = 1
PRINT_SESSION = False
MAX_LOGIN_TRIAL = 3     # How many times to retry login
FILLCHR = ' '

# Unix dependency
FPATH = os.environ.get('HOME')+'/.'+AGENT       # User's init file

class FullPage(object):
    """
    This class provides for an object that contains a full print page
    and methods to insert text fields or blocks at specified row,col addresses.
    Also provides a placeLabelPage method that is geared to generating 3-wide
    label sheets.
    """
    def __init__(self, nrow=60, ncol=120):
        # defaults work for 1 x 2 5/8 in labels, 30 per page
        self.initial = nrow * [ ncol * FILLCHR ]
        self.lines = self.initial[:]
        self.nrow = nrow
        self.ncol = ncol
        self.lab_width = 42     # 42 chars, label to label
        self.lab_height = 6     # 6 lines down, label to label
        self.lab_horiz_ct = 3   # labels across
        self.lab_vert_ct = 10   # labels down
        self.maxseq = self.lab_horiz_ct * self.lab_vert_ct  # labels/page

    def place(self, row, col, s):
        if row > self.nrow:
            return
        if len(s)+col >= self.ncol:
            s = s[:self.ncol - col + 1]
        l = self.lines[row-1]           # NB: Rows & cols go 1...nrow etc
        self.lines[row-1] = l[:col-1] + s + l[col + len(s) - 1:]

    def placeBlock(self, row, col, b):
        # b is list of strings
        ix = row
        for x in b:
            self.place(ix, col, x)
            ix += 1

    # Produces output for 1" x 2 5/8" labels
    # 3 labels horizontally x 10 vertically, when piped to mpage using following
    # parameters:
    #   FullPage(60,120)
    #   mpage -o -m20l30t10r30b -L60 -W120 -1P -  # for HP LaserJet 1200 e.g.

    def placeLabelPage(self, seq, b):   # place on a page of (seq: 1..30) labels
        if seq>self.maxseq or seq<1:
            raise ValueError, "seq number out of range."
        col1 = ((seq-1) % self.lab_horiz_ct) * self.lab_width + 1   # index right
        row1 = ((seq-1) / self.lab_horiz_ct) * self.lab_height + 1  # index down
#        print >>sys.stderr,"printing at col=%d, row=%d" % (col1,row1)
        if len(b) > self.lab_height-1:               # vert spacing less one
            b = b[:self.lab_height-1]
        for i in range(len(b)):
            if len(b[i]) > self.lab_width:           # too wide!
                print >>sys.stderr, '** %s line truncated: %s' % (b[0],b[i])
                b[i] = b[i][:self.lab_width]
        self.placeBlock(row1, col1, b)

    def prt(self):              # Print the buffer
        for x in self.lines:
            print x.rstrip()

    def clear(self):            # Re-initialize the buffer
        self.lines = self.initial[:]

# get_info collects data into dictionary from XML tag_name='Session' 
# or 'Callsign'.
def get_info(rt, tag_name):
    Ans_D = {}
    if not tag_name in ['Session', 'Callsign']:
        return None     # error
    rtelements = rt. getElementsByTagName(tag_name)
    if len(rtelements) < 1: 
        return None     # error
    s_elems = rtelements[0].getElementsByTagName('*')
    for s in s_elems:
        for ss in s.childNodes:
            # Ignore if not a text node...
            if ss.nodeName == '#text':
                Ans_D[s.nodeName] = ss.nodeValue
    return Ans_D
    
def gie(dic, key):  # Get dict. entry if exists, + blank, otherwise ''
    if dic.has_key(key):
        return dic[key] + ' '
    else:
        return ''

def gie2(dic, key):
    return gie(dic,key).strip()     # same as gie, without extra ' '

def cmplower(s,t):                  # compare ignoring case (for d.sort)
    return cmp(s.lower(),t.lower())

def dmp1(row,col,page,dic,key):     # in C, this would have been a macro...
    page.place(row,col,key+': '+gie(dic,key))
    return [key]

def rawdumper(dic):                 # dump 1 key per line
    for x in dic:
        print x+': '+dic[x]
      
# dumper takes data from a dictionary derived from QRZ.com XML record
# and tries to make a reasonably compact and screen-friendly dump of
# expected keys.  Any unexpected keys are output at end, one line per
# key.  Each field is identified by key name -- this is a 'dump' after
# all!
  
def dumper(dic):
        # Dump of all data received.
        print '--------Callsign info for %s--(%d)--' % (call,len(dic))
        mykeys = dic.keys()
        mykeys.sort(cmplower)       # output sorted by key name
        ku = []                     # list of keys that have been 'used'
        db = FullPage(12,80)        # dump buffer, guessing at v. size
        ku += dmp1(1,1,db,dic,'call')
        ku += dmp1(1,21,db,dic,'class')
        ku += dmp1(1,41,db,dic,'codes')
        ku += dmp1(2,1,db,dic,'fname')
        ku += dmp1(2,21,db,dic,'name')
        ku += dmp1(3,1,db,dic,'addr1')
        ku += dmp1(4,1,db,dic,'addr2')
        ku += dmp1(3,41,db,dic,'TimeZone')
        ku += dmp1(4,41,db,dic,'county')
        ku += dmp1(5,1,db,dic,'state')
        ku += dmp1(5,21,db,dic,'zip')
        ku += dmp1(5,41,db,dic,'country')
        ku += dmp1(6,1,db,dic,'grid')
        ku += dmp1(6,21,db,dic,'latd')
        ku += dmp1(6,41,db,dic,'lond')
        ku += dmp1(7,1,db,dic,'efdate')
        ku += dmp1(7,21,db,dic,'expdate')
        ku += dmp1(7,41,db,dic,'grid')
        ku += dmp1(8,1,db,dic,'AreaCode')
        ku += dmp1(8,21,db,dic,'GMTOffset')
        ku += dmp1(8,41,db,dic,'DST')
        ku += dmp1(9,1,db,dic,'land')
        ku += dmp1(9,21,db,dic,'MSA')
        ku += dmp1(9,41,db,dic,'views')
        ku += dmp1(10,1,db,dic,'serial')
        ku += dmp1(10,21,db,dic,'moddate')
        ku += dmp1(11,1,db,dic,'email')
        ku += dmp1(11,41,db,dic,'frn')
        ku += dmp1(12,1,db,dic,'url')
        db.prt()
        for x in mykeys:            # print any other keys in alpha order
            if x not in ku:
                print x+': '+dic[x]
        print

# labelgen formats data to make a single mailing label as a list
# of lines of text from a dictionary that was obtained from a single
# QRZ.com XML record.  There are no limits on size imposed here.
def labelgen(dic):
        lbl = []
        lbl += [gie(dic,'call')]    # callsign
        ll = gie(dic,'fname')       # first name
        ll += gie(dic,'name')       # last name
        lbl += [ll]
        lbl += [gie(dic,'addr1')]   # address 1
        ll = gie(dic,'addr2')       # address 2
        ll += gie(dic,'state')      # state
        ll += gie(dic,'zip')        #   + zip
        lbl += [ll]
        ll = gie(dic,'country')     # country
        if ll != 'USA ':            # but not if 'USA'
            lbl += [ll]
        return lbl

#-----------------+   
#    main entry   |
#-----------------+

# Must print to stderr here to allow label output to pass through to 
# page printing without extra lines.
print >>sys.stderr, 'This is %s from aa6e.net.' % IDENT

try:
    myopts, call_list = getopt.getopt(sys.argv[1:],'hdf:lmrs:uz')
except getopt.GetoptError:
    print >>sys.stderr,"Invalid command arguments:", sys.argv[1:]
    sys.exit()
output_mode = ''
input_file = ''             # file with list of callsigns, if provided
nskip = 0                   # label 'slots' to skip on first page
output_mode = 'm'           # Single mail is default output mode
for x in myopts:            # Check user-supplied options.
    if x[0] == '-h':
        print """qrz.py logs in to QRZ.com XML service and returns
data for requested callsigns.

qrz.py options <callsign_list>
Available options:
-f <filename> - get list of callsigns from file
-d  dump all available data for each callsign
-h  print this message
-l  output mailing labels in 3 x 10 page format
-m  output mailing labels in simple format (default)
-r  dump all data in raw format (1 key per line)
-s <skip> - number of labels to skip on first page...
-z  remove .qrzpy file

Output is ASCII to stdout. -l format can be piped as follows:
... | mpage -o -m20l30t10r30b -L60 -W120 -1P -  (for HP LJ1200 printer)

User must have a QRZ.com XML access account. (http://online.qrz.com)
"""
        sys.exit()
    elif x[0] == '-d':
        output_mode = 'd'   # dump
    elif x[0] == '-r':
        output_mode = 'r'   # raw dump
    elif x[0] == '-m':
        output_mode = 'm'   # mailing label, single
    elif x[0] == '-l':
        output_mode = 'l'   # mailing label, page (up to 30/page)
    elif x[0] == '-f':
        input_file = x[1]   # callsign requests from file
    elif x[0] == '-z':
        # remove the old .qrzpy file, if it exists.
        # Unix dependency, could do something similar for another OS
        try:
            os.remove(FPATH)
        except:
            print >>sys.stderr,'** %s could not be removed.' % FPATH
            sys.exit()
        print '%s removed.' % FPATH
        sys.exit()
    elif x[0] == '-s':  # skip a number of label places before starting
                        # first label page.  Handy if you've already used a number
                        # of labels on your sheet on a prior run.
        try:
            nskip = int(x[1])
        except ValueError:
            print >>sys.stderr,'invalid -s (skip) parameter %s' % x[1]
            sys.exit()
    elif x[0] == '-u':  # Print usage info.
        print 'usage: qrz.py [ -d | -m | -l | -r ] [-f <file>]  [-s <skip>] [-h] [-z] <call_list>'
        sys.exit()
        
if input_file == '':        # callsigns on command line?
    if call_list == []:
        print >>sys.stderr,'No calls requested.'
        sys.exit()
else:                       # No, go get file
    try:
        fc = open(input_file, 'r')
    except IOError:
        print >>sys.stderr,'Can\'t open input_file %s' % input_file
        sys.exit()
    fclines = fc.readlines(8192)    # arbitrary size ceiling
    fc.close()
    if fclines == []:
        print >>sys.stderr,'No callsigns in input file'
        sys.exit()
    call_list = []
    for x in fclines:           # calls are whitespace separated on
        call_list += x.split()  # any number of lines

if output_mode == 'l':
    page = FullPage()      # page object for mailing labels

# Log in and get session key, prompt if valid key not previously stored.

for login_trial in range(MAX_LOGIN_TRIAL):
    try:
        fr = open(FPATH, 'r')       # Do we have a .qrzpy file already?
    except IOError:                 # No, must create one.
        print 'Please provide login info for QRZ.com XML service...'
        user = raw_input('Username: ')
        pwd = getpass.getpass('Password: ')
        login_url = LOGIN_URL1+user+LOGIN_URL2+pwd+LOGIN_URL3
        # Unix dependencies
        try:
            fw = open(FPATH, 'w')                       # .qrzpy
            fw.write(login_url)
        except:
            print >>sys.stderr,'** Can\'t write to %s' % FPATH
            sys.exit()
        fw.close()
        os.chmod(FPATH,0600)                        # a little security
    else:
        login_url = fr.read().strip()

    # We've got a login_url, but will it be accepted?
    fd = urllib.urlopen(login_url)
    doc = mdom.parse(fd)            # Construct DOM w/ Python heavy lifting
    rt = doc.documentElement        # Find root element
    Session_D =  get_info(rt, 'Session')
    if 'Error' in Session_D:        # No, that key won't work.
        print >>sys.stderr,'** Error ** %s' % Session_D['Error']
        print 'Reenter password info, please.'
        # Unix dependency: remove .qrzpy file if it exists
        try:
            os.remove(FPATH)
        except OSError:
            pass
        continue                        # try again, please.
    break                               # We're authenticated OK now, stop loop
    
else:                                   # End of 'for' loop, no success
    print >>sys.stderr,'Login trial limit exceeded.  Sorry'
    sys.exit()

if 'Alert' in Session_D:
    print '** Alert ** %s' % Session_D['Alert']
if 'Expires' in Session_D:
    print 'Note: QRZ.com account expires %s' % Session_D['Expires']
if PRINT_SESSION:                   # This is usually uninteresting
    print '--------Session'
    for x in Session_D:
        print x, Session_D[x]
    print
fd.close()

# For each requested call, get its data.
seq = nskip                         # zero unless '-s nnn' specified
for call in call_list:
    query = '%ss=%s;callsign=%s' % (QUERY_URL, Session_D['Key'], call)
    fd = urllib.urlopen(query)      # access XML record from Internet
    doc = mdom.parse(fd)            # Construct DOM with Python magic
    rt = doc.documentElement        # Find root element
    fd.close()

    csd = get_info(rt, 'Callsign')  # Place XML data into friendly dictionary
    if csd == None:
        print >>sys.stderr, '** Record not found for %s.' % call
    else:
        seq += 1
        if output_mode == 'd':
            dumper(csd)         # print structured dump of entry
        elif output_mode == 'r':
            rawdumper(csd)      # print "raw" dump
        elif output_mode == 'm' or output_mode == 'l':  # mail mode
            label = labelgen(csd)       # build a label
            if output_mode == 'm':
                print                   # print a simple label
                for x in label:
                    print x
            else:                       # 'l' -> generate label matrix
#                print >>sys.stderr,"Printing seq=%d" % seq
                page.placeLabelPage(seq,label) # place label on page
                if seq >= page.maxseq:  # have we completed a page?
                    page.prt()
                    seq = 0
                    page.clear()
        else:
            print >>sys.stderr, '** No output specified...?'
            sys.exit()

if output_mode == 'l' and seq > 0:      # must print final label page?
    page.prt()