Skip to content

Commit

Permalink
Merge e379c80 into 6eb0b53
Browse files Browse the repository at this point in the history
  • Loading branch information
07pepa committed Apr 10, 2021
2 parents 6eb0b53 + e379c80 commit 446d451
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 2 deletions.
4 changes: 3 additions & 1 deletion AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Thanks to all the wonderful folks who have contributed to schedule over the year
- 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>
- sosolidkk <https://github.com/sosolidkk>
- 07pepa <https://github.com/07pepa>
50 changes: 50 additions & 0 deletions docs/async-support.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
Asyncio & Coroutines
====================

This example shows how to schedule an async job on the asyncio event loop.

.. code-block:: python
import schedule
import asyncio
async def job_async():
print("I am an async job")
await asyncio.sleep(1)
schedule.every(5).seconds.do(lambda: asyncio.create_task(job_async()))
async def scheduler():
while True:
await asyncio.sleep(1)
schedule.run_pending()
asyncio.run(scheduler())
The call to ``asyncio.create_task()`` submits the ``job_async`` coroutine to the asyncio event loop.
it may or may not run immediately, depending on the how busy the event loop is.

Sleeping the right amount of time
---------------------------------

The ``scheduler()`` can be optimized by sleeping *exactly* [1]_ the amount of time until the next job is scheduled to run:

.. code-block:: python
async def scheduler():
while 1:
seconds = schedule.idle_seconds()
if seconds is None:
# no more jobs, terminate
break
elif seconds > 0:
# sleep exactly the right amount of time
await asyncio.sleep(seconds)
schedule.run_pending()
asyncio.run(scheduler())
Keep in mind that if a new job is scheduled while sleeping, the new job's earliest run is whenever the sleep finishes.




.. rubric:: Footnotes
.. [1] `asyncio.sleep` may sleep little more then expected, depending on loop implementation of loop and system load.
157 changes: 157 additions & 0 deletions docs/asyncio-event-signals.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
Asyncio examples with events and signals
========================================


Those examples how python asyncio facilities may interact with schedule.
It will touch Terminate-able loop, handling signals and changing sleep time due scheduling during sleep.

