Job queue time units (#452)

* Adding timeunit and day support to the jobqueue

* Adding tests

* Changed the file permission back to 644.

* Changed AssertEqual argument order to (actual, expectd).

* Removed the TimeUnit enum and unit param, instead use datetime.time for interval.

* Removing the TimeUnits enum and unit param in favour of optionally using a datetime.time as the interval.

* Removing the TimeUnits enumeration, forgot the remove it in the last one.

* Removed some old docstrings refering to the TimeUnits enum.

* Removed the old TimeUnits import.

* Adding some error handling for the 'days' argument (only a 'tuple' with 'Days')

* Writing the error message directly in the exception.

* Moving a debug statement wrongfully saying a job would be running on days it wouldn't.

* Writing error messages directly in the exceptions instead of making an extra variable.

* Replacing datetime.time in favour of datetime.timedelta because of the get_seconds() method.

* Adding error handling for the method .

* Splitting the tests up in multiple ones, no float test because I haven't found a reliable way to test it.

* Excluding .exrc file.

* Removing \ at EOF of ValueError.

* Replacing Enums with plain new-style classes.

* Using numbers.number to check for ints/floats instead of seperate int/float checks.

* Fixing typo, number -> Number.

* Changed lower_case Days attributes to UPPER_CASE.

* Different formatting for Days class, removed the get_days function in favour of a tuple.

* Removed redundant function get_days.

* Edited the docstring for next_t to also take datetime.timedelta.

* Removed for-loop in favour of any().

* Changed docstring for interval.

* Removed debug print.

* Changing some docstrings.

* Changing some docstrings (again).
This commit is contained in:
Wesley Gahr 2016-11-08 23:39:25 +01:00 committed by Jannes Höke
parent a7bfb0c3a1
commit 68e87db909
4 changed files with 74 additions and 10 deletions

3
.gitignore vendored
View file

@ -71,3 +71,6 @@ telegram.webp
# original files from merges # original files from merges
*.orig *.orig
# Exclude .exrc file for Vim
.exrc

View file

@ -32,6 +32,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `Shelomentsev D <https://github.com/shelomentsevd>`_ - `Shelomentsev D <https://github.com/shelomentsevd>`_
- `sooyhwang <https://github.com/sooyhwang>`_ - `sooyhwang <https://github.com/sooyhwang>`_
- `Valentijn <https://github.com/Faalentijn>`_ - `Valentijn <https://github.com/Faalentijn>`_
- `voider1 <https://github.com/voider1>`_
- `wjt <https://github.com/wjt>`_ - `wjt <https://github.com/wjt>`_
Please add yourself here alphabetically when you submit your first pull request. Please add yourself here alphabetically when you submit your first pull request.

View file

@ -21,10 +21,17 @@
import logging import logging
import time import time
import warnings import warnings
import datetime
from numbers import Number
from threading import Thread, Lock, Event from threading import Thread, Lock, Event
from queue import PriorityQueue, Empty from queue import PriorityQueue, Empty
class Days(object):
MON, TUE, WED, THU, FRI, SAT, SUN = range(7)
EVERY_DAY = tuple(range(7))
class JobQueue(object): class JobQueue(object):
"""This class allows you to periodically perform tasks with the bot. """This class allows you to periodically perform tasks with the bot.
@ -61,14 +68,25 @@ class JobQueue(object):
Args: Args:
job (telegram.ext.Job): The ``Job`` instance representing the new job job (telegram.ext.Job): The ``Job`` instance representing the new job
next_t (Optional[float]): Time in seconds in which the job should be executed first. next_t (Optional[int, float, datetime.timedelta]): Time in which the job
Defaults to ``job.interval`` should be executed first. Defaults to ``job.interval``. ``int`` and ``float``
will be interpreted as seconds.
""" """
job.job_queue = self job.job_queue = self
if next_t is None: if next_t is None:
next_t = job.interval interval = job.interval
if isinstance(interval, Number):
next_t = interval
elif isinstance(interval, datetime.timedelta):
next_t = interval.total_seconds()
else:
raise ValueError("The interval argument should be of type datetime.timedelta,"
" int or float")
elif isinstance(next_t, datetime.timedelta):
next_t = next_t.total_second()
now = time.time() now = time.time()
next_t += now next_t += now
@ -123,11 +141,11 @@ class JobQueue(object):
continue continue
if job.enabled: if job.enabled:
self.logger.debug('Running job %s', job.name)
try: try:
job.run(self.bot) current_week_day = datetime.datetime.now().weekday()
if any(day == current_week_day for day in job.days):
self.logger.debug('Running job %s', job.name)
job.run(self.bot)
except: except:
self.logger.exception('An uncaught error was raised while executing job %s', self.logger.exception('An uncaught error was raised while executing job %s',
job.name) job.name)
@ -200,6 +218,7 @@ class Job(object):
Attributes: Attributes:
callback (function): callback (function):
interval (float): interval (float):
days: (tuple)
repeat (bool): repeat (bool):
name (str): name (str):
enabled (bool): Boolean property that decides if this job is currently active enabled (bool): Boolean property that decides if this job is currently active
@ -208,22 +227,34 @@ class Job(object):
callback (function): The callback function that should be executed by the Job. It should callback (function): The callback function that should be executed by the Job. It should
take two parameters ``bot`` and ``job``, where ``job`` is the ``Job`` instance. It take two parameters ``bot`` and ``job``, where ``job`` is the ``Job`` instance. It
can be used to terminate the job or modify its interval. can be used to terminate the job or modify its interval.
interval (float): The interval in which this job should execute its callback function in interval ([int, float, datetime.timedelta]): The interval in which the job will execute its
seconds. callback function. ``int`` and ``float`` will be interpreted as seconds.
repeat (Optional[bool]): If this job should be periodically execute its callback function repeat (Optional[bool]): If this job should be periodically execute its callback function
(``True``) or only once (``False``). Defaults to ``True`` (``True``) or only once (``False``). Defaults to ``True``
context (Optional[object]): Additional data needed for the callback function. Can be context (Optional[object]): Additional data needed for the callback function. Can be
accessed through ``job.context`` in the callback. Defaults to ``None`` accessed through ``job.context`` in the callback. Defaults to ``None``
days (Tuple): Defines on which days the job should be ran.
""" """
job_queue = None job_queue = None
def __init__(self, callback, interval, repeat=True, context=None): def __init__(self, callback, interval, repeat=True, context=None, days=Days.EVERY_DAY):
self.callback = callback self.callback = callback
self.interval = interval self.interval = interval
self.repeat = repeat self.repeat = repeat
self.context = context self.context = context
if not isinstance(days, tuple):
raise ValueError("The 'days argument should be of type 'tuple'")
if not all(isinstance(day, int) for day in days):
raise ValueError("The elements of the 'days' argument should be of type 'int'")
if not all(day >= 0 and day <= 6 for day in days):
raise ValueError("The elements of the 'days' argument should be from 0 up to and "
"including 6")
self.days = days
self.name = callback.__name__ self.name = callback.__name__
self._remove = Event() self._remove = Event()
self._enabled = Event() self._enabled = Event()

View file

@ -23,6 +23,9 @@ This module contains an object that represents Tests for JobQueue
import logging import logging
import sys import sys
import unittest import unittest
import datetime
import time
from math import ceil
from time import sleep from time import sleep
from tests.test_updater import MockBot from tests.test_updater import MockBot
@ -53,11 +56,15 @@ class JobQueueTest(BaseTest, unittest.TestCase):
self.jq = JobQueue(MockBot('jobqueue_test')) self.jq = JobQueue(MockBot('jobqueue_test'))
self.jq.start() self.jq.start()
self.result = 0 self.result = 0
self.job_time = 0
def tearDown(self): def tearDown(self):
if self.jq is not None: if self.jq is not None:
self.jq.stop() self.jq.stop()
def getSeconds(self):
return int(ceil(time.time()))
def job1(self, bot, job): def job1(self, bot, job):
self.result += 1 self.result += 1
@ -71,6 +78,9 @@ class JobQueueTest(BaseTest, unittest.TestCase):
def job4(self, bot, job): def job4(self, bot, job):
self.result += job.context self.result += job.context
def job5(self, bot, job):
self.job_time = self.getSeconds()
def test_basic(self): def test_basic(self):
self.jq.put(Job(self.job1, 0.1)) self.jq.put(Job(self.job1, 0.1))
sleep(1.5) sleep(1.5)
@ -169,6 +179,25 @@ class JobQueueTest(BaseTest, unittest.TestCase):
finally: finally:
u.stop() u.stop()
def test_time_unit_int(self):
# Testing seconds in int
seconds_interval = 5
expected_time = self.getSeconds() + seconds_interval
self.jq.put(Job(self.job5, seconds_interval, repeat=False))
sleep(6)
self.assertEqual(self.job_time, expected_time)
def test_time_unit_dt_time(self):
# Testing seconds, minutes and hours as datetime.timedelta object
# This is sufficient to test that it actually works.
interval = datetime.timedelta(seconds=5)
expected_time = self.getSeconds() + interval.total_seconds()
self.jq.put(Job(self.job5, interval, repeat=False))
sleep(6)
self.assertEqual(self.job_time, expected_time)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()