|  | @@ -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)
 |