3 import logging as _logging
5 from . import text as _text
8 _LOG = _logging.getLogger(__name__)
12 r"""An iCalendar entry (e.g. VEVENT)
18 >>> root_dir = os.curdir
19 >>> data_file = os.path.abspath(os.path.join(
20 ... root_dir, 'test', 'data', 'geohash.ics'))
21 >>> with codecs.open(data_file, 'r', 'UTF-8') as f:
22 ... content = f.read()
26 >>> calendar = Entry(content=content)
28 Investigate the entry.
30 >>> print(calendar) # doctest: +REPORT_UDIFF
33 PRODID:-//Example Calendar//NONSGML v1.0//EN
35 UID:2013-06-30@geohash.invalid
36 DTSTAMP:2013-06-30T00:00:00Z
37 DTSTART;VALUE=DATE:20130630
38 DTEND;VALUE=DATE:20130701
39 SUMMARY:XKCD geohashing\, Boston graticule
40 URL:http://xkcd.com/426/
41 LOCATION:Snow Hill\, Dover\, Massachusetts
42 GEO:42.226663,-71.28676
49 ``Entry`` subclasses Python's ``dict``, so you can access raw
50 field values in the usual ways.
52 >>> calendar['VERSION']
54 >>> calendar.get('missing')
55 >>> calendar.get('missing', 'some default')
57 >>> sorted(calendar.keys())
58 ['PRODID', 'VERSION', 'VEVENT']
61 Dig into the children (which are always stored as lists):
63 >>> event = calendar['VEVENT'][0]
67 >>> event.content # doctest: +ELLIPSIS
68 'BEGIN:VEVENT\r\nUID:...\r\nEND:VEVENT\r\n'
69 >>> sorted(event.keys()) # doctest: +NORMALIZE_WHITESPACE
70 ['DTEND;VALUE=DATE', 'DTSTAMP', 'DTSTART;VALUE=DATE', 'GEO',
71 'LOCATION', 'SUMMARY', 'UID', 'URL']
74 'Snow Hill\\, Dover\\, Massachusetts'
76 You can also use ``get_text`` to unescape text fields.
78 >>> event.get_text('LOCATION')
79 'Snow Hill, Dover, Massachusetts'
81 def __init__(self, type=None, content=None):
82 super(Entry, self).__init__()
83 if type is None and content:
84 firstline = content.splitlines()[0]
85 type = firstline.split(':', 1)[1]
87 self.content = content
88 self._lines = None # unwrapped semantic lines
99 return hash(_text.unescape(self['UID']))
104 return self.content.replace('\r\n', '\n').strip()
108 return '<{} type:{}>'.format(type(self).__name__, self.type)
116 for index,verb,expected in [
117 [0, 'begin', 'BEGIN:{}'.format(self.type)],
118 [-1, 'end', 'END:{}'.format(self.type)],
120 if self._lines[index] != expected:
121 raise ValueError('entry should {} with {!r}, not {!r}'.format(
122 verb, expected, self._lines[index]))
125 for i,line in enumerate(self._lines[1:-1]):
126 key,value = [x.strip() for x in line.split(':', 1)]
128 _LOG.debug('{!r}: begin {}'.format(self, value))
131 child_lines.append(line)
133 _LOG.debug('{!r}: end {}'.format(self, value))
134 if not stack or value != stack[-1]:
136 ('closing {} on line {}, but current stack is {}'
137 ).format(value, i+1, stack))
142 content='\r\n'.join(child_lines) + '\r\n',
144 child._lines = child_lines
146 self._add_value(key=value, value=child, force_list=True)
148 elif not stack: # our own data, not a child's
150 v = _text.unescape(value)
152 raise NotImplementedError(
153 'cannot parse VERSION {} feed'.format(v))
154 self._add_value(key=key, value=value)
156 def _add_value(self, key, value, force_list=False):
157 if force_list and key not in self:
160 if type(self[key]) == str:
161 self[key] = [self[key]]
162 self[key].append(value)
167 """Unfold wrapped lines
169 Following :RFC:`5545`, section 3.1 (Content Lines)
172 semantic_line_chunks = []
173 for line in self.content.splitlines():
174 lstrip = line.lstrip()
176 if not semantic_line_chunks:
178 ('whitespace-prefixed line {!r} is not a continuation '
179 'of a previous line').format(line))
180 semantic_line_chunks.append(lstrip)
182 if semantic_line_chunks:
183 self._lines.append(''.join(semantic_line_chunks))
184 semantic_line_chunks = [line]
185 if semantic_line_chunks:
186 self._lines.append(''.join(semantic_line_chunks))
188 def get_text(self, *args, **kwargs):
189 value = self.get(*args, **kwargs)
190 return _text.unescape(value)
192 def write(self, stream):
193 stream.write(self.content)