good lick with examples: https://schedule.readthedocs.io/en/stable/examples.htmldev-linux
parent
77bb953c19
commit
d13a939041
@ -0,0 +1,35 @@
|
|||||||
|
Thanks to all the wonderful folks who have contributed to schedule over the years:
|
||||||
|
|
||||||
|
- mattss <https://github.com/mattss>
|
||||||
|
- mrhwick <https://github.com/mrhwick>
|
||||||
|
- cfrco <https://github.com/cfrco>
|
||||||
|
- matrixise <https://github.com/matrixise>
|
||||||
|
- abultman <https://github.com/abultman>
|
||||||
|
- mplewis <https://github.com/mplewis>
|
||||||
|
- WoLfulus <https://github.com/WoLfulus>
|
||||||
|
- dylwhich <https://github.com/dylwhich>
|
||||||
|
- fkromer <https://github.com/fkromer>
|
||||||
|
- alaingilbert <https://github.com/alaingilbert>
|
||||||
|
- Zerrossetto <https://github.com/Zerrossetto>
|
||||||
|
- yetingsky <https://github.com/yetingsky>
|
||||||
|
- schnepp <https://github.com/schnepp> <https://bitbucket.org/saschaschnepp>
|
||||||
|
- grampajoe <https://github.com/grampajoe>
|
||||||
|
- gilbsgilbs <https://github.com/gilbsgilbs>
|
||||||
|
- Nathan Wailes <https://github.com/NathanWailes>
|
||||||
|
- Connor Skees <https://github.com/ConnorSkees>
|
||||||
|
- qmorek <https://github.com/qmorek>
|
||||||
|
- aisk <https://github.com/aisk>
|
||||||
|
- MichaelCorleoneLi <https://github.com/MichaelCorleoneLi>
|
||||||
|
- sijmenhuizenga <https://github.com/SijmenHuizenga>
|
||||||
|
- eladbi <https://github.com/eladbi>
|
||||||
|
- chankeypathak <https://github.com/chankeypathak>
|
||||||
|
- vubon <https://github.com/vubon>
|
||||||
|
- gaguirregabiria <https://github.com/gaguirregabiria>
|
||||||
|
- rhagenaars <https://github.com/RHagenaars>
|
||||||
|
- Skenvy <https://github.com/skenvy>
|
||||||
|
- zcking <https://github.com/zcking>
|
||||||
|
- Martin Thoma <https://github.com/MartinThoma>
|
||||||
|
- ebllg <https://github.com/ebllg>
|
||||||
|
- fredthomsen <https://github.com/fredthomsen>
|
||||||
|
- biggerfisch <https://github.com/biggerfisch>
|
||||||
|
- sosolidkk <https://github.com/sosolidkk>
|
@ -0,0 +1 @@
|
|||||||
|
pip
|
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2013 Daniel Bader (http://dbader.org)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
@ -0,0 +1,91 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: schedule
|
||||||
|
Version: 1.1.0
|
||||||
|
Summary: Job scheduling for humans.
|
||||||
|
Home-page: https://github.com/dbader/schedule
|
||||||
|
Author: Daniel Bader
|
||||||
|
Author-email: mail@dbader.org
|
||||||
|
License: MIT
|
||||||
|
Download-URL: https://github.com/dbader/schedule/tarball/1.1.0
|
||||||
|
Keywords: schedule,periodic,jobs,scheduling,clockwork,cron,scheduler,job scheduling
|
||||||
|
Platform: UNKNOWN
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: License :: OSI Approved :: MIT License
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Operating System :: OS Independent
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3.6
|
||||||
|
Classifier: Programming Language :: Python :: 3.7
|
||||||
|
Classifier: Programming Language :: Python :: 3.8
|
||||||
|
Classifier: Programming Language :: Python :: 3.9
|
||||||
|
Classifier: Natural Language :: English
|
||||||
|
Requires-Python: >=3.6
|
||||||
|
|
||||||
|
`schedule <https://schedule.readthedocs.io/>`__
|
||||||
|
===============================================
|
||||||
|
|
||||||
|
|
||||||
|
.. image:: https://github.com/dbader/schedule/workflows/Tests/badge.svg
|
||||||
|
:target: https://github.com/dbader/schedule/actions?query=workflow%3ATests+branch%3Amaster
|
||||||
|
|
||||||
|
.. image:: https://coveralls.io/repos/dbader/schedule/badge.svg?branch=master
|
||||||
|
:target: https://coveralls.io/r/dbader/schedule
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/v/schedule.svg
|
||||||
|
:target: https://pypi.python.org/pypi/schedule
|
||||||
|
|
||||||
|
Python job scheduling for humans. Run Python functions (or any other callable) periodically using a friendly syntax.
|
||||||
|
|
||||||
|
- A simple to use API for scheduling jobs, made for humans.
|
||||||
|
- In-process scheduler for periodic jobs. No extra processes needed!
|
||||||
|
- Very lightweight and no external dependencies.
|
||||||
|
- Excellent test coverage.
|
||||||
|
- Tested on Python and 3.6, 3.7, 3.8, 3.9
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ pip install schedule
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import schedule
|
||||||
|
import time
|
||||||
|
|
||||||
|
def job():
|
||||||
|
print("I'm working...")
|
||||||
|
|
||||||
|
schedule.every(10).seconds.do(job)
|
||||||
|
schedule.every(10).minutes.do(job)
|
||||||
|
schedule.every().hour.do(job)
|
||||||
|
schedule.every().day.at("10:30").do(job)
|
||||||
|
schedule.every(5).to(10).minutes.do(job)
|
||||||
|
schedule.every().monday.do(job)
|
||||||
|
schedule.every().wednesday.at("13:15").do(job)
|
||||||
|
schedule.every().minute.at(":17").do(job)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
schedule.run_pending()
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Schedule's documentation lives at `schedule.readthedocs.io <https://schedule.readthedocs.io/>`_.
|
||||||
|
|
||||||
|
|
||||||
|
Meta
|
||||||
|
----
|
||||||
|
|
||||||
|
Daniel Bader - `@dbader_org <https://twitter.com/dbader_org>`_ - mail@dbader.org
|
||||||
|
|
||||||
|
Inspired by `Adam Wiggins' <https://github.com/adamwiggins>`_ article `"Rethinking Cron" <https://adam.herokuapp.com/past/2010/4/13/rethinking_cron/>`_ and the `clockwork <https://github.com/Rykian/clockwork>`_ Ruby module.
|
||||||
|
|
||||||
|
Distributed under the MIT license. See `LICENSE.txt <https://github.com/dbader/schedule/blob/master/LICENSE.txt>`_ for more information.
|
||||||
|
|
||||||
|
https://github.com/dbader/schedule
|
||||||
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
|||||||
|
schedule-1.1.0.dist-info/AUTHORS.rst,sha256=kdTW4zMQMcIf50TZeSRgSd9Xt-GgBWmNBU7T-L42w4Y,1539
|
||||||
|
schedule-1.1.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
schedule-1.1.0.dist-info/LICENSE.txt,sha256=MKg1LDGM4bZFrN4CmWlzQtQ4DtJjfXyhiorSVmHjtBs,1099
|
||||||
|
schedule-1.1.0.dist-info/METADATA,sha256=PqtfjpOzw6RLQBETDHUTpy3--PbtzxBFsqerANEqRGE,2996
|
||||||
|
schedule-1.1.0.dist-info/RECORD,,
|
||||||
|
schedule-1.1.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
schedule-1.1.0.dist-info/WHEEL,sha256=Z-nyYpwrcSqxfdux5Mbn_DQ525iP7J2DG3JgGvOYyTQ,110
|
||||||
|
schedule-1.1.0.dist-info/top_level.txt,sha256=gFq0D6Oy7pGyXL4X3ITQVefZHZ6ScWCUk8Z3C1ub7SE,9
|
||||||
|
schedule/__init__.py,sha256=t_wUVwkVvmyOeFy0vm8lZLRwkt6AbqbjFbc4jMsUk8w,28680
|
||||||
|
schedule/__pycache__/__init__.cpython-37.pyc,,
|
@ -0,0 +1,6 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: bdist_wheel (0.36.2)
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py2-none-any
|
||||||
|
Tag: py3-none-any
|
||||||
|
|
@ -0,0 +1 @@
|
|||||||
|
schedule
|
@ -0,0 +1,839 @@
|
|||||||
|
"""
|
||||||
|
Python job scheduling for humans.
|
||||||
|
|
||||||
|
github.com/dbader/schedule
|
||||||
|
|
||||||
|
An in-process scheduler for periodic jobs that uses the builder pattern
|
||||||
|
for configuration. Schedule lets you run Python functions (or any other
|
||||||
|
callable) periodically at pre-determined intervals using a simple,
|
||||||
|
human-friendly syntax.
|
||||||
|
|
||||||
|
Inspired by Addam Wiggins' article "Rethinking Cron" [1] and the
|
||||||
|
"clockwork" Ruby module [2][3].
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- A simple to use API for scheduling jobs.
|
||||||
|
- Very lightweight and no external dependencies.
|
||||||
|
- Excellent test coverage.
|
||||||
|
- Tested on Python 3.6, 3.7, 3.8, 3.9
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
>>> import schedule
|
||||||
|
>>> import time
|
||||||
|
|
||||||
|
>>> def job(message='stuff'):
|
||||||
|
>>> print("I'm working on:", message)
|
||||||
|
|
||||||
|
>>> schedule.every(10).minutes.do(job)
|
||||||
|
>>> schedule.every(5).to(10).days.do(job)
|
||||||
|
>>> schedule.every().hour.do(job, message='things')
|
||||||
|
>>> schedule.every().day.at("10:30").do(job)
|
||||||
|
|
||||||
|
>>> while True:
|
||||||
|
>>> schedule.run_pending()
|
||||||
|
>>> time.sleep(1)
|
||||||
|
|
||||||
|
[1] https://adam.herokuapp.com/past/2010/4/13/rethinking_cron/
|
||||||
|
[2] https://github.com/Rykian/clockwork
|
||||||
|
[3] https://adam.herokuapp.com/past/2010/6/30/replace_cron_with_clockwork/
|
||||||
|
"""
|
||||||
|
from collections.abc import Hashable
|
||||||
|
import datetime
|
||||||
|
import functools
|
||||||
|
import logging
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
from typing import Set, List, Optional, Callable, Union
|
||||||
|
|
||||||
|
logger = logging.getLogger("schedule")
|
||||||
|
|
||||||
|
|
||||||
|
class ScheduleError(Exception):
|
||||||
|
"""Base schedule exception"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ScheduleValueError(ScheduleError):
|
||||||
|
"""Base schedule value error"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class IntervalError(ScheduleValueError):
|
||||||
|
"""An improper interval was used"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CancelJob(object):
|
||||||
|
"""
|
||||||
|
Can be returned from a job to unschedule itself.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Scheduler(object):
|
||||||
|
"""
|
||||||
|
Objects instantiated by the :class:`Scheduler <Scheduler>` are
|
||||||
|
factories to create jobs, keep record of scheduled jobs and
|
||||||
|
handle their execution.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.jobs: List[Job] = []
|
||||||
|
|
||||||
|
def run_pending(self) -> None:
|
||||||
|
"""
|
||||||
|
Run all jobs that are scheduled to run.
|
||||||
|
|
||||||
|
Please note that it is *intended behavior that run_pending()
|
||||||
|
does not run missed jobs*. For example, if you've registered a job
|
||||||
|
that should run every minute and you only call run_pending()
|
||||||
|
in one hour increments then your job won't be run 60 times in
|
||||||
|
between but only once.
|
||||||
|
"""
|
||||||
|
runnable_jobs = (job for job in self.jobs if job.should_run)
|
||||||
|
for job in sorted(runnable_jobs):
|
||||||
|
self._run_job(job)
|
||||||
|
|
||||||
|
def run_all(self, delay_seconds: int = 0) -> None:
|
||||||
|
"""
|
||||||
|
Run all jobs regardless if they are scheduled to run or not.
|
||||||
|
|
||||||
|
A delay of `delay` seconds is added between each job. This helps
|
||||||
|
distribute system load generated by the jobs more evenly
|
||||||
|
over time.
|
||||||
|
|
||||||
|
:param delay_seconds: A delay added between every executed job
|
||||||
|
"""
|
||||||
|
logger.debug(
|
||||||
|
"Running *all* %i jobs with %is delay in between",
|
||||||
|
len(self.jobs),
|
||||||
|
delay_seconds,
|
||||||
|
)
|
||||||
|
for job in self.jobs[:]:
|
||||||
|
self._run_job(job)
|
||||||
|
time.sleep(delay_seconds)
|
||||||
|
|
||||||
|
def get_jobs(self, tag: Optional[Hashable] = None) -> List["Job"]:
|
||||||
|
"""
|
||||||
|
Gets scheduled jobs marked with the given tag, or all jobs
|
||||||
|
if tag is omitted.
|
||||||
|
|
||||||
|
:param tag: An identifier used to identify a subset of
|
||||||
|
jobs to retrieve
|
||||||
|
"""
|
||||||
|
if tag is None:
|
||||||
|
return self.jobs[:]
|
||||||
|
else:
|
||||||
|
return [job for job in self.jobs if tag in job.tags]
|
||||||
|
|
||||||
|
def clear(self, tag: Optional[Hashable] = None) -> None:
|
||||||
|
"""
|
||||||
|
Deletes scheduled jobs marked with the given tag, or all jobs
|
||||||
|
if tag is omitted.
|
||||||
|
|
||||||
|
:param tag: An identifier used to identify a subset of
|
||||||
|
jobs to delete
|
||||||
|
"""
|
||||||
|
if tag is None:
|
||||||
|
logger.debug("Deleting *all* jobs")
|
||||||
|
del self.jobs[:]
|
||||||
|
else:
|
||||||
|
logger.debug('Deleting all jobs tagged "%s"', tag)
|
||||||
|
self.jobs[:] = (job for job in self.jobs if tag not in job.tags)
|
||||||
|
|
||||||
|
def cancel_job(self, job: "Job") -> None:
|
||||||
|
"""
|
||||||
|
Delete a scheduled job.
|
||||||
|
|
||||||
|
:param job: The job to be unscheduled
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.debug('Cancelling job "%s"', str(job))
|
||||||
|
self.jobs.remove(job)
|
||||||
|
except ValueError:
|
||||||
|
logger.debug('Cancelling not-scheduled job "%s"', str(job))
|
||||||
|
|
||||||
|
def every(self, interval: int = 1) -> "Job":
|
||||||
|
"""
|
||||||
|
Schedule a new periodic job.
|
||||||
|
|
||||||
|
:param interval: A quantity of a certain time unit
|
||||||
|
:return: An unconfigured :class:`Job <Job>`
|
||||||
|
"""
|
||||||
|
job = Job(interval, self)
|
||||||
|
return job
|
||||||
|
|
||||||
|
def _run_job(self, job: "Job") -> None:
|
||||||
|
ret = job.run()
|
||||||
|
if isinstance(ret, CancelJob) or ret is CancelJob:
|
||||||
|
self.cancel_job(job)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def next_run(self) -> Optional[datetime.datetime]:
|
||||||
|
"""
|
||||||
|
Datetime when the next job should run.
|
||||||
|
|
||||||
|
:return: A :class:`~datetime.datetime` object
|
||||||
|
or None if no jobs scheduled
|
||||||
|
"""
|
||||||
|
if not self.jobs:
|
||||||
|
return None
|
||||||
|
return min(self.jobs).next_run
|
||||||
|
|
||||||
|
@property
|
||||||
|
def idle_seconds(self) -> Optional[float]:
|
||||||
|
"""
|
||||||
|
:return: Number of seconds until
|
||||||
|
:meth:`next_run <Scheduler.next_run>`
|
||||||
|
or None if no jobs are scheduled
|
||||||
|
"""
|
||||||
|
if not self.next_run:
|
||||||
|
return None
|
||||||
|
return (self.next_run - datetime.datetime.now()).total_seconds()
|
||||||
|
|
||||||
|
|
||||||
|
class Job(object):
|
||||||
|
"""
|
||||||
|
A periodic job as used by :class:`Scheduler`.
|
||||||
|
|
||||||
|
:param interval: A quantity of a certain time unit
|
||||||
|
:param scheduler: The :class:`Scheduler <Scheduler>` instance that
|
||||||
|
this job will register itself with once it has
|
||||||
|
been fully configured in :meth:`Job.do()`.
|
||||||
|
|
||||||
|
Every job runs at a given fixed time interval that is defined by:
|
||||||
|
|
||||||
|
* a :meth:`time unit <Job.second>`
|
||||||
|
* a quantity of `time units` defined by `interval`
|
||||||
|
|
||||||
|
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
|
||||||
|
self.job_func: Optional[functools.partial] = None # the job job_func to run
|
||||||
|
|
||||||
|
# time units, e.g. 'minutes', 'hours', ...
|
||||||
|
self.unit: Optional[str] = None
|
||||||
|
|
||||||
|
# optional time at which this job runs
|
||||||
|
self.at_time: Optional[datetime.time] = None
|
||||||
|
|
||||||
|
# datetime of the last run
|
||||||
|
self.last_run: Optional[datetime.datetime] = None
|
||||||
|
|
||||||
|
# datetime of the next run
|
||||||
|
self.next_run: Optional[datetime.datetime] = None
|
||||||
|
|
||||||
|
# timedelta between runs, only valid for
|
||||||
|
self.period: Optional[datetime.timedelta] = 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
|
||||||
|
|
||||||
|
def __lt__(self, other) -> bool:
|
||||||
|
"""
|
||||||
|
PeriodicJobs are sortable based on the scheduled time they
|
||||||
|
run next.
|
||||||
|
"""
|
||||||
|
return self.next_run < other.next_run
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
if hasattr(self.job_func, "__name__"):
|
||||||
|
job_func_name = self.job_func.__name__ # type: ignore
|
||||||
|
else:
|
||||||
|
job_func_name = repr(self.job_func)
|
||||||
|
|
||||||
|
return ("Job(interval={}, unit={}, do={}, args={}, kwargs={})").format(
|
||||||
|
self.interval,
|
||||||
|
self.unit,
|
||||||
|
job_func_name,
|
||||||
|
"()" if self.job_func is None else self.job_func.args,
|
||||||
|
"{}" if self.job_func is None else self.job_func.keywords,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
def format_time(t):
|
||||||
|
return t.strftime("%Y-%m-%d %H:%M:%S") if t else "[never]"
|
||||||
|
|
||||||
|
def is_repr(j):
|
||||||
|
return not isinstance(j, Job)
|
||||||
|
|
||||||
|
timestats = "(last run: %s, next run: %s)" % (
|
||||||
|
format_time(self.last_run),
|
||||||
|
format_time(self.next_run),
|
||||||
|
)
|
||||||
|
|
||||||
|
if hasattr(self.job_func, "__name__"):
|
||||||
|
job_func_name = self.job_func.__name__
|
||||||
|
else:
|
||||||
|
job_func_name = repr(self.job_func)
|
||||||
|
args = [repr(x) if is_repr(x) else str(x) for x in self.job_func.args]
|
||||||
|
kwargs = ["%s=%s" % (k, repr(v)) for k, v in self.job_func.keywords.items()]
|
||||||
|
call_repr = job_func_name + "(" + ", ".join(args + kwargs) + ")"
|
||||||
|
|
||||||
|
if self.at_time is not None:
|
||||||
|
return "Every %s %s at %s do %s %s" % (
|
||||||
|
self.interval,
|
||||||
|
self.unit[:-1] if self.interval == 1 else self.unit,
|
||||||
|
self.at_time,
|
||||||
|
call_repr,
|
||||||
|
timestats,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
fmt = (
|
||||||
|
"Every %(interval)s "
|
||||||
|
+ ("to %(latest)s " if self.latest is not None else "")
|
||||||
|
+ "%(unit)s do %(call_repr)s %(timestats)s"
|
||||||
|
)
|
||||||
|
|
||||||
|
return fmt % dict(
|
||||||
|
interval=self.interval,
|
||||||
|
latest=self.latest,
|
||||||
|
unit=(self.unit[:-1] if self.interval == 1 else self.unit),
|
||||||
|
call_repr=call_repr,
|
||||||
|
timestats=timestats,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def second(self):
|
||||||
|
if self.interval != 1:
|
||||||
|
raise IntervalError("Use seconds instead of second")
|
||||||
|
return self.seconds
|
||||||
|
|
||||||
|
@property
|
||||||
|
def seconds(self):
|
||||||
|
self.unit = "seconds"
|
||||||
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def minute(self):
|
||||||
|
if self.interval != 1:
|
||||||
|
raise IntervalError("Use minutes instead of minute")
|
||||||
|
return self.minutes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def minutes(self):
|
||||||
|
self.unit = "minutes"
|
||||||
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hour(self):
|
||||||
|
if self.interval != 1:
|
||||||
|
raise IntervalError("Use hours instead of hour")
|
||||||
|
return self.hours
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hours(self):
|
||||||
|
self.unit = "hours"
|
||||||
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def day(self):
|
||||||
|
if self.interval != 1:
|
||||||
|
raise IntervalError("Use days instead of day")
|
||||||
|
return self.days
|
||||||
|
|
||||||
|
@property
|
||||||
|
def days(self):
|
||||||
|
self.unit = "days"
|
||||||
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def week(self):
|
||||||
|
if self.interval != 1:
|
||||||
|
raise IntervalError("Use weeks instead of week")
|
||||||
|
return self.weeks
|
||||||
|
|
||||||
|
@property
|
||||||
|
def weeks(self):
|
||||||
|
self.unit = "weeks"
|
||||||
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def monday(self):
|
||||||
|
if self.interval != 1:
|
||||||
|
raise IntervalError(
|
||||||
|
"Scheduling .monday() jobs is only allowed for weekly jobs. "
|
||||||
|
"Using .monday() on a job scheduled to run every 2 or more weeks "
|
||||||
|
"is not supported."
|
||||||
|
)
|
||||||
|
self.start_day = "monday"
|
||||||
|
return self.weeks
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tuesday(self):
|
||||||
|
if self.interval != 1:
|
||||||
|
raise IntervalError(
|
||||||
|
"Scheduling .tuesday() jobs is only allowed for weekly jobs. "
|
||||||
|
"Using .tuesday() on a job scheduled to run every 2 or more weeks "
|
||||||
|
"is not supported."
|
||||||
|
)
|
||||||
|
self.start_day = "tuesday"
|
||||||
|
return self.weeks
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wednesday(self):
|
||||||
|
if self.interval != 1:
|
||||||
|
raise IntervalError(
|
||||||
|
"Scheduling .wednesday() jobs is only allowed for weekly jobs. "
|
||||||
|
"Using .wednesday() on a job scheduled to run every 2 or more weeks "
|
||||||
|
"is not supported."
|
||||||
|
)
|
||||||
|
self.start_day = "wednesday"
|
||||||
|
return self.weeks
|
||||||
|
|
||||||
|
@property
|
||||||
|
def thursday(self):
|
||||||
|
if self.interval != 1:
|
||||||
|
raise IntervalError(
|
||||||
|
"Scheduling .thursday() jobs is only allowed for weekly jobs. "
|
||||||
|
"Using .thursday() on a job scheduled to run every 2 or more weeks "
|
||||||
|
"is not supported."
|
||||||
|
)
|
||||||
|
self.start_day = "thursday"
|
||||||
|
return self.weeks
|
||||||
|
|
||||||
|
@property
|
||||||
|
def friday(self):
|
||||||
|
if self.interval != 1:
|
||||||
|
raise IntervalError(
|
||||||
|
"Scheduling .friday() jobs is only allowed for weekly jobs. "
|
||||||
|
"Using .friday() on a job scheduled to run every 2 or more weeks "
|
||||||
|
"is not supported."
|
||||||
|
)
|
||||||
|
self.start_day = "friday"
|
||||||
|
return self.weeks
|
||||||
|
|
||||||
|
@property
|
||||||
|
def saturday(self):
|
||||||
|
if self.interval != 1:
|
||||||
|
raise IntervalError(
|
||||||
|
"Scheduling .saturday() jobs is only allowed for weekly jobs. "
|
||||||
|
"Using .saturday() on a job scheduled to run every 2 or more weeks "
|
||||||
|
"is not supported."
|
||||||
|
)
|
||||||
|
self.start_day = "saturday"
|
||||||
|
return self.weeks
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sunday(self):
|
||||||
|
if self.interval != 1:
|
||||||
|
raise IntervalError(
|
||||||
|
"Scheduling .sunday() jobs is only allowed for weekly jobs. "
|
||||||
|
"Using .sunday() on a job scheduled to run every 2 or more weeks "
|
||||||
|
"is not supported."
|
||||||
|
)
|
||||||
|
self.start_day = "sunday"
|
||||||
|
return self.weeks
|
||||||
|
|
||||||
|
def tag(self, *tags: Hashable):
|
||||||
|
"""
|
||||||
|
Tags the job with one or more unique identifiers.
|
||||||
|
|
||||||
|
Tags must be hashable. Duplicate tags are discarded.
|
||||||
|
|
||||||
|
:param tags: A unique list of ``Hashable`` tags.
|
||||||
|
:return: The invoked job instance
|
||||||
|
"""
|
||||||
|
if not all(isinstance(tag, Hashable) for tag in tags):
|
||||||
|
raise TypeError("Tags must be hashable")
|
||||||
|
self.tags.update(tags)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def at(self, time_str):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Specify a particular time that the job should be run at.
|
||||||
|
|
||||||
|
:param time_str: A string in one of the following formats:
|
||||||
|
|
||||||
|
- For daily jobs -> `HH:MM:SS` or `HH:MM`
|
||||||
|
- For hourly jobs -> `MM:SS` or `:MM`
|
||||||
|
- For minute jobs -> `:SS`
|
||||||
|
|
||||||
|
The format must make sense given how often the job is
|
||||||
|
repeating; for example, a job that repeats every minute
|
||||||
|
should not be given a string in the form `HH:MM:SS`. The
|
||||||
|
difference between `:MM` and :SS` is inferred from the
|
||||||
|
selected time-unit (e.g. `every().hour.at(':30')` vs.
|
||||||
|
`every().minute.at(':30')`).
|
||||||
|
|
||||||
|
:return: The invoked job instance
|
||||||
|
"""
|
||||||
|
if self.unit not in ("days", "hours", "minutes") and not self.start_day:
|
||||||
|
raise ScheduleValueError(
|
||||||
|
"Invalid unit (valid units are `days`, `hours`, and `minutes`)"
|
||||||
|
)
|
||||||
|
if not isinstance(time_str, str):
|
||||||
|
raise TypeError("at() should be passed a string")
|
||||||
|
if self.unit == "days" or self.start_day:
|
||||||
|
if not re.match(r"^([0-2]\d:)?[0-5]\d:[0-5]\d$", time_str):
|
||||||
|
raise ScheduleValueError(
|
||||||
|
"Invalid time format for a daily job (valid format is HH:MM(:SS)?)"
|
||||||
|
)
|
||||||
|
if self.unit == "hours":
|
||||||
|
if not re.match(r"^([0-5]\d)?:[0-5]\d$", time_str):
|
||||||
|
raise ScheduleValueError(
|
||||||
|
"Invalid time format for an hourly job (valid format is (MM)?:SS)"
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.unit == "minutes":
|
||||||
|
if not re.match(r"^:[0-5]\d$", time_str):
|
||||||
|
raise ScheduleValueError(
|
||||||
|
"Invalid time format for a minutely job (valid format is :SS)"
|
||||||
|
)
|
||||||
|
time_values = time_str.split(":")
|
||||||
|
hour: Union[str, int]
|
||||||
|
minute: Union[str, int]
|
||||||
|
second: Union[str, int]
|
||||||
|
if len(time_values) == 3:
|
||||||
|
hour, minute, second = time_values
|
||||||
|
elif len(time_values) == 2 and self.unit == "minutes":
|
||||||
|
hour = 0
|
||||||
|
minute = 0
|
||||||
|
_, second = time_values
|
||||||
|
elif len(time_values) == 2 and self.unit == "hours" and len(time_values[0]):
|
||||||
|
hour = 0
|
||||||
|
minute, second = time_values
|
||||||
|
else:
|
||||||
|
hour, minute = time_values
|
||||||
|
second = 0
|
||||||
|
if self.unit == "days" or self.start_day:
|
||||||
|
hour = int(hour)
|
||||||
|
if not (0 <= hour <= 23):
|
||||||
|
raise ScheduleValueError(
|
||||||
|
"Invalid number of hours ({} is not between 0 and 23)"
|
||||||
|
)
|
||||||
|
elif self.unit == "hours":
|
||||||
|
hour = 0
|
||||||
|
elif self.unit == "minutes":
|
||||||
|
hour = 0
|
||||||
|
minute = 0
|
||||||
|
minute = int(minute)
|
||||||
|
second = int(second)
|
||||||
|
self.at_time = datetime.time(hour, minute, second)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def to(self, latest: int):
|
||||||
|
"""
|
||||||
|
Schedule the job to run at an irregular (randomized) interval.
|
||||||
|
|
||||||
|
The job's interval will randomly vary from the value given
|
||||||
|
to `every` to `latest`. The range defined is inclusive on
|
||||||
|
both ends. For example, `every(A).to(B).seconds` executes
|
||||||
|
the job function every N seconds such that A <= N <= B.
|
||||||
|
|
||||||
|
:param latest: Maximum interval between randomized job runs
|
||||||
|
:return: The invoked job instance
|
||||||
|
"""
|
||||||
|
self.latest = latest
|
||||||
|
return self
|
||||||
|
|
||||||
|
def until(
|
||||||
|
self,
|
||||||
|
until_time: Union[datetime.datetime, datetime.timedelta, datetime.time, str],
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Schedule job to run until the specified moment.
|
||||||
|
|
||||||
|
The job is canceled whenever the next run is calculated and it turns out the
|
||||||
|
next run is after the until_time. The job is also canceled right before it runs,
|
||||||
|
if the current time is after until_time. This latter case can happen when the
|
||||||
|
the job was scheduled to run before until_time, but runs after until_time.
|
||||||
|
|
||||||
|
If until_time is a moment in the past, ScheduleValueError is thrown.
|
||||||
|
|
||||||
|
:param until_time: A moment in the future representing the latest time a job can
|
||||||
|
be run. If only a time is supplied, the date is set to today.
|
||||||
|
The following formats are accepted:
|
||||||
|
|
||||||
|
- datetime.datetime
|
||||||
|
- datetime.timedelta
|
||||||
|
- datetime.time
|
||||||
|
- String in one of the following formats: "%Y-%m-%d %H:%M:%S",
|
||||||
|
"%Y-%m-%d %H:%M", "%Y-%m-%d", "%H:%M:%S", "%H:%M"
|
||||||
|
as defined by strptime() behaviour. If an invalid string format is passed,
|
||||||
|
ScheduleValueError is thrown.
|
||||||
|
|
||||||
|
:return: The invoked job instance
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(until_time, datetime.datetime):
|
||||||
|
self.cancel_after = until_time
|
||||||
|
elif isinstance(until_time, datetime.timedelta):
|
||||||
|
self.cancel_after = datetime.datetime.now() + until_time
|
||||||
|
elif isinstance(until_time, datetime.time):
|
||||||
|
self.cancel_after = datetime.datetime.combine(
|
||||||
|
datetime.datetime.now(), until_time
|
||||||
|
)
|
||||||
|
elif isinstance(until_time, str):
|
||||||
|
cancel_after = self._decode_datetimestr(
|
||||||
|
until_time,
|
||||||
|
[
|
||||||
|
"%Y-%m-%d %H:%M:%S",
|
||||||
|
"%Y-%m-%d %H:%M",
|
||||||
|
"%Y-%m-%d",
|
||||||
|
"%H:%M:%S",
|
||||||
|
"%H:%M",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
if cancel_after is None:
|
||||||
|
raise ScheduleValueError("Invalid string format for until()")
|
||||||
|
if "-" not in until_time:
|
||||||
|
# the until_time is a time-only format. Set the date to today
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
cancel_after = cancel_after.replace(
|
||||||
|
year=now.year, month=now.month, day=now.day
|
||||||
|
)
|
||||||
|
self.cancel_after = cancel_after
|
||||||
|
else:
|
||||||
|
raise TypeError(
|
||||||
|
"until() takes a string, datetime.datetime, datetime.timedelta, "
|
||||||
|
"datetime.time parameter"
|
||||||
|
)
|
||||||
|
if self.cancel_after < datetime.datetime.now():
|
||||||
|
raise ScheduleValueError(
|
||||||
|
"Cannot schedule a job to run until a time in the past"
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def do(self, job_func: Callable, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Specifies the job_func that should be called every time the
|
||||||
|
job runs.
|
||||||
|
|
||||||
|
Any additional arguments are passed on to job_func when
|
||||||
|
the job runs.
|
||||||
|
|
||||||
|
:param job_func: The function to be scheduled
|
||||||
|
:return: The invoked job instance
|
||||||
|
"""
|
||||||
|
self.job_func = functools.partial(job_func, *args, **kwargs)
|
||||||
|
functools.update_wrapper(self.job_func, job_func)
|
||||||
|
self._schedule_next_run()
|
||||||
|
if self.scheduler is None:
|
||||||
|
raise ScheduleError(
|
||||||
|
"Unable to a add job to schedule. "
|
||||||
|
"Job is not associated with an scheduler"
|
||||||
|
)
|
||||||
|
self.scheduler.jobs.append(self)
|
||||||
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_run(self) -> bool:
|
||||||
|
"""
|
||||||
|
:return: ``True`` if the job should be run now.
|
||||||
|
"""
|
||||||
|
assert self.next_run is not None, "must run _schedule_next_run before"
|
||||||
|
return datetime.datetime.now() >= self.next_run
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""
|
||||||
|
Run the job and immediately reschedule it.
|
||||||
|
If the job's deadline is reached (configured using .until()), the job is not
|
||||||
|
run and CancelJob is returned immediately. If the next scheduled run exceeds
|
||||||
|
the job's deadline, CancelJob is returned after the execution. In this latter
|
||||||
|
case CancelJob takes priority over any other returned value.
|
||||||
|
|
||||||
|
:return: The return value returned by the `job_func`, or CancelJob if the job's
|
||||||
|
deadline is reached.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self._is_overdue(datetime.datetime.now()):
|
||||||
|
logger.debug("Cancelling job %s", self)
|
||||||
|
return CancelJob
|
||||||
|
|
||||||
|
logger.debug("Running job %s", self)
|
||||||
|
ret = self.job_func()
|
||||||
|
self.last_run = datetime.datetime.now()
|
||||||
|
self._schedule_next_run()
|
||||||
|
|
||||||
|
if self._is_overdue(self.next_run):
|
||||||
|
logger.debug("Cancelling job %s", self)
|
||||||
|
return CancelJob
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def _schedule_next_run(self) -> None:
|
||||||
|
"""
|
||||||
|
Compute the instant when this job should run next.
|
||||||
|
"""
|
||||||
|
if self.unit not in ("seconds", "minutes", "hours", "days", "weeks"):
|
||||||
|
raise ScheduleValueError(
|
||||||
|
"Invalid unit (valid units are `seconds`, `minutes`, `hours`, "
|
||||||
|
"`days`, and `weeks`)"
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.latest is not None:
|
||||||
|
if not (self.latest >= self.interval):
|
||||||
|
raise ScheduleError("`latest` is greater than `interval`")
|
||||||
|
interval = random.randint(self.interval, self.latest)
|
||||||
|
else:
|
||||||
|
interval = self.interval
|
||||||
|
|
||||||
|
self.period = datetime.timedelta(**{self.unit: interval})
|
||||||
|
self.next_run = datetime.datetime.now() + self.period
|
||||||
|
if self.start_day is not None:
|
||||||
|
if self.unit != "weeks":
|
||||||
|
raise ScheduleValueError("`unit` should be 'weeks'")
|
||||||
|
weekdays = (
|
||||||
|
"monday",
|
||||||
|
"tuesday",
|
||||||
|
"wednesday",
|
||||||
|
"thursday",
|
||||||
|
"friday",
|
||||||
|
"saturday",
|
||||||
|
"sunday",
|
||||||
|
)
|
||||||
|
if self.start_day not in weekdays:
|
||||||
|
raise ScheduleValueError(
|
||||||
|
"Invalid start day (valid start days are {})".format(weekdays)
|
||||||
|
)
|
||||||
|
weekday = weekdays.index(self.start_day)
|
||||||
|
days_ahead = weekday - self.next_run.weekday()
|
||||||
|
if days_ahead <= 0: # Target day already happened this week
|
||||||
|
days_ahead += 7
|
||||||
|
self.next_run += datetime.timedelta(days_ahead) - self.period
|
||||||
|
if self.at_time is not None:
|
||||||
|
if self.unit not in ("days", "hours", "minutes") and self.start_day is None:
|
||||||
|
raise ScheduleValueError("Invalid unit without specifying start day")
|
||||||
|
kwargs = {"second": self.at_time.second, "microsecond": 0}
|
||||||
|
if self.unit == "days" or self.start_day is not None:
|
||||||
|
kwargs["hour"] = self.at_time.hour
|
||||||
|
if self.unit in ["days", "hours"] or self.start_day is not None:
|
||||||
|
kwargs["minute"] = self.at_time.minute
|
||||||
|
self.next_run = self.next_run.replace(**kwargs) # type: ignore
|
||||||
|
# Make sure we run at the specified time *today* (or *this hour*)
|
||||||
|
# as well. This accounts for when a job takes so long it finished
|
||||||
|
# in the next period.
|
||||||
|
if not self.last_run or (self.next_run - self.last_run) > self.period:
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
if (
|
||||||
|
self.unit == "days"
|
||||||
|
and self.at_time > now.time()
|
||||||
|
and self.interval == 1
|
||||||
|
):
|
||||||
|
self.next_run = self.next_run - datetime.timedelta(days=1)
|
||||||
|
elif self.unit == "hours" and (
|
||||||
|
self.at_time.minute > now.minute
|
||||||
|
or (
|
||||||
|
self.at_time.minute == now.minute
|
||||||
|
and self.at_time.second > now.second
|
||||||
|
)
|
||||||
|
):
|
||||||
|
self.next_run = self.next_run - datetime.timedelta(hours=1)
|
||||||
|
elif self.unit == "minutes" and self.at_time.second > now.second:
|
||||||
|
self.next_run = self.next_run - datetime.timedelta(minutes=1)
|
||||||
|
if self.start_day is not None and self.at_time is not None:
|
||||||
|
# Let's see if we will still make that time we specified today
|
||||||
|
if (self.next_run - datetime.datetime.now()).days >= 7:
|
||||||
|
self.next_run -= self.period
|
||||||
|
|
||||||
|
def _is_overdue(self, when: datetime.datetime):
|
||||||
|
return self.cancel_after is not None and when > self.cancel_after
|
||||||
|
|
||||||
|
def _decode_datetimestr(
|
||||||
|
self, datetime_str: str, formats: List[str]
|
||||||
|
) -> Optional[datetime.datetime]:
|
||||||
|
for f in formats:
|
||||||
|
try:
|
||||||
|
return datetime.datetime.strptime(datetime_str, f)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# The following methods are shortcuts for not having to
|
||||||
|
# create a Scheduler instance:
|
||||||
|
|
||||||
|
#: Default :class:`Scheduler <Scheduler>` object
|
||||||
|
default_scheduler = Scheduler()
|
||||||
|
|
||||||
|
#: Default :class:`Jobs <Job>` list
|
||||||
|
jobs = default_scheduler.jobs # todo: should this be a copy, e.g. jobs()?
|
||||||
|
|
||||||
|
|
||||||
|
def every(interval: int = 1) -> Job:
|
||||||
|
"""Calls :meth:`every <Scheduler.every>` on the
|
||||||
|
:data:`default scheduler instance <default_scheduler>`.
|
||||||
|
"""
|
||||||
|
return default_scheduler.every(interval)
|
||||||
|
|
||||||
|
|
||||||
|
def run_pending() -> None:
|
||||||
|
"""Calls :meth:`run_pending <Scheduler.run_pending>` on the
|
||||||
|
:data:`default scheduler instance <default_scheduler>`.
|
||||||
|
"""
|
||||||
|
default_scheduler.run_pending()
|
||||||
|
|
||||||
|
|
||||||
|
def run_all(delay_seconds: int = 0) -> None:
|
||||||
|
"""Calls :meth:`run_all <Scheduler.run_all>` on the
|
||||||
|
:data:`default scheduler instance <default_scheduler>`.
|
||||||
|
"""
|
||||||
|
default_scheduler.run_all(delay_seconds=delay_seconds)
|
||||||
|
|
||||||
|
|
||||||
|
def get_jobs(tag: Optional[Hashable] = None) -> List[Job]:
|
||||||
|
"""Calls :meth:`get_jobs <Scheduler.get_jobs>` on the
|
||||||
|
:data:`default scheduler instance <default_scheduler>`.
|
||||||
|
"""
|
||||||
|
return default_scheduler.get_jobs(tag)
|
||||||
|
|
||||||
|
|
||||||
|
def clear(tag: Optional[Hashable] = None) -> None:
|
||||||
|
"""Calls :meth:`clear <Scheduler.clear>` on the
|
||||||
|
:data:`default scheduler instance <default_scheduler>`.
|
||||||
|
"""
|
||||||
|
default_scheduler.clear(tag)
|
||||||
|
|
||||||
|
|
||||||
|
def cancel_job(job: Job) -> None:
|
||||||
|
"""Calls :meth:`cancel_job <Scheduler.cancel_job>` on the
|
||||||
|
:data:`default scheduler instance <default_scheduler>`.
|
||||||
|
"""
|
||||||
|
default_scheduler.cancel_job(job)
|
||||||
|
|
||||||
|
|
||||||
|
def next_run() -> Optional[datetime.datetime]:
|
||||||
|
"""Calls :meth:`next_run <Scheduler.next_run>` on the
|
||||||
|
:data:`default scheduler instance <default_scheduler>`.
|
||||||
|
"""
|
||||||
|
return default_scheduler.next_run
|
||||||
|
|
||||||
|
|
||||||
|
def idle_seconds() -> Optional[float]:
|
||||||
|
"""Calls :meth:`idle_seconds <Scheduler.idle_seconds>` on the
|
||||||
|
:data:`default scheduler instance <default_scheduler>`.
|
||||||
|
"""
|
||||||
|
return default_scheduler.idle_seconds
|
||||||
|
|
||||||
|
|
||||||
|
def repeat(job, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Decorator to schedule a new periodic job.
|
||||||
|
|
||||||
|
Any additional arguments are passed on to the decorated function
|
||||||
|
when the job runs.
|
||||||
|
|
||||||
|
:param job: a :class:`Jobs <Job>`
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _schedule_decorator(decorated_function):
|
||||||
|
job.do(decorated_function, *args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
return _schedule_decorator
|
Loading…
Reference in new issue