|
@@ -22,12 +22,12 @@ from .five import range, string_t
|
|
|
from .utils import is_iterable
|
|
|
from .utils.timeutils import (
|
|
|
weekday, maybe_timedelta, remaining, humanize_seconds,
|
|
|
- timezone, maybe_make_aware, ffwd
|
|
|
+ timezone, maybe_make_aware, ffwd, localize
|
|
|
)
|
|
|
from .datastructures import AttributeDict
|
|
|
|
|
|
__all__ = ['ParseException', 'schedule', 'crontab', 'crontab_parser',
|
|
|
- 'maybe_schedule']
|
|
|
+ 'maybe_schedule', 'solar']
|
|
|
|
|
|
schedstate = namedtuple('schedstate', ('is_due', 'next'))
|
|
|
|
|
@@ -591,3 +591,148 @@ def maybe_schedule(s, relative=False, app=None):
|
|
|
else:
|
|
|
s.app = app
|
|
|
return s
|
|
|
+
|
|
|
+SOLAR_INVALID_LATITUDE = """\
|
|
|
+Argument latitude {lat} is invalid, must be between -90 and 90.\
|
|
|
+"""
|
|
|
+
|
|
|
+SOLAR_INVALID_LONGITUDE = """\
|
|
|
+Argument longitude {lon} is invalid, must be between -180 and 180.\
|
|
|
+"""
|
|
|
+
|
|
|
+SOLAR_INVALID_EVENT = """\
|
|
|
+Argument event \"{event}\" is invalid, must be one of {all_events}.\
|
|
|
+"""
|
|
|
+
|
|
|
+class solar(schedule):
|
|
|
+ """A solar event can be used as the `run_every` value of a
|
|
|
+ :class:`PeriodicTask` to schedule based on certain solar events.
|
|
|
+
|
|
|
+ :param event: Solar event that triggers this task. Available
|
|
|
+ values are: dawn_astronomical, dawn_nautical, dawn_civil,
|
|
|
+ sunrise, solar_noon, sunset, dusk_civil, dusk_nautical,
|
|
|
+ dusk_astronomical
|
|
|
+ :param lat: The latitude of the observer.
|
|
|
+ :param lon: The longitude of the observer.
|
|
|
+ :param nowfun: Function returning the current date and time
|
|
|
+ (class:`~datetime.datetime`).
|
|
|
+ :param app: Celery app instance.
|
|
|
+ """
|
|
|
+
|
|
|
+
|
|
|
+ _all_events = ['dawn_astronomical',
|
|
|
+ 'dawn_nautical',
|
|
|
+ 'dawn_civil',
|
|
|
+ 'sunrise',
|
|
|
+ 'solar_noon',
|
|
|
+ 'sunset',
|
|
|
+ 'dusk_civil',
|
|
|
+ 'dusk_nautical',
|
|
|
+ 'dusk_astronomical']
|
|
|
+ _horizons = {'dawn_astronomical': '-18',
|
|
|
+ 'dawn_nautical': '-12',
|
|
|
+ 'dawn_civil': '-6',
|
|
|
+ 'sunrise': '-0:34',
|
|
|
+ 'solar_noon': '0',
|
|
|
+ 'sunset': '-0:34',
|
|
|
+ 'dusk_civil': '-6',
|
|
|
+ 'dusk_nautical': '-12',
|
|
|
+ 'dusk_astronomical': '18'}
|
|
|
+ _methods = {'dawn_astronomical': 'next_rising',
|
|
|
+ 'dawn_nautical': 'next_rising',
|
|
|
+ 'dawn_civil': 'next_rising',
|
|
|
+ 'sunrise': 'next_rising',
|
|
|
+ 'solar_noon': 'next_transit',
|
|
|
+ 'sunset': 'next_setting',
|
|
|
+ 'dusk_civil': 'next_setting',
|
|
|
+ 'dusk_nautical': 'next_setting',
|
|
|
+ 'dusk_astronomical': 'next_setting'}
|
|
|
+ _use_center_l = {'dawn_astronomical': True,
|
|
|
+ 'dawn_nautical': True,
|
|
|
+ 'dawn_civil': True,
|
|
|
+ 'sunrise': False,
|
|
|
+ 'solar_noon': True,
|
|
|
+ 'sunset': False,
|
|
|
+ 'dusk_civil': True,
|
|
|
+ 'dusk_nautical': True,
|
|
|
+ 'dusk_astronomical': True}
|
|
|
+
|
|
|
+ def __init__(self, event, lat, lon, nowfun=None, app=None):
|
|
|
+ self.ephem = __import__('ephem')
|
|
|
+ self.event = event
|
|
|
+ self.lat = lat
|
|
|
+ self.lon = lon
|
|
|
+ self.nowfun = nowfun
|
|
|
+ self._app = app
|
|
|
+
|
|
|
+ if event not in self._all_events:
|
|
|
+ raise ValueError(SOLAR_INVALID_EVENT.format(event=event, all_events=', '.join(self._all_events)))
|
|
|
+ if lat < -90 or lat > 90:
|
|
|
+ raise ValueError(SOLAR_INVALID_LATITUDE.format(lat=lat))
|
|
|
+ if lon < -180 or lon > 180:
|
|
|
+ raise ValueError(SOLAR_INVALID_LONGITUDE.format(lon=lon))
|
|
|
+
|
|
|
+ cal = self.ephem.Observer()
|
|
|
+ cal.lat = str(lat)
|
|
|
+ cal.lon = str(lon)
|
|
|
+ cal.elev = 0
|
|
|
+ cal.horizon = self._horizons[event]
|
|
|
+ cal.pressure = 0
|
|
|
+ self.cal = cal
|
|
|
+
|
|
|
+ self.method = self._methods[event]
|
|
|
+ self.use_center = self._use_center_l[event]
|
|
|
+
|
|
|
+ def now(self):
|
|
|
+ return (self.nowfun or self.app.now)()
|
|
|
+
|
|
|
+ def __reduce__(self):
|
|
|
+ return (self.__class__, (self.event,
|
|
|
+ self.lat,
|
|
|
+ self.lon), None)
|
|
|
+
|
|
|
+ def __repr__(self):
|
|
|
+ return "<solar: " + self.event + " at latitude " + str(self.lat) + ", longitude " + str(self.lon) + ">"
|
|
|
+
|
|
|
+ def remaining_estimate(self, last_run_at):
|
|
|
+ """Returns when the periodic task should run next as a timedelta,
|
|
|
+ or if it shouldn't run today (e.g. the sun does not rise today),
|
|
|
+ returns the time when the next check should take place."""
|
|
|
+ last_run_at = self.maybe_make_aware(last_run_at)
|
|
|
+ last_run_at_utc = localize(last_run_at, timezone.utc)
|
|
|
+ self.cal.date = last_run_at_utc
|
|
|
+ try:
|
|
|
+ next_utc = getattr(self.cal, self.method)(self.ephem.Sun(), start=last_run_at_utc, use_center=self.use_center)
|
|
|
+ except self.ephem.CircumpolarError:
|
|
|
+ """Sun will not rise/set today. Check again tomorrow
|
|
|
+ (specifically, after the next anti-transit)."""
|
|
|
+ next_utc = self.cal.next_antitransit(self.ephem.Sun()) + timedelta(minutes=1)
|
|
|
+ next = self.maybe_make_aware(next_utc.datetime())
|
|
|
+ now = self.maybe_make_aware(self.now())
|
|
|
+ delta = next - now
|
|
|
+ return delta
|
|
|
+
|
|
|
+ def is_due(self, last_run_at):
|
|
|
+ """Returns tuple of two items `(is_due, next_time_to_run)`,
|
|
|
+ where next time to run is in seconds.
|
|
|
+
|
|
|
+ See :meth:`celery.schedules.schedule.is_due` for more information.
|
|
|
+
|
|
|
+ """
|
|
|
+ rem_delta = self.remaining_estimate(last_run_at)
|
|
|
+ rem = max(rem_delta.total_seconds(), 0)
|
|
|
+ due = rem == 0
|
|
|
+ if due:
|
|
|
+ rem_delta = self.remaining_estimate(self.now())
|
|
|
+ rem = max(rem_delta.total_seconds(), 0)
|
|
|
+ return schedstate(due, rem)
|
|
|
+
|
|
|
+ def __eq__(self, other):
|
|
|
+ if isinstance(other, solar):
|
|
|
+ return (other.event == self.event and
|
|
|
+ other.lat == self.lat and
|
|
|
+ other.lon == self.lon)
|
|
|
+ return NotImplemented
|
|
|
+
|
|
|
+ def __ne__(self, other):
|
|
|
+ return not self.__eq__(other)
|