thirdparty/google_appengine/google/appengine/cron/groctimespecification.py
changeset 828 f5fd65cc3bf3
child 1278 a7766286a7be
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/thirdparty/google_appengine/google/appengine/cron/groctimespecification.py	Tue Jan 20 13:19:45 2009 +0000
@@ -0,0 +1,257 @@
+#!/usr/bin/env python
+#
+# Copyright 2007 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+"""Implementation of scheduling for Groc format schedules.
+
+A Groc schedule looks like '1st,2nd monday 9:00', or 'every 20 mins'. This
+module takes a parsed schedule (produced by Antlr) and creates objects that
+can produce times that match this schedule.
+
+A parsed schedule is one of two types - an Interval, and a Specific Time.
+See the class docstrings for more.
+
+Extensions to be considered:
+
+  allowing a comma separated list of times to run
+  allowing the user to specify particular days of the month to run
+
+"""
+
+
+import calendar
+import datetime
+
+import groc
+
+HOURS = 'hours'
+MINUTES = 'minutes'
+
+
+def GrocTimeSpecification(schedule):
+  """Factory function.
+
+  Turns a schedule specification into a TimeSpecification.
+
+  Arguments:
+    schedule: the schedule specification, as a string
+
+  Returns:
+    a TimeSpecification instance
+  """
+
+  parser = groc.CreateParser(schedule)
+  parser.timespec()
+
+  if parser.interval_mins:
+    return IntervalTimeSpecification(parser.interval_mins, parser.period_string)
+  else:
+    return SpecificTimeSpecification(parser.ordinal_set, parser.weekday_set,
+                                     parser.month_set, None, parser.time_string)
+
+
+class TimeSpecification(object):
+  """Base class for time specifications."""
+
+  def GetMatches(self, start, n):
+    """Returns the next n times that match the schedule, starting at time start.
+
+    Arguments:
+      start: a datetime to start from. Matches will start from after this time
+      n:     the number of matching times to return
+
+    Returns:
+      a list of n datetime objects
+    """
+    out = []
+    for _ in range(n):
+      start = self.GetMatch(start)
+      out.append(start)
+    return out
+
+  def GetMatch(self, start):
+    """Returns the next match after time start.
+
+    Must be implemented in subclasses.
+
+    Arguments:
+      start: a datetime to start with. Matches will start from this time
+
+    Returns:
+      a datetime object
+    """
+    raise NotImplementedError
+
+
+class IntervalTimeSpecification(TimeSpecification):
+  """A time specification for a given interval.
+
+  An Interval type spec runs at the given fixed interval. They have two
+  attributes:
+  period   - the type of interval, either "hours" or "minutes"
+  interval - the number of units of type period.
+  """
+
+  def __init__(self, interval, period):
+    super(IntervalTimeSpecification, self).__init__(self)
+    self.interval = interval
+    self.period = period
+
+  def GetMatch(self, t):
+    """Returns the next match after time 't'.
+
+    Arguments:
+      t: a datetime to start from. Matches will start from after this time
+
+    Returns:
+      a datetime object
+    """
+    if self.period == HOURS:
+      return t + datetime.timedelta(hours=self.interval)
+    else:
+      return t + datetime.timedelta(minutes=self.interval)
+
+
+class SpecificTimeSpecification(TimeSpecification):
+  """Specific time specification.
+
+  A Specific interval is more complex, but define a certain time to run, on
+  given days. They have the following attributes:
+  time     - the time of day to run, as "HH:MM"
+  ordinals - first, second, third &c, as a set of integers in 1..5
+  months   - the months that this is valid, as a set of integers in 1..12
+  weekdays - the days of the week to run this, 0=Sunday, 6=Saturday.
+
+  The specific time interval can be quite complex. A schedule could look like
+  this:
+  "1st,third sat,sun of jan,feb,mar 09:15"
+
+  In this case, ordinals would be [1,3], weekdays [0,6], months [1,2,3] and time
+  would be "09:15".
+  """
+
+  def __init__(self, ordinals=None, weekdays=None, months=None, monthdays=None,
+               timestr='00:00'):
+    super(SpecificTimeSpecification, self).__init__(self)
+    if weekdays and monthdays:
+      raise ValueError("can't supply both monthdays and weekdays")
+    if ordinals is None:
+      self.ordinals = set(range(1, 6))
+    else:
+      self.ordinals = ordinals
+
+    if weekdays is None:
+      self.weekdays = set(range(7))
+    else:
+      self.weekdays = weekdays
+
+    if months is None:
+      self.months = set(range(1, 13))
+    else:
+      self.months = months
+
+    if monthdays is None:
+      self.monthdays = set()
+    else:
+      self.monthdays = monthdays
+    hourstr, minutestr = timestr.split(':')
+    self.time = datetime.time(int(hourstr), int(minutestr))
+
+  def _MatchingDays(self, year, month):
+    """Returns matching days for the given year and month.
+
+    For the given year and month, return the days that match this instance's
+    day specification, based on the ordinals and weekdays.
+
+    Arguments:
+      year: the year as an integer
+      month: the month as an integer, in range 1-12
+
+    Returns:
+      a list of matching days, as ints in range 1-31
+    """
+    out_days = []
+    start_day, last_day = calendar.monthrange(year, month)
+    start_day = (start_day + 1) % 7
+    for ordinal in self.ordinals:
+      for weekday in self.weekdays:
+        day = ((weekday - start_day) % 7) + 1
+        day += 7 * (ordinal - 1)
+        if day <= last_day:
+          out_days.append(day)
+    return sorted(out_days)
+
+  def _NextMonthGenerator(self, start, matches):
+    """Creates a generator that produces results from the set 'matches'.
+
+    Matches must be >= 'start'. If none match, the wrap counter is incremented,
+    and the result set is reset to the full set. Yields a 2-tuple of (match,
+    wrapcount).
+
+    Arguments:
+      start: first set of matches will be >= this value (an int)
+      matches: the set of potential matches (a sequence of ints)
+
+    Yields:
+      a two-tuple of (match, wrap counter). match is an int in range (1-12),
+      wrapcount is a int indicating how many times we've wrapped around.
+    """
+    potential = matches = sorted(matches)
+    after = start - 1
+    wrapcount = 0
+    while True:
+      potential = [x for x in potential if x > after]
+      if not potential:
+        wrapcount += 1
+        potential = matches
+      after = potential[0]
+      yield (after, wrapcount)
+
+  def GetMatch(self, start):
+    """Returns the next time that matches the schedule after time start.
+
+    Arguments:
+      start: a datetime to start with. Matches will start after this time
+
+    Returns:
+      a datetime object
+    """
+    start_time = start
+    if self.months:
+      months = self._NextMonthGenerator(start.month, self.months)
+    while True:
+      month, yearwraps = months.next()
+      candidate = start_time.replace(day=1, month=month,
+                                     year=start_time.year + yearwraps)
+
+      if self.monthdays:
+        _, last_day = calendar.monthrange(candidate.year, candidate.month)
+        day_matches = sorted([x for x in self.monthdays if x <= last_day])
+      else:
+        day_matches = self._MatchingDays(candidate.year, month)
+
+      if ((candidate.year, candidate.month)
+          == (start_time.year, start_time.month)):
+        day_matches = [x for x in day_matches if x >= start_time.day]
+        if day_matches and day_matches[0] == start_time.day:
+          if start_time.time() >= self.time:
+            day_matches.pop(0)
+      if not day_matches:
+        continue
+      out = candidate.replace(day=day_matches[0], hour=self.time.hour,
+                              minute=self.time.minute, second=0, microsecond=0)
+      return out