Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
future>=0.16.0
certifi
tornado>=5.1
croniter
cryptography
33 changes: 26 additions & 7 deletions telegram/ext/jobqueue.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)

Expand Down Expand Up @@ -144,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`): The interval in which
the job will run. If it is an :obj:`int` or a :obj:`float`, it will be interpreted
as seconds.
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
Expand Down Expand Up @@ -335,6 +347,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.
Expand Down Expand Up @@ -438,9 +453,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

Expand Down
22 changes: 21 additions & 1 deletion tests/test_jobqueue.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'):
Expand All @@ -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

Expand Down