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.
  • [get | view] (2008-02-10 21:25:23, 169.7 KB) [[attachment:fullpatch.diff]]
  • [get | view] (2008-02-10 21:24:58, 4.4 KB) [[attachment:test_text_x_hcard.py]]
  • [get | view] (2008-02-10 21:25:03, 1.7 KB) [[attachment:text_vcard.py]]
  • [get | view] (2008-02-10 21:25:09, 7.7 KB) [[attachment:text_x_hcard.py]]
 All files | Selected Files: delete move to page copy to page

You are not allowed to attach a file to this page.