Run update-copyright.py
[pycalendar.git] / pycalendar / feed.py
1 # Copyright (C) 2013 W. Trevor King <wking@tremily.us>
2 #
3 # This file is part of pycalender.
4 #
5 # pycalender is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free Software
7 # Foundation, either version 3 of the License, or (at your option) any later
8 # version.
9 #
10 # pycalender is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along with
15 # pycalender.  If not, see <http://www.gnu.org/licenses/>.
16
17 import codecs as _codecs
18 import logging as _logging
19 import urllib.request as _urllib_request
20
21 from . import USER_AGENT as _USER_AGENT
22 from . import property as _property
23 from . import unfold as _unfold
24 from .component import calendar as _calendar
25
26
27 _LOG = _logging.getLogger(__name__)
28
29
30 class Feed (_calendar.Calendar):
31     r"""An iCalendar feed (:RFC:`5545`)
32
33     Figure out where the example feed is located, relative to the
34     directory from which you run this doctest (i.e., the project's
35     root directory).
36
37     >>> import os
38     >>> root_dir = os.curdir
39     >>> data_file = os.path.abspath(os.path.join(
40     ...         root_dir, 'test', 'data', 'geohash.ics'))
41     >>> url = 'file://{}'.format(data_file.replace(os.sep, '/'))
42
43     Create a new feed pointing to this URL.
44
45     >>> f = Feed(url=url)
46     >>> f  # doctest: +ELLIPSIS
47     <Feed url:file://.../test/data/geohash.ics>
48
49     Load the feed content.
50
51     >>> f.fetch()
52
53     The ``.__str__`` method displays the feed content using Python's
54     universal newlines.
55
56     >>> print(f)  # doctest: +REPORT_UDIFF
57     BEGIN:VCALENDAR
58     PRODID:-//Example Calendar//NONSGML v1.0//EN
59     VERSION:2.0
60     BEGIN:VEVENT
61     DTSTAMP:20130630T000000Z
62     UID:2013-06-30@geohash.invalid
63     DTSTART;VALUE=DATE:20130630
64     GEO:42.226663;-71.286760
65     LOCATION:Snow Hill\, Dover\, Massachusetts
66     SUMMARY:XKCD geohashing\, Boston graticule
67     URL:http://xkcd.com/426/
68     DTEND;VALUE=DATE:20130701
69     END:VEVENT
70     END:VCALENDAR
71
72     To get the CRLF line endings specified in :RFC:`5545`, use the
73     ``.write`` method.
74
75     >>> import io
76     >>> stream = io.StringIO()
77     >>> f.write(stream=stream)
78     >>> stream.getvalue()  # doctest: +ELLIPSIS
79     'BEGIN:VCALENDAR\r\nPRODID:...END:VCALENDAR\r\n'
80
81     You can also iterate through events:
82
83     >>> for event in f['VEVENT']:
84     ...     print(repr(event))
85     ...     print(event)
86     ... # doctest: +ELLIPSIS, +REPORT_UDIFF
87     <Event name:VEVENT at 0x...>
88     BEGIN:VEVENT
89     DTSTAMP:20130630T000000Z
90     UID:2013-06-30@geohash.invalid
91     DTSTART;VALUE=DATE:20130630
92     GEO:42.226663;-71.286760
93     LOCATION:Snow Hill\, Dover\, Massachusetts
94     SUMMARY:XKCD geohashing\, Boston graticule
95     URL:http://xkcd.com/426/
96     DTEND;VALUE=DATE:20130701
97     END:VEVENT
98     """
99     def __init__(self, url, user_agent=None):
100         super(Feed, self).__init__(type='VCALENDAR')
101         self.url = url
102         if user_agent is None:
103             user_agent = _USER_AGENT
104         self.user_agent = user_agent
105
106     def __repr__(self):
107         return '<{}.{} url:{}>'.format(
108             self.__module__, type(self).__name__, self.url)
109
110     def fetch(self):
111         self.clear()
112         request = _urllib_request.Request(
113             url=self.url,
114             headers={
115                 'User-Agent': self.user_agent,
116                 },
117             )
118         with _urllib_request.urlopen(url=request) as f:
119             info = f.info()
120             content_type = info.get('Content-type', None)
121             if content_type != 'text/calendar':
122                 raise ValueError(content_type)
123             codec = _codecs.lookup('UTF-8')
124             with codec.streamreader(stream=f) as stream:
125                 self.parse(stream=stream)
126
127     def parse(self, stream):
128         lines = _unfold.unfold(stream=stream)
129         line = next(lines)
130         prop = _property.parse(line=line)
131         if prop.name != 'BEGIN' or prop.value != self.name:
132             raise ValueError(
133                 "stream {} must start with 'BEGIN:VCALENDAR', not {!r}".format(
134                     stream, line))
135         self.read(lines=lines)