1.1 --- a/imiptools/__init__.py Thu May 12 17:51:10 2016 +0200
1.2 +++ b/imiptools/__init__.py Thu May 12 22:58:57 2016 +0200
1.3 @@ -194,6 +194,14 @@
1.4 self.publishing_dir = getvalue(publishing_dir)
1.5 self.preferences_dir = getvalue(preferences_dir)
1.6 self.journal_dir = getvalue(journal_dir)
1.7 +
1.8 + # If debug mode is set, extend the line length for convenience.
1.9 +
1.10 + if self.debug:
1.11 + config.CALENDAR_LINE_LENGTH = 1000
1.12 +
1.13 + # Process the input.
1.14 +
1.15 self.process(stream, original_recipients)
1.16
1.17 def __call__(self):
2.1 --- a/imiptools/client.py Thu May 12 17:51:10 2016 +0200
2.2 +++ b/imiptools/client.py Thu May 12 22:58:57 2016 +0200
2.3 @@ -21,8 +21,8 @@
2.4
2.5 from datetime import datetime, timedelta
2.6 from imiptools import config
2.7 -from imiptools.data import Object, get_address, get_uri, get_window_end, \
2.8 - is_new_object, make_freebusy, to_part, \
2.9 +from imiptools.data import Object, check_delegation, get_address, get_uri, \
2.10 + get_window_end, is_new_object, make_freebusy, to_part, \
2.11 uri_dict, uri_item, uri_items, uri_parts, uri_values
2.12 from imiptools.dates import check_permitted_values, format_datetime, get_default_timezone, \
2.13 get_duration, get_timestamp
2.14 @@ -270,7 +270,7 @@
2.15
2.16 user_attr = {}
2.17 self.update_sender(user_attr)
2.18 - return to_part("PUBLISH", [make_freebusy(freebusy, uid, self.user, user_attr)])
2.19 + return self.to_part("PUBLISH", [make_freebusy(freebusy, uid, self.user, user_attr)])
2.20
2.21 return None
2.22
2.23 @@ -301,6 +301,20 @@
2.24 else:
2.25 freebusy.update_freebusy(periods, transp, uid, recurrenceid, summary, organiser)
2.26
2.27 + # Preparation of content.
2.28 +
2.29 + def to_part(self, method, fragments):
2.30 +
2.31 + "Return an encoded MIME part for the given 'method' and 'fragments'."
2.32 +
2.33 + return to_part(method, fragments, line_length=config.CALENDAR_LINE_LENGTH)
2.34 +
2.35 + def object_to_part(self, method, obj):
2.36 +
2.37 + "Return an encoded MIME part for the given 'method' and 'obj'."
2.38 +
2.39 + return obj.to_part(method, line_length=config.CALENDAR_LINE_LENGTH)
2.40 +
2.41 # Preparation of messages communicating the state of events.
2.42
2.43 def get_message_parts(self, obj, method, attendee=None):
2.44 @@ -327,7 +341,7 @@
2.45 if not attendee or self.is_participating(attendee, obj=obj):
2.46 organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER"))
2.47 self.update_sender(organiser_attr)
2.48 - responses.append(obj.to_part(method))
2.49 + responses.append(self.object_to_part(method, obj))
2.50 methods.add(method)
2.51
2.52 # Get recurrences for parent events.
2.53 @@ -350,7 +364,7 @@
2.54 if not attendee or self.is_participating(attendee, obj=obj):
2.55 organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER"))
2.56 self.update_sender(organiser_attr)
2.57 - responses.append(obj.to_part(rmethod))
2.58 + responses.append(self.object_to_part(rmethod, obj))
2.59 methods.add(rmethod)
2.60
2.61 return methods, responses
2.62 @@ -455,7 +469,7 @@
2.63 continue
2.64 obj["RECURRENCE-ID"] = obj["DTSTART"] = [(format_datetime(p.get_start()), p.get_start_attr())]
2.65 obj["DTEND"] = [(format_datetime(p.get_end()), p.get_end_attr())]
2.66 - unscheduled_parts.append(obj.to_part("CANCEL"))
2.67 + unscheduled_parts.append(self.object_to_part("CANCEL", obj))
2.68
2.69 return unscheduled_parts
2.70
2.71 @@ -700,7 +714,7 @@
2.72 self.update_senders(obj=obj)
2.73 obj.update_dtstamp()
2.74 obj.update_sequence(False)
2.75 - self._send_message(get_address(self.user), [get_address(attendee)], [obj.to_part(method)], True)
2.76 + self._send_message(get_address(self.user), [get_address(attendee)], [self.object_to_part(method, obj)], True)
2.77 return True
2.78
2.79 def process_received_request(self, changed=False):
2.80 @@ -726,7 +740,8 @@
2.81
2.82 self.update_dtstamp()
2.83 self.update_sequence(False)
2.84 - self.send_message([self.obj.to_part(changed and "COUNTER" or "REPLY")], get_address(self.user), self.obj, False, True)
2.85 + self.send_message([self.object_to_part(changed and "COUNTER" or "REPLY", self.obj)],
2.86 + get_address(self.user), self.obj, False, True)
2.87 return True
2.88
2.89 def process_created_request(self, method, to_cancel=None, to_unschedule=None):
2.90 @@ -782,7 +797,7 @@
2.91
2.92 # Send a cancellation to all uninvited attendees.
2.93
2.94 - parts = [obj.to_part("CANCEL")]
2.95 + parts = [self.object_to_part("CANCEL", obj)]
2.96 self.send_message(parts, get_address(organiser), obj, True, False)
2.97
2.98 # Issue a CANCEL message to the user's address.
3.1 --- a/imiptools/config.py Thu May 12 17:51:10 2016 +0200
3.2 +++ b/imiptools/config.py Thu May 12 22:58:57 2016 +0200
3.3 @@ -134,4 +134,9 @@
3.4
3.5 IMIP_COUNTER_AS_REQUEST = True
3.6
3.7 +# Calendar object maximum line length. If None, the recommended iCalendar line
3.8 +# length is used (which should be 76 characters).
3.9 +
3.10 +CALENDAR_LINE_LENGTH = None
3.11 +
3.12 # vim: tabstop=4 expandtab shiftwidth=4
4.1 --- a/imiptools/data.py Thu May 12 17:51:10 2016 +0200
4.2 +++ b/imiptools/data.py Thu May 12 22:58:57 2016 +0200
4.3 @@ -161,14 +161,16 @@
4.4 def get_duration(self, name):
4.5 return get_duration(self.get_value(name))
4.6
4.7 + # Serialisation.
4.8 +
4.9 def to_node(self):
4.10 return to_node({self.objtype : [(self.details, self.attr)]})
4.11
4.12 - def to_part(self, method):
4.13 - return to_part(method, [self.to_node()])
4.14 + def to_part(self, method, encoding="utf-8", line_length=None):
4.15 + return to_part(method, [self.to_node()], encoding, line_length)
4.16
4.17 - def to_string(self):
4.18 - return to_string(self.to_node())
4.19 + def to_string(self, encoding="utf-8", line_length=None):
4.20 + return to_string(self.to_node(), encoding, line_length)
4.21
4.22 # Direct access to the structure.
4.23
4.24 @@ -615,7 +617,7 @@
4.25 # Get a constrained view if start and end limits are specified.
4.26
4.27 if period:
4.28 - periods = freebusy.period_overlaps(period, True)
4.29 + periods = freebusy.get_overlapping(period)
4.30 else:
4.31 periods = freebusy
4.32
4.33 @@ -677,17 +679,16 @@
4.34
4.35 return parse_object(StringIO(s), encoding, objtype)
4.36
4.37 -def to_part(method, calendar):
4.38 +def to_part(method, fragments, encoding="utf-8", line_length=None):
4.39
4.40 """
4.41 - Write using the given 'method', the 'calendar' details to a MIME
4.42 + Write using the given 'method', the given 'fragments' to a MIME
4.43 text/calendar part.
4.44 """
4.45
4.46 - encoding = "utf-8"
4.47 out = StringIO()
4.48 try:
4.49 - to_stream(out, make_calendar(calendar, method), encoding)
4.50 + to_stream(out, make_calendar(fragments, method), encoding, line_length)
4.51 part = MIMEText(out.getvalue(), "calendar", encoding)
4.52 part.set_param("method", method)
4.53 return part
4.54 @@ -695,19 +696,19 @@
4.55 finally:
4.56 out.close()
4.57
4.58 -def to_stream(out, fragment, encoding="utf-8"):
4.59 +def to_stream(out, fragment, encoding="utf-8", line_length=None):
4.60
4.61 "Write to the 'out' stream the given 'fragment'."
4.62
4.63 - iterwrite(out, encoding=encoding).append(fragment)
4.64 + iterwrite(out, encoding=encoding, line_length=line_length).append(fragment)
4.65
4.66 -def to_string(fragment, encoding="utf-8"):
4.67 +def to_string(fragment, encoding="utf-8", line_length=None):
4.68
4.69 "Return a string encoding the given 'fragment'."
4.70
4.71 out = StringIO()
4.72 try:
4.73 - to_stream(out, fragment, encoding)
4.74 + to_stream(out, fragment, encoding, line_length)
4.75 return out.getvalue()
4.76
4.77 finally:
5.1 --- a/imiptools/stores/file.py Thu May 12 17:51:10 2016 +0200
5.2 +++ b/imiptools/stores/file.py Thu May 12 22:58:57 2016 +0200
5.3 @@ -29,7 +29,7 @@
5.4 from imiptools.period import FreeBusyPeriod, FreeBusyGroupPeriod, \
5.5 FreeBusyOfferPeriod, FreeBusyCollection, \
5.6 FreeBusyGroupCollection, FreeBusyOffersCollection
5.7 -from imiptools.text import parse_line
5.8 +from imiptools.text import get_table, set_defaults
5.9 from os.path import isdir, isfile, join
5.10 from os import listdir, remove, rmdir
5.11 import codecs
5.12 @@ -47,18 +47,13 @@
5.13 # Utility methods.
5.14
5.15 def _set_defaults(self, t, empty_defaults):
5.16 - for i, default in empty_defaults:
5.17 - if i >= len(t):
5.18 - t += [None] * (i - len(t) + 1)
5.19 - if not t[i]:
5.20 - t[i] = default
5.21 - return t
5.22 + return set_defaults(t, empty_defaults)
5.23
5.24 - def _get_table(self, user, filename, empty_defaults=None, tab_separated=True):
5.25 + def _get_table(self, filename, empty_defaults=None, tab_separated=True):
5.26
5.27 """
5.28 - From the file for the given 'user' having the given 'filename', return
5.29 - a list of tuples representing the file's contents.
5.30 + From the file having the given 'filename', return a list of tuples
5.31 + representing the file's contents.
5.32
5.33 The 'empty_defaults' is a list of (index, value) tuples indicating the
5.34 default value where a column either does not exist or provides an empty
5.35 @@ -69,21 +64,7 @@
5.36 splitting each line of the file using tab characters as separators.
5.37 """
5.38
5.39 - f = codecs.open(filename, "rb", encoding="utf-8")
5.40 - try:
5.41 - l = []
5.42 - for line in f.readlines():
5.43 - line = line.strip(" \r\n")
5.44 - if tab_separated:
5.45 - t = line.split("\t")
5.46 - else:
5.47 - t = parse_line(line)
5.48 - if empty_defaults:
5.49 - t = self._set_defaults(t, empty_defaults)
5.50 - l.append(tuple(t))
5.51 - return l
5.52 - finally:
5.53 - f.close()
5.54 + return get_table(filename, empty_defaults, tab_separated)
5.55
5.56 def _get_table_atomic(self, user, filename, empty_defaults=None, tab_separated=True):
5.57
5.58 @@ -102,15 +83,14 @@
5.59
5.60 self.acquire_lock(user)
5.61 try:
5.62 - return self._get_table(user, filename, empty_defaults, tab_separated)
5.63 + return self._get_table(filename, empty_defaults, tab_separated)
5.64 finally:
5.65 self.release_lock(user)
5.66
5.67 - def _set_table(self, user, filename, items, empty_defaults=None):
5.68 + def _set_table(self, filename, items, empty_defaults=None):
5.69
5.70 """
5.71 - For the given 'user', write to the file having the given 'filename' the
5.72 - 'items'.
5.73 + Write to the file having the given 'filename' the 'items'.
5.74
5.75 The 'empty_defaults' is a list of (index, value) tuples indicating the
5.76 default value where a column either does not exist or provides an empty
5.77 @@ -146,7 +126,7 @@
5.78
5.79 self.acquire_lock(user)
5.80 try:
5.81 - self._set_table(user, filename, items, empty_defaults)
5.82 + self._set_table(filename, items, empty_defaults)
5.83 finally:
5.84 self.release_lock(user)
5.85
6.1 --- a/imiptools/text.py Thu May 12 17:51:10 2016 +0200
6.2 +++ b/imiptools/text.py Thu May 12 22:58:57 2016 +0200
6.3 @@ -3,7 +3,7 @@
6.4 """
6.5 Parsing of textual content.
6.6
6.7 -Copyright (C) 2016 Paul Boddie <paul@boddie.org.uk>
6.8 +Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
6.9
6.10 This program is free software; you can redistribute it and/or modify it under
6.11 the terms of the GNU General Public License as published by the Free Software
6.12 @@ -19,6 +19,7 @@
6.13 this program. If not, see <http://www.gnu.org/licenses/>.
6.14 """
6.15
6.16 +import codecs
6.17 import re
6.18
6.19 # Parsing of lines to obtain functions and arguments.
6.20 @@ -53,4 +54,72 @@
6.21
6.22 return parts
6.23
6.24 +# Parsing of tabular files.
6.25 +
6.26 +def set_defaults(t, empty_defaults):
6.27 +
6.28 + """
6.29 + In the list 't', replace values that are empty or absent with defaults
6.30 + provided by the 'empty_defaults' collection whose entries are of the form
6.31 + (index, value).
6.32 + """
6.33 +
6.34 + for i, default in empty_defaults:
6.35 + if i >= len(t):
6.36 + t += [None] * (i - len(t) + 1)
6.37 + if not t[i]:
6.38 + t[i] = default
6.39 + return t
6.40 +
6.41 +def get_table(filename, empty_defaults=None, tab_separated=True):
6.42 +
6.43 + """
6.44 + From the file having the given 'filename', return a list of tuples
6.45 + representing the file's contents.
6.46 +
6.47 + The 'empty_defaults' is a list of (index, value) tuples indicating the
6.48 + default value where a column either does not exist or provides an empty
6.49 + value.
6.50 +
6.51 + If 'tab_separated' is specified and is a false value, line parsing using
6.52 + the imiptools.text.parse_line function will be performed instead of
6.53 + splitting each line of the file using tab characters as separators.
6.54 + """
6.55 +
6.56 + f = codecs.open(filename, "rb", encoding="utf-8")
6.57 + try:
6.58 + return get_table_from_stream(f, empty_defaults, tab_separated)
6.59 + finally:
6.60 + f.close()
6.61 +
6.62 +def get_table_from_stream(f, empty_defaults=None, tab_separated=True):
6.63 +
6.64 + """
6.65 + Return a list of tuples representing the contents of the stream 'f'.
6.66 +
6.67 + The 'empty_defaults' is a list of (index, value) tuples indicating the
6.68 + default value where a column either does not exist or provides an empty
6.69 + value.
6.70 +
6.71 + If 'tab_separated' is specified and is a false value, line parsing using
6.72 + the imiptools.text.parse_line function will be performed instead of
6.73 + splitting each line of the file using tab characters as separators.
6.74 + """
6.75 +
6.76 + l = []
6.77 +
6.78 + for line in f.readlines():
6.79 + line = line.strip(" \r\n")
6.80 +
6.81 + if tab_separated:
6.82 + t = line.split("\t")
6.83 + else:
6.84 + t = parse_line(line)
6.85 +
6.86 + if empty_defaults:
6.87 + t = set_defaults(t, empty_defaults)
6.88 + l.append(tuple(t))
6.89 +
6.90 + return l
6.91 +
6.92 # vim: tabstop=4 expandtab shiftwidth=4