abtool, das Adressbuch Werkzeug

Adressbuch Bild: Adressbuch - Bestimmte Rechte vorbehalten: Andy Drop (cc-by 3.0)

Wer dieses Blog schon länger verfolgt, hat mit Sicherheit mitbekommen, das ich Programme mag, die Ihre Daten in einfachen Ascii-Dateien ablegen. Eines dieser Programme ist die Adressverwaltung abook.
Die Datei die abook anlegt ist aufgebaut, wie man es von früheren Windows Konfigurationsdateien gewohnt ist. Ein Eintag sieht beispielsweise so aus:

[168]
name=Restaurant Royals & Rice
custom2=51.964056, 7.6215647
address=Frauenstraße 51-52
city=Münster
state=NRW
zip=48143
country=Deutschland
workphone=+4925139633699
url=http://royalsandrice.com
notes=Mo-Sa 09:00-24:00, So 10:00-24:00

Ich nenne diese Art der Datenverwaltung gerne LoTek, nach den Rebellen aus der Kurzgeschichte Der mnemonische Johnny von William Gibson. Sie macht mich völlig unabhängig von der Software, mit der ich die Daten bearbeite, ich kann sie mit den simpelsten Programmen lesen und verarbeiten. Kein Softwarehaus kann mich in seine Software binden, nur weil ich bei einem Wechsel alle Daten noch mal neu erfassen müsste.

We are LoTek. Fracking the system, it’s what we do. Reject the accepted paradigm. See you on the road…

So beschreibt es einer der LoTek Anhänger in der Geschichte.

Von Hause aus kann abook bereits eine Mail an eine Adresse schreiben, bzw eine Webseite anhand der gespeicherten URL aufrufen. Leider muß ich dazu immer noch das Programm öffnen, den entsprechenden Eintrag suchen, und dort die entsprechenden Tasten drücken. Außerdem fehlten mir noch ein paar Funktionen.

Die erste davon, die ich mit einem eigenen kleinen Python-Skript umgesetzt habe, ist der Aufruf einer Karte, die mir anzeigt, wo sich eine Adresse befindet.
Der gewünschte Aufruf sollte so aussehen abmap royals und dann sollte im Browser OpenStreetMap geöffnet werden, und das Restaurant aus dem Beispiel in der Karte markiert sein.

Das Skript

Generell ist das Skript, so simpel wie es bisher auch ist, bereits in zwei Teile unterteilt.

  1. Einer Klasse, die die Schnittstelle zur abook Adressdatei darstellt, und
  2. einem kurzen Programm, welches diese Klasse nutzt.

Fangen wir mit dem Nutzteil an. Dieser ist recht übersichtlich:

# Only execute this if we are called directly
if __name__ == "__main__":

# parse arguments

    parser = argparse.ArgumentParser(description='Searches for entries in an abook address-file and takes some actions on it.',
                                     epilog='If you find there are some useful actions missing, feel free to report them.')

    parser.add_argument('name', nargs='+')
    parser.add_argument('-n','--nick', nargs = '*', metavar = 'Fieldname')

    if sys.argv[0].split('/')[-1] != 'abmap':
        parser.add_argument('-m','--map', action = 'store_true')
    if sys.argv[0].split('/')[-1] != 'abmail':
        parser.add_argument('-e','--email', action = 'store_true')
    if sys.argv[0].split('/')[-1] != 'aburl':
        parser.add_argument('-u','--url', action = 'store_true')

    parser.add_argument('-v','--verbose', action = 'count') # FATAL, ERROR, WARN, INFO, DEBUG 
    parser.add_argument('-q','--quiet', action = 'store_true')
    parser.add_argument('-V','--version', action='version', version='%(prog)s 0.1')

    args = parser.parse_args()
    

    book = Abook()
    
    name = " ".join(args.name)
    coordinates = book.coordinates(name)
    for x, y in coordinates:
        webbrowser.open_new("https://www.openstreetmap.org/?mlat=" + x + "&mlon=" + y +"#map=16/" + x + "/" +y )

Der gesamte erste Teil, mit den vielen parser.gedöns Zeilen dient dazu die Kammandozeile des Programms auszuwerten. Im Moment kann man dort nur einen Namen, oder Teile eines Namens eingeben, aber das Programm soll in Zukunft noch ausgebaut werden, und dazu habe ich schon mal eine Handvoll Aufrufparameter vorgesehen. Interessant wird es erst in den letzten 5 Programmzeilen.

Hier wird zunächst book als Instanz des Adressbuchs angelegt. Danach werden die Suchbegriffe der Kommandozeile in die Variable name verfrachtet. Zu diesem name lassen wir uns nun eine Liste von Koordinaten zurückgeben.
Für jede einzelne der Koordinaten wird nun eine Webseite geöffnet, die die entsprechenden koordinaten in Openstreetmap anzeigt, und markiert.

coordinates ist dabei eine Liste von (x,y) Tupeln. Eine Liste daher, weil wir unter umständen mit unserem Suchbegriff mehrere Treffer erhalten, und ein Treffer unter Umständen auch mehrere Adressen enthalten kann.

Die Klasse

Schauen wir uns die Funktion coordinates(name) einmal genauer an:

def coordinates(self, searchname):
        pattern = "(-?[1234567890]+\.[1234567890]+)[^-.1234567890]+(-?[1234567890]+\.[1234567890]+)"
        coords = re.compile(pattern)
        result = []
        for name in self.searchname(searchname):
            for entry, value in name:
                match = coords.search(value)
                if match != None:
                   result.append((match.group(1),match.group(2)))

        return result

Die ersten beiden Zeilen bereiten das Suchmuster – hier als regulären Ausdruck – vor. Zu beachten sind dabei vor alem die beiden Paare von Runden Klammern, die jeweils eine Gruppierung innerhalb des Suchausdrucks darstellen. Danach wird über die searchname Funktion eine Liste von Treffern zu unserem Suchbegriff zurückgeliefert, über die wir iterieren.
Jeder Treffer enthält eine Liste von Tupeln aus Keyword und Wert, wie sie in der Textdatei von oben standen. Wenn in diesen Werten nun irgendwo ein Match auf eine Koordinate stattfindet, wird der Resultate liste ein Tupel aus den beiden Gruppen dieses Matches hinzugefügt. Jede Gruppe enthält einen Teil der Koordinate.

Die Suche nach einem Treffer zu unserem Suchbegriff habe ich ausgelagert, da wohl verschiedene Funktionen in Zukunft nach einem Namen suchen können möchten. Die Funktion selbst sieht so aus:

def searchname(self, searchname):

        result=[]
        for s in self.abook.sections():
            if self.abook.has_option(s,'name'):
                name=self.abook.get(s,'name')
                if name.lower().count(searchname.lower()) > 0:
                    result.append(self.abook.items(s))

        return result

Dabei ist self.abook die klasseninterne Variable, mit der auf die Datei zugegriffen wird. Genau genommen allerdings nicht auf die Datei als file Object, sondern wir benutzen hier, da es sich ja um eine Datei handelt, die sich wie eine Windows-Config Datei verhält, die Python Klasse ConfigParser. Die einzelnen Personen darin sind als sections
bezeichnet, die Attribute jeder Person als options. Im Detail ist das hier dokumentiert.

Entsprechend einfach gestaltet sich also auch die Init-Funktion der Klasse:

def __init__(self):
        "Takes an abook file an reads it"
    
        self.abook = ConfigParser.ConfigParser()
        self.abook.read(os.environ["HOME"] + "/.abook/addressbook")
        self.abook.remove_section('format')

Wie man sieht greife ich hier auf eine hard-coded Adressbuch-Datei zu. Ja ja, extrem schlechter Stil! Aber um das in zukünftigen Versionen nicht tun zu müssen haben wir ja die Kommandozeilen-Parameter vorbereitet. Das komplette Python-Skript findet sich auf Github als derzeit erstes im LoTek Repository. Die anderen Skripte werde ich hier ebenfalls hinzufügen.