Telegram User Bot that plays uno with mau_mau_bot
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

530 lines
16 KiB

  1. """
  2. Modified to work with asyncio by Jerry Xiao <jerry at mail.jerryxiao.cc>
  3. Don't tell me there's aiocron. I don't know.
  4. Python job scheduling for humans.
  5. github.com/dbader/schedule
  6. An in-process scheduler for periodic jobs that uses the builder pattern
  7. for configuration. Schedule lets you run Python functions (or any other
  8. callable) periodically at pre-determined intervals using a simple,
  9. human-friendly syntax.
  10. Inspired by Addam Wiggins' article "Rethinking Cron" [1] and the
  11. "clockwork" Ruby module [2][3].
  12. Features:
  13. - A simple to use API for scheduling jobs.
  14. - Very lightweight and no external dependencies.
  15. - Excellent test coverage.
  16. - Tested on Python 2.7, 3.5 to 3.7
  17. Usage:
  18. >>> import schedule_async
  19. >>> import time
  20. >>> def job(message='stuff'):
  21. >>> print("I'm working on:", message)
  22. >>> schedule.every(10).minutes.do(job)
  23. >>> schedule.every(5).to(10).days.do(job)
  24. >>> schedule.every().hour.do(job, message='things')
  25. >>> schedule.every().day.at("10:30").do(job)
  26. >>> while True:
  27. >>> await schedule.run_pending()
  28. >>> await asyncio.sleep(1)
  29. [1] https://adam.herokuapp.com/past/2010/4/13/rethinking_cron/
  30. [2] https://github.com/Rykian/clockwork
  31. [3] https://adam.herokuapp.com/past/2010/6/30/replace_cron_with_clockwork/
  32. """
  33. import collections
  34. import datetime
  35. import functools
  36. import logging
  37. import random
  38. import time
  39. import asyncio
  40. logger = logging.getLogger('schedule')
  41. class CancelJob(object):
  42. """
  43. Can be returned from a job to unschedule itself.
  44. """
  45. pass
  46. class Scheduler(object):
  47. """
  48. Objects instantiated by the :class:`Scheduler <Scheduler>` are
  49. factories to create jobs, keep record of scheduled jobs and
  50. handle their execution.
  51. """
  52. def __init__(self):
  53. self.jobs = []
  54. async def run_pending(self):
  55. """
  56. Run all jobs that are scheduled to run.
  57. Please note that it is *intended behavior that run_pending()
  58. does not run missed jobs*. For example, if you've registered a job
  59. that should run every minute and you only call run_pending()
  60. in one hour increments then your job won't be run 60 times in
  61. between but only once.
  62. """
  63. runnable_jobs = (job for job in self.jobs if job.should_run)
  64. for job in sorted(runnable_jobs):
  65. await self._run_job(job)
  66. async def run_all(self, delay_seconds=0):
  67. """
  68. Run all jobs regardless if they are scheduled to run or not.
  69. A delay of `delay` seconds is added between each job. This helps
  70. distribute system load generated by the jobs more evenly
  71. over time.
  72. :param delay_seconds: A delay added between every executed job
  73. """
  74. logger.info('Running *all* %i jobs with %is delay inbetween',
  75. len(self.jobs), delay_seconds)
  76. for job in self.jobs[:]:
  77. await self._run_job(job)
  78. await asyncio.sleep(delay_seconds)
  79. def clear(self, tag=None):
  80. """
  81. Deletes scheduled jobs marked with the given tag, or all jobs
  82. if tag is omitted.
  83. :param tag: An identifier used to identify a subset of
  84. jobs to delete
  85. """
  86. if tag is None:
  87. del self.jobs[:]
  88. else:
  89. self.jobs[:] = (job for job in self.jobs if tag not in job.tags)
  90. def cancel_job(self, job):
  91. """
  92. Delete a scheduled job.
  93. :param job: The job to be unscheduled
  94. """
  95. try:
  96. self.jobs.remove(job)
  97. except ValueError:
  98. pass
  99. def every(self, interval=1):
  100. """
  101. Schedule a new periodic job.
  102. :param interval: A quantity of a certain time unit
  103. :return: An unconfigured :class:`Job <Job>`
  104. """
  105. job = Job(interval, self)
  106. return job
  107. async def _run_job(self, job):
  108. ret = await job.run()
  109. if isinstance(ret, CancelJob) or ret is CancelJob:
  110. self.cancel_job(job)
  111. @property
  112. def next_run(self):
  113. """
  114. Datetime when the next job should run.
  115. :return: A :class:`~datetime.datetime` object
  116. """
  117. if not self.jobs:
  118. return None
  119. return min(self.jobs).next_run
  120. @property
  121. def idle_seconds(self):
  122. """
  123. :return: Number of seconds until
  124. :meth:`next_run <Scheduler.next_run>`.
  125. """
  126. return (self.next_run - datetime.datetime.now()).total_seconds()
  127. class Job(object):
  128. """
  129. A periodic job as used by :class:`Scheduler`.
  130. :param interval: A quantity of a certain time unit
  131. :param scheduler: The :class:`Scheduler <Scheduler>` instance that
  132. this job will register itself with once it has
  133. been fully configured in :meth:`Job.do()`.
  134. Every job runs at a given fixed time interval that is defined by:
  135. * a :meth:`time unit <Job.second>`
  136. * a quantity of `time units` defined by `interval`
  137. A job is usually created and returned by :meth:`Scheduler.every`
  138. method, which also defines its `interval`.
  139. """
  140. def __init__(self, interval, scheduler=None):
  141. self.interval = interval # pause interval * unit between runs
  142. self.latest = None # upper limit to the interval
  143. self.job_func = None # the job job_func to run
  144. self.unit = None # time units, e.g. 'minutes', 'hours', ...
  145. self.at_time = None # optional time at which this job runs
  146. self.last_run = None # datetime of the last run
  147. self.next_run = None # datetime of the next run
  148. self.period = None # timedelta between runs, only valid for
  149. self.start_day = None # Specific day of the week to start on
  150. self.tags = set() # unique set of tags for the job
  151. self.scheduler = scheduler # scheduler to register with
  152. def __lt__(self, other):
  153. """
  154. PeriodicJobs are sortable based on the scheduled time they
  155. run next.
  156. """
  157. return self.next_run < other.next_run
  158. def __repr__(self):
  159. def format_time(t):
  160. return t.strftime('%Y-%m-%d %H:%M:%S') if t else '[never]'
  161. timestats = '(last run: %s, next run: %s)' % (
  162. format_time(self.last_run), format_time(self.next_run))
  163. if hasattr(self.job_func, '__name__'):
  164. job_func_name = self.job_func.__name__
  165. else:
  166. job_func_name = repr(self.job_func)
  167. args = [repr(x) for x in self.job_func.args]
  168. kwargs = ['%s=%s' % (k, repr(v))
  169. for k, v in self.job_func.keywords.items()]
  170. call_repr = job_func_name + '(' + ', '.join(args + kwargs) + ')'
  171. if self.at_time is not None:
  172. return 'Every %s %s at %s do %s %s' % (
  173. self.interval,
  174. self.unit[:-1] if self.interval == 1 else self.unit,
  175. self.at_time, call_repr, timestats)
  176. else:
  177. fmt = (
  178. 'Every %(interval)s ' +
  179. ('to %(latest)s ' if self.latest is not None else '') +
  180. '%(unit)s do %(call_repr)s %(timestats)s'
  181. )
  182. return fmt % dict(
  183. interval=self.interval,
  184. latest=self.latest,
  185. unit=(self.unit[:-1] if self.interval == 1 else self.unit),
  186. call_repr=call_repr,
  187. timestats=timestats
  188. )
  189. @property
  190. def second(self):
  191. assert self.interval == 1, 'Use seconds instead of second'
  192. return self.seconds
  193. @property
  194. def seconds(self):
  195. self.unit = 'seconds'
  196. return self
  197. @property
  198. def minute(self):
  199. assert self.interval == 1, 'Use minutes instead of minute'
  200. return self.minutes
  201. @property
  202. def minutes(self):
  203. self.unit = 'minutes'
  204. return self
  205. @property
  206. def hour(self):
  207. assert self.interval == 1, 'Use hours instead of hour'
  208. return self.hours
  209. @property
  210. def hours(self):
  211. self.unit = 'hours'
  212. return self
  213. @property
  214. def day(self):
  215. assert self.interval == 1, 'Use days instead of day'
  216. return self.days
  217. @property
  218. def days(self):
  219. self.unit = 'days'
  220. return self
  221. @property
  222. def week(self):
  223. assert self.interval == 1, 'Use weeks instead of week'
  224. return self.weeks
  225. @property
  226. def weeks(self):
  227. self.unit = 'weeks'
  228. return self
  229. @property
  230. def monday(self):
  231. assert self.interval == 1, 'Use mondays instead of monday'
  232. self.start_day = 'monday'
  233. return self.weeks
  234. @property
  235. def tuesday(self):
  236. assert self.interval == 1, 'Use tuesdays instead of tuesday'
  237. self.start_day = 'tuesday'
  238. return self.weeks
  239. @property
  240. def wednesday(self):
  241. assert self.interval == 1, 'Use wedesdays instead of wednesday'
  242. self.start_day = 'wednesday'
  243. return self.weeks
  244. @property
  245. def thursday(self):
  246. assert self.interval == 1, 'Use thursdays instead of thursday'
  247. self.start_day = 'thursday'
  248. return self.weeks
  249. @property
  250. def friday(self):
  251. assert self.interval == 1, 'Use fridays instead of friday'
  252. self.start_day = 'friday'
  253. return self.weeks
  254. @property
  255. def saturday(self):
  256. assert self.interval == 1, 'Use saturdays instead of saturday'
  257. self.start_day = 'saturday'
  258. return self.weeks
  259. @property
  260. def sunday(self):
  261. assert self.interval == 1, 'Use sundays instead of sunday'
  262. self.start_day = 'sunday'
  263. return self.weeks
  264. def tag(self, *tags):
  265. """
  266. Tags the job with one or more unique indentifiers.
  267. Tags must be hashable. Duplicate tags are discarded.
  268. :param tags: A unique list of ``Hashable`` tags.
  269. :return: The invoked job instance
  270. """
  271. if not all(isinstance(tag, collections.Hashable) for tag in tags):
  272. raise TypeError('Tags must be hashable')
  273. self.tags.update(tags)
  274. return self
  275. def at(self, time_str):
  276. """
  277. Schedule the job every day at a specific time.
  278. Calling this is only valid for jobs scheduled to run
  279. every N day(s).
  280. :param time_str: A string in `XX:YY` format.
  281. :return: The invoked job instance
  282. """
  283. assert self.unit in ('days', 'hours') or self.start_day
  284. hour, minute = time_str.split(':')
  285. minute = int(minute)
  286. if self.unit == 'days' or self.start_day:
  287. hour = int(hour)
  288. assert 0 <= hour <= 23
  289. elif self.unit == 'hours':
  290. hour = 0
  291. assert 0 <= minute <= 59
  292. self.at_time = datetime.time(hour, minute)
  293. return self
  294. def to(self, latest):
  295. """
  296. Schedule the job to run at an irregular (randomized) interval.
  297. The job's interval will randomly vary from the value given
  298. to `every` to `latest`. The range defined is inclusive on
  299. both ends. For example, `every(A).to(B).seconds` executes
  300. the job function every N seconds such that A <= N <= B.
  301. :param latest: Maximum interval between randomized job runs
  302. :return: The invoked job instance
  303. """
  304. self.latest = latest
  305. return self
  306. def do(self, job_func, *args, **kwargs):
  307. """
  308. Specifies the job_func that should be called every time the
  309. job runs.
  310. Any additional arguments are passed on to job_func when
  311. the job runs.
  312. :param job_func: The function to be scheduled
  313. :return: The invoked job instance
  314. """
  315. self.job_func = functools.partial(job_func, *args, **kwargs)
  316. try:
  317. functools.update_wrapper(self.job_func, job_func)
  318. except AttributeError:
  319. # job_funcs already wrapped by functools.partial won't have
  320. # __name__, __module__ or __doc__ and the update_wrapper()
  321. # call will fail.
  322. pass
  323. self._schedule_next_run()
  324. self.scheduler.jobs.append(self)
  325. return self
  326. @property
  327. def should_run(self):
  328. """
  329. :return: ``True`` if the job should be run now.
  330. """
  331. return datetime.datetime.now() >= self.next_run
  332. async def run(self):
  333. """
  334. Run the job and immediately reschedule it.
  335. :return: The return value returned by the `job_func`
  336. """
  337. logger.info('Running job %s', self)
  338. ret = await self.job_func()
  339. self.last_run = datetime.datetime.now()
  340. self._schedule_next_run()
  341. return ret
  342. def _schedule_next_run(self):
  343. """
  344. Compute the instant when this job should run next.
  345. """
  346. assert self.unit in ('seconds', 'minutes', 'hours', 'days', 'weeks')
  347. if self.latest is not None:
  348. assert self.latest >= self.interval
  349. interval = random.randint(self.interval, self.latest)
  350. else:
  351. interval = self.interval
  352. self.period = datetime.timedelta(**{self.unit: interval})
  353. self.next_run = datetime.datetime.now() + self.period
  354. if self.start_day is not None:
  355. assert self.unit == 'weeks'
  356. weekdays = (
  357. 'monday',
  358. 'tuesday',
  359. 'wednesday',
  360. 'thursday',
  361. 'friday',
  362. 'saturday',
  363. 'sunday'
  364. )
  365. assert self.start_day in weekdays
  366. weekday = weekdays.index(self.start_day)
  367. days_ahead = weekday - self.next_run.weekday()
  368. if days_ahead <= 0: # Target day already happened this week
  369. days_ahead += 7
  370. self.next_run += datetime.timedelta(days_ahead) - self.period
  371. if self.at_time is not None:
  372. assert self.unit in ('days', 'hours') or self.start_day is not None
  373. kwargs = {
  374. 'minute': self.at_time.minute,
  375. 'second': self.at_time.second,
  376. 'microsecond': 0
  377. }
  378. if self.unit == 'days' or self.start_day is not None:
  379. kwargs['hour'] = self.at_time.hour
  380. self.next_run = self.next_run.replace(**kwargs)
  381. # If we are running for the first time, make sure we run
  382. # at the specified time *today* (or *this hour*) as well
  383. if not self.last_run:
  384. now = datetime.datetime.now()
  385. if (self.unit == 'days' and self.at_time > now.time() and
  386. self.interval == 1):
  387. self.next_run = self.next_run - datetime.timedelta(days=1)
  388. elif self.unit == 'hours' and self.at_time.minute > now.minute:
  389. self.next_run = self.next_run - datetime.timedelta(hours=1)
  390. if self.start_day is not None and self.at_time is not None:
  391. # Let's see if we will still make that time we specified today
  392. if (self.next_run - datetime.datetime.now()).days >= 7:
  393. self.next_run -= self.period
  394. # The following methods are shortcuts for not having to
  395. # create a Scheduler instance:
  396. #: Default :class:`Scheduler <Scheduler>` object
  397. default_scheduler = Scheduler()
  398. #: Default :class:`Jobs <Job>` list
  399. jobs = default_scheduler.jobs # todo: should this be a copy, e.g. jobs()?
  400. def every(interval=1):
  401. """Calls :meth:`every <Scheduler.every>` on the
  402. :data:`default scheduler instance <default_scheduler>`.
  403. """
  404. return default_scheduler.every(interval)
  405. async def run_pending():
  406. """Calls :meth:`run_pending <Scheduler.run_pending>` on the
  407. :data:`default scheduler instance <default_scheduler>`.
  408. """
  409. await default_scheduler.run_pending()
  410. def run_all(delay_seconds=0):
  411. """Calls :meth:`run_all <Scheduler.run_all>` on the
  412. :data:`default scheduler instance <default_scheduler>`.
  413. """
  414. default_scheduler.run_all(delay_seconds=delay_seconds)
  415. def clear(tag=None):
  416. """Calls :meth:`clear <Scheduler.clear>` on the
  417. :data:`default scheduler instance <default_scheduler>`.
  418. """
  419. default_scheduler.clear(tag)
  420. def cancel_job(job):
  421. """Calls :meth:`cancel_job <Scheduler.cancel_job>` on the
  422. :data:`default scheduler instance <default_scheduler>`.
  423. """
  424. default_scheduler.cancel_job(job)
  425. def next_run():
  426. """Calls :meth:`next_run <Scheduler.next_run>` on the
  427. :data:`default scheduler instance <default_scheduler>`.
  428. """
  429. return default_scheduler.next_run
  430. def idle_seconds():
  431. """Calls :meth:`idle_seconds <Scheduler.idle_seconds>` on the
  432. :data:`default scheduler instance <default_scheduler>`.
  433. """
  434. return default_scheduler.idle_seconds