schedules.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755
  1. # -*- coding: utf-8 -*-
  2. """
  3. celery.schedules
  4. ~~~~~~~~~~~~~~~~
  5. Schedules define the intervals at which periodic tasks
  6. should run.
  7. """
  8. from __future__ import absolute_import
  9. import numbers
  10. import re
  11. from collections import namedtuple
  12. from datetime import datetime, timedelta
  13. from kombu.utils import cached_property
  14. from . import current_app
  15. from .five import range, string_t
  16. from .utils import is_iterable
  17. from .utils.timeutils import (
  18. weekday, maybe_timedelta, remaining, humanize_seconds,
  19. timezone, maybe_make_aware, ffwd, localize
  20. )
  21. from .datastructures import AttributeDict
  22. __all__ = ['ParseException', 'schedule', 'crontab', 'crontab_parser',
  23. 'maybe_schedule', 'solar']
  24. schedstate = namedtuple('schedstate', ('is_due', 'next'))
  25. CRON_PATTERN_INVALID = """\
  26. Invalid crontab pattern. Valid range is {min}-{max}. \
  27. '{value}' was found.\
  28. """
  29. CRON_INVALID_TYPE = """\
  30. Argument cronspec needs to be of any of the following types: \
  31. int, str, or an iterable type. {type!r} was given.\
  32. """
  33. CRON_REPR = """\
  34. <crontab: {0._orig_minute} {0._orig_hour} {0._orig_day_of_week} \
  35. {0._orig_day_of_month} {0._orig_month_of_year} (m/h/d/dM/MY)>\
  36. """
  37. SOLAR_INVALID_LATITUDE = """\
  38. Argument latitude {lat} is invalid, must be between -90 and 90.\
  39. """
  40. SOLAR_INVALID_LONGITUDE = """\
  41. Argument longitude {lon} is invalid, must be between -180 and 180.\
  42. """
  43. SOLAR_INVALID_EVENT = """\
  44. Argument event "{event}" is invalid, must be one of {all_events}.\
  45. """
  46. def cronfield(s):
  47. return '*' if s is None else s
  48. class ParseException(Exception):
  49. """Raised by crontab_parser when the input can't be parsed."""
  50. class schedule(object):
  51. """Schedule for periodic task.
  52. :param run_every: Interval in seconds (or a :class:`~datetime.timedelta`).
  53. :param relative: If set to True the run time will be rounded to the
  54. resolution of the interval.
  55. :param nowfun: Function returning the current date and time
  56. (class:`~datetime.datetime`).
  57. :param app: Celery app instance.
  58. """
  59. relative = False
  60. def __init__(self, run_every=None, relative=False, nowfun=None, app=None):
  61. self.run_every = maybe_timedelta(run_every)
  62. self.relative = relative
  63. self.nowfun = nowfun
  64. self._app = app
  65. def now(self):
  66. return (self.nowfun or self.app.now)()
  67. def remaining_estimate(self, last_run_at):
  68. return remaining(
  69. self.maybe_make_aware(last_run_at), self.run_every,
  70. self.maybe_make_aware(self.now()), self.relative,
  71. )
  72. def is_due(self, last_run_at):
  73. """Returns tuple of two items `(is_due, next_time_to_check)`,
  74. where next time to check is in seconds.
  75. e.g.
  76. * `(True, 20)`, means the task should be run now, and the next
  77. time to check is in 20 seconds.
  78. * `(False, 12.3)`, means the task is not due, but that the scheduler
  79. should check again in 12.3 seconds.
  80. The next time to check is used to save energy/cpu cycles,
  81. it does not need to be accurate but will influence the precision
  82. of your schedule. You must also keep in mind
  83. the value of :setting:`beat_max_loop_interval`,
  84. which decides the maximum number of seconds the scheduler can
  85. sleep between re-checking the periodic task intervals. So if you
  86. have a task that changes schedule at runtime then your next_run_at
  87. check will decide how long it will take before a change to the
  88. schedule takes effect. The max loop interval takes precendence
  89. over the next check at value returned.
  90. .. admonition:: Scheduler max interval variance
  91. The default max loop interval may vary for different schedulers.
  92. For the default scheduler the value is 5 minutes, but for e.g.
  93. the django-celery database scheduler the value is 5 seconds.
  94. """
  95. last_run_at = self.maybe_make_aware(last_run_at)
  96. rem_delta = self.remaining_estimate(last_run_at)
  97. remaining_s = max(rem_delta.total_seconds(), 0)
  98. if remaining_s == 0:
  99. return schedstate(is_due=True, next=self.seconds)
  100. return schedstate(is_due=False, next=remaining_s)
  101. def maybe_make_aware(self, dt):
  102. return maybe_make_aware(dt, self.tz)
  103. def __repr__(self):
  104. return '<freq: {0.human_seconds}>'.format(self)
  105. def __eq__(self, other):
  106. if isinstance(other, schedule):
  107. return self.run_every == other.run_every
  108. return self.run_every == other
  109. def __ne__(self, other):
  110. return not self.__eq__(other)
  111. def __reduce__(self):
  112. return self.__class__, (self.run_every, self.relative, self.nowfun)
  113. @property
  114. def seconds(self):
  115. return max(self.run_every.total_seconds(), 0)
  116. @property
  117. def human_seconds(self):
  118. return humanize_seconds(self.seconds)
  119. @property
  120. def app(self):
  121. return self._app or current_app._get_current_object()
  122. @app.setter # noqa
  123. def app(self, app):
  124. self._app = app
  125. @cached_property
  126. def tz(self):
  127. return self.app.timezone
  128. @cached_property
  129. def utc_enabled(self):
  130. return self.app.conf.enable_utc
  131. def to_local(self, dt):
  132. if not self.utc_enabled:
  133. return timezone.to_local_fallback(dt)
  134. return dt
  135. class crontab_parser(object):
  136. """Parser for crontab expressions. Any expression of the form 'groups'
  137. (see BNF grammar below) is accepted and expanded to a set of numbers.
  138. These numbers represent the units of time that the crontab needs to
  139. run on::
  140. digit :: '0'..'9'
  141. dow :: 'a'..'z'
  142. number :: digit+ | dow+
  143. steps :: number
  144. range :: number ( '-' number ) ?
  145. numspec :: '*' | range
  146. expr :: numspec ( '/' steps ) ?
  147. groups :: expr ( ',' expr ) *
  148. The parser is a general purpose one, useful for parsing hours, minutes and
  149. day_of_week expressions. Example usage::
  150. >>> minutes = crontab_parser(60).parse('*/15')
  151. [0, 15, 30, 45]
  152. >>> hours = crontab_parser(24).parse('*/4')
  153. [0, 4, 8, 12, 16, 20]
  154. >>> day_of_week = crontab_parser(7).parse('*')
  155. [0, 1, 2, 3, 4, 5, 6]
  156. It can also parse day_of_month and month_of_year expressions if initialized
  157. with an minimum of 1. Example usage::
  158. >>> days_of_month = crontab_parser(31, 1).parse('*/3')
  159. [1, 4, 7, 10, 13, 16, 19, 22, 25, 28, 31]
  160. >>> months_of_year = crontab_parser(12, 1).parse('*/2')
  161. [1, 3, 5, 7, 9, 11]
  162. >>> months_of_year = crontab_parser(12, 1).parse('2-12/2')
  163. [2, 4, 6, 8, 10, 12]
  164. The maximum possible expanded value returned is found by the formula::
  165. max_ + min_ - 1
  166. """
  167. ParseException = ParseException
  168. _range = r'(\w+?)-(\w+)'
  169. _steps = r'/(\w+)?'
  170. _star = r'\*'
  171. def __init__(self, max_=60, min_=0):
  172. self.max_ = max_
  173. self.min_ = min_
  174. self.pats = (
  175. (re.compile(self._range + self._steps), self._range_steps),
  176. (re.compile(self._range), self._expand_range),
  177. (re.compile(self._star + self._steps), self._star_steps),
  178. (re.compile('^' + self._star + '$'), self._expand_star),
  179. )
  180. def parse(self, spec):
  181. acc = set()
  182. for part in spec.split(','):
  183. if not part:
  184. raise self.ParseException('empty part')
  185. acc |= set(self._parse_part(part))
  186. return acc
  187. def _parse_part(self, part):
  188. for regex, handler in self.pats:
  189. m = regex.match(part)
  190. if m:
  191. return handler(m.groups())
  192. return self._expand_range((part,))
  193. def _expand_range(self, toks):
  194. fr = self._expand_number(toks[0])
  195. if len(toks) > 1:
  196. to = self._expand_number(toks[1])
  197. if to < fr: # Wrap around max_ if necessary
  198. return (list(range(fr, self.min_ + self.max_)) +
  199. list(range(self.min_, to + 1)))
  200. return list(range(fr, to + 1))
  201. return [fr]
  202. def _range_steps(self, toks):
  203. if len(toks) != 3 or not toks[2]:
  204. raise self.ParseException('empty filter')
  205. return self._expand_range(toks[:2])[::int(toks[2])]
  206. def _star_steps(self, toks):
  207. if not toks or not toks[0]:
  208. raise self.ParseException('empty filter')
  209. return self._expand_star()[::int(toks[0])]
  210. def _expand_star(self, *args):
  211. return list(range(self.min_, self.max_ + self.min_))
  212. def _expand_number(self, s):
  213. if isinstance(s, string_t) and s[0] == '-':
  214. raise self.ParseException('negative numbers not supported')
  215. try:
  216. i = int(s)
  217. except ValueError:
  218. try:
  219. i = weekday(s)
  220. except KeyError:
  221. raise ValueError('Invalid weekday literal {0!r}.'.format(s))
  222. max_val = self.min_ + self.max_ - 1
  223. if i > max_val:
  224. raise ValueError(
  225. 'Invalid end range: {0} > {1}.'.format(i, max_val))
  226. if i < self.min_:
  227. raise ValueError(
  228. 'Invalid beginning range: {0} < {1}.'.format(i, self.min_))
  229. return i
  230. class crontab(schedule):
  231. """A crontab can be used as the `run_every` value of a
  232. :class:`PeriodicTask` to add cron-like scheduling.
  233. Like a :manpage:`cron` job, you can specify units of time of when
  234. you would like the task to execute. It is a reasonably complete
  235. implementation of cron's features, so it should provide a fair
  236. degree of scheduling needs.
  237. You can specify a minute, an hour, a day of the week, a day of the
  238. month, and/or a month in the year in any of the following formats:
  239. .. attribute:: minute
  240. - A (list of) integers from 0-59 that represent the minutes of
  241. an hour of when execution should occur; or
  242. - A string representing a crontab pattern. This may get pretty
  243. advanced, like `minute='*/15'` (for every quarter) or
  244. `minute='1,13,30-45,50-59/2'`.
  245. .. attribute:: hour
  246. - A (list of) integers from 0-23 that represent the hours of
  247. a day of when execution should occur; or
  248. - A string representing a crontab pattern. This may get pretty
  249. advanced, like `hour='*/3'` (for every three hours) or
  250. `hour='0,8-17/2'` (at midnight, and every two hours during
  251. office hours).
  252. .. attribute:: day_of_week
  253. - A (list of) integers from 0-6, where Sunday = 0 and Saturday =
  254. 6, that represent the days of a week that execution should
  255. occur.
  256. - A string representing a crontab pattern. This may get pretty
  257. advanced, like `day_of_week='mon-fri'` (for weekdays only).
  258. (Beware that `day_of_week='*/2'` does not literally mean
  259. 'every two days', but 'every day that is divisible by two'!)
  260. .. attribute:: day_of_month
  261. - A (list of) integers from 1-31 that represents the days of the
  262. month that execution should occur.
  263. - A string representing a crontab pattern. This may get pretty
  264. advanced, such as `day_of_month='2-30/3'` (for every even
  265. numbered day) or `day_of_month='1-7,15-21'` (for the first and
  266. third weeks of the month).
  267. .. attribute:: month_of_year
  268. - A (list of) integers from 1-12 that represents the months of
  269. the year during which execution can occur.
  270. - A string representing a crontab pattern. This may get pretty
  271. advanced, such as `month_of_year='*/3'` (for the first month
  272. of every quarter) or `month_of_year='2-12/2'` (for every even
  273. numbered month).
  274. .. attribute:: nowfun
  275. Function returning the current date and time
  276. (:class:`~datetime.datetime`).
  277. .. attribute:: app
  278. The Celery app instance.
  279. It is important to realize that any day on which execution should
  280. occur must be represented by entries in all three of the day and
  281. month attributes. For example, if `day_of_week` is 0 and `day_of_month`
  282. is every seventh day, only months that begin on Sunday and are also
  283. in the `month_of_year` attribute will have execution events. Or,
  284. `day_of_week` is 1 and `day_of_month` is '1-7,15-21' means every
  285. first and third monday of every month present in `month_of_year`.
  286. """
  287. def __init__(self, minute='*', hour='*', day_of_week='*',
  288. day_of_month='*', month_of_year='*', nowfun=None, app=None):
  289. self._orig_minute = cronfield(minute)
  290. self._orig_hour = cronfield(hour)
  291. self._orig_day_of_week = cronfield(day_of_week)
  292. self._orig_day_of_month = cronfield(day_of_month)
  293. self._orig_month_of_year = cronfield(month_of_year)
  294. self.hour = self._expand_cronspec(hour, 24)
  295. self.minute = self._expand_cronspec(minute, 60)
  296. self.day_of_week = self._expand_cronspec(day_of_week, 7)
  297. self.day_of_month = self._expand_cronspec(day_of_month, 31, 1)
  298. self.month_of_year = self._expand_cronspec(month_of_year, 12, 1)
  299. self.nowfun = nowfun
  300. self._app = app
  301. @staticmethod
  302. def _expand_cronspec(cronspec, max_, min_=0):
  303. """Takes the given cronspec argument in one of the forms::
  304. int (like 7)
  305. str (like '3-5,*/15', '*', or 'monday')
  306. set (like {0,15,30,45}
  307. list (like [8-17])
  308. And convert it to an (expanded) set representing all time unit
  309. values on which the crontab triggers. Only in case of the base
  310. type being 'str', parsing occurs. (It is fast and
  311. happens only once for each crontab instance, so there is no
  312. significant performance overhead involved.)
  313. For the other base types, merely Python type conversions happen.
  314. The argument `max_` is needed to determine the expansion of '*'
  315. and ranges.
  316. The argument `min_` is needed to determine the expansion of '*'
  317. and ranges for 1-based cronspecs, such as day of month or month
  318. of year. The default is sufficient for minute, hour, and day of
  319. week.
  320. """
  321. if isinstance(cronspec, numbers.Integral):
  322. result = {cronspec}
  323. elif isinstance(cronspec, string_t):
  324. result = crontab_parser(max_, min_).parse(cronspec)
  325. elif isinstance(cronspec, set):
  326. result = cronspec
  327. elif is_iterable(cronspec):
  328. result = set(cronspec)
  329. else:
  330. raise TypeError(CRON_INVALID_TYPE.format(type=type(cronspec)))
  331. # assure the result does not preceed the min or exceed the max
  332. for number in result:
  333. if number >= max_ + min_ or number < min_:
  334. raise ValueError(CRON_PATTERN_INVALID.format(
  335. min=min_, max=max_ - 1 + min_, value=number))
  336. return result
  337. def _delta_to_next(self, last_run_at, next_hour, next_minute):
  338. """
  339. Takes a datetime of last run, next minute and hour, and
  340. returns a relativedelta for the next scheduled day and time.
  341. Only called when day_of_month and/or month_of_year cronspec
  342. is specified to further limit scheduled task execution.
  343. """
  344. from bisect import bisect, bisect_left
  345. datedata = AttributeDict(year=last_run_at.year)
  346. days_of_month = sorted(self.day_of_month)
  347. months_of_year = sorted(self.month_of_year)
  348. def day_out_of_range(year, month, day):
  349. try:
  350. datetime(year=year, month=month, day=day)
  351. except ValueError:
  352. return True
  353. return False
  354. def roll_over():
  355. while 1:
  356. flag = (datedata.dom == len(days_of_month) or
  357. day_out_of_range(datedata.year,
  358. months_of_year[datedata.moy],
  359. days_of_month[datedata.dom]) or
  360. (self.maybe_make_aware(datetime(datedata.year,
  361. months_of_year[datedata.moy],
  362. days_of_month[datedata.dom])) < last_run_at))
  363. if flag:
  364. datedata.dom = 0
  365. datedata.moy += 1
  366. if datedata.moy == len(months_of_year):
  367. datedata.moy = 0
  368. datedata.year += 1
  369. else:
  370. break
  371. if last_run_at.month in self.month_of_year:
  372. datedata.dom = bisect(days_of_month, last_run_at.day)
  373. datedata.moy = bisect_left(months_of_year, last_run_at.month)
  374. else:
  375. datedata.dom = 0
  376. datedata.moy = bisect(months_of_year, last_run_at.month)
  377. if datedata.moy == len(months_of_year):
  378. datedata.moy = 0
  379. roll_over()
  380. while 1:
  381. th = datetime(year=datedata.year,
  382. month=months_of_year[datedata.moy],
  383. day=days_of_month[datedata.dom])
  384. if th.isoweekday() % 7 in self.day_of_week:
  385. break
  386. datedata.dom += 1
  387. roll_over()
  388. return ffwd(year=datedata.year,
  389. month=months_of_year[datedata.moy],
  390. day=days_of_month[datedata.dom],
  391. hour=next_hour,
  392. minute=next_minute,
  393. second=0,
  394. microsecond=0)
  395. def now(self):
  396. return (self.nowfun or self.app.now)()
  397. def __repr__(self):
  398. return CRON_REPR.format(self)
  399. def __reduce__(self):
  400. return (self.__class__, (self._orig_minute,
  401. self._orig_hour,
  402. self._orig_day_of_week,
  403. self._orig_day_of_month,
  404. self._orig_month_of_year), None)
  405. def remaining_delta(self, last_run_at, tz=None, ffwd=ffwd):
  406. tz = tz or self.tz
  407. last_run_at = self.maybe_make_aware(last_run_at)
  408. now = self.maybe_make_aware(self.now())
  409. dow_num = last_run_at.isoweekday() % 7 # Sunday is day 0, not day 7
  410. execute_this_date = (last_run_at.month in self.month_of_year and
  411. last_run_at.day in self.day_of_month and
  412. dow_num in self.day_of_week)
  413. execute_this_hour = (execute_this_date and
  414. last_run_at.day == now.day and
  415. last_run_at.month == now.month and
  416. last_run_at.year == now.year and
  417. last_run_at.hour in self.hour and
  418. last_run_at.minute < max(self.minute))
  419. if execute_this_hour:
  420. next_minute = min(minute for minute in self.minute
  421. if minute > last_run_at.minute)
  422. delta = ffwd(minute=next_minute, second=0, microsecond=0)
  423. else:
  424. next_minute = min(self.minute)
  425. execute_today = (execute_this_date and
  426. last_run_at.hour < max(self.hour))
  427. if execute_today:
  428. next_hour = min(hour for hour in self.hour
  429. if hour > last_run_at.hour)
  430. delta = ffwd(hour=next_hour, minute=next_minute,
  431. second=0, microsecond=0)
  432. else:
  433. next_hour = min(self.hour)
  434. all_dom_moy = (self._orig_day_of_month == '*' and
  435. self._orig_month_of_year == '*')
  436. if all_dom_moy:
  437. next_day = min([day for day in self.day_of_week
  438. if day > dow_num] or self.day_of_week)
  439. add_week = next_day == dow_num
  440. delta = ffwd(weeks=add_week and 1 or 0,
  441. weekday=(next_day - 1) % 7,
  442. hour=next_hour,
  443. minute=next_minute,
  444. second=0,
  445. microsecond=0)
  446. else:
  447. delta = self._delta_to_next(last_run_at,
  448. next_hour, next_minute)
  449. return self.to_local(last_run_at), delta, self.to_local(now)
  450. def remaining_estimate(self, last_run_at, ffwd=ffwd):
  451. """Returns when the periodic task should run next as a timedelta."""
  452. return remaining(*self.remaining_delta(last_run_at, ffwd=ffwd))
  453. def is_due(self, last_run_at):
  454. """Returns tuple of two items `(is_due, next_time_to_run)`,
  455. where next time to run is in seconds.
  456. See :meth:`celery.schedules.schedule.is_due` for more information.
  457. """
  458. rem_delta = self.remaining_estimate(last_run_at)
  459. rem = max(rem_delta.total_seconds(), 0)
  460. due = rem == 0
  461. if due:
  462. rem_delta = self.remaining_estimate(self.now())
  463. rem = max(rem_delta.total_seconds(), 0)
  464. return schedstate(due, rem)
  465. def __eq__(self, other):
  466. if isinstance(other, crontab):
  467. return (other.month_of_year == self.month_of_year and
  468. other.day_of_month == self.day_of_month and
  469. other.day_of_week == self.day_of_week and
  470. other.hour == self.hour and
  471. other.minute == self.minute)
  472. return NotImplemented
  473. def __ne__(self, other):
  474. res = self.__eq__(other)
  475. if res is NotImplemented:
  476. return True
  477. return not res
  478. def maybe_schedule(s, relative=False, app=None):
  479. if s is not None:
  480. if isinstance(s, numbers.Integral):
  481. s = timedelta(seconds=s)
  482. if isinstance(s, timedelta):
  483. return schedule(s, relative, app=app)
  484. else:
  485. s.app = app
  486. return s
  487. class solar(schedule):
  488. """A solar event can be used as the `run_every` value of a
  489. :class:`PeriodicTask` to schedule based on certain solar events.
  490. :param event: Solar event that triggers this task. Available
  491. values are: dawn_astronomical, dawn_nautical, dawn_civil,
  492. sunrise, solar_noon, sunset, dusk_civil, dusk_nautical,
  493. dusk_astronomical
  494. :param lat: The latitude of the observer.
  495. :param lon: The longitude of the observer.
  496. :param nowfun: Function returning the current date and time
  497. (class:`~datetime.datetime`).
  498. :param app: Celery app instance.
  499. """
  500. _all_events = [
  501. 'dawn_astronomical',
  502. 'dawn_nautical',
  503. 'dawn_civil',
  504. 'sunrise',
  505. 'solar_noon',
  506. 'sunset',
  507. 'dusk_civil',
  508. 'dusk_nautical',
  509. 'dusk_astronomical',
  510. ]
  511. _horizons = {
  512. 'dawn_astronomical': '-18',
  513. 'dawn_nautical': '-12',
  514. 'dawn_civil': '-6',
  515. 'sunrise': '-0:34',
  516. 'solar_noon': '0',
  517. 'sunset': '-0:34',
  518. 'dusk_civil': '-6',
  519. 'dusk_nautical': '-12',
  520. 'dusk_astronomical': '18',
  521. }
  522. _methods = {
  523. 'dawn_astronomical': 'next_rising',
  524. 'dawn_nautical': 'next_rising',
  525. 'dawn_civil': 'next_rising',
  526. 'sunrise': 'next_rising',
  527. 'solar_noon': 'next_transit',
  528. 'sunset': 'next_setting',
  529. 'dusk_civil': 'next_setting',
  530. 'dusk_nautical': 'next_setting',
  531. 'dusk_astronomical': 'next_setting',
  532. }
  533. _use_center_l = {
  534. 'dawn_astronomical': True,
  535. 'dawn_nautical': True,
  536. 'dawn_civil': True,
  537. 'sunrise': False,
  538. 'solar_noon': True,
  539. 'sunset': False,
  540. 'dusk_civil': True,
  541. 'dusk_nautical': True,
  542. 'dusk_astronomical': True,
  543. }
  544. def __init__(self, event, lat, lon, nowfun=None, app=None):
  545. self.ephem = __import__('ephem')
  546. self.event = event
  547. self.lat = lat
  548. self.lon = lon
  549. self.nowfun = nowfun
  550. self._app = app
  551. if event not in self._all_events:
  552. raise ValueError(SOLAR_INVALID_EVENT.format(
  553. event=event, all_events=', '.join(self._all_events),
  554. ))
  555. if lat < -90 or lat > 90:
  556. raise ValueError(SOLAR_INVALID_LATITUDE.format(lat=lat))
  557. if lon < -180 or lon > 180:
  558. raise ValueError(SOLAR_INVALID_LONGITUDE.format(lon=lon))
  559. cal = self.ephem.Observer()
  560. cal.lat = str(lat)
  561. cal.lon = str(lon)
  562. cal.elev = 0
  563. cal.horizon = self._horizons[event]
  564. cal.pressure = 0
  565. self.cal = cal
  566. self.method = self._methods[event]
  567. self.use_center = self._use_center_l[event]
  568. def __reduce__(self):
  569. return self.__class__, (self.event, self.lat, self.lon)
  570. def __repr__(self):
  571. return '<solar: {0} at latitude {1}, longitude: {2}>'.format(
  572. self.event, self.lat, self.lon,
  573. )
  574. def remaining_estimate(self, last_run_at):
  575. """Returns when the periodic task should run next as a timedelta,
  576. or if it shouldn't run today (e.g. the sun does not rise today),
  577. returns the time when the next check should take place."""
  578. last_run_at = self.maybe_make_aware(last_run_at)
  579. last_run_at_utc = localize(last_run_at, timezone.utc)
  580. self.cal.date = last_run_at_utc
  581. try:
  582. next_utc = getattr(self.cal, self.method)(
  583. self.ephem.Sun(),
  584. start=last_run_at_utc, use_center=self.use_center,
  585. )
  586. except self.ephem.CircumpolarError: # pragma: no cover
  587. """Sun will not rise/set today. Check again tomorrow
  588. (specifically, after the next anti-transit)."""
  589. next_utc = (
  590. self.cal.next_antitransit(self.ephem.Sun()) +
  591. timedelta(minutes=1)
  592. )
  593. next = self.maybe_make_aware(next_utc.datetime())
  594. now = self.maybe_make_aware(self.now())
  595. delta = next - now
  596. return delta
  597. def is_due(self, last_run_at):
  598. """Returns tuple of two items `(is_due, next_time_to_run)`,
  599. where next time to run is in seconds.
  600. See :meth:`celery.schedules.schedule.is_due` for more information.
  601. """
  602. rem_delta = self.remaining_estimate(last_run_at)
  603. rem = max(rem_delta.total_seconds(), 0)
  604. due = rem == 0
  605. if due:
  606. rem_delta = self.remaining_estimate(self.now())
  607. rem = max(rem_delta.total_seconds(), 0)
  608. return schedstate(due, rem)
  609. def __eq__(self, other):
  610. if isinstance(other, solar):
  611. return (other.event == self.event and
  612. other.lat == self.lat and
  613. other.lon == self.lon)
  614. return NotImplemented
  615. def __ne__(self, other):
  616. res = self.__eq__(other)
  617. if res is NotImplemented:
  618. return True
  619. return not res