schedule.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. # -*- coding: utf-8 -*-
  2. """
  3. Classes for calling functions a schedule.
  4. """
  5. from __future__ import absolute_import
  6. import datetime
  7. import numbers
  8. import pytz
  9. def now():
  10. """
  11. Provide the current timezone-aware datetime.
  12. A client may override this function to change the default behavior,
  13. such as to use local time or timezone-naïve times.
  14. """
  15. return datetime.datetime.utcnow().replace(tzinfo=pytz.utc)
  16. def from_timestamp(ts):
  17. """
  18. Convert a numeric timestamp to a timezone-aware datetime.
  19. A client may override this function to change the default behavior,
  20. such as to use local time or timezone-naïve times.
  21. """
  22. return datetime.datetime.utcfromtimestamp(ts).replace(tzinfo=pytz.utc)
  23. class DelayedCommand(datetime.datetime):
  24. """
  25. A command to be executed after some delay (seconds or timedelta).
  26. """
  27. @classmethod
  28. def from_datetime(cls, other):
  29. return cls(other.year, other.month, other.day, other.hour,
  30. other.minute, other.second, other.microsecond,
  31. other.tzinfo)
  32. @classmethod
  33. def after(cls, delay, function):
  34. if not isinstance(delay, datetime.timedelta):
  35. delay = datetime.timedelta(seconds=delay)
  36. due_time = now() + delay
  37. cmd = cls.from_datetime(due_time)
  38. cmd.delay = delay
  39. cmd.function = function
  40. return cmd
  41. @staticmethod
  42. def _from_timestamp(input):
  43. """
  44. If input is a real number, interpret it as a Unix timestamp
  45. (seconds sinc Epoch in UTC) and return a timezone-aware
  46. datetime object. Otherwise return input unchanged.
  47. """
  48. if not isinstance(input, numbers.Real):
  49. return input
  50. return from_timestamp(input)
  51. @classmethod
  52. def at_time(cls, at, function):
  53. """
  54. Construct a DelayedCommand to come due at `at`, where `at` may be
  55. a datetime or timestamp.
  56. """
  57. at = cls._from_timestamp(at)
  58. cmd = cls.from_datetime(at)
  59. cmd.delay = at - now()
  60. cmd.function = function
  61. return cmd
  62. def due(self):
  63. return now() >= self
  64. class PeriodicCommand(DelayedCommand):
  65. """
  66. Like a delayed command, but expect this command to run every delay
  67. seconds.
  68. """
  69. def next(self):
  70. cmd = self.__class__.from_datetime(self + self.delay)
  71. cmd.delay = self.delay
  72. cmd.function = self.function
  73. return cmd
  74. def __setattr__(self, key, value):
  75. if key == 'delay' and not value > datetime.timedelta():
  76. raise ValueError("A PeriodicCommand must have a positive, "
  77. "non-zero delay.")
  78. super(PeriodicCommand, self).__setattr__(key, value)
  79. class PeriodicCommandFixedDelay(PeriodicCommand):
  80. """
  81. Like a periodic command, but don't calculate the delay based on
  82. the current time. Instead use a fixed delay following the initial
  83. run.
  84. """
  85. @classmethod
  86. def at_time(cls, at, delay, function):
  87. at = cls._from_timestamp(at)
  88. cmd = cls.from_datetime(at)
  89. if not isinstance(delay, datetime.timedelta):
  90. delay = datetime.timedelta(seconds=delay)
  91. cmd.delay = delay
  92. cmd.function = function
  93. return cmd
  94. @classmethod
  95. def daily_at(cls, at, function):
  96. """
  97. Schedule a command to run at a specific time each day.
  98. """
  99. daily = datetime.timedelta(days=1)
  100. # convert when to the next datetime matching this time
  101. when = datetime.datetime.combine(datetime.date.today(), at)
  102. if when < now():
  103. when += daily
  104. return cls.at_time(when, daily, function)