Hide keyboard shortcuts

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 

4 

5import numbers 

6import re 

7from bisect import bisect, bisect_left 

8from collections import namedtuple 

9from datetime import datetime, timedelta 

10 

11from kombu.utils.objects import cached_property 

12 

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) 

18 

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 

24 

25 

26__all__ = ( 

27 'ParseException', 'schedule', 'crontab', 'crontab_parser', 

28 'maybe_schedule', 'solar', 

29) 

30 

31schedstate = namedtuple('schedstate', ('is_due', 'next')) 

32 

33CRON_PATTERN_INVALID = """\ 

34Invalid crontab pattern. Valid range is {min}-{max}. \ 

35'{value}' was found.\ 

36""" 

37 

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""" 

42 

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""" 

47 

48SOLAR_INVALID_LATITUDE = """\ 

49Argument latitude {lat} is invalid, must be between -90 and 90.\ 

50""" 

51 

52SOLAR_INVALID_LONGITUDE = """\ 

53Argument longitude {lon} is invalid, must be between -180 and 180.\ 

54""" 

55 

56SOLAR_INVALID_EVENT = """\ 

57Argument event "{event}" is invalid, must be one of {all_events}.\ 

58""" 

59 

60 

61def cronfield(s): 

62 return '*' if s is None else s 

63 

64 

65class ParseException(Exception): 

66 """Raised by :class:`crontab_parser` when the input can't be parsed.""" 

67 

68 

69class BaseSchedule(object): 

70 

71 def __init__(self, nowfun=None, app=None): 

72 self.nowfun = nowfun 

73 self._app = app 

74 

75 def now(self): 

76 return (self.nowfun or self.app.now)() 

77 

78 def remaining_estimate(self, last_run_at): 

79 raise NotImplementedError() 

80 

81 def is_due(self, last_run_at): 

82 raise NotImplementedError() 

83 

84 def maybe_make_aware(self, dt): 

85 return maybe_make_aware(dt, self.tz) 

86 

87 @property 

88 def app(self): 

89 return self._app or current_app._get_current_object() 

90 

91 @app.setter # noqa 

92 def app(self, app): 

93 self._app = app 

94 

95 @cached_property 

96 def tz(self): 

97 return self.app.timezone 

98 

99 @cached_property 

100 def utc_enabled(self): 

101 return self.app.conf.enable_utc 

102 

103 def to_local(self, dt): 

104 if not self.utc_enabled: 

105 return timezone.to_local_fallback(dt) 

106 return dt 

107 

108 def __eq__(self, other): 

109 if isinstance(other, BaseSchedule): 

110 return other.nowfun == self.nowfun 

111 return NotImplemented 

112 

113 

114@python_2_unicode_compatible 

115class schedule(BaseSchedule): 

116 """Schedule for periodic task. 

117 

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 """ 

126 

127 relative = False 

128 

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) 

133 

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 ) 

139 

140 def is_due(self, last_run_at): 

141 """Return tuple of ``(is_due, next_time_to_check)``. 

142 

143 Notes: 

144 - next time to check is in seconds. 

145 

146 - ``(True, 20)``, means the task should be run now, and the next 

147 time to check is in 20 seconds. 

148 

149 - ``(False, 12.3)``, means the task is not due, but that the 

150 scheduler should check again in 12.3 seconds. 

151 

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. 

162 

163 .. admonition:: Scheduler max interval variance 

164 

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) 

176 

177 def __repr__(self): 

178 return '<freq: {0.human_seconds}>'.format(self) 

179 

180 def __eq__(self, other): 

181 if isinstance(other, schedule): 

182 return self.run_every == other.run_every 

183 return self.run_every == other 

184 

185 def __ne__(self, other): 

186 return not self.__eq__(other) 

187 

188 def __reduce__(self): 

189 return self.__class__, (self.run_every, self.relative, self.nowfun) 

190 

191 @property 

192 def seconds(self): 

193 return max(self.run_every.total_seconds(), 0) 

194 

195 @property 

196 def human_seconds(self): 

197 return humanize_seconds(self.seconds) 

198 

199 

200class crontab_parser(object): 

201 """Parser for Crontab expressions. 

202 

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: 

207 

208 .. code-block:: bnf 

209 

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

218 

219 The parser is a general purpose one, useful for parsing hours, minutes and 

220 day of week expressions. Example usage: 

221 

222 .. code-block:: pycon 

223 

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] 

230 

