Contact: fumanchu@aminus.org

Log in as guest/dejavu to create tickets

I think I've seen this ORM somewhere before...

root/trunk/recur.py

Revision 204 (checked in by fumanchu, 7 years ago)

Updated recur module.

  • Property svn:eol-style set to native
Line 
1 """
2 Iterators for datetime objects.
3
4 This work, including the source code, documentation
5 and related data, is placed into the public domain.
6
7 The original author is Robert Brewer, Amor Ministries.
8 See http://projects.amor.org/misc/wiki/Recur for more docs.
9
10 THIS SOFTWARE IS PROVIDED AS-IS, WITHOUT WARRANTY
11 OF ANY KIND, NOT EVEN THE IMPLIED WARRANTY OF
12 MERCHANTABILITY. THE AUTHOR OF THIS SOFTWARE
13 ASSUMES _NO_ RESPONSIBILITY FOR ANY CONSEQUENCE
14 RESULTING FROM THE USE, MODIFICATION, OR
15 REDISTRIBUTION OF THIS SOFTWARE.
16
17 Western-language descriptions of recurrence tend to fall into
18 two distinct types. In order to provide some mnemonic consistency,
19 the base functions are named differently according to these types.
20 However, despite the differing names, every function yields
21 datetime.date or datetime.datetime objects.
22
23 First, there are the declarations which define a unit of time,
24 and then count successive "leaps" of those units. For example,
25 the declaration, "every 4 days," uses a day as the unit, and adds
26 4 to produce each value in the series. The functions which provide
27 these series are named according to the whole unit, in the plural.
28 Examples:
29     "Every 4 days" becomes: days(start, 4, [end])
30     "Every 2 weeks" becomes: weeks(start, 2, [end])
31     "Every 6 hours" becomes: hours(start, 6, [end])
32
33 Second, there are the declarations which define a unit of time,
34 and then count by subdivisions of that unit. For example, the
35 declaration, "the ninth day of each month," uses a month as the
36 whole units and a day as the subdivision. The functions which
37 provide these series are named according to the whole unit,
38 in the singular, prefixed by "each".
39 Examples:
40     "The ninth [day] of each month" becomes: eachmonth(start, 9, [end])
41     "The penultimate [day] of each month" becomes:
42         eachmonth(start, -1, [end])
43     "Every Thursday" becomes "The 3rd [day] of each week" [since
44         datetime.weekday() returns Thursday as the value 3]
45         which becomes: eachweek(start, 3, [end])
46     "08:30:00 on each day" becomes:
47         eachday(start, datetime.time(8, 30), [end])
48 Notice that, in almost every case, the subdivision is understood to be
49 the "next smallest component". In the example above, one might just as
50 well have written, "the ninth of each month," and been understood,
51 since months are "composed of" days (not weeks!). Therefore, our
52 functions do not incorporate this "smaller unit" in the function name.
53 """
54
55 import datetime
56 import re
57 import threading
58
59
60 def sane_date(year, month, day, fixMonth=False):
61     """Return a valid datetime.date even if parameters are out of bounds.
62     
63     If the month param is out of bounds, both it and the year will
64     be modified. If negative, the year will be decremented.
65     
66     If fixMonth is False, and the day param is out of bounds, both the
67     day param and the month will be modified.
68     
69     If fixMonth is True, and the day param is out of bounds, the month
70     will not change, and the day will be set to the appropriate boundary.
71     The month may still, however, modify the year.
72     
73     Examples:
74         sane_date(2003, 2, 1) = datetime.date(2003, 2, 1)
75         sane_date(2003, -10, 13) = datetime.date(2002, 2, 13)
76         sane_date(2003, 12, -5) = datetime.date(2003, 11, 25)
77         sane_date(2003, 1, 35, True) = datetime.date(2003, 1, 31)
78     """
79     while month > 12:
80         month -= 12
81         year += 1
82     while month < 1:
83         month += 12
84         year -= 1
85     if fixMonth:
86         if day < 1:
87             newDate = datetime.date(year, month, 1)
88         else:
89             while True:
90                 try:
91                     newDate = datetime.date(year, month, day)
92                 except ValueError:
93                     day -= 1
94                     if day < 1:
95                         raise ValueError("A valid day for month: %s in "
96                                          "year: %s could not be found",
97                                          (month, year))
98                 else:
99                     break
100     else:
101         if day < 1:
102             # Count backward from the end of the current month.
103             firstOfMonth = sane_date(year, month + 1, 1)
104         else:
105             # Count forward from the first of the current month.
106             firstOfMonth = datetime.date(year, month, 1)
107         newDate = (firstOfMonth + datetime.timedelta(day - 1))
108     return newDate
109
110 def sane_time(day, hour, minute, second):
111     """Return a valid (day, datetime.time) even if parameters are out of bounds.
112     
113     If the hour param is out of bounds, both it and the day will
114     be modified. If negative, the day will be decremented.
115     
116     If the minute param is out of bounds, both it and the hour will
117     be modified. If negative, the hour will be decremented.
118     
119     If the second param is out of bounds, both it and the minute will
120     be modified. If negative, the minute will be decremented.
121     
122     Examples:
123         sane_time(0, 4, 2, 1) = (0, datetime.time(4, 2, 1)
124         sane_time(0, 25, 2, 1) = (1, datetime.time(1, 2, 1)
125         sane_time(0, 4, 1440, 1) = (1, datetime.time(4, 2, 1)
126         sane_time(0, 0, 0, -1) = (-1, datetime.time(23, 59, 59)
127     """
128     while second > 59:
129         second -= 60
130         minute += 1
131     while second < 0:
132         second += 60
133         minute -= 1
134     while minute > 59:
135         minute -= 60
136         hour += 1
137     while minute < 0:
138         minute += 60
139         hour -= 1
140     while hour > 23:
141         hour -= 24
142         day += 1
143     while hour < 0:
144         hour += 24
145         day -= 1
146     newTime = (day, datetime.time(hour, minute, second))
147     return newTime
148
149 def seconds(startDate, frequency=1, endDate=None):
150     """Yield a sequence of datetimes, adding 'frequency' seconds each time.
151     
152     For example:
153         seconds(datetime.datetime(2004, 5, 4, 14, 0), 6)
154     yields the sequence: 2004-05-04 14:00:00, 2004-05-04 14:00:06,
155                          2004-05-04 14:00:12, ...
156     
157     If startDate has no time component (i.e. if it is a datetime.date),
158     then the first yielded time will be midnight (0:00:00) on that date.
159     
160     If endDate has no time component (i.e. if it is a datetime.date),
161     then the last yielded time will be the last valid time before
162     midnight on that date.
163     
164     For example:
165         seconds(datetime.datetime(2004, 5, 4), 15, datetime.datetime(2004, 5, 5))
166     yields the sequence: 2004-05-04 00:00:00, 2004-05-04 00:00:15,
167                          2004-05-04 00:00:30, ...
168                                           ... 2004-05-05 23:59:15,
169                          2004-05-05 23:59:30, 2004-05-05 23:59:45.
170     """
171     if not hasattr(startDate, u'time'):
172         startDate = datetime.datetime.combine(startDate, datetime.time(0))
173     while (endDate is None) or (startDate <= endDate):
174         yield startDate
175         startDate += datetime.timedelta(seconds=frequency)
176
177 def eachminute(startDate, seconds=0, endDate=None):
178     """Yield the same time for each minute. Defaults to 0 seconds.
179     
180     Yielded values are datetime.datetime objects.
181     For example:
182         eachminute(datetime.date(2004, 5, 4, 23, 55), 15)
183     yields the sequence: 2004-05-04 23:55:15, 2004-05-04 23:56:15,
184                          2004-05-04 23:57:15, ...
185     
186     If startDate has no time component (i.e. if it is a datetime.date),
187     then the first yielded time will be the first valid time after
188     midnight (0:00:00) on that date.
189     
190     If endDate has no time component (i.e. if it is a datetime.date),
191     then the last yielded time will be the last valid time before
192     midnight on that date.
193     """
194     seconds = int(seconds)
195    
196     if hasattr(startDate, u'time'):
197         days, zerotime = sane_time(0, startDate.hour,
198                                    startDate.minute, seconds)
199         if days < 0 or zerotime < startDate.time():
200             days, zerotime = sane_time(0, startDate.hour,
201                                       startDate.minute + 1, seconds)
202     else:
203         days, zerotime = sane_time(0, 0, 0, seconds)
204     startDate = sane_date(startDate.year, startDate.month,
205                           startDate.day + days)
206     startDate = datetime.datetime.combine(startDate, zerotime)
207    
208     while (endDate is None) or (startDate <= endDate):
209         yield startDate
210         startDate += datetime.timedelta(minutes=1)
211
212 def minutes(startDate, frequency=1, endDate=None):
213     """Yield a sequence of datetimes, adding 'frequency' minutes each time.
214     
215     For example:
216         minutes(datetime.datetime(2004, 5, 4, 14), 30)
217     yields the sequence: 2004-05-04 14:00:00, 2004-05-04 14:30:00,
218                          2004-05-04 15:00:00, ...
219     
220     If startDate has no time component (i.e. if it is a datetime.date),
221     then the first yielded time will be midnight (0:00:00) on that date.
222     
223     If endDate has no time component (i.e. if it is a datetime.date),
224     then the last yielded time will be the last valid time before
225     midnight on that date.
226     
227     For example:
228         minutes(datetime.datetime(2004, 5, 4), 15, datetime.datetime(2004, 5, 5))
229     yields the sequence: 2004-05-04 00:00:00, 2004-05-04 00:15:00,
230                          2004-05-04 00:30:00, ...
231                                           ... 2004-05-05 23:15:00,
232                          2004-05-05 23:30:00, 2004-05-05 23:45:00.
233     """
234     if not hasattr(startDate, u'time'):
235         startDate = datetime.datetime.combine(startDate, datetime.time(0))
236     while (endDate is None) or (startDate <= endDate):
237         yield startDate
238         startDate += datetime.timedelta(minutes=frequency)
239
240 def eachhour(startDate, minutes=0, seconds=0, endDate=None):
241     """Yield the same time for each hour. Defaults to 00:00.
242     
243     Yielded values are datetime.datetime objects.
244     For example:
245         eachhour(datetime.date(2004, 5, 4, 6), 15)
246     yields the sequence: 2004-05-04 06:15:00, 2004-05-04 07:15:00,
247                          2004-05-04 08:15:00, ...
248     
249     If startDate has no time component (i.e. if it is a datetime.date),
250     then the first yielded time will be the first valid time after
251     midnight (0:00:00) on that date.
252     
253     If endDate has no time component (i.e. if it is a datetime.date),
254     then the last yielded time will be the last valid time before
255     midnight on that date.
256     """
257     minutes = int(minutes)
258     seconds = int(seconds)
259    
260     if hasattr(startDate, u'time'):
261         zerotime = datetime.time(startDate.hour, minutes, seconds)
262         if zerotime < startDate.time():
263             if zerotime.hour < 23:
264                 zerotime = datetime.time(zerotime.hour + 1, minutes, seconds)
265             else:
266                 zerotime = datetime.time(0, minutes, seconds)
267                 startDate = sane_date(startDate.year, startDate.month,
268                                       startDate.day + 1)
269     else:
270         zerotime = datetime.time(0, minutes, seconds)
271     startDate = datetime.datetime.combine(startDate, zerotime)
272    
273     while (endDate is None) or (startDate <= endDate):
274         yield startDate
275         startDate += datetime.timedelta(hours=1)
276
277 def hours(startDate, frequency=1, endDate=None):
278     """Yield a sequence of datetimes, adding 'frequency' hours each time.
279     
280     For example:
281         hours(datetime.datetime(2004, 5, 4, 14), 6)
282     yields the sequence: 2004-05-04 14:00:00, 2004-05-04 20:00:00,
283                          2004-05-05 2:00:00, ...
284     
285     If startDate has no time component (i.e. if it is a datetime.date),
286     then the first yielded time will be midnight (0:00:00) on that date.
287     
288     If endDate has no time component (i.e. if it is a datetime.date),
289     then the last yielded time will be the last valid time before
290     midnight on that date.
291     
292     For example:
293         hours(datetime.datetime(2004, 5, 4), 8, datetime.datetime(2004, 5, 5))
294     yields the sequence: 2004-05-04 00:00:00, 2004-05-04 08:00:00,
295                          2004-05-04 16:00:00, 2004-05-05 00:00:00,
296                          2004-05-05 08:00:00, 2004-05-05 16:00:00.
297     """
298     if not hasattr(startDate, "time"):
299         startDate = datetime.datetime.combine(startDate, datetime.time(0))
300     if not hasattr(endDate, "time"):
301         endDate = datetime.datetime.combine(endDate, datetime.time(23, 59, 59))
302     while (endDate is None) or (startDate <= endDate):
303         yield startDate
304         startDate += datetime.timedelta(hours=frequency)
305
306 def time_from_str(timeofday):
307     atoms = timeofday.split(u":")
308     def pop_or_zero():
309         try:
310             return int(atoms.pop(0))
311         except TypeError:
312             raise ValueError("The supplied time '%s' could not be parsed."
313                              % timeofday)
314         except IndexError:
315             return 0
316     hour = pop_or_zero()
317     minute = pop_or_zero()
318     second = pop_or_zero()
319     return datetime.time(hour, minute, second)
320
321 def eachday(startDate, timeofday=None, endDate=None):
322     """Yield the same time-of-day for each day. Defaults to midnight.
323     
324     Yielded values are datetime.datetime objects.
325     For example:
326         eachday(datetime.date(2004, 5, 4), datetime.time(14, 3, 0))
327     yields the sequence: 2004-05-04 14:03:00, 2004-05-05 14:03:00,
328                          2004-05-06 14:03:00, ...
329     
330     timeofday may be a datetime.time, as in the above example, or it
331     may be a string, of the form "hour:min:sec". Seconds and minutes
332     may be omitted if their colon ":" separator is also omitted. So
333     the example above could be rewritten:
334         eachday(datetime.date(2004, 5, 4), "14:03")
335     """
336     if timeofday is None:
337         timeofday = datetime.time(0)
338     elif isinstance(timeofday, (str, unicode)):
339         timeofday = time_from_str(timeofday)
340    
341     # If the timeofday is less than the time of startDate,
342     # don't include the startDate in the results.
343     try:
344         if timeofday < startDate.time():
345             startDate = sane_date(startDate.year, startDate.month,
346                                   startDate.day + 1)
347     except AttributeError:
348         # startDate is a datetime.date, and has no time() attribute
349         pass
350     startDate = datetime.datetime.combine(startDate, timeofday)
351     # Now that we've coerced our startDate to a datetime, we need to
352     # do the same thing to endDate so we can compare them.
353     if not hasattr(endDate, "time"):
354         endDate = datetime.datetime.combine(endDate, timeofday)
355    
356     while (endDate is None) or (startDate <= endDate):
357         yield startDate
358         startDate += datetime.timedelta(1)
359
360 def days(startDate, frequency=1, endDate=None):
361     """Yield a sequence of dates, adding 'frequency' days each time.
362     
363     For example:
364         days(datetime.date(2004, 5, 4), 7)
365     yields the sequence: 2004-5-4, 2004-5-11, 2004-5-18, ...
366     """
367     while (endDate is None) or (startDate <= endDate):
368         yield startDate
369         startDate += datetime.timedelta(frequency)
370
371 def eachweek(startDate, weekday=0, endDate=None):
372     """Yield the same day-of-the-week for each week. Defaults to Monday.
373     
374     Yielded values are datetime.date objects.
375     
376     Weekday follows the same days of the week as datetime.weekday().
377     For example:
378         mon, tue, wed, thu, fri, sat, sun = range(7)
379         eachweek(datetime.date(2004, 5, 4), thu)
380     yields the sequence: 2004-5-6, 2004-5-13, 2004-5-20, ...
381     
382     If weekday is out of bounds (0-6), it will be brought in bounds.
383     """
384     weekday = int(weekday)
385     offset = (7 + weekday) - startDate.weekday()
386     while offset > 6:
387         offset -= 7
388     while offset < 0:
389         offset += 7
390     startDate += datetime.timedelta(offset)
391     return days(startDate, 7, endDate)
392
393 def weeks(startDate, frequency=1, endDate=None):
394     """Yield a sequence of dates, adding 'frequency' weeks each time.
395     
396     For example:
397         weeks(datetime.date(2004, 5, 4), 2)
398     yields the sequence: 2004-5-4, 2004-5-18, 2004-6-1, ...
399     """
400     while (endDate is None) or (startDate <= endDate):
401         yield startDate
402         startDate += datetime.timedelta(frequency * 7)
403
404 def eachmonth(startDate, day=1, endDate=None):
405     """Yield the same day of each month. Defaults to the first day.
406     
407     Yielded values are datetime.date objects.
408     
409     If day is a positive number, return that date for each month,
410     starting with startDate. For example:
411         eachmonth(datetime.date(2004, 5, 4), 15)
412     yields the sequence: 2004-5-15, 2004-6-15, 2004-7-15, ...
413     
414     If day is zero or negative, return the same date counting
415     backwards from the end of the month. For example:
416         eachmonth(datetime.date(2004, 5, 4), -5)
417     yields the sequence: 2004-5-26, 2004-6-25, 2004-7-26, ...
418     
419     If day specifies a day which does not appear in every month,
420     then the closest valid date within that month will be used instead.
421     For example:
422         eachmonth(datetime.date(2004, 5, 4), 31)
423     yields the sequence: 2004-5-31, 2004-6-30, 2004-7-31, ...
424     
425     If startDate is greater than what would otherwise be the first date
426     in the sequence, that first item is not yielded; instead, the next
427     item becomes the first item yielded.
428     
429     If endDate is less than what would otherwise be the last date in the
430     sequence, that last item is not yielded, and the sequence ends.
431     """
432     day = int(day)
433     fixmonth = (day > 0)
434     index = 0
435     while True:
436         firstDate = sane_date(startDate.year, startDate.month + index, day, fixmonth)
437         if firstDate >= startDate:
438             break
439         index += 1
440     startDate = firstDate
441    
442     while (endDate is None) or (startDate <= endDate):
443         yield startDate
444         startDate = sane_date(startDate.year, startDate.month + 1, day, fixmonth)
445
446 def months(startDate, frequency=1, endDate=None):
447     """Yield a sequence of dates, adding 'frequency' months each time.
448     
449     For example:
450         months(datetime.date(2004, 5, 4), 3)
451     yields the sequence: 2004-5-4, 2004-8-4, 2004-11-4, ...
452     
453     If the specified startDate contains a day which does not appear
454     in every month, then the closest valid date within that month
455     will be used instead.
456     For example:
457         months(datetime.date(2004, 5, 31), 3)
458     yields the sequence: 2004-5-31, 2004-8-31, 2004-11-30, ...
459     
460     If the frequency parameter is negative, the sequence descends.
461     """
462     idealDay = startDate.day
463     while True:
464         if endDate is not None:
465             if frequency < 0:
466                 if startDate < endDate: break
467             else:
468                 if startDate > endDate: break
469        
470         yield startDate
471         startDate = sane_date(startDate.year, startDate.month + frequency,
472                                idealDay, True)
473
474 def eachyear(startDate, month=1, day=1, endDate=None):
475     """Yield the same day of the year for each year. Defaults to 1/1.
476     
477     Yielded values are datetime.date objects.
478     
479     If day and month are positive numbers, return that day/month for each
480     year, starting with startDate. For example:
481         eachyear(datetime.date(2004, 5, 4), 8, 15)
482     yields the sequence: 2004-8-15, 2005-8-15, 2006-8-15, ...
483     
484     If month is zero or negative, return the same date counting months
485     backwards from the end of the year. For example:
486         eachyear(datetime.date(2004, 5, 4), -2, 15)
487     yields the sequence: 2004-10-15, 2005-10-15, 2006-10-15, ...
488     
489     If day is zero or negative, return the same date counting days
490     backwards from the end of the month. For example:
491         eachyear(datetime.date(2004, 5, 4), -2, -1)
492     yields the sequence: 2004-10-30, 2005-10-30, 2006-10-30, ...
493     
494     If day specifies a day which does not appear in the given month,
495     then the closest valid date within that month will be used instead.
496     For example:
497         eachyear(datetime.date(2004, 5, 4), 5, 31)
498     yields the sequence: 2004-5-30, 2005-5-30, 2006-5-30, ...
499     
500     If startDate is greater than what would otherwise be the first date
501     in the sequence, that first item is not yielded; instead, the next
502     item becomes the first item yielded.
503     
504     If endDate is less than what would otherwise be the last date in the
505     sequence, that last item is not yielded, and the sequence ends.
506     """
507     month = int(month)
508     day = int(day)
509    
510     index = 0
511     while True:
512         curDate = sane_date(startDate.year + index, month, day, True)
513         if curDate >= startDate:
514             break
515         index += 1
516    
517     while (endDate is None) or (curDate <= endDate):
518         yield curDate
519         index += 1
520         curDate = sane_date(startDate.year + index, month, day, True)
521
522 def years(startDate, frequency=1, endDate=None):
523     """Yield a sequence of dates, adding 'frequency' years each time.
524     
525     For example:
526         years(datetime.date(2004, 5, 4), 3)
527     yields the sequence: 2004-5-4, 2007-5-4, 2010-5-4, ...
528     
529     If the specified startDate contains a day which does not appear
530     in every year (i.e. leap years), then the closest valid date
531     within that month will be used instead.
532     
533     For example:
534         years(datetime.date(2004, 2, 29), 3)
535     yields the sequence: 2004-2-29, 2007-2-28, 2010-2-28, ...
536     
537     If the frequency parameter is negative, the sequence descends.
538     """
539     idealDay = startDate.day
540     while True:
541         if endDate is not None:
542             if frequency < 0:
543                 if startDate < endDate: break
544             else:
545                 if startDate > endDate: break
546        
547         yield startDate
548         startDate = sane_date(startDate.year + frequency, startDate.month,
549                                idealDay, True)
550
551 def byunits(startDate, whichUnit, frequency=1, endDate=None):
552     """Dispatch to the appropriate unit handler.
553     
554     This really just exists to help out Locale series.
555     """
556     frequency = int(frequency)
557     unithandler = (seconds, minutes, hours, days, weeks, months, years)
558     return unithandler[whichUnit](startDate, frequency, endDate)
559
560 def singledate(startDate, year, month=1, day=1, endDate=None):
561     """Yield a single datetime.date if y/m/d occurs between start and end."""
562     year = int(year)
563     month = int(month)
564     day = int(day)
565    
566     curDate = sane_date(year, month, day, True)
567     if curDate < startDate:
568         raise StopIteration
569    
570     if (endDate is None) or (curDate <= endDate):
571         yield curDate
572
573
574 class Locale(object):
575     """Language-specific expression matching.
576     
577     To use a language other than English with Recurrence objects,
578     either subclass Locale and override the "patterns" dictionary,
579     or write some other callable that takes a description string
580     and returns a recurrence function and its "inner" args.
581     """
582    
583     patterns = {byunits: [r"([0-9]+) sec(?:ond)?s?",
584                           r"([0-9]+) min(?:ute)?s?",
585                           r"([0-9]+) hours?",
586                           r"([0-9]+) days?",
587                           r"([0-9]+) weeks?",
588                           r"([0-9]+) months?",
589                           r"([0-9]+) years?",
590                           ],
591                 # \S is any non-whitespace character.
592                 eachday: r"([\S]+) (?:every|each) day",
593                 eachweek: [r"mon(?:days?)?", r"tues?(?:days?)?",
594                            r"wed(?:nesdays?)?", r"thurs?(?:days?)?",
595                            r"fri(?:days?)?", r"sat(?:urdays?)?",
596                            r"sun(?:days?)?",
597                            ],
598                 eachmonth: r"(-?\d+) (?:every|each) month",
599                 # Lookbehind for a digit and separator so we don't
600                 # screw up singledate, below.
601                 eachyear: [r"^(dummy entry to line up indexing)$",
602                            r"(?<!\d\d[/ \-])(?:jan(?:uary)?|0?1)[/ \-]([0-9]+)",
603                            r"(?<!\d\d[/ \-])(?:febr?(?:uary)?|0?2)[/ \-]([0-9]+)",
604                            r"(?<!\d\d[/ \-])(?:mar(?:ch)?|0?3)[/ \-]([0-9]+)",
605                            r"(?<!\d\d[/ \-])(?:apr(?:il)?|0?4)[/ \-]([0-9]+)",
606                            r"(?<!\d\d[/ \-])(?:may|0?5)[/ \-]([0-9]+)",
607                            r"(?<!\d\d[/ \-])(?:june?|0?6)[/ \-]([0-9]+)",
608                            r"(?<!\d\d[/ \-])(?:july?|0?7)[/ \-]([0-9]+)",
609                            r"(?<!\d\d[/ \-])(?:aug(?:ust)?|0?8)[/ \-]([0-9]+)",
610                            r"(?<!\d\d[/ \-])(?:sept?(?:ember)?|0?9)[/ \-]([0-9]+)",
611                            r"(?<!\d\d[/ \-])(?:oct(?:ober)?|10)[/ \-]([0-9]+)",
612                            r"(?<!\d\d[/ \-])(?:nov(?:ember)?|11)[/ \-]([0-9]+)",
613                            r"(?<!\d\d[/ \-])(?:dec(?:ember)?|12)[/ \-]([0-9]+)",
614                            ],
615                 # ISO format (relaxed: 1-digit month and day OK,
616                 # slash or space OK)
617                 singledate: r"(\d\d\d\d)[/ \-]([01]?\d)[/ \-]([0123]?\d)",
618                 }
619     regexes = {}
620    
621     def __init__(self):
622         for key, regSet in self.patterns.items():
623             if isinstance(regSet, list):
624                 self.regexes[key] = [re.compile(x, re.IGNORECASE)
625                                      for x in regSet]
626             else:
627                 self.regexes[key] = re.compile(regSet, re.IGNORECASE)
628    
629     def __call__(self, description):
630         for rule, regSet in self.regexes.items():
631             if isinstance(regSet, list):
632                 for index, regex in enumerate(regSet):
633                     matches = regex.match(description)
634                     if matches:
635                         return rule, (index,) + matches.groups()
636             else:
637                 matches = regSet.match(description)
638                 if matches:
639                     return rule, matches.groups()
640        
641         raise ValueError(u"The supplied description ('%s') "
642                          u"could not be parsed." % description)
643
644 localeEnglish = Locale()
645
646
647 class Recurrence(object):
648     """A recurrence pattern and its iterator.
649     
650     The Recurrence class provides natural-language hooks for common recur
651     operations. The "description" parameter should be a set of keywords in
652     a natural language, which is then looked up in self.locale.regexes.
653     
654     Usage:
655     
656     import datetime, recur
657     
658     firstDate = datetime.date(2004, 1, 7)
659     lastDate = datetime.date(2004, 2, 11)
660     for eachDate in recur.Recurrence(firstDate, "Saturday", lastDate):
661         print eachDate
662     
663     2004-01-10
664     2004-01-17
665     2004-01-24
666     2004-01-31
667     2004-02-07
668     """
669    
670     def __init__(self, startDate=None, description="", endDate=None,
671                  locale=localeEnglish):
672         """
673         If startDate is None (not supplied), then it will be set
674         to the current date and time.
675         
676         Leading and trailing whitespace will be stripped from the
677         description parameter.
678         """
679         if startDate is None:
680             startDate = datetime.datetime.now()
681         description = description.strip()
682        
683         self.startDate = startDate
684         self.description = description
685         self.endDate = endDate
686         self.locale = locale
687         self.function, args = locale(description)
688         self.args = (startDate,) + args + (endDate,)
689         # Form an initial generator, if for no other reason than to test args early.
690         self.reset()
691    
692     def reset(self):
693         try:
694             self.generator = self.function(*self.args)
695         except TypeError, x:
696             x.args += self.args
697             raise
698    
699     def __iter__(self):
700         self.reset()
701         return self
702    
703     def next(self):
704         return self.generator.next()
705
706
707 class Worker(object):
708     """Perform work on a schedule.
709     
710     You must override work(), which is called at each interval.
711     
712     paused: a boolean flag indicating whether or not the Worker's run()
713         method should be executed at each interval. Notice that, even if
714         paused is True, recurring Workers will continue to schedule new
715         threads--they simply won't do anything at run() time.
716     
717     terminated: a boolean flag indicating whether or not the Worker should
718         continue to cycle. If terminated is True, recurring Workers will
719         not schedule new threads.
720     """
721    
722     def __init__(self, recurrence):
723         if isinstance(recurrence, basestring):
724             if recurrence:
725                 recurrence = Recurrence(None, recurrence)
726             else:
727                 recurrence = None
728         self.recurrence = recurrence
729        
730         self.createdate = datetime.datetime.now()
731         self.lastrun = None
732         self.nextrun = None
733         self.paused = False
734         self.terminated = False
735    
736     def motivate(self):
737         """Start a new immediate or recurring thread for work."""
738         self.nextrun = None
739         if not self.terminated:
740             if self.recurrence:
741                 # Start a recurring, timed thread.
742                 now = datetime.datetime.now()
743                 while True:
744                     try:
745                         next = self.recurrence.next()
746                     except StopIteration:
747                         # The recurrence series was exhausted.
748                         return
749                     diff = next - now
750                     diff = (diff.days * 86400) + diff.seconds
751                     if diff >= 0:
752                         self.nextrun = next
753                         break
754                 iv = diff
755                 func = self._cycle
756             else:
757                 # Start a single, non-recurring thread.
758                 iv = 0
759                 func = self.run
760             self.curthread = threading.Timer(iv, func)
761             self.curthread.start()
762    
763     def _cycle(self):
764         """Run the worker on a schedule."""
765         self.motivate()
766         self.run()
767    
768     def run(self):
769         """Prepare for work."""
770         if not self.paused and not self.terminated:
771             self.work()
772             self.lastrun = datetime.datetime.now()
773    
774     def stop(self):
775         """Stop work."""
776         self.terminated = True
777         self.curthread.cancel()
778    
779     def work(self):
780         raise NotImplementedError
781
Note: See TracBrowser for help on using the browser.