#!/usr/bin/python """toIcal.py -- convert RDF to iCalendar syntax USAGE: python toIcal.py foo.rdf> foo.ics python toIcal.py foo.n3> foo.ics python toIcal.py http://example/foo.rdf> foo.ics To override floating times and put them in a timezone: python toIcal.py --floattz America/Chicago work.rdf> work.ics see also: RDF Calendar Workspace http://www.w3.org/2002/12/cal/ """ """ References: Internet Calendaring and Scheduling Core Object Specification (iCalendar) November 1998 http://www.ietf.org/rfc/rfc2445.txt A quick look at iCalendar http://www.w3.org/2000/01/foo NOTE: see earlier work: http://www.w3.org/2002/01dc-nj/toICal.py @@cite and use python style see changelog at end Copyright (C) 2000-2004 World Wide Web Consortium, (Massachusetts Institute of Technology, European Research Consortium for Informatics and Mathematics, Keio University). All Rights Reserved. This work is distributed under the W3C(R) Software License [1] 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. """ __version__ = '$Id: toIcal.py,v 2.41 2010年03月30日 20:29:21 timbl Exp $' from string import maketrans, translate from swap.myStore import Namespace, load, setStore # http://www.w3.org/2000/10/swap/ from swap.RDFSink import LITERAL_DT #hmm... generate from schema? from fromIcal import iCalendarDefs # http://www.w3.org/2002/12/cal/ CRLF = chr(13) + chr(10) RDF = Namespace("http://www.w3.org/1999/02/22-rdf-syntax-ns#") # ICAL = Namespace('http://www.w3.org/2002/12/cal/icaltzd#') ICAL = Namespace('http://www.w3.org/2002/12/cal/ical#') XMLSchema = Namespace('http://www.w3.org/2001/XMLSchema#') class CalWr: def __init__(self, writeFun): self._w = writeFun self.floatTZ = None # timezone for floating times def export(self, sts, addr): """export calendar objects from an RDF graph in iCalendar syntax """ for cal in sts.each(pred = RDF.type, obj = ICAL.Vcalendar): progress('Calendar:') self.doComponent(sts, cal, "VCALENDAR", iCalendarDefs) def doComponent(self, sts, comp, name, decls): w = self._w w("BEGIN:%s%s" % (name, CRLF)) className, props, subs = decls[name] if self.floatTZ and name == "VCALENDAR": # In the floatTZ case, we write out a timezone decl, # but it has a fully-qualified TZID, which Apple iCal doesn't # seem to grok (@@bug report pending). # So we use the short TZID to refer to this timezone, # which works even though it shouldn't. tzaddr = TZD + self.floatTZ progress("loading timezone...", tzaddr) tzkb = load(tzaddr) for tzc in tzkb.each(pred = RDF.type, obj = ICAL.Vtimezone): progress("exporting timezone...", tzc) save, self.floatTZ = self.floatTZ, None self.doComponent(tzkb, tzc, "VTIMEZONE", subs) self.floatTZ = save propNames = props.keys() propNames.sort() for prop in propNames: predName, valueType = props[prop][:2] for val in sts.each(comp, ICAL.sym(predName)): if valueType == 'TEXT': self.doSIMPLE(mkTEXT(val, sts), prop) elif valueType == 'INTEGER': self.doSIMPLE(mkINTEGER(val), prop) elif valueType == 'FLOAT': self.doSIMPLE(mkFLOAT(val), prop) elif valueType == 'URI': self.doURI(val, prop) elif valueType == 'DATE-TIME': self.doDateTime(sts, val, prop, predName) elif valueType == 'DURATION': self.doDuration(sts, val, prop, predName) elif valueType == 'RECUR': self.doRecur(sts, val, prop, predName) elif valueType == 'CAL-ADDRESS': self.doCalAddress(sts, val, prop, predName) elif type(valueType) == tuple: itemType = valueType[0] if itemType not in ('TEXT', 'INTEGER', 'FLOAT'): raise RuntimeError, "list value type not implemented" values = [] while 1: first = val.first val = val.rest mkSIMPLE = {'TEXT': mkTEXT, 'INTEGER': mkINTEGER, 'FLOAT': mkFLOAT}[itemType] v = mkSIMPLE(first) values.append(v) if val == RDF.nil: break self.doSIMPLE(';'.join(values), prop) else: raise RuntimeError, "value type not implemented: " + \ str(valueType) + " on " + str(prop) compToDo = [] for sub in sts.each(subj = comp, pred = ICAL.component): for subName in subs.keys(): className, p, s = subs[subName] if sts.statementsMatching(RDF.type, sub, ICAL.sym(className)): compToDo.append((sts, sub, subName, subs)) break else: raise ValueError, "no component class found: %s" % subName # compToDo.sort(key=compKey) # darn... only in python 2.4 compToDo.sort(componentOrder) for sts, sub, subName, subs in compToDo: self.doComponent(sts, sub, subName, subs) # timezone standard/daylight components use a different structure # hmm... is this a good idea? if name == 'VTIMEZONE': self.doTimeZone(sts, comp, subs) w("END:%s%s" % (name, CRLF)) def doTimeZone(self, sts, comp, subs): partNames = subs.keys() partNames.sort() for part in partNames: n, p, c = subs[part] sub = sts.any(subj = comp, pred=ICAL.sym(n)) if sub: self.doComponent(sts, sub, part, subs) def doSIMPLE(self, v, propName): w = self._w w("%s:%s%s" % (propName, v, CRLF)) def doURI(self, sym, propName): """ handle reference properties i.e. properties with value type URI http://www.w3.org/2002/12/cal/rfc2445#sec4.3.13 @@perhaps add support for example from 4.2.8 Format Type http://www.w3.org/2002/12/cal/rfc2445#sec4.2.8 ATTACH;FMTTYPE=application/binary:ftp://domain.com/pub/docs/agenda.doc """ uri = sym.uriref() #@@need to encode non-ascii chars w = self._w w("%s;VALUE=URI:%s%s" % (propName, uri, CRLF)) def doDateTime(self, sts, when, propName, predName): """ helper function to output general date/dateTime value""" w = self._w tk, tv = when.asPair() if tk is LITERAL_DT: tlit, dt = tv if dt == XMLSchema.date.uriref(): w("%s;VALUE=DATE:%s%s" % (propName, mkDATE(tlit), CRLF)) else: tlit = tlit.replace("-", "").replace(":", "") z = "" if tlit[-1:] == "Z": z = "Z" tlit = tlit[:-1] tlit = (tlit + "000000")[:15] # Must include seconds if dt == XMLSchema.dateTime.uriref(): w("%s:%s%s%s" % (propName, tlit, z, CRLF)) elif dt == ICAL.dateTime.uriref(): if self.floatTZ: w("%s;TZID=%s:%s%s%s" % (propName, self.floatTZ, tlit, z, CRLF)) else: w("%s:%s%s%s" % (propName, tlit, z, CRLF)) else: whenTZ = tzid(dt) w("%s;VALUE=DATE-TIME;TZID=%s:%s%s" % (propName, str(whenTZ), tlit, CRLF)) def doDuration(self, sts, r, propName, predName): w = self._w w(propName) related = sts.any(r, ICAL.related) if related: w(";RELATED=" + str(related)) dur = sts.the(r, RDF.value) w(":" + str(dur)) w(CRLF) def doRecur(self, sts, r, propName, predName): w = self._w w(propName + ":") freq = sts.any(r, ICAL.freq) if freq: w("FREQ=%s" % freq) else: raise ValueError, "no freq in recur" when = sts.any(r, ICAL.until) if when: w(";UNTIL=%s" % mkDATE(when)) ival = sts.any(r, ICAL.count) if ival: w(";COUNT=%s" % ival) ival = sts.any(r, ICAL.interval) if ival: w(";INTERVAL=%s" % ival) by = sts.any(r, ICAL.byday) if by: w(";BYDAY=%s" % by) by = sts.any(r, ICAL.bymonthday) if by: w(";BYMONTHDAY=%s" % by) by = sts.any(r, ICAL.bymonth) if by: w(";BYMONTH=%s" % by) w(CRLF) def doCalAddress(self, sts, who, propName, predName): w = self._w w(propName) #@@ hmm... are there others? for sym, paramName in ((ICAL.cn, "CN"), (ICAL.dir, "DIR"), (ICAL.cutype, "CUTYPE"), (ICAL.language, "LANGUAGE"), (ICAL.partstat, "PARTSTAT"), (ICAL.role, "ROLE"), (ICAL.rsvp, "RSVP"), (ICAL.sentBy, "SENT-BY"), ): v = sts.any(who, sym) if v: if sym is ICAL.dir: v = v.uriref() else: v = v.string.encode('utf-8') if ';' in v or ' ' in v or ':' in v: #@@hmm... what if " in v? w(";%s=\"%s\"" % (paramName, v)) else: w(";%s=%s" % (paramName, v)) address = str(sts.the(who, ICAL.calAddress, None)) # MAILTO seems to be capitalized in the iCalendar world. odd. # hmm... perhaps not in apple's world #if address.startswith("mailto:"): # address = "MAILTO:" + address[7:] w(":" + address + "\n") def componentOrder(a, b): return cmp(compKey(a), compKey(b)) def compKey(item): """extract a sort key from a component item >>> from myStore import formula, literal, symbol, existential >>> f=formula() >>> e1=symbol("http://example#e1") >>> w1=existential("t", f, None) >>> e2=symbol("http://example#e2") >>> w2=existential("t", f, None) >>> f.add(e1, ICAL.uid, literal("abcdef")) 1 >>> f.add(e1, ICAL.dtstart, w1) 1 >>> f.add(e2, ICAL.dtstart, w2) 1 >>> f.add(w1, ICAL.date, literal("2002-12-23")) 1 >>> f.add(w2, ICAL.dateTime, literal("2002-12-23T12:32:31Z")) 1 >>> compKey((f, e1, 'dummy', [])) ('abcdef', '2002-12-23') >>> compKey((f, e2, 'dummy', [])) (None, '2002-12-23T12:32:31Z') """ # " help emacs sts, sub, subName, subs = item uid = sts.any(sub, ICAL.uid) if uid: uid = str(uid) when = sts.any(sub, ICAL.dtstart) if when: whenV = sts.any(when, ICAL.date) if whenV: when = str(whenV) else: whenV = sts.any(when, ICAL.dateTime) if whenV: when = str(whenV) return (uid, when) def mkTEXT(val, fmla=None): # @@TODO: wrap at 75 cols try: text = val.string.encode('utf-8') except AttributeError: text = fmla.the(val, RDF.value).string.encode('utf-8') for c in ('\\', ';', ','): text = text.replace(c, "\\"+c) text = text.replace('\n', "\\n") return text def mkINTEGER(val): i = int(str(val)) return "%i" % i def mkFLOAT(val): n = float(str(val)) return "%f" % n def mkDATE(val): """ >>> mkDATE('2004-11-19') '20041119' """ return translate(str(val), maketrans("", ""), "-:") TZD = "http://www.w3.org/2002/12/cal/tzd/" def tzid(tzi): """convert timezones from RdfCalendar norms to iCalendar norms ASSUME we're using one of the 2002/12/cal timezones. @@ """ rel = uripath.refTo(TZD, tzi) short = uripath.splitFrag(rel)[0] return "/softwarestudio.org/Olson_20011030_5/" + short import sys, os from swap import uripath def usage(): print __doc__ def main(args): if not args[1:]: usage() sys.exit(1) c = CalWr(sys.stdout.write) if args[3:] and args[1] == '--floattz': tz = args[2] c.floatTZ = tz del args[1:3] addr = uripath.join("file:" + os.getcwd() + "/", args[1]) progress("loading...", addr) sts = load(addr) progress("exporting...") c.export(sts, addr) def _test(): import doctest doctest.testmod() def progress(*args): for i in args: sys.stderr.write(str(i)) sys.stderr.write("\n") def debug(*args): for i in args: sys.stderr.write(str(i)) sys.stderr.write("\n") if __name__ == '__main__': if '--test' in sys.argv: _test() else: main(sys.argv) # $Log: toIcal.py,v $ # Revision 2.41 2010年03月30日 20:29:21 timbl # Catchup after messing with maps and taxes # # Revision 2.40 2008年01月05日 21:42:37 timbl # qif directory was blocking qif ontology on website # # Revision 2.39 2007年02月20日 14:49:24 timbl # from swap.* import # # Revision 2.38 2006年10月09日 13:32:20 connolly # refine the --floattz hack # # Revision 2.37 2006年10月03日 05:31:23 connolly # for --floattz, add timezone component by reading from the web # # Revision 2.36 2006年10月03日 05:09:35 connolly # add --floattz option to override timezone of floating events # # Revision 2.35 2006年07月13日 23:04:10 connolly # fix name clobbering with more than one value of a list-typed property # # Revision 2.34 2006年07月06日 01:19:09 connolly # support rdf:value on text fields # # Revision 2.33 2005年11月10日 14:40:32 connolly # updated duration handling # # Revision 2.32 2005年09月05日 23:53:35 connolly # handle bymonthday # # Revision 2.31 2005年08月26日 15:21:35 connolly # undo borken commit # # Revision 2.29 2005年04月18日 14:33:25 connolly # try not capitalizing mailto: # # Revision 2.28 2005年04月18日 13:21:37 connolly # handle non-ascii chars in attendee names # # Revision 2.27 2005年03月30日 15:33:23 connolly # switched namespace name to stop abusing the old one # # Revision 2.26 2005年03月19日 14:10:46 connolly # COUNT param # timezones as datatypes # # Revision 2.25 2005年02月17日 23:34:37 connolly # each, not just any value, e.g. for EXDATE # # Revision 2.24 2005年02月17日 23:02:27 connolly # - sort components by uid, dtstart # - sort properties by name # - added --test arg to run doctest tests # - pychecker fixes: warn() not imported, # - got rid of wrapString() deadcode (though there's an @@ in mkTEXT) # # Revision 2.23 2004年11月13日 17:02:58 connolly # fixed punctuation of UNTIL; factored out mkDATE # added --test option for doctest style testing # # Revision 2.22 2004年11月13日 16:51:14 connolly # added UNTIL support in doRecur (IOU a test) # # Revision 2.21 2004年09月08日 15:46:05 connolly # - update to using timezones as properties # - kinda kludge converting timezone URIs to tzids # # Revision 2.20 2004年04月15日 22:39:46 connolly # integrated patch from SeanP: # - adds support for list of float/int/text (e.g. GEO) # - refactor doTEXT etc. as mkTEXT, doSIMPLE # # Revision 2.19 2004年04月09日 22:19:44 connolly # working on encoding issues in doTEXT. # not sure this is exactly the right fix. # # Revision 2.18 2004年03月30日 00:17:41 connolly # found bug in INTERVAL handling while porting to rdflib # # Revision 2.17 2004年03月13日 00:01:37 connolly # fixed the punctuation of RECUR values # # Revision 2.16 2004年03月10日 00:04:55 connolly # slightly nicer error message # # Revision 2.15 2004年03月09日 23:29:40 connolly # removed non-ascii chars from copyright blurb # reformatted changelog to 79chars # # Revision 2.14 2004年03月06日 20:39:40 timbl # added copyright blurb. # # Revision 2.13 2004年02月23日 16:50:54 connolly # schema-based rewrite. might have regressed a bit. # # Revision 2.8 2004年02月02日 16:46:50 connolly # more tweaks for status; fixed a progress message # # Revision 2.7 2004年01月31日 00:45:10 timbl # Add COMPLETED: # # Revision 2.6 2004年01月31日 00:11:06 connolly # todo status support; no test; blech # # Revision 2.5 2004年01月29日 21:28:13 timbl # Changed ONE newline to a CRLF, suspect many more should be changed. # iCal needs both # # Revision 2.4 2004年01月29日 21:09:16 timbl # Added DTSTART and UID to events. iCal needs DTSTART it seems. # Fixed VALUE=URI format. # # Revision 2.3 2004年01月29日 19:41:57 timbl # minor fixes # # Revision 2.2 2004年01月29日 15:20:05 connolly # - added some Vtodo support (@@owe tests; struggling with cal test harness) # - added uri property support (@@I18N bugs) # - updated to thing API changes (which turns out to be obsolete. oops) # - unexpected usage gives help text rather than backtrace # - a few more code review notes # # Revision 2.1 2003年08月28日 15:49:04 connolly # various code review notes that I want to discuss with ghuo # # Revision 2.0 2003年08月23日 08:40:40 ghuo # Added support for numerous new properties and parameters. # Added the VALARM component. Restructured the export # procedures. Fixed various output bugs. # # Revision 1.14 2003年06月13日 22:06:10 timbl # Fixed DATE-TIMEs to be always 15char and punctuation bug in some datetimes # # Revision 1.13 2003年06月03日 17:35:38 connolly # added duration support # # Revision 1.12 2003年04月16日 17:21:01 ryanlee # more rfc2445 fixes in textProp # # Revision 1.11 2003年03月24日 20:18:01 ryanlee # first pass at making toIcal understand non-ascii characters # # Revision 1.10 2003年03月14日 06:01:04 connolly # cleaned up DATE-TIME vs DATE stuff # # Revision 1.9 2003年03月14日 03:17:23 connolly # oops! forgot begin/end around vtimezone. # clearly I don't have any good way to test this code. # # Revision 1.8 2003年03月14日 03:12:35 connolly # update to 2002/12 namespace # export timezones, at least well enough for one case # export rrules well enough for one test case # update to each()/any() API # kill dead (commented out) Namespace class code # # Revision 1.7 2003年01月13日 19:48:23 timbl # Changed API, using thing.Namespace # # Revision 1.6 2002年12月12日 22:58:07 timbl # minor # # Revision 1.5 2002年09月22日 21:53:44 connolly # handle location, priority # # Revision 1.4 2002年08月28日 22:00:24 connolly # updated to diag interface # # Revision 1.3 2002年07月23日 23:09:31 connolly # grumble... evo is case-sensitive where the RFC says not to be # # Revision 1.2 2002年07月23日 21:44:16 connolly # - updated ICAL namespace pointer, case of RDF terms # - handle description, as well as summary # - be more clear about what properties we skip/don't handle # - don't complain about floating/local time # - pointer to earlier work # - used Namespace() trick # # Revision 1.1 2002年07月20日 17:14:39 timbl # DanC's with slight updates # # Revision 1.7 2002年06月27日 02:36:27 timbl # Extend datetimes to allways have 15 chars on output # # Revision 1.6 2002年05月29日 20:12:12 connolly # moved to formula-based API from store.every() # # Revision 1.5 2002年05月29日 19:53:42 connolly # tweak timezone handling # # Revision 1.4 2002年03月20日 16:48:42 connolly # more strict URI handling # # Revision 1.3 2002年01月11日 04:45:54 connolly # decided the main class is a writer