231 It can also parse day of month and month of year expressions if initialized 

232 with a minimum of 1. Example usage: 

233 

234 .. code-block:: pycon 

235 

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] 

242 

243 The maximum possible expanded value returned is found by the formula: 

244 

245 :math:`max_ + min_ - 1` 

246 """ 

247 

248 ParseException = ParseException 

249 

250 _range = r'(\w+?)-(\w+)' 

251 _steps = r'/(\w+)?' 

252 _star = r'\*' 

253 

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 ) 

263 

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 

271 

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

278 

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] 

288 

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

293 

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

298 

299 def _expand_star(self, *args): 

300 return list(range(self.min_, self.max_ + self.min_)) 

301 

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

312 

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

320 

321 return i 

322 

323 

324@python_2_unicode_compatible 

325class crontab(BaseSchedule): 

326 """Crontab schedule. 

327 

328 A Crontab can be used as the ``run_every`` value of a 

329 periodic task entry to add :manpage:`crontab(5)`-like scheduling. 

330 

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. 

335 

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: 

338 

339 .. attribute:: minute 

340 

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'``. 

346 

347 .. attribute:: hour 

348 

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

355 

356 .. attribute:: day_of_week 

357 

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'!) 

365 

366 .. attribute:: day_of_month 

367 

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

374 

375 .. attribute:: month_of_year 

376 

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

383 

384 .. attribute:: nowfun 

385 

386 Function returning the current date and time 

387 (:class:`~datetime.datetime`). 

388 

389 .. attribute:: app 

390 

391 The Celery app instance. 

392 

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 """ 

402 

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) 

417 

418 @staticmethod 

419 def _expand_cronspec(cronspec, max_, min_=0): 

420 """Expand cron specification. 

421 

422 Takes the given cronspec argument in one of the forms: 

423 

424 .. code-block:: text 

425 

426 int (like 7) 

427 str (like '3-5,*/15', '*', or 'monday') 

428 set (like {0,15,30,45} 

429 list (like [8-17]) 

430 

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

436 

437 For the other base types, merely Python type conversions happen. 

438 

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

455 

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 

462 

463 def _delta_to_next(self, last_run_at, next_hour, next_minute): 

464 """Find next delta. 

465 

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. 

469 

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) 

476 

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 

483 

484 def is_before_last_run(year, month, day): 

485 return self.maybe_make_aware(datetime(year, 

486 month, 

487 day)) < last_run_at 

488 

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

498 

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

511 

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

521 

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

530 

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) 

538 

539 def __repr__(self): 

540 return CRON_REPR.format(self) 

541 

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) 

548 

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) 

553 

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 

561 

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 ) 

567 

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 ) 

576 

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

585 

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 

599 

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) 

612 

613 def remaining_estimate(self, last_run_at, ffwd=ffwd): 

614 """Estimate of next run time. 

615 

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

622 

623 def is_due(self, last_run_at): 

624 """Return tuple of ``(is_due, next_time_to_run)``. 

625 

626 Note: 

627 Next time to run is in seconds. 

628 

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) 

639 

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 

651 

652 def __ne__(self, other): 

653 res = self.__eq__(other) 

654 if res is NotImplemented: 

655 return True 

656 return not res 

657 

658 

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 

669 

670 

671@python_2_unicode_compatible 

672class solar(BaseSchedule): 

673 """Solar event. 

674 

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. 

677 

678 Notes: 

679 

680 Available event valus are: 

681 

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`` 

691 

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 """ 

701 

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 } 

746 

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) 

753 

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

762 

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 

770 

771 self.method = self._methods[event] 

772 self.use_center = self._use_center_l[event] 

773 

774 def __reduce__(self): 

775 return self.__class__, (self.event, self.lat, self.lon) 

776 

777 def __repr__(self): 

778 return '<solar: {0} at latitude {1}, longitude: {2}>'.format( 

779 self.event, self.lat, self.lon, 

780 ) 

781 

782 def remaining_estimate(self, last_run_at): 

783 """Return estimate of next time to run. 

784 

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 ) 

804 

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 

816 

817 def is_due(self, last_run_at): 

818 """Return tuple of ``(is_due, next_time_to_run)``. 

819 

820 Note: 

821 next time to run is in seconds. 

822 

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) 

833 

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 

842 

843 def __ne__(self, other): 

844 res = self.__eq__(other) 

845 if res is NotImplemented: 

846 return True 

847 return not res