1.1 --- a/imiptools/data.py Sun Oct 15 23:22:28 2017 +0200
1.2 +++ b/imiptools/data.py Sun Oct 15 23:30:38 2017 +0200
1.3 @@ -44,13 +44,16 @@
1.4
1.5 "Access to calendar structures."
1.6
1.7 - def __init__(self, fragment):
1.8 + def __init__(self, fragment, tzid=None):
1.9
1.10 """
1.11 - Initialise the object with the given 'fragment'. This must be a
1.12 - dictionary mapping an object type (such as "VEVENT") to a tuple
1.13 - containing the object details and attributes, each being a dictionary
1.14 - itself.
1.15 + Initialise the object with the given 'fragment'. The optional 'tzid'
1.16 + sets the fallback time zone used to convert datetimes without time zone
1.17 + information.
1.18 +
1.19 + The 'fragment' must be a dictionary mapping an object type (such as
1.20 + "VEVENT") to a tuple containing the object details and attributes,
1.21 + each being a dictionary itself.
1.22
1.23 The result of parse_object can be processed to obtain a fragment by
1.24 obtaining a collection of records for an object type. For example:
1.25 @@ -73,6 +76,16 @@
1.26 """
1.27
1.28 self.objtype, (self.details, self.attr) = fragment.items()[0]
1.29 + self.set_tzid(tzid)
1.30 +
1.31 + def set_tzid(self, tzid):
1.32 +
1.33 + """
1.34 + Set the fallback 'tzid' for interpreting datetimes without time zone
1.35 + information.
1.36 + """
1.37 +
1.38 + self.tzid = tzid
1.39
1.40 def get_uid(self):
1.41 return self.get_value("UID")
1.42 @@ -102,12 +115,13 @@
1.43 dt = to_timezone(to_datetime(dt, tzid), "UTC")
1.44 return format_datetime(dt)
1.45
1.46 - def get_recurrence_start_point(self, recurrenceid, tzid):
1.47 + def get_recurrence_start_point(self, recurrenceid):
1.48
1.49 """
1.50 Return the start point corresponding to the given 'recurrenceid', using
1.51 - the fallback 'tzid' to define the specific point in time referenced by
1.52 - the recurrence identifier if the identifier has a date representation.
1.53 + the fallback time zone to define the specific point in time referenced
1.54 + by the recurrence identifier if the identifier has a date
1.55 + representation.
1.56
1.57 If 'recurrenceid' is given as None, this object's recurrence identifier
1.58 is used to obtain a start point, but if this object does not provide a
1.59 @@ -119,21 +133,18 @@
1.60
1.61 recurrenceid = recurrenceid or self.get_recurrenceid()
1.62 if recurrenceid:
1.63 - return get_recurrence_start_point(recurrenceid, tzid)
1.64 + return get_recurrence_start_point(recurrenceid, self.tzid)
1.65 else:
1.66 return None
1.67
1.68 - def get_recurrence_start_points(self, recurrenceids, tzid):
1.69 + def get_recurrence_start_points(self, recurrenceids):
1.70
1.71 """
1.72 - Return start points for 'recurrenceids' using the fallback 'tzid' for
1.73 + Return start points for 'recurrenceids' using the fallback time zone for
1.74 identifiers with date representations.
1.75 """
1.76
1.77 - points = []
1.78 - for recurrenceid in recurrenceids:
1.79 - points.append(self.get_recurrence_start_point(recurrenceid, tzid))
1.80 - return points
1.81 + return map(self.get_recurrence_start_point, recurrenceids)
1.82
1.83 # Structure access.
1.84
1.85 @@ -149,7 +160,7 @@
1.86 l.append((obj.details, obj.attr))
1.87
1.88 def copy(self):
1.89 - return Object(self.to_dict())
1.90 + return Object(self.to_dict(), self.tzid)
1.91
1.92 def get_items(self, name, all=True):
1.93 return get_items(self.details, name, all)
1.94 @@ -169,17 +180,18 @@
1.95 def set_value(self, name, value, attr=None):
1.96 self.details[name] = [(value, attr or {})]
1.97
1.98 - def get_utc_datetime(self, name, date_tzid=None):
1.99 - return get_utc_datetime(self.details, name, date_tzid)
1.100 + def get_utc_datetime(self, name):
1.101 + return get_utc_datetime(self.details, name, self.tzid)
1.102
1.103 - def get_date_value_items(self, name, tzid=None):
1.104 - return get_date_value_items(self.details, name, tzid)
1.105 + def get_date_value_items(self, name):
1.106 + return get_date_value_items(self.details, name, self.tzid)
1.107
1.108 - def get_date_value_item_periods(self, name, tzid=None):
1.109 - return get_date_value_item_periods(self.details, name, self.get_main_period(tzid).get_duration(), tzid)
1.110 + def get_date_value_item_periods(self, name):
1.111 + return get_date_value_item_periods(self.details, name,
1.112 + self.get_main_period().get_duration(), self.tzid)
1.113
1.114 - def get_period_values(self, name, tzid=None):
1.115 - return get_period_values(self.details, name, tzid)
1.116 + def get_period_values(self, name):
1.117 + return get_period_values(self.details, name, self.tzid)
1.118
1.119 def get_datetime(self, name):
1.120 t = get_datetime_item(self.details, name)
1.121 @@ -244,7 +256,7 @@
1.122
1.123 # Computed results.
1.124
1.125 - def get_main_period(self, tzid=None):
1.126 + def get_main_period(self):
1.127
1.128 """
1.129 Return a period object corresponding to the main start-end period for
1.130 @@ -252,7 +264,7 @@
1.131 """
1.132
1.133 (dtstart, dtstart_attr), (dtend, dtend_attr) = self.get_main_period_items()
1.134 - tzid = tzid or get_tzid(dtstart_attr, dtend_attr)
1.135 + tzid = get_tzid(dtstart_attr, dtend_attr) or self.tzid
1.136 return RecurringPeriod(dtstart, dtend, tzid, "DTSTART", dtstart_attr, dtend_attr)
1.137
1.138 def get_main_period_items(self):
1.139 @@ -275,12 +287,12 @@
1.140
1.141 return (dtstart, dtstart_attr), (dtend, dtend_attr)
1.142
1.143 - def get_periods(self, tzid, start=None, end=None, inclusive=False):
1.144 + def get_periods(self, start=None, end=None, inclusive=False):
1.145
1.146 """
1.147 - Return periods defined by this object, employing the given 'tzid' where
1.148 - no time zone information is defined, and limiting the collection to a
1.149 - window of time with the given 'start' and 'end'.
1.150 + Return periods defined by this object, employing the fallback time zone
1.151 + where no time zone information is defined, and limiting the collection
1.152 + to a window of time with the given 'start' and 'end'.
1.153
1.154 If 'end' is omitted, only explicit recurrences and recurrences from
1.155 explicitly-terminated rules will be returned.
1.156 @@ -289,43 +301,45 @@
1.157 will be included.
1.158 """
1.159
1.160 - return get_periods(self, tzid, start, end, inclusive)
1.161 + return get_periods(self, start, end, inclusive)
1.162
1.163 - def has_period(self, tzid, period):
1.164 + def has_period(self, period):
1.165
1.166 """
1.167 - Return whether this object, employing the given 'tzid' where no time
1.168 - zone information is defined, has the given 'period'.
1.169 + Return whether this object, employing the fallback time zone where no
1.170 + time zone information is defined, has the given 'period'.
1.171 """
1.172
1.173 - return period in self.get_periods(tzid, end=period.get_start_point(), inclusive=True)
1.174 + return period in self.get_periods(end=period.get_start_point(), inclusive=True)
1.175
1.176 - def has_recurrence(self, tzid, recurrenceid):
1.177 + def has_recurrence(self, recurrenceid):
1.178
1.179 """
1.180 - Return whether this object, employing the given 'tzid' where no time
1.181 - zone information is defined, has the given 'recurrenceid'.
1.182 + Return whether this object, employing the fallback time zone where no
1.183 + time zone information is defined, has the given 'recurrenceid'.
1.184 """
1.185
1.186 - start_point = self.get_recurrence_start_point(recurrenceid, tzid)
1.187 - for p in self.get_periods(tzid, end=start_point, inclusive=True):
1.188 + start_point = self.get_recurrence_start_point(recurrenceid)
1.189 +
1.190 + for p in self.get_periods(end=start_point, inclusive=True):
1.191 if p.get_start_point() == start_point:
1.192 return True
1.193 +
1.194 return False
1.195
1.196 - def get_active_periods(self, recurrenceids, tzid, start=None, end=None):
1.197 + def get_active_periods(self, recurrenceids, start=None, end=None):
1.198
1.199 """
1.200 Return all periods specified by this object that are not replaced by
1.201 - those defined by 'recurrenceids', using 'tzid' as a fallback time zone
1.202 - to convert floating dates and datetimes, and using 'start' and 'end' to
1.203 + those defined by 'recurrenceids', using the fallback time zone to
1.204 + convert floating dates and datetimes, and using 'start' and 'end' to
1.205 respectively indicate the start and end of the time window within which
1.206 periods are considered.
1.207 """
1.208
1.209 # Specific recurrences yield all specified periods.
1.210
1.211 - periods = self.get_periods(tzid, start, end)
1.212 + periods = self.get_periods(start, end)
1.213
1.214 if self.get_recurrenceid():
1.215 return periods
1.216 @@ -398,17 +412,13 @@
1.217
1.218 """
1.219 Return a time zone identifier used by the start or end datetimes,
1.220 - potentially suitable for converting dates to datetimes.
1.221 + potentially suitable for converting dates to datetimes. Where no
1.222 + identifier is associated with the datetimes, provide any fallback time
1.223 + zone identifier.
1.224 """
1.225
1.226 - if not self.has_key("DTSTART"):
1.227 - return None
1.228 - dtstart, dtstart_attr = self.get_datetime_item("DTSTART")
1.229 - if self.has_key("DTEND"):
1.230 - dtend, dtend_attr = self.get_datetime_item("DTEND")
1.231 - else:
1.232 - dtend_attr = None
1.233 - return get_tzid(dtstart_attr, dtend_attr)
1.234 + (dtstart, dtstart_attr), (dtend, dtend_attr) = self.get_main_period_items()
1.235 + return get_tzid(dtstart_attr, dtend_attr) or self.tzid
1.236
1.237 def is_shared(self):
1.238
1.239 @@ -419,15 +429,16 @@
1.240
1.241 return self.get_value("SEQUENCE") is not None
1.242
1.243 - def possibly_active_from(self, dt, tzid):
1.244 + def possibly_active_from(self, dt):
1.245
1.246 """
1.247 Return whether the object is possibly active from or after the given
1.248 - datetime 'dt' using 'tzid' to convert any dates or floating datetimes.
1.249 + datetime 'dt' using the fallback time zone to convert any dates or
1.250 + floating datetimes.
1.251 """
1.252
1.253 - dt = to_datetime(dt, tzid)
1.254 - periods = self.get_periods(tzid)
1.255 + dt = to_datetime(dt, self.tzid)
1.256 + periods = self.get_periods()
1.257
1.258 for p in periods:
1.259 if p.get_end_point() > dt:
1.260 @@ -457,16 +468,16 @@
1.261
1.262 # Modification methods.
1.263
1.264 - def set_datetime(self, name, dt, tzid=None):
1.265 + def set_datetime(self, name, dt):
1.266
1.267 """
1.268 - Set a datetime for property 'name' using 'dt' and the optional fallback
1.269 - 'tzid', returning whether an update has occurred.
1.270 + Set a datetime for property 'name' using 'dt' and the fallback time zone
1.271 + where necessary, returning whether an update has occurred.
1.272 """
1.273
1.274 if dt:
1.275 old_value = self.get_value(name)
1.276 - self[name] = [get_item_from_datetime(dt, tzid)]
1.277 + self[name] = [get_item_from_datetime(dt, self.tzid)]
1.278 return format_datetime(dt) != old_value
1.279
1.280 return False
1.281 @@ -581,17 +592,14 @@
1.282
1.283 return set(old_exdates) != new_exdates
1.284
1.285 - def correct_object(self, tzid, permitted_values):
1.286 + def correct_object(self, permitted_values):
1.287
1.288 - """
1.289 - Correct the object's period details using the given 'tzid' and
1.290 - 'permitted_values'.
1.291 - """
1.292 + "Correct the object's period details using the 'permitted_values'."
1.293
1.294 corrected = set()
1.295 rdates = []
1.296
1.297 - for period in self.get_periods(tzid):
1.298 + for period in self.get_periods():
1.299 corrected_period = period.get_corrected(permitted_values)
1.300
1.301 if corrected_period is period:
1.302 @@ -674,11 +682,12 @@
1.303
1.304 return ("VFREEBUSY", {}, record)
1.305
1.306 -def parse_calendar(f, encoding):
1.307 +def parse_calendar(f, encoding, tzid=None):
1.308
1.309 """
1.310 Parse the iTIP content from 'f' having the given 'encoding'. Return a
1.311 - mapping from object types to collections of calendar objects.
1.312 + mapping from object types to collections of calendar objects. If 'tzid' is
1.313 + specified, use it to set the fallback time zone on all returned objects.
1.314 """
1.315
1.316 cal = parse_object(f, encoding, "VCALENDAR")
1.317 @@ -687,7 +696,7 @@
1.318 for objtype, values in cal.items():
1.319 d[objtype] = l = []
1.320 for value in values:
1.321 - l.append(Object({objtype : value}))
1.322 + l.append(Object({objtype : value}, tzid))
1.323
1.324 return d
1.325
1.326 @@ -767,12 +776,12 @@
1.327 finally:
1.328 out.close()
1.329
1.330 -def new_object(object_type, organiser=None, organiser_attr=None):
1.331 +def new_object(object_type, organiser=None, organiser_attr=None, tzid=None):
1.332
1.333 """
1.334 Make a new object of the given 'object_type' and optional 'organiser',
1.335 with optional 'organiser_attr' describing any organiser identity in more
1.336 - detail.
1.337 + detail. An optional 'tzid' can also be provided.
1.338 """
1.339
1.340 details = {}
1.341 @@ -782,7 +791,7 @@
1.342 details["ORGANIZER"] = [(organiser, organiser_attr or {})]
1.343 details["DTSTAMP"] = [(get_timestamp(), {})]
1.344
1.345 - return Object({object_type : (details, {})})
1.346 + return Object({object_type : (details, {})}, tzid)
1.347
1.348 def make_uid(user):
1.349
1.350 @@ -1089,13 +1098,13 @@
1.351
1.352 return delegators
1.353
1.354 -def get_periods(obj, tzid, start=None, end=None, inclusive=False):
1.355 +def get_periods(obj, start=None, end=None, inclusive=False):
1.356
1.357 """
1.358 - Return periods for the given object 'obj', employing the given 'tzid' where
1.359 - no time zone information is available (for whole day events, for example),
1.360 - confining materialised periods to after the given 'start' datetime and
1.361 - before the given 'end' datetime.
1.362 + Return periods for the given object 'obj', employing the object's fallback
1.363 + time zone where no time zone information is available (for whole day events,
1.364 + for example), confining materialised periods to after the given 'start'
1.365 + datetime and before the given 'end' datetime.
1.366
1.367 If 'end' is omitted, only explicit recurrences and recurrences from
1.368 explicitly-terminated rules will be returned.
1.369 @@ -1104,12 +1113,13 @@
1.370 will be included.
1.371 """
1.372
1.373 + tzid = obj.get_tzid()
1.374 rrule = obj.get_value("RRULE")
1.375 parameters = rrule and get_parameters(rrule)
1.376
1.377 # Use localised datetimes.
1.378
1.379 - main_period = obj.get_main_period(tzid)
1.380 + main_period = obj.get_main_period()
1.381
1.382 dtstart = main_period.get_start()
1.383 dtstart_attr = main_period.get_start_attr()
1.384 @@ -1176,7 +1186,7 @@
1.385
1.386 # Add recurrence dates.
1.387
1.388 - rdates = obj.get_date_value_item_periods("RDATE", tzid)
1.389 + rdates = obj.get_date_value_item_periods("RDATE")
1.390 if rdates:
1.391 periods += rdates
1.392
1.393 @@ -1186,7 +1196,7 @@
1.394
1.395 # Exclude exception dates.
1.396
1.397 - exdates = obj.get_date_value_item_periods("EXDATE", tzid)
1.398 + exdates = obj.get_date_value_item_periods("EXDATE")
1.399
1.400 if exdates:
1.401 for period in exdates: