Skip to content

Commit

Permalink
Add execute until functionality
Browse files Browse the repository at this point in the history
Add Job.until function which will execute a job until a certain time is
passed.
  • Loading branch information
fredthomsen committed Feb 25, 2021
1 parent 30ac823 commit db1ec32
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 5 deletions.
14 changes: 14 additions & 0 deletions docs/examples.rst
Expand Up @@ -216,6 +216,20 @@ Run a job at random intervals
``every(A).to(B).seconds`` executes the job function every N seconds such that A <= N <= B.


Run a job until a certain time
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. code-block:: python
def my_job():
print('Boo')
schedule.every(1).hours.until("15:30").do(my_job)
``every(A).until(B).hours`` executes the job function every A hours until time B.



Time until the next execution
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Use ``schedule.idle_seconds()`` to get the number of seconds until the next job is scheduled to run.
Expand Down
84 changes: 79 additions & 5 deletions schedule/__init__.py
Expand Up @@ -214,7 +214,6 @@ class Job(object):
A job is usually created and returned by :meth:`Scheduler.every`
method, which also defines its `interval`.
"""

def __init__(self, interval: int, scheduler: Scheduler = None):
self.interval: int = interval # pause interval * unit between runs
self.latest: Optional[int] = None # upper limit to the interval
Expand All @@ -238,6 +237,9 @@ def __init__(self, interval: int, scheduler: Scheduler = None):
# Specific day of the week to start on
self.start_day: Optional[str] = None

# optional time of final run
self.cancel_after: Optional[datetime.datetime] = None

self.tags: Set[Hashable] = set() # unique set of tags for the job
self.scheduler: Optional[Scheduler] = scheduler # scheduler to register with

Expand Down Expand Up @@ -511,6 +513,69 @@ def to(self, latest: int):
self.latest = latest
return self

def until(self, until_time: Union[datetime.datetime, str]):
"""
Schedule job will run until the specified time. The final execution time
of this job can be greater than until_time if the interval that run_pending
is called on is longer than the interval the job is being executed on.
For example:
>>> schedule.every(10).seconds.until("00:48").do(
>>> lambda: print(datetime.datetime.now())
>>> )
>>> while True:
>>> schedule.run_pending()
>>> time.sleep(24)
could result in the following output:
2021-02-25 00:47:20.440422
2021-02-25 00:47:44.462194
2021-02-25 00:48:08.485673
:param until_time: A datetime or str for a different day representing the
latest time a job can be run. If only a time is supplied, it is assumed
that date is the current date.
:return: The invoked job instance
"""
VALID_TS_FORMATS = (
"%Y-%m-%d %H:%M:%S",
"%Y-%m-%d %H:%M",
"%Y-%m-%d",
"%H:%M:%S",
"%H:%M",
)

def convert_str_to_datetime(ts_str):
now = datetime.datetime.now()
for format_ in VALID_TS_FORMATS:
try:
time = datetime.datetime.strptime(ts_str, format_)
except ValueError:
pass
else:
if "%Y-%m-%d" not in format_:
time = time.replace(
year=now.year, month=now.month, day=now.day
)
return time

raise ScheduleValueError(
"until() param %s did not match any of the following formats: %s",
ts_str,
VALID_TS_FORMATS
)

if isinstance(until_time, datetime.datetime):
self.cancel_after = until_time
elif isinstance(until_time, str):
self.cancel_after = convert_str_to_datetime(until_time)
else:
raise TypeError("until() takes a datetime.datetime or str parameter")

return self

def do(self, job_func: Callable, *args, **kwargs):
"""
Specifies the job_func that should be called every time the
Expand Down Expand Up @@ -547,10 +612,19 @@ def run(self):
:return: The return value returned by the `job_func`
"""
logger.debug("Running job %s", self)
ret = self.job_func()
self.last_run = datetime.datetime.now()
self._schedule_next_run()
if (
self.cancel_after is not None
and self.next_run is not None
and self.next_run > self.cancel_after
):
logger.debug("Cancelling job %s", self)
ret = CancelJob
else:
logger.debug("Running job %s", self)
ret = self.job_func()
self.last_run = datetime.datetime.now()
self._schedule_next_run()

return ret

def _schedule_next_run(self) -> None:
Expand Down
59 changes: 59 additions & 0 deletions test_schedule.py
Expand Up @@ -57,6 +57,10 @@ def now(cls):
self.original_datetime = datetime.datetime
datetime.datetime = MockDate

return MockDate(
self.year, self.month, self.day, self.hour, self.minute, self.second
)

def __exit__(self, *args, **kwargs):
datetime.datetime = self.original_datetime

Expand Down Expand Up @@ -210,6 +214,59 @@ def test_at_time(self):
with self.assertRaises(IntervalError):
every(interval=2).sunday

def test_until_time(self):
with mock_datetime(2020, 1, 1, 0, 0) as m:
mock_job = make_mock_job()
assert every().day.until(
"10:30"
).do(mock_job).cancel_after == m.replace(
hour=10, minute=30, second=0, microsecond=0
)
assert every().day.until(
"10:30:50"
).do(mock_job).cancel_after == m.replace(
hour=10, minute=30, second=50, microsecond=0
)
assert every().day.until(
"3000-01-01 10:30"
).do(mock_job).cancel_after == datetime.datetime(3000, 1, 1, 10, 30, 0)
assert every().day.until(
"3000-01-01 10:30:50"
).do(mock_job).cancel_after == datetime.datetime(3000, 1, 1, 10, 30, 50)
assert every().day.until(
datetime.datetime(3000, 1, 1, 10, 30, 50)
).do(mock_job).cancel_after == datetime.datetime(3000, 1, 1, 10, 30, 50)

self.assertRaises(TypeError, every().day.until, 123)
self.assertRaises(ScheduleValueError, every().day.until, "123")
self.assertRaises(ScheduleValueError, every().day.until, "01-01-3000")

assert every().day.until(
datetime.datetime(2019, 12, 31, 0, 0)
).do(mock_job).run() is schedule.CancelJob

with mock_datetime(2020, 1, 1, 11, 58):
mock_job = make_mock_job()
every().minute.until("12:00").do(mock_job)
every().minute.until("11:00").do(mock_job) # this job will never run
schedule.run_pending()
assert mock_job.call_count == 0

with mock_datetime(2020, 1, 1, 11, 59):
mock_job.reset_mock()
schedule.run_pending()
assert mock_job.call_count == 1

with mock_datetime(2020, 1, 1, 12, 0):
mock_job.reset_mock()
schedule.run_pending()
assert mock_job.call_count == 1

with mock_datetime(2020, 1, 1, 12, 1):
mock_job.reset_mock()
schedule.run_pending()
assert mock_job.call_count == 0

def test_weekday_at_todady(self):
mock_job = make_mock_job()

Expand Down Expand Up @@ -301,6 +358,8 @@ def test_next_run_time(self):
assert every().friday.do(mock_job).next_run.day == 8
assert every().saturday.do(mock_job).next_run.day == 9
assert every().sunday.do(mock_job).next_run.day == 10
assert every().minute.until('12:17').do(
mock_job).next_run.minute == 16

def test_next_run_time_day_end(self):
mock_job = make_mock_job()
Expand Down

0 comments on commit db1ec32

Please sign in to comment.