Rules implemented
[frenchie/icalparse.git] / icalparse.py
1 #!/usr/bin/python
2 #
3 # Copyright (c) 2010 James French <[email protected]>
4 #
5 # Permission is hereby granted, free of charge, to any person obtaining a copy
6 # of this software and associated documentation files (the "Software"), to deal
7 # in the Software without restriction, including without limitation the rights
8 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 # copies of the Software, and to permit persons to whom the Software is
10 # furnished to do so, subject to the following conditions:
11 #
12 # The above copyright notice and this permission notice shall be included in
13 # all copies or substantial portions of the Software.
14 #
15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 # THE SOFTWARE.
22
23 import sys
24 import urlparse
25 import os
26
27
28 class InvalidICS(Exception): pass
29 class notJoined(Exception): pass
30 class IncompleteICS(InvalidICS): pass
31
32
33 def lineJoiner(oldcal):
34         '''Takes a string containing a calendar and returns an array of its lines'''
35
36         if not oldcal[0:15] == 'BEGIN:VCALENDAR':
37                 raise InvalidICS, "Does not appear to be a valid ICS file"
38
39         if not 'END:VCALENDAR' in oldcal[-15:-1]:
40                 raise IncompleteICS, "File appears to be incomplete"
41
42         if list(oldcal) == oldcal:
43                 oldcal = '\r\n'.join(oldcal)
44
45         oldcal = oldcal.replace('\r\n ', '').replace('\r\n\t','')
46         return oldcal.strip().split('\r\n')
47
48
49 def lineFolder(oldcal, length=75):
50         '''Folds content lines to a specified length, returns a list'''
51
52         if length > 75:
53                 sys.stderr.write('WARN: lines > 75 octets are not RFC compliant\n')
54
55         cal = []
56         sl = length - 1
57
58         for line in oldcal:
59                 # Line fits inside length, do nothing
60                 if len(line.rstrip()) <= length:
61                         cal.append(line)
62                 else:
63                         brokenline = [line[0:length] + '\r\n']
64                         ll = length
65                         while ll < len(line.rstrip('\r\n')) + 1:
66                                 brokenline.append(' ' + line[ll:sl+ll].rstrip('\r\n') + '\r\n')
67                                 ll += sl
68                         cal += brokenline
69
70         return cal
71
72
73 def splitFields(cal):
74         '''Takes a list of lines in a calendar file and returns a list of key, value pairs'''
75
76         ical = [tuple(x.split(':',1)) for x in cal]
77
78         # Check that we got 2 items on every line
79         for line in ical:
80                 if not len(line) == 2:
81                         raise InvalidICS, "Didn't find a content key on: %s"%(line)
82
83         return ical
84
85
86 def getContent(url='',stdin=False):
87         '''Generic content retriever, DO NOT use this function in a CGI script as
88         it can read from the local disk (which you probably don't want it to).
89         '''
90
91         # Special case, if this is a HTTP url, return the data from it using
92         # the HTTP functions which attempt to play a bit nicer.
93         parsedURL = urlparse.urlparse(url)
94         if 'http' in parsedURL[0]: return getHTTPContent(url)
95
96         if stdin:
97                 content = sys.stdin.read()
98                 return content
99
100         if not parsedURL[0]:
101                 try: content = open(os.path.abspath(url),'r').read()
102                 except (IOError, OSError), e:
103                         sys.stderr.write('%s\n'%e)
104                         sys.exit(1)
105                 return content
106
107         # If we've survived, use python's generic URL opening library to handle it
108         import urllib2
109         try:
110                 res = urllib2.urlopen(url)
111                 content = res.read()
112                 res.close()
113         except (urllib2.URLError, OSError), e:
114                 sys.stderr.write('%s\n'%e)
115                 sys.exit(1)
116         return content
117
118
119 def getHTTPContent(url='',cache='.httplib2-cache'):
120         '''This function attempts to play nice when retrieving content from HTTP
121         services. It's what you should use in a CGI script. It will (by default)
122         slurp the first 20 bytes of the file and check that we are indeed looking
123         at an ICS file before going for broke.'''
124
125         try:
126                 import httplib2
127         except ImportError:
128                 import urllib2
129
130         if not url: return ''
131
132         if 'httplib2' in sys.modules:
133                 try: h = httplib2.Http('.httplib2-cache')
134                 except OSError: h = httplib2.Http()
135         else: h = False
136
137         try:
138                 if h: content = h.request(url)[1]
139                 return content
140         except ValueError, e:
141                 sys.stderr.write('%s\n'%e)
142                 sys.exit(1)
143
144         try:
145                 content = urllib2.urlopen(url).read()
146                 return content
147         except (urllib2.URLError, OSError), e:
148                 sys.stderr.write('%s\n'%e)
149                 sys.exit(1)
150
151         return ''
152
153
154 def generateRules():
155         '''Attempts to load a series of rules into a list'''
156         try:
157                 import parserrules
158         except ImportError:
159                 return []
160
161         rules = [getattr(parserrules, rule) for rule in dir(parserrules) if callable(getattr(parserrules, rule))]
162         return rules
163
164
165 def applyRules(ical, rules=[], verbose=False):
166         'Runs a series of rules on the lines in ical and mangles its output'
167
168         for rule in rules:
169                 output = []
170                 if rule.__doc__ and verbose:
171                         print(rule.__doc__)
172                 for line in ical:
173                         try:
174                                 out = rule(line[0],line[1])
175                         except TypeError, e:
176                                 output.append(line)
177                                 print(e)
178                                 continue
179
180                         # Drop lines that are boolean False
181                         if not out and not out == None: continue
182
183                         # If the rule did something and is a tuple or a list we'll accept it
184                         # otherwise, pay no attention to the man behind the curtain
185                         try:
186                                 if tuple(out) == out or list(out) == out and len(out) == 2:
187                                         output.append(tuple(out))
188                                 else:
189                                         output.append(line)
190                         except TypeError, e:
191                                 output.append(line)
192
193                 ical = output
194
195         return ical
196
197 if __name__ == '__main__':
198         from optparse import OptionParser
199         # If the user passed us a 'stdin' argument, we'll go with that,
200         # otherwise we'll try for a url opener
201
202         parser = OptionParser('usage: %prog [options] url')
203         parser.add_option('-s', '--stdin', action='store_true', dest='stdin',
204                 default=False, help='Take a calendar from standard input')
205         parser.add_option('-o', '--output', dest='outfile', default='',
206                 help='Specify output file (defaults to standard output)')
207
208         (options, args) = parser.parse_args()
209
210         if not args and not options.stdin:
211                 parser.print_usage()
212                 sys.exit(0)
213         elif not options.stdin:
214                 url = args[0]
215         else:
216                 url = ''
217
218         content = getContent(url, options.stdin)
219         cal = lineJoiner(content)
220         ical = applyRules(splitFields(cal), generateRules())
221         print ical

UCC git Repository :: git.ucc.asn.au