Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/celery/schedules.py : 38%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2"""Schedules define the intervals at which periodic tasks run."""
3from __future__ import absolute_import, unicode_literals
5import numbers
6import re
7from bisect import bisect, bisect_left
8from collections import namedtuple
9from datetime import datetime, timedelta
11from kombu.utils.objects import cached_property
13from . import current_app
14from .five import python_2_unicode_compatible, range, string_t
15from .utils.collections import AttributeDict
16from .utils.time import (ffwd, humanize_seconds, localize, maybe_make_aware,
17 maybe_timedelta, remaining, timezone, weekday)
19try:
20 from collections.abc import Iterable
21except ImportError:
22 # TODO: Remove this when we drop Python 2.7 support
23 from collections import Iterable
26__all__ = (
27 'ParseException', 'schedule', 'crontab', 'crontab_parser',
28 'maybe_schedule', 'solar',
29)
31schedstate = namedtuple('schedstate', ('is_due', 'next'))
33CRON_PATTERN_INVALID = """\
34Invalid crontab pattern. Valid range is {min}-{max}. \
35'{value}' was found.\
36"""
38CRON_INVALID_TYPE = """\
39Argument cronspec needs to be of any of the following types: \
40int, str, or an iterable type. {type!r} was given.\
41"""
43CRON_REPR = """\
44<crontab: {0._orig_minute} {0._orig_hour} {0._orig_day_of_week} \
45{0._orig_day_of_month} {0._orig_month_of_year} (m/h/d/dM/MY)>\
46"""
48SOLAR_INVALID_LATITUDE = """\
49Argument latitude {lat} is invalid, must be between -90 and 90.\
50"""
52SOLAR_INVALID_LONGITUDE = """\
53Argument longitude {lon} is invalid, must be between -180 and 180.\
54"""
56SOLAR_INVALID_EVENT = """\
57Argument event "{event}" is invalid, must be one of {all_events}.\
58"""
61def cronfield(s):
62 return '*' if s is None else s
65class ParseException(Exception):
66 """Raised by :class:`crontab_parser` when the input can't be parsed."""
69class BaseSchedule(object):
71 def __init__(self, nowfun=None, app=None):
72 self.nowfun = nowfun
73 self._app = app
75 def now(self):
76 return (self.nowfun or self.app.now)()
78 def remaining_estimate(self, last_run_at):
79 raise NotImplementedError()
81 def is_due(self, last_run_at):
82 raise NotImplementedError()
84 def maybe_make_aware(self, dt):
85 return maybe_make_aware(dt, self.tz)
87 @property
88 def app(self):
89 return self._app or current_app._get_current_object()
91 @app.setter # noqa
92 def app(self, app):
93 self._app = app
95 @cached_property
96 def tz(self):
97 return self.app.timezone
99 @cached_property
100 def utc_enabled(self):
101 return self.app.conf.enable_utc
103 def to_local(self, dt):
104 if not self.utc_enabled:
105 return timezone.to_local_fallback(dt)
106 return dt
108 def __eq__(self, other):
109 if isinstance(other, BaseSchedule):
110 return other.nowfun == self.nowfun
111 return NotImplemented
114@python_2_unicode_compatible
115class schedule(BaseSchedule):
116 """Schedule for periodic task.
118 Arguments:
119 run_every (float, ~datetime.timedelta): Time interval.
120 relative (bool): If set to True the run time will be rounded to the
121 resolution of the interval.
122 nowfun (Callable): Function returning the current date and time
123 (:class:`~datetime.datetime`).
124 app (Celery): Celery app instance.
125 """
127 relative = False
129 def __init__(self, run_every=None, relative=False, nowfun=None, app=None):
130 self.run_every = maybe_timedelta(run_every)
131 self.relative = relative
132 super(schedule, self).__init__(nowfun=nowfun, app=app)
134 def remaining_estimate(self, last_run_at):
135 return remaining(
136 self.maybe_make_aware(last_run_at), self.run_every,
137 self.maybe_make_aware(self.now()), self.relative,
138 )
140 def is_due(self, last_run_at):
141 """Return tuple of ``(is_due, next_time_to_check)``.
143 Notes:
144 - next time to check is in seconds.
146 - ``(True, 20)``, means the task should be run now, and the next
147 time to check is in 20 seconds.
149 - ``(False, 12.3)``, means the task is not due, but that the
150 scheduler should check again in 12.3 seconds.
152 The next time to check is used to save energy/CPU cycles,
153 it does not need to be accurate but will influence the precision
154 of your schedule. You must also keep in mind
155 the value of :setting:`beat_max_loop_interval`,
156 that decides the maximum number of seconds the scheduler can
157 sleep between re-checking the periodic task intervals. So if you
158 have a task that changes schedule at run-time then your next_run_at
159 check will decide how long it will take before a change to the
160 schedule takes effect. The max loop interval takes precedence
161 over the next check at value returned.
163 .. admonition:: Scheduler max interval variance
165 The default max loop interval may vary for different schedulers.
166 For the default scheduler the value is 5 minutes, but for example
167 the :pypi:`django-celery-beat` database scheduler the value
168 is 5 seconds.
169 """
170 last_run_at = self.maybe_make_aware(last_run_at)
171 rem_delta = self.remaining_estimate(last_run_at)
172 remaining_s = max(rem_delta.total_seconds(), 0)
173 if remaining_s == 0:
174 return schedstate(is_due=True, next=self.seconds)
175 return schedstate(is_due=False, next=remaining_s)
177 def __repr__(self):
178 return '<freq: {0.human_seconds}>'.format(self)
180 def __eq__(self, other):
181 if isinstance(other, schedule):
182 return self.run_every == other.run_every
183 return self.run_every == other
185 def __ne__(self, other):
186 return not self.__eq__(other)
188 def __reduce__(self):
189 return self.__class__, (self.run_every, self.relative, self.nowfun)
191 @property
192 def seconds(self):
193 return max(self.run_every.total_seconds(), 0)
195 @property
196 def human_seconds(self):
197 return humanize_seconds(self.seconds)
200class crontab_parser(object):
201 """Parser for Crontab expressions.
203 Any expression of the form 'groups'
204 (see BNF grammar below) is accepted and expanded to a set of numbers.
205 These numbers represent the units of time that the Crontab needs to
206 run on:
208 .. code-block:: bnf
210 digit :: '0'..'9'
211 dow :: 'a'..'z'
212 number :: digit+ | dow+
213 steps :: number
214 range :: number ( '-' number ) ?
215 numspec :: '*' | range
216 expr :: numspec ( '/' steps ) ?
217 groups :: expr ( ',' expr ) *
219 The parser is a general purpose one, useful for parsing hours, minutes and
220 day of week expressions. Example usage:
222 .. code-block:: pycon
224 >>> minutes = crontab_parser(60).parse('*/15')
225 [0, 15, 30, 45]
226 >>> hours = crontab_parser(24).parse('*/4')
227 [0, 4, 8, 12, 16, 20]
228 >>> day_of_week = crontab_parser(7).parse('*')
229 [0, 1, 2, 3, 4, 5, 6]
231 It can also parse day of month and month of year expressions if initialized
232 with a minimum of 1. Example usage:
234 .. code-block:: pycon
236 >>> days_of_month = crontab_parser(31, 1).parse('*/3')
237 [1, 4, 7, 10, 13, 16, 19, 22, 25, 28, 31]
238 >>> months_of_year = crontab_parser(12, 1).parse('*/2')
239 [1, 3, 5, 7, 9, 11]
240 >>> months_of_year = crontab_parser(12, 1).parse('2-12/2')
241 [2, 4, 6, 8, 10, 12]
243 The maximum possible expanded value returned is found by the formula:
245 :math:`max_ + min_ - 1`
246 """
248 ParseException = ParseException
250 _range = r'(\w+?)-(\w+)'
251 _steps = r'/(\w+)?'
252 _star = r'\*'
254 def __init__(self, max_=60, min_=0):
255 self.max_ = max_
256 self.min_ = min_
257 self.pats = (
258 (re.compile(self._range + self._steps), self._range_steps),
259 (re.compile(self._range), self._expand_range),
260 (re.compile(self._star + self._steps), self._star_steps),
261 (re.compile('^' + self._star + '$'), self._expand_star),
262 )
264 def parse(self, spec):
265 acc = set()
266 for part in spec.split(','):
267 if not part:
268 raise self.ParseException('empty part')
269 acc |= set(self._parse_part(part))
270 return acc
272 def _parse_part(self, part):
273 for regex, handler in self.pats:
274 m = regex.match(part)
275 if m:
276 return handler(m.groups())
277 return self._expand_range((part,))
279 def _expand_range(self, toks):
280 fr = self._expand_number(toks[0])
281 if len(toks) > 1:
282 to = self._expand_number(toks[1])
283 if to < fr: # Wrap around max_ if necessary
284 return (list(range(fr, self.min_ + self.max_)) +
285 list(range(self.min_, to + 1)))
286 return list(range(fr, to + 1))
287 return [fr]
289 def _range_steps(self, toks):
290 if len(toks) != 3 or not toks[2]:
291 raise self.ParseException('empty filter')
292 return self._expand_range(toks[:2])[::int(toks[2])]
294 def _star_steps(self, toks):
295 if not toks or not toks[0]:
296 raise self.ParseException('empty filter')
297 return self._expand_star()[::int(toks[0])]
299 def _expand_star(self, *args):
300 return list(range(self.min_, self.max_ + self.min_))
302 def _expand_number(self, s):
303 if isinstance(s, string_t) and s[0] == '-':
304 raise self.ParseException('negative numbers not supported')
305 try:
306 i = int(s)
307 except ValueError:
308 try:
309 i = weekday(s)
310 except KeyError:
311 raise ValueError('Invalid weekday literal {0!r}.'.format(s))
313 max_val = self.min_ + self.max_ - 1
314 if i > max_val:
315 raise ValueError(
316 'Invalid end range: {0} > {1}.'.format(i, max_val))
317 if i < self.min_:
318 raise ValueError(
319 'Invalid beginning range: {0} < {1}.'.format(i, self.min_))
321 return i
324@python_2_unicode_compatible
325class crontab(BaseSchedule):
326 """Crontab schedule.
328 A Crontab can be used as the ``run_every`` value of a
329 periodic task entry to add :manpage:`crontab(5)`-like scheduling.
331 Like a :manpage:`cron(5)`-job, you can specify units of time of when
332 you'd like the task to execute. It's a reasonably complete
333 implementation of :command:`cron`'s features, so it should provide a fair
334 degree of scheduling needs.
336 You can specify a minute, an hour, a day of the week, a day of the
337 month, and/or a month in the year in any of the following formats:
339 .. attribute:: minute
341 - A (list of) integers from 0-59 that represent the minutes of
342 an hour of when execution should occur; or
343 - A string representing a Crontab pattern. This may get pretty
344 advanced, like ``minute='*/15'`` (for every quarter) or
345 ``minute='1,13,30-45,50-59/2'``.
347 .. attribute:: hour
349 - A (list of) integers from 0-23 that represent the hours of
350 a day of when execution should occur; or
351 - A string representing a Crontab pattern. This may get pretty
352 advanced, like ``hour='*/3'`` (for every three hours) or
353 ``hour='0,8-17/2'`` (at midnight, and every two hours during
354 office hours).
356 .. attribute:: day_of_week
358 - A (list of) integers from 0-6, where Sunday = 0 and Saturday =
359 6, that represent the days of a week that execution should
360 occur.
361 - A string representing a Crontab pattern. This may get pretty
362 advanced, like ``day_of_week='mon-fri'`` (for weekdays only).
363 (Beware that ``day_of_week='*/2'`` does not literally mean
364 'every two days', but 'every day that is divisible by two'!)
366 .. attribute:: day_of_month
368 - A (list of) integers from 1-31 that represents the days of the
369 month that execution should occur.
370 - A string representing a Crontab pattern. This may get pretty
371 advanced, such as ``day_of_month='2-30/2'`` (for every even
372 numbered day) or ``day_of_month='1-7,15-21'`` (for the first and
373 third weeks of the month).
375 .. attribute:: month_of_year
377 - A (list of) integers from 1-12 that represents the months of
378 the year during which execution can occur.
379 - A string representing a Crontab pattern. This may get pretty
380 advanced, such as ``month_of_year='*/3'`` (for the first month
381 of every quarter) or ``month_of_year='2-12/2'`` (for every even
382 numbered month).
384 .. attribute:: nowfun
386 Function returning the current date and time
387 (:class:`~datetime.datetime`).
389 .. attribute:: app
391 The Celery app instance.
393 It's important to realize that any day on which execution should
394 occur must be represented by entries in all three of the day and
395 month attributes. For example, if ``day_of_week`` is 0 and
396 ``day_of_month`` is every seventh day, only months that begin
397 on Sunday and are also in the ``month_of_year`` attribute will have
398 execution events. Or, ``day_of_week`` is 1 and ``day_of_month``
399 is '1-7,15-21' means every first and third Monday of every month
400 present in ``month_of_year``.
401 """
403 def __init__(self, minute='*', hour='*', day_of_week='*',
404 day_of_month='*', month_of_year='*', **kwargs):
405 self._orig_minute = cronfield(minute)
406 self._orig_hour = cronfield(hour)
407 self._orig_day_of_week = cronfield(day_of_week)
408 self._orig_day_of_month = cronfield(day_of_month)
409 self._orig_month_of_year = cronfield(month_of_year)
410 self._orig_kwargs = kwargs
411 self.hour = self._expand_cronspec(hour, 24)
412 self.minute = self._expand_cronspec(minute, 60)
413 self.day_of_week = self._expand_cronspec(day_of_week, 7)
414 self.day_of_month = self._expand_cronspec(day_of_month, 31, 1)
415 self.month_of_year = self._expand_cronspec(month_of_year, 12, 1)
416 super(crontab, self).__init__(**kwargs)
418 @staticmethod
419 def _expand_cronspec(cronspec, max_, min_=0):
420 """Expand cron specification.
422 Takes the given cronspec argument in one of the forms:
424 .. code-block:: text
426 int (like 7)
427 str (like '3-5,*/15', '*', or 'monday')
428 set (like {0,15,30,45}
429 list (like [8-17])
431 And convert it to an (expanded) set representing all time unit
432 values on which the Crontab triggers. Only in case of the base
433 type being :class:`str`, parsing occurs. (It's fast and
434 happens only once for each Crontab instance, so there's no
435 significant performance overhead involved.)
437 For the other base types, merely Python type conversions happen.
439 The argument ``max_`` is needed to determine the expansion of
440 ``*`` and ranges. The argument ``min_`` is needed to determine
441 the expansion of ``*`` and ranges for 1-based cronspecs, such as
442 day of month or month of year. The default is sufficient for minute,
443 hour, and day of week.
444 """
445 if isinstance(cronspec, numbers.Integral):
446 result = {cronspec}
447 elif isinstance(cronspec, string_t):
448 result = crontab_parser(max_, min_).parse(cronspec)
449 elif isinstance(cronspec, set):
450 result = cronspec
451 elif isinstance(cronspec, Iterable):
452 result = set(cronspec)
453 else:
454 raise TypeError(CRON_INVALID_TYPE.format(type=type(cronspec)))
456 # assure the result does not preceed the min or exceed the max
457 for number in result:
458 if number >= max_ + min_ or number < min_:
459 raise ValueError(CRON_PATTERN_INVALID.format(
460 min=min_, max=max_ - 1 + min_, value=number))
461 return result
463 def _delta_to_next(self, last_run_at, next_hour, next_minute):
464 """Find next delta.
466 Takes a :class:`~datetime.datetime` of last run, next minute and hour,
467 and returns a :class:`~celery.utils.time.ffwd` for the next
468 scheduled day and time.
470 Only called when ``day_of_month`` and/or ``month_of_year``
471 cronspec is specified to further limit scheduled task execution.
472 """
473 datedata = AttributeDict(year=last_run_at.year)
474 days_of_month = sorted(self.day_of_month)
475 months_of_year = sorted(self.month_of_year)
477 def day_out_of_range(year, month, day):
478 try:
479 datetime(year=year, month=month, day=day)
480 except ValueError:
481 return True
482 return False
484 def is_before_last_run(year, month, day):
485 return self.maybe_make_aware(datetime(year,
486 month,
487 day)) < last_run_at
489 def roll_over():
490 for _ in range(2000):
491 flag = (datedata.dom == len(days_of_month) or
492 day_out_of_range(datedata.year,
493 months_of_year[datedata.moy],
494 days_of_month[datedata.dom]) or
495 (is_before_last_run(datedata.year,
496 months_of_year[datedata.moy],
497 days_of_month[datedata.dom])))
499 if flag:
500 datedata.dom = 0
501 datedata.moy += 1
502 if datedata.moy == len(months_of_year):
503 datedata.moy = 0
504 datedata.year += 1
505 else:
506 break
507 else:
508 # Tried 2000 times, we're most likely in an infinite loop
509 raise RuntimeError('unable to rollover, '
510 'time specification is probably invalid')
512 if last_run_at.month in self.month_of_year:
513 datedata.dom = bisect(days_of_month, last_run_at.day)
514 datedata.moy = bisect_left(months_of_year, last_run_at.month)
515 else:
516 datedata.dom = 0
517 datedata.moy = bisect(months_of_year, last_run_at.month)
518 if datedata.moy == len(months_of_year):
519 datedata.moy = 0
520 roll_over()
522 while 1:
523 th = datetime(year=datedata.year,
524 month=months_of_year[datedata.moy],
525 day=days_of_month[datedata.dom])
526 if th.isoweekday() % 7 in self.day_of_week:
527 break
528 datedata.dom += 1
529 roll_over()
531 return ffwd(year=datedata.year,
532 month=months_of_year[datedata.moy],
533 day=days_of_month[datedata.dom],
534 hour=next_hour,
535 minute=next_minute,
536 second=0,
537 microsecond=0)
539 def __repr__(self):
540 return CRON_REPR.format(self)
542 def __reduce__(self):
543 return (self.__class__, (self._orig_minute,
544 self._orig_hour,
545 self._orig_day_of_week,
546 self._orig_day_of_month,
547 self._orig_month_of_year), self._orig_kwargs)
549 def __setstate__(self, state):
550 # Calling super's init because the kwargs aren't necessarily passed in
551 # the same form as they are stored by the superclass
552 super(crontab, self).__init__(**state)
554 def remaining_delta(self, last_run_at, tz=None, ffwd=ffwd):
555 # pylint: disable=redefined-outer-name
556 # caching global ffwd
557 tz = tz or self.tz
558 last_run_at = self.maybe_make_aware(last_run_at)
559 now = self.maybe_make_aware(self.now())
560 dow_num = last_run_at.isoweekday() % 7 # Sunday is day 0, not day 7
562 execute_this_date = (
563 last_run_at.month in self.month_of_year and
564 last_run_at.day in self.day_of_month and
565 dow_num in self.day_of_week
566 )
568 execute_this_hour = (
569 execute_this_date and
570 last_run_at.day == now.day and
571 last_run_at.month == now.month and
572 last_run_at.year == now.year and
573 last_run_at.hour in self.hour and
574 last_run_at.minute < max(self.minute)
575 )
577 if execute_this_hour:
578 next_minute = min(minute for minute in self.minute
579 if minute > last_run_at.minute)
580 delta = ffwd(minute=next_minute, second=0, microsecond=0)
581 else:
582 next_minute = min(self.minute)
583 execute_today = (execute_this_date and
584 last_run_at.hour < max(self.hour))
586 if execute_today:
587 next_hour = min(hour for hour in self.hour
588 if hour > last_run_at.hour)
589 delta = ffwd(hour=next_hour, minute=next_minute,
590 second=0, microsecond=0)
591 else:
592 next_hour = min(self.hour)
593 all_dom_moy = (self._orig_day_of_month == '*' and
594 self._orig_month_of_year == '*')
595 if all_dom_moy:
596 next_day = min([day for day in self.day_of_week
597 if day > dow_num] or self.day_of_week)
598 add_week = next_day == dow_num
600 delta = ffwd(
601 weeks=add_week and 1 or 0,
602 weekday=(next_day - 1) % 7,
603 hour=next_hour,
604 minute=next_minute,
605 second=0,
606 microsecond=0,
607 )
608 else:
609 delta = self._delta_to_next(last_run_at,
610 next_hour, next_minute)
611 return self.to_local(last_run_at), delta, self.to_local(now)
613 def remaining_estimate(self, last_run_at, ffwd=ffwd):
614 """Estimate of next run time.
616 Returns when the periodic task should run next as a
617 :class:`~datetime.timedelta`.
618 """
619 # pylint: disable=redefined-outer-name
620 # caching global ffwd
621 return remaining(*self.remaining_delta(last_run_at, ffwd=ffwd))
623 def is_due(self, last_run_at):
624 """Return tuple of ``(is_due, next_time_to_run)``.
626 Note:
627 Next time to run is in seconds.
629 SeeAlso:
630 :meth:`celery.schedules.schedule.is_due` for more information.
631 """
632 rem_delta = self.remaining_estimate(last_run_at)
633 rem = max(rem_delta.total_seconds(), 0)
634 due = rem == 0
635 if due:
636 rem_delta = self.remaining_estimate(self.now())
637 rem = max(rem_delta.total_seconds(), 0)
638 return schedstate(due, rem)
640 def __eq__(self, other):
641 if isinstance(other, crontab):
642 return (
643 other.month_of_year == self.month_of_year and
644 other.day_of_month == self.day_of_month and
645 other.day_of_week == self.day_of_week and
646 other.hour == self.hour and
647 other.minute == self.minute and
648 super(crontab, self).__eq__(other)
649 )
650 return NotImplemented
652 def __ne__(self, other):
653 res = self.__eq__(other)
654 if res is NotImplemented:
655 return True
656 return not res
659def maybe_schedule(s, relative=False, app=None):
660 """Return schedule from number, timedelta, or actual schedule."""
661 if s is not None:
662 if isinstance(s, numbers.Number):
663 s = timedelta(seconds=s)
664 if isinstance(s, timedelta):
665 return schedule(s, relative, app=app)
666 else:
667 s.app = app
668 return s
671@python_2_unicode_compatible
672class solar(BaseSchedule):
673 """Solar event.
675 A solar event can be used as the ``run_every`` value of a
676 periodic task entry to schedule based on certain solar events.
678 Notes:
680 Available event valus are:
682 - ``dawn_astronomical``
683 - ``dawn_nautical``
684 - ``dawn_civil``
685 - ``sunrise``
686 - ``solar_noon``
687 - ``sunset``
688 - ``dusk_civil``
689 - ``dusk_nautical``
690 - ``dusk_astronomical``
692 Arguments:
693 event (str): Solar event that triggers this task.
694 See note for available values.
695 lat (int): The latitude of the observer.
696 lon (int): The longitude of the observer.
697 nowfun (Callable): Function returning the current date and time
698 as a class:`~datetime.datetime`.
699 app (Celery): Celery app instance.
700 """
702 _all_events = {
703 'dawn_astronomical',
704 'dawn_nautical',
705 'dawn_civil',
706 'sunrise',
707 'solar_noon',
708 'sunset',
709 'dusk_civil',
710 'dusk_nautical',
711 'dusk_astronomical',
712 }
713 _horizons = {
714 'dawn_astronomical': '-18',
715 'dawn_nautical': '-12',
716 'dawn_civil': '-6',
717 'sunrise': '-0:34',
718 'solar_noon': '0',
719 'sunset': '-0:34',
720 'dusk_civil': '-6',
721 'dusk_nautical': '-12',
722 'dusk_astronomical': '18',
723 }
724 _methods = {
725 'dawn_astronomical': 'next_rising',
726 'dawn_nautical': 'next_rising',
727 'dawn_civil': 'next_rising',
728 'sunrise': 'next_rising',
729 'solar_noon': 'next_transit',
730 'sunset': 'next_setting',
731 'dusk_civil': 'next_setting',
732 'dusk_nautical': 'next_setting',
733 'dusk_astronomical': 'next_setting',
734 }
735 _use_center_l = {
736 'dawn_astronomical': True,
737 'dawn_nautical': True,
738 'dawn_civil': True,
739 'sunrise': False,
740 'solar_noon': False,
741 'sunset': False,
742 'dusk_civil': True,
743 'dusk_nautical': True,
744 'dusk_astronomical': True,
745 }
747 def __init__(self, event, lat, lon, **kwargs):
748 self.ephem = __import__('ephem')
749 self.event = event
750 self.lat = lat
751 self.lon = lon
752 super(solar, self).__init__(**kwargs)
754 if event not in self._all_events:
755 raise ValueError(SOLAR_INVALID_EVENT.format(
756 event=event, all_events=', '.join(sorted(self._all_events)),
757 ))
758 if lat < -90 or lat > 90:
759 raise ValueError(SOLAR_INVALID_LATITUDE.format(lat=lat))
760 if lon < -180 or lon > 180:
761 raise ValueError(SOLAR_INVALID_LONGITUDE.format(lon=lon))
763 cal = self.ephem.Observer()
764 cal.lat = str(lat)
765 cal.lon = str(lon)
766 cal.elev = 0
767 cal.horizon = self._horizons[event]
768 cal.pressure = 0
769 self.cal = cal
771 self.method = self._methods[event]
772 self.use_center = self._use_center_l[event]
774 def __reduce__(self):
775 return self.__class__, (self.event, self.lat, self.lon)
777 def __repr__(self):
778 return '<solar: {0} at latitude {1}, longitude: {2}>'.format(
779 self.event, self.lat, self.lon,
780 )
782 def remaining_estimate(self, last_run_at):
783 """Return estimate of next time to run.
785 Returns:
786 ~datetime.timedelta: when the periodic task should
787 run next, or if it shouldn't run today (e.g., the sun does
788 not rise today), returns the time when the next check
789 should take place.
790 """
791 last_run_at = self.maybe_make_aware(last_run_at)
792 last_run_at_utc = localize(last_run_at, timezone.utc)
793 self.cal.date = last_run_at_utc
794 try:
795 if self.use_center:
796 next_utc = getattr(self.cal, self.method)(
797 self.ephem.Sun(),
798 start=last_run_at_utc, use_center=self.use_center
799 )
800 else:
801 next_utc = getattr(self.cal, self.method)(
802 self.ephem.Sun(), start=last_run_at_utc
803 )
805 except self.ephem.CircumpolarError: # pragma: no cover
806 # Sun won't rise/set today. Check again tomorrow
807 # (specifically, after the next anti-transit).
808 next_utc = (
809 self.cal.next_antitransit(self.ephem.Sun()) +
810 timedelta(minutes=1)
811 )
812 next = self.maybe_make_aware(next_utc.datetime())
813 now = self.maybe_make_aware(self.now())
814 delta = next - now
815 return delta
817 def is_due(self, last_run_at):
818 """Return tuple of ``(is_due, next_time_to_run)``.
820 Note:
821 next time to run is in seconds.
823 See Also:
824 :meth:`celery.schedules.schedule.is_due` for more information.
825 """
826 rem_delta = self.remaining_estimate(last_run_at)
827 rem = max(rem_delta.total_seconds(), 0)
828 due = rem == 0
829 if due:
830 rem_delta = self.remaining_estimate(self.now())
831 rem = max(rem_delta.total_seconds(), 0)
832 return schedstate(due, rem)
834 def __eq__(self, other):
835 if isinstance(other, solar):
836 return (
837 other.event == self.event and
838 other.lat == self.lat and
839 other.lon == self.lon
840 )
841 return NotImplemented
843 def __ne__(self, other):
844 res = self.__eq__(other)
845 if res is NotImplemented:
846 return True
847 return not res