Major restructuring to get automatic decoding/encoding
[pycalendar.git] / pycalendar / component / base.py
diff --git a/pycalendar/component/base.py b/pycalendar/component/base.py
new file mode 100644 (file)
index 0000000..902ccd4
--- /dev/null
@@ -0,0 +1,114 @@
+# Copyright
+
+import io as _io
+import itertools as _itertools
+
+from .. import property as _property
+from .. import unfold as _unfold
+
+
+_COMPONENT = {}
+
+
+class Component (dict):
+    r"""A base class for calendar components
+
+    As defined in :RFC:`5545`, section 3.6 (Calendar Components).
+    """
+    name = None
+    # properties
+    required = []
+    optional = []
+    multiple = []
+    # sub components
+    subcomponents = []
+
+    def __hash__(self):
+        if self.name in [
+                'VEVENT',
+                'VFREEBUSY',
+                'VJOURNAL',
+                'VTODO',
+                ] or 'UID' in self:
+            return hash(self['UID'])
+        return id(self)
+
+    def __str__(self):
+        with _io.StringIO() as stream:
+            self.write(stream=stream, newline='\n')
+            return stream.getvalue()[:-1]  # strip the trailing newline
+
+    def __repr__(self):
+        return '<{}.{} name:{} at {:#x}>'.format(
+            self.__module__, type(self).__name__, self.name, id(self))
+
+    def read(self, stream=None, lines=None):
+        """Read an input stream and parse into properties and subcomponents
+        """
+        if lines is None:
+            lines = _unfold.unfold(stream=stream)
+        elif stream is not None:
+            raise ValueError("cannot specify both 'stream' and 'lines'")
+        for line in lines:
+            prop = _property.parse(line)
+            if prop.name == 'BEGIN':  # a subcomponent
+                if prop.value not in self.subcomponents:
+                    raise ValueError('invalid component {} for {!r}'.format(
+                        prop.value, self))
+                component_class = _COMPONENT[prop.value]
+                component = component_class()
+                component.read(lines=lines)
+                self.add_component(component)
+            elif prop.name == 'END':  # we're done with this component
+                if prop.value != self.name:
+                    raise ValueError('cannot close {!r} with {}'.format(
+                        self, line))
+                return
+            else:  # a property
+                self.add_property(property=prop)
+
+    def add_component(self, component):
+        name = component.name
+        if name not in self.subcomponents:
+            raise ValueError('invalid component {} for {!r}'.format(
+                name, self))
+        if name not in self:
+            self[name] = []
+        self[name].append(component)
+
+    def add_property(self, property):
+        name = property.name
+        if name not in self.required and name not in self.optional:
+            raise ValueError('invalid property {} for {!r}'.format(name, self))
+        if name in self.multiple:
+            if name not in self:
+                self[name] = []
+            self[name].append(property)
+        else:
+            self[name] = property
+
+    def write(self, stream, newline='\r\n'):
+        stream.write('BEGIN:{}{}'.format(self.name, newline))
+        for prop in _itertools.chain(self.required, self.optional):
+            self._write_property_by_name(
+                name=prop, stream=stream, newline=newline)
+        for component in self.subcomponents:
+            self._write_component_by_name(
+                name=component, stream=stream, newline=newline)
+        stream.write('END:{}{}'.format(self.name, newline))
+
+    def _write_component_by_name(self, name, stream, newline='\r\n'):
+        component = self.get(name, [])
+        if isinstance(component, list):
+            for c in component:
+                c.write(stream=stream, newline=newline)
+        else:
+            component.write(stream=stream, newline=newline)
+
+    def _write_property_by_name(self, name, stream, newline='\r\n'):
+        prop = self.get(name, [])
+        if isinstance(prop, list):
+            for p in prop:
+                p.write(stream=stream, newline=newline)
+        else:
+            prop.write(stream=stream, newline=newline)