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