Terminate-able loop
-------------
.. code-block:: python
import schedule
import asyncio
event=None
async def terminator():
print("I will be back")
await asyncio.sleep(1)
event.set()
schedule.every(5).seconds.do(lambda: asyncio.create_task(terminator()))
async def sleep(seconds):
if seconds is None:
return True #no more jobs, terminate
if seconds==0:
return False
try:
return await asyncio.wait_for(event.wait(), timeout=seconds)
except asyncio.TimeoutError:
return False
async def scheduler():
global event
event=asyncio.Event()
while not await sleep(schedule.idle_seconds()): # will sleep until next or exits
schedule.run_pending()
asyncio.run(scheduler())
Save Scheduling during sleep
-----------------------------
.. code-block:: python
import schedule
import asyncio
event = None
# this can even be called from another threads but you must have loop that schedule loop is running on
# it can be done like loop.call_soon_threadsafe(async_schedule,schedule_call)
# this is threadsafe thanks to cooperative multitasking (newer multiple async_schedule are running at same time)
def async_schedule(*schedule_calls):
next_run = schedule.next_run()
for call in schedule_calls:
call()
if next_run != schedule.next_run():
event.set()
def submit_once():
async def late_submit():
await asyncio.sleep(1)#submiting during sleep
print("submiting new task")
async_schedule(lambda: schedule.every(2).seconds.do(lambda: print("new task running")))
asyncio.create_task(late_submit())
return schedule.CancelJob
schedule.every(1).seconds.do(submit_once)
schedule.every(8).seconds.do(lambda: print("keep loop working"))
async def scheduler():
global event
event = asyncio.Event()
interrupt_task = asyncio.create_task(event.wait())
while 1:
seconds = schedule.idle_seconds()
if seconds is None:
break # no more jobs, terminate
elif seconds > 0:
done, _ = await asyncio.wait({interrupt_task}, timeout=seconds)
if interrupt_task in done: # someone set event
event.clear()
interrupt_task = asyncio.create_task(event.wait()) #start monitoring again
continue # re-schedule
schedule.run_pending()
asyncio.run(scheduler())
Termination on os signals
--------------------------
.. code-block:: python
import schedule
import asyncio
print(schedule.every().wednesday.do(lambda: print("It is Wednesday...")))
print(schedule.every().sunday.do(lambda: asyncio.create_task(something_useful)))
terminateEvent = None
def setup(): #needs to run on asyncio loop and setup on main thread
import signal
global terminateEvent
terminateEvent = asyncio.Event() #needs to run on asyncio loop
def terminate(signum):
print(f"sayonara, I received {signum}")
terminateEvent.set() # kill loop
loop = asyncio.get_running_loop() #needs to run on asyncio loop
def handler(signum): # this is universal
global handler # allows self modifying code
try: #runtime probe if this python have add_signal_handler defined
loop.add_signal_handler(signum, lambda:terminate(signum))
def simplified(signum):#yay it has lets remove try catch to simplify it on next run
loop.add_signal_handler(signum, lambda:terminate(signum))
handler=simplified
except NotImplementedError: #not defined lets use self modifying code to execute correct version on next run
def backup_handler(signum):
orig_impl=signal.getsignal(signalnum)
if not orig_impl:
orig_impl = signal.SIG_IGN
def wrapper(sig, frame):
terminate(sig)
orig_impl(sig, frame)
signal.signal(signum, wrapper)
handler = backup_handler
backup_handler(signum)
finally:
handler.__name__="handler"
handler(signal.SIGINT)
async def sleep(seconds):
if seconds is None:
return True #no more jobs, terminate
if seconds==0:
return False
try:
return await asyncio.wait_for(event.wait(), timeout=seconds)
except asyncio.TimeoutError:
return False
async def main():
setup()
while not await sleep(schedule.idle_seconds()):
schedule.run_pending()
asyncio.run(main())
24 changes: 24 additions & 0 deletions docs/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,30 @@ How to continuously run the scheduler without blocking the main thread?
-----------------------------------------------------------------------
:doc:`Background Execution<background-execution>`.

Does schedule support asyncio coroutines?
-----------------------------------------
Schedule does not accept coroutines as jobs directly yet.
However, using using ``asyncio.create_task`` you able to schedule async jobs.
See :doc:`this page <async-support>` for an example.


Can i use threading.Event.wait as means of scheduling and Terminating loop with loong sleep?
----------------------------------------------------------------------------------------
Only in cases when you are running scheduling loop in separate thread
you also must make sure that you are not blocking main thread with joins/waiting on event etc.
This is because python will not handle any signals
However you can use ``asyncio.Event`` See :doc:`this page <async-events.signals>`

My long sleeping scheduler is not reacting on ctr+c
---------------------------------------------------
In your code or other dependencies there is somewhere overridden signal handler
without also calling previous implementation that means KeyboardInterrupt is no longer thrown
so you will have to override their signal handler with your that exits loop and execute their implementation if needed.

or you may be blocking main thread.

See :doc:`this page <async-events.signals>`.

Another question?
-----------------
If you are left with an unanswered question, `browse the issue tracker <http://github.com/dbader/schedule/issues>`_ to see if your question has been asked before.
Expand Down
4 changes: 3 additions & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ Python job scheduling for humans. Run Python functions (or any other callable) p
- 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.
- Works with :doc:`asyncio coroutines<async-support>`
- Excellent test coverage.
- Tested on Python 3.6, 3.7, 3.8 and 3.9


:doc:`Example <examples>`
:doc:`Example<examples>`
-------------------------

.. code-block:: bash
Expand Down Expand Up @@ -75,6 +76,7 @@ Read More
examples
background-execution
parallel-execution
asyncio
exception-handling
logging
multiple-schedulers
Expand Down

0 comments on commit 446d451

Please sign in to comment.