hipAgent.py
changeset 302: 03c221a1f62a
parent 289:6b40c9b15de0
child 316:a08706b9d821
manifest: 03c221a1f62a
author: Dan Connolly http://www.w3.org/People/Connolly/
date: Mon Sep 10 17:34:28 2007 -0500 (8 months ago)
permissions: -rw-r--r--
* move RDF KB loading from hipAgent to calitems
** work around myStore global wierdness

* verify hipsrv.asLocalDate with another test

* include client info in PUT login
* hipwsgi was posting from the pre-edit data. duh.
** fix is incomplete; browsing sees only pre-edit data

* in pull, add spaces between keys (untested)
        1 """
        2 hipAgent.py -- danger sidekick hiptop agent, via desktop interface
        3 $Id: hipAgent.py,v 1.28 2007/09/03 17:28:24 connolly Exp $
        4 see change log at end
        5 
        6 USAGE
        7   Load an itinerary:
        8 
        9   $ python hipAgent.py --number=2025551212 --passwd=xyz --importItinerary <<EOF
       10 2005-02-26 lv MCI 15:00 ar ORD 16:27 Saturday AMERICAN AIRLINES #1094
       11 2005-02-26 lv ORD 17:29 ar BOS 20:46 Saturday AMERICAN AIRLINES #874
       12 EOF
       13 
       14 See parseFlight() below if you'd rather use a different format.
       15 
       16   There are some test routines, but they're probably bitrotten.
       17   
       18   python hipAgent.py --number telnum --passwd passwd --datebook
       19     to grab your calendar homepage and extract some pointers
       20     
       21   python hipAgent.py --number telnum --passwd passwd --contact who
       22     to grab a pointer to the page about a contact named who
       23 
       24 ACKNOWLEDGEMENTS
       25 
       26 login technique reverse-engineered using ClientCookie
       27  http://wwwsearch.sourceforge.net/ClientCookie/
       28 The technique described in http://hipme.com/software/appointment/
       29 no longer works.
       30 
       31 
       32 LICENSE: Share and Enjoy.
       33 Copyright (c) 2001 W3C (MIT, INRIA, Keio)
       34 Open Source license:
       35 http://www.w3.org/Consortium/Legal/copyright-software-19980720
       36 
       37 
       38 
       39 """
       40 
       41 import logging
       42 import pprint
       43 import urllib, urllib2
       44 from urllib2 import urlopen
       45 from string import lower
       46 import re
       47 import calendar, time
       48 
       49 import dangerSync # for posting events
       50 import hipsrv # for asHiptopDate
       51 
       52 # mnot's HtmlDom
       53 # http://www.mnot.net/python/HtmlDom.py
       54 # <AaronSw> it lets me do: d = fetch(url); print xml.xpath.Evaluate("//*[@class='rss:item']/text()", d)
       55 #import HtmlDom # http://www.mnot.net/python/HtmlDom.py
       56 #import xml.xpath
       57 
       58 
       59 def progress(*args):
       60     import sys
       61     for a in args:
       62         sys.stderr.write('%s ' % a)
       63     sys.stderr.write("\n")
       64 
       65 
       66 class TMobileDangerAgent:
       67 
       68     def __init__(self):
       69 	import ClientCookie # http://wwwsearch.sourceforge.net/ClientCookie/
       70 	ClientCookie.HTTP_DEBUG = 0
       71 	ClientCookie.CLIENTCOOKIE_DEBUG = 0
       72 	from ClientForm import ParseResponse # http://wwwsearch.sourceforge.net/ClientForm/
       73 
       74         self._icon = 15  # default to airplane
       75 
       76     def login(self, phone, passwd):
       77         """set up cookies based on login credentials
       78 
       79         we cheat here, using module state of ClientCookie.
       80         We should keep an opener object here.
       81         This interface will stay the same when we fix
       82         that; i.e. the caller shouldn't assume
       83         a global opener.
       84         """
       85         
       86         progress("loggin in with creds: ", phone, passwd)
       87 
       88         form = ParseResponse(urlopen("https://my.t-mobile.com/login/MyTmobileLogin.aspx"))[0]
       89 
       90         progress("=== start page form: ", form)
       91 
       92         form["Login1:txtMSISDN"] = phone
       93         form["Login1:txtPassword"] = passwd
       94         arg = 'Login1$btnLogin'
       95         form.find_control("__EVENTTARGET").readonly = False
       96         form.find_control("__EVENTARGUMENT").readonly = False
       97         form["__EVENTTARGET"] = ':'.join(arg.split('$'))
       98         form["__EVENTARGUMENT"] = arg
       99         request = form.click()
      100 
      101         resp2 = ClientCookie.urlopen(request)
      102         progress("=== login response headers: ", resp2.info())
      103 
      104         html = resp2.read()
      105         logging.debug("resp2:" + html)
      106         i = html.index("http://login.sidekick.dngr.com/")
      107         addr = html[i:]
      108         addr = addr[:addr.index('"')]
      109 
      110         progress("desktop addr:", addr)
      111         resp3 = ClientCookie.urlopen(addr)
      112         
      113         progress("=== desktop interface headers: ", resp3.info())
      114 
      115         progress("logged in.")
      116 
      117 
      118     def urlopen(self, addr, data=None):
      119         req = urllib2.Request(addr, data=data)
      120         resp  = ClientCookie.urlopen(req)
      121         return resp
      122 
      123 
      124     def pageOfContactNamed(self, name):
      125         letter = lower(name[0])
      126         listAddr = 'http://address.sidekick.dngr.com/index?high=%s&low=%s' \
      127                    % (letter, letter)
      128         listDoc = parseContent(self.urlopen(listAddr))
      129         progress("parsed as: ", listDoc)
      130         nodes = listDoc.xpathNewContext().xpathEval( \
      131             '//a[@class="text" and contains(text(), "%s")]' % name)
      132 
      133         progress("got %d nodes about '%s'" % (len(nodes), name))
      134 
      135         if len(nodes) <> 1:
      136             raise KeyError, name
      137 
      138         return nodes[0].prop('href')
      139 
      140     def aboutDay(self, fp, yyyy_mm_dd):
      141         """return a list of (addr, title) for each item on a day.
      142 
      143         @@TODO: put time in there too.
      144         
      145         example:
      146         <tr>
      147 	<td align=right valign=top class=timeGridCell rowspan=1><a href='add?h=19&min=0&date=2003-06-12' class='pmTimeDisplay'>7:00</a></td>
      148 	<td height=20 class=pmGridCell></td>
      149     <td valign=top width='100%' height='120' class='eventGridCell' colspan='1' rowspan='5'>
      150     <table cellpadding=2 cellspacing=0 border=0>
      151     <tr>
      152         <td width='100%' height='120' valign=top><span class='eventGridCellText'><a href='http://calendar.sidekick.dngr.com/event?id=310' class='data'><b>Royals</b>  </a></span></td>
      153 
      154         <td height='120' width=1><img src='http://img.sidekick.dngr.com/img/voicestream/pixel.gif' height='120' width='1' border=0></td>
      155     </tr>
      156     </table>
      157 </td>
      158 
      159 </tr>
      160 
      161         """
      162         pgAddr = 'http://calendar.sidekick.dngr.com/day?date=' + yyyy_mm_dd
      163         doc = parseContent(self.urlopen(pgAddr))
      164         ctxt = doc.xpathNewContext()
      165         nodes = ctxt.xpathEval("//a[@class='data']")
      166 
      167         dayTerm = '<#theDay%s>' % yyyy_mm_dd
      168         fp.write('<%s> foaf:topic %s.\n' % (pgAddr, dayTerm))
      169         fp.write('%s xsdt:date "%s".\n' % (dayTerm, yyyy_mm_dd))
      170 
      171         events = []
      172         for ev in nodes:
      173             addr = ev.prop('href')
      174             try:
      175                 eid = int(addr[addr.find("id=")+3:])
      176             except ValueError:
      177                 eid = hash(addr) # darn. 2 names for same event
      178             title = ev.content #@@n3-escape
      179             evTerm = "<#event_%s>" % eid 
      180             fp.write('%s k:temporallyIntersects %s;\n' % (evTerm, dayTerm))
      181             fp.write('  rdf:value """%s"""; is foaf:topic of <%s>.\n' %
      182                      (title, addr))
      183             events.append((addr, title))
      184         fp.write('\n\n');
      185 
      186         return events
      187 
      188 
      189     def aboutMonth(self, year, month):
      190         calAddr = 'http://calendar.sidekick.dngr.com/month?date=%04d-%d' % (year, month)
      191         txt = self.urlopen(calAddr).read()
      192         doc = HtmlDom._HTMLReader().fromString(txt)
      193         nodes = xml.xpath.Evaluate('//TR/TD[@CLASS="monthWeekNav"]', doc)
      194 
      195         ret = []
      196         for wkn in nodes:
      197             nodes = xml.xpath.Evaluate( \
      198             'SPAN/A[@CLASS="data"]', wkn)
      199             
      200             addr = nodes[0].prop('href')
      201             ret.append(addr)
      202 
      203         return ret
      204 
      205 
      206     def aboutEvent(self, fp, id):
      207         progress("== grabbing event:", id)
      208 
      209         pgAddr = 'http://calendar.sidekick.dngr.com/edit?id=%d' % id
      210         doc = parseContent(self.urlopen(pgAddr))
      211 
      212         # grab form
      213         ctxt = doc.xpathNewContext()
      214         form = ctxt.xpathEval("//form[@id='calendar_add_form']")[0]
      215         ctxt.xpathFreeContext()
      216     
      217         props = formProps(doc, form)
      218         progress("event properties:", props.keys())
      219 
      220         evTerm = "<#event_%s>" % id
      221 
      222         fp.write('<%s> foaf:topic [= %s; \n' % (pgAddr, evTerm))
      223         for p in (
      224             'e_title',
      225             'e_icon_id', # hint at type
      226 
      227             'year_start',
      228             'month_start',
      229             'day_start',
      230 
      231             'e_allday',
      232             'e_day_dur',
      233 
      234             'hour_start',
      235             'min_start',
      236             'use_duration',
      237             'e_hour_dur',
      238             'e_min_dur',
      239 
      240             'provides_endtime',
      241             'provides_starttime',
      242             'provides_endtime',
      243             'provides_icon',
      244             'provides_notes',
      245             'provides_starttime',
      246             'provides_title',
      247 
      248             'e_notes',
      249             ):
      250             if props.has_key(p):
      251                 fp.write('  dngr:%s """%s""";\n' % (p, props[p]))
      252         fp.write('].\n\n')
      253 
      254         return props
      255 
      256     def addTimedEvent(self, yyyy_mm_dd, t1, title, desc,
      257                       reminder_increment='minutes',
      258                       reminder_length=15,
      259                       where=''):
      260         addr = 'http://calendar.sidekick.dngr.com/add?type=timed&date=%s' % yyyy_mm_dd
      261         forms = ParseResponse(self.urlopen(addr))
      262         progress("forms: ", [ f.name for f in forms])
      263         form = [ f for f in forms if f.name == "calendar_add_form" ][0]
      264         progress("add form:", form)
      265         form['e_title'] =  title
      266         form['e_location'] =  where
      267         form['hour_start'] = [str(int(t1[0:2]))] # strip leading 0
      268 
      269         # the form has a limited selection of minute values
      270         # but the danger service groks others.
      271         form.find_control('min_start')._selected = t1[3:5]
      272 
      273         #@@form['reminder_type'] = '0' # in desktop iface, this is on a separate form
      274         #@@form['reminder_increment_0'] = reminder_increment
      275         #@@form['reminder_length_0'] = str(reminder_length)
      276         form['e_notes'] = desc
      277         form['e_icon_id'] = ['%d' % self._icon]
      278 
      279         progress('POSTing add request')
      280         res = ClientCookie.urlopen(form.click()) #@@
      281         progress('POST reply headers: ', res.info())
      282 
      283         # debugging... save response
      284         #open(title, "w").write(res.read())
      285 
      286         #@@get event ID from
      287         #Set-Cookie: calendar_last_visited=%2Fevent%3Fid%3D439; expires=...
      288 
      289 
      290 
      291 def parseContent(pg):
      292     import libxml2 # http://xmlsoft.org/python.html
      293 
      294     txt = pg.read()
      295 
      296     progress("Content: ", txt[:50])
      297     
      298     # xhtml-ize a little bit
      299     # fix &foo=  ==> &#38;foo=
      300     txt = re.sub(r'&(\w+)=', r'&#38;\1=', txt)
      301     txt = txt.replace('&nbsp;', '&#160;')
      302     txt = txt.replace('&copy;', '&#169;')
      303     txt = txt.replace(' SELECTED>', ' selected="selected">')
      304     txt = txt.replace(' CHECKED>', ' checked="checked">')
      305 
      306     print >>sys.stderr, "@@txt type", type(txt)
      307     
      308     #@@return libxml2.htmlParseDoc(txt, 'us-ascii')
      309     return libxml2.htmlParseDoc(txt, None)
      310 
      311 
      312 def parseFlight(flt):
      313     """ separate flight line into date, departure, short description
      314 
      315     >>> parseFlight('2005-02-08 lv MCI 15:00 ar ORD 16:27 Tuesday AMERICAN AIRLINES #1094')
      316     ('2005-02-08', '15:00', 'flt#1094 ->ORD')
      317     """
      318 
      319     desc = 'flt' + flt[flt.rindex("#"):] + ' ->' + flt[27:30]
      320     return flt[:10], flt[18:23], desc
      321 
      322 
      323 def importItinerary(phone, passwd, fp):
      324     a = TMobileDangerAgent()
      325     a.login(phone, passwd)
      326 
      327     for flt in fp:
      328         flt = flt.strip()
      329         date, ti, desc = parseFlight(flt)
      330         progress("== adding event: [%s] %s" % (desc, flt))
      331 
      332         a.addTimedEvent(date, ti, desc, flt, 'minutes', 45)
      333     
      334 def importTimedEvents(phone, passwd, fp, icon):
      335     """import timed events from fp
      336 
      337     read tab-delimited event descriptions:
      338     yyyy-mm-dd hh:mm title desc location
      339     and import them into the sidekick calendar
      340     """
      341 
      342     a = TMobileDangerAgent()
      343     a.login(phone, passwd)
      344     if icon: a._icon = icon
      345 
      346     while 1:
      347         ev = fp.readline()
      348         if not ev: break
      349         ev = ev.strip()
      350         if not ev: break
      351         progress("== adding event:", ev.split("\t"))
      352 
      353 	dt, ti, titl, desc, loc = ev.split("\t")
      354         a.addTimedEvent(dt, ti,
      355                         titl, desc,
      356                         'minutes', 45, #@@hardcoded
      357                          where=loc)
      358     
      359 def importTimedRDF(phone, passwd, client, kb, icon):
      360     """import timed events from RDF kb
      361 
      362     supports timed events only
      363     summary, dtstart required
      364     loc, description optional
      365     """
      366 
      367     import calitems
      368     
      369     assert phone
      370     assert passwd
      371     assert client
      372     
      373     progress("@@scanning RDF:", kb)
      374     records = []
      375     for ev in calitems.eachEvent(kb):
      376         progress("== found event:", ev['title'])
      377         ev['icon_id'] = icon
      378         records.append(ev)
      379 
      380     progress("peer.post", pprint.pformat(records))
      381     if records:
      382         peer = dangerSync.Anchored()
      383         peer.useProduction()
      384         peer.setClient(client)
      385         peer.login(phone, passwd)
      386         ids = peer.post("event", records)
      387         progress("post ids:", ids)
      388 
      389 def testContact(phone, passwd, contact):
      390     a = TMobileDangerAgent()
      391     a.login(phone, passwd)
      392 
      393     progress("== grabbing contact")
      394 
      395     pg = a.pageOfContactNamed(contact)
      396 
      397     progress("== got it", pg)
      398 
      399 
      400 def testCalPage(phone, passwd):
      401     a = TMobileDangerAgent()
      402     a.login(phone, passwd)
      403 
      404     progress("== grabbing calendar")
      405 
      406     pg  = a.urlopen('http://calendar.sidekick.dngr.com/')
      407 
      408     progress("=== calendar headers: ", pg.info())
      409 
      410     doc = parseContent(pg)
      411     print doc.children #@@
      412 
      413     ctxt = doc.xpathNewContext()
      414     res = ctxt.xpathEval("//a[@class='data']")
      415 
      416     for n in res:
      417         progress("a data link:", n.prop('href'), n.children)
      418         
      419     doc.freeDoc()
      420     ctxt.xpathFreeContext()
      421 
      422 
      423 
      424 
      425 def formProps(doc, form):
      426     """return a dictionary of field name/value pairs.
      427 
      428     @@sometimes there's more than one value per name; that
      429     case isn't handled here.
      430     """
      431     
      432     ctxt = doc.xpathNewContext()
      433     ctxt.setContextNode(form)
      434     
      435     props = {}
      436     
      437     fields = ctxt.xpathEval(".//input")
      438     for field in fields:
      439         ty = field.prop('type') or 'text' # defaults to text
      440         name = field.prop('name')
      441 
      442         if ty == 'hidden' or ty == 'text':
      443             v = field.prop('value')
      444             props[name] = v
      445         elif (ty == 'radio'):
      446             if field.prop('checked'):
      447                 v = field.prop('value') or field.content
      448                 props[name] = v
      449         elif ty == 'image' or ty == 'submit':
      450             pass
      451         else:
      452             progress("NOTIMPL: form input; name:", name,
      453                      "type:", ty)
      454 
      455         
      456     fields = ctxt.xpathEval(".//select")
      457 
      458     for f in fields:
      459         name = f.prop('name')
      460         progress("form select; name:", name)
      461         o = f.children
      462         while o:
      463             v = o.prop('value') or o.content
      464             #progress("  option value:", v, "selected?", o.prop('selected'))
      465             if o.prop('selected'): props[name] = v
      466             o = o.next
      467 
      468     fields = ctxt.xpathEval(".//textarea")
      469 
      470     for f in fields:
      471         name = f.prop('name')
      472         progress("textarea name:", f.prop('name'))
      473         props[name] = f.content
      474         
      475     ctxt.xpathFreeContext()
      476 
      477     return props
      478 
      479 
      480 def testCalDay(phone, passwd, dt):
      481     import sys
      482     
      483     a = TMobileDangerAgent()
      484     a.login(phone, passwd)
      485 
      486     progress("== grabbing calendar day", dt)
      487 
      488     res = a.aboutDay(sys.stdout, dt)
      489 
      490     progress("== res:", res)
      491 
      492 
      493 def testEvent(phone, passwd, id):
      494     import sys
      495     
      496     a = TMobileDangerAgent()
      497     a.login(phone, passwd)
      498 
      499     progress("== grabbing event", id)
      500 
      501     a.aboutEvent(sys.stdout, id)
      502 
      503 
      504 def testTrip(outFp, phone, passwd, id):
      505     a = TMobileDangerAgent()
      506     a.login(phone, passwd)
      507 
      508 
      509     outFp.write("""@prefix dngr: <http://dev.w3.org/2001/palmagent/dngr@@#>.
      510 @prefix foaf: <http://xmlns.com/foaf/0.1/>.
      511 @prefix xsdt: <http://www.w3.org/2001/XMLSchema#>.
      512 @prefix k: <http://opencyc.sourceforge.net/daml/cyc.daml#>.
      513 @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.
      514 """)
      515 
      516     progress("== grabbing trip event", id)
      517 
      518     tripProps = a.aboutEvent(outFp, id)
      519     days = int(tripProps['e_day_dur'])
      520     year = int(tripProps['year_start'])
      521     month = int(tripProps['month_start'])
      522     day = int(tripProps['day_start'])
      523     secs = calendar.timegm((year, month, day, 0, 0, 0, 0, 0, 0))
      524 
      525     while days > 0:
      526         year, month, day, hour, minute, second, wday, jday, dst = time.gmtime(secs)
      527         ymd = "%04d-%02d-%02d" % (year, month, day)
      528         progress("trip includes day:", ymd)
      529         events = a.aboutDay(outFp, ymd)
      530         for (addr, title) in events:
      531             progress("found event", title, " on ", ymd)
      532             qs = addr[addr.rindex('id=')+3:] # ...id=434&foo => 434&foo
      533             try: qs = qs[:qs.index('&')]
      534             except ValueError: pass
      535             eid = int(qs)
      536             a.aboutEvent(outFp, eid)
      537             
      538         secs = secs + 60 * 60 * 24
      539         days = days - 1
      540 
      541 
      542 def _unitTest():
      543     import doctest
      544     doctest.testmod()
      545 
      546 
      547 if __name__ == '__main__':
      548     import getopt, sys
      549 
      550     try:
      551         opts, args = getopt.getopt(sys.argv[1:],
      552                                    "dc:n:p:i:",
      553                                    ["datebook", "contact=",
      554                                     "number=", "passwd=",
      555                                     "client=",
      556                                     "icon=",
      557                                     "output=",
      558                                     "testDay=",
      559                                     "testEvent=", "testTrip=",
      560                                     "test",
      561                                     "importItinerary",
      562                                     "importTimed",
      563                                     "importRDF="])
      564     except getopt.GetoptError:
      565         # print help information and exit:
      566         print __doc__
      567         sys.exit(2)
      568 
      569     number = None
      570     passwd = None
      571     contact = None
      572     icon = None
      573     client = None
      574     
      575     for o, a in opts:
      576         if o in ("-p", "--passwd"):
      577             passwd = a
      578         elif o in ("-n", "--number"):
      579             number = a
      580         elif o == '--client':
      581             client = a
      582         elif o in ("-i", "--icon"):
      583             icon = int(a)
      584         elif o in ("-n", "--contact"):
      585             contact = a
      586             testContact(number, passwd, contact)
      587         elif o in ("-d", "--datebook"):
      588             testCalPage(number, passwd)
      589         elif o in ("--testDay",):
      590             dt = a
      591             testCalDay(number, passwd, dt)
      592         elif o in ("--testEvent",):
      593             evid = int(a)
      594             testEvent(number, passwd, evid)
      595         elif o in ("--output",):
      596             outFp = open(a, "w")
      597         elif o in ("--testTrip",):
      598             evid = int(a)
      599             testTrip(outFp, number, passwd, evid)
      600         elif o in ("--test",):
      601             _unitTest()
      602         elif o in ("--importItinerary",):
      603             importItinerary(number, passwd, sys.stdin)
      604         elif o in ("--importTimed",):
      605             importTimedEvents(number, passwd, sys.stdin, icon)
      606         elif o in ("--importRDF",):
      607             import calitems
      608             kb = calitems.fn2kb(a)
      609             progress("loaded RDF:", kb)
      610             import credstore #@@ do this only if cmd line flag
      611             importTimedRDF(number, str(credstore.decrypt()), client, kb, icon)
      612             
      613 
      614 # $Log: hipAgent.py,v $
      615 # Revision 1.27  2007/05/04 20:41:09  connolly
      616 # tweak dates before uploading to danger
      617 #
      618 # Revision 1.26  2007/01/06 00:03:46  connolly
      619 # used credstore to import
      620 #
      621 # Revision 1.25  2006/12/30 06:43:18  connolly
      622 # handle all-day events in importRDF
      623 # show results of post
      624 # import ClientCookie, libxml2 only if needed
      625 #
      626 # Revision 1.24  2006/07/14 07:19:22  connolly
      627 # using hipsrv for date/tz conversion
      628 # using dangerSync for RDF upload, since t-mobile's web site login changed
      629 # and I figured out the XMLRPC write issues
      630 #
      631 # Revision 1.23  2006/05/16 19:59:14  connolly
      632 # normalize space in ical descriptions
      633 #
      634 # Revision 1.22  2006/05/03 20:34:26  connolly
      635 # support import of RDF events without locations
      636 # use logging for debug notices
      637 #
      638 # Revision 1.21  2006/03/26 05:37:39  connolly
      639 # not quite so verbose diagnostics
      640 #
      641 # Revision 1.20  2006/03/26 05:32:10  connolly
      642 # new login sequence seems to work
      643 #
      644 # Revision 1.19  2005/12/27 20:37:17  connolly
      645 # select 2nd form by name
      646 #
      647 # Revision 1.18  2005/11/08 23:03:25  connolly
      648 # location support in importTimed
      649 #
      650 # Revision 1.17  2005/09/08 03:11:56  connolly
      651 # add --icon command-line option
      652 #
      653 # Revision 1.16  2005/02/05 04:33:51  connolly
      654 # updated __doc__ a bit
      655 #
      656 # Revision 1.15  2005/02/05 04:20:14  connolly
      657 # strip leading 0s on hours
      658 #
      659 # Revision 1.14  2005/02/05 04:14:18  connolly
      660 # uploaded an itinerary!
      661 #
      662 # Revision 1.13  2005/02/05 04:02:30  connolly
      663 # yes! added an event! now... just need to override list choices for minutes
      664 #
      665 # Revision 1.12  2003/11/08 17:46:10  connolly
      666 # timed events, for basketball schedule, based on importItinerary
      667 #
      668 # Revision 1.11  2003/07/19 19:15:39  connolly
      669 # cleaned up event naming
      670 #
      671 # Revision 1.10  2003/07/19 18:49:19  connolly
      672 # parseDoc api change?
      673 #
      674 # Revision 1.9  2003/05/16 13:57:50  connolly
      675 # before BUD
      676 #
      677 # Revision 1.8  2003/04/18 16:46:26  connolly
      678 # --testTrip now writes real N3
      679 #
      680 # Revision 1.7  2003/03/27 23:56:22  connolly
      681 # generalized --testApr just a bit, to --importItin
      682 #
      683 # Revision 1.6  2003/03/13 05:06:05  connolly
      684 # testDay: get a day's worth of stuff in N3
      685 # testApr: POSTing some events
      686 #
      687 # Revision 1.5  2003/03/03 16:23:44  connolly
      688 # - got one test for aboutDay working.
      689 # - started playing with pure-python HTML/DOM/XPath
      690 #   replacement for libxml2 (not winning yet)
      691 #
      692 # Revision 1.4  2003/01/30 16:33:22  connolly
      693 # 1Jan: clues for eliminating dependency on libxml2 in favor of PyMXL
      694 #
      695 # Revision 1.3  2002/12/19 22:08:17  connolly
      696 # initial foray into addressbook navigation
      697 # converted options processing to getopt
      698 #
      699 # Revision 1.2  2002/12/14 21:10:12  connolly
      700 # extracting links from html using libxml2 works
      701 #
      702 # Revision 1.1  2002/12/14 18:46:34  connolly
      703 # got login working
      704 #