Back to index

plone3  3.1.7
cal.py
Go to the documentation of this file.
00001 # -*- coding: latin-1 -*-
00002 
00003 """
00004 
00005 Calendar is a dictionary like Python object that can render itself as VCAL
00006 files according to rfc2445.
00007 
00008 These are the defined components.
00009 
00010 """
00011 
00012 # from python
00013 from types import ListType, TupleType
00014 SequenceTypes = (ListType, TupleType)
00015 import re
00016 
00017 # from this package
00018 from icalendar.caselessdict import CaselessDict
00019 from icalendar.parser import Contentlines, Contentline, Parameters
00020 from icalendar.parser import q_split, q_join
00021 from icalendar.prop import TypesFactory
00022 
00023 
00024 ######################################
00025 # The component factory
00026 
00027 class ComponentFactory(CaselessDict):
00028     """
00029     All components defined in rfc 2445 are registered in this factory class. To
00030     get a component you can use it like this.
00031 
00032     >>> factory = ComponentFactory()
00033     >>> component = factory['VEVENT']
00034     >>> event = component(dtstart='19700101')
00035     >>> event.as_string()
00036     'BEGIN:VEVENT\\r\\nDTSTART:19700101\\r\\nEND:VEVENT\\r\\n'
00037 
00038     >>> factory.get('VCALENDAR', Component)
00039     <class 'icalendar.cal.Calendar'>
00040     """
00041 
00042     def __init__(self, *args, **kwargs):
00043         "Set keys to upper for initial dict"
00044         CaselessDict.__init__(self, *args, **kwargs)
00045         self['VEVENT'] = Event
00046         self['VTODO'] = Todo
00047         self['VJOURNAL'] = Journal
00048         self['VFREEBUSY'] = FreeBusy
00049         self['VTIMEZONE'] = Timezone
00050         self['VALARM'] = Alarm
00051         self['VCALENDAR'] = Calendar
00052 
00053 
00054 # These Properties have multiple property values inlined in one propertyline
00055 # seperated by comma. Use CaselessDict as simple caseless set.
00056 INLINE = CaselessDict(
00057     [(cat, 1) for cat in ('CATEGORIES', 'RESOURCES', 'FREEBUSY')]
00058 )
00059 
00060 _marker = []
00061 
00062 class Component(CaselessDict):
00063     """
00064     Component is the base object for calendar, Event and the other components
00065     defined in RFC 2445. normally you will not use this class directy, but
00066     rather one of the subclasses.
00067 
00068     A component is like a dictionary with extra methods and attributes.
00069     >>> c = Component()
00070     >>> c.name = 'VCALENDAR'
00071 
00072     Every key defines a property. A property can consist of either a single
00073     item. This can be set with a single value
00074     >>> c['prodid'] = '-//max m//icalendar.mxm.dk/'
00075     >>> c
00076     VCALENDAR({'PRODID': '-//max m//icalendar.mxm.dk/'})
00077 
00078     or with a list
00079     >>> c['ATTENDEE'] = ['Max M', 'Rasmussen']
00080 
00081     if you use the add method you don't have to considder if a value is a list
00082     or not.
00083     >>> c = Component()
00084     >>> c.name = 'VEVENT'
00085     >>> c.add('attendee', 'maxm@mxm.dk')
00086     >>> c.add('attendee', 'test@example.dk')
00087     >>> c
00088     VEVENT({'ATTENDEE': [vCalAddress('maxm@mxm.dk'), vCalAddress('test@example.dk')]})
00089 
00090     You can get the values back directly
00091     >>> c.add('prodid', '-//my product//')
00092     >>> c['prodid']
00093     vText(u'-//my product//')
00094 
00095     or decoded to a python type
00096     >>> c.decoded('prodid')
00097     u'-//my product//'
00098 
00099     With default values for non existing properties
00100     >>> c.decoded('version', 'No Version')
00101     'No Version'
00102 
00103     The component can render itself in the RFC 2445 format.
00104     >>> c = Component()
00105     >>> c.name = 'VCALENDAR'
00106     >>> c.add('attendee', 'Max M')
00107     >>> c.as_string()
00108     'BEGIN:VCALENDAR\\r\\nATTENDEE:Max M\\r\\nEND:VCALENDAR\\r\\n'
00109 
00110     >>> from icalendar.prop import vDatetime
00111 
00112     Components can be nested, so You can add a subcompont. Eg a calendar holds events.
00113     >>> e = Component(summary='A brief history of time')
00114     >>> e.name = 'VEVENT'
00115     >>> e.add('dtend', '20000102T000000', encode=0)
00116     >>> e.add('dtstart', '20000101T000000', encode=0)
00117     >>> e.as_string()
00118     'BEGIN:VEVENT\\r\\nDTEND:20000102T000000\\r\\nDTSTART:20000101T000000\\r\\nSUMMARY:A brief history of time\\r\\nEND:VEVENT\\r\\n'
00119 
00120     >>> c.add_component(e)
00121     >>> c.subcomponents
00122     [VEVENT({'DTEND': '20000102T000000', 'DTSTART': '20000101T000000', 'SUMMARY': 'A brief history of time'})]
00123 
00124     We can walk over nested componentes with the walk method.
00125     >>> [i.name for i in c.walk()]
00126     ['VCALENDAR', 'VEVENT']
00127 
00128     We can also just walk over specific component types, by filtering them on
00129     their name.
00130     >>> [i.name for i in c.walk('VEVENT')]
00131     ['VEVENT']
00132 
00133     >>> [i['dtstart'] for i in c.walk('VEVENT')]
00134     ['20000101T000000']
00135 
00136     INLINE properties have their values on one property line. Note the double
00137     quoting of the value with a colon in it.
00138     >>> c = Calendar()
00139     >>> c['resources'] = 'Chair, Table, "Room: 42"'
00140     >>> c
00141     VCALENDAR({'RESOURCES': 'Chair, Table, "Room: 42"'})
00142 
00143     >>> c.as_string()
00144     'BEGIN:VCALENDAR\\r\\nRESOURCES:Chair, Table, "Room: 42"\\r\\nEND:VCALENDAR\\r\\n'
00145 
00146     The inline values must be handled by the get_inline() and set_inline()
00147     methods.
00148 
00149     >>> c.get_inline('resources', decode=0)
00150     ['Chair', 'Table', 'Room: 42']
00151 
00152     These can also be decoded
00153     >>> c.get_inline('resources', decode=1)
00154     [u'Chair', u'Table', u'Room: 42']
00155 
00156     You can set them directly
00157     >>> c.set_inline('resources', ['A', 'List', 'of', 'some, recources'], encode=1)
00158     >>> c['resources']
00159     'A,List,of,"some, recources"'
00160 
00161     and back again
00162     >>> c.get_inline('resources', decode=0)
00163     ['A', 'List', 'of', 'some, recources']
00164 
00165     >>> c['freebusy'] = '19970308T160000Z/PT3H,19970308T200000Z/PT1H,19970308T230000Z/19970309T000000Z'
00166     >>> c.get_inline('freebusy', decode=0)
00167     ['19970308T160000Z/PT3H', '19970308T200000Z/PT1H', '19970308T230000Z/19970309T000000Z']
00168 
00169     >>> freebusy = c.get_inline('freebusy', decode=1)
00170     >>> type(freebusy[0][0]), type(freebusy[0][1])
00171     (<type 'datetime.datetime'>, <type 'datetime.timedelta'>)
00172     """
00173 
00174     name = ''       # must be defined in each component
00175     required = ()   # These properties are required
00176     singletons = () # These properties must only appear once
00177     multiple = ()   # may occur more than once
00178     exclusive = ()  # These properties are mutually exclusive
00179     inclusive = ()  # if any occurs the other(s) MUST occur ('duration', 'repeat')
00180 
00181     def __init__(self, *args, **kwargs):
00182         "Set keys to upper for initial dict"
00183         CaselessDict.__init__(self, *args, **kwargs)
00184         # set parameters here for properties that use non-default values
00185         self.subcomponents = [] # Components can be nested.
00186 
00187 
00188 #    def non_complience(self, warnings=0):
00189 #        """
00190 #        not implemented yet!
00191 #        Returns a dict describing non compliant properties, if any.
00192 #        If warnings is true it also returns warnings.
00193 #
00194 #        If the parser is too strict it might prevent parsing erroneous but
00195 #        otherwise compliant properties. So the parser is pretty lax, but it is
00196 #        possible to test for non-complience by calling this method.
00197 #        """
00198 #        nc = {}
00199 #        if not getattr(self, 'name', ''):
00200 #            nc['name'] = {'type':'ERROR', 'description':'Name is not defined'}
00201 #        return nc
00202 
00203 
00204     #############################
00205     # handling of property values
00206 
00207     def _encode(self, name, value, cond=1):
00208         # internal, for conditional convertion of values.
00209         if cond:
00210             klass = types_factory.for_property(name)
00211             return klass(value)
00212         return value
00213 
00214 
00215     def set(self, name, value, encode=1):
00216         if type(value) == ListType:
00217             self[name] = [self._encode(name, v, encode) for v in value]
00218         else:
00219             self[name] = self._encode(name, value, encode)
00220 
00221 
00222     def add(self, name, value, encode=1):
00223         "If property exists append, else create and set it"
00224         if name in self:
00225             oldval = self[name]
00226             value = self._encode(name, value, encode)
00227             if type(oldval) == ListType:
00228                 oldval.append(value)
00229             else:
00230                 self.set(name, [oldval, value], encode=0)
00231         else:
00232             self.set(name, value, encode)
00233 
00234 
00235     def _decode(self, name, value):
00236         # internal for decoding property values
00237         decoded = types_factory.from_ical(name, value)
00238         return decoded
00239 
00240 
00241     def decoded(self, name, default=_marker):
00242         "Returns decoded value of property"
00243         if name in self:
00244             value = self[name]
00245             if type(value) == ListType:
00246                 return [self._decode(name, v) for v in value]
00247             return self._decode(name, value)
00248         else:
00249             if default is _marker:
00250                 raise KeyError, name
00251             else:
00252                 return default
00253 
00254 
00255     ########################################################################
00256     # Inline values. A few properties have multiple values inlined in in one
00257     # property line. These methods are used for splitting and joining these.
00258 
00259     def get_inline(self, name, decode=1):
00260         """
00261         Returns a list of values (split on comma).
00262         """
00263         vals = [v.strip('" ') for v in q_split(self[name])]
00264         if decode:
00265             return [self._decode(name, val) for val in vals]
00266         return vals
00267 
00268 
00269     def set_inline(self, name, values, encode=1):
00270         """
00271         Converts a list of values into comma seperated string and sets value to
00272         that.
00273         """
00274         if encode:
00275             values = [self._encode(name, value, 1) for value in values]
00276         self[name] = types_factory['inline'](q_join(values))
00277 
00278 
00279     #########################
00280     # Handling of components
00281 
00282     def add_component(self, component):
00283         "add a subcomponent to this component"
00284         self.subcomponents.append(component)
00285 
00286 
00287     def _walk(self, name):
00288         # private!
00289         result = []
00290         if name is None or self.name == name:
00291             result.append(self)
00292         for subcomponent in self.subcomponents:
00293             result += subcomponent._walk(name)
00294         return result
00295 
00296 
00297     def walk(self, name=None):
00298         """
00299         Recursively traverses component and subcomponents. Returns sequence of
00300         same. If name is passed, only components with name will be returned.
00301         """
00302         if not name is None:
00303             name = name.upper()
00304         return self._walk(name)
00305 
00306     #####################
00307     # Generation
00308 
00309     def property_items(self):
00310         """
00311         Returns properties in this component and subcomponents as:
00312         [(name, value), ...]
00313         """
00314         vText = types_factory['text']
00315         properties = [('BEGIN', vText(self.name).ical())]
00316         property_names = self.keys()
00317         property_names.sort()
00318         for name in property_names:
00319             values = self[name]
00320             if type(values) == ListType:
00321                 # normally one property is one line
00322                 for value in values:
00323                     properties.append((name, value))
00324             else:
00325                 properties.append((name, values))
00326         # recursion is fun!
00327         for subcomponent in self.subcomponents:
00328             properties += subcomponent.property_items()
00329         properties.append(('END', vText(self.name).ical()))
00330         return properties
00331 
00332 
00333     def from_string(st, multiple=False):
00334         """
00335         Populates the component recursively from a string
00336         """
00337         stack = [] # a stack of components
00338         comps = []
00339         for line in Contentlines.from_string(st): # raw parsing
00340             if not line:
00341                 continue
00342             name, params, vals = line.parts()
00343             uname = name.upper()
00344             # check for start of component
00345             if uname == 'BEGIN':
00346                 # try and create one of the components defined in the spec,
00347                 # otherwise get a general Components for robustness.
00348                 component_name = vals.upper()
00349                 component_class = component_factory.get(component_name, Component)
00350                 component = component_class()
00351                 if not getattr(component, 'name', ''): # for undefined components
00352                     component.name = component_name
00353                 stack.append(component)
00354             # check for end of event
00355             elif uname == 'END':
00356                 # we are done adding properties to this component
00357                 # so pop it from the stack and add it to the new top.
00358                 component = stack.pop()
00359                 if not stack: # we are at the end
00360                     comps.append(component)
00361                 else:
00362                     stack[-1].add_component(component)
00363             # we are adding properties to the current top of the stack
00364             else:
00365                 factory = types_factory.for_property(name)
00366                 vals = factory(factory.from_ical(vals))
00367                 vals.params = params
00368                 stack[-1].add(name, vals, encode=0)
00369         if multiple:
00370             return comps
00371         if not len(comps) == 1:
00372             raise ValueError('Found multiple components where '
00373                              'only one is allowed')
00374         return comps[0]
00375     from_string = staticmethod(from_string)
00376 
00377 
00378     def __repr__(self):
00379         return '%s(' % self.name + dict.__repr__(self) + ')'
00380 
00381 #    def content_line(self, name):
00382 #        "Returns property as content line"
00383 #        value = self[name]
00384 #        params = getattr(value, 'params', Parameters())
00385 #        return Contentline.from_parts((name, params, value))
00386 
00387     def content_lines(self):
00388         "Converts the Component and subcomponents into content lines"
00389         contentlines = Contentlines()
00390         for name, values in self.property_items():
00391             params = getattr(values, 'params', Parameters())
00392             contentlines.append(Contentline.from_parts((name, params, values)))
00393         contentlines.append('') # remember the empty string in the end
00394         return contentlines
00395 
00396 
00397     def as_string(self):
00398         return str(self.content_lines())
00399 
00400 
00401     def __str__(self):
00402         "Returns rendered iCalendar"
00403         return self.as_string()
00404 
00405 
00406 
00407 #######################################
00408 # components defined in RFC 2445
00409 
00410 
00411 class Event(Component):
00412 
00413     name = 'VEVENT'
00414 
00415     required = ('UID',)
00416     singletons = (
00417         'CLASS', 'CREATED', 'DESCRIPTION', 'DTSTART', 'GEO',
00418         'LAST-MOD', 'LOCATION', 'ORGANIZER', 'PRIORITY', 'DTSTAMP', 'SEQUENCE',
00419         'STATUS', 'SUMMARY', 'TRANSP', 'URL', 'RECURID', 'DTEND', 'DURATION',
00420         'DTSTART',
00421     )
00422     exclusive = ('DTEND', 'DURATION', )
00423     multiple = (
00424         'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT','CONTACT', 'EXDATE',
00425         'EXRULE', 'RSTATUS', 'RELATED', 'RESOURCES', 'RDATE', 'RRULE'
00426     )
00427 
00428 
00429 
00430 class Todo(Component):
00431 
00432     name = 'VTODO'
00433 
00434     required = ('UID',)
00435     singletons = (
00436         'CLASS', 'COMPLETED', 'CREATED', 'DESCRIPTION', 'DTSTAMP', 'DTSTART',
00437         'GEO', 'LAST-MOD', 'LOCATION', 'ORGANIZER', 'PERCENT', 'PRIORITY',
00438         'RECURID', 'SEQUENCE', 'STATUS', 'SUMMARY', 'UID', 'URL', 'DUE', 'DURATION',
00439     )
00440     exclusive = ('DUE', 'DURATION',)
00441     multiple = (
00442         'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT', 'CONTACT', 'EXDATE',
00443         'EXRULE', 'RSTATUS', 'RELATED', 'RESOURCES', 'RDATE', 'RRULE'
00444     )
00445 
00446 
00447 
00448 class Journal(Component):
00449 
00450     name = 'VJOURNAL'
00451 
00452     required = ('UID',)
00453     singletons = (
00454         'CLASS', 'CREATED', 'DESCRIPTION', 'DTSTART', 'DTSTAMP', 'LAST-MOD',
00455         'ORGANIZER', 'RECURID', 'SEQUENCE', 'STATUS', 'SUMMARY', 'UID', 'URL',
00456     )
00457     multiple = (
00458         'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT', 'CONTACT', 'EXDATE',
00459         'EXRULE', 'RELATED', 'RDATE', 'RRULE', 'RSTATUS',
00460     )
00461 
00462 
00463 class FreeBusy(Component):
00464 
00465     name = 'VFREEBUSY'
00466 
00467     required = ('UID',)
00468     singletons = (
00469         'CONTACT', 'DTSTART', 'DTEND', 'DURATION', 'DTSTAMP', 'ORGANIZER',
00470         'UID', 'URL',
00471     )
00472     multiple = ('ATTENDEE', 'COMMENT', 'FREEBUSY', 'RSTATUS',)
00473 
00474 
00475 class Timezone(Component):
00476 
00477     name = 'VTIMEZONE'
00478 
00479     required = (
00480         'TZID', 'STANDARDC', 'DAYLIGHTC', 'DTSTART', 'TZOFFSETTO',
00481         'TZOFFSETFROM'
00482         )
00483     singletons = ('LAST-MOD', 'TZURL', 'TZID',)
00484     multiple = ('COMMENT', 'RDATE', 'RRULE', 'TZNAME',)
00485 
00486 
00487 class Alarm(Component):
00488 
00489     name = 'VALARM'
00490     # not quite sure about these ...
00491     required = ('ACTION', 'TRIGGER',)
00492     singletons = ('ATTACH', 'ACTION', 'TRIGGER', 'DURATION', 'REPEAT',)
00493     inclusive = (('DURATION', 'REPEAT',),)
00494     multiple = ('STANDARDC', 'DAYLIGHTC')
00495 
00496 
00497 class Calendar(Component):
00498     """
00499     This is the base object for an iCalendar file.
00500 
00501     Setting up a minimal calendar component looks like this
00502     >>> cal = Calendar()
00503 
00504     Som properties are required to be compliant
00505     >>> cal['prodid'] = '-//My calendar product//mxm.dk//'
00506     >>> cal['version'] = '2.0'
00507 
00508     We also need at least one subcomponent for a calendar to be compliant
00509     >>> from datetime import datetime
00510     >>> event = Event()
00511     >>> event['summary'] = 'Python meeting about calendaring'
00512     >>> event['uid'] = '42'
00513     >>> event.set('dtstart', datetime(2005,4,4,8,0,0))
00514     >>> cal.add_component(event)
00515     >>> cal.subcomponents[0].as_string()
00516     'BEGIN:VEVENT\\r\\nDTSTART:20050404T080000\\r\\nSUMMARY:Python meeting about calendaring\\r\\nUID:42\\r\\nEND:VEVENT\\r\\n'
00517 
00518     Write to disc
00519     >>> import tempfile, os
00520     >>> directory = tempfile.mkdtemp()
00521     >>> open(os.path.join(directory, 'test.ics'), 'wb').write(cal.as_string())
00522     """
00523 
00524     name = 'VCALENDAR'
00525     required = ('prodid', 'version', )
00526     singletons = ('prodid', 'version', )
00527     multiple = ('calscale', 'method', )
00528 
00529 
00530 # These are read only singleton, so one instance is enough for the module
00531 types_factory = TypesFactory()
00532 component_factory = ComponentFactory()