1.1 --- a/EventAggregatorSupport.py Sat Mar 13 16:00:03 2010 +0100
1.2 +++ b/EventAggregatorSupport.py Sun Mar 14 02:32:51 2010 +0100
1.3 @@ -21,6 +21,11 @@
1.4 except NameError:
1.5 from sets import Set as set
1.6
1.7 +try:
1.8 + import pytz
1.9 +except ImportError:
1.10 + pytz = None
1.11 +
1.12 __version__ = "0.6"
1.13
1.14 # Date labels.
1.15 @@ -40,8 +45,19 @@
1.16
1.17 # Value parsing.
1.18
1.19 -date_regexp = re.compile(ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})', re.UNICODE)
1.20 -month_regexp = re.compile(ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})', re.UNICODE)
1.21 +country_code_regexp = re.compile(ur'(?:^|\s)(?P<code>[A-Z]{2})(?:$|\s)', re.UNICODE)
1.22 +
1.23 +month_regexp_str = ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})'
1.24 +date_regexp_str = ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})'
1.25 +time_regexp_str = ur'(?P<hour>[0-2][0-9]):(?P<minute>[0-5][0-9])(?::(?P<second>[0-6][0-9]))?'
1.26 +timezone_regexp_str = ur'(?P<zone>[A-Z]{3,}|[a-zA-Z]+/[-_a-zA-Z]+)'
1.27 +datetime_regexp_str = date_regexp_str + ur'(?:\s+' + time_regexp_str + ur'(?:\s+' + timezone_regexp_str + ur')?)?'
1.28 +
1.29 +month_regexp = re.compile(month_regexp_str, re.UNICODE)
1.30 +date_regexp = re.compile(date_regexp_str, re.UNICODE)
1.31 +time_regexp = re.compile(time_regexp_str, re.UNICODE)
1.32 +datetime_regexp = re.compile(datetime_regexp_str, re.UNICODE)
1.33 +
1.34 verbatim_regexp = re.compile(ur'(?:'
1.35 ur'<<Verbatim\((?P<verbatim>.*?)\)>>'
1.36 ur'|'
1.37 @@ -70,6 +86,12 @@
1.38 category_regexp = re.compile(u'^%s$' % ur'(?P<all>Category(?P<key>(?!Template)\S+))', re.UNICODE)
1.39 return category_regexp
1.40
1.41 +def int_or_none(x):
1.42 + if x is None:
1.43 + return x
1.44 + else:
1.45 + return int(x)
1.46 +
1.47 # Textual representations.
1.48
1.49 def getHTTPTimeString(tmtuple):
1.50 @@ -300,7 +322,7 @@
1.51
1.52 # Labels which may well be quoted.
1.53
1.54 - elif term in ("title", "summary", "description"):
1.55 + elif term in ("title", "summary", "description", "location"):
1.56 desc = getSimpleWikiText(desc)
1.57
1.58 if desc is not None:
1.59 @@ -309,11 +331,22 @@
1.60 # details.
1.61
1.62 if details.has_key(term):
1.63 +
1.64 + # Perform additional event configuration.
1.65 +
1.66 + self.events[-1].fixTimeZone()
1.67 +
1.68 + # Make a new event.
1.69 +
1.70 details = {}
1.71 self.events.append(Event(self, details))
1.72
1.73 details[term] = desc
1.74
1.75 + # Perform additional event configuration.
1.76 +
1.77 + self.events[-1].fixTimeZone()
1.78 +
1.79 return self.events
1.80
1.81 def setEvents(self, events):
1.82 @@ -398,16 +431,16 @@
1.83 # Lists (whose elements may be quoted).
1.84
1.85 elif term in ("topics", "categories"):
1.86 - desc = ", ".join(getEncodedWikiText(event_details[term]))
1.87 + desc = ", ".join([getEncodedWikiText(item) for item in event_details[term]])
1.88
1.89 - # Labels which may well be quoted.
1.90 + # Labels which must be quoted.
1.91
1.92 elif term in ("title", "summary"):
1.93 desc = getEncodedWikiText(event_details[term])
1.94
1.95 # Text which need not be quoted, but it will be Wiki text.
1.96
1.97 - elif term in ("description", "link"):
1.98 + elif term in ("description", "link", "location"):
1.99 desc = event_details[term]
1.100
1.101 replaced_terms.add(term)
1.102 @@ -462,6 +495,16 @@
1.103 self.page = page
1.104 self.details = details
1.105
1.106 + def fixTimeZone(self):
1.107 +
1.108 + # Combine location and time zone information.
1.109 +
1.110 + location = self.details.get("location")
1.111 +
1.112 + if location:
1.113 + self.details["start"].apply_location(location)
1.114 + self.details["end"].apply_location(location)
1.115 +
1.116 def __cmp__(self, other):
1.117
1.118 """
1.119 @@ -756,28 +799,41 @@
1.120 def months(self):
1.121 return self.data[0] * 12 + self.data[1]
1.122
1.123 -class Month:
1.124 +class Temporal:
1.125
1.126 - "A simple year-month representation."
1.127 + "A simple temporal representation, common to dates and times."
1.128
1.129 def __init__(self, data):
1.130 - self.data = tuple(data)
1.131 + self.data = list(data)
1.132
1.133 def __repr__(self):
1.134 return "%s(%r)" % (self.__class__.__name__, self.data)
1.135
1.136 - def __str__(self):
1.137 - return "%04d-%02d" % self.as_tuple()[:2]
1.138 -
1.139 def __hash__(self):
1.140 return hash(self.as_tuple())
1.141
1.142 def as_tuple(self):
1.143 - return self.data
1.144 + return tuple(self.data)
1.145 +
1.146 + def __cmp__(self, other):
1.147 + data = self.as_tuple()
1.148 + other_data = other.as_tuple()
1.149 + length = min(len(data), len(other_data))
1.150 + return cmp(self.data[:length], other.data[:length])
1.151 +
1.152 +class Month(Temporal):
1.153 +
1.154 + "A simple year-month representation."
1.155 +
1.156 + def __str__(self):
1.157 + return "%04d-%02d" % self.as_tuple()[:2]
1.158
1.159 def as_date(self, day):
1.160 return Date(self.as_tuple() + (day,))
1.161
1.162 + def as_month(self):
1.163 + return self
1.164 +
1.165 def year(self):
1.166 return self.data[0]
1.167
1.168 @@ -791,14 +847,14 @@
1.169 days, as a tuple.
1.170 """
1.171
1.172 - year, month = self.data
1.173 + year, month = self.as_tuple()[:2]
1.174 return calendar.monthrange(year, month)
1.175
1.176 def month_update(self, n=1):
1.177
1.178 "Return the month updated by 'n' months."
1.179
1.180 - year, month = self.data
1.181 + year, month = self.as_tuple()[:2]
1.182 return Month((year + (month - 1 + n) / 12, (month - 1 + n) % 12 + 1))
1.183
1.184 def next_month(self):
1.185 @@ -822,24 +878,31 @@
1.186
1.187 return Period([(x - y) for x, y in zip(self.data, start.data)])
1.188
1.189 - def __cmp__(self, other):
1.190 - return cmp(self.data, other.data)
1.191 + def until(self, start, end, nextfn, prevfn):
1.192 +
1.193 + """
1.194 + Return a collection of units of time by starting from the given 'start'
1.195 + and stepping across intervening units until 'end' is reached, using the
1.196 + given 'nextfn' and 'prevfn' to step from one unit to the next.
1.197 + """
1.198
1.199 - def until(self, end, nextfn, prevfn):
1.200 - month = self
1.201 - months = [month]
1.202 - if month < end:
1.203 - while month < end:
1.204 - month = nextfn(month)
1.205 - months.append(month)
1.206 - elif month > end:
1.207 - while month > end:
1.208 - month = prevfn(month)
1.209 - months.append(month)
1.210 - return months
1.211 + current = start
1.212 + units = [current]
1.213 + if current < end:
1.214 + while current < end:
1.215 + current = nextfn(current)
1.216 + units.append(current)
1.217 + elif current > end:
1.218 + while current > end:
1.219 + current = prevfn(current)
1.220 + units.append(current)
1.221 + return units
1.222
1.223 def months_until(self, end):
1.224 - return self.until(end, Month.next_month, Month.previous_month)
1.225 +
1.226 + "Return the collection of months from this month until 'end'."
1.227 +
1.228 + return self.until(self.as_month(), end.as_month(), Month.next_month, Month.previous_month)
1.229
1.230 class Date(Month):
1.231
1.232 @@ -848,6 +911,9 @@
1.233 def __str__(self):
1.234 return "%04d-%02d-%02d" % self.as_tuple()[:3]
1.235
1.236 + def as_date(self):
1.237 + return self
1.238 +
1.239 def as_month(self):
1.240 return Month(self.data[:2])
1.241
1.242 @@ -858,7 +924,7 @@
1.243
1.244 "Return the date following this one."
1.245
1.246 - year, month, day = self.data
1.247 + year, month, day = self.as_tuple()[:3]
1.248 _wd, end_day = calendar.monthrange(year, month)
1.249 if day == end_day:
1.250 if month == 12:
1.251 @@ -872,7 +938,7 @@
1.252
1.253 "Return the date preceding this one."
1.254
1.255 - year, month, day = self.data
1.256 + year, month, day = self.as_tuple()[:3]
1.257 if day == 1:
1.258 if month == 1:
1.259 return Date((year - 1, 12, 31))
1.260 @@ -883,15 +949,114 @@
1.261 return Date((year, month, day - 1))
1.262
1.263 def days_until(self, end):
1.264 - return self.until(end, Date.next_day, Date.previous_day)
1.265 +
1.266 + "Return the collection of days from this date until 'end'."
1.267 +
1.268 + return self.until(self.as_date(), end.as_date(), Date.next_day, Date.previous_day)
1.269 +
1.270 +class DateTime(Date):
1.271 +
1.272 + "A simple date plus time representation."
1.273 +
1.274 + def __init__(self, data):
1.275 + Date.__init__(self, data)
1.276 + self.utc_offset = None
1.277 +
1.278 + def __str__(self):
1.279 + if self.has_time():
1.280 + data = self.as_tuple()
1.281 + time_str = " %02d:%02d" % data[3:5]
1.282 + if data[5] is not None:
1.283 + time_str += ":%02d" % data[5]
1.284 + if data[6] is not None:
1.285 + time_str += " %s" % data[6]
1.286 + else:
1.287 + time_str = ""
1.288 +
1.289 + return Date.__str__(self) + time_str
1.290 +
1.291 + def as_date(self):
1.292 + return Date(self.data[:3])
1.293 +
1.294 + def has_time(self):
1.295 + return self.data[3] is not None and self.data[4] is not None
1.296 +
1.297 + def seconds(self):
1.298 + return self.data[5]
1.299 +
1.300 + def time_zone(self):
1.301 + return self.data[6]
1.302 +
1.303 + def set_time_zone(self, value, utc_offset=None):
1.304 + self.data[6] = value
1.305 + self.utc_offset = utc_offset
1.306 +
1.307 + def padded(self):
1.308 +
1.309 + "Return a datetime with missing fields defined as being zero."
1.310 +
1.311 + data = map(lambda x: x or 0, self.data[:6]) + self.data[6:]
1.312 + return DateTime(data)
1.313 +
1.314 + def apply_location(self, location):
1.315 +
1.316 + """
1.317 + Apply 'location' information, setting the time zone if none is already
1.318 + set.
1.319 + """
1.320 +
1.321 + if not self.time_zone():
1.322 +
1.323 + # Only try and set a time zone if pytz is present and able to
1.324 + # suggest one.
1.325 +
1.326 + if pytz is not None:
1.327 +
1.328 + # Find a country code in the location.
1.329 +
1.330 + match = country_code_regexp.search(location)
1.331 +
1.332 + if match:
1.333 +
1.334 + # Attempt to discover zones for that country.
1.335 +
1.336 + try:
1.337 + zones = pytz.country_timezones(match.group("code"))
1.338 +
1.339 + # Unambiguous choice of zone.
1.340 +
1.341 + if len(zones) == 1:
1.342 + self.set_time_zone(zones[0], pytz.timezone(zones[0]).utcoffset(None))
1.343 +
1.344 + # Many potential zones.
1.345 +
1.346 + elif len(zones) > 1:
1.347 + for zone in zones:
1.348 + continent, city = zone.split("/")
1.349 +
1.350 + # If the specific city is mentioned, choose the
1.351 + # zone.
1.352 +
1.353 + if location.find(city) != -1:
1.354 + self.set_time_zone(zone, pytz.timezone(zone).utcoffset(None))
1.355 + break
1.356 + else:
1.357 + self.set_time_zone(zones[0], pytz.timezone(zones[0]).utcoffset(None))
1.358 +
1.359 + except KeyError:
1.360 + pass
1.361
1.362 def getDate(s):
1.363
1.364 - "Parse the string 's', extracting and returning a date object."
1.365 + "Parse the string 's', extracting and returning a datetime object."
1.366
1.367 - m = date_regexp.search(s)
1.368 + m = datetime_regexp.search(s)
1.369 if m:
1.370 - return Date(map(int, m.groups()))
1.371 + groups = list(m.groups())
1.372 +
1.373 + # Convert all but the zone to integer or None.
1.374 +
1.375 + return DateTime(map(int_or_none, groups[:-1]) + groups[-1:])
1.376 else:
1.377 return None
1.378
1.379 @@ -934,13 +1099,33 @@
1.380 # User interface functions.
1.381
1.382 def getParameter(request, name, default=None):
1.383 +
1.384 + """
1.385 + Using the given 'request', return the value of the parameter with the given
1.386 + 'name', returning the optional 'default' (or None) if no value was supplied
1.387 + in the 'request'.
1.388 + """
1.389 +
1.390 return request.form.get(name, [default])[0]
1.391
1.392 def getQualifiedParameter(request, calendar_name, argname, default=None):
1.393 +
1.394 + """
1.395 + Using the given 'request', 'calendar_name' and 'argname', retrieve the
1.396 + value of the qualified parameter, returning the optional 'default' (or None)
1.397 + if no value was supplied in the 'request'.
1.398 + """
1.399 +
1.400 argname = getQualifiedParameterName(calendar_name, argname)
1.401 return getParameter(request, argname, default)
1.402
1.403 def getQualifiedParameterName(calendar_name, argname):
1.404 +
1.405 + """
1.406 + Return the qualified parameter name using the given 'calendar_name' and
1.407 + 'argname'.
1.408 + """
1.409 +
1.410 if calendar_name is None:
1.411 return argname
1.412 else: