Contact: fumanchu@aminus.org

Log in as guest/dejavu to create tickets

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

root/branches/1.5/dejavu/recur.py

Revision 487 (checked in by fumanchu, 5 years ago)

Moved all content down one folder. Added setup.py.

  • 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, highzero=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 be
64     modified relative to the first month of the given year.
65     
66     If the day param is out of bounds, the day, month, and possibly year
67     will be modified. If the day param is zero or negative, then the
68     "zeroth day" of the given month is assumed to be the last day of the
69     previous month, unless highzero is True, in which case the "zeroth day"
70     is the last day of the given month.
71     
72     Examples:
73         sane_date(2003, 2, 1) = datetime.date(2003, 2, 1)
74         sane_date(2003, -10, 13) = datetime.date(2002, 2, 13)
75         sane_date(2003, 12, -5) = datetime.date(2003, 11, 25)
76         sane_date(2003, 12, -5, highzero=True) = datetime.date(2003, 12, 26)
77     """
78     while month > 12:
79         month -= 12
80         year += 1
81     while month < 1:
82         month += 12
83         year -= 1
84     if highzero and day < 1:
85         # Count backward from the first of *next* month.
86         firstOfMonth = sane_date(year, month + 1, 1)
87     else:
88         # Count backward/forward from the first of the current month.
89         firstOfMonth = datetime.date(year, month, 1)
90     newDate = firstOfMonth + datetime.timedelta(day - 1)
91     return newDate
92
93 def sane_time(day, hour, minute, second):
94     """Return a valid (day, datetime.time) even if parameters are out of bounds.
95     
96     If the hour param is out of bounds, both it and the day will
97     be modified. If negative, the day will be decremented.
98     
99     If the minute param is out of bounds, both it and the hour will
100     be modified. If negative, the hour will be decremented.
101     
102     If the second param is out of bounds, both it and the minute will
103     be modified. If negative, the minute will be decremented.
104     
105     Examples:
106         sane_time(0, 4, 2, 1) = (0, datetime.time(4, 2, 1)
107         sane_time(0, 25, 2, 1) = (1, datetime.time(1, 2, 1)
108         sane_time(0, 4, 1440, 1) = (1, datetime.time(4, 2, 1)
109         sane_time(0, 0, 0, -1) = (-1, datetime.time(23, 59, 59)
110     """
111     while second > 59:
112         second -= 60
113         minute += 1
114     while second < 0:
115         second += 60
116         minute -= 1
117     while minute > 59:
118         minute -= 60
119         hour += 1
120     while minute < 0:
121         minute += 60
122         hour -= 1
123     while hour > 23:
124         hour -= 24
125         day += 1
126     while hour < 0:
127         hour += 24
128         day -= 1
129     newTime = (day, datetime.time(hour, minute, second))
130     return newTime
131
132 def seconds(startDate, frequency=1, endDate=None):
133     """Yield a sequence of datetimes, adding 'frequency' seconds each time.
134     
135     For example:
136         seconds(datetime.datetime(2004, 5, 4, 14, 0), 6)
137     yields the sequence: 2004-05-04 14:00:00, 2004-05-04 14:00:06,
138                          2004-05-04 14:00:12, ...
139     
140     If startDate has no time component (i.e. if it is a datetime.date),
141     then the first yielded time will be midnight (0:00:00) on that date.
142     
143     If endDate has no time component (i.e. if it is a datetime.date),
144     then the last yielded time will be the last valid time before
145     midnight on that date.
146     
147     For example:
148         seconds(datetime.datetime(2004, 5, 4), 15, datetime.datetime(2004, 5, 5))
149     yields the sequence: 2004-05-04 00:00:00, 2004-05-04 00:00:15,
150                          2004-05-04 00:00:30, ...
151                                           ... 2004-05-05 23:59:15,
152                          2004-05-05 23:59:30, 2004-05-05 23:59:45.
153     """
154     if not hasattr(startDate, u'time'):
155         startDate = datetime.datetime.combine(startDate, datetime.time(0))
156     while (endDate is None) or (startDate <= endDate):
157         yield startDate
158         startDate += datetime.timedelta(seconds=frequency)
159
160 def eachminute(startDate, seconds=0, endDate=None):
161     """Yield the same time for each minute. Defaults to 0 seconds.
162     
163     Yielded values are datetime.datetime objects.
164     For example:
165         eachminute(datetime.date(2004, 5, 4, 23, 55), 15)
166     yields the sequence: 2004-05-04 23:55:15, 2004-05-04 23:56:15,
167                          2004-05-04 23:57:15, ...
168     
169     If startDate has no time component (i.e. if it is a datetime.date),
170     then the first yielded time will be the first valid time after
171     midnight (0:00:00) on that date.
172     
173     If endDate has no time component (i.e. if it is a datetime.date),
174     then the last yielded time will be the last valid time before
175     midnight on that date.
176     """
177     seconds = int(seconds)
178    
179     if hasattr(startDate, u'time'):
180         days, zerotime = sane_time(0, startDate.hour,
181                                    startDate.minute, seconds)
182         if days < 0 or zerotime < startDate.time():
183             days, zerotime = sane_time(0, startDate.hour,
184                                       startDate.minute + 1, seconds)
185     else:
186         days, zerotime = sane_time(0, 0, 0, seconds)
187     startDate = sane_date(startDate.year, startDate.month,
188                           startDate.day + days)
189     startDate = datetime.datetime.combine(startDate, zerotime)
190    
191     while (endDate is None) or (startDate <= endDate):
192         yield startDate
193         startDate += datetime.timedelta(minutes=1)
194
195 def minutes(startDate, frequency=1, endDate=None):
196     """Yield a sequence of datetimes, adding 'frequency' minutes each time.
197     
198     For example:
199         minutes(datetime.datetime(2004, 5, 4, 14), 30)
200     yields the sequence: 2004-05-04 14:00:00, 2004-05-04 14:30:00,
201                          2004-05-04 15:00:00, ...
202     
203     If startDate has no time component (i.e. if it is a datetime.date),
204     then the first yielded time will be midnight (0:00:00) on that date.
205     
206     If endDate has no time component (i.e. if it is a datetime.date),
207     then the last yielded time will be the last valid time before
208     midnight on that date.
209     
210     For example:
211         minutes(datetime.datetime(2004, 5, 4), 15, datetime.datetime(2004, 5, 5))
212     yields the sequence: 2004-05-04 00:00:00, 2004-05-04 00:15:00,
213                          2004-05-04 00:30:00, ...
214                                           ... 2004-05-05 23:15:00,
215                          2004-05-05 23:30:00, 2004-05-05 23:45:00.
216     """
217     if not hasattr(startDate, u'time'):
218         startDate = datetime.datetime.combine(startDate, datetime.time(0))
219     while (endDate is None) or (startDate <= endDate):
220         yield startDate
221         startDate += datetime.timedelta(minutes=frequency)
222
223 def eachhour(startDate, minutes=0, seconds=0, endDate=None):
224     """Yield the same time for each hour. Defaults to 00:00.
225     
226     Yielded values are datetime.datetime objects.
227     For example:
228         eachhour(datetime.date(2004, 5, 4, 6), 15)
229     yields the sequence: 2004-05-04 06:15:00, 2004-05-04 07:15:00,
230                          2004-05-04 08:15:00, ...
231     
232     If startDate has no time component (i.e. if it is a datetime.date),
233     then the first yielded time will be the first valid time after
234     midnight (0:00:00) on that date.
235     
236     If endDate has no time component (i.e. if it is a datetime.date),
237     then the last yielded time will be the last valid time before
238     midnight on that date.
239     """
240     minutes = int(minutes)
241     seconds = int(seconds)
242    
243     if hasattr(startDate, u'time'):
244         zerotime = datetime.time(startDate.hour, minutes, seconds)
245         if zerotime < startDate.time():
246             if zerotime.hour < 23:
247                 zerotime = datetime.time(zerotime.hour + 1, minutes, seconds)
248             else:
249                 zerotime = datetime.time(0, minutes, seconds)
250                 startDate = sane_date(startDate.year, startDate.month,
251                                       startDate.day + 1)
252     else:
253         zerotime = datetime.time(0, minutes, seconds)
254     startDate = datetime.datetime.combine(startDate, zerotime)
255    
256     while (endDate is None) or (startDate <= endDate):
257         yield startDate
258         startDate += datetime.timedelta(hours=1)
259
260 def hours(startDate, frequency=1, endDate=None):
261     """Yield a sequence of datetimes, adding 'frequency' hours each time.
262     
263     For example:
264         hours(datetime.datetime(2004, 5, 4, 14), 6)
265     yields the sequence: 2004-05-04 14:00:00, 2004-05-04 20:00:00,
266                          2004-05-05 2:00:00, ...
267     
268     If startDate has no time component (i.e. if it is a datetime.date),
269     then the first yielded time will be midnight (0:00:00) on that date.
270     
271     If endDate has no time component (i.e. if it is a datetime.date),
272     then the last yielded time will be the last valid time before
273     midnight on that date.
274     
275     For example:
276         hours(datetime.datetime(2004, 5, 4), 8, datetime.datetime(2004, 5, 5))
277     yields the sequence: 2004-05-04 00:00:00, 2004-05-04 08:00:00,
278                          2004-05-04 16:00:00, 2004-05-05 00:00:00,
279                          2004-05-05 08:00:00, 2004-05-05 16:00:00.
280     """
281     if not hasattr(startDate, "time"):
282         startDate = datetime.datetime.combine(startDate, datetime.time(0))
283     if endDate and not hasattr(endDate, "time"):
284         endDate = datetime.datetime.combine(endDate, datetime.time(23, 59, 59))
285     while (endDate is None) or (startDate <= endDate):
286         yield startDate
287         startDate += datetime.timedelta(hours=frequency)
288
289 def time_from_str(timeofday):
290     atoms = timeofday.split(u":")
291     def pop_or_zero():
292         try:
293             return int(atoms.pop(0))
294         except TypeError:
295             raise ValueError("The supplied time '%s' could not be parsed."
296                              % timeofday)
297         except IndexError:
298             return 0
299     hour = pop_or_zero()
300     minute = pop_or_zero()
301     second = pop_or_zero()
302     return datetime.time(hour, minute, second)
303
304 def eachday(startDate, timeofday=None, endDate=None):
305     """Yield the same time-of-day for each day. Defaults to midnight.
306     
307     Yielded values are datetime.datetime objects.
308     For example:
309         eachday(datetime.date(2004, 5, 4), datetime.time(14, 3, 0))
310     yields the sequence: 2004-05-04 14:03:00, 2004-05-05 14:03:00,
311                          2004-05-06 14:03:00, ...
312     
313     timeofday may be a datetime.time, as in the above example, or it
314     may be a string, of the form "hour:min:sec". Seconds and minutes
315     may be omitted if their colon ":" separator is also omitted. So
316     the example above could be rewritten:
317         eachday(datetime.date(2004, 5, 4), "14:03")
318     """
319     if timeofday is None:
320         timeofday = datetime.time(0)
321     elif isinstance(timeofday, (str, unicode)):
322         timeofday = time_from_str(timeofday)
323    
324     # If the timeofday is less than the time of startDate,
325     # don't include the startDate in the results.
326     try:
327         if timeofday < startDate.time():
328             startDate = sane_date(startDate.year, startDate.month,
329                                   startDate.day + 1)
330     except AttributeError:
331         # startDate is a datetime.date, and has no time() attribute
332         pass
333     startDate = datetime.datetime.combine(startDate, timeofday)
334     # Now that we've coerced our startDate to a datetime, we need to
335     # do the same thing to endDate so we can compare them.
336     if endDate and not hasattr(endDate, "time"):
337         endDate = datetime.datetime.combine(endDate, timeofday)
338    
339     while (endDate is None) or (startDate <= endDate):
340         yield startDate
341         startDate += datetime.timedelta(1)
342
343 def eachweekday(startDate, weekday, timeofday=None, endDate=None):
344     """Yield the same time-of-day each week for the given day. The time-of-day
345     defaults to midnight.
346     
347     Yielded values are datetime.datetime objects.
348     For example:
349         eachweekday(datetime.date(2006, 8, 10), 3, datetime.time(14, 3, 0))
350     yields the sequence: 2006-08-11 14:03:00, 2006-08-18 14:03:00,
351                          2006-08-25 14:03:00, ...
352     
353     timeofday may be a datetime.time, as in the above example, or it
354     may be a string, of the form "hour:min:sec". Seconds and minutes
355     may be omitted if their colon ":" separator is also omitted. So
356     the example above could be rewritten:
357         eachday(datetime.date(2004, 5, 4), "14:03")
358     """
359     if timeofday is None:
360         timeofday = datetime.time(0)
361     elif isinstance(timeofday, (str, unicode)):
362         timeofday = time_from_str(timeofday)
363
364     # get the given start time or datetime.time(0,0)
365     startTime = getattr(startDate, 'time', datetime.time)()
366    
367     if startDate.weekday() > weekday or startTime > timeofday:
368         offset = (7 + weekday) - startDate.weekday()
369         while offset > 6:
370             offset -= 7
371         while offset <= 0:
372             offset += 7
373         startDate += datetime.timedelta(offset)
374    
375     startDate = datetime.datetime.combine(startDate, timeofday)
376    
377     # Now that we've coerced our startDate to a datetime, we need to
378     # do the same thing to endDate so we can compare them.
379     if endDate and not hasattr(endDate, "time"):
380         endDate = datetime.datetime.combine(endDate, timeofday)
381    
382     end = getattr(endDate, 'date', lambda: None)()
383     day_iter = eachweek(startDate.date(), weekday, end)
384     startDate = datetime.datetime.combine(day_iter.next(), timeofday)
385    
386     while (endDate is None) or (startDate <= endDate):
387         yield startDate
388         startDate = datetime.datetime.combine(day_iter.next(), timeofday)
389
390 def days(startDate, frequency=1, endDate=None):
391     """Yield a sequence of dates, adding 'frequency' days each time.
392     
393     For example:
394         days(datetime.date(2004, 5, 4), 7)
395     yields the sequence: 2004-5-4, 2004-5-11, 2004-5-18, ...
396     """
397     while (endDate is None) or (startDate <= endDate):
398         yield startDate
399         startDate += datetime.timedelta(frequency)
400
401 def eachweek(startDate, weekday=0, endDate=None):
402     """Yield the same day-of-the-week for each week. Defaults to Monday.
403     
404     Yielded values are datetime.date objects.
405     
406     Weekday follows the same days of the week as datetime.weekday().
407     For example:
408         mon, tue, wed, thu, fri, sat, sun = range(7)
409         eachweek(datetime.date(2004, 5, 4), thu)
410     yields the sequence: 2004-5-6, 2004-5-13, 2004-5-20, ...
411     
412     If weekday is out of bounds (0-6), it will be brought in bounds.
413     """
414     if hasattr(startDate, 'time'):
415         startDate = startDate.date()
416     weekday = int(weekday)
417     offset = (7 + weekday) - startDate.weekday()
418     while offset > 6:
419         offset -= 7
420     while offset < 0:
421         offset += 7
422     startDate += datetime.timedelta(offset)
423     return days(startDate, 7, endDate)
424
425 def weeks(startDate, frequency=1, endDate=None):
426     """Yield a sequence of dates, adding 'frequency' weeks each time.
427     
428     For example:
429         weeks(datetime.date(2004, 5, 4), 2)
430     yields the sequence: 2004-5-4, 2004-5-18, 2004-6-1, ...
431     """
432     while (endDate is None) or (startDate <= endDate):
433         yield startDate
434         startDate += datetime.timedelta(frequency * 7)
435
436 def eachmonth(startDate, day=1, endDate=None):
437     """Yield the same day of each month. Defaults to the first day.
438     
439     Yielded values are datetime.date objects.
440     
441     If day is a positive number, return that date for each month,
442     starting with startDate. For example:
443         eachmonth(datetime.date(2004, 5, 4), 15)
444     yields the sequence: 2004-5-15, 2004-6-15, 2004-7-15, ...
445     
446     If day is zero or negative, return the same date counting
447     backwards from the end of the month. For example:
448         eachmonth(datetime.date(2004, 5, 4), -5)
449     yields the sequence: 2004-5-26, 2004-6-25, 2004-7-26, ...
450     
451     If day specifies a day which does not appear in every month,
452     then the closest valid date within that month will be used instead.
453     For example:
454         eachmonth(datetime.date(2004, 5, 4), 31)
455     yields the sequence: 2004-5-31, 2004-6-30, 2004-7-31, ...
456     
457     If startDate is greater than what would otherwise be the first date
458     in the sequence, that first item is not yielded; instead, the next
459     item becomes the first item yielded.
460     
461     If endDate is less than what would otherwise be the last date in the
462     sequence, that last item is not yielded, and the sequence ends.
463     """
464     if hasattr(startDate, 'time'):
465         startDate = startDate.date()
466     day = int(day)
467     highzero = (day < 1)
468     index = 0
469     while True:
470         firstDate = sane_date(startDate.year, startDate.month + index, day, highzero)
471         if firstDate >= startDate:
472             break
473         index += 1
474     startDate = firstDate
475    
476     while (endDate is None) or (startDate <= endDate):
477         yield startDate
478         startDate = sane_date(startDate.year, startDate.month + 1, day, highzero)
479
480 def months(startDate, frequency=1, endDate=None):
481     """Yield a sequence of dates, adding 'frequency' months each time.
482     
483     For example:
484         months(datetime.date(2004, 5, 4), 3)
485     yields the sequence: 2004-5-4, 2004-8-4, 2004-11-4, ...
486     
487     If the specified startDate contains a day which does not appear
488     in every month, then the corresponding day from the next month
489     will be used instead.
490     For example:
491         months(datetime.date(2004, 5, 31), 3)
492     yields the sequence: 2004-5-31, 2004-8-31, 2004-12-1, ...
493     
494     If the frequency parameter is negative, the sequence descends.
495     """
496     day = startDate.day
497     month = startDate.month
498     year = startDate.year
499     while True:
500         if endDate is not None:
501             if frequency < 0:
502                 if startDate < endDate: break
503             else:
504                 if startDate > endDate: break
505        
506         yield startDate
507         month += frequency
508         startDate = sane_date(year, month, day)
509
510 def eachyear(startDate, month=1, day=1, endDate=None):
511     """Yield the same day of the year for each year. Defaults to 1/1.
512     
513     Yielded values are datetime.date objects.
514     
515     If day and month are positive numbers, return that day/month for each
516     year, starting with startDate. For example:
517         eachyear(datetime.date(2004, 5, 4), 8, 15)
518     yields the sequence: 2004-8-15, 2005-8-15, 2006-8-15, ...
519     
520     If month is zero or negative, return the same date counting months
521     backwards from the end of the year. For example:
522         eachyear(datetime.date(2004, 5, 4), -2, 15)
523     yields the sequence: 2004-10-15, 2005-10-15, 2006-10-15, ...
524     
525     If day is zero or negative, return the same date counting days
526     backwards from the end of the month. For example:
527         eachyear(datetime.date(2004, 5, 4), -2, -1)
528     yields the sequence: 2004-10-30, 2005-10-30, 2006-10-30, ...
529     
530     If day specifies a day which does not appear in the given month,
531     then the corresponding day from the next month will be used instead.
532     For example:
533         eachyear(datetime.date(2004, 5, 4), 5, 31)
534     yields the sequence: 2004-6-1, 2005-6-1, 2006-6-1, ...
535     
536     If startDate is greater than what would otherwise be the first date
537     in the sequence, that first item is not yielded; instead, the next
538     item becomes the first item yielded.
539     
540     If endDate is less than what would otherwise be the last date in the
541     sequence, that last item is not yielded, and the sequence ends.
542     """
543     if hasattr(startDate, 'time'):
544         startDate = startDate.date()
545     month = int(month)
546     day = int(day)
547    
548     index = 0
549     while True:
550         curDate = sane_date(startDate.year + index, month, day, True)
551         if curDate >= startDate:
552             break
553         index += 1
554    
555     while (endDate is None) or (curDate <= endDate):
556         yield curDate
557         index += 1
558         curDate = sane_date(startDate.year + index, month, day, True)
559
560 def years(startDate, frequency=1, endDate=None):
561     """Yield a sequence of dates, adding 'frequency' years each time.
562     
563     For example:
564         years(datetime.date(2004, 5, 4), 3)
565     yields the sequence: 2004-5-4, 2007-5-4, 2010-5-4, ...
566     
567     If the specified startDate contains a day which does not appear
568     in every year (i.e. leap years), then the corresponding day from
569     the next month will be used instead.
570     
571     For example:
572         years(datetime.date(2004, 2, 29), 3)
573     yields the sequence: 2004-2-29, 2007-3-1, 2010-3-1, ...
574     
575     If the frequency parameter is negative, the sequence descends.
576     """
577     day = startDate.day
578     month = startDate.month
579     year = startDate.year
580     while True:
581         if endDate is not None:
582             if frequency < 0:
583                 if startDate < endDate: break
584             else:
585                 if startDate > endDate: break
586        
587         yield startDate
588         year += frequency
589         startDate = sane_date(year, month, day)
590
591 def byunits(startDate, whichUnit, frequency=1, endDate=None):
592     """Dispatch to the appropriate unit handler.
593     
594     This really just exists to help out Locale series.
595     """
596     frequency = int(frequency)
597     unithandler = (seconds, minutes, hours, days, weeks, months, years)
598     return unithandler[whichUnit](startDate, frequency, endDate)
599
600 def singledate(startDate, year, month=1, day=1, endDate=None):
601     """Yield a single datetime.date if y/m/d occurs between start and end."""
602     year = int(year)
603     month = int(month)
604     day = int(day)
605    
606     curDate = sane_date(year, month, day)
607     if curDate < startDate:
608         raise StopIteration
609    
610     if (endDate is None) or (curDate <= endDate):
611         yield curDate
612
613
614 class Locale(object):
615     """Language-specific expression matching.
616     
617     To use a language other than English with Recurrence objects,
618     either subclass Locale and override the "patterns" dictionary,
619     or write some other callable that takes a description string
620     and returns a recurrence function and its "inner" args.
621     """
622    
623     patterns = {byunits: [r"([0-9]+) sec",
624                           r"([0-9]+) min",
625                           r"([0-9]+) hour",
626                           r"([0-9]+) day",
627                           r"([0-9]+) week",
628                           r"([0-9]+) month",
629                           r"([0-9]+) year",
630                           ],
631                 # \S is any non-whitespace character.
632                 eachday: r"([\S]+) (?:every|each) day",
633                 eachweekday: [# don't match "month"
634                               r"([\S]+) (?:every|each) mon(?!th)",
635                               r"([\S]+) (?:every|each) tue",
636                               r"([\S]+) (?:every|each) wed",
637                               r"([\S]+) (?:every|each) thu",
638                               r"([\S]+) (?:every|each) fri",
639                               r"([\S]+) (?:every|each) sat",
640                               r"([\S]+) (?:every|each) sun",
641                               ],
642                 eachweek: [r"mon", r"tue", r"wed", r"thu", r"fri", r"sat", r"sun"],
643                 eachmonth: r"(-?\d+) (?:every|each) month",
644                 # Lookbehind for a digit and separator so we don't
645                 # screw up singledate, below.
646                 eachyear: [r"^(dummy entry to line up indexing)$",
647                            r"(?<!\d\d[/ \-])(?:jan(?:uary)?|0?1)[/ \-]([0-9]+)",
648                            r"(?<!\d\d[/ \-])(?:febr?(?:uary)?|0?2)[/ \-]([0-9]+)",
649                            r"(?<!\d\d[/ \-])(?:mar(?:ch)?|0?3)[/ \-]([0-9]+)",
650                            r"(?<!\d\d[/ \-])(?:apr(?:il)?|0?4)[/ \-]([0-9]+)",
651                            r"(?<!\d\d[/ \-])(?:may|0?5)[/ \-]([0-9]+)",
652                            r"(?<!\d\d[/ \-])(?:june?|0?6)[/ \-]([0-9]+)",
653                            r"(?<!\d\d[/ \-])(?:july?|0?7)[/ \-]([0-9]+)",
654                            r"(?<!\d\d[/ \-])(?:aug(?:ust)?|0?8)[/ \-]([0-9]+)",
655                            r"(?<!\d\d[/ \-])(?:sept?(?:ember)?|0?9)[/ \-]([0-9]+)",
656                            r"(?<!\d\d[/ \-])(?:oct(?:ober)?|10)[/ \-]([0-9]+)",
657                            r"(?<!\d\d[/ \-])(?:nov(?:ember)?|11)[/ \-]([0-9]+)",
658                            r"(?<!\d\d[/ \-])(?:dec(?:ember)?|12)[/ \-]([0-9]+)",
659                            ],
660                 # ISO format (relaxed: 1-digit month and day OK,
661                 # slash or space OK)
662                 singledate: r"(\d\d\d\d)[/ \-]([01]?\d)[/ \-]([0123]?\d)",
663                 }
664     regexes = {}
665    
666     def __init__(self):
667         for key, regSet in self.patterns.items():
668             if isinstance(regSet, list):
669                 self.regexes[key] = [re.compile(x, re.IGNORECASE)
670                                      for x in regSet]
671             else:
672                 self.regexes[key] = re.compile(regSet, re.IGNORECASE)
673    
674     def __call__(self, description):
675         for rule, regSet in self.regexes.items():
676             if isinstance(regSet, list):
677                 for index, regex in enumerate(regSet):
678                     matches = regex.match(description)
679                     if matches:
680                         return rule, (index,) + matches.groups()
681             else:
682                 matches = regSet.match(description)
683                 if matches:
684                     return rule, matches.groups()
685        
686         raise ValueError(u"The supplied description ('%s') "
687                          u"could not be parsed." % description)
688
689 localeEnglish = Locale()
690
691
692 class Recurrence(object):
693     """A recurrence pattern and its iterator.
694     
695     The Recurrence class provides natural-language hooks for common recur
696     operations. The "description" parameter should be a set of keywords in
697     a natural language, which is then looked up in self.locale.regexes.
698     
699     Usage:
700     
701     import datetime, recur
702     
703     firstDate = datetime.date(2004, 1, 7)
704     lastDate = datetime.date(2004, 2, 11)
705     for eachDate in recur.Recurrence(firstDate, "Saturday", lastDate):
706         print eachDate
707     
708     2004-01-10
709     2004-01-17
710     2004-01-24
711     2004-01-31
712     2004-02-07
713     """
714    
715     def __init__(self, startDate=None, description="", endDate=None,
716                  locale=localeEnglish):
717         """
718         If startDate is None (not supplied), then it will be set
719         to the current date and time.
720         
721         Leading and trailing whitespace will be stripped from the
722         description parameter.
723         """
724         if startDate is None:
725             startDate = datetime.datetime.now()
726         description = description.strip()
727        
728         self.startDate = startDate
729         self.description = description
730         self.endDate = endDate
731         self.locale = locale
732         self.function, args = locale(description)
733         self.args = (startDate,) + args + (endDate,)
734         # Form an initial generator, if for no other reason than to test args early.
735         self.reset()
736    
737     def reset(self):
738         try:
739             self.generator = self.function(*self.args)
740         except TypeError, x:
741             x.args += self.args
742             raise
743    
744     def __iter__(self):
745         self.reset()
746         return self
747    
748     def next(self):
749         return self.generator.next()
750
751
752 deltazero = datetime.timedelta(0)
753
754 class Worker(object):
755     """Perform work on a schedule.
756     
757     You must override work(), which is called at each interval.
758     """
759    
760     def __init__(self, recurrence):
761         if isinstance(recurrence, basestring):
762             if recurrence:
763                 recurrence = Recurrence(None, recurrence)
764             else:
765                 recurrence = None
766         self.recurrence = recurrence
767        
768         self.createdate = datetime.datetime.now()
769         self.lastrun = None
770         self.nextrun = None
771         self.curthread = None
772         self.active = True
773    
774     def interval(self, next):
775         """Return a timedelta (next - now).
776         
777         if next is None, return None.
778         If next < now, return datetime.timedelta(0).
779         """
780         if next is None:
781             return None
782        
783         # next can be either a datetime.datetime or a datetime.date;
784         # get the correct representation of "now" from either one.
785         now = getattr(next, 'now', getattr(next, 'today'))()
786         return max(next - now, deltazero)
787    
788     def advance(self):
789         """Advance self.recurrence and set self.nextrun.
790         
791         This function ignores dates which are in the past.
792         If the recurrence series is exhausted, self.nextrun will be None.
793         """
794         if not self.recurrence:
795             self.nextrun = None
796             return
797        
798         try:
799             next = self.recurrence.next()
800         except StopIteration:
801             # The recurrence series was exhausted immediately.
802             self.nextrun = None
803             return
804        
805         # next can be either a datetime.datetime or a datetime.date;
806         # get the correct representation of "now" from either one.
807         now = getattr(next, 'now', getattr(next, 'today'))()
808         while True:
809             if next >= now:
810                 self.nextrun = next
811                 break
812            
813             try:
814                 next = self.recurrence.next()
815             except StopIteration:
816                 # The recurrence series was exhausted.
817                 self.nextrun = None
818                 break
819    
820     def start(self, secs=0):
821         """Call self.run in a new thread."""
822         if self.active:
823             self.curthread = threading.Timer(secs, self.run)
824             self.curthread.start()
825    
826     def run(self):
827         """Prepare for work."""
828         if self.active:
829             self.work()
830             self.lastrun = datetime.datetime.now()
831    
832     def work(self):
833         """Perform the actual work. Must be overridden."""
834         raise NotImplementedError
835    
836     def stop(self):
837         """Stop work."""
838         self.active = False
839         if self.curthread:
840             self.curthread.cancel()
841
842
843 class Scheduler(object):
844     """Collection of Workers governed by a single scheduler thread.
845     
846     paused: a boolean flag indicating whether or not each Worker's start()
847         method should be executed at each interval. Notice that, even if
848         paused is True, the scheduler thread will still cycle, but Workers
849         will not be run at each interval.
850     
851     terminated: a boolean flag indicating whether or not the Worker should
852         continue to cycle. If terminated is True, recurring Workers will
853         not schedule new threads.
854     """
855    
856     def __init__(self, workers=None):
857         if workers is None:
858             workers = {}
859         self.workers = workers
860         self.curthread = None
861         self.paused = False
862         self.terminated = False
863    
864     def start(self):
865         """Start a new recurring thread for all workers.
866         
867         This sets self.terminated to False, but doesn't set self.paused.
868         """
869         # Set nextrun for all workers
870         for worker in self.workers.values():
871             worker.advance()
872         self.terminated = False
873         self._cycle()
874    
875     def _cycle(self):
876         """Start a new Timer for the next worker."""
877         if self.terminated:
878             return
879        
880         ivs = []
881         for w in self.workers.values():
882             next = w.nextrun
883             if next is not None:
884                 ivs.append((w.interval(next), w))
885        
886         if ivs:
887             ivs.sort()
888             iv, nextworker = ivs[0]
889             iv = (iv.days * 86400) + iv.seconds + (iv.microseconds / 1000000.0)
890             nextworker.advance()
891             self.curthread = threading.Timer(iv, self.run, (nextworker,))
892             self.curthread.start()
893    
894     def run(self, worker):
895         """Run the worker, then cycle again."""
896         if not self.paused and not self.terminated:
897             worker.start()
898         self._cycle()
899    
900     def stop(self):
901         self.terminated = True
902         if self.curthread:
903             self.curthread.cancel()
904         for w in self.workers.values():
905             w.stop()
906
Note: See TracBrowser for help on using the browser.