From f1e5b7b4321cc6de02c8a25cdf8853e47adcdb5b Mon Sep 17 00:00:00 2001 From: marek Date: Fri, 23 Aug 2019 17:27:45 +0200 Subject: [PATCH 1/2] imlemented crontab based repeating job for jobqueue --- requirements.txt | 1 + telegram/ext/jobqueue.py | 29 +++++++++++++++++++++++------ tests/test_jobqueue.py | 22 +++++++++++++++++++++- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index c004d5fc790..d17fed033e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ future>=0.16.0 certifi tornado>=5.1 +croniter cryptography diff --git a/telegram/ext/jobqueue.py b/telegram/ext/jobqueue.py index 0cfa539cf04..845b93790de 100644 --- a/telegram/ext/jobqueue.py +++ b/telegram/ext/jobqueue.py @@ -23,6 +23,7 @@ import time import warnings import weakref +from croniter import croniter from numbers import Number from queue import PriorityQueue, Empty from threading import Thread, Lock, Event @@ -71,13 +72,14 @@ def set_dispatcher(self, dispatcher): self._dispatcher = dispatcher def _put(self, job, next_t=None, last_t=None): - if next_t is None: + if next_t is None and not isinstance(job.interval, str): next_t = job.interval if next_t is None: raise ValueError('next_t is None') if isinstance(next_t, datetime.datetime): next_t = (next_t - datetime.datetime.now()).total_seconds() + next_t += last_t or time.time() elif isinstance(next_t, datetime.time): next_datetime = datetime.datetime.combine(datetime.date.today(), next_t) @@ -86,11 +88,19 @@ def _put(self, job, next_t=None, last_t=None): next_datetime += datetime.timedelta(days=1) next_t = (next_datetime - datetime.datetime.now()).total_seconds() + next_t += last_t or time.time() elif isinstance(next_t, datetime.timedelta): next_t = next_t.total_seconds() + next_t += last_t or time.time() - next_t += last_t or time.time() + elif isinstance(job.interval, str): + base = self._now() + dt = croniter(job.interval, base).get_next(datetime.datetime) + next_t = datetime.datetime.timestamp(dt) + + else: + next_t += last_t or time.time() self.logger.debug('Putting job %s with t=%f', job.name, next_t) @@ -144,9 +154,9 @@ def run_repeating(self, callback, interval, first=None, context=None, name=None) job. It should take ``bot, job`` as parameters, where ``job`` is the :class:`telegram.ext.Job` instance. It can be used to access its ``Job.context`` or change it to a repeating job. - interval (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta`): The interval in which + interval (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta`) | :obj:`str`): The interval in which the job will run. If it is an :obj:`int` or a :obj:`float`, it will be interpreted - as seconds. + as seconds, if it is an :obj:`str` it will be interpreted as crontab first (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \ :obj:`datetime.datetime` | :obj:`datetime.time`, optional): Time in or at which the job should run. This parameter will be interpreted @@ -335,6 +345,9 @@ def get_jobs_by_name(self, name): with self._queue.mutex: return tuple(job[1] for job in self._queue.queue if job and job[1].name == name) + def _now(self): + return datetime.datetime.now() + class Job(object): """This class encapsulates a Job. @@ -438,9 +451,13 @@ def interval(self, interval): if interval is None and self.repeat: raise ValueError("The 'interval' can not be 'None' when 'repeat' is set to 'True'") - if not (interval is None or isinstance(interval, (Number, datetime.timedelta))): + if not (interval is None or isinstance(interval, (Number, datetime.timedelta, str))): raise ValueError("The 'interval' must be of type 'datetime.timedelta'," - " 'int' or 'float'") + " 'int' or 'float' or 'str'") + + if isinstance(interval, str): + if not croniter.is_valid(interval): + raise ValueError("The 'interval' of type 'str' must be a valid crontab") self._interval = interval diff --git a/tests/test_jobqueue.py b/tests/test_jobqueue.py index f0c7ac4051c..f82e47f729b 100644 --- a/tests/test_jobqueue.py +++ b/tests/test_jobqueue.py @@ -25,6 +25,7 @@ import pytest from flaky import flaky +from unittest.mock import patch from telegram.ext import JobQueue, Updater, Job, CallbackContext from telegram.utils.deprecate import TelegramDeprecationWarning @@ -228,6 +229,23 @@ def test_run_daily(self, job_queue): assert self.result == 1 assert pytest.approx(job_queue._queue.get(False)[0]) == expected_time + def test_run_cron_daily(self, job_queue): + dt = datetime.datetime(year=2019, month=1, day=1, hour=1, minute=0, second=0) + ts = datetime.datetime.timestamp(dt) + def patchDateTime() : + return dt + def patchTime() : + return ts + with patch('telegram.ext.JobQueue._now', side_effect=patchDateTime): + with patch('time.time', side_effect=patchTime): + with patch('threading.Event.wait', return_value=True): + job_queue.run_repeating(self.job_run_once, '0 2 * * *') + for i in range(24 * 7 * 2): + dt += datetime.timedelta(hours=1) + ts = datetime.datetime.timestamp(dt) + sleep(0.01) + assert self.result == 14 + def test_warnings(self, job_queue): j = Job(self.job_run_once, repeat=False) with pytest.raises(ValueError, match='can not be set to'): @@ -239,7 +257,9 @@ def test_warnings(self, job_queue): j.interval = None j.repeat = False with pytest.raises(ValueError, match='must be of type'): - j.interval = 'every 3 minutes' + j.interval = {'every': {'minutes': 3}} + with pytest.raises(ValueError, match='must be a valid crontab'): + j.interval = '* * * janu-jun *' j.interval = 15 assert j.interval_seconds == 15 From 3a84e6f4b03d5034374dea074d348b9476da84f0 Mon Sep 17 00:00:00 2001 From: marek Date: Fri, 23 Aug 2019 22:23:21 +0200 Subject: [PATCH 2/2] comply with PEP 8 style --- telegram/ext/jobqueue.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/telegram/ext/jobqueue.py b/telegram/ext/jobqueue.py index 845b93790de..549eb23220a 100644 --- a/telegram/ext/jobqueue.py +++ b/telegram/ext/jobqueue.py @@ -154,9 +154,11 @@ def run_repeating(self, callback, interval, first=None, context=None, name=None) job. It should take ``bot, job`` as parameters, where ``job`` is the :class:`telegram.ext.Job` instance. It can be used to access its ``Job.context`` or change it to a repeating job. - interval (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta`) | :obj:`str`): The interval in which - the job will run. If it is an :obj:`int` or a :obj:`float`, it will be interpreted - as seconds, if it is an :obj:`str` it will be interpreted as crontab + interval (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | + :obj:`str`): The interval in which the job will run. If it is + an :obj:`int` or a :obj:`float`, it will be interpreted as + seconds, if it is an :obj:`str` it will be + interpreted as crontab first (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \ :obj:`datetime.datetime` | :obj:`datetime.time`, optional): Time in or at which the job should run. This parameter will be interpreted