MoinSupport

DateSupport.py

46:99b3f4138cc2
2013-03-10 Paul Boddie Added macro argument parsing from MoinForms and a function to find parsers by content type. Changed the item storage API to use path tuples instead of strings when specifying directories. Introduced support for reverse iteration over stored items. Updated the release notes and version number.
     1 # -*- coding: iso-8859-1 -*-     2 """     3     MoinMoin - DateSupport library (derived from EventAggregatorSupport)     4      5     @copyright: 2008, 2009, 2010, 2011, 2012 by Paul Boddie <paul@boddie.org.uk>     6     @license: GNU GPL (v2 or later), see COPYING.txt for details.     7 """     8      9 import calendar    10 import datetime    11 import re    12 import bisect    13     14 try:    15     import pytz    16 except ImportError:    17     pytz = None    18     19 __version__ = "0.1"    20     21 # Date labels.    22     23 month_labels = ["January", "February", "March", "April", "May", "June",    24     "July", "August", "September", "October", "November", "December"]    25 weekday_labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]    26     27 # Month, date, time and datetime parsing.    28     29 month_regexp_str = ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})'    30 date_regexp_str = ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})'    31 time_regexp_str = ur'(?P<hour>[0-2][0-9]):(?P<minute>[0-5][0-9])(?::(?P<second>[0-6][0-9]))?'    32 timezone_offset_str = ur'(?P<offset>(UTC)?(?:(?P<sign>[-+])?(?P<hours>[0-9]{2})(?::?(?P<minutes>[0-9]{2}))?))'    33 timezone_olson_str = ur'(?P<olson>[a-zA-Z]+(?:/[-_a-zA-Z]+){1,2})'    34 timezone_utc_str = ur'UTC'    35 timezone_regexp_str = ur'(?P<zone>' + timezone_offset_str + '|' + timezone_olson_str + '|' + timezone_utc_str + ')'    36 datetime_regexp_str = date_regexp_str + ur'(?:\s+' + time_regexp_str + ur'(?:\s+' + timezone_regexp_str + ur')?)?'    37     38 month_regexp = re.compile(month_regexp_str, re.UNICODE)    39 date_regexp = re.compile(date_regexp_str, re.UNICODE)    40 time_regexp = re.compile(time_regexp_str, re.UNICODE)    41 timezone_olson_regexp = re.compile(timezone_olson_str, re.UNICODE)    42 timezone_offset_regexp = re.compile(timezone_offset_str, re.UNICODE)    43 datetime_regexp = re.compile(datetime_regexp_str, re.UNICODE)    44     45 # iCalendar date and datetime parsing.    46     47 date_icalendar_regexp_str = ur'(?P<year>[0-9]{4})(?P<month>[0-9]{2})(?P<day>[0-9]{2})'    48 datetime_icalendar_regexp_str = date_icalendar_regexp_str + \    49     ur'(?:' \    50     ur'T(?P<hour>[0-2][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-6][0-9])' \    51     ur'(?P<utc>Z)?' \    52     ur')?'    53     54 date_icalendar_regexp = re.compile(date_icalendar_regexp_str, re.UNICODE)    55 datetime_icalendar_regexp = re.compile(datetime_icalendar_regexp_str, re.UNICODE)    56     57 # Utility functions.    58     59 def sign(x):    60     if x < 0:    61         return -1    62     else:    63         return 1    64     65 def int_or_none(x):    66     if x is None:    67         return x    68     else:    69         return int(x)    70     71 def getMonthLabel(month):    72     73     "Return an unlocalised label for the given 'month'."    74     75     return month_labels[month - 1] # zero-based labels    76     77 def getDayLabel(weekday):    78     79     "Return an unlocalised label for the given 'weekday'."    80     81     return weekday_labels[weekday]    82     83 # Interfaces.    84     85 class ActsAsTimespan:    86     pass    87     88 # Date-related functions.    89     90 def cmp_dates_as_day_start(a, b):    91     92     """    93     Compare dates/datetimes 'a' and 'b' treating dates without time information    94     as the earliest time in a particular day.    95     """    96     97     are_equal = a == b    98     99     if are_equal:   100         a2 = a.as_datetime_or_date()   101         b2 = b.as_datetime_or_date()   102    103         if isinstance(a2, Date) and isinstance(b2, DateTime):   104             return -1   105         elif isinstance(a2, DateTime) and isinstance(b2, Date):   106             return 1   107    108     return cmp(a, b)   109    110 class Convertible:   111    112     "Support for converting temporal objects."   113    114     def _get_converter(self, resolution):   115         if resolution == "month":   116             return lambda x: x and x.as_month()   117         elif resolution == "date":   118             return lambda x: x and x.as_date()   119         elif resolution == "datetime":   120             return lambda x: x and x.as_datetime_or_date()   121         else:   122             return lambda x: x   123    124 class Temporal(Convertible):   125    126     "A simple temporal representation, common to dates and times."   127    128     def __init__(self, data):   129         self.data = list(data)   130    131     def __repr__(self):   132         return "%s(%r)" % (self.__class__.__name__, self.data)   133    134     def __hash__(self):   135         return hash(self.as_tuple())   136    137     def as_tuple(self):   138         return tuple(self.data)   139    140     def convert(self, resolution):   141         return self._get_converter(resolution)(self)   142    143     def __cmp__(self, other):   144    145         """   146         The result of comparing this instance with 'other' is derived from a   147         comparison of the instances' date(time) data at the highest common   148         resolution, meaning that if a date is compared to a datetime, the   149         datetime will be considered as a date. Thus, a date and a datetime   150         referring to the same date will be considered equal.   151         """   152    153         if not isinstance(other, Temporal):   154             return NotImplemented   155         else:   156             data = self.as_tuple()   157             other_data = other.as_tuple()   158             length = min(len(data), len(other_data))   159             return cmp(data[:length], other_data[:length])   160    161     def __sub__(self, other):   162    163         """   164         Return the difference between this object and the 'other' object at the   165         highest common accuracy of both objects.   166         """   167    168         if not isinstance(other, Temporal):   169             return NotImplemented   170         else:   171             data = self.as_tuple()   172             other_data = other.as_tuple()   173             direction = self < other and 1 or -1   174    175             if len(data) < len(other_data):   176                 return (len(self.until(other)) - 1) * direction   177             else:   178                 return (len(other.until(self)) - 1) * -direction   179    180     def _until(self, start, end, nextfn, prevfn):   181    182         """   183         Return a collection of units of time by starting from the given 'start'   184         and stepping across intervening units until 'end' is reached, using the   185         given 'nextfn' and 'prevfn' to step from one unit to the next.   186         """   187    188         current = start   189         units = [current]   190         if current < end:   191             while current < end:   192                 current = nextfn(current)   193                 units.append(current)   194         elif current > end:   195             while current > end:   196                 current = prevfn(current)   197                 units.append(current)   198         return units   199    200     def ambiguous(self):   201    202         "Only times can be ambiguous."   203    204         return 0   205    206 class Month(Temporal):   207    208     "A simple year-month representation."   209    210     def __str__(self):   211         return "%04d-%02d" % self.as_tuple()[:2]   212    213     def as_datetime(self, day, hour, minute, second, zone):   214         return DateTime(self.as_tuple() + (day, hour, minute, second, zone))   215    216     def as_date(self, day):   217         if day < 0:   218             weekday, ndays = self.month_properties()   219             day = ndays + 1 + day   220         return Date(self.as_tuple() + (day,))   221    222     def as_month(self):   223         return self   224    225     def year(self):   226         return self.data[0]   227    228     def month(self):   229         return self.data[1]   230    231     def month_properties(self):   232    233         """   234         Return the weekday of the 1st of the month, along with the number of   235         days, as a tuple.   236         """   237    238         year, month = self.as_tuple()[:2]   239         return calendar.monthrange(year, month)   240    241     def month_update(self, n=1):   242    243         "Return the month updated by 'n' months."   244    245         year, month = self.as_tuple()[:2]   246         return Month((year + (month - 1 + n) / 12, (month - 1 + n) % 12 + 1))   247    248     update = month_update   249    250     def next_month(self):   251    252         "Return the month following this one."   253    254         return self.month_update(1)   255    256     next = next_month   257    258     def previous_month(self):   259    260         "Return the month preceding this one."   261    262         return self.month_update(-1)   263    264     previous = previous_month   265    266     def months_until(self, end):   267    268         "Return the collection of months from this month until 'end'."   269    270         return self._until(self.as_month(), end.as_month(), Month.next_month, Month.previous_month)   271    272     until = months_until   273    274 class Date(Month):   275    276     "A simple year-month-day representation."   277    278     def constrain(self):   279         year, month, day = self.as_tuple()[:3]   280    281         month = max(min(month, 12), 1)   282         wd, last_day = calendar.monthrange(year, month)   283         day = max(min(day, last_day), 1)   284    285         self.data[1:3] = month, day   286    287     def __str__(self):   288         return "%04d-%02d-%02d" % self.as_tuple()[:3]   289    290     def as_datetime(self, hour, minute, second, zone):   291         return DateTime(self.as_tuple() + (hour, minute, second, zone))   292    293     def as_start_of_day(self):   294         return self.as_datetime(None, None, None, None)   295    296     def as_date(self):   297         return self   298    299     def as_datetime_or_date(self):   300         return self   301    302     def as_month(self):   303         return Month(self.data[:2])   304    305     def day(self):   306         return self.data[2]   307    308     def day_update(self, n=1):   309    310         "Return the month updated by 'n' days."   311    312         delta = datetime.timedelta(n)   313         dt = datetime.date(*self.as_tuple()[:3])   314         dt_new = dt + delta   315         return Date((dt_new.year, dt_new.month, dt_new.day))   316    317     update = day_update   318    319     def next_day(self):   320    321         "Return the date following this one."   322    323         year, month, day = self.as_tuple()[:3]   324         _wd, end_day = calendar.monthrange(year, month)   325         if day == end_day:   326             if month == 12:   327                 return Date((year + 1, 1, 1))   328             else:   329                 return Date((year, month + 1, 1))   330         else:   331             return Date((year, month, day + 1))   332    333     next = next_day   334    335     def previous_day(self):   336    337         "Return the date preceding this one."   338    339         year, month, day = self.as_tuple()[:3]   340         if day == 1:   341             if month == 1:   342                 return Date((year - 1, 12, 31))   343             else:   344                 _wd, end_day = calendar.monthrange(year, month - 1)   345                 return Date((year, month - 1, end_day))   346         else:   347             return Date((year, month, day - 1))   348    349     previous = previous_day   350    351     def days_until(self, end):   352    353         "Return the collection of days from this date until 'end'."   354    355         return self._until(self.as_date(), end.as_date(), Date.next_day, Date.previous_day)   356    357     until = days_until   358    359 class DateTime(Date):   360    361     "A simple date plus time representation."   362    363     def constrain(self):   364         Date.constrain(self)   365    366         hour, minute, second = self.as_tuple()[3:6]   367    368         if self.has_time():   369             hour = max(min(hour, 23), 0)   370             minute = max(min(minute, 59), 0)   371    372             if second is not None:   373                 second = max(min(second, 60), 0) # support leap seconds   374    375             self.data[3:6] = hour, minute, second   376    377     def __str__(self):   378         return Date.__str__(self) + self.time_string()   379    380     def time_string(self, zone_as_offset=0):   381         if self.has_time():   382             data = self.as_tuple()   383             time_str = " %02d:%02d" % data[3:5]   384             if data[5] is not None:   385                 time_str += ":%02d" % data[5]   386             if data[6] is not None:   387                 if zone_as_offset:   388                     utc_offset = self.utc_offset()   389                     if utc_offset:   390                         time_str += " %+03d:%02d" % utc_offset   391                 else:   392                     time_str += " %s" % data[6]   393             return time_str   394         else:   395             return ""   396    397     def as_HTTP_datetime_string(self):   398         weekday = calendar.weekday(*self.data[:3])   399         return "%s, %02d %s %04d %02d:%02d:%02d GMT" % ((   400             getDayLabel(weekday),   401             self.data[2],   402             getMonthLabel(self.data[1]),   403             self.data[0]   404             ) + tuple(self.data[3:6]))   405    406     def as_ISO8601_datetime_string(self):   407         return Date.__str__(self) + self.time_string(1)   408    409     def as_datetime(self):   410         return self   411    412     def as_date(self):   413         return Date(self.data[:3])   414    415     def as_datetime_or_date(self):   416    417         """   418         Return a date for this datetime if fields are missing. Otherwise, return   419         this datetime itself.   420         """   421    422         if not self.has_time():   423             return self.as_date()   424         else:   425             return self   426    427     def __cmp__(self, other):   428    429         """   430         The result of comparing this instance with 'other' is, if both instances   431         are datetime instances, derived from a comparison of the datetimes   432         converted to UTC. If one or both datetimes cannot be converted to UTC,   433         the datetimes are compared using the basic temporal comparison which   434         compares their raw time data.   435         """   436    437         this = self   438    439         if this.has_time():   440             if isinstance(other, DateTime):   441                 if other.has_time():   442                     this_utc = this.to_utc()   443                     other_utc = other.to_utc()   444                     if this_utc is not None and other_utc is not None:   445                         return cmp(this_utc.as_tuple(), other_utc.as_tuple())   446                 else:   447                     other = other.padded()   448         else:   449             this = this.padded()   450    451         return Date.__cmp__(this, other)   452    453     def __sub__(self, other):   454    455         """   456         Return the difference between this object and the 'other' object at the   457         highest common accuracy of both objects.   458         """   459    460         if not isinstance(other, Temporal):   461             return NotImplemented   462         elif not other.has_time():   463             return self.as_date() - other   464         else:   465             utc = self.to_utc()   466             other = other.to_utc()   467             days = utc.as_date() - other.as_date()   468             h1, m1, s1 = utc.as_tuple()[3:6]   469             h2, m2, s2 = other.as_tuple()[3:6]   470             return days * 24 * 3600 + (h1 - h2) * 3600 + (m1 - m2) * 60 + s1 - s2   471    472     def has_time(self):   473    474         """   475         Return whether this object has any time information. Objects without   476         time information can refer to the very start of a day.   477         """   478    479         return self.data[3] is not None and self.data[4] is not None   480    481     def time(self):   482         return self.data[3:]   483    484     def seconds(self):   485         return self.data[5]   486    487     def time_zone(self):   488         return self.data[6]   489    490     def set_time_zone(self, value):   491         self.data[6] = value   492    493     def padded(self, empty_value=0):   494    495         """   496         Return a datetime with missing fields defined as being the given   497         'empty_value' or 0 if not specified.   498         """   499    500         data = []   501         for x in self.data[:6]:   502             if x is None:   503                 data.append(empty_value)   504             else:   505                 data.append(x)   506    507         data += self.data[6:]   508         return DateTime(data)   509    510     def to_utc(self):   511    512         """   513         Return this object converted to UTC, or None if such a conversion is not   514         defined.   515         """   516    517         if not self.has_time():   518             return None   519    520         offset = self.utc_offset()   521         if offset:   522             hours, minutes = offset   523    524             # Invert the offset to get the correction.   525    526             hours, minutes = -hours, -minutes   527    528             # Get the components.   529    530             hour, minute, second, zone = self.time()   531             date = self.as_date()   532    533             # Add the minutes and hours.   534    535             minute += minutes   536             if minute < 0 or minute > 59:   537                 hour += minute / 60   538                 minute = minute % 60   539    540             # NOTE: This makes various assumptions and probably would not work   541             # NOTE: for general arithmetic.   542    543             hour += hours   544             if hour < 0:   545                 date = date.previous_day()   546                 hour += 24   547             elif hour > 23:   548                 date = date.next_day()   549                 hour -= 24   550    551             return date.as_datetime(hour, minute, second, "UTC")   552    553         # Cannot convert.   554    555         else:   556             return None   557    558     def utc_offset(self):   559    560         "Return the UTC offset in hours and minutes."   561    562         zone = self.time_zone()   563         if not zone:   564             return None   565    566         # Support explicit UTC zones.   567    568         if zone == "UTC":   569             return 0, 0   570    571         # Attempt to return a UTC offset where an explicit offset has been set.   572    573         match = timezone_offset_regexp.match(zone)   574         if match:   575             if match.group("sign") == "-":   576                 offset_sign = -1   577             else:   578                 offset_sign = 1   579    580             hours = int(match.group("hours")) * offset_sign   581             minutes = int(match.group("minutes") or 0) * offset_sign   582             return hours, minutes   583    584         # Attempt to handle Olson time zone identifiers.   585    586         dt = self.as_olson_datetime()   587         if dt:   588             seconds = dt.utcoffset().seconds + dt.utcoffset().days * 24 * 3600   589             hours = abs(seconds) / 3600   590             minutes = (abs(seconds) % 3600) / 60   591             return sign(seconds) * hours, sign(seconds) * minutes   592    593         # Otherwise return None.   594    595         return None   596    597     def olson_identifier(self):   598    599         "Return the Olson identifier from any zone information."   600    601         zone = self.time_zone()   602         if not zone:   603             return None   604    605         # Attempt to match an identifier.   606    607         match = timezone_olson_regexp.match(zone)   608         if match:   609             return match.group("olson")   610         else:   611             return None   612    613     def _as_olson_datetime(self, hours=None):   614    615         """   616         Return a Python datetime object for this datetime interpreted using any   617         Olson time zone identifier and the given 'hours' offset, raising one of   618         the pytz exceptions in case of ambiguity.   619         """   620    621         olson = self.olson_identifier()   622         if olson and pytz:   623             tz = pytz.timezone(olson)   624             data = self.padded().as_tuple()[:6]   625             dt = datetime.datetime(*data)   626    627             # With an hours offset, find a time probably in a previously   628             # applicable time zone.   629    630             if hours is not None:   631                 td = datetime.timedelta(0, hours * 3600)   632                 dt += td   633    634             ldt = tz.localize(dt, None)   635    636             # With an hours offset, adjust the time to define it within the   637             # previously applicable time zone but at the presumably intended   638             # position.   639    640             if hours is not None:   641                 ldt -= td   642    643             return ldt   644         else:   645             return None   646    647     def as_olson_datetime(self):   648    649         """   650         Return a Python datetime object for this datetime interpreted using any   651         Olson time zone identifier, choosing the time from the zone before the   652         period of ambiguity.   653         """   654    655         try:   656             return self._as_olson_datetime()   657         except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError):   658    659             # Try again, using an earlier local time and then stepping forward   660             # in the chosen zone.   661             # NOTE: Four hours earlier seems reasonable.   662    663             return self._as_olson_datetime(-4)   664    665     def ambiguous(self):   666    667         "Return whether the time is local and ambiguous."   668    669         try:   670             self._as_olson_datetime()   671         except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError):   672             return 1   673    674         return 0   675    676 class Timespan(ActsAsTimespan, Convertible):   677    678     """   679     A period of time which can be compared against others to check for overlaps.   680     """   681    682     def __init__(self, start, end):   683         self.start = start   684         self.end = end   685    686         # NOTE: Should perhaps catch ambiguous time problems elsewhere.   687    688         if self.ambiguous() and self.start is not None and self.end is not None and start > end:   689             self.start, self.end = end, start   690    691     def __repr__(self):   692         return "%s(%r, %r)" % (self.__class__.__name__, self.start, self.end)   693    694     def __hash__(self):   695         return hash((self.start, self.end))   696    697     def as_timespan(self):   698         return self   699    700     def as_limits(self):   701         return self.start, self.end   702    703     def ambiguous(self):   704         return self.start is not None and self.start.ambiguous() or self.end is not None and self.end.ambiguous()   705    706     def convert(self, resolution):   707         return Timespan(*map(self._get_converter(resolution), self.as_limits()))   708    709     def is_before(self, a, b):   710    711         """   712         Return whether 'a' is before 'b'. Since the end datetime of one period   713         may be the same as the start datetime of another period, and yet the   714         first period is intended to be concluded by the end datetime and not   715         overlap with the other period, a different test is employed for datetime   716         comparisons.   717         """   718    719         # Datetimes without times can be equal to dates and be considered as   720         # occurring before those dates. Generally, datetimes should not be   721         # produced without time information as getDateTime converts such   722         # datetimes to dates.   723    724         if isinstance(a, DateTime) and (isinstance(b, DateTime) or not a.has_time()):   725             return a <= b   726         else:   727             return a < b   728    729     def __contains__(self, other):   730    731         """   732         This instance is considered to contain 'other' if one is not before or   733         after the other. If this instance overlaps or coincides with 'other',   734         then 'other' is regarded as belonging to this instance's time period.   735         """   736    737         return self == other   738    739     def __cmp__(self, other):   740    741         """   742         Return whether this timespan occupies the same period of time as the   743         'other'. Timespans are considered less than others if their end points   744         precede the other's start point, and are considered greater than others   745         if their start points follow the other's end point.   746         """   747    748         if isinstance(other, ActsAsTimespan):   749             other = other.as_timespan()   750    751             if self.end is not None and other.start is not None and self.is_before(self.end, other.start):   752                 return -1   753             elif self.start is not None and other.end is not None and self.is_before(other.end, self.start):   754                 return 1   755             else:   756                 return 0   757    758         else:   759             if self.end is not None and self.is_before(self.end, other):   760                 return -1   761             elif self.start is not None and self.is_before(other, self.start):   762                 return 1   763             else:   764                 return 0   765    766 class TimespanCollection:   767    768     """   769     A class providing a list-like interface supporting membership tests at a   770     particular resolution in order to maintain a collection of non-overlapping   771     timespans.   772     """   773    774     def __init__(self, resolution, values=None):   775         self.resolution = resolution   776         self.values = values or []   777    778     def as_timespan(self):   779         return Timespan(*self.as_limits())   780    781     def as_limits(self):   782    783         "Return the earliest and latest points in time for this collection."   784    785         if not self.values:   786             return None, None   787         else:   788             first, last = self.values[0], self.values[-1]   789             if isinstance(first, ActsAsTimespan):   790                 first = first.as_timespan().start   791             if isinstance(last, ActsAsTimespan):   792                 last = last.as_timespan().end   793             return first, last   794    795     def convert(self, value):   796         if isinstance(value, ActsAsTimespan):   797             ts = value.as_timespan()   798             return ts and ts.convert(self.resolution)   799         else:   800             return value.convert(self.resolution)   801    802     def __iter__(self):   803         return iter(self.values)   804    805     def __len__(self):   806         return len(self.values)   807    808     def __getitem__(self, i):   809         return self.values[i]   810    811     def __setitem__(self, i, value):   812         self.values[i] = value   813    814     def __contains__(self, value):   815         test_value = self.convert(value)   816         return test_value in self.values   817    818     def append(self, value):   819         self.values.append(value)   820    821     def insert(self, i, value):   822         self.values.insert(i, value)   823    824     def pop(self):   825         return self.values.pop()   826    827     def insert_in_order(self, value):   828         bisect.insort_left(self, value)   829    830 def getDate(s):   831    832     "Parse the string 's', extracting and returning a date object."   833    834     dt = getDateTime(s)   835     if dt is not None:   836         return dt.as_date()   837     else:   838         return None   839    840 def getDateTime(s):   841    842     """   843     Parse the string 's', extracting and returning a datetime object where time   844     information has been given or a date object where time information is   845     absent.   846     """   847    848     m = datetime_regexp.search(s)   849     if m:   850         groups = list(m.groups())   851    852         # Convert date and time data to integer or None.   853    854         return DateTime(map(int_or_none, groups[:6]) + [m.group("zone")]).as_datetime_or_date()   855     else:   856         return None   857    858 def getDateFromCalendar(s):   859    860     """   861     Parse the iCalendar format string 's', extracting and returning a date   862     object.   863     """   864    865     dt = getDateTimeFromCalendar(s)   866     if dt is not None:   867         return dt.as_date()   868     else:   869         return None   870    871 def getDateTimeFromCalendar(s):   872    873     """   874     Parse the iCalendar format datetime string 's', extracting and returning a   875     datetime object where time information has been given or a date object where   876     time information is absent.   877     """   878    879     m = datetime_icalendar_regexp.search(s)   880     if m:   881         groups = list(m.groups())   882    883         # Convert date and time data to integer or None.   884    885         return DateTime(map(int_or_none, groups[:6]) + [m.group("utc") and "UTC" or None]).as_datetime_or_date()   886     else:   887         return None   888    889 def getDateStrings(s):   890    891     "Parse the string 's', extracting and returning all date strings."   892    893     start = 0   894     m = date_regexp.search(s, start)   895     l = []   896     while m:   897         l.append("-".join(m.groups()))   898         m = date_regexp.search(s, m.end())   899     return l   900    901 def getMonth(s):   902    903     "Parse the string 's', extracting and returning a month object."   904    905     m = month_regexp.search(s)   906     if m:   907         return Month(map(int, m.groups()))   908     else:   909         return None   910    911 def getCurrentDate():   912    913     "Return the current date as a Date instance."   914    915     today = datetime.date.today()   916     return Date((today.year, today.month, today.day))   917    918 def getCurrentMonth():   919    920     "Return the current month as a Month instance."   921    922     today = datetime.date.today()   923     return Month((today.year, today.month))   924    925 def getCurrentYear():   926    927     "Return the current year."   928    929     today = datetime.date.today()   930     return today.year   931    932 def getCurrentTime():   933    934     "Return the current time as a DateTime instance."   935    936     now = datetime.datetime.utcnow()   937     return DateTime((now.year, now.month, now.day, now.hour, now.minute, now.second, "UTC"))   938    939 # vim: tabstop=4 expandtab shiftwidth=4