hipsrv.py
changeset 302: 03c221a1f62a
parent 275:7a3fa52edd18
child 317:aac29f4d7588
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 hipsrv.py -- serve hiptop/sidekick data as hCalendar, hCard, and/or RDF/XML
        3 
        4 First use case:
        5 
        6 DESCRIBE ?E WHERE { ?E cal:dtstart ?S FILTER str(?S) >= 2006-01-01 }.
        7 
        8 hmm... use templates, a la nevow or kid_?
        9 
       10 Kid User's Guide
       11 0.6.
       12 Revision: 	131
       13 Date: 	2005-03-09 15:26:45 -0500 (Wed, 09 Mar 2005)
       14 
       15 .. kid_: http://lesscode.org/projects/kid/
       16 
       17 pytz - World Timezone Definitions for Python
       18 Stuart Bishop <stuart@stuartbishop.net>
       19 http://pytz.sourceforge.net/
       20 2005r 2005-12-31
       21 
       22 """
       23 
       24 import itertools
       25 import shelve
       26 
       27 from datetime import datetime
       28 
       29 
       30 class Usage(Exception):
       31     """
       32     Usage:
       33 
       34     python hipserv.py --current=2006-01 event-template.kid >event-hcal.html
       35     python hipserv.py --weekly event-template.kid >event-hcal.html
       36     """
       37 
       38     def __str__(self):
       39 	return self.__doc__ + "\n"
       40 
       41 
       42 def load(event = 'event', contact = 'contact', task='task'):
       43     if task: task = shelve.open(task, "r")
       44     return HipTop(event = shelve.open(event, "r"),
       45 		  contact = shelve.open(contact, "r"),
       46                   task = task
       47                   )
       48 
       49 
       50 class HipTop(object):
       51     """Serve hiptop data as XHTML using microformats
       52     """
       53 
       54     def __init__(self, event, contact, task):
       55 	"""
       56 	:param event: a dict/shelf of event objects, per danger XMLRPC
       57 	:param contact: a dict/shelf of contact objects, per danger XMLRPC
       58 	:param task: a dict/shelf of task objects, per danger XMLRPC
       59 	"""
       60 	self._event = event
       61 	self._contact = contact
       62 	self._task = task
       63 
       64     def events(self):
       65 	"""iterate over events
       66 
       67 	RFE/TODO: support ETag, max-age, if-modified-since
       68 
       69 	Hmm... we're pushing the stdEvent() conversion below filtering
       70 	"""
       71 	events = self._event
       72 	keys = events.keys()
       73 	del keys[keys.index('anchor')]  # RFE/TODO: use it as ETag
       74 	keys.sort(key=lambda k: events[k]['start_date'])
       75 	for k in keys:
       76 	    item = events[k]
       77 	    yield stdEvent(item)
       78 
       79     def tasks(self):
       80 	"""iterate over tasks/todos
       81 
       82 	RFE/TODO: support ETag, max-age, if-modified-since
       83 	"""
       84         if self._task:
       85             for k, v in self._task.iteritems():
       86                 if k != 'anchor':
       87                     yield stdTask(v)
       88 
       89 
       90     def contacts(self):
       91 	"""iterate over contacts
       92 
       93 	RFE/TODO: support ETag, max-age, if-modified-since
       94 	"""
       95         if self._contact:
       96             keys = self._contact.keys()
       97             keys.sort()
       98             for k in keys:
       99                 if k != 'anchor':
      100                     yield stdContact(self._contact[k])
      101 
      102 
      103     def events_ht(self, template, filter):
      104 	"""get plans, filtered, as hypertext
      105 	i.e. a generator over strings ala kid.Template.generate()
      106 
      107 	RFE/TODO: support ETag, max-age, if-modified-since
      108 	"""
      109 
      110 	events = itertools.ifilter(filter, self.events())
      111 	template.events = events
      112 	template.todos = self.tasks()
      113 	template.contacts = itertools.ifilter(filter, self.contacts()) #@@hmm
      114 	for s in template.generate(output='xml', encoding='utf-8'):
      115 	    yield s
      116 
      117 
      118 def stdEvent(d):
      119     """Convert from danger/hiptop/sidekick vocab to iCalendar vocab
      120 
      121     Hm... for single-occurrence events, return Z time or -hh:mm time?
      122     Currently returning Z time in dtstart, -hh:mm time in dtstart_
      123     """
      124     hipaddr = 'http://%s@calendar.sidekick.dngr.com/event?id=%s' % \
      125               ('majo', #@@
      126                d['id'])
      127     e = {'id': d['id'], #hmm... not really iCalendar vocab; more like HTML
      128          'uid': hipaddr,
      129          'url': hipaddr,
      130 	 'summary': d['title'],
      131 	 'description': d.get('notes', ''),
      132 	 'dtstamp': asDate(d['last_modified']),
      133 	 'location': d.get('location', ''),
      134 	 }
      135 
      136     if d['event_type'] == 2: # all day
      137 	e['dtstart'] = asDate(d['start_date'][:8])
      138 	e['dtend'] = asDate(d['end_date'][:8])
      139 
      140     if d['repeat_type'] == 0: # no repeat
      141 	if d['event_type'] in (0, 1):
      142 	    e['dtstart_'], e['dtstart'] = asLocalDate(d['start_date'],
      143                                                       d['timezone'])
      144 	    e['dtend_'], e['dtend'] = asLocalDate(d['end_date'], d['timezone'])
      145     else:
      146 	if d['event_type'] in (0, 1): # repeating at some time
      147 	    e['dtstart'] = asLocalDate(d['start_date'], d['timezone'], float=1)
      148 	    e['dtend'] = asLocalDate(d['end_date'], d['timezone'], float=1)
      149 
      150 	e['rrule'] = rrule = {}
      151 	rrule['freq'] = freq = (None, 'DAILY', 'WEEKLY', 'MONTHLY',
      152 				'MONTHLY', 'YEARLY')[d['repeat_type']]
      153 	rrule['interval'] = d['interval']
      154 	if d.get('repeat_end_date', ''):
      155 	    rrule['until'] = asDate(d['repeat_end_date'])
      156 
      157 	if d['repeat_type'] == 3 and d.has_key('primary_repeat'):
      158 		rrule['bymonthday'] = d['primary_repeat']
      159 
      160 	if d.has_key('weekdays'):
      161 	    rrule['byday'] = whichdays(d['weekdays'],
      162 				       d.get('primary_repeat', None))
      163 	if d.has_key('exceptions'):
      164 	    e['exdate'] = stdExcept(d['exceptions'], e['dtstart'])
      165 
      166     if d['icon_id'] > 0:
      167 	cat = DngrCalIcons[d['icon_id']]
      168 	e['categories'] = cat
      169 	e['attach'] = DngrCalIconPat % cat
      170     return e
      171 
      172 
      173 def stdExcept(exs, start):
      174     """
      175     >>> stdExcept([{'date': '20040424'}], '1993-04-24T00:00:00Z')
      176     ['2004-04-24T00:00:00Z']
      177     """
      178     tod = start[11:20]
      179     return ["%sT%s" % (asDate(ex['date']), tod) for ex in exs]
      180 
      181 
      182 DngrCalIconPat = 'http://img.prod1.dngr.net/img/voicestream/apps/calendar/icons/32/%s.gif'
      183 DngrCalIcons=(
      184     'calendar_page',
      185     'backpack',
      186     'birthday',
      187     'briefcase',
      188     'coffee',
      189     'concert',
      190     'exercise',
      191     'flower',
      192     'home',
      193     'martini',
      194     'meeting',
      195     'popcorn',
      196     'romantic',
      197     'shopping',
      198     'sports',
      199     'travel',
      200     'vacation'
      201 )
      202 
      203 def whichdays(weekdays, nth=None):
      204     """convert weekday bits to a string
      205 
      206     >>> whichdays(62)
      207     'MO,TU,WE,TH,FR'
      208     >>> whichdays(16, 2)
      209     '2TH'
      210     """
      211 
      212     days = ('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA')
      213     r = []
      214     for i in range(0, 7):
      215 	if weekdays & 1<<i:
      216 	    r.append(days[i])
      217     s = ','.join(r)
      218     if nth: s = `nth` + s
      219     return s
      220 
      221 
      222 def asDate(item):
      223     """punctuate item as per ISO8601 and W3C XML Schema datatypes
      224 
      225     >>> asDate("20041225")
      226     '2004-12-25'
      227     >>> asDate("20041202T170000Z")
      228     '2004-12-02T17:00:00Z'
      229     """
      230     if len(item) == 16:
      231 	return "%s-%s-%s:%s:%s" % (item[:4], item[4:6], item[6:11],
      232 				   item[11:13] ,item[13:])
      233     elif len(item) == 8:
      234 	return "%s-%s-%s" % (item[:4], item[4:6], item[6:8])
      235     else:
      236 	raise ValueError, item
      237 
      238 def asLocalDate(item, tz, float=0):
      239     """Use pytz to convert dngr datetime to hCalendar style
      240 
      241     >>> asLocalDate('20060321T143000Z', 'America/Chicago')
      242     ('2006-03-21T08:30:00-06:00', '2006-03-21T14:30:00Z')
      243 
      244     >>> asLocalDate("20070911T003000Z", "America/Chicago")
      245     ('2007-09-10T19:30:00-05:00', '2007-09-11T00:30:00Z')
      246 
      247     >>> asLocalDate('20060321T143000Z', 'America/Chicago', float=1)
      248     '2006-03-21T08:30:00'
      249     """
      250     from pytz import timezone # http://pytz.sourceforge.net/
      251 
      252     when = datetime(year=int(item[:4]),
      253                     month=int(item[4:6]),
      254                     day=int(item[6:8]),
      255                     hour=int(item[9:11]),
      256                     minute=int(item[11:13]),
      257                     second=int(item[13:15]),
      258                     tzinfo=timezone("UTC"))
      259     when = when.astimezone(timezone(tz))
      260     s = when.isoformat()
      261     if float: return s[:19]
      262     else: return s, when.astimezone(timezone("UTC")).isoformat()[:19]+"Z"
      263 
      264 def asHiptopDate(ldate, tz, float=0):
      265     """Use pytz to convert local datetime to z time hiptop style
      266 
      267     >>> asHiptopDate('2006-07-17T12:55:00', 'America/Chicago')
      268     '20060717T175500Z'
      269     """
      270     import dateutil.tz # http://labix.org/python-dateutil
      271 
      272     when = datetime(year=int(ldate[:4]),
      273                     month=int(ldate[5:7]),
      274                     day=int(ldate[8:10]),
      275                     hour=int(ldate[11:13]),
      276                     minute=int(ldate[14:16]),
      277                     second=int(ldate[17:19]),
      278                     tzinfo=dateutil.tz.gettz(tz))
      279     whenz = when.astimezone(dateutil.tz.tzutc()).isoformat()[:19]+"Z"
      280     return whenz.replace("-", '').replace(":", "")
      281 
      282 def stdTask(d):
      283     """Convert from danger/hiptop/sidekick task to iCalendar VTODO vocab
      284     """
      285     hipaddr = 'http://%s@todo.sidekick.dngr.com/task?id=%s' % \
      286               ('majo', #@@
      287                d['id'])
      288     t = {'id': d['id'], #hmm... not really iCalendar vocab; more like HTML
      289          'uid': hipaddr,
      290          'url': hipaddr,
      291 	 'summary': d['title'],
      292 	 'description': d.get('notes', None),
      293          # 1 goes to 1, 2 goes to 5, 3 goes to 9
      294          'priority': 4 * d['priority'] - 3,
      295 	 'dtstamp': asDate(d['last_modified']),
      296 	 }
      297 
      298     when = d.get('due_date', None)
      299     if when: t['due'] = asDate(when)
      300     
      301     if d['is_done']:
      302         t['status'] = 'COMPLETED'
      303         t['completed'] = t['dtstamp']
      304     return t
      305 
      306 
      307 def stdContact(d):
      308     """Convert from danger/hiptop/sidekick contact to vCard vocab
      309     """
      310     hipaddr = 'http://%s@address.sidekick.dngr.com/edit?contact_id=%s' % \
      311               ('majo', #@@
      312                d['id'])
      313     c = {'id': d['id'], #hmm... not really vCard vocab; more like HTML
      314          'uid': hipaddr,
      315          'url': hipaddr,
      316 	 'rev': asDate(d['last_modified']),
      317 	 }
      318 
      319     n = {}
      320     copyField(d, 'first_name', n, 'given-name')
      321     copyField(d, 'last_name', n, 'family-name')
      322     copyField(d, 'middle_name', n, 'additional-name')
      323     if n.keys(): c['n'] = n
      324 
      325     copyField(d, 'nick_name', c, 'nickname')
      326     
      327     if d.get('first_name', None) or \
      328        d.get('last_name', None):
      329         c['fn'] = "%s %s %s" % (d['first_name'], d['middle_name'],
      330                                 d['last_name'])
      331     elif d.has_key('company'):
      332         c['fn'] = d['company']
      333     elif d.has_key('nick_name'):
      334         c['fn'] = d['nick_name']
      335 
      336     copyField(d, 'job_title', c, 'title')
      337     copyField(d, 'company', c, 'org')
      338     copyField(d, 'notes', c, 'note')
      339     copyField(d, 'category', c, 'categories')
      340 
      341     copyField(d, 'urls', c, 'urls') 
      342     if d.has_key('urls'): #@@ not standard
      343         for link in d['urls']:
      344             addr = link['address']
      345             # fix www.foo.com addresses to be real URIs
      346             if not ':' in addr: link['address'] = "http://" + addr
      347             if not link['label']: del link['label']
      348         c['urls'] = d['urls']
      349 
      350     adrs = d.get('addresses', None)
      351     if adrs is not None:
      352         c['adr'] = [fixAdr(adr) for adr in adrs]
      353     lines = d.get('phone_numbers', None)
      354     if lines is not None:
      355         c['tel'] = [fixTel(line) for line in lines]
      356     mboxes = d.get('email_addresses', None)
      357     if mboxes is not None:
      358 	c['email'] = [fixMail(mbox) for mbox in mboxes]
      359     
      360     return c
      361 
      362 TelTypes = {"Home": "home msg",
      363 	    "Work": "work msg",
      364 	    "Office": "work msg",
      365 	    "Fax": "fax",
      366 	    "Personal": "voice", #@@ InverseFunctional
      367 	    "Toll Free": "work msg",
      368 	    "Mobile": "cell msg",
      369 	    "Unlabeled": "voice",
      370 	    "":  "voice"
      371 	    }
      372 
      373 
      374 def fixTel(line):
      375     """
      376     >>> fixTel({'number': "(913) 696-1234"})['value']
      377     '+1-913-696-1234'
      378     """
      379     num = line['number']
      380     if not num.startswith("+"):
      381 	num=num.replace(" ", "")
      382 	if num.startswith("1-"): num = "+" + num
      383         elif num.startswith("("):
      384 	    num="+1-" + num[1:].replace(")", "-")
      385         elif len(num) == 12: num = "+1-" + num
      386     line['value'] = num
      387     try:
      388 	line['type'] = TelTypes[line['label']].split()
      389     except KeyError:
      390 	warn("unkown tel type", line)
      391 	line['type'] = ['voice']
      392     return line
      393 
      394 
      395 def fixMail(mbox):
      396     mbox['value'] = mbox['address']
      397     mbox['type'] = ["internet"]
      398     return mbox
      399 
      400 def fixAdr(dngr):
      401     adr = {}
      402     #hmm... use a dictionary instead, maybe..
      403     for l, t in (("Home", "dom"), # @@"intl postal parcel dom"
      404                  ("Work", "work"), # @@ intl postal parcel work
      405                  ("Office", "work"), #@@intl postal parcel work
      406                  ("Unlabeled", "intl postal parcel work"),
      407                  ("",  "intl postal parcel work")):
      408         if dngr['label'] == l:
      409             adr['type'] = t.split()
      410             break
      411     else:
      412         warn("unknown address label", dngr['label'])
      413     copyField(dngr, 'street', adr, 'street-address')
      414     copyField(dngr, 'city', adr, 'locality')
      415     copyField(dngr, 'state', adr, 'region')
      416     copyField(dngr, 'zip', adr, 'postal-code')
      417     copyField(dngr, 'country', adr, 'country-name')
      418     return adr
      419 
      420 def copyField(src, fns, dest, fnd):
      421     v = src.get(fns, None)
      422     if v: dest[fnd] = v
      423 
      424 class CurrentFilter(object):
      425     """recurring events are contemporary with a horizon if their
      426     end-date is unknown or >= the horizon.
      427     single-occurrence events are contemporary iff their end-date
      428     is >= the horizon
      429 
      430     We assume 9999 is after all dates.
      431 
      432     >>> CurrentFilter('2006-01')({'dtstart': '2006-02-01'})
      433     True
      434     >>> CurrentFilter('2006-01')({'dtstart': '2005-02-01', 'dtend': '2005-02-02'})
      435     False
      436     >>> CurrentFilter('2006-01-01')({'dtstart': '2006-05-22', 'dtend': '2006-05-27'})
      437     True
      438     >>> CurrentFilter('2006-01-01')({'dtstart': '2006-07-16', 'dtend':  '2006-07-21'})
      439     True
      440     
      441     """
      442     def __init__(self, horizon):
      443 	self._when = horizon
      444 
      445     def __call__(self, event):
      446 	horizon = self._when
      447 	r = event.get('dtend', event.get('dtstart', '0000')) >= horizon or \
      448 	    (event.get('rrule', False) and \
      449 	     event['rrule'].get('until', '9999') >= horizon)
      450 	return r
      451 
      452 def _test():
      453     import doctest
      454     doctest.testmod()
      455 
      456 def _test2():
      457     import sys
      458 
      459     h = load()
      460     for s in h.events_ht(template=kid.Template(file='event.kid'),
      461 			horizon="20060101"):
      462 	sys.stdout.write(s)
      463 
      464 def warn(*args):
      465     import sys
      466     for a in args:
      467 	sys.stderr.write(str(a))
      468 	sys.stderr.write("\n")
      469 	
      470 
      471 def main(argv):
      472     # move to a JSON/SPARQL module?
      473     from trxtsv import PathFilter, AndFilter # @@ devcvs/2000/quacken/
      474     import kid # http://lesscode.org/projects/kid/
      475 
      476     import getopt
      477     opts, args = getopt.getopt(argv[1:], "w",
      478 			       ["weekly", "current=", "category=",
      479                                 "todo"])
      480 
      481     filter = None
      482     current = None
      483     task = None
      484     for o, a in opts:
      485 	if o == '--weekly':
      486 	    f = PathFilter('WEEKLY', ('rrule', 'freq'))
      487 	    if filter: filter = AndFilter(filter, f)
      488 	    else: filter = f
      489 	elif o == '--category':
      490             fs = [PathFilter(cat, ('categories',)) for cat in a.split(",")]
      491             if len(fs) == 1: f = fs[0]
      492             else: f = OrFilter(*fs)
      493 
      494 	    if filter: filter = AndFilter(filter, f)
      495 	    else: filter = f
      496 	elif o == '--current':
      497 	    current = a
      498 	    f = CurrentFilter(a)
      499 	    if filter: filter = AndFilter(filter, f)
      500 	    else: filter = f
      501 	elif o == '--todo':
      502             task = 'task'
      503 	else:
      504 	    raise Usage #@@ include the offending arg?
      505     if len(args) <> 1:
      506 	raise Usage
      507 
      508     h = load(task=task)
      509     tpl = kid.Template(file=args[0])
      510     tpl.when = current
      511     for s in h.events_ht(template=tpl, filter=filter):
      512 	sys.stdout.write(s)
      513 
      514 
      515 
      516 if __name__ == '__main__':
      517     import sys
      518 
      519     if '--test' in sys.argv:
      520 	_test()
      521     else:
      522 	try:
      523 	    main(sys.argv)
      524 	except Usage, e:
      525 	    sys.stderr.write(str(e))
      526 	    sys.exit(2)
      527 
      528     if 0: #@@hmm...
      529 	try:
      530 	    main(sys.argv)
      531 	except getopt.GetoptError:
      532 	    print __doc__
      533 	    sys.exit(2)
      534 
      535