Attachment 'fullpatch.diff'
Download 1 diff -r 2dde35b02026 MoinMoin/parser/text_vcard.py
2 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
3 +++ b/MoinMoin/parser/text_vcard.py Thu Dec 20 20:04:49 2007 +0100
4 @@ -0,0 +1,46 @@
5 +# -*- coding: iso-8859-1 -*-
6 +"""
7 + MoinMoin - Parser for vCard
8 +
9 + @copyright: 2007 Rafael Weber
10 + @license: GNU GPL, see COPYING for details.
11 +"""
12 +from MoinMoin.support import vobject
13 +from MoinMoin.parser.text_x_hcard import main
14 +
15 +Dependencies = []
16 +
17 +class Parser:
18 + extensions = ['.vcf', '.vcard']
19 + Dependencies = []
20 +
21 + def __init__(self, raw, request, **kw):
22 + self.raw = raw
23 + self.request = request
24 + self.form = request.form
25 + self._ = request.getText
26 +
27 + def format(self, formatter):
28 + card = vobject.readOne(self.raw)
29 +
30 + # we must generate a list like `[(key, type, value)]` to use the
31 + # vcard data with the `main` function
32 + data = []
33 + for line in card.lines():
34 + if line.name == 'ADR':
35 + # the address must splitted into street, postal code etc.
36 + parts = line.value.split(';')
37 + sorted = {'post-office-box': parts[-7],
38 + 'extended-address': parts[-6],
39 + 'street-address': parts[-5],
40 + 'locality': parts[-4],
41 + 'region': parts[-3],
42 + 'postal-code': parts[-2],
43 + 'country-name': parts[-1]
44 + }
45 + for k,v in sorted.items():
46 + if v:
47 + data.append((k, line.singletonparams, v))
48 + else:
49 + data.append((line.name, line.singletonparams, line.value))
50 + main(data, self.request, formatter)
51 diff -r 2dde35b02026 MoinMoin/parser/text_x_hcard.py
52 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
53 +++ b/MoinMoin/parser/text_x_hcard.py Thu Dec 20 20:04:49 2007 +0100
54 @@ -0,0 +1,229 @@
55 +# -*- coding: iso-8859-1 -*-
56 +"""
57 + MoinMoin - Parser for hCard
58 +
59 + Note: Own extension (.hcf odr .hcard) is used.
60 +
61 + @copyright: 2007 Rafael Weber
62 + @license: GNU GPL, see COPYING for details.
63 +"""
64 +from MoinMoin.support.microformatparser import parse
65 +from StringIO import StringIO
66 +from time import strftime
67 +
68 +Dependencies = []
69 +
70 +order = (
71 + 'n',
72 + 'nickname',
73 + 'photo',
74 + 'logo',
75 + 'org',
76 + 'title',
77 + 'role',
78 + 'sound',
79 + 'bday',
80 + 'url',
81 + 'email',
82 + 'tel',
83 + 'post-office-box',
84 + 'extended-address',
85 + 'street-address',
86 + 'locality',
87 + 'region',
88 + 'postal-code',
89 + 'country-name',
90 + 'geo',
91 + 'tz',
92 + 'class',
93 + 'key',
94 + 'mailer',
95 + 'rev',
96 + 'uid',
97 + 'note'
98 +)
99 +
100 +names = {
101 + 'n': 'Name',
102 + 'bday': 'Birthday',
103 + 'tz': 'Timezone',
104 + 'org': 'Organisation',
105 + 'url': 'Homepage',
106 + 'street-address': 'Street Address',
107 + 'postal-code': 'Postal Code',
108 + 'tel': 'Telephone',
109 + 'country-name': 'Country',
110 + 'post-office-box': 'Post Office Box',
111 + 'extended-address': 'Extended Address',
112 + 'geo': 'Global Position',
113 + 'prodid': 'Product ID',
114 + 'rev': 'Revision',
115 + 'uid': 'UID'
116 +}
117 +
118 +
119 +def main(card, req, fmt):
120 + """
121 + The main function. Outputs the table in form of
122 + || key || type || value || .
123 + """
124 + def _get_types(x):
125 + if len(x) == 3:
126 + if isinstance(x[1], list):
127 + return ', '.join([type.capitalize() for type in x[1]])
128 + return x[1]
129 + return ''
130 +
131 + def nice_name(name):
132 + """
133 + Returns a nicer name for the keys.
134 + Some are defined in the dict and others just get capitalized.
135 + """
136 + try:
137 + return names[name.lower()]
138 + except KeyError:
139 + return name.capitalize()
140 +
141 + def nice_value(line):
142 + """
143 + Returns linked homepages, linked email addresses and stuff like
144 + that.
145 + """
146 + key = line[0].lower()
147 + value = line[-1]
148 + if key == 'sort-string' or key == 'sound' or key == 'fn':
149 + # hide it
150 + pass
151 + else:
152 + req.write(fmt.table_cell(1))
153 + if key == 'n':
154 + parts = value.split(';')
155 + output = []
156 + for part in parts:
157 + if ',' in part:
158 + output.append(' '.join(p for p in part.split(',')))
159 + else:
160 + output.append(part)
161 + req.write(' '.join(output))
162 + elif key == 'url':
163 + req.write(fmt.url(1, value, css='http'))
164 + req.write(value)
165 + req.write(fmt.url(0))
166 + elif key == 'email':
167 + req.write(fmt.url(1, 'mailto:%s' % value.\
168 + lstrip('mailto:'), css='mailto'))
169 + # avoid two "mailto:"
170 + req.write(value.lstrip('mailto:'))
171 + req.write(fmt.url(0))
172 + elif key == 'photo' or key == 'logo':
173 + req.write(fmt.url(1, value))
174 + req.write(fmt.image(value, width='100px', height='133px'))
175 + req.write(fmt.url(0))
176 + elif key == 'country-name':
177 + req.write(value.capitalize())
178 + elif key == 'uid':
179 + req.write(value) # without capitalize()
180 + elif key == 'rev' or key == 'bday':
181 + if len(value) >= 19 and value[4] == '-' and value[7] == \
182 + '-':
183 + year, month, day, = int(value[0:4]), \
184 + int(value[5:7]), int(value[8:10])
185 + hour, minute, second = int(value[11:13]), \
186 + int(value[14:16]), int(value[17:19])
187 + tz = value[20:]
188 + if tz:
189 + result = '%d-%d-%d %d:%d:%d %s' % (year, month, day,
190 + hour, minute, second, tz)
191 + else:
192 + result = '%d-%d-%d %d:%d:%d' % (year, month, day,
193 + hour, minute, second)
194 + else:
195 + # only day-precise
196 + year, month, day = int(value[0:4]), int(value[5:7]), \
197 + int(value[8:10])
198 + result = '%d-%d-%d' % (year, month, day)
199 + req.write(result)
200 +
201 + else:
202 + req.write(value.rstrip(';'))
203 + req.write(fmt.table_cell(0))
204 +
205 + req.write(fmt.table(1))
206 +
207 + # create head line
208 + req.write(fmt.table_row(1))
209 +
210 + req.write(fmt.table_cell(1))
211 + req.write('Key')
212 + req.write(fmt.table_cell(0))
213 +
214 + req.write(fmt.table_cell(1))
215 + req.write('Type')
216 + req.write(fmt.table_cell(0))
217 +
218 + req.write(fmt.table_cell(1))
219 + req.write('Value')
220 + req.write(fmt.table_cell(0))
221 +
222 + req.write(fmt.table_row(0))
223 +
224 + for o in order:
225 + for field in card:
226 + if field[0].lower() == o:
227 + req.write(fmt.table_row(1))
228 + # key cell
229 + req.write(fmt.table_cell(1))
230 + req.write(fmt.strong(1))
231 + req.write(nice_name(field[0]))
232 + req.write(fmt.strong(0))
233 + req.write(fmt.table_cell(0))
234 +
235 + # type cell
236 + req.write(fmt.table_cell(1))
237 + req.write(fmt.emphasis(1))
238 + req.write(_get_types(field))
239 + req.write(fmt.emphasis(0))
240 +
241 + # value cell
242 + nice_value(field)
243 +
244 + req.write(fmt.table_row(0))
245 + req.write(fmt.table(0))
246 +
247 +class Parser:
248 +
249 + extensions = ['.hcf', '.hcard']
250 + Dependencies = []
251 +
252 + def __init__(self, raw, request, **kw):
253 + self.raw = raw
254 + self.request = request
255 + self.form = request.form
256 + self._ = request.getText
257 +
258 + def format(self, formatter):
259 + tree = parse(StringIO(self.raw))
260 +
261 + def get_dict():
262 + card = []
263 + for t in tree[0][0][1]:
264 + if isinstance(t, tuple):
265 + # flat, but maybe list or something in value
266 + if isinstance(t[1], basestring):
267 + # already flat
268 + card.append((t[0],t[1]))
269 + elif isinstance(t[1], list):
270 + if len(t[1]) == 1:
271 + card.append((t[0], t[1][0][1]))
272 + elif t[1][0][0] == 'type':
273 + card.append((t[0], t[1][0][1], t[1][1][1]))
274 + elif t[1][1][0] == 'type':
275 + card.append((t[0], t[1][1][1], t[1][0][1]))
276 + else:
277 + for t_ in t[1]:
278 + card.append((t_[0], t_[1]))
279 + return card
280 +
281 + card = get_dict()
282 +
283 + main(card, self.request, formatter)
284 diff -r 2dde35b02026 MoinMoin/support/microformatparser.py
285 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
286 +++ b/MoinMoin/support/microformatparser.py Thu Dec 20 20:04:49 2007 +0100
287 @@ -0,0 +1,216 @@
288 +#!/usr/bin/env python
289 +#
290 +# Microformat parser hack
291 +# - My lame attempt to build a generic microformat parser engine
292 +# (C) Phil Dawes 2005
293 +# Distributed under a New BSD style license:
294 +# See: http://www.opensource.org/licenses/bsd-license.php
295 +#
296 +# Usage: python ./
297 +
298 +import sys
299 +import urlparse
300 +from HTMLParser import HTMLParser
301 +import re
302 +import urllib2
303 +
304 +class MicroformatSchema:
305 +
306 + def __init__(self,props,parentprops):
307 + self.props = props
308 + self.parentprops = parentprops
309 +
310 + def isValidProperty(self,prop):
311 + if prop in self.props + self.parentprops:
312 + return True
313 + return False
314 +
315 + def isParentProperty(self,prop):
316 + return prop in self.parentprops
317 +
318 +vcardprops = MicroformatSchema(['fn','family-name', 'given-name', 'additional-name', 'honorific-prefix', 'honorific-suffix', 'nickname', 'sort-string','url','email','type','tel','post-office-box', 'extended-address', 'street-address', 'locality', 'region', 'postal-code', 'country-name', 'label', 'latitude', 'longitude', 'tz', 'photo', 'logo', 'sound', 'bday','title', 'role','organization-name', 'organization-unit','category', 'note','class', 'key', 'mailer', 'uid', 'rev'],['n','email','adr','geo','org','tel'])
319 +
320 +veventprops = MicroformatSchema(["summary","url","dtstart","dtend","location"],[])
321 +
322 +SCHEMAS= {'vcard':vcardprops,'vevent':veventprops}
323 +
324 +class nodeitem:
325 + def __init__(self,id,tag,predicates,attrs,nested):
326 + self.tag = tag
327 + self.id = id
328 + self.predicates = predicates
329 + self.attrs = attrs
330 + self.nested = nested
331 +
332 + def __repr__(self):
333 + return "<nodeitem %s, %s, %s, %s, %s>"%(self.tag, self.id, self.predicates,
334 + self.attrs,self.nested)
335 +
336 +class MicroformatToStmts(HTMLParser):
337 + def __init__(self,url):
338 + self.url = url
339 + HTMLParser.__init__(self)
340 + self.nodestack = []
341 + self.nodemap = {}
342 + self.chars = ""
343 + self.tree = []
344 + self.treestack = []
345 +
346 + def _getattr(self,name,attrs):
347 + for attr in attrs:
348 + if name == attr[0]: return attr[1]
349 +
350 + def predicateIsAParent(self,pred):
351 + if SCHEMAS[self.currentCompoundFormat].isParentProperty(pred):
352 + return True
353 + return False
354 +
355 + def handle_starttag(self, elementtag, attrs):
356 + self.chars=""
357 + if self.currentlyInAMicroformat():
358 + try:
359 + preds = self._getattr("class",attrs).split()
360 + except AttributeError:
361 + self.nodestack.append(nodeitem(1,elementtag,None,attrs,False))
362 + return
363 +
364 + prevpreds = []
365 + #while 1:
366 + nested = False
367 + while 1:
368 + if prevpreds == preds:
369 + break
370 + prevpreds = preds
371 + if self.predicateIsAParent(preds[0]):
372 + self.openParentProperty(preds[0])
373 + nested = True
374 +
375 + if elementtag == "img":
376 + self.emitAttributeAsPropertyIfExists('src',attrs, preds)
377 + elif elementtag == "a":
378 + self.emitAttributeAsPropertyIfExists('href',attrs, preds)
379 + self.emitAttributeAsPropertyIfExists('title',attrs, preds)
380 + elif elementtag == "abbr":
381 + self.emitAttributeAsPropertyIfExists('title',attrs, preds)
382 +
383 + self.nodestack.append(nodeitem(1,elementtag,preds,attrs,nested))
384 +
385 + elif self.nodeStartsAMicroformat(attrs):
386 +
387 + classattrs = self._getattr('class',attrs).split()
388 + for classattr in classattrs:
389 + if classattr in SCHEMAS.keys():
390 + self.currentCompoundFormat = classattr
391 + break
392 + self.nodestack.append(nodeitem(1,elementtag,[self._getattr('class',attrs)],attrs,True))
393 + self.tree.append([])
394 + self.treestack = [self.tree[-1]] # opening tree stack frame
395 + self.openParentProperty(self.currentCompoundFormat)
396 +
397 + def openParentProperty(self,prop):
398 + self.treestack[-1].append((prop,[]))
399 + self.treestack.append(self.treestack[-1][-1][1])
400 +
401 + def currentlyInAMicroformat(self):
402 + return self.nodestack != []
403 +
404 + def nodeStartsAMicroformat(self, attrs):
405 + return self._getattr('class',attrs) in SCHEMAS.keys()
406 +
407 + def emitAttributeAsPropertyIfExists(self, attrname, attrs, preds):
408 + obj = self._getattr(attrname,attrs)
409 + if obj is not None:
410 + try:
411 + pred = preds[0]
412 + if SCHEMAS[self.currentCompoundFormat].isValidProperty(pred):
413 + if attrname in ("href","src"):
414 + obj = urlparse.urljoin(self.url,obj)
415 + obj = self.makeDatesParsable(pred,obj)
416 + self.addPropertyValueToOutput(pred,obj)
417 + del preds[0]
418 + except IndexError:
419 + pass
420 +
421 + def addPropertyValueToOutput(self,prop,val):
422 + self.treestack[-1].append((prop,val))
423 +
424 + def handle_endtag(self,tag):
425 + if self.currentlyInAMicroformat():
426 + while 1:
427 + try:
428 + item = self.nodestack.pop()
429 + except IndexError:
430 + return # no more elements
431 + if item.tag == tag:
432 + break # found it!
433 +
434 + # if there's still predicates, then output the text as object
435 + if item.predicates and item.predicates != [] and self.chars.strip() != "":
436 + #if item.tag == 'a':
437 + # print "ITEM:a",self.treestack
438 + preds = item.predicates
439 + self.treestack[-1].append((preds[0],self.chars))
440 + del preds[0]
441 + if item.nested == 1:
442 + self.treestack.pop()
443 + self.chars = ""
444 +
445 + # HTMLPARSER interface
446 + def handle_data(self,content):
447 + if self.hasPredicatesPending():
448 + content = content.strip()
449 + if content == "":
450 + return
451 + self.chars += content
452 +
453 + def hasPredicatesPending(self):
454 + for n in self.nodestack:
455 + if n.predicates != []:
456 + return 1
457 + return 0
458 +
459 + # hack to stop dates like '20051005' being interpreted as floats downstream
460 + def makeDatesParsable(self,p,o):
461 + if p in ["dtstart","dtend"]:
462 + try:
463 + float(o) # can it be interpreted as a float?
464 + o = "%s-%s-%s"%(o[:4],o[4:6],o[6:])
465 + except ValueError:
466 + pass
467 + return o
468 +
469 +
470 +def printTree(tree,tab=""):
471 + for p,v in tree:
472 + if isinstance(v,list):
473 + print tab + p
474 + printTree(v,tab+" ")
475 + else:
476 + print tab + str(p),":",v
477 +
478 +def printTreeStack(treestack,tab=""):
479 + for t in treestack:
480 + if isinstance(t,list):
481 + printTreeStack(t,tab+" ")
482 + else:
483 + print t
484 +
485 +def parse(f,url="http://dummyurl.com/"):
486 + m = MicroformatToStmts(url)
487 + s = f.read()
488 + m.feed(s)
489 + m.close()
490 +
491 + return m.tree
492 +
493 +
494 +if __name__ == "__main__":
495 + import urllib
496 + if len(sys.argv) == 1:
497 + print "Usage:",sys.argv[0],"<url>"
498 + sys.exit(0)
499 + else:
500 + for url in sys.argv[1:]:
501 + trees = parse(urllib.urlopen(url),url)
502 + for tree in trees:
503 + printTree(tree)
504 diff -r 2dde35b02026 MoinMoin/support/vobject/__init__.py
505 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
506 +++ b/MoinMoin/support/vobject/__init__.py Thu Dec 20 20:04:49 2007 +0100
507 @@ -0,0 +1,86 @@
508 +"""
509 +VObject Overview
510 +================
511 + vobject parses vCard or vCalendar files, returning a tree of Python objects.
512 + It also provids an API to create vCard or vCalendar data structures which
513 + can then be serialized.
514 +
515 + Parsing existing streams
516 + ------------------------
517 + Streams containing one or many L{Component<base.Component>}s can be
518 + parsed using L{readComponents<base.readComponents>}. As each Component
519 + is parsed, vobject will attempt to give it a L{Behavior<behavior.Behavior>}.
520 + If an appropriate Behavior is found, any base64, quoted-printable, or
521 + backslash escaped data will automatically be decoded. Dates and datetimes
522 + will be transformed to datetime.date or datetime.datetime instances.
523 + Components containing recurrence information will have a special rruleset
524 + attribute (a dateutil.rrule.rruleset instance).
525 +
526 + Validation
527 + ----------
528 + L{Behavior<behavior.Behavior>} classes implement validation for
529 + L{Component<base.Component>}s. To validate, an object must have all
530 + required children. There (TODO: will be) a toggle to raise an exception or
531 + just log unrecognized, non-experimental children and parameters.
532 +
533 + Creating objects programatically
534 + --------------------------------
535 + A L{Component<base.Component>} can be created from scratch. No encoding
536 + is necessary, serialization will encode data automatically. Factory
537 + functions (TODO: will be) available to create standard objects.
538 +
539 + Serializing objects
540 + -------------------
541 + Serialization:
542 + - Looks for missing required children that can be automatically generated,
543 + like a UID or a PRODID, and adds them
544 + - Encodes all values that can be automatically encoded
545 + - Checks to make sure the object is valid (unless this behavior is
546 + explicitly disabled)
547 + - Appends the serialized object to a buffer, or fills a new
548 + buffer and returns it
549 +
550 + Examples
551 + --------
552 +
553 + >>> import datetime
554 + >>> import dateutil.rrule as rrule
555 + >>> x = iCalendar()
556 + >>> x.add('vevent')
557 + <VEVENT| []>
558 + >>> x
559 + <VCALENDAR| [<VEVENT| []>]>
560 + >>> v = x.vevent
561 + >>> utc = icalendar.utc
562 + >>> v.add('dtstart').value = datetime.datetime(2004, 12, 15, 14, tzinfo = utc)
563 + >>> v
564 + <VEVENT| [<DTSTART{}2004-12-15 14:00:00+00:00>]>
565 + >>> x
566 + <VCALENDAR| [<VEVENT| [<DTSTART{}2004-12-15 14:00:00+00:00>]>]>
567 + >>> newrule = rrule.rruleset()
568 + >>> newrule.rrule(rrule.rrule(rrule.WEEKLY, count=2, dtstart=v.dtstart.value))
569 + >>> v.rruleset = newrule
570 + >>> list(v.rruleset)
571 + [datetime.datetime(2004, 12, 15, 14, 0, tzinfo=tzutc()), datetime.datetime(2004, 12, 22, 14, 0, tzinfo=tzutc())]
572 + >>> v.add('uid').value = "randomuid@MYHOSTNAME"
573 + >>> print x.serialize()
574 + BEGIN:VCALENDAR
575 + VERSION:2.0
576 + PRODID:-//PYVOBJECT//NONSGML Version 1//EN
577 + BEGIN:VEVENT
578 + UID:randomuid@MYHOSTNAME
579 + DTSTART:20041215T140000Z
580 + RRULE:FREQ=WEEKLY;COUNT=2
581 + END:VEVENT
582 + END:VCALENDAR
583 +
584 +"""
585 +
586 +import base, icalendar, vcard
587 +from base import readComponents, readOne, newFromBehavior
588 +
589 +def iCalendar():
590 + return newFromBehavior('vcalendar', '2.0')
591 +
592 +def vCard():
593 + return newFromBehavior('vcard', '3.0')
594 \ No newline at end of file
595 diff -r 2dde35b02026 MoinMoin/support/vobject/base.py
596 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
597 +++ b/MoinMoin/support/vobject/base.py Thu Dec 20 20:04:49 2007 +0100
598 @@ -0,0 +1,1072 @@
599 +"""vobject module for reading vCard and vCalendar files."""
600 +
601 +import copy
602 +import re
603 +import sys
604 +import logging
605 +import StringIO
606 +import string
607 +import exceptions
608 +import codecs
609 +
610 +#------------------------------------ Logging ----------------------------------
611 +logger = logging.getLogger(__name__)
612 +if not logging.getLogger().handlers:
613 + handler = logging.StreamHandler()
614 + formatter = logging.Formatter('%(name)s %(levelname)s %(message)s')
615 + handler.setFormatter(formatter)
616 + logger.addHandler(handler)
617 +logger.setLevel(logging.ERROR) # Log errors
618 +DEBUG = False # Don't waste time on debug calls
619 +#----------------------------------- Constants ---------------------------------
620 +CR = unichr(13)
621 +LF = unichr(10)
622 +CRLF = CR + LF
623 +SPACE = unichr(32)
624 +TAB = unichr(9)
625 +SPACEORTAB = SPACE + TAB
626 +#-------------------------------- Useful modules -------------------------------
627 +# use doctest, it kills two birds with one stone and docstrings often become
628 +# more readable to boot (see parseLine's docstring).
629 +# use logging, then when debugging we can just set our verbosity.
630 +# use epydoc syntax for documenting code, please document every class and non-
631 +# trivial method (see http://epydoc.sourceforge.net/epytext.html
632 +# and http://epydoc.sourceforge.net/fields.html). Also, please
633 +# follow http://www.python.org/peps/pep-0257.html for docstrings.
634 +#-------------------------------------------------------------------------------
635 +
636 +#--------------------------------- Main classes --------------------------------
637 +class VBase(object):
638 + """Base class for ContentLine and Component.
639 +
640 + @ivar behavior:
641 + The Behavior class associated with this object, which controls
642 + validation, transformations, and encoding.
643 + @ivar parentBehavior:
644 + The object's parent's behavior, or None if no behaviored parent exists.
645 + @ivar isNative:
646 + Boolean describing whether this component is a Native instance.
647 + @ivar group:
648 + An optional group prefix, should be used only to indicate sort order in
649 + vCards, according to RFC2426
650 + """
651 + def __init__(self, group=None, *args, **kwds):
652 + super(VBase, self).__init__(*args, **kwds)
653 + self.group = group
654 + self.behavior = None
655 + self.parentBehavior = None
656 + self.isNative = False
657 +
658 + def copy(self, copyit):
659 + self.group = copyit.group
660 + self.behavior = copyit.behavior
661 + self.parentBehavior = copyit.parentBehavior
662 + self.isNative = copyit.isNative
663 +
664 + def validate(self, *args, **kwds):
665 + """Call the behavior's validate method, or return True."""
666 + if self.behavior:
667 + return self.behavior.validate(self, *args, **kwds)
668 + else: return True
669 +
670 + def getChildren(self):
671 + """Return an iterable containing the contents of the object."""
672 + return []
673 +
674 + def clearBehavior(self, cascade=True):
675 + """Set behavior to None. Do for all descendants if cascading."""
676 + self.behavior=None
677 + if cascade: self.transformChildrenFromNative()
678 +
679 + def autoBehavior(self, cascade=False):
680 + """Set behavior if name is in self.parentBehavior.knownChildren.
681 +
682 + If cascade is True, unset behavior and parentBehavior for all
683 + descendants, then recalculate behavior and parentBehavior.
684 +
685 + """
686 + parentBehavior = self.parentBehavior
687 + if parentBehavior is not None:
688 + knownChildTup = parentBehavior.knownChildren.get(self.name, None)
689 + if knownChildTup is not None:
690 + behavior = getBehavior(self.name, knownChildTup[2])
691 + if behavior is not None:
692 + self.setBehavior(behavior, cascade)
693 + if isinstance(self, ContentLine) and self.encoded:
694 + self.behavior.decode(self)
695 + elif isinstance(self, ContentLine):
696 + self.behavior = parentBehavior.defaultBehavior
697 + if self.encoded and self.behavior:
698 + self.behavior.decode(self)
699 +
700 + def setBehavior(self, behavior, cascade=True):
701 + """Set behavior. If cascade is True, autoBehavior all descendants."""
702 + self.behavior=behavior
703 + if cascade:
704 + for obj in self.getChildren():
705 + obj.parentBehavior=behavior
706 + obj.autoBehavior(True)
707 +
708 + def transformToNative(self):
709 + """Transform this object into a custom VBase subclass.
710 +
711 + transformToNative should always return a representation of this object.
712 + It may do so by modifying self in place then returning self, or by
713 + creating a new object.
714 +
715 + """
716 + if self.isNative or not self.behavior or not self.behavior.hasNative:
717 + return self
718 + else:
719 + try:
720 + return self.behavior.transformToNative(self)
721 + except Exception, e:
722 + # wrap errors in transformation in a ParseError
723 + lineNumber = getattr(self, 'lineNumber', None)
724 + if isinstance(e, ParseError):
725 + if lineNumber is not None:
726 + e.lineNumber = lineNumber
727 + raise
728 + else:
729 + msg = "In transformToNative, unhandled exception: %s: %s"
730 + msg = msg % (sys.exc_info()[0], sys.exc_info()[1])
731 + new_error = ParseError(msg, lineNumber)
732 + raise ParseError, new_error, sys.exc_info()[2]
733 +
734 +
735 + def transformFromNative(self):
736 + """Return self transformed into a ContentLine or Component if needed.
737 +
738 + May have side effects. If it does, transformFromNative and
739 + transformToNative MUST have perfectly inverse side effects. Allowing
740 + such side effects is convenient for objects whose transformations only
741 + change a few attributes.
742 +
743 + Note that it isn't always possible for transformFromNative to be a
744 + perfect inverse of transformToNative, in such cases transformFromNative
745 + should return a new object, not self after modifications.
746 +
747 + """
748 + if self.isNative and self.behavior and self.behavior.hasNative:
749 + try:
750 + return self.behavior.transformFromNative(self)
751 + except Exception, e:
752 + # wrap errors in transformation in a NativeError
753 + lineNumber = getattr(self, 'lineNumber', None)
754 + if isinstance(e, NativeError):
755 + if lineNumber is not None:
756 + e.lineNumber = lineNumber
757 + raise
758 + else:
759 + msg = "In transformFromNative, unhandled exception: %s: %s"
760 + msg = msg % (sys.exc_info()[0], sys.exc_info()[1])
761 + new_error = NativeError(msg, lineNumber)
762 + raise NativeError, new_error, sys.exc_info()[2]
763 + else: return self
764 +
765 + def transformChildrenToNative(self):
766 + """Recursively replace children with their native representation."""
767 + pass
768 +
769 + def transformChildrenFromNative(self, clearBehavior=True):
770 + """Recursively transform native children to vanilla representations."""
771 + pass
772 +
773 + def serialize(self, buf=None, lineLength=75, validate=True, behavior=None):
774 + """Serialize to buf if it exists, otherwise return a string.
775 +
776 + Use self.behavior.serialize if behavior exists.
777 +
778 + """
779 + if not behavior:
780 + behavior = self.behavior
781 +
782 + if behavior:
783 + if DEBUG: logger.debug("serializing %s with behavior" % self.name)
784 + return behavior.serialize(self, buf, lineLength, validate)
785 + else:
786 + if DEBUG: logger.debug("serializing %s without behavior" % self.name)
787 + return defaultSerialize(self, buf, lineLength)
788 +
789 +def ascii(s):
790 + """Turn s into a printable string. Won't work for 8-bit ASCII."""
791 + return unicode(s).encode('ascii', 'replace')
792 +
793 +def toVName(name, stripNum = 0, upper = False):
794 + """
795 + Turn a Python name into an iCalendar style name, optionally uppercase and
796 + with characters stripped off.
797 + """
798 + if upper:
799 + name = name.upper()
800 + if stripNum != 0:
801 + name = name[:-stripNum]
802 + return name.replace('_', '-')
803 +
804 +class ContentLine(VBase):
805 + """Holds one content line for formats like vCard and vCalendar.
806 +
807 + For example::
808 + <SUMMARY{u'param1' : [u'val1'], u'param2' : [u'val2']}Bastille Day Party>
809 +
810 + @ivar name:
811 + The uppercased name of the contentline.
812 + @ivar params:
813 + A dictionary of parameters and associated lists of values (the list may
814 + be empty for empty parameters).
815 + @ivar value:
816 + The value of the contentline.
817 + @ivar singletonparams:
818 + A list of parameters for which it's unclear if the string represents the
819 + parameter name or the parameter value. In vCard 2.1, "The value string
820 + can be specified alone in those cases where the value is unambiguous".
821 + This is crazy, but we have to deal with it.
822 + @ivar encoded:
823 + A boolean describing whether the data in the content line is encoded.
824 + Generally, text read from a serialized vCard or vCalendar should be
825 + considered encoded. Data added programmatically should not be encoded.
826 + @ivar lineNumber:
827 + An optional line number associated with the contentline.
828 + """
829 + def __init__(self, name, params, value, group=None,
830 + encoded=False, isNative=False,
831 + lineNumber = None, *args, **kwds):
832 + """Take output from parseLine, convert params list to dictionary."""
833 + # group is used as a positional argument to match parseLine's return
834 + super(ContentLine, self).__init__(group, *args, **kwds)
835 + self.name = name.upper()
836 + self.value = value
837 + self.encoded = encoded
838 + self.params = {}
839 + self.singletonparams = []
840 + self.isNative = isNative
841 + self.lineNumber = lineNumber
842 + def updateTable(x):
843 + if len(x) == 1:
844 + self.singletonparams += x
845 + else:
846 + paramlist = self.params.setdefault(x[0].upper(), [])
847 + paramlist.extend(x[1:])
848 + map(updateTable, params)
849 + qp = False
850 + if 'ENCODING' in self.params:
851 + if 'QUOTED-PRINTABLE' in self.params['ENCODING']:
852 + qp = True
853 + self.params['ENCODING'].remove('QUOTED-PRINTABLE')
854 + if 0==len(self.params['ENCODING']):
855 + del self.params['ENCODING']
856 + if 'QUOTED-PRINTABLE' in self.singletonparams:
857 + qp = True
858 + self.singletonparams.remove('QUOTED-PRINTABLE')
859 + if qp:
860 + self.value = str(self.value).decode('quoted-printable')
861 +
862 + @classmethod
863 + def duplicate(clz, copyit):
864 + newcopy = clz('', {}, '')
865 + newcopy.copy(copyit)
866 + return newcopy
867 +
868 + def copy(self, copyit):
869 + super(ContentLine, self).copy(copyit)
870 + self.name = copyit.name
871 + self.value = copy.copy(copyit.value)
872 + self.encoded = self.encoded
873 + self.params = copy.copy(copyit.params)
874 + self.singletonparams = copy.copy(copyit.singletonparams)
875 + self.lineNumber = copyit.lineNumber
876 +
877 + def __eq__(self, other):
878 + try:
879 + return (self.name == other.name) and (self.params == other.params) and (self.value == other.value)
880 + except:
881 + return False
882 +
883 + def __getattr__(self, name):
884 + """Make params accessible via self.foo_param or self.foo_paramlist.
885 +
886 + Underscores, legal in python variable names, are converted to dashes,
887 + which are legal in IANA tokens.
888 +
889 + """
890 + try:
891 + if name.endswith('_param'):
892 + return self.params[toVName(name, 6, True)][0]
893 + elif name.endswith('_paramlist'):
894 + return self.params[toVName(name, 10, True)]
895 + else:
896 + raise exceptions.AttributeError, name
897 + except KeyError:
898 + raise exceptions.AttributeError, name
899 +
900 + def __setattr__(self, name, value):
901 + """Make params accessible via self.foo_param or self.foo_paramlist.
902 +
903 + Underscores, legal in python variable names, are converted to dashes,
904 + which are legal in IANA tokens.
905 +
906 + """
907 + if name.endswith('_param'):
908 + if type(value) == list:
909 + self.params[toVName(name, 6, True)] = value
910 + else:
911 + self.params[toVName(name, 6, True)] = [value]
912 + elif name.endswith('_paramlist'):
913 + if type(value) == list:
914 + self.params[toVName(name, 10, True)] = value
915 + else:
916 + raise VObjectError("Parameter list set to a non-list")
917 + else:
918 + prop = getattr(self.__class__, name, None)
919 + if isinstance(prop, property):
920 + prop.fset(self, value)
921 + else:
922 + object.__setattr__(self, name, value)
923 +
924 + def __delattr__(self, name):
925 + try:
926 + if name.endswith('_param'):
927 + del self.params[toVName(name, 6, True)]
928 + elif name.endswith('_paramlist'):
929 + del self.params[toVName(name, 10, True)]
930 + else:
931 + object.__delattr__(self, name)
932 + except KeyError:
933 + raise exceptions.AttributeError, name
934 +
935 + def valueRepr( self ):
936 + """transform the representation of the value according to the behavior,
937 + if any"""
938 + v = self.value
939 + if self.behavior:
940 + v = self.behavior.valueRepr( self )
941 + return ascii( v )
942 +
943 + def __str__(self):
944 + return "<"+ascii(self.name)+ascii(self.params)+self.valueRepr()+">"
945 +
946 + def __repr__(self):
947 + return self.__str__().replace('\n', '\\n')
948 +
949 + def prettyPrint(self, level = 0, tabwidth=3):
950 + pre = ' ' * level * tabwidth
951 + print pre, self.name + ":", self.valueRepr()
952 + if self.params:
953 + lineKeys= self.params.keys()
954 + print pre, "params for ", self.name +':'
955 + for aKey in lineKeys:
956 + print pre + ' ' * tabwidth, aKey, ascii(self.params[aKey])
957 +
958 +class Component(VBase):
959 + """A complex property that can contain multiple ContentLines.
960 +
961 + For our purposes, a component must start with a BEGIN:xxxx line and end with
962 + END:xxxx, or have a PROFILE:xxx line if a top-level component.
963 +
964 + @ivar contents:
965 + A dictionary of lists of Component or ContentLine instances. The keys
966 + are the lowercased names of child ContentLines or Components.
967 + Note that BEGIN and END ContentLines are not included in contents.
968 + @ivar name:
969 + Uppercase string used to represent this Component, i.e VCARD if the
970 + serialized object starts with BEGIN:VCARD.
971 + @ivar useBegin:
972 + A boolean flag determining whether BEGIN: and END: lines should
973 + be serialized.
974 +
975 + """
976 + def __init__(self, name=None, *args, **kwds):
977 + super(Component, self).__init__(*args, **kwds)
978 + self.contents = {}
979 + if name:
980 + self.name=name.upper()
981 + self.useBegin = True
982 + else:
983 + self.name = ''
984 + self.useBegin = False
985 +
986 + self.autoBehavior()
987 +
988 + @classmethod
989 + def duplicate(clz, copyit):
990 + newcopy = clz()
991 + newcopy.copy(copyit)
992 + return newcopy
993 +
994 + def copy(self, copyit):
995 + super(Component, self).copy(copyit)
996 +
997 + # deep copy of contents
998 + self.contents = {}
999 + for key, lvalue in copyit.contents.items():
1000 + newvalue = []
1001 + for value in lvalue:
1002 + newitem = value.duplicate(value)
1003 + newvalue.append(newitem)
1004 + self.contents[key] = newvalue
1005 +
1006 + self.name = copyit.name
1007 + self.useBegin = copyit.useBegin
1008 +
1009 + def setProfile(self, name):
1010 + """Assign a PROFILE to this unnamed component.
1011 +
1012 + Used by vCard, not by vCalendar.
1013 +
1014 + """
1015 + if self.name or self.useBegin:
1016 + if self.name == name: return
1017 + raise VObjectError("This component already has a PROFILE or uses BEGIN.")
1018 + self.name = name.upper()
1019 +
1020 + def __getattr__(self, name):
1021 + """For convenience, make self.contents directly accessible.
1022 +
1023 + Underscores, legal in python variable names, are converted to dashes,
1024 + which are legal in IANA tokens.
1025 +
1026 + """
1027 + try:
1028 + if name.endswith('_list'):
1029 + return self.contents[toVName(name, 5)]
1030 + else:
1031 + return self.contents[toVName(name)][0]
1032 + except KeyError:
1033 + raise exceptions.AttributeError, name
1034 +
1035 + normal_attributes = ['contents','name','behavior','parentBehavior','group']
1036 + def __setattr__(self, name, value):
1037 + """For convenience, make self.contents directly accessible.
1038 +
1039 + Underscores, legal in python variable names, are converted to dashes,
1040 + which are legal in IANA tokens.
1041 +
1042 + """
1043 + if name not in self.normal_attributes and name.lower()==name:
1044 + if type(value) == list:
1045 + if name.endswith('_list'):
1046 + name = name[:-5]
1047 + self.contents[toVName(name)] = value
1048 + elif name.endswith('_list'):
1049 + raise VObjectError("Component list set to a non-list")
1050 + else:
1051 + self.contents[toVName(name)] = [value]
1052 + else:
1053 + prop = getattr(self.__class__, name, None)
1054 + if isinstance(prop, property):
1055 + prop.fset(self, value)
1056 + else:
1057 + object.__setattr__(self, name, value)
1058 +
1059 + def __delattr__(self, name):
1060 + try:
1061 + if name not in self.normal_attributes and name.lower()==name:
1062 + if name.endswith('_list'):
1063 + del self.contents[toVName(name, 5)]
1064 + else:
1065 + del self.contents[toVName(name)]
1066 + else:
1067 + object.__delattr__(self, name)
1068 + except KeyError:
1069 + raise exceptions.AttributeError, name
1070 +
1071 + def getChildValue(self, childName, default = None, childNumber = 0):
1072 + """Return a child's value (the first, by default), or None."""
1073 + child = self.contents.get(toVName(childName))
1074 + if child is None:
1075 + return default
1076 + else:
1077 + return child[childNumber].value
1078 +
1079 + def add(self, objOrName, group = None):
1080 + """Add objOrName to contents, set behavior if it can be inferred.
1081 +
1082 + If objOrName is a string, create an empty component or line based on
1083 + behavior. If no behavior is found for the object, add a ContentLine.
1084 +
1085 + group is an optional prefix to the name of the object (see
1086 + RFC 2425).
1087 + """
1088 + if isinstance(objOrName, VBase):
1089 + obj = objOrName
1090 + if self.behavior:
1091 + obj.parentBehavior = self.behavior
1092 + obj.autoBehavior(True)
1093 + else:
1094 + name = objOrName.upper()
1095 + try:
1096 + id=self.behavior.knownChildren[name][2]
1097 + behavior = getBehavior(name, id)
1098 + if behavior.isComponent:
1099 + obj = Component(name)
1100 + else:
1101 + obj = ContentLine(name, [], '', group)
1102 + obj.parentBehavior = self.behavior
1103 + obj.behavior = behavior
1104 + obj = obj.transformToNative()
1105 + except (KeyError, AttributeError):
1106 + obj = ContentLine(objOrName, [], '', group)
1107 + if obj.behavior is None and self.behavior is not None:
1108 + if isinstance(obj, ContentLine):
1109 + obj.behavior = self.behavior.defaultBehavior
1110 + self.contents.setdefault(obj.name.lower(), []).append(obj)
1111 + return obj
1112 +
1113 + def remove(self, obj):
1114 + """Remove obj from contents."""
1115 + named = self.contents.get(obj.name.lower())
1116 + if named:
1117 + try:
1118 + named.remove(obj)
1119 + if len(named) == 0:
1120 + del self.contents[obj.name.lower()]
1121 + except ValueError:
1122 + pass;
1123 +
1124 + def getChildren(self):
1125 + """Return an iterable of all children."""
1126 + for objList in self.contents.values():
1127 + for obj in objList: yield obj
1128 +
1129 + def components(self):
1130 + """Return an iterable of all Component children."""
1131 + return (i for i in self.getChildren() if isinstance(i, Component))
1132 +
1133 + def lines(self):
1134 + """Return an iterable of all ContentLine children."""
1135 + return (i for i in self.getChildren() if isinstance(i, ContentLine))
1136 +
1137 + def sortChildKeys(self):
1138 + try:
1139 + first = [s for s in self.behavior.sortFirst if s in self.contents]
1140 + except:
1141 + first = []
1142 + return first + sorted(k for k in self.contents.keys() if k not in first)
1143 +
1144 + def getSortedChildren(self):
1145 + return [obj for k in self.sortChildKeys() for obj in self.contents[k]]
1146 +
1147 + def setBehaviorFromVersionLine(self, versionLine):
1148 + """Set behavior if one matches name, versionLine.value."""
1149 + v=getBehavior(self.name, versionLine.value)
1150 + if v: self.setBehavior(v)
1151 +
1152 + def transformChildrenToNative(self):
1153 + """Recursively replace children with their native representation."""
1154 + #sort to get dependency order right, like vtimezone before vevent
1155 + for childArray in (self.contents[k] for k in self.sortChildKeys()):
1156 + for i in xrange(len(childArray)):
1157 + childArray[i]=childArray[i].transformToNative()
1158 + childArray[i].transformChildrenToNative()
1159 +
1160 + def transformChildrenFromNative(self, clearBehavior=True):
1161 + """Recursively transform native children to vanilla representations."""
1162 + for childArray in self.contents.values():
1163 + for i in xrange(len(childArray)):
1164 + childArray[i]=childArray[i].transformFromNative()
1165 + childArray[i].transformChildrenFromNative(clearBehavior)
1166 + if clearBehavior:
1167 + childArray[i].behavior = None
1168 + childArray[i].parentBehavior = None
1169 +
1170 + def __str__(self):
1171 + if self.name:
1172 + return "<" + self.name + "| " + str(self.getSortedChildren()) + ">"
1173 + else:
1174 + return '<' + '*unnamed*' + '| ' + str(self.getSortedChildren()) + '>'
1175 +
1176 + def __repr__(self):
1177 + return self.__str__()
1178 +
1179 + def prettyPrint(self, level = 0, tabwidth=3):
1180 + pre = ' ' * level * tabwidth
1181 + print pre, self.name
1182 + if isinstance(self, Component):
1183 + for line in self.getChildren():
1184 + line.prettyPrint(level + 1, tabwidth)
1185 + print
1186 +
1187 +class VObjectError(Exception):
1188 + def __init__(self, message, lineNumber=None):
1189 + self.message = message
1190 + if lineNumber is not None:
1191 + self.lineNumber = lineNumber
1192 + def __str__(self):
1193 + if hasattr(self, 'lineNumber'):
1194 + return "At line %s: %s" % \
1195 + (self.lineNumber, self.message)
1196 + else:
1197 + return repr(self.message)
1198 +
1199 +class ParseError(VObjectError):
1200 + pass
1201 +
1202 +class ValidateError(VObjectError):
1203 + pass
1204 +
1205 +class NativeError(VObjectError):
1206 + pass
1207 +
1208 +#-------------------------- Parsing functions ----------------------------------
1209 +
1210 +# parseLine regular expressions
1211 +
1212 +patterns = {}
1213 +
1214 +patterns['name'] = '[a-zA-Z0-9\-]+'
1215 +patterns['safe_char'] = '[^";:,]'
1216 +patterns['qsafe_char'] = '[^"]'
1217 +
1218 +# the combined Python string replacement and regex syntax is a little confusing;
1219 +# remember that %(foobar)s is replaced with patterns['foobar'], so for instance
1220 +# param_value is any number of safe_chars or any number of qsaf_chars surrounded
1221 +# by double quotes.
1222 +
1223 +patterns['param_value'] = ' "%(qsafe_char)s * " | %(safe_char)s * ' % patterns
1224 +
1225 +
1226 +# get a tuple of two elements, one will be empty, the other will have the value
1227 +patterns['param_value_grouped'] = """
1228 +" ( %(qsafe_char)s * )" | ( %(safe_char)s + )
1229 +""" % patterns
1230 +
1231 +# get a parameter and its values, without any saved groups
1232 +patterns['param'] = r"""
1233 +; (?: %(name)s ) # parameter name
1234 +(?:
1235 + (?: = (?: %(param_value)s ) )? # 0 or more parameter values, multiple
1236 + (?: , (?: %(param_value)s ) )* # parameters are comma separated
1237 +)*
1238 +""" % patterns
1239 +
1240 +# get a parameter, saving groups for name and value (value still needs parsing)
1241 +patterns['params_grouped'] = r"""
1242 +; ( %(name)s )
1243 +
1244 +(?: =
1245 + (
1246 + (?: (?: %(param_value)s ) )? # 0 or more parameter values, multiple
1247 + (?: , (?: %(param_value)s ) )* # parameters are comma separated
1248 + )
1249 +)?
1250 +""" % patterns
1251 +
1252 +# get a full content line, break it up into group, name, parameters, and value
1253 +patterns['line'] = r"""
1254 +^ ((?P<group> %(name)s)\.)?(?P<name> %(name)s) # name group
1255 + (?P<params> (?: %(param)s )* ) # params group (may be empty)
1256 +: (?P<value> .* )$ # value group
1257 +""" % patterns
1258 +
1259 +' "%(qsafe_char)s*" | %(safe_char)s* '
1260 +
1261 +param_values_re = re.compile(patterns['param_value_grouped'], re.VERBOSE)
1262 +params_re = re.compile(patterns['params_grouped'], re.VERBOSE)
1263 +line_re = re.compile(patterns['line'], re.VERBOSE)
1264 +begin_re = re.compile('BEGIN', re.IGNORECASE)
1265 +
1266 +
1267 +def parseParams(string):
1268 + """
1269 + >>> parseParams(';ALTREP="http://www.wiz.org"')
1270 + [['ALTREP', 'http://www.wiz.org']]
1271 + >>> parseParams('')
1272 + []
1273 + >>> parseParams(';ALTREP="http://www.wiz.org;;",Blah,Foo;NEXT=Nope;BAR')
1274 + [['ALTREP', 'http://www.wiz.org;;', 'Blah', 'Foo'], ['NEXT', 'Nope'], ['BAR']]
1275 + """
1276 + all = params_re.findall(string)
1277 + allParameters = []
1278 + for tup in all:
1279 + paramList = [tup[0]] # tup looks like (name, valuesString)
1280 + for pair in param_values_re.findall(tup[1]):
1281 + # pair looks like ('', value) or (value, '')
1282 + if pair[0] != '':
1283 + paramList.append(pair[0])
1284 + else:
1285 + paramList.append(pair[1])
1286 + allParameters.append(paramList)
1287 + return allParameters
1288 +
1289 +
1290 +def parseLine(line, lineNumber = None):
1291 + """
1292 + >>> parseLine("BLAH:")
1293 + ('BLAH', [], '', None)
1294 + >>> parseLine("RDATE:VALUE=DATE:19970304,19970504,19970704,19970904")
1295 + ('RDATE', [], 'VALUE=DATE:19970304,19970504,19970704,19970904', None)
1296 + >>> parseLine('DESCRIPTION;ALTREP="http://www.wiz.org":The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA')
1297 + ('DESCRIPTION', [['ALTREP', 'http://www.wiz.org']], 'The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA', None)
1298 + >>> parseLine("EMAIL;PREF;INTERNET:john@nowhere.com")
1299 + ('EMAIL', [['PREF'], ['INTERNET']], 'john@nowhere.com', None)
1300 + >>> parseLine('EMAIL;TYPE="blah",hah;INTERNET="DIGI",DERIDOO:john@nowhere.com')
1301 + ('EMAIL', [['TYPE', 'blah', 'hah'], ['INTERNET', 'DIGI', 'DERIDOO']], 'john@nowhere.com', None)
1302 + >>> parseLine('item1.ADR;type=HOME;type=pref:;;Reeperbahn 116;Hamburg;;20359;')
1303 + ('ADR', [['type', 'HOME'], ['type', 'pref']], ';;Reeperbahn 116;Hamburg;;20359;', 'item1')
1304 + >>> parseLine(":")
1305 + Traceback (most recent call last):
1306 + ...
1307 + ParseError: 'Failed to parse line: :'
1308 + """
1309 +
1310 + match = line_re.match(line)
1311 + if match is None:
1312 + raise ParseError("Failed to parse line: %s" % line, lineNumber)
1313 + return (match.group('name'),
1314 + parseParams(match.group('params')),
1315 + match.group('value'), match.group('group'))
1316 +
1317 +# logical line regular expressions
1318 +
1319 +patterns['lineend'] = r'(?:\r\n|\r|\n|$)'
1320 +patterns['wrap'] = r'%(lineend)s [\t ]' % patterns
1321 +patterns['logicallines'] = r"""
1322 +(
1323 + (?: [^\r\n] | %(wrap)s )*
1324 + %(lineend)s
1325 +)
1326 +""" % patterns
1327 +
1328 +patterns['wraporend'] = r'(%(wrap)s | %(lineend)s )' % patterns
1329 +
1330 +wrap_re = re.compile(patterns['wraporend'], re.VERBOSE)
1331 +logical_lines_re = re.compile(patterns['logicallines'], re.VERBOSE)
1332 +
1333 +testLines="""
1334 +Line 0 text
1335 + , Line 0 continued.
1336 +Line 1;encoding=quoted-printable:this is an evil=
1337 + evil=
1338 + format.
1339 +Line 2 is a new line, it does not start with whitespace.
1340 +"""
1341 +
1342 +def getLogicalLines(fp, allowQP=True, findBegin=False):
1343 + """Iterate through a stream, yielding one logical line at a time.
1344 +
1345 + Because many applications still use vCard 2.1, we have to deal with the
1346 + quoted-printable encoding for long lines, as well as the vCard 3.0 and
1347 + vCalendar line folding technique, a whitespace character at the start
1348 + of the line.
1349 +
1350 + Quoted-printable data will be decoded in the Behavior decoding phase.
1351 +
1352 + >>> import StringIO
1353 + >>> f=StringIO.StringIO(testLines)
1354 + >>> for n, l in enumerate(getLogicalLines(f)):
1355 + ... print "Line %s: %s" % (n, l[0])
1356 + ...
1357 + Line 0: Line 0 text, Line 0 continued.
1358 + Line 1: Line 1;encoding=quoted-printable:this is an evil=
1359 + evil=
1360 + format.
1361 + Line 2: Line 2 is a new line, it does not start with whitespace.
1362 +
1363 + """
1364 + if not allowQP:
1365 + bytes = fp.read(-1)
1366 + if len(bytes) > 0:
1367 + if type(bytes[0]) == unicode:
1368 + val = bytes
1369 + elif not findBegin:
1370 + val = bytes.decode('utf-8')
1371 + else:
1372 + for encoding in 'utf-8', 'utf-16-LE', 'utf-16-BE', 'iso-8859-1':
1373 + try:
1374 + val = bytes.decode(encoding)
1375 + if begin_re.search(val) is not None:
1376 + break
1377 + except UnicodeDecodeError:
1378 + pass
1379 + else:
1380 + raise ParseError, 'Could not find BEGIN when trying to determine encoding'
1381 + else:
1382 + val = bytes
1383 +
1384 + # strip off any UTF8 BOMs which Python's UTF8 decoder leaves
1385 +
1386 + val = val.lstrip( unicode( codecs.BOM_UTF8, "utf8" ) )
1387 +
1388 + lineNumber = 1
1389 + for match in logical_lines_re.finditer(val):
1390 + line, n = wrap_re.subn('', match.group())
1391 + if line != '':
1392 + yield line, lineNumber
1393 + lineNumber += n
1394 +
1395 + else:
1396 + quotedPrintable=False
1397 + newbuffer = StringIO.StringIO
1398 + logicalLine = newbuffer()
1399 + lineNumber = 0
1400 + lineStartNumber = 0
1401 + while True:
1402 + line = fp.readline()
1403 + if line == '':
1404 + break
1405 + else:
1406 + line = line.rstrip(CRLF)
1407 + lineNumber += 1
1408 + if line.rstrip() == '':
1409 + if logicalLine.pos > 0:
1410 + yield logicalLine.getvalue(), lineStartNumber
1411 + lineStartNumber = lineNumber
1412 + logicalLine = newbuffer()
1413 + quotedPrintable=False
1414 + continue
1415 +
1416 + if quotedPrintable and allowQP:
1417 + logicalLine.write('\n')
1418 + logicalLine.write(line)
1419 + quotedPrintable=False
1420 + elif line[0] in SPACEORTAB:
1421 + logicalLine.write(line[1:])
1422 + elif logicalLine.pos > 0:
1423 + yield logicalLine.getvalue(), lineStartNumber
1424 + lineStartNumber = lineNumber
1425 + logicalLine = newbuffer()
1426 + logicalLine.write(line)
1427 + else:
1428 + logicalLine = newbuffer()
1429 + logicalLine.write(line)
1430 +
1431 + # hack to deal with the fact that vCard 2.1 allows parameters to be
1432 + # encoded without a parameter name. False positives are unlikely, but
1433 + # possible.
1434 + val = logicalLine.getvalue()
1435 + if val[-1]=='=' and val.lower().find('quoted-printable') >= 0:
1436 + quotedPrintable=True
1437 +
1438 + if logicalLine.pos > 0:
1439 + yield logicalLine.getvalue(), lineStartNumber
1440 +
1441 +
1442 +def textLineToContentLine(text, n=None):
1443 + return ContentLine(*parseLine(text, n), **{'encoded':True, 'lineNumber' : n})
1444 +
1445 +
1446 +def dquoteEscape(param):
1447 + """Return param, or "param" if ',' or ';' or ':' is in param."""
1448 + if param.find('"') >= 0:
1449 + raise VObjectError("Double quotes aren't allowed in parameter values.")
1450 + for char in ',;:':
1451 + if param.find(char) >= 0:
1452 + return '"'+ param + '"'
1453 + return param
1454 +
1455 +def foldOneLine(outbuf, input, lineLength = 75):
1456 + if isinstance(input, basestring): input = StringIO.StringIO(input)
1457 + input.seek(0)
1458 + outbuf.write(input.read(lineLength) + CRLF)
1459 + brokenline = input.read(lineLength - 1)
1460 + while brokenline:
1461 + outbuf.write(' ' + brokenline + CRLF)
1462 + brokenline = input.read(lineLength - 1)
1463 +
1464 +def defaultSerialize(obj, buf, lineLength):
1465 + """Encode and fold obj and its children, write to buf or return a string."""
1466 +
1467 + outbuf = buf or StringIO.StringIO()
1468 +
1469 + if isinstance(obj, Component):
1470 + if obj.group is None:
1471 + groupString = ''
1472 + else:
1473 + groupString = obj.group + '.'
1474 + if obj.useBegin:
1475 + foldOneLine(outbuf, groupString + u"BEGIN:" + obj.name, lineLength)
1476 + for child in obj.getSortedChildren():
1477 + #validate is recursive, we only need to validate once
1478 + child.serialize(outbuf, lineLength, validate=False)
1479 + if obj.useBegin:
1480 + foldOneLine(outbuf, groupString + u"END:" + obj.name, lineLength)
1481 + if DEBUG: logger.debug("Finished %s" % obj.name.upper())
1482 +
1483 + elif isinstance(obj, ContentLine):
1484 + startedEncoded = obj.encoded
1485 + if obj.behavior and not startedEncoded: obj.behavior.encode(obj)
1486 + s=StringIO.StringIO() #unfolded buffer
1487 + if obj.group is not None:
1488 + s.write(obj.group + '.')
1489 + if DEBUG: logger.debug("Serializing line" + str(obj))
1490 + s.write(obj.name.upper())
1491 + for key, paramvals in obj.params.iteritems():
1492 + s.write(';' + key + '=' + ','.join(map(dquoteEscape, paramvals)))
1493 + s.write(':' + obj.value)
1494 + if obj.behavior and not startedEncoded: obj.behavior.decode(obj)
1495 + foldOneLine(outbuf, s, lineLength)
1496 + if DEBUG: logger.debug("Finished %s line" % obj.name.upper())
1497 +
1498 + return buf or outbuf.getvalue()
1499 +
1500 +
1501 +testVCalendar="""
1502 +BEGIN:VCALENDAR
1503 +BEGIN:VEVENT
1504 +SUMMARY;blah=hi!:Bastille Day Party
1505 +END:VEVENT
1506 +END:VCALENDAR"""
1507 +
1508 +class Stack:
1509 + def __init__(self):
1510 + self.stack = []
1511 + def __len__(self):
1512 + return len(self.stack)
1513 + def top(self):
1514 + if len(self) == 0: return None
1515 + else: return self.stack[-1]
1516 + def topName(self):
1517 + if len(self) == 0: return None
1518 + else: return self.stack[-1].name
1519 + def modifyTop(self, item):
1520 + top = self.top()
1521 + if top:
1522 + top.add(item)
1523 + else:
1524 + new = Component()
1525 + self.push(new)
1526 + new.add(item) #add sets behavior for item and children
1527 + def push(self, obj): self.stack.append(obj)
1528 + def pop(self): return self.stack.pop()
1529 +
1530 +
1531 +def readComponents(streamOrString, validate=False, transform=True,
1532 + findBegin=True, ignoreUnreadable=False):
1533 + """Generate one Component at a time from a stream.
1534 +
1535 + >>> import StringIO
1536 + >>> f = StringIO.StringIO(testVCalendar)
1537 + >>> cal=readComponents(f).next()
1538 + >>> cal
1539 + <VCALENDAR| [<VEVENT| [<SUMMARY{u'BLAH': [u'hi!']}Bastille Day Party>]>]>
1540 + >>> cal.vevent.summary
1541 + <SUMMARY{u'BLAH': [u'hi!']}Bastille Day Party>
1542 +
1543 + """
1544 + if isinstance(streamOrString, basestring):
1545 + stream = StringIO.StringIO(streamOrString)
1546 + else:
1547 + stream = streamOrString
1548 +
1549 + try:
1550 + stack = Stack()
1551 + versionLine = None
1552 + n = 0
1553 + for line, n in getLogicalLines(stream, False, findBegin):
1554 + if ignoreUnreadable:
1555 + try:
1556 + vline = textLineToContentLine(line, n)
1557 + except VObjectError, e:
1558 + if e.lineNumber is not None:
1559 + msg = "Skipped line %(lineNumber)s, message: %(msg)s"
1560 + else:
1561 + msg = "Skipped a line, message: %(msg)s"
1562 + logger.error(msg % {'lineNumber' : e.lineNumber,
1563 + 'msg' : e.message})
1564 + continue
1565 + else:
1566 + vline = textLineToContentLine(line, n)
1567 + if vline.name == "VERSION":
1568 + versionLine = vline
1569 + stack.modifyTop(vline)
1570 + elif vline.name == "BEGIN":
1571 + stack.push(Component(vline.value, group=vline.group))
1572 + elif vline.name == "PROFILE":
1573 + if not stack.top(): stack.push(Component())
1574 + stack.top().setProfile(vline.value)
1575 + elif vline.name == "END":
1576 + if len(stack) == 0:
1577 + err = "Attempted to end the %s component, \
1578 + but it was never opened" % vline.value
1579 + raise ParseError(err, n)
1580 + if vline.value.upper() == stack.topName(): #START matches END
1581 + if len(stack) == 1:
1582 + component=stack.pop()
1583 + if versionLine is not None:
1584 + component.setBehaviorFromVersionLine(versionLine)
1585 + if validate: component.validate(raiseException=True)
1586 + if transform: component.transformChildrenToNative()
1587 + yield component #EXIT POINT
1588 + else: stack.modifyTop(stack.pop())
1589 + else:
1590 + err = "%s component wasn't closed"
1591 + raise ParseError(err % stack.topName(), n)
1592 + else: stack.modifyTop(vline) #not a START or END line
1593 + if stack.top():
1594 + if stack.topName() is None:
1595 + logger.warning("Top level component was never named")
1596 + elif stack.top().useBegin:
1597 + raise ParseError("Component %s was never closed" % (stack.topName()), n)
1598 + yield stack.pop()
1599 +
1600 + except ParseError, e:
1601 + e.input = streamOrString
1602 + raise
1603 +
1604 +
1605 +def readOne(stream, validate=False, transform=True, findBegin=True,
1606 + ignoreUnreadable=False):
1607 + """Return the first component from stream."""
1608 + return readComponents(stream, validate, transform, findBegin,
1609 + ignoreUnreadable).next()
1610 +
1611 +#--------------------------- version registry ----------------------------------
1612 +__behaviorRegistry={}
1613 +
1614 +def registerBehavior(behavior, name=None, default=False, id=None):
1615 + """Register the given behavior.
1616 +
1617 + If default is True (or if this is the first version registered with this
1618 + name), the version will be the default if no id is given.
1619 +
1620 + """
1621 + if not name: name=behavior.name.upper()
1622 + if id is None: id=behavior.versionString
1623 + if name in __behaviorRegistry:
1624 + if default:
1625 + __behaviorRegistry[name].insert(0, (id, behavior))
1626 + else:
1627 + __behaviorRegistry[name].append((id, behavior))
1628 + else:
1629 + __behaviorRegistry[name]=[(id, behavior)]
1630 +
1631 +def getBehavior(name, id=None):
1632 + """Return a matching behavior if it exists, or None.
1633 +
1634 + If id is None, return the default for name.
1635 +
1636 + """
1637 + name=name.upper()
1638 + if name in __behaviorRegistry:
1639 + if id:
1640 + for n, behavior in __behaviorRegistry[name]:
1641 + if n==id:
1642 + return behavior
1643 + else:
1644 + return __behaviorRegistry[name][0][1]
1645 + return None
1646 +
1647 +def newFromBehavior(name, id=None):
1648 + """Given a name, return a behaviored ContentLine or Component."""
1649 + name = name.upper()
1650 + behavior = getBehavior(name, id)
1651 + if behavior is None:
1652 + raise VObjectError("No behavior found named %s" % name)
1653 + if behavior.isComponent:
1654 + obj = Component(name)
1655 + else:
1656 + obj = ContentLine(name, [], '')
1657 + obj.behavior = behavior
1658 + obj.isNative = True
1659 + return obj
1660 +
1661 +
1662 +#--------------------------- Helper function -----------------------------------
1663 +def backslashEscape(s):
1664 + s=s.replace("\\","\\\\").replace(";","\;").replace(",","\,")
1665 + return s.replace("\r\n", "\\n").replace("\n","\\n").replace("\r","\\n")
1666 +
1667 +#------------------- Testing and running functions -----------------------------
1668 +if __name__ == '__main__':
1669 + import tests
1670 + tests._test()
1671 diff -r 2dde35b02026 MoinMoin/support/vobject/behavior.py
1672 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1673 +++ b/MoinMoin/support/vobject/behavior.py Thu Dec 20 20:04:49 2007 +0100
1674 @@ -0,0 +1,164 @@
1675 +"""Behavior (validation, encoding, and transformations) for vobjects."""
1676 +
1677 +import base
1678 +
1679 +#------------------------ Abstract class for behavior --------------------------
1680 +class Behavior(object):
1681 + """Abstract class to describe vobject options, requirements and encodings.
1682 +
1683 + Behaviors are used for root components like VCALENDAR, for subcomponents
1684 + like VEVENT, and for individual lines in components.
1685 +
1686 + Behavior subclasses are not meant to be instantiated, all methods should
1687 + be classmethods.
1688 +
1689 + @cvar name:
1690 + The uppercase name of the object described by the class, or a generic
1691 + name if the class defines behavior for many objects.
1692 + @cvar description:
1693 + A brief excerpt from the RFC explaining the function of the component or
1694 + line.
1695 + @cvar versionString:
1696 + The string associated with the component, for instance, 2.0 if there's a
1697 + line like VERSION:2.0, an empty string otherwise.
1698 + @cvar knownChildren:
1699 + A dictionary with uppercased component/property names as keys and a
1700 + tuple (min, max, id) as value, where id is the id used by
1701 + L{registerBehavior}, min and max are the limits on how many of this child
1702 + must occur. None is used to denote no max or no id.
1703 + @cvar quotedPrintable:
1704 + A boolean describing whether the object should be encoded and decoded
1705 + using quoted printable line folding and character escaping.
1706 + @cvar defaultBehavior:
1707 + Behavior to apply to ContentLine children when no behavior is found.
1708 + @cvar hasNative:
1709 + A boolean describing whether the object can be transformed into a more
1710 + Pythonic object.
1711 + @cvar isComponent:
1712 + A boolean, True if the object should be a Component.
1713 + @cvar sortFirst:
1714 + The lower-case list of children which should come first when sorting.
1715 + @cvar allowGroup:
1716 + Whether or not vCard style group prefixes are allowed.
1717 + """
1718 + name=''
1719 + description=''
1720 + versionString=''
1721 + knownChildren = {}
1722 + quotedPrintable = False
1723 + defaultBehavior = None
1724 + hasNative= False
1725 + isComponent = False
1726 + allowGroup = False
1727 + forceUTC = False
1728 + sortFirst = []
1729 +
1730 + def __init__(self):
1731 + err="Behavior subclasses are not meant to be instantiated"
1732 + raise base.VObjectError(err)
1733 +
1734 + @classmethod
1735 + def validate(cls, obj, raiseException=False, complainUnrecognized=False):
1736 + """Check if the object satisfies this behavior's requirements.
1737 +
1738 + @param obj:
1739 + The L{ContentLine<base.ContentLine>} or
1740 + L{Component<base.Component>} to be validated.
1741 + @param raiseException:
1742 + If True, raise a L{base.ValidateError} on validation failure.
1743 + Otherwise return a boolean.
1744 + @param complainUnrecognized:
1745 + If True, fail to validate if an uncrecognized parameter or child is
1746 + found. Otherwise log the lack of recognition.
1747 +
1748 + """
1749 + if not cls.allowGroup and obj.group is not None:
1750 + err = str(obj) + " has a group, but this object doesn't support groups"
1751 + raise base.VObjectError(err)
1752 + if isinstance(obj, base.ContentLine):
1753 + return cls.lineValidate(obj, raiseException, complainUnrecognized)
1754 + elif isinstance(obj, base.Component):
1755 + count = {}
1756 + for child in obj.getChildren():
1757 + if not child.validate(raiseException, complainUnrecognized):
1758 + return False
1759 + name=child.name.upper()
1760 + count[name] = count.get(name, 0) + 1
1761 + for key, val in cls.knownChildren.iteritems():
1762 + if count.get(key,0) < val[0]:
1763 + if raiseException:
1764 + m = "%s components must contain at least %i %s"
1765 + raise base.ValidateError(m % (cls.name, val[0], key))
1766 + return False
1767 + if val[1] and count.get(key,0) > val[1]:
1768 + if raiseException:
1769 + m = "%s components cannot contain more than %i %s"
1770 + raise base.ValidateError(m % (cls.name, val[1], key))
1771 + return False
1772 + return True
1773 + else:
1774 + err = str(obj) + " is not a Component or Contentline"
1775 + raise base.VObjectError(err)
1776 +
1777 + @classmethod
1778 + def lineValidate(cls, line, raiseException, complainUnrecognized):
1779 + """Examine a line's parameters and values, return True if valid."""
1780 + return True
1781 +
1782 + @classmethod
1783 + def decode(cls, line):
1784 + if line.encoded: line.encoded=0
1785 +
1786 + @classmethod
1787 + def encode(cls, line):
1788 + if not line.encoded: line.encoded=1
1789 +
1790 + @classmethod
1791 + def transformToNative(cls, obj):
1792 + """Turn a ContentLine or Component into a Python-native representation.
1793 +
1794 + If appropriate, turn dates or datetime strings into Python objects.
1795 + Components containing VTIMEZONEs turn into VtimezoneComponents.
1796 +
1797 + """
1798 + return obj
1799 +
1800 + @classmethod
1801 + def transformFromNative(cls, obj):
1802 + """Inverse of transformToNative."""
1803 + raise base.NativeError("No transformFromNative defined")
1804 +
1805 + @classmethod
1806 + def generateImplicitParameters(cls, obj):
1807 + """Generate any required information that don't yet exist."""
1808 + pass
1809 +
1810 + @classmethod
1811 + def serialize(cls, obj, buf, lineLength, validate=True):
1812 + """Set implicit parameters, do encoding, return unicode string.
1813 +
1814 + If validate is True, raise VObjectError if the line doesn't validate
1815 + after implicit parameters are generated.
1816 +
1817 + Default is to call base.defaultSerialize.
1818 +
1819 + """
1820 +
1821 + cls.generateImplicitParameters(obj)
1822 + if validate: cls.validate(obj, raiseException=True)
1823 +
1824 + if obj.isNative:
1825 + transformed = obj.transformFromNative()
1826 + undoTransform = True
1827 + else:
1828 + transformed = obj
1829 + undoTransform = False
1830 +
1831 + out = base.defaultSerialize(transformed, buf, lineLength)
1832 + if undoTransform: obj.transformToNative()
1833 + return out
1834 +
1835 + @classmethod
1836 + def valueRepr( cls, line ):
1837 + """return the representation of the given content line value"""
1838 + return line.value
1839 \ No newline at end of file
1840 diff -r 2dde35b02026 MoinMoin/support/vobject/hcalendar.py
1841 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1842 +++ b/MoinMoin/support/vobject/hcalendar.py Thu Dec 20 20:04:49 2007 +0100
1843 @@ -0,0 +1,125 @@
1844 +"""
1845 +hCalendar: A microformat for serializing iCalendar data
1846 + (http://microformats.org/wiki/hcalendar)
1847 +
1848 +Here is a sample event in an iCalendar:
1849 +
1850 +BEGIN:VCALENDAR
1851 +PRODID:-//XYZproduct//EN
1852 +VERSION:2.0
1853 +BEGIN:VEVENT
1854 +URL:http://www.web2con.com/
1855 +DTSTART:20051005
1856 +DTEND:20051008
1857 +SUMMARY:Web 2.0 Conference
1858 +LOCATION:Argent Hotel\, San Francisco\, CA
1859 +END:VEVENT
1860 +END:VCALENDAR
1861 +
1862 +and an equivalent event in hCalendar format with various elements optimized appropriately.
1863 +
1864 +<span class="vevent">
1865 + <a class="url" href="http://www.web2con.com/">
1866 + <span class="summary">Web 2.0 Conference</span>:
1867 + <abbr class="dtstart" title="2005-10-05">October 5</abbr>-
1868 + <abbr class="dtend" title="2005-10-08">7</abbr>,
1869 + at the <span class="location">Argent Hotel, San Francisco, CA</span>
1870 + </a>
1871 +</span>
1872 +"""
1873 +
1874 +from base import foldOneLine, CRLF, registerBehavior
1875 +from icalendar import VCalendar2_0
1876 +from datetime import date, datetime, timedelta
1877 +import StringIO
1878 +
1879 +class HCalendar(VCalendar2_0):
1880 + name = 'HCALENDAR'
1881 +
1882 + @classmethod
1883 + def serialize(cls, obj, buf=None, lineLength=None, validate=True):
1884 + """
1885 + Serialize iCalendar to HTML using the hCalendar microformat (http://microformats.org/wiki/hcalendar)
1886 + """
1887 +
1888 + outbuf = buf or StringIO.StringIO()
1889 + level = 0 # holds current indentation level
1890 + tabwidth = 3
1891 +
1892 + def indent():
1893 + return ' ' * level * tabwidth
1894 +
1895 + def out(s):
1896 + outbuf.write(indent())
1897 + outbuf.write(s)
1898 +
1899 + # not serializing optional vcalendar wrapper
1900 +
1901 + vevents = obj.vevent_list
1902 +
1903 + for event in vevents:
1904 + out('<span class="vevent">' + CRLF)
1905 + level += 1
1906 +
1907 + # URL
1908 + url = event.getChildValue("url")
1909 + if url:
1910 + out('<a class="url" href="' + url + '">' + CRLF)
1911 + level += 1
1912 + # SUMMARY
1913 + summary = event.getChildValue("summary")
1914 + if summary:
1915 + out('<span class="summary">' + summary + '</span>:' + CRLF)
1916 +
1917 + # DTSTART
1918 + dtstart = event.getChildValue("dtstart")
1919 + if dtstart:
1920 + if type(dtstart) == date:
1921 + timeformat = "%A, %B %e"
1922 + machine = "%Y%m%d"
1923 + elif type(dtstart) == datetime:
1924 + timeformat = "%A, %B %e, %H:%M"
1925 + machine = "%Y%m%dT%H%M%S%z"
1926 +
1927 + #TODO: Handle non-datetime formats?
1928 + #TODO: Spec says we should handle when dtstart isn't included
1929 +
1930 + out('<abbr class="dtstart", title="%s">%s</abbr>\r\n' %
1931 + (dtstart.strftime(machine), dtstart.strftime(timeformat)))
1932 +
1933 + # DTEND
1934 + dtend = event.getChildValue("dtend")
1935 + if not dtend:
1936 + duration = event.getChildValue("duration")
1937 + if duration:
1938 + dtend = duration + dtstart
1939 + # TODO: If lacking dtend & duration?
1940 +
1941 + if dtend:
1942 + human = dtend
1943 + # TODO: Human readable part could be smarter, excluding repeated data
1944 + if type(dtend) == date:
1945 + human = dtend - timedelta(days=1)
1946 +
1947 + out('- <abbr class="dtend", title="%s">%s</abbr>\r\n' %
1948 + (dtend.strftime(machine), human.strftime(timeformat)))
1949 +
1950 + # LOCATION
1951 + location = event.getChildValue("location")
1952 + if location:
1953 + out('at <span class="location">' + location + '</span>' + CRLF)
1954 +
1955 + description = event.getChildValue("description")
1956 + if description:
1957 + out('<div class="description">' + description + '</div>' + CRLF)
1958 +
1959 + if url:
1960 + level -= 1
1961 + out('</a>' + CRLF)
1962 +
1963 + level -= 1
1964 + out('</span>' + CRLF) # close vevent
1965 +
1966 + return buf or outbuf.getvalue()
1967 +
1968 +registerBehavior(HCalendar)
1969 \ No newline at end of file
1970 diff -r 2dde35b02026 MoinMoin/support/vobject/icalendar.py
1971 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1972 +++ b/MoinMoin/support/vobject/icalendar.py Thu Dec 20 20:04:49 2007 +0100
1973 @@ -0,0 +1,1769 @@
1974 +"""Definitions and behavior for iCalendar, also known as vCalendar 2.0"""
1975 +
1976 +import string
1977 +import behavior
1978 +import dateutil.rrule
1979 +import dateutil.tz
1980 +import StringIO
1981 +import datetime
1982 +import socket, random #for generating a UID
1983 +import itertools
1984 +
1985 +from base import VObjectError, NativeError, ValidateError, ParseError, \
1986 + VBase, Component, ContentLine, logger, defaultSerialize, \
1987 + registerBehavior, backslashEscape, foldOneLine, \
1988 + newFromBehavior, CRLF, LF
1989 +
1990 +#------------------------------- Constants -------------------------------------
1991 +DATENAMES = ("rdate", "exdate")
1992 +RULENAMES = ("exrule", "rrule")
1993 +DATESANDRULES = ("exrule", "rrule", "rdate", "exdate")
1994 +PRODID = u"-//PYVOBJECT//NONSGML Version 1//EN"
1995 +
1996 +WEEKDAYS = "MO", "TU", "WE", "TH", "FR", "SA", "SU"
1997 +FREQUENCIES = ('YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY',
1998 + 'SECONDLY')
1999 +
2000 +zeroDelta = datetime.timedelta(0)
2001 +twoHours = datetime.timedelta(hours=2)
2002 +
2003 +#---------------------------- TZID registry ------------------------------------
2004 +__tzidMap={}
2005 +
2006 +def registerTzid(tzid, tzinfo):
2007 + """Register a tzid -> tzinfo mapping."""
2008 + __tzidMap[tzid]=tzinfo
2009 +
2010 +def getTzid(tzid):
2011 + """Return the tzid if it exists, or None."""
2012 + return __tzidMap.get(tzid, None)
2013 +
2014 +utc = dateutil.tz.tzutc()
2015 +registerTzid("UTC", utc)
2016 +
2017 +#-------------------- Helper subclasses ----------------------------------------
2018 +
2019 +class TimezoneComponent(Component):
2020 + """A VTIMEZONE object.
2021 +
2022 + VTIMEZONEs are parsed by dateutil.tz.tzical, the resulting datetime.tzinfo
2023 + subclass is stored in self.tzinfo, self.tzid stores the TZID associated
2024 + with this timezone.
2025 +
2026 + @ivar name:
2027 + The uppercased name of the object, in this case always 'VTIMEZONE'.
2028 + @ivar tzinfo:
2029 + A datetime.tzinfo subclass representing this timezone.
2030 + @ivar tzid:
2031 + The string used to refer to this timezone.
2032 +
2033 + """
2034 + def __init__(self, tzinfo=None, *args, **kwds):
2035 + """Accept an existing Component or a tzinfo class."""
2036 + super(TimezoneComponent, self).__init__(*args, **kwds)
2037 + self.isNative=True
2038 + # hack to make sure a behavior is assigned
2039 + if self.behavior is None:
2040 + self.behavior = VTimezone
2041 + if tzinfo is not None:
2042 + self.tzinfo = tzinfo
2043 + if not hasattr(self, 'name') or self.name == '':
2044 + self.name = 'VTIMEZONE'
2045 + self.useBegin = True
2046 +
2047 + @classmethod
2048 + def registerTzinfo(obj, tzinfo):
2049 + """Register tzinfo if it's not already registered, return its tzid."""
2050 + tzid = obj.pickTzid(tzinfo)
2051 + if tzid and not getTzid(tzid):
2052 + registerTzid(tzid, tzinfo)
2053 + return tzid
2054 +
2055 + def gettzinfo(self):
2056 + # workaround for dateutil failing to parse some experimental properties
2057 + good_lines = ('rdate', 'rrule', 'dtstart', 'tzname', 'tzoffsetfrom',
2058 + 'tzoffsetto', 'tzid')
2059 + buffer = StringIO.StringIO()
2060 + # allow empty VTIMEZONEs
2061 + if len(self.contents) == 0:
2062 + return None
2063 + def customSerialize(obj):
2064 + if isinstance(obj, Component):
2065 + foldOneLine(buffer, u"BEGIN:" + obj.name)
2066 + for child in obj.lines():
2067 + if child.name.lower() in good_lines:
2068 + child.serialize(buffer, 75, validate=False)
2069 + for comp in obj.components():
2070 + customSerialize(comp)
2071 + foldOneLine(buffer, u"END:" + obj.name)
2072 + customSerialize(self)
2073 + return dateutil.tz.tzical(StringIO.StringIO(str(buffer.getvalue()))).get()
2074 +
2075 + def settzinfo(self, tzinfo, start=2000, end=2030):
2076 + """Create appropriate objects in self to represent tzinfo.
2077 +
2078 + Collapse DST transitions to rrules as much as possible.
2079 +
2080 + Assumptions:
2081 + - DST <-> Standard transitions occur on the hour
2082 + - never within a month of one another
2083 + - twice or fewer times a year
2084 + - never in the month of December
2085 + - DST always moves offset exactly one hour later
2086 + - tzinfo classes dst method always treats times that could be in either
2087 + offset as being in the later regime
2088 +
2089 + """
2090 + def fromLastWeek(dt):
2091 + """How many weeks from the end of the month dt is, starting from 1."""
2092 + weekDelta = datetime.timedelta(weeks=1)
2093 + n = 1
2094 + current = dt + weekDelta
2095 + while current.month == dt.month:
2096 + n += 1
2097 + current += weekDelta
2098 + return n
2099 +
2100 + # lists of dictionaries defining rules which are no longer in effect
2101 + completed = {'daylight' : [], 'standard' : []}
2102 +
2103 + # dictionary defining rules which are currently in effect
2104 + working = {'daylight' : None, 'standard' : None}
2105 +
2106 + # rule may be based on the nth week of the month or the nth from the last
2107 + for year in xrange(start, end + 1):
2108 + newyear = datetime.datetime(year, 1, 1)
2109 + for transitionTo in 'daylight', 'standard':
2110 + transition = getTransition(transitionTo, year, tzinfo)
2111 + oldrule = working[transitionTo]
2112 +
2113 + if transition == newyear:
2114 + # transitionTo is in effect for the whole year
2115 + rule = {'end' : None,
2116 + 'start' : newyear,
2117 + 'month' : 1,
2118 + 'weekday' : None,
2119 + 'hour' : None,
2120 + 'plus' : None,
2121 + 'minus' : None,
2122 + 'name' : tzinfo.tzname(newyear),
2123 + 'offset' : tzinfo.utcoffset(newyear),
2124 + 'offsetfrom' : tzinfo.utcoffset(newyear)}
2125 + if oldrule is None:
2126 + # transitionTo was not yet in effect
2127 + working[transitionTo] = rule
2128 + else:
2129 + # transitionTo was already in effect
2130 + if (oldrule['offset'] !=
2131 + tzinfo.utcoffset(newyear)):
2132 + # old rule was different, it shouldn't continue
2133 + oldrule['end'] = year - 1
2134 + completed[transitionTo].append(oldrule)
2135 + working[transitionTo] = rule
2136 + elif transition is None:
2137 + # transitionTo is not in effect
2138 + if oldrule is not None:
2139 + # transitionTo used to be in effect
2140 + oldrule['end'] = year - 1
2141 + completed[transitionTo].append(oldrule)
2142 + working[transitionTo] = None
2143 + else:
2144 + # an offset transition was found
2145 + old_offset = tzinfo.utcoffset(transition - twoHours)
2146 + rule = {'end' : None, # None, or an integer year
2147 + 'start' : transition, # the datetime of transition
2148 + 'month' : transition.month,
2149 + 'weekday' : transition.weekday(),
2150 + 'hour' : transition.hour,
2151 + 'name' : tzinfo.tzname(transition),
2152 + 'plus' : (transition.day - 1)/ 7 + 1,#nth week of the month
2153 + 'minus' : fromLastWeek(transition), #nth from last week
2154 + 'offset' : tzinfo.utcoffset(transition),
2155 + 'offsetfrom' : old_offset}
2156 +
2157 + if oldrule is None:
2158 + working[transitionTo] = rule
2159 + else:
2160 + plusMatch = rule['plus'] == oldrule['plus']
2161 + minusMatch = rule['minus'] == oldrule['minus']
2162 + truth = plusMatch or minusMatch
2163 + for key in 'month', 'weekday', 'hour', 'offset':
2164 + truth = truth and rule[key] == oldrule[key]
2165 + if truth:
2166 + # the old rule is still true, limit to plus or minus
2167 + if not plusMatch:
2168 + oldrule['plus'] = None
2169 + if not minusMatch:
2170 + oldrule['minus'] = None
2171 + else:
2172 + # the new rule did not match the old
2173 + oldrule['end'] = year - 1
2174 + completed[transitionTo].append(oldrule)
2175 + working[transitionTo] = rule
2176 +
2177 + for transitionTo in 'daylight', 'standard':
2178 + if working[transitionTo] is not None:
2179 + completed[transitionTo].append(working[transitionTo])
2180 +
2181 + self.tzid = []
2182 + self.daylight = []
2183 + self.standard = []
2184 +
2185 + self.add('tzid').value = self.pickTzid(tzinfo, True)
2186 +
2187 + old = None
2188 + for transitionTo in 'daylight', 'standard':
2189 + for rule in completed[transitionTo]:
2190 + comp = self.add(transitionTo)
2191 + dtstart = comp.add('dtstart')
2192 + dtstart.value = rule['start']
2193 + if rule['name'] is not None:
2194 + comp.add('tzname').value = rule['name']
2195 + line = comp.add('tzoffsetto')
2196 + line.value = deltaToOffset(rule['offset'])
2197 + line = comp.add('tzoffsetfrom')
2198 + line.value = deltaToOffset(rule['offsetfrom'])
2199 +
2200 + if rule['plus'] is not None:
2201 + num = rule['plus']
2202 + elif rule['minus'] is not None:
2203 + num = -1 * rule['minus']
2204 + else:
2205 + num = None
2206 + if num is not None:
2207 + dayString = ";BYDAY=" + str(num) + WEEKDAYS[rule['weekday']]
2208 + else:
2209 + dayString = ""
2210 + if rule['end'] is not None:
2211 + if rule['hour'] is None:
2212 + # all year offset, with no rule
2213 + endDate = datetime.datetime(rule['end'], 1, 1)
2214 + else:
2215 + weekday = dateutil.rrule.weekday(rule['weekday'], num)
2216 + du_rule = dateutil.rrule.rrule(dateutil.rrule.YEARLY,
2217 + bymonth = rule['month'],byweekday = weekday,
2218 + dtstart = datetime.datetime(
2219 + rule['end'], 1, 1, rule['hour'])
2220 + )
2221 + endDate = du_rule[0]
2222 + endDate = endDate.replace(tzinfo = utc) - rule['offsetfrom']
2223 + endString = ";UNTIL="+ dateTimeToString(endDate)
2224 + else:
2225 + endString = ''
2226 + rulestring = "FREQ=YEARLY%s;BYMONTH=%s%s" % \
2227 + (dayString, str(rule['month']), endString)
2228 +
2229 + comp.add('rrule').value = rulestring
2230 +
2231 + tzinfo = property(gettzinfo, settzinfo)
2232 + # prevent Component's __setattr__ from overriding the tzinfo property
2233 + normal_attributes = Component.normal_attributes + ['tzinfo']
2234 +
2235 + @staticmethod
2236 + def pickTzid(tzinfo, allowUTC=False):
2237 + """
2238 + Given a tzinfo class, use known APIs to determine TZID, or use tzname.
2239 + """
2240 + if tzinfo is None or (not allowUTC and tzinfo_eq(tzinfo, utc)):
2241 + #If tzinfo is UTC, we don't need a TZID
2242 + return None
2243 + # try PyICU's tzid key
2244 + if hasattr(tzinfo, 'tzid'):
2245 + return tzinfo.tzid
2246 +
2247 + # try pytz zone key
2248 + if hasattr(tzinfo, 'zone'):
2249 + return tzinfo.zone
2250 +
2251 + # try tzical's tzid key
2252 + elif hasattr(tzinfo, '_tzid'):
2253 + return tzinfo._tzid
2254 + else:
2255 + # return tzname for standard (non-DST) time
2256 + notDST = datetime.timedelta(0)
2257 + for month in xrange(1,13):
2258 + dt = datetime.datetime(2000, month, 1)
2259 + if tzinfo.dst(dt) == notDST:
2260 + return tzinfo.tzname(dt)
2261 + # there was no standard time in 2000!
2262 + raise VObjectError("Unable to guess TZID for tzinfo %s" % str(tzinfo))
2263 +
2264 + def __str__(self):
2265 + return "<VTIMEZONE | " + str(getattr(self, 'tzid', 'No TZID')) +">"
2266 +
2267 + def __repr__(self):
2268 + return self.__str__()
2269 +
2270 + def prettyPrint(self, level, tabwidth):
2271 + pre = ' ' * level * tabwidth
2272 + print pre, self.name
2273 + print pre, "TZID:", self.tzid
2274 + print
2275 +
2276 +class RecurringComponent(Component):
2277 + """A vCalendar component like VEVENT or VTODO which may recur.
2278 +
2279 + Any recurring component can have one or multiple RRULE, RDATE,
2280 + EXRULE, or EXDATE lines, and one or zero DTSTART lines. It can also have a
2281 + variety of children that don't have any recurrence information.
2282 +
2283 + In the example below, note that dtstart is included in the rruleset.
2284 + This is not the default behavior for dateutil's rrule implementation unless
2285 + dtstart would already have been a member of the recurrence rule, and as a
2286 + result, COUNT is wrong. This can be worked around when getting rruleset by
2287 + adjusting count down by one if an rrule has a count and dtstart isn't in its
2288 + result set, but by default, the rruleset property doesn't do this work
2289 + around, to access it getrruleset must be called with addRDate set True.
2290 +
2291 + >>> import dateutil.rrule, datetime
2292 + >>> vevent = RecurringComponent(name='VEVENT')
2293 + >>> vevent.add('rrule').value =u"FREQ=WEEKLY;COUNT=2;INTERVAL=2;BYDAY=TU,TH"
2294 + >>> vevent.add('dtstart').value = datetime.datetime(2005, 1, 19, 9)
2295 +
2296 + When creating rrule's programmatically it should be kept in
2297 + mind that count doesn't necessarily mean what rfc2445 says.
2298 +
2299 + >>> list(vevent.rruleset)
2300 + [datetime.datetime(2005, 1, 20, 9, 0), datetime.datetime(2005, 2, 1, 9, 0)]
2301 + >>> list(vevent.getrruleset(addRDate=True))
2302 + [datetime.datetime(2005, 1, 19, 9, 0), datetime.datetime(2005, 1, 20, 9, 0)]
2303 +
2304 + Also note that dateutil will expand all-day events (datetime.date values) to
2305 + datetime.datetime value with time 0 and no timezone.
2306 +
2307 + >>> vevent.dtstart.value = datetime.date(2005,3,18)
2308 + >>> list(vevent.rruleset)
2309 + [datetime.datetime(2005, 3, 29, 0, 0), datetime.datetime(2005, 3, 31, 0, 0)]
2310 + >>> list(vevent.getrruleset(True))
2311 + [datetime.datetime(2005, 3, 18, 0, 0), datetime.datetime(2005, 3, 29, 0, 0)]
2312 +
2313 + @ivar rruleset:
2314 + A U{rruleset<https://moin.conectiva.com.br/DateUtil>}.
2315 + """
2316 + def __init__(self, *args, **kwds):
2317 + super(RecurringComponent, self).__init__(*args, **kwds)
2318 + self.isNative=True
2319 + #self.clobberedRDates=[]
2320 +
2321 +
2322 + def getrruleset(self, addRDate = False):
2323 + """Get an rruleset created from self.
2324 +
2325 + If addRDate is True, add an RDATE for dtstart if it's not included in
2326 + an RRULE, and count is decremented if it exists.
2327 +
2328 + Note that for rules which don't match DTSTART, DTSTART may not appear
2329 + in list(rruleset), although it should. By default, an RDATE is not
2330 + created in these cases, and count isn't updated, so dateutil may list
2331 + a spurious occurrence.
2332 +
2333 + """
2334 + rruleset = None
2335 + for name in DATESANDRULES:
2336 + addfunc = None
2337 + for line in self.contents.get(name, ()):
2338 + # don't bother creating a rruleset unless there's a rule
2339 + if rruleset is None:
2340 + rruleset = dateutil.rrule.rruleset()
2341 + if addfunc is None:
2342 + addfunc=getattr(rruleset, name)
2343 +
2344 + if name in DATENAMES:
2345 + if type(line.value[0]) == datetime.datetime:
2346 + map(addfunc, line.value)
2347 + elif type(line.value[0]) == datetime.date:
2348 + for dt in line.value:
2349 + addfunc(datetime.datetime(dt.year, dt.month, dt.day))
2350 + else:
2351 + # ignore RDATEs with PERIOD values for now
2352 + pass
2353 + elif name in RULENAMES:
2354 + try:
2355 + dtstart = self.dtstart.value
2356 + except AttributeError, KeyError:
2357 + # Special for VTODO - try DUE property instead
2358 + try:
2359 + if self.name == "VTODO":
2360 + dtstart = self.due.value
2361 + else:
2362 + # if there's no dtstart, just return None
2363 + return None
2364 + except AttributeError, KeyError:
2365 + # if there's no due, just return None
2366 + return None
2367 +
2368 + # rrulestr complains about unicode, so cast to str
2369 + rule = dateutil.rrule.rrulestr(str(line.value),
2370 + dtstart=dtstart)
2371 + until = rule._until
2372 + if until is not None and \
2373 + isinstance(dtstart, datetime.datetime) and \
2374 + (until.tzinfo != dtstart.tzinfo):
2375 + # dateutil converts the UNTIL date to a datetime,
2376 + # check to see if the UNTIL parameter value was a date
2377 + vals = dict(pair.split('=') for pair in
2378 + line.value.upper().split(';'))
2379 + if len(vals.get('UNTIL', '')) == 8:
2380 + until = datetime.datetime.combine(until.date(),
2381 + dtstart.time())
2382 + # While RFC2445 says UNTIL MUST be UTC, Chandler allows
2383 + # floating recurring events, and uses floating UNTIL values.
2384 + # Also, some odd floating UNTIL but timezoned DTSTART values
2385 + # have shown up in the wild, so put floating UNTIL values
2386 + # DTSTART's timezone
2387 + if until.tzinfo is None:
2388 + until = until.replace(tzinfo=dtstart.tzinfo)
2389 +
2390 + if dtstart.tzinfo is not None:
2391 + until = until.astimezone(dtstart.tzinfo)
2392 +
2393 + rule._until = until
2394 +
2395 + # add the rrule or exrule to the rruleset
2396 + addfunc(rule)
2397 +
2398 + if name == 'rrule' and addRDate:
2399 + try:
2400 + # dateutils does not work with all-day (datetime.date) items
2401 + # so we need to convert to a datetime.datetime
2402 + # (which is what dateutils does internally)
2403 + if not isinstance(dtstart, datetime.datetime):
2404 + adddtstart = datetime.datetime.fromordinal(dtstart.toordinal())
2405 + else:
2406 + adddtstart = dtstart
2407 + if rruleset._rrule[-1][0] != adddtstart:
2408 + rruleset.rdate(adddtstart)
2409 + added = True
2410 + else:
2411 + added = False
2412 + except IndexError:
2413 + # it's conceivable that an rrule might have 0 datetimes
2414 + added = False
2415 + if added and rruleset._rrule[-1]._count != None:
2416 + rruleset._rrule[-1]._count -= 1
2417 + return rruleset
2418 +
2419 + def setrruleset(self, rruleset):
2420 +
2421 + # Get DTSTART from component (or DUE if no DTSTART in a VTODO)
2422 + try:
2423 + dtstart = self.dtstart.value
2424 + except AttributeError, KeyError:
2425 + if self.name == "VTODO":
2426 + dtstart = self.due.value
2427 + else:
2428 + raise
2429 +
2430 + isDate = datetime.date == type(dtstart)
2431 + if isDate:
2432 + dtstart = datetime.datetime(dtstart.year,dtstart.month, dtstart.day)
2433 + untilSerialize = dateToString
2434 + else:
2435 + # make sure to convert time zones to UTC
2436 + untilSerialize = lambda x: dateTimeToString(x, True)
2437 +
2438 + for name in DATESANDRULES:
2439 + if hasattr(self.contents, name):
2440 + del self.contents[name]
2441 + setlist = getattr(rruleset, '_' + name)
2442 + if name in DATENAMES:
2443 + setlist = list(setlist) # make a copy of the list
2444 + if name == 'rdate' and dtstart in setlist:
2445 + setlist.remove(dtstart)
2446 + if isDate:
2447 + setlist = [dt.date() for dt in setlist]
2448 + if len(setlist) > 0:
2449 + self.add(name).value = setlist
2450 + elif name in RULENAMES:
2451 + for rule in setlist:
2452 + buf = StringIO.StringIO()
2453 + buf.write('FREQ=')
2454 + buf.write(FREQUENCIES[rule._freq])
2455 +
2456 + values = {}
2457 +
2458 + if rule._interval != 1:
2459 + values['INTERVAL'] = [str(rule._interval)]
2460 + if rule._wkst != 0: # wkst defaults to Monday
2461 + values['WKST'] = [WEEKDAYS[rule._wkst]]
2462 + if rule._bysetpos is not None:
2463 + values['BYSETPOS'] = [str(i) for i in rule._bysetpos]
2464 +
2465 + if rule._count is not None:
2466 + values['COUNT'] = [str(rule._count)]
2467 + elif rule._until is not None:
2468 + values['UNTIL'] = [untilSerialize(rule._until)]
2469 +
2470 + days = []
2471 + if (rule._byweekday is not None and (
2472 + dateutil.rrule.WEEKLY != rule._freq or
2473 + len(rule._byweekday) != 1 or
2474 + rule._dtstart.weekday() != rule._byweekday[0])):
2475 + # ignore byweekday if freq is WEEKLY and day correlates
2476 + # with dtstart because it was automatically set by
2477 + # dateutil
2478 + days.extend(WEEKDAYS[n] for n in rule._byweekday)
2479 +
2480 + if rule._bynweekday is not None:
2481 + days.extend(str(n) + WEEKDAYS[day] for day, n in rule._bynweekday)
2482 +
2483 + if len(days) > 0:
2484 + values['BYDAY'] = days
2485 +
2486 + if rule._bymonthday is not None and len(rule._bymonthday) > 0:
2487 + if not (rule._freq <= dateutil.rrule.MONTHLY and
2488 + len(rule._bymonthday) == 1 and
2489 + rule._bymonthday[0] == rule._dtstart.day):
2490 + # ignore bymonthday if it's generated by dateutil
2491 + values['BYMONTHDAY'] = [str(n) for n in rule._bymonthday]
2492 +
2493 + if rule._bymonth is not None and len(rule._bymonth) > 0:
2494 + if (rule._byweekday is not None or
2495 + len(rule._bynweekday or ()) > 0 or
2496 + not (rule._freq == dateutil.rrule.YEARLY and
2497 + len(rule._bymonth) == 1 and
2498 + rule._bymonth[0] == rule._dtstart.month)):
2499 + # ignore bymonth if it's generated by dateutil
2500 + values['BYMONTH'] = [str(n) for n in rule._bymonth]
2501 +
2502 + if rule._byyearday is not None:
2503 + values['BYYEARDAY'] = [str(n) for n in rule._byyearday]
2504 + if rule._byweekno is not None:
2505 + values['BYWEEKNO'] = [str(n) for n in rule._byweekno]
2506 +
2507 + # byhour, byminute, bysecond are always ignored for now
2508 +
2509 +
2510 + for key, paramvals in values.iteritems():
2511 + buf.write(';')
2512 + buf.write(key)
2513 + buf.write('=')
2514 + buf.write(','.join(paramvals))
2515 +
2516 + self.add(name).value = buf.getvalue()
2517 +
2518 +
2519 +
2520 + rruleset = property(getrruleset, setrruleset)
2521 +
2522 + def __setattr__(self, name, value):
2523 + """For convenience, make self.contents directly accessible."""
2524 + if name == 'rruleset':
2525 + self.setrruleset(value)
2526 + else:
2527 + super(RecurringComponent, self).__setattr__(name, value)
2528 +
2529 +class TextBehavior(behavior.Behavior):
2530 + """Provide backslash escape encoding/decoding for single valued properties.
2531 +
2532 + TextBehavior also deals with base64 encoding if the ENCODING parameter is
2533 + explicitly set to BASE64.
2534 +
2535 + """
2536 + base64string = 'BASE64' # vCard uses B
2537 +
2538 + @classmethod
2539 + def decode(cls, line):
2540 + """Remove backslash escaping from line.value."""
2541 + if line.encoded:
2542 + encoding = getattr(line, 'encoding_param', None)
2543 + if encoding and encoding.upper() == cls.base64string:
2544 + line.value = line.value.decode('base64')
2545 + else:
2546 + line.value = stringToTextValues(line.value)[0]
2547 + line.encoded=False
2548 +
2549 + @classmethod
2550 + def encode(cls, line):
2551 + """Backslash escape line.value."""
2552 + if not line.encoded:
2553 + encoding = getattr(line, 'encoding_param', None)
2554 + if encoding and encoding.upper() == cls.base64string:
2555 + line.value = line.value.encode('base64').replace('\n', '')
2556 + else:
2557 + line.value = backslashEscape(line.value)
2558 + line.encoded=True
2559 +
2560 +class VCalendarComponentBehavior(behavior.Behavior):
2561 + defaultBehavior = TextBehavior
2562 + isComponent = True
2563 +
2564 +class RecurringBehavior(VCalendarComponentBehavior):
2565 + """Parent Behavior for components which should be RecurringComponents."""
2566 + hasNative = True
2567 +
2568 + @staticmethod
2569 + def transformToNative(obj):
2570 + """Turn a recurring Component into a RecurringComponent."""
2571 + if not obj.isNative:
2572 + object.__setattr__(obj, '__class__', RecurringComponent)
2573 + obj.isNative = True
2574 + return obj
2575 +
2576 + @staticmethod
2577 + def transformFromNative(obj):
2578 + if obj.isNative:
2579 + object.__setattr__(obj, '__class__', Component)
2580 + obj.isNative = False
2581 + return obj
2582 +
2583 + @staticmethod
2584 + def generateImplicitParameters(obj):
2585 + """Generate a UID if one does not exist.
2586 +
2587 + This is just a dummy implementation, for now.
2588 +
2589 + """
2590 + if not hasattr(obj, 'uid'):
2591 + rand = str(int(random.random() * 100000))
2592 + now = datetime.datetime.now(utc)
2593 + now = dateTimeToString(now)
2594 + host = socket.gethostname()
2595 + obj.add(ContentLine('UID', [], now + '-' + rand + '@' + host))
2596 +
2597 +
2598 +class DateTimeBehavior(behavior.Behavior):
2599 + """Parent Behavior for ContentLines containing one DATE-TIME."""
2600 + hasNative = True
2601 +
2602 + @staticmethod
2603 + def transformToNative(obj):
2604 + """Turn obj.value into a datetime.
2605 +
2606 + RFC2445 allows times without time zone information, "floating times"
2607 + in some properties. Mostly, this isn't what you want, but when parsing
2608 + a file, real floating times are noted by setting to 'TRUE' the
2609 + X-VOBJ-FLOATINGTIME-ALLOWED parameter.
2610 +
2611 + """
2612 + if obj.isNative: return obj
2613 + obj.isNative = True
2614 + if obj.value == '': return obj
2615 + obj.value=str(obj.value)
2616 + #we're cheating a little here, parseDtstart allows DATE
2617 + obj.value=parseDtstart(obj)
2618 + if obj.value.tzinfo is None:
2619 + obj.params['X-VOBJ-FLOATINGTIME-ALLOWED'] = ['TRUE']
2620 + if obj.params.get('TZID'):
2621 + # Keep a copy of the original TZID around
2622 + obj.params['X-VOBJ-ORIGINAL-TZID'] = obj.params['TZID']
2623 + del obj.params['TZID']
2624 + return obj
2625 +
2626 + @classmethod
2627 + def transformFromNative(cls, obj):
2628 + """Replace the datetime in obj.value with an ISO 8601 string."""
2629 + if obj.isNative:
2630 + obj.isNative = False
2631 + tzid = TimezoneComponent.registerTzinfo(obj.value.tzinfo)
2632 + obj.value = dateTimeToString(obj.value, cls.forceUTC)
2633 + if not cls.forceUTC and tzid is not None:
2634 + obj.tzid_param = tzid
2635 + if obj.params.get('X-VOBJ-ORIGINAL-TZID'):
2636 + if not hasattr(obj, 'tzid_param'):
2637 + obj.tzid_param = obj.params['X-VOBJ-ORIGINAL-TZID']
2638 + del obj.params['X-VOBJ-ORIGINAL-TZID']
2639 +
2640 + return obj
2641 +
2642 +class UTCDateTimeBehavior(DateTimeBehavior):
2643 + """A value which must be specified in UTC."""
2644 + forceUTC = True
2645 +
2646 +class DateOrDateTimeBehavior(behavior.Behavior):
2647 + """Parent Behavior for ContentLines containing one DATE or DATE-TIME."""
2648 + hasNative = True
2649 +
2650 + @staticmethod
2651 + def transformToNative(obj):
2652 + """Turn obj.value into a date or datetime."""
2653 + if obj.isNative: return obj
2654 + obj.isNative = True
2655 + if obj.value == '': return obj
2656 + obj.value=str(obj.value)
2657 + obj.value=parseDtstart(obj, allowSignatureMismatch=True)
2658 + if getattr(obj, 'value_param', 'DATE-TIME').upper() == 'DATE-TIME':
2659 + if hasattr(obj, 'tzid_param'):
2660 + # Keep a copy of the original TZID around
2661 + obj.params['X-VOBJ-ORIGINAL-TZID'] = obj.tzid_param
2662 + del obj.tzid_param
2663 + return obj
2664 +
2665 + @staticmethod
2666 + def transformFromNative(obj):
2667 + """Replace the date or datetime in obj.value with an ISO 8601 string."""
2668 + if type(obj.value) == datetime.date:
2669 + obj.isNative = False
2670 + obj.value_param = 'DATE'
2671 + obj.value = dateToString(obj.value)
2672 + return obj
2673 + else: return DateTimeBehavior.transformFromNative(obj)
2674 +
2675 +class MultiDateBehavior(behavior.Behavior):
2676 + """
2677 + Parent Behavior for ContentLines containing one or more DATE, DATE-TIME, or
2678 + PERIOD.
2679 +
2680 + """
2681 + hasNative = True
2682 +
2683 + @staticmethod
2684 + def transformToNative(obj):
2685 + """
2686 + Turn obj.value into a list of dates, datetimes, or
2687 + (datetime, timedelta) tuples.
2688 +
2689 + """
2690 + if obj.isNative:
2691 + return obj
2692 + obj.isNative = True
2693 + if obj.value == '':
2694 + obj.value = []
2695 + return obj
2696 + tzinfo = getTzid(getattr(obj, 'tzid_param', None))
2697 + valueParam = getattr(obj, 'value_param', "DATE-TIME").upper()
2698 + valTexts = obj.value.split(",")
2699 + if valueParam == "DATE":
2700 + obj.value = [stringToDate(x) for x in valTexts]
2701 + elif valueParam == "DATE-TIME":
2702 + obj.value = [stringToDateTime(x, tzinfo) for x in valTexts]
2703 + elif valueParam == "PERIOD":
2704 + obj.value = [stringToPeriod(x, tzinfo) for x in valTexts]
2705 + return obj
2706 +
2707 + @staticmethod
2708 + def transformFromNative(obj):
2709 + """
2710 + Replace the date, datetime or period tuples in obj.value with
2711 + appropriate strings.
2712 +
2713 + """
2714 + if obj.value and type(obj.value[0]) == datetime.date:
2715 + obj.isNative = False
2716 + obj.value_param = 'DATE'
2717 + obj.value = ','.join([dateToString(val) for val in obj.value])
2718 + return obj
2719 + # Fixme: handle PERIOD case
2720 + else:
2721 + if obj.isNative:
2722 + obj.isNative = False
2723 + transformed = []
2724 + tzid = None
2725 + for val in obj.value:
2726 + if tzid is None and type(val) == datetime.datetime:
2727 + tzid = TimezoneComponent.registerTzinfo(val.tzinfo)
2728 + if tzid is not None:
2729 + obj.tzid_param = tzid
2730 + transformed.append(dateTimeToString(val))
2731 + obj.value = ','.join(transformed)
2732 + return obj
2733 +
2734 +class MultiTextBehavior(behavior.Behavior):
2735 + """Provide backslash escape encoding/decoding of each of several values.
2736 +
2737 + After transformation, value is a list of strings.
2738 +
2739 + """
2740 +
2741 + @staticmethod
2742 + def decode(line):
2743 + """Remove backslash escaping from line.value, then split on commas."""
2744 + if line.encoded:
2745 + line.value = stringToTextValues(line.value)
2746 + line.encoded=False
2747 +
2748 + @staticmethod
2749 + def encode(line):
2750 + """Backslash escape line.value."""
2751 + if not line.encoded:
2752 + line.value = ','.join(backslashEscape(val) for val in line.value)
2753 + line.encoded=True
2754 +
2755 +
2756 +#------------------------ Registered Behavior subclasses -----------------------
2757 +class VCalendar2_0(VCalendarComponentBehavior):
2758 + """vCalendar 2.0 behavior."""
2759 + name = 'VCALENDAR'
2760 + description = 'vCalendar 2.0, also known as iCalendar.'
2761 + versionString = '2.0'
2762 + sortFirst = ('version', 'calscale', 'method', 'prodid', 'vtimezone')
2763 + knownChildren = {'CALSCALE': (0, 1, None),#min, max, behaviorRegistry id
2764 + 'METHOD': (0, 1, None),
2765 + 'VERSION': (0, 1, None),#required, but auto-generated
2766 + 'PRODID': (1, 1, None),
2767 + 'VTIMEZONE': (0, None, None),
2768 + 'VEVENT': (0, None, None),
2769 + 'VTODO': (0, None, None),
2770 + 'VJOURNAL': (0, None, None),
2771 + 'VFREEBUSY': (0, None, None)
2772 + }
2773 +
2774 + @classmethod
2775 + def generateImplicitParameters(cls, obj):
2776 + """Create PRODID, VERSION, and VTIMEZONEs if needed.
2777 +
2778 + VTIMEZONEs will need to exist whenever TZID parameters exist or when
2779 + datetimes with tzinfo exist.
2780 +
2781 + """
2782 + for comp in obj.components():
2783 + if comp.behavior is not None:
2784 + comp.behavior.generateImplicitParameters(comp)
2785 + if not hasattr(obj, 'prodid'):
2786 + obj.add(ContentLine('PRODID', [], PRODID))
2787 + if not hasattr(obj, 'version'):
2788 + obj.add(ContentLine('VERSION', [], cls.versionString))
2789 + tzidsUsed = {}
2790 +
2791 + def findTzids(obj, table):
2792 + if isinstance(obj, ContentLine) and (obj.behavior is None or
2793 + not obj.behavior.forceUTC):
2794 + if getattr(obj, 'tzid_param', None):
2795 + table[obj.tzid_param] = 1
2796 + else:
2797 + if type(obj.value) == list:
2798 + for item in obj.value:
2799 + tzinfo = getattr(obj.value, 'tzinfo', None)
2800 + tzid = TimezoneComponent.registerTzinfo(tzinfo)
2801 + if tzid:
2802 + table[tzid] = 1
2803 + else:
2804 + tzinfo = getattr(obj.value, 'tzinfo', None)
2805 + tzid = TimezoneComponent.registerTzinfo(tzinfo)
2806 + if tzid:
2807 + table[tzid] = 1
2808 + for child in obj.getChildren():
2809 + if obj.name != 'VTIMEZONE':
2810 + findTzids(child, table)
2811 +
2812 + findTzids(obj, tzidsUsed)
2813 + oldtzids = [x.tzid.value for x in getattr(obj, 'vtimezone_list', [])]
2814 + for tzid in tzidsUsed.keys():
2815 + if tzid != 'UTC' and tzid not in oldtzids:
2816 + obj.add(TimezoneComponent(tzinfo=getTzid(tzid)))
2817 +registerBehavior(VCalendar2_0)
2818 +
2819 +class VTimezone(VCalendarComponentBehavior):
2820 + """Timezone behavior."""
2821 + name = 'VTIMEZONE'
2822 + hasNative = True
2823 + description = 'A grouping of component properties that defines a time zone.'
2824 + sortFirst = ('tzid', 'last-modified', 'tzurl', 'standard', 'daylight')
2825 + knownChildren = {'TZID': (1, 1, None),#min, max, behaviorRegistry id
2826 + 'LAST-MODIFIED':(0, 1, None),
2827 + 'TZURL': (0, 1, None),
2828 + 'STANDARD': (0, None, None),#NOTE: One of Standard or
2829 + 'DAYLIGHT': (0, None, None) # Daylight must appear
2830 + }
2831 +
2832 + @classmethod
2833 + def validate(cls, obj, raiseException, *args):
2834 + if not hasattr(obj, 'tzid') or obj.tzid.value is None:
2835 + if raiseException:
2836 + m = "VTIMEZONE components must contain a valid TZID"
2837 + raise ValidateError(m)
2838 + return False
2839 + if obj.contents.has_key('standard') or obj.contents.has_key('daylight'):
2840 + return super(VTimezone, cls).validate(obj, raiseException, *args)
2841 + else:
2842 + if raiseException:
2843 + m = "VTIMEZONE components must contain a STANDARD or a DAYLIGHT\
2844 + component"
2845 + raise ValidateError(m)
2846 + return False
2847 +
2848 +
2849 + @staticmethod
2850 + def transformToNative(obj):
2851 + if not obj.isNative:
2852 + object.__setattr__(obj, '__class__', TimezoneComponent)
2853 + obj.isNative = True
2854 + obj.registerTzinfo(obj.tzinfo)
2855 + return obj
2856 +
2857 + @staticmethod
2858 + def transformFromNative(obj):
2859 + return obj
2860 +
2861 +
2862 +registerBehavior(VTimezone)
2863 +
2864 +class DaylightOrStandard(VCalendarComponentBehavior):
2865 + hasNative = False
2866 + knownChildren = {'DTSTART': (1, 1, None),#min, max, behaviorRegistry id
2867 + 'RRULE': (0, 1, None)}
2868 +
2869 +registerBehavior(DaylightOrStandard, 'STANDARD')
2870 +registerBehavior(DaylightOrStandard, 'DAYLIGHT')
2871 +
2872 +
2873 +class VEvent(RecurringBehavior):
2874 + """Event behavior."""
2875 + name='VEVENT'
2876 + sortFirst = ('uid', 'recurrence-id', 'dtstart', 'duration', 'dtend')
2877 +
2878 + description='A grouping of component properties, and possibly including \
2879 + "VALARM" calendar components, that represents a scheduled \
2880 + amount of time on a calendar.'
2881 + knownChildren = {'DTSTART': (0, 1, None),#min, max, behaviorRegistry id
2882 + 'CLASS': (0, 1, None),
2883 + 'CREATED': (0, 1, None),
2884 + 'DESCRIPTION': (0, 1, None),
2885 + 'GEO': (0, 1, None),
2886 + 'LAST-MODIFIED':(0, 1, None),
2887 + 'LOCATION': (0, 1, None),
2888 + 'ORGANIZER': (0, 1, None),
2889 + 'PRIORITY': (0, 1, None),
2890 + 'DTSTAMP': (0, 1, None),
2891 + 'SEQUENCE': (0, 1, None),
2892 + 'STATUS': (0, 1, None),
2893 + 'SUMMARY': (0, 1, None),
2894 + 'TRANSP': (0, 1, None),
2895 + 'UID': (1, 1, None),
2896 + 'URL': (0, 1, None),
2897 + 'RECURRENCE-ID':(0, 1, None),
2898 + 'DTEND': (0, 1, None), #NOTE: Only one of DtEnd or
2899 + 'DURATION': (0, 1, None), # Duration can appear
2900 + 'ATTACH': (0, None, None),
2901 + 'ATTENDEE': (0, None, None),
2902 + 'CATEGORIES': (0, None, None),
2903 + 'COMMENT': (0, None, None),
2904 + 'CONTACT': (0, None, None),
2905 + 'EXDATE': (0, None, None),
2906 + 'EXRULE': (0, None, None),
2907 + 'REQUEST-STATUS': (0, None, None),
2908 + 'RELATED-TO': (0, None, None),
2909 + 'RESOURCES': (0, None, None),
2910 + 'RDATE': (0, None, None),
2911 + 'RRULE': (0, None, None),
2912 + 'VALARM': (0, None, None)
2913 + }
2914 +
2915 + @classmethod
2916 + def validate(cls, obj, raiseException, *args):
2917 + if obj.contents.has_key('DTEND') and obj.contents.has_key('DURATION'):
2918 + if raiseException:
2919 + m = "VEVENT components cannot contain both DTEND and DURATION\
2920 + components"
2921 + raise ValidateError(m)
2922 + return False
2923 + else:
2924 + return super(VEvent, cls).validate(obj, raiseException, *args)
2925 +
2926 +registerBehavior(VEvent)
2927 +
2928 +
2929 +class VTodo(RecurringBehavior):
2930 + """To-do behavior."""
2931 + name='VTODO'
2932 + description='A grouping of component properties and possibly "VALARM" \
2933 + calendar components that represent an action-item or \
2934 + assignment.'
2935 + knownChildren = {'DTSTART': (0, 1, None),#min, max, behaviorRegistry id
2936 + 'CLASS': (0, 1, None),
2937 + 'COMPLETED': (0, 1, None),
2938 + 'CREATED': (0, 1, None),
2939 + 'DESCRIPTION': (0, 1, None),
2940 + 'GEO': (0, 1, None),
2941 + 'LAST-MODIFIED':(0, 1, None),
2942 + 'LOCATION': (0, 1, None),
2943 + 'ORGANIZER': (0, 1, None),
2944 + 'PERCENT': (0, 1, None),
2945 + 'PRIORITY': (0, 1, None),
2946 + 'DTSTAMP': (0, 1, None),
2947 + 'SEQUENCE': (0, 1, None),
2948 + 'STATUS': (0, 1, None),
2949 + 'SUMMARY': (0, 1, None),
2950 + 'UID': (0, 1, None),
2951 + 'URL': (0, 1, None),
2952 + 'RECURRENCE-ID':(0, 1, None),
2953 + 'DUE': (0, 1, None), #NOTE: Only one of Due or
2954 + 'DURATION': (0, 1, None), # Duration can appear
2955 + 'ATTACH': (0, None, None),
2956 + 'ATTENDEE': (0, None, None),
2957 + 'CATEGORIES': (0, None, None),
2958 + 'COMMENT': (0, None, None),
2959 + 'CONTACT': (0, None, None),
2960 + 'EXDATE': (0, None, None),
2961 + 'EXRULE': (0, None, None),
2962 + 'REQUEST-STATUS': (0, None, None),
2963 + 'RELATED-TO': (0, None, None),
2964 + 'RESOURCES': (0, None, None),
2965 + 'RDATE': (0, None, None),
2966 + 'RRULE': (0, None, None),
2967 + 'VALARM': (0, None, None)
2968 + }
2969 +
2970 + @classmethod
2971 + def validate(cls, obj, raiseException, *args):
2972 + if obj.contents.has_key('DUE') and obj.contents.has_key('DURATION'):
2973 + if raiseException:
2974 + m = "VTODO components cannot contain both DUE and DURATION\
2975 + components"
2976 + raise ValidateError(m)
2977 + return False
2978 + else:
2979 + return super(VTodo, cls).validate(obj, raiseException, *args)
2980 +
2981 +registerBehavior(VTodo)
2982 +
2983 +
2984 +class VJournal(RecurringBehavior):
2985 + """Journal entry behavior."""
2986 + name='VJOURNAL'
2987 + knownChildren = {'DTSTART': (0, 1, None),#min, max, behaviorRegistry id
2988 + 'CLASS': (0, 1, None),
2989 + 'CREATED': (0, 1, None),
2990 + 'DESCRIPTION': (0, 1, None),
2991 + 'LAST-MODIFIED':(0, 1, None),
2992 + 'ORGANIZER': (0, 1, None),
2993 + 'DTSTAMP': (0, 1, None),
2994 + 'SEQUENCE': (0, 1, None),
2995 + 'STATUS': (0, 1, None),
2996 + 'SUMMARY': (0, 1, None),
2997 + 'UID': (0, 1, None),
2998 + 'URL': (0, 1, None),
2999 + 'RECURRENCE-ID':(0, 1, None),
3000 + 'ATTACH': (0, None, None),
3001 + 'ATTENDEE': (0, None, None),
3002 + 'CATEGORIES': (0, None, None),
3003 + 'COMMENT': (0, None, None),
3004 + 'CONTACT': (0, None, None),
3005 + 'EXDATE': (0, None, None),
3006 + 'EXRULE': (0, None, None),
3007 + 'REQUEST-STATUS': (0, None, None),
3008 + 'RELATED-TO': (0, None, None),
3009 + 'RDATE': (0, None, None),
3010 + 'RRULE': (0, None, None)
3011 + }
3012 +registerBehavior(VJournal)
3013 +
3014 +
3015 +class VFreeBusy(VCalendarComponentBehavior):
3016 + """Free/busy state behavior.
3017 +
3018 + >>> vfb = newFromBehavior('VFREEBUSY')
3019 + >>> vfb.add('uid').value = 'test'
3020 + >>> vfb.add('dtstart').value = datetime.datetime(2006, 2, 16, 1, tzinfo=utc)
3021 + >>> vfb.add('dtend').value = vfb.dtstart.value + twoHours
3022 + >>> vfb.add('freebusy').value = [(vfb.dtstart.value, twoHours / 2)]
3023 + >>> print vfb.serialize()
3024 + BEGIN:VFREEBUSY
3025 + UID:test
3026 + DTSTART:20060216T010000Z
3027 + DTEND:20060216T030000Z
3028 + FREEBUSY:20060216T010000Z/PT1H
3029 + END:VFREEBUSY
3030 +
3031 + """
3032 + name='VFREEBUSY'
3033 + description='A grouping of component properties that describe either a \
3034 + request for free/busy time, describe a response to a request \
3035 + for free/busy time or describe a published set of busy time.'
3036 + sortFirst = ('uid', 'dtstart', 'duration', 'dtend')
3037 + knownChildren = {'DTSTART': (0, 1, None),#min, max, behaviorRegistry id
3038 + 'CONTACT': (0, 1, None),
3039 + 'DTEND': (0, 1, None),
3040 + 'DURATION': (0, 1, None),
3041 + 'ORGANIZER': (0, 1, None),
3042 + 'DTSTAMP': (0, 1, None),
3043 + 'UID': (0, 1, None),
3044 + 'URL': (0, 1, None),
3045 + 'ATTENDEE': (0, None, None),
3046 + 'COMMENT': (0, None, None),
3047 + 'FREEBUSY': (0, None, None),
3048 + 'REQUEST-STATUS': (0, None, None)
3049 + }
3050 +registerBehavior(VFreeBusy)
3051 +
3052 +
3053 +class VAlarm(VCalendarComponentBehavior):
3054 + """Alarm behavior."""
3055 + name='VALARM'
3056 + description='Alarms describe when and how to provide alerts about events \
3057 + and to-dos.'
3058 + knownChildren = {'ACTION': (1, 1, None),#min, max, behaviorRegistry id
3059 + 'TRIGGER': (1, 1, None),
3060 + 'DURATION': (0, 1, None),
3061 + 'REPEAT': (0, 1, None),
3062 + 'DESCRIPTION': (0, 1, None)
3063 + }
3064 +
3065 + @staticmethod
3066 + def generateImplicitParameters(obj):
3067 + """Create default ACTION and TRIGGER if they're not set."""
3068 + try:
3069 + obj.action
3070 + except AttributeError:
3071 + obj.add('action').value = 'AUDIO'
3072 + try:
3073 + obj.trigger
3074 + except AttributeError:
3075 + obj.add('trigger').value = datetime.timedelta(0)
3076 +
3077 +
3078 + @classmethod
3079 + def validate(cls, obj, raiseException, *args):
3080 + """
3081 + #TODO
3082 + audioprop = 2*(
3083 +
3084 + ; 'action' and 'trigger' are both REQUIRED,
3085 + ; but MUST NOT occur more than once
3086 +
3087 + action / trigger /
3088 +
3089 + ; 'duration' and 'repeat' are both optional,
3090 + ; and MUST NOT occur more than once each,
3091 + ; but if one occurs, so MUST the other
3092 +
3093 + duration / repeat /
3094 +
3095 + ; the following is optional,
3096 + ; but MUST NOT occur more than once
3097 +
3098 + attach /
3099 +
3100 + dispprop = 3*(
3101 +
3102 + ; the following are all REQUIRED,
3103 + ; but MUST NOT occur more than once
3104 +
3105 + action / description / trigger /
3106 +
3107 + ; 'duration' and 'repeat' are both optional,
3108 + ; and MUST NOT occur more than once each,
3109 + ; but if one occurs, so MUST the other
3110 +
3111 + duration / repeat /
3112 +
3113 + emailprop = 5*(
3114 +
3115 + ; the following are all REQUIRED,
3116 + ; but MUST NOT occur more than once
3117 +
3118 + action / description / trigger / summary
3119 +
3120 + ; the following is REQUIRED,
3121 + ; and MAY occur more than once
3122 +
3123 + attendee /
3124 +
3125 + ; 'duration' and 'repeat' are both optional,
3126 + ; and MUST NOT occur more than once each,
3127 + ; but if one occurs, so MUST the other
3128 +
3129 + duration / repeat /
3130 +
3131 + procprop = 3*(
3132 +
3133 + ; the following are all REQUIRED,
3134 + ; but MUST NOT occur more than once
3135 +
3136 + action / attach / trigger /
3137 +
3138 + ; 'duration' and 'repeat' are both optional,
3139 + ; and MUST NOT occur more than once each,
3140 + ; but if one occurs, so MUST the other
3141 +
3142 + duration / repeat /
3143 +
3144 + ; 'description' is optional,
3145 + ; and MUST NOT occur more than once
3146 +
3147 + description /
3148 + if obj.contents.has_key('DTEND') and obj.contents.has_key('DURATION'):
3149 + if raiseException:
3150 + m = "VEVENT components cannot contain both DTEND and DURATION\
3151 + components"
3152 + raise ValidateError(m)
3153 + return False
3154 + else:
3155 + return super(VEvent, cls).validate(obj, raiseException, *args)
3156 + """
3157 + return True
3158 +
3159 +registerBehavior(VAlarm)
3160 +
3161 +class Duration(behavior.Behavior):
3162 + """Behavior for Duration ContentLines. Transform to datetime.timedelta."""
3163 + name = 'DURATION'
3164 + hasNative = True
3165 +
3166 + @staticmethod
3167 + def transformToNative(obj):
3168 + """Turn obj.value into a datetime.timedelta."""
3169 + if obj.isNative: return obj
3170 + obj.isNative = True
3171 + obj.value=str(obj.value)
3172 + if obj.value == '':
3173 + return obj
3174 + else:
3175 + deltalist=stringToDurations(obj.value)
3176 + #When can DURATION have multiple durations? For now:
3177 + if len(deltalist) == 1:
3178 + obj.value = deltalist[0]
3179 + return obj
3180 + else:
3181 + raise ParseError("DURATION must have a single duration string.")
3182 +
3183 + @staticmethod
3184 + def transformFromNative(obj):
3185 + """Replace the datetime.timedelta in obj.value with an RFC2445 string.
3186 + """
3187 + if not obj.isNative: return obj
3188 + obj.isNative = False
3189 + obj.value = timedeltaToString(obj.value)
3190 + return obj
3191 +
3192 +registerBehavior(Duration)
3193 +
3194 +class Trigger(behavior.Behavior):
3195 + """DATE-TIME or DURATION"""
3196 + name='TRIGGER'
3197 + description='This property specifies when an alarm will trigger.'
3198 + hasNative = True
3199 + forceUTC = True
3200 +
3201 + @staticmethod
3202 + def transformToNative(obj):
3203 + """Turn obj.value into a timedelta or datetime."""
3204 + if obj.isNative: return obj
3205 + value = getattr(obj, 'value_param', 'DURATION').upper()
3206 + if hasattr(obj, 'value_param'):
3207 + del obj.value_param
3208 + if obj.value == '':
3209 + obj.isNative = True
3210 + return obj
3211 + elif value == 'DURATION':
3212 + try:
3213 + return Duration.transformToNative(obj)
3214 + except ParseError:
3215 + logger.warn("TRIGGER not recognized as DURATION, trying "
3216 + "DATE-TIME, because iCal sometimes exports "
3217 + "DATE-TIMEs without setting VALUE=DATE-TIME")
3218 + try:
3219 + obj.isNative = False
3220 + dt = DateTimeBehavior.transformToNative(obj)
3221 + return dt
3222 + except:
3223 + msg = "TRIGGER with no VALUE not recognized as DURATION " \
3224 + "or as DATE-TIME"
3225 + raise ParseError(msg)
3226 + elif value == 'DATE-TIME':
3227 + #TRIGGERs with DATE-TIME values must be in UTC, we could validate
3228 + #that fact, for now we take it on faith.
3229 + return DateTimeBehavior.transformToNative(obj)
3230 + else:
3231 + raise ParseError("VALUE must be DURATION or DATE-TIME")
3232 +
3233 + @staticmethod
3234 + def transformFromNative(obj):
3235 + if type(obj.value) == datetime.datetime:
3236 + obj.value_param = 'DATE-TIME'
3237 + return UTCDateTimeBehavior.transformFromNative(obj)
3238 + elif type(obj.value) == datetime.timedelta:
3239 + return Duration.transformFromNative(obj)
3240 + else:
3241 + raise NativeError("Native TRIGGER values must be timedelta or datetime")
3242 +
3243 +registerBehavior(Trigger)
3244 +
3245 +class PeriodBehavior(behavior.Behavior):
3246 + """A list of (date-time, timedelta) tuples.
3247 +
3248 + >>> line = ContentLine('test', [], '', isNative=True)
3249 + >>> line.behavior = PeriodBehavior
3250 + >>> line.value = [(datetime.datetime(2006, 2, 16, 10), twoHours)]
3251 + >>> line.transformFromNative().value
3252 + '20060216T100000/PT2H'
3253 + >>> line.transformToNative().value
3254 + [(datetime.datetime(2006, 2, 16, 10, 0), datetime.timedelta(0, 7200))]
3255 + >>> line.value.append((datetime.datetime(2006, 5, 16, 10), twoHours))
3256 + >>> print line.serialize().strip()
3257 + TEST:20060216T100000/PT2H,20060516T100000/PT2H
3258 + """
3259 + hasNative = True
3260 +
3261 + @staticmethod
3262 + def transformToNative(obj):
3263 + """Convert comma separated periods into tuples."""
3264 + if obj.isNative:
3265 + return obj
3266 + obj.isNative = True
3267 + if obj.value == '':
3268 + obj.value = []
3269 + return obj
3270 + tzinfo = getTzid(getattr(obj, 'tzid_param', None))
3271 + obj.value = [stringToPeriod(x, tzinfo) for x in obj.value.split(",")]
3272 + return obj
3273 +
3274 + @classmethod
3275 + def transformFromNative(cls, obj):
3276 + """Convert the list of tuples in obj.value to strings."""
3277 + if obj.isNative:
3278 + obj.isNative = False
3279 + transformed = []
3280 + for tup in obj.value:
3281 + transformed.append(periodToString(tup, cls.forceUTC))
3282 + if len(transformed) > 0:
3283 + tzid = TimezoneComponent.registerTzinfo(tup[0].tzinfo)
3284 + if not cls.forceUTC and tzid is not None:
3285 + obj.tzid_param = tzid
3286 +
3287 + obj.value = ','.join(transformed)
3288 +
3289 + return obj
3290 +
3291 +class FreeBusy(PeriodBehavior):
3292 + """Free or busy period of time, must be specified in UTC."""
3293 + name = 'FREEBUSY'
3294 + forceUTC = True
3295 +registerBehavior(FreeBusy)
3296 +
3297 +class RRule(behavior.Behavior):
3298 + """
3299 + Dummy behavior to avoid having RRULEs being treated as text lines (and thus
3300 + having semi-colons inaccurately escaped).
3301 + """
3302 +registerBehavior(RRule, 'RRULE')
3303 +registerBehavior(RRule, 'EXRULE')
3304 +
3305 +#------------------------ Registration of common classes -----------------------
3306 +
3307 +utcDateTimeList = ['LAST-MODIFIED', 'CREATED', 'COMPLETED', 'DTSTAMP']
3308 +map(lambda x: registerBehavior(UTCDateTimeBehavior, x), utcDateTimeList)
3309 +
3310 +dateTimeOrDateList = ['DTEND', 'DTSTART', 'DUE', 'RECURRENCE-ID']
3311 +map(lambda x: registerBehavior(DateOrDateTimeBehavior, x),
3312 + dateTimeOrDateList)
3313 +
3314 +registerBehavior(MultiDateBehavior, 'RDATE')
3315 +registerBehavior(MultiDateBehavior, 'EXDATE')
3316 +
3317 +
3318 +textList = ['CALSCALE', 'METHOD', 'PRODID', 'CLASS', 'COMMENT', 'DESCRIPTION',
3319 + 'LOCATION', 'STATUS', 'SUMMARY', 'TRANSP', 'CONTACT', 'RELATED-TO',
3320 + 'UID', 'ACTION', 'REQUEST-STATUS', 'TZID']
3321 +map(lambda x: registerBehavior(TextBehavior, x), textList)
3322 +
3323 +multiTextList = ['CATEGORIES', 'RESOURCES']
3324 +map(lambda x: registerBehavior(MultiTextBehavior, x), multiTextList)
3325 +
3326 +#------------------------ Serializing helper functions -------------------------
3327 +
3328 +def numToDigits(num, places):
3329 + """Helper, for converting numbers to textual digits."""
3330 + s = str(num)
3331 + if len(s) < places:
3332 + return ("0" * (places - len(s))) + s
3333 + elif len(s) > places:
3334 + return s[len(s)-places: ]
3335 + else:
3336 + return s
3337 +
3338 +def timedeltaToString(delta):
3339 + """Convert timedelta to an rfc2445 DURATION."""
3340 + if delta.days == 0: sign = 1
3341 + else: sign = delta.days / abs(delta.days)
3342 + delta = abs(delta)
3343 + days = delta.days
3344 + hours = delta.seconds / 3600
3345 + minutes = (delta.seconds % 3600) / 60
3346 + seconds = delta.seconds % 60
3347 + out = ''
3348 + if sign == -1: out = '-'
3349 + out += 'P'
3350 + if days: out += str(days) + 'D'
3351 + if hours or minutes or seconds: out += 'T'
3352 + elif not days: #Deal with zero duration
3353 + out += 'T0S'
3354 + if hours: out += str(hours) + 'H'
3355 + if minutes: out += str(minutes) + 'M'
3356 + if seconds: out += str(seconds) + 'S'
3357 + return out
3358 +
3359 +def timeToString(dateOrDateTime):
3360 + """
3361 + Wraps dateToString and dateTimeToString, returning the results
3362 + of either based on the type of the argument
3363 + """
3364 + # Didn't use isinstance here as date and datetime sometimes evalutes as both
3365 + if (type(dateOrDateTime) == datetime.date):
3366 + return dateToString(dateOrDateTime)
3367 + elif(type(dateOrDateTime) == datetime.datetime):
3368 + return dateTimeToString(dateOrDateTime)
3369 +
3370 +
3371 +def dateToString(date):
3372 + year = numToDigits( date.year, 4 )
3373 + month = numToDigits( date.month, 2 )
3374 + day = numToDigits( date.day, 2 )
3375 + return year + month + day
3376 +
3377 +def dateTimeToString(dateTime, convertToUTC=False):
3378 + """Ignore tzinfo unless convertToUTC. Output string."""
3379 + if dateTime.tzinfo and convertToUTC:
3380 + dateTime = dateTime.astimezone(utc)
3381 + if tzinfo_eq(dateTime.tzinfo, utc): utcString = "Z"
3382 + else: utcString = ""
3383 +
3384 + year = numToDigits( dateTime.year, 4 )
3385 + month = numToDigits( dateTime.month, 2 )
3386 + day = numToDigits( dateTime.day, 2 )
3387 + hour = numToDigits( dateTime.hour, 2 )
3388 + mins = numToDigits( dateTime.minute, 2 )
3389 + secs = numToDigits( dateTime.second, 2 )
3390 +
3391 + return year + month + day + "T" + hour + mins + secs + utcString
3392 +
3393 +def deltaToOffset(delta):
3394 + absDelta = abs(delta)
3395 + hours = absDelta.seconds / 3600
3396 + hoursString = numToDigits(hours, 2)
3397 + minutesString = '00'
3398 + if absDelta == delta:
3399 + signString = "+"
3400 + else:
3401 + signString = "-"
3402 + return signString + hoursString + minutesString
3403 +
3404 +def periodToString(period, convertToUTC=False):
3405 + txtstart = dateTimeToString(period[0], convertToUTC)
3406 + if isinstance(period[1], datetime.timedelta):
3407 + txtend = timedeltaToString(period[1])
3408 + else:
3409 + txtend = dateTimeToString(period[1], convertToUTC)
3410 + return txtstart + "/" + txtend
3411 +
3412 +#----------------------- Parsing functions -------------------------------------
3413 +
3414 +def isDuration(s):
3415 + s = string.upper(s)
3416 + return (string.find(s, "P") != -1) and (string.find(s, "P") < 2)
3417 +
3418 +def stringToDate(s):
3419 + year = int( s[0:4] )
3420 + month = int( s[4:6] )
3421 + day = int( s[6:8] )
3422 + return datetime.date(year,month,day)
3423 +
3424 +def stringToDateTime(s, tzinfo=None):
3425 + """Returns datetime.datetime object."""
3426 + try:
3427 + year = int( s[0:4] )
3428 + month = int( s[4:6] )
3429 + day = int( s[6:8] )
3430 + hour = int( s[9:11] )
3431 + minute = int( s[11:13] )
3432 + second = int( s[13:15] )
3433 + if len(s) > 15:
3434 + if s[15] == 'Z':
3435 + tzinfo = utc
3436 + except:
3437 + raise ParseError("'%s' is not a valid DATE-TIME" % s)
3438 + return datetime.datetime(year, month, day, hour, minute, second, 0, tzinfo)
3439 +
3440 +
3441 +escapableCharList = "\\;,Nn"
3442 +
3443 +def stringToTextValues(s, listSeparator=',', charList=None, strict=False):
3444 + """Returns list of strings."""
3445 +
3446 + if charList is None:
3447 + charList = escapableCharList
3448 +
3449 + def escapableChar (c):
3450 + return c in charList
3451 +
3452 + def error(msg):
3453 + if strict:
3454 + raise ParseError(msg)
3455 + else:
3456 + #logger.error(msg)
3457 + print msg
3458 +
3459 + #vars which control state machine
3460 + charIterator = enumerate(s)
3461 + state = "read normal"
3462 +
3463 + current = ""
3464 + results = []
3465 +
3466 + while True:
3467 + try:
3468 + charIndex, char = charIterator.next()
3469 + except:
3470 + char = "eof"
3471 +
3472 + if state == "read normal":
3473 + if char == '\\':
3474 + state = "read escaped char"
3475 + elif char == listSeparator:
3476 + state = "read normal"
3477 + results.append(current)
3478 + current = ""
3479 + elif char == "eof":
3480 + state = "end"
3481 + else:
3482 + state = "read normal"
3483 + current = current + char
3484 +
3485 + elif state == "read escaped char":
3486 + if escapableChar(char):
3487 + state = "read normal"
3488 + if char in 'nN':
3489 + current = current + '\n'
3490 + else:
3491 + current = current + char
3492 + else:
3493 + state = "read normal"
3494 + # leave unrecognized escaped characters for later passes
3495 + current = current + '\\' + char
3496 +
3497 + elif state == "end": #an end state
3498 + if current != "" or len(results) == 0:
3499 + results.append(current)
3500 + return results
3501 +
3502 + elif state == "error": #an end state
3503 + return results
3504 +
3505 + else:
3506 + state = "error"
3507 + error("error: unknown state: '%s' reached in %s" % (state, s))
3508 +
3509 +def stringToDurations(s, strict=False):
3510 + """Returns list of timedelta objects."""
3511 + def makeTimedelta(sign, week, day, hour, minute, sec):
3512 + if sign == "-": sign = -1
3513 + else: sign = 1
3514 + week = int(week)
3515 + day = int(day)
3516 + hour = int(hour)
3517 + minute = int(minute)
3518 + sec = int(sec)
3519 + return sign * datetime.timedelta(weeks=week, days=day, hours=hour, minutes=minute, seconds=sec)
3520 +
3521 + def error(msg):
3522 + if strict:
3523 + raise ParseError(msg)
3524 + else:
3525 + raise ParseError(msg)
3526 + #logger.error(msg)
3527 +
3528 + #vars which control state machine
3529 + charIterator = enumerate(s)
3530 + state = "start"
3531 +
3532 + durations = []
3533 + current = ""
3534 + sign = None
3535 + week = 0
3536 + day = 0
3537 + hour = 0
3538 + minute = 0
3539 + sec = 0
3540 +
3541 + while True:
3542 + try:
3543 + charIndex, char = charIterator.next()
3544 + except:
3545 + charIndex += 1
3546 + char = "eof"
3547 +
3548 + if state == "start":
3549 + if char == '+':
3550 + state = "start"
3551 + sign = char
3552 + elif char == '-':
3553 + state = "start"
3554 + sign = char
3555 + elif char.upper() == 'P':
3556 + state = "read field"
3557 + elif char == "eof":
3558 + state = "error"
3559 + error("got end-of-line while reading in duration: " + s)
3560 + elif char in string.digits:
3561 + state = "read field"
3562 + current = current + char #update this part when updating "read field"
3563 + else:
3564 + state = "error"
3565 + print "got unexpected character %s reading in duration: %s" % (char, s)
3566 + error("got unexpected character %s reading in duration: %s" % (char, s))
3567 +
3568 + elif state == "read field":
3569 + if (char in string.digits):
3570 + state = "read field"
3571 + current = current + char #update part above when updating "read field"
3572 + elif char.upper() == 'T':
3573 + state = "read field"
3574 + elif char.upper() == 'W':
3575 + state = "read field"
3576 + week = current
3577 + current = ""
3578 + elif char.upper() == 'D':
3579 + state = "read field"
3580 + day = current
3581 + current = ""
3582 + elif char.upper() == 'H':
3583 + state = "read field"
3584 + hour = current
3585 + current = ""
3586 + elif char.upper() == 'M':
3587 + state = "read field"
3588 + minute = current
3589 + current = ""
3590 + elif char.upper() == 'S':
3591 + state = "read field"
3592 + sec = current
3593 + current = ""
3594 + elif char == ",":
3595 + state = "start"
3596 + durations.append( makeTimedelta(sign, week, day, hour, minute, sec) )
3597 + current = ""
3598 + sign = None
3599 + week = None
3600 + day = None
3601 + hour = None
3602 + minute = None
3603 + sec = None
3604 + elif char == "eof":
3605 + state = "end"
3606 + else:
3607 + state = "error"
3608 + error("got unexpected character reading in duration: " + s)
3609 +
3610 + elif state == "end": #an end state
3611 + #print "stuff: %s, durations: %s" % ([current, sign, week, day, hour, minute, sec], durations)
3612 +
3613 + if (sign or week or day or hour or minute or sec):
3614 + durations.append( makeTimedelta(sign, week, day, hour, minute, sec) )
3615 + return durations
3616 +
3617 + elif state == "error": #an end state
3618 + error("in error state")
3619 + return durations
3620 +
3621 + else:
3622 + state = "error"
3623 + error("error: unknown state: '%s' reached in %s" % (state, s))
3624 +
3625 +def parseDtstart(contentline, allowSignatureMismatch=False):
3626 + """Convert a contentline's value into a date or date-time.
3627 +
3628 + A variety of clients don't serialize dates with the appropriate VALUE
3629 + parameter, so rather than failing on these (technically invalid) lines,
3630 + if allowSignatureMismatch is True, try to parse both varieties.
3631 +
3632 + """
3633 + tzinfo = getTzid(getattr(contentline, 'tzid_param', None))
3634 + valueParam = getattr(contentline, 'value_param', 'DATE-TIME').upper()
3635 + if valueParam == "DATE":
3636 + return stringToDate(contentline.value)
3637 + elif valueParam == "DATE-TIME":
3638 + try:
3639 + return stringToDateTime(contentline.value, tzinfo)
3640 + except:
3641 + if allowSignatureMismatch:
3642 + return stringToDate(contentline.value)
3643 + else:
3644 + raise
3645 +
3646 +def stringToPeriod(s, tzinfo=None):
3647 + values = string.split(s, "/")
3648 + start = stringToDateTime(values[0], tzinfo)
3649 + valEnd = values[1]
3650 + if isDuration(valEnd): #period-start = date-time "/" dur-value
3651 + delta = stringToDurations(valEnd)[0]
3652 + return (start, delta)
3653 + else:
3654 + return (start, stringToDateTime(valEnd, tzinfo) - start)
3655 +
3656 +
3657 +def getTransition(transitionTo, year, tzinfo):
3658 + """Return the datetime of the transition to/from DST, or None."""
3659 +
3660 + def firstTransition(iterDates, test):
3661 + """
3662 + Return the last date not matching test, or None if all tests matched.
3663 + """
3664 + success = None
3665 + for dt in iterDates:
3666 + if not test(dt):
3667 + success = dt
3668 + else:
3669 + if success is not None:
3670 + return success
3671 + return success # may be None
3672 +
3673 + def generateDates(year, month=None, day=None):
3674 + """Iterate over possible dates with unspecified values."""
3675 + months = range(1, 13)
3676 + days = range(1, 32)
3677 + hours = range(0, 24)
3678 + if month is None:
3679 + for month in months:
3680 + yield datetime.datetime(year, month, 1)
3681 + elif day is None:
3682 + for day in days:
3683 + try:
3684 + yield datetime.datetime(year, month, day)
3685 + except ValueError:
3686 + pass
3687 + else:
3688 + for hour in hours:
3689 + yield datetime.datetime(year, month, day, hour)
3690 +
3691 + assert transitionTo in ('daylight', 'standard')
3692 + if transitionTo == 'daylight':
3693 + def test(dt): return tzinfo.dst(dt) != zeroDelta
3694 + elif transitionTo == 'standard':
3695 + def test(dt): return tzinfo.dst(dt) == zeroDelta
3696 + newyear = datetime.datetime(year, 1, 1)
3697 + monthDt = firstTransition(generateDates(year), test)
3698 + if monthDt is None:
3699 + return newyear
3700 + elif monthDt.month == 12:
3701 + return None
3702 + else:
3703 + # there was a good transition somewhere in a non-December month
3704 + month = monthDt.month
3705 + day = firstTransition(generateDates(year, month), test).day
3706 + uncorrected = firstTransition(generateDates(year, month, day), test)
3707 + if transitionTo == 'standard':
3708 + # assuming tzinfo.dst returns a new offset for the first
3709 + # possible hour, we need to add one hour for the offset change
3710 + # and another hour because firstTransition returns the hour
3711 + # before the transition
3712 + return uncorrected + datetime.timedelta(hours=2)
3713 + else:
3714 + return uncorrected + datetime.timedelta(hours=1)
3715 +
3716 +def tzinfo_eq(tzinfo1, tzinfo2, startYear = 2000, endYear=2020):
3717 + """Compare offsets and DST transitions from startYear to endYear."""
3718 + if tzinfo1 == tzinfo2:
3719 + return True
3720 + elif tzinfo1 is None or tzinfo2 is None:
3721 + return False
3722 +
3723 + def dt_test(dt):
3724 + if dt is None:
3725 + return True
3726 + return tzinfo1.utcoffset(dt) == tzinfo2.utcoffset(dt)
3727 +
3728 + if not dt_test(datetime.datetime(startYear, 1, 1)):
3729 + return False
3730 + for year in xrange(startYear, endYear):
3731 + for transitionTo in 'daylight', 'standard':
3732 + t1=getTransition(transitionTo, year, tzinfo1)
3733 + t2=getTransition(transitionTo, year, tzinfo2)
3734 + if t1 != t2 or not dt_test(t1):
3735 + return False
3736 + return True
3737 +
3738 +
3739 +#------------------- Testing and running functions -----------------------------
3740 +if __name__ == '__main__':
3741 + import tests
3742 + tests._test()
3743 diff -r 2dde35b02026 MoinMoin/support/vobject/ics_diff.py
3744 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
3745 +++ b/MoinMoin/support/vobject/ics_diff.py Thu Dec 20 20:04:49 2007 +0100
3746 @@ -0,0 +1,219 @@
3747 +"""Compare VTODOs and VEVENTs in two iCalendar sources."""
3748 +from base import Component, getBehavior, newFromBehavior
3749 +
3750 +def getSortKey(component):
3751 + def getUID(component):
3752 + return component.getChildValue('uid', '')
3753 +
3754 + # it's not quite as simple as getUID, need to account for recurrenceID and
3755 + # sequence
3756 +
3757 + def getSequence(component):
3758 + sequence = component.getChildValue('sequence', 0)
3759 + return "%05d" % int(sequence)
3760 +
3761 + def getRecurrenceID(component):
3762 + recurrence_id = component.getChildValue('recurrence_id', None)
3763 + if recurrence_id is None:
3764 + return '0000-00-00'
3765 + else:
3766 + return recurrence_id.isoformat()
3767 +
3768 + return getUID(component) + getSequence(component) + getRecurrenceID(component)
3769 +
3770 +def sortByUID(components):
3771 + return sorted(components, key=getSortKey)
3772 +
3773 +def deleteExtraneous(component, ignore_dtstamp=False):
3774 + """
3775 + Recursively walk the component's children, deleting extraneous details like
3776 + X-VOBJ-ORIGINAL-TZID.
3777 + """
3778 + for comp in component.components():
3779 + deleteExtraneous(comp, ignore_dtstamp)
3780 + for line in component.lines():
3781 + if line.params.has_key('X-VOBJ-ORIGINAL-TZID'):
3782 + del line.params['X-VOBJ-ORIGINAL-TZID']
3783 + if ignore_dtstamp and hasattr(component, 'dtstamp_list'):
3784 + del component.dtstamp_list
3785 +
3786 +def diff(left, right):
3787 + """
3788 + Take two VCALENDAR components, compare VEVENTs and VTODOs in them,
3789 + return a list of object pairs containing just UID and the bits
3790 + that didn't match, using None for objects that weren't present in one
3791 + version or the other.
3792 +
3793 + When there are multiple ContentLines in one VEVENT, for instance many
3794 + DESCRIPTION lines, such lines original order is assumed to be
3795 + meaningful. Order is also preserved when comparing (the unlikely case
3796 + of) multiple parameters of the same type in a ContentLine
3797 +
3798 + """
3799 +
3800 + def processComponentLists(leftList, rightList):
3801 + output = []
3802 + rightIndex = 0
3803 + rightListSize = len(rightList)
3804 +
3805 + for comp in leftList:
3806 + if rightIndex >= rightListSize:
3807 + output.append((comp, None))
3808 + else:
3809 + leftKey = getSortKey(comp)
3810 + rightComp = rightList[rightIndex]
3811 + rightKey = getSortKey(rightComp)
3812 + while leftKey > rightKey:
3813 + output.append((None, rightComp))
3814 + rightIndex += 1
3815 + if rightIndex >= rightListSize:
3816 + output.append((comp, None))
3817 + break
3818 + else:
3819 + rightComp = rightList[rightIndex]
3820 + rightKey = getSortKey(rightComp)
3821 +
3822 + if leftKey < rightKey:
3823 + output.append((comp, None))
3824 + elif leftKey == rightKey:
3825 + rightIndex += 1
3826 + matchResult = processComponentPair(comp, rightComp)
3827 + if matchResult is not None:
3828 + output.append(matchResult)
3829 +
3830 + return output
3831 +
3832 + def newComponent(name, body):
3833 + if body is None:
3834 + return None
3835 + else:
3836 + c = Component(name)
3837 + c.behavior = getBehavior(name)
3838 + c.isNative = True
3839 + return c
3840 +
3841 + def processComponentPair(leftComp, rightComp):
3842 + """
3843 + Return None if a match, or a pair of components including UIDs and
3844 + any differing children.
3845 +
3846 + """
3847 + leftChildKeys = leftComp.contents.keys()
3848 + rightChildKeys = rightComp.contents.keys()
3849 +
3850 + differentContentLines = []
3851 + differentComponents = {}
3852 +
3853 + for key in leftChildKeys:
3854 + rightList = rightComp.contents.get(key, [])
3855 + if isinstance(leftComp.contents[key][0], Component):
3856 + compDifference = processComponentLists(leftComp.contents[key],
3857 + rightList)
3858 + if len(compDifference) > 0:
3859 + differentComponents[key] = compDifference
3860 +
3861 + elif leftComp.contents[key] != rightList:
3862 + differentContentLines.append((leftComp.contents[key],
3863 + rightList))
3864 +
3865 + for key in rightChildKeys:
3866 + if key not in leftChildKeys:
3867 + if isinstance(rightComp.contents[key][0], Component):
3868 + differentComponents[key] = ([], rightComp.contents[key])
3869 + else:
3870 + differentContentLines.append(([], rightComp.contents[key]))
3871 +
3872 + if len(differentContentLines) == 0 and len(differentComponents) == 0:
3873 + return None
3874 + else:
3875 + left = newFromBehavior(leftComp.name)
3876 + right = newFromBehavior(leftComp.name)
3877 + # add a UID, if one existed, despite the fact that they'll always be
3878 + # the same
3879 + uid = leftComp.getChildValue('uid')
3880 + if uid is not None:
3881 + left.add( 'uid').value = uid
3882 + right.add('uid').value = uid
3883 +
3884 + for name, childPairList in differentComponents.iteritems():
3885 + leftComponents, rightComponents = zip(*childPairList)
3886 + if len(leftComponents) > 0:
3887 + # filter out None
3888 + left.contents[name] = filter(None, leftComponents)
3889 + if len(rightComponents) > 0:
3890 + # filter out None
3891 + right.contents[name] = filter(None, rightComponents)
3892 +
3893 + for leftChildLine, rightChildLine in differentContentLines:
3894 + nonEmpty = leftChildLine or rightChildLine
3895 + name = nonEmpty[0].name
3896 + if leftChildLine is not None:
3897 + left.contents[name] = leftChildLine
3898 + if rightChildLine is not None:
3899 + right.contents[name] = rightChildLine
3900 +
3901 + return left, right
3902 +
3903 +
3904 + vevents = processComponentLists(sortByUID(getattr(left, 'vevent_list', [])),
3905 + sortByUID(getattr(right, 'vevent_list', [])))
3906 +
3907 + vtodos = processComponentLists(sortByUID(getattr(left, 'vtodo_list', [])),
3908 + sortByUID(getattr(right, 'vtodo_list', [])))
3909 +
3910 + return vevents + vtodos
3911 +
3912 +def prettyDiff(leftObj, rightObj):
3913 + for left, right in diff(leftObj, rightObj):
3914 + print "<<<<<<<<<<<<<<<"
3915 + if left is not None:
3916 + left.prettyPrint()
3917 + print "==============="
3918 + if right is not None:
3919 + right.prettyPrint()
3920 + print ">>>>>>>>>>>>>>>"
3921 + print
3922 +
3923 +
3924 +from optparse import OptionParser
3925 +import icalendar, base
3926 +import os
3927 +import codecs
3928 +
3929 +def main():
3930 + options, args = getOptions()
3931 + if args:
3932 + ignore_dtstamp = options.ignore
3933 + ics_file1, ics_file2 = args
3934 + cal1 = base.readOne(file(ics_file1))
3935 + cal2 = base.readOne(file(ics_file2))
3936 + deleteExtraneous(cal1, ignore_dtstamp=ignore_dtstamp)
3937 + deleteExtraneous(cal2, ignore_dtstamp=ignore_dtstamp)
3938 + prettyDiff(cal1, cal2)
3939 +
3940 +version = "0.1"
3941 +
3942 +def getOptions():
3943 + ##### Configuration options #####
3944 +
3945 + usage = "usage: %prog [options] ics_file1 ics_file2"
3946 + parser = OptionParser(usage=usage, version=version)
3947 + parser.set_description("ics_diff will print a comparison of two iCalendar files ")
3948 +
3949 + parser.add_option("-i", "--ignore-dtstamp", dest="ignore", action="store_true",
3950 + default=False, help="ignore DTSTAMP lines [default: False]")
3951 +
3952 + (cmdline_options, args) = parser.parse_args()
3953 + if len(args) < 2:
3954 + print "error: too few arguments given"
3955 + print
3956 + print parser.format_help()
3957 + return False, False
3958 +
3959 + return cmdline_options, args
3960 +
3961 +if __name__ == "__main__":
3962 + try:
3963 + main()
3964 + except KeyboardInterrupt:
3965 + print "Aborted"
3966 diff -r 2dde35b02026 MoinMoin/support/vobject/midnight.py
3967 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
3968 +++ b/MoinMoin/support/vobject/midnight.py Thu Dec 20 20:04:49 2007 +0100
3969 @@ -0,0 +1,59 @@
3970 +#!/usr/bin/env python
3971 +"""Rewrite VEVENT's DTSTART and DTEND if they're midnight to midnight"""
3972 +
3973 +from optparse import OptionParser
3974 +import icalendar, base, datetime
3975 +import os
3976 +import codecs
3977 +
3978 +midnight = datetime.time(0)
3979 +one_day = datetime.timedelta(1)
3980 +
3981 +def changeVeventTimes(vevent):
3982 + dtstart = vevent.getChildValue('dtstart')
3983 + dtend = vevent.getChildValue('dtend')
3984 + if isinstance(dtstart, datetime.datetime):
3985 + if dtend is None:
3986 + if dtstart.tzinfo is None and dtstart.time() == midnight:
3987 + vevent.dtstart.value = dtstart.date()
3988 + elif (isinstance(dtend, datetime.datetime) and
3989 + dtend.tzinfo is None and dtend.time() == midnight):
3990 + vevent.dtstart.value = dtstart.date()
3991 + vevent.dtend.value = dtend.date()
3992 +
3993 +
3994 +def main():
3995 + options, args = getOptions()
3996 + if args:
3997 + in_filename, out_filename = args
3998 + cal = base.readOne(file(in_filename))
3999 + for vevent in cal.vevent_list:
4000 + changeVeventTimes(vevent)
4001 + out_file = codecs.open(out_filename, "w", "utf-8")
4002 + cal.serialize(out_file)
4003 + out_file.close()
4004 +
4005 +
4006 +version = "0.1"
4007 +
4008 +def getOptions():
4009 + ##### Configuration options #####
4010 +
4011 + usage = "usage: %prog [options] in_file out_file"
4012 + parser = OptionParser(usage=usage, version=version)
4013 + parser.set_description("convert midnight events to all")
4014 +
4015 + (cmdline_options, args) = parser.parse_args()
4016 + if len(args) < 2:
4017 + print "error: too few arguments given"
4018 + print
4019 + print parser.format_help()
4020 + return False, False
4021 +
4022 + return cmdline_options, args
4023 +
4024 +if __name__ == "__main__":
4025 + try:
4026 + main()
4027 + except KeyboardInterrupt:
4028 + print "Aborted"
4029 diff -r 2dde35b02026 MoinMoin/support/vobject/to_pst.py
4030 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
4031 +++ b/MoinMoin/support/vobject/to_pst.py Thu Dec 20 20:04:49 2007 +0100
4032 @@ -0,0 +1,55 @@
4033 +#!/usr/bin/env python
4034 +"""Rewrite VEVENT's DTSTART and DTEND if they're midnight to midnight"""
4035 +
4036 +from optparse import OptionParser
4037 +import icalendar, base, datetime
4038 +import os
4039 +import codecs
4040 +import PyICU
4041 +
4042 +pst = PyICU.ICUtzinfo.getInstance('US/Pacific')
4043 +
4044 +def changeVeventTimes(vevent):
4045 + dtstart = vevent.getChildValue('dtstart')
4046 + dtend = vevent.getChildValue('dtend')
4047 + if isinstance(dtstart, datetime.datetime):
4048 + if dtstart.tzinfo is None:
4049 + vevent.dtstart.value = dtstart.replace(tzinfo=pst)
4050 + if dtend is not None and dtend.tzinfo is None:
4051 + vevent.dtend.value = dtend.replace(tzinfo=pst)
4052 +
4053 +def main():
4054 + options, args = getOptions()
4055 + if args:
4056 + in_filename, out_filename = args
4057 + cal = base.readOne(file(in_filename))
4058 + for vevent in cal.vevent_list:
4059 + changeVeventTimes(vevent)
4060 + out_file = codecs.open(out_filename, "w", "utf-8")
4061 + cal.serialize(out_file)
4062 + out_file.close()
4063 +
4064 +
4065 +version = "0.1"
4066 +
4067 +def getOptions():
4068 + ##### Configuration options #####
4069 +
4070 + usage = "usage: %prog [options] in_file out_file"
4071 + parser = OptionParser(usage=usage, version=version)
4072 + parser.set_description("convert midnight events to all")
4073 +
4074 + (cmdline_options, args) = parser.parse_args()
4075 + if len(args) < 2:
4076 + print "error: too few arguments given"
4077 + print
4078 + print parser.format_help()
4079 + return False, False
4080 +
4081 + return cmdline_options, args
4082 +
4083 +if __name__ == "__main__":
4084 + try:
4085 + main()
4086 + except KeyboardInterrupt:
4087 + print "Aborted"
4088 diff -r 2dde35b02026 MoinMoin/support/vobject/vcard.py
4089 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
4090 +++ b/MoinMoin/support/vobject/vcard.py Thu Dec 20 20:04:49 2007 +0100
4091 @@ -0,0 +1,289 @@
4092 +"""Definitions and behavior for vCard 3.0"""
4093 +
4094 +import behavior
4095 +import itertools
4096 +
4097 +from base import VObjectError, NativeError, ValidateError, ParseError, \
4098 + VBase, Component, ContentLine, logger, defaultSerialize, \
4099 + registerBehavior, backslashEscape, ascii
4100 +from icalendar import stringToTextValues
4101 +
4102 +#------------------------ vCard structs ----------------------------------------
4103 +
4104 +class Name(object):
4105 + def __init__(self, family = '', given = '', additional = '', prefix = '',
4106 + suffix = ''):
4107 + """Each name attribute can be a string or a list of strings."""
4108 + self.family = family
4109 + self.given = given
4110 + self.additional = additional
4111 + self.prefix = prefix
4112 + self.suffix = suffix
4113 +
4114 + @staticmethod
4115 + def toString(val):
4116 + """Turn a string or array value into a string."""
4117 + if type(val) in (list, tuple):
4118 + return ' '.join(val)
4119 + return val
4120 +
4121 + def __str__(self):
4122 + eng_order = ('prefix', 'given', 'additional', 'family', 'suffix')
4123 + out = ' '.join(self.toString(getattr(self, val)) for val in eng_order)
4124 + return ascii(out)
4125 +
4126 + def __repr__(self):
4127 + return "<Name: %s>" % self.__str__()
4128 +
4129 + def __eq__(self, other):
4130 + try:
4131 + return (self.family == other.family and
4132 + self.given == other.given and
4133 + self.additional == other.additional and
4134 + self.prefix == other.prefix and
4135 + self.suffix == other.suffix)
4136 + except:
4137 + return False
4138 +
4139 +class Address(object):
4140 + def __init__(self, street = '', city = '', region = '', code = '',
4141 + country = '', box = '', extended = ''):
4142 + """Each name attribute can be a string or a list of strings."""
4143 + self.box = box
4144 + self.extended = extended
4145 + self.street = street
4146 + self.city = city
4147 + self.region = region
4148 + self.code = code
4149 + self.country = country
4150 +
4151 + @staticmethod
4152 + def toString(val, join_char='\n'):
4153 + """Turn a string or array value into a string."""
4154 + if type(val) in (list, tuple):
4155 + return join_char.join(val)
4156 + return val
4157 +
4158 + lines = ('box', 'extended', 'street')
4159 + one_line = ('city', 'region', 'code')
4160 +
4161 + def __str__(self):
4162 + lines = '\n'.join(self.toString(getattr(self, val)) for val in self.lines if getattr(self, val))
4163 + one_line = tuple(self.toString(getattr(self, val), ' ') for val in self.one_line)
4164 + lines += "\n%s, %s %s" % one_line
4165 + if self.country:
4166 + lines += '\n' + self.toString(self.country)
4167 + return ascii(lines)
4168 +
4169 + def __repr__(self):
4170 + return "<Address: %s>" % repr(str(self))[1:-1]
4171 +
4172 + def __eq__(self, other):
4173 + try:
4174 + return (self.box == other.box and
4175 + self.extended == other.extended and
4176 + self.street == other.street and
4177 + self.city == other.city and
4178 + self.region == other.region and
4179 + self.code == other.code and
4180 + self.country == other.country)
4181 + except:
4182 + False
4183 +
4184 +
4185 +#------------------------ Registered Behavior subclasses -----------------------
4186 +
4187 +class VCardTextBehavior(behavior.Behavior):
4188 + """Provide backslash escape encoding/decoding for single valued properties.
4189 +
4190 + TextBehavior also deals with base64 encoding if the ENCODING parameter is
4191 + explicitly set to BASE64.
4192 +
4193 + """
4194 + allowGroup = True
4195 + base64string = 'B'
4196 +
4197 + @classmethod
4198 + def decode(cls, line):
4199 + """Remove backslash escaping from line.valueDecode line, either to remove
4200 + backslash espacing, or to decode base64 encoding. The content line should
4201 + contain a ENCODING=b for base64 encoding, but Apple Addressbook seems to
4202 + export a singleton parameter of 'BASE64', which does not match the 3.0
4203 + vCard spec. If we encouter that, then we transform the parameter to
4204 + ENCODING=b"""
4205 + if line.encoded:
4206 + if 'BASE64' in line.singletonparams:
4207 + line.singletonparams.remove('BASE64')
4208 + line.encoding_param = cls.base64string
4209 + encoding = getattr(line, 'encoding_param', None)
4210 + if encoding:
4211 + line.value = line.value.decode('base64')
4212 + else:
4213 + line.value = stringToTextValues(line.value)[0]
4214 + line.encoded=False
4215 +
4216 + @classmethod
4217 + def encode(cls, line):
4218 + """Backslash escape line.value."""
4219 + if not line.encoded:
4220 + encoding = getattr(line, 'encoding_param', None)
4221 + if encoding and encoding.upper() == cls.base64string:
4222 + line.value = line.value.encode('base64').replace('\n', '')
4223 + else:
4224 + line.value = backslashEscape(line.value)
4225 + line.encoded=True
4226 +
4227 +
4228 +class VCardBehavior(behavior.Behavior):
4229 + allowGroup = True
4230 + defaultBehavior = VCardTextBehavior
4231 +
4232 +class VCard3_0(VCardBehavior):
4233 + """vCard 3.0 behavior."""
4234 + name = 'VCARD'
4235 + description = 'vCard 3.0, defined in rfc2426'
4236 + versionString = '3.0'
4237 + isComponent = True
4238 + sortFirst = ('version', 'prodid', 'uid')
4239 + knownChildren = {'N': (1, 1, None),#min, max, behaviorRegistry id
4240 + 'FN': (1, 1, None),
4241 + 'VERSION': (1, 1, None),#required, auto-generated
4242 + 'PRODID': (0, 1, None),
4243 + 'LABEL': (0, None, None),
4244 + 'UID': (0, None, None),
4245 + 'ADR': (0, None, None),
4246 + 'ORG': (0, None, None),
4247 + 'PHOTO': (0, None, None),
4248 + 'CATEGORIES':(0, None, None)
4249 + }
4250 +
4251 + @classmethod
4252 + def generateImplicitParameters(cls, obj):
4253 + """Create PRODID, VERSION, and VTIMEZONEs if needed.
4254 +
4255 + VTIMEZONEs will need to exist whenever TZID parameters exist or when
4256 + datetimes with tzinfo exist.
4257 +
4258 + """
4259 + if not hasattr(obj, 'version'):
4260 + obj.add(ContentLine('VERSION', [], cls.versionString))
4261 +registerBehavior(VCard3_0, default=True)
4262 +
4263 +class FN(VCardTextBehavior):
4264 + name = "FN"
4265 + description = 'Formatted name'
4266 +registerBehavior(FN)
4267 +
4268 +class Label(VCardTextBehavior):
4269 + name = "Label"
4270 + description = 'Formatted address'
4271 +registerBehavior(FN)
4272 +
4273 +class Photo(VCardTextBehavior):
4274 + name = "Photo"
4275 + description = 'Photograph'
4276 + @classmethod
4277 + def valueRepr( cls, line ):
4278 + return " (BINARY PHOTO DATA at 0x%s) " % id( line.value )
4279 +
4280 +registerBehavior(Photo)
4281 +
4282 +def toListOrString(string):
4283 + stringList = stringToTextValues(string)
4284 + if len(stringList) == 1:
4285 + return stringList[0]
4286 + else:
4287 + return stringList
4288 +
4289 +def splitFields(string):
4290 + """Return a list of strings or lists from a Name or Address."""
4291 + return [toListOrString(i) for i in
4292 + stringToTextValues(string, listSeparator=';', charList=';')]
4293 +
4294 +def toList(stringOrList):
4295 + if isinstance(stringOrList, basestring):
4296 + return [stringOrList]
4297 + return stringOrList
4298 +
4299 +def serializeFields(obj, order=None):
4300 + """Turn an object's fields into a ';' and ',' seperated string.
4301 +
4302 + If order is None, obj should be a list, backslash escape each field and
4303 + return a ';' separated string.
4304 + """
4305 + fields = []
4306 + if order is None:
4307 + fields = [backslashEscape(val) for val in obj]
4308 + else:
4309 + for field in order:
4310 + escapedValueList = [backslashEscape(val) for val in
4311 + toList(getattr(obj, field))]
4312 + fields.append(','.join(escapedValueList))
4313 + return ';'.join(fields)
4314 +
4315 +NAME_ORDER = ('family', 'given', 'additional', 'prefix', 'suffix')
4316 +
4317 +class NameBehavior(VCardBehavior):
4318 + """A structured name."""
4319 + hasNative = True
4320 +
4321 + @staticmethod
4322 + def transformToNative(obj):
4323 + """Turn obj.value into a Name."""
4324 + if obj.isNative: return obj
4325 + obj.isNative = True
4326 + obj.value = Name(**dict(zip(NAME_ORDER, splitFields(obj.value))))
4327 + return obj
4328 +
4329 + @staticmethod
4330 + def transformFromNative(obj):
4331 + """Replace the Name in obj.value with a string."""
4332 + obj.isNative = False
4333 + obj.value = serializeFields(obj.value, NAME_ORDER)
4334 + return obj
4335 +registerBehavior(NameBehavior, 'N')
4336 +
4337 +ADDRESS_ORDER = ('box', 'extended', 'street', 'city', 'region', 'code',
4338 + 'country')
4339 +
4340 +class AddressBehavior(VCardBehavior):
4341 + """A structured address."""
4342 + hasNative = True
4343 +
4344 + @staticmethod
4345 + def transformToNative(obj):
4346 + """Turn obj.value into an Address."""
4347 + if obj.isNative: return obj
4348 + obj.isNative = True
4349 + obj.value = Address(**dict(zip(ADDRESS_ORDER, splitFields(obj.value))))
4350 + return obj
4351 +
4352 + @staticmethod
4353 + def transformFromNative(obj):
4354 + """Replace the Address in obj.value with a string."""
4355 + obj.isNative = False
4356 + obj.value = serializeFields(obj.value, ADDRESS_ORDER)
4357 + return obj
4358 +registerBehavior(AddressBehavior, 'ADR')
4359 +
4360 +class OrgBehavior(VCardBehavior):
4361 + """A list of organization values and sub-organization values."""
4362 + hasNative = True
4363 +
4364 + @staticmethod
4365 + def transformToNative(obj):
4366 + """Turn obj.value into a list."""
4367 + if obj.isNative: return obj
4368 + obj.isNative = True
4369 + obj.value = splitFields(obj.value)
4370 + return obj
4371 +
4372 + @staticmethod
4373 + def transformFromNative(obj):
4374 + """Replace the list in obj.value with a string."""
4375 + if not obj.isNative: return obj
4376 + obj.isNative = False
4377 + obj.value = serializeFields(obj.value)
4378 + return obj
4379 +registerBehavior(OrgBehavior, 'ORG')
4380 +
4381 diff -r 2dde35b02026 MoinMoin/support/vobject/win32tz.py
4382 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
4383 +++ b/MoinMoin/support/vobject/win32tz.py Thu Dec 20 20:04:49 2007 +0100
4384 @@ -0,0 +1,156 @@
4385 +import _winreg
4386 +import struct
4387 +import datetime
4388 +
4389 +handle=_winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE)
4390 +tzparent=_winreg.OpenKey(handle,
4391 + "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones")
4392 +parentsize=_winreg.QueryInfoKey(tzparent)[0]
4393 +
4394 +localkey=_winreg.OpenKey(handle,
4395 + "SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation")
4396 +WEEKS=datetime.timedelta(7)
4397 +
4398 +def list_timezones():
4399 + """Return a list of all time zones known to the system."""
4400 + l=[]
4401 + for i in xrange(parentsize):
4402 + l.append(_winreg.EnumKey(tzparent, i))
4403 + return l
4404 +
4405 +class win32tz(datetime.tzinfo):
4406 + """tzinfo class based on win32's timezones available in the registry.
4407 +
4408 + >>> local = win32tz('Central Standard Time')
4409 + >>> oct1 = datetime.datetime(month=10, year=2004, day=1, tzinfo=local)
4410 + >>> dec1 = datetime.datetime(month=12, year=2004, day=1, tzinfo=local)
4411 + >>> oct1.dst()
4412 + datetime.timedelta(0, 3600)
4413 + >>> dec1.dst()
4414 + datetime.timedelta(0)
4415 + >>> braz = win32tz('E. South America Standard Time')
4416 + >>> braz.dst(oct1)
4417 + datetime.timedelta(0)
4418 + >>> braz.dst(dec1)
4419 + datetime.timedelta(0, 3600)
4420 +
4421 + """
4422 + def __init__(self, name):
4423 + self.data=win32tz_data(name)
4424 +
4425 + def utcoffset(self, dt):
4426 + if self._isdst(dt):
4427 + return datetime.timedelta(minutes=self.data.dstoffset)
4428 + else:
4429 + return datetime.timedelta(minutes=self.data.stdoffset)
4430 +
4431 + def dst(self, dt):
4432 + if self._isdst(dt):
4433 + minutes = self.data.dstoffset - self.data.stdoffset
4434 + return datetime.timedelta(minutes=minutes)
4435 + else:
4436 + return datetime.timedelta(0)
4437 +
4438 + def tzname(self, dt):
4439 + if self._isdst(dt): return self.data.dstname
4440 + else: return self.data.stdname
4441 +
4442 + def _isdst(self, dt):
4443 + dat=self.data
4444 + dston = pickNthWeekday(dt.year, dat.dstmonth, dat.dstdayofweek,
4445 + dat.dsthour, dat.dstminute, dat.dstweeknumber)
4446 + dstoff = pickNthWeekday(dt.year, dat.stdmonth, dat.stddayofweek,
4447 + dat.stdhour, dat.stdminute, dat.stdweeknumber)
4448 + if dston < dstoff:
4449 + if dston <= dt.replace(tzinfo=None) < dstoff: return True
4450 + else: return False
4451 + else:
4452 + if dstoff <= dt.replace(tzinfo=None) < dston: return False
4453 + else: return True
4454 +
4455 + def __repr__(self):
4456 + return "<win32tz - %s>" % self.data.display
4457 +
4458 +def pickNthWeekday(year, month, dayofweek, hour, minute, whichweek):
4459 + """dayofweek == 0 means Sunday, whichweek > 4 means last instance"""
4460 + first = datetime.datetime(year=year, month=month, hour=hour, minute=minute,
4461 + day=1)
4462 + weekdayone = first.replace(day=((dayofweek - first.isoweekday()) % 7 + 1))
4463 + for n in xrange(whichweek - 1, -1, -1):
4464 + dt=weekdayone + n * WEEKS
4465 + if dt.month == month: return dt
4466 +
4467 +
4468 +class win32tz_data(object):
4469 + """Read a registry key for a timezone, expose its contents."""
4470 +
4471 + def __init__(self, path):
4472 + """Load path, or if path is empty, load local time."""
4473 + if path:
4474 + keydict=valuesToDict(_winreg.OpenKey(tzparent, path))
4475 + self.display = keydict['Display']
4476 + self.dstname = keydict['Dlt']
4477 + self.stdname = keydict['Std']
4478 +
4479 + #see http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
4480 + tup = struct.unpack('=3l16h', keydict['TZI'])
4481 + self.stdoffset = -tup[0]-tup[1] #Bias + StandardBias * -1
4482 + self.dstoffset = self.stdoffset - tup[2] # + DaylightBias * -1
4483 +
4484 + offset=3
4485 + self.stdmonth = tup[1 + offset]
4486 + self.stddayofweek = tup[2 + offset] #Sunday=0
4487 + self.stdweeknumber = tup[3 + offset] #Last = 5
4488 + self.stdhour = tup[4 + offset]
4489 + self.stdminute = tup[5 + offset]
4490 +
4491 + offset=11
4492 + self.dstmonth = tup[1 + offset]
4493 + self.dstdayofweek = tup[2 + offset] #Sunday=0
4494 + self.dstweeknumber = tup[3 + offset] #Last = 5
4495 + self.dsthour = tup[4 + offset]
4496 + self.dstminute = tup[5 + offset]
4497 +
4498 + else:
4499 + keydict=valuesToDict(localkey)
4500 +
4501 + self.stdname = keydict['StandardName']
4502 + self.dstname = keydict['DaylightName']
4503 +
4504 + sourcekey=_winreg.OpenKey(tzparent, self.stdname)
4505 + self.display = valuesToDict(sourcekey)['Display']
4506 +
4507 + self.stdoffset = -keydict['Bias']-keydict['StandardBias']
4508 + self.dstoffset = self.stdoffset - keydict['DaylightBias']
4509 +
4510 + #see http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
4511 + tup = struct.unpack('=8h', keydict['StandardStart'])
4512 +
4513 + offset=0
4514 + self.stdmonth = tup[1 + offset]
4515 + self.stddayofweek = tup[2 + offset] #Sunday=0
4516 + self.stdweeknumber = tup[3 + offset] #Last = 5
4517 + self.stdhour = tup[4 + offset]
4518 + self.stdminute = tup[5 + offset]
4519 +
4520 + tup = struct.unpack('=8h', keydict['DaylightStart'])
4521 + self.dstmonth = tup[1 + offset]
4522 + self.dstdayofweek = tup[2 + offset] #Sunday=0
4523 + self.dstweeknumber = tup[3 + offset] #Last = 5
4524 + self.dsthour = tup[4 + offset]
4525 + self.dstminute = tup[5 + offset]
4526 +
4527 +def valuesToDict(key):
4528 + """Convert a registry key's values to a dictionary."""
4529 + dict={}
4530 + size=_winreg.QueryInfoKey(key)[1]
4531 + for i in xrange(size):
4532 + dict[_winreg.EnumValue(key, i)[0]]=_winreg.EnumValue(key, i)[1]
4533 + return dict
4534 +
4535 +def _test():
4536 + import win32tz, doctest
4537 + doctest.testmod(win32tz, verbose=0)
4538 +
4539 +if __name__ == '__main__':
4540 + _test()
4541 \ No newline at end of file
4542 diff -r 2dde35b02026 MoinMoin/wikiutil.py
4543 --- a/MoinMoin/wikiutil.py Wed Nov 28 10:43:48 2007 +0100
4544 +++ b/MoinMoin/wikiutil.py Thu Dec 20 20:04:49 2007 +0100
4545 @@ -921,6 +921,8 @@ MIMETYPES_MORE = {
4546 '.ots': 'application/vnd.oasis.opendocument.spreadsheet-template',
4547 '.otp': 'application/vnd.oasis.opendocument.presentation-template',
4548 '.otg': 'application/vnd.oasis.opendocument.graphics-template',
4549 + '.hcf': 'text/x-hcard',
4550 + '.hcard': 'text/x-hcard'
4551 }
4552 [mimetypes.add_type(mimetype, ext, True) for ext, mimetype in MIMETYPES_MORE.items()]
Attached Files
To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.You are not allowed to attach a file to this page.