2 hipsrv.py -- serve hiptop/sidekick data as hCalendar, hCard, and/or RDF/XML
6 DESCRIBE ?E WHERE { ?E cal:dtstart ?S FILTER str(?S) >= 2006-01-01 }.
8 hmm... use templates, a la nevow or kid_?
13 Date: 2005-03-09 15:26:45 -0500 (Wed, 09 Mar 2005)
15 .. kid_: http://lesscode.org/projects/kid/
17 pytz - World Timezone Definitions for Python
18 Stuart Bishop <stuart@stuartbishop.net>
19 http://pytz.sourceforge.net/
27 from datetime import datetime
30 class Usage(Exception):
34 python hipserv.py --current=2006-01 event-template.kid >event-hcal.html
35 python hipserv.py --weekly event-template.kid >event-hcal.html
39 return self.__doc__ + "\n"
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"),
51 """Serve hiptop data as XHTML using microformats
54 def __init__(self, event, contact, task):
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
61 self._contact = contact
65 """iterate over events
67 RFE/TODO: support ETag, max-age, if-modified-since
69 Hmm... we're pushing the stdEvent() conversion below filtering
73 del keys[keys.index('anchor')] # RFE/TODO: use it as ETag
74 keys.sort(key=lambda k: events[k]['start_date'])
80 """iterate over tasks/todos
82 RFE/TODO: support ETag, max-age, if-modified-since
85 for k, v in self._task.iteritems():
91 """iterate over contacts
93 RFE/TODO: support ETag, max-age, if-modified-since
96 keys = self._contact.keys()
100 yield stdContact(self._contact[k])
103 def events_ht(self, template, filter):
104 """get plans, filtered, as hypertext
105 i.e. a generator over strings ala kid.Template.generate()
107 RFE/TODO: support ETag, max-age, if-modified-since
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'):
119 """Convert from danger/hiptop/sidekick vocab to iCalendar vocab
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_
124 hipaddr = 'http://%s@calendar.sidekick.dngr.com/event?id=%s' % \
127 e = {'id': d['id'], #hmm... not really iCalendar vocab; more like HTML
130 'summary': d['title'],
131 'description': d.get('notes', ''),
132 'dtstamp': asDate(d['last_modified']),
133 'location': d.get('location', ''),
136 if d['event_type'] == 2: # all day
137 e['dtstart'] = asDate(d['start_date'][:8])
138 e['dtend'] = asDate(d['end_date'][:8])
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'],
144 e['dtend_'], e['dtend'] = asLocalDate(d['end_date'], d['timezone'])
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)
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'])
157 if d['repeat_type'] == 3 and d.has_key('primary_repeat'):
158 rrule['bymonthday'] = d['primary_repeat']
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'])
167 cat = DngrCalIcons[d['icon_id']]
168 e['categories'] = cat
169 e['attach'] = DngrCalIconPat % cat
173 def stdExcept(exs, start):
175 >>> stdExcept([{'date': '20040424'}], '1993-04-24T00:00:00Z')
176 ['2004-04-24T00:00:00Z']
179 return ["%sT%s" % (asDate(ex['date']), tod) for ex in exs]
182 DngrCalIconPat = 'http://img.prod1.dngr.net/img/voicestream/apps/calendar/icons/32/%s.gif'
203 def whichdays(weekdays, nth=None):
204 """convert weekday bits to a string
212 days = ('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA')
214 for i in range(0, 7):
218 if nth: s = `nth` + s
223 """punctuate item as per ISO8601 and W3C XML Schema datatypes
225 >>> asDate("20041225")
227 >>> asDate("20041202T170000Z")
228 '2004-12-02T17:00:00Z'
231 return "%s-%s-%s:%s:%s" % (item[:4], item[4:6], item[6:11],
232 item[11:13] ,item[13:])
234 return "%s-%s-%s" % (item[:4], item[4:6], item[6:8])
236 raise ValueError, item
238 def asLocalDate(item, tz, float=0):
239 """Use pytz to convert dngr datetime to hCalendar style
241 >>> asLocalDate('20060321T143000Z', 'America/Chicago')
242 ('2006-03-21T08:30:00-06:00', '2006-03-21T14:30:00Z')
244 >>> asLocalDate("20070911T003000Z", "America/Chicago")
245 ('2007-09-10T19:30:00-05:00', '2007-09-11T00:30:00Z')
247 >>> asLocalDate('20060321T143000Z', 'America/Chicago', float=1)
248 '2006-03-21T08:30:00'
250 from pytz import timezone # http://pytz.sourceforge.net/
252 when = datetime(year=int(item[:4]),
253 month=int(item[4:6]),
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))
261 if float: return s[:19]
262 else: return s, when.astimezone(timezone("UTC")).isoformat()[:19]+"Z"
264 def asHiptopDate(ldate, tz, float=0):
265 """Use pytz to convert local datetime to z time hiptop style
267 >>> asHiptopDate('2006-07-17T12:55:00', 'America/Chicago')
270 import dateutil.tz # http://labix.org/python-dateutil
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(":", "")
283 """Convert from danger/hiptop/sidekick task to iCalendar VTODO vocab
285 hipaddr = 'http://%s@todo.sidekick.dngr.com/task?id=%s' % \
288 t = {'id': d['id'], #hmm... not really iCalendar vocab; more like HTML
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']),
298 when = d.get('due_date', None)
299 if when: t['due'] = asDate(when)
302 t['status'] = 'COMPLETED'
303 t['completed'] = t['dtstamp']
308 """Convert from danger/hiptop/sidekick contact to vCard vocab
310 hipaddr = 'http://%s@address.sidekick.dngr.com/edit?contact_id=%s' % \
313 c = {'id': d['id'], #hmm... not really vCard vocab; more like HTML
316 'rev': asDate(d['last_modified']),
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
325 copyField(d, 'nick_name', c, 'nickname')
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'],
331 elif d.has_key('company'):
332 c['fn'] = d['company']
333 elif d.has_key('nick_name'):
334 c['fn'] = d['nick_name']
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')
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']
350 adrs = d.get('addresses', 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]
362 TelTypes = {"Home": "home msg",
364 "Office": "work msg",
366 "Personal": "voice", #@@ InverseFunctional
367 "Toll Free": "work msg",
368 "Mobile": "cell msg",
369 "Unlabeled": "voice",
376 >>> fixTel({'number': "(913) 696-1234"})['value']
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
388 line['type'] = TelTypes[line['label']].split()
390 warn("unkown tel type", line)
391 line['type'] = ['voice']
396 mbox['value'] = mbox['address']
397 mbox['type'] = ["internet"]
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()
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')
420 def copyField(src, fns, dest, fnd):
421 v = src.get(fns, None)
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
430 We assume 9999 is after all dates.
432 >>> CurrentFilter('2006-01')({'dtstart': '2006-02-01'})
434 >>> CurrentFilter('2006-01')({'dtstart': '2005-02-01', 'dtend': '2005-02-02'})
436 >>> CurrentFilter('2006-01-01')({'dtstart': '2006-05-22', 'dtend': '2006-05-27'})
438 >>> CurrentFilter('2006-01-01')({'dtstart': '2006-07-16', 'dtend': '2006-07-21'})
442 def __init__(self, horizon):
445 def __call__(self, event):
447 r = event.get('dtend', event.get('dtstart', '0000')) >= horizon or \
448 (event.get('rrule', False) and \
449 event['rrule'].get('until', '9999') >= horizon)
460 for s in h.events_ht(template=kid.Template(file='event.kid'),
467 sys.stderr.write(str(a))
468 sys.stderr.write("\n")
472 # move to a JSON/SPARQL module?
473 from trxtsv import PathFilter, AndFilter # @@ devcvs/2000/quacken/
474 import kid # http://lesscode.org/projects/kid/
477 opts, args = getopt.getopt(argv[1:], "w",
478 ["weekly", "current=", "category=",
486 f = PathFilter('WEEKLY', ('rrule', 'freq'))
487 if filter: filter = AndFilter(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)
494 if filter: filter = AndFilter(filter, f)
496 elif o == '--current':
499 if filter: filter = AndFilter(filter, f)
504 raise Usage #@@ include the offending arg?
509 tpl = kid.Template(file=args[0])
511 for s in h.events_ht(template=tpl, filter=filter):
516 if __name__ == '__main__':
519 if '--test' in sys.argv:
525 sys.stderr.write(str(e))
531 except getopt.GetoptError: