Repository: dbader/schedule
Branch: master
Commit: 82a43db1b938
Files: 32
Total size: 162.5 KB
Directory structure:
gitextract_ic2muaty/
├── .github/
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── AUTHORS.rst
├── HISTORY.rst
├── LICENSE.txt
├── MANIFEST.in
├── README.rst
├── docs/
│ ├── Makefile
│ ├── _static/
│ │ └── custom.css
│ ├── _templates/
│ │ └── sidebarintro.html
│ ├── background-execution.rst
│ ├── changelog.rst
│ ├── conf.py
│ ├── development.rst
│ ├── examples.rst
│ ├── exception-handling.rst
│ ├── faq.rst
│ ├── index.rst
│ ├── installation.rst
│ ├── logging.rst
│ ├── multiple-schedulers.rst
│ ├── parallel-execution.rst
│ ├── reference.rst
│ └── timezones.rst
├── pyproject.toml
├── requirements-dev.txt
├── schedule/
│ ├── __init__.py
│ └── py.typed
├── setup.cfg
├── setup.py
├── test_schedule.py
└── tox.ini
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/ci.yml
================================================
name: Tests
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
test:
runs-on: ubuntu-latest
strategy:
max-parallel: 6
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: pip install tox tox-gh-actions
- name: Tests
run: tox
- name: Coveralls
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_FLAG_NAME: python-${{ matrix.python-version }}
COVERALLS_PARALLEL: true
run: |
pip3 install coveralls
coveralls --service=github
coveralls:
# Notify coveralls that the built has finished so they can
# combine the results and post a comment with the summary.
name: coverage push
needs: test
runs-on: ubuntu-latest
steps:
- name: Set up Python 3.11
uses: actions/setup-python@v4
with:
python-version: 3.11
- name: Finished
run: |
pip3 install coveralls
coveralls --finish --service=github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.11
uses: actions/setup-python@v4
with:
python-version: 3.11
- name: Install dependencies
run: pip install tox
- name: Check docs
run: tox -e docs
formatting:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.11
uses: actions/setup-python@v4
with:
python-version: 3.11
- name: Install dependencies
run: pip install tox
- name: Check formatting
run: tox -e format
setuppy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.11
uses: actions/setup-python@v4
with:
python-version: 3.11
- name: Install dependencies
run: pip install tox
- name: Check docs
run: tox -e setuppy
================================================
FILE: .gitignore
================================================
*.py[cod]
# C extensions
*.so
# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64
MANIFEST
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
nosetests.xml
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
env
env3
__pycache__
venv
.cache
docs/_build
# For Idea (e.g. PyCharm) users
.idea
*.iml
================================================
FILE: AUTHORS.rst
================================================
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>
- rudSarkar <https://github.com/rudSarkar>
- chrimaho <https://github.com/chrimaho>
- jweijers <https://github.com/jweijers>
- Akuli <https://github.com/Akuli>
- NaelsonDouglas <https://github.com/NaelsonDouglas>
- SergBobrovsky <https://github.com/SergBobrovsky>
- CPickens42 <https://github.com/CPickens42>
- emollier <https://github.com/emollier>
- sunpro108 <https://github.com/sunpro108>
- kurtasov <https://github.com/kurtasov>
- AnezeR <https://github.com/AnezeR>
- a-detiste <https://github.com/a-detiste>
================================================
FILE: HISTORY.rst
================================================
.. :changelog:
History
-------
1.2.2 (2024-05-25)
++++++++++++++++++
- Fix bugs in cross-timezone scheduling (#601, #602, #604, #623)
- Add support for python 3.12 (#606)
- Remove dependency on old mock (#622) Thanks @a-detiste!
1.2.1 (2023-11-01)
++++++++++++++++++
- Fix bug where schedule was off when using .at with timezone (#583) Thanks @AnezeR!
1.2.0 (2023-04-10)
++++++++++++++++++
- Dropped support for Python 3.6, add support for Python 3.10 and 3.11.
- Add timezone support for .at(). See #517. Thanks @chrimaho!
- Get next run by tag (#463) Thanks @jweijers!
- Add py.typed file. See #521. Thanks @Akuli!
- Fix the re pattern of the 'days'. See #506 Thanks @sunpro108!
- Fix test_until_time failure when run early. See #563. Thanks @emollier!
- Fix crash repr on partially constructed job. See #569. Thanks @CPickens42!
- Code cleanup and modernization. See #567, #536. Thanks @masa-08 and @SergBobrovsky!
- Documentation improvements and fix typos. See #469, #479, #493, #519, #520. Thanks to @NaelsonDouglas, @chrimaho, @rudSarkar
1.1.0 (2021-04-09)
++++++++++++++++++
- Added @repeat() decorator. See #148. Thanks @rhagenaars!
- Added execute .until(). See #195. Thanks @fredthomsen!
- Added job retrieval filtered by tags using get_jobs('tag'). See #419. Thanks @skenvy!
- Added type annotations. See #427. Thanks @martinthoma!
- Bugfix: str() of job when there is no __name__. See #430. Thanks @biggerfisch!
- Improved error messages. See #280, #439. Thanks @connorskees and @sosolidkk!
- Improved logging. See #193. Thanks @zcking!
- Documentation improvements and fix typos. See #424, #435, #436, #453, #437, #448. Thanks @ebllg!
1.0.0 (2021-01-20)
++++++++++++++++++
Depending on your configuration, the following bugfixes might change schedule's behaviour:
- Fix: idle_seconds crashes when no jobs are scheduled. See #401. Thanks @yoonghm!
- Fix: day.at('HH:MM:SS') where HMS=now+10s doesn't run today. See #331. Thanks @qmorek!
- Fix: hour.at('MM:SS'), the seconds are set to 00. See #290. Thanks @eladbi!
- Fix: Long-running jobs skip a day when they finish in the next day #404. Thanks @4379711!
Other changes:
- Dropped Python 2.7 and 3.5 support, added 3.8 and 3.9 support. See #409
- Fix RecursionError when the job is passed to the do function as an arg. See #190. Thanks @connorskees!
- Fix DeprecationWarning of 'collections'. See #296. Thanks @gaguirregabiria!
- Replaced Travis with Github Actions for automated testing
- Revamp and extend documentation. See #395
- Improved tests. Thanks @connorskees and @Jamim!
- Changed log messages to DEBUG level. Thanks @aisk!
0.6.0 (2019-01-20)
++++++++++++++++++
- Make at() accept timestamps with 1 second precision (#267). Thanks @NathanWailes!
- Introduce proper exception hierarchy (#271). Thanks @ConnorSkees!
0.5.0 (2017-11-16)
++++++++++++++++++
- Keep partially scheduled jobs from breaking the scheduler (#125)
- Add support for random intervals (Thanks @grampajoe and @gilbsgilbs)
0.4.3 (2017-06-10)
++++++++++++++++++
- Improve docs & clean up docstrings
0.4.2 (2016-11-29)
++++++++++++++++++
- Publish to PyPI as a universal (py2/py3) wheel
0.4.0 (2016-11-28)
++++++++++++++++++
- Add proper HTML (Sphinx) docs available at https://schedule.readthedocs.io/
- CI builds now run against Python 2.7 and 3.5 (3.3 and 3.4 should work fine but are untested)
- Fixed an issue with ``run_all()`` and having more than one job that deletes itself in the same iteration. Thanks @alaingilbert.
- Add ability to tag jobs and to cancel jobs by tag. Thanks @Zerrossetto.
- Improve schedule docs. Thanks @Zerrossetto.
- Additional docs fixes by @fkromer and @yetingsky.
0.3.2 (2015-07-02)
++++++++++++++++++
- Fixed issues where scheduling a job with a functools.partial as the job function fails. Thanks @dylwhich.
- Fixed an issue where scheduling a job to run every >= 2 days would cause the initial execution to happen one day early. Thanks @WoLfulus for identifying this and providing a fix.
- Added a FAQ item to describe how to schedule a job that runs only once.
0.3.1 (2014-09-03)
++++++++++++++++++
- Fixed an issue with unicode handling in setup.py that was causing trouble on Python 3 and Debian (https://github.com/dbader/schedule/issues/27). Thanks to @waghanza for reporting it.
- Added an FAQ item to describe how to deal with job functions that throw exceptions. Thanks @mplewis.
0.3.0 (2014-06-14)
++++++++++++++++++
- Added support for scheduling jobs on specific weekdays. Example: ``schedule.every().tuesday.do(job)`` or ``schedule.every().wednesday.at("13:15").do(job)`` (Thanks @abultman.)
- Run tests against Python 2.7 and 3.4. Python 3.3 should continue to work but we're not actively testing it on CI anymore.
0.2.1 (2013-11-20)
++++++++++++++++++
- Fixed history (no code changes).
0.2.0 (2013-11-09)
++++++++++++++++++
- This release introduces two new features in a backwards compatible way:
- Allow jobs to cancel repeated execution: Jobs can be cancelled by calling ``schedule.cancel_job()`` or by returning ``schedule.CancelJob`` from the job function. (Thanks to @cfrco and @matrixise.)
- Updated ``at_time()`` to allow running jobs at a particular time every hour. Example: ``every().hour.at(':15').do(job)`` will run ``job`` 15 minutes after every full hour. (Thanks @mattss.)
- Refactored unit tests to mock ``datetime`` in a cleaner way. (Thanks @matts.)
0.1.11 (2013-07-30)
+++++++++++++++++++
- Fixed an issue with ``next_run()`` throwing a ``ValueError`` exception when the job queue is empty. Thanks to @dpagano for pointing this out and thanks to @mrhwick for quickly providing a fix.
0.1.10 (2013-06-07)
+++++++++++++++++++
- Fixed issue with ``at_time`` jobs not running on the same day the job is created (Thanks to @mattss)
0.1.9 (2013-05-27)
++++++++++++++++++
- Added ``schedule.next_run()``
- Added ``schedule.idle_seconds()``
- Args passed into ``do()`` are forwarded to the job function at call time
- Increased test coverage to 100%
0.1.8 (2013-05-21)
++++++++++++++++++
- Changed default ``delay_seconds`` for ``schedule.run_all()`` to 0 (from 60)
- Increased test coverage
0.1.7 (2013-05-20)
++++++++++++++++++
- API change: renamed ``schedule.run_all_jobs()`` to ``schedule.run_all()``
- API change: renamed ``schedule.run_pending_jobs()`` to ``schedule.run_pending()``
- API change: renamed ``schedule.clear_all_jobs()`` to ``schedule.clear()``
- Added ``schedule.jobs``
0.1.6 (2013-05-20)
++++++++++++++++++
- Fix packaging
- README fixes
0.1.4 (2013-05-20)
++++++++++++++++++
- API change: renamed ``schedule.tick()`` to ``schedule.run_pending_jobs()``
- Updated README and ``setup.py`` packaging
0.1.0 (2013-05-19)
++++++++++++++++++
- Initial release
================================================
FILE: LICENSE.txt
================================================
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.
================================================
FILE: MANIFEST.in
================================================
include README.rst
include HISTORY.rst
include LICENSE.txt
include test_schedule.py
recursive-exclude * __pycache__
recursive-exclude * *.py[co]
================================================
FILE: README.rst
================================================
`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.7, 3.8, 3.9, 3.10, 3.11, 3.12
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().day.at("12:42", "Europe/Amsterdam").do(job)
schedule.every().minute.at(":17").do(job)
def job_with_argument(name):
print(f"I am {name}")
schedule.every(10).seconds.do(job_with_argument, name="Peter")
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
================================================
FILE: docs/Makefile
================================================
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " applehelp to make an Apple Help Book"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " epub3 to make an epub3"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
@echo " dummy to check syntax errors of document sources"
.PHONY: clean
clean:
rm -rf $(BUILDDIR)/*
.PHONY: html
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
.PHONY: dirhtml
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
.PHONY: singlehtml
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
.PHONY: pickle
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
.PHONY: json
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
.PHONY: htmlhelp
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
.PHONY: qthelp
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/schedule.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/schedule.qhc"
.PHONY: applehelp
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
@echo "N.B. You won't be able to view it unless you put it in" \
"~/Library/Documentation/Help or install it in your application" \
"bundle."
.PHONY: devhelp
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/schedule"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/schedule"
@echo "# devhelp"
.PHONY: epub
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
.PHONY: epub3
epub3:
$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
@echo
@echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
.PHONY: latex
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
.PHONY: latexpdf
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: latexpdfja
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: text
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
.PHONY: man
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
.PHONY: texinfo
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
.PHONY: info
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
.PHONY: gettext
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
.PHONY: changes
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
.PHONY: linkcheck
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
.PHONY: doctest
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
.PHONY: coverage
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
.PHONY: xml
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
.PHONY: pseudoxml
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
.PHONY: dummy
dummy:
$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
@echo
@echo "Build finished. Dummy builder generates no files."
================================================
FILE: docs/_static/custom.css
================================================
.toctree-l1 {
padding-bottom: 4px;
}
================================================
FILE: docs/_templates/sidebarintro.html
================================================
<h3>📰 Useful Links</h3>
<ul>
<li><a href="http://github.com/dbader/schedule">Schedule @ GitHub</a></li>
<li><a href="http://pypi.python.org/pypi/schedule">Schedule @ PyPI</a></li>
<li><a href="http://github.com/dbader/schedule/issues">Issue Tracker</a></li>
</ul>
<h3>🐍 More Python</h3>
<p><a href="https://twitter.com/dbader_org" class="twitter-follow-button" data-show-count="false">Follow @dbader_org</a> <script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');</script></p>
<ul>
<li><a href="https://dbader.org/screencasts/">Dan's Python Screencasts</a></li>
<li><a href="https://dbader.org/newsletter">Dan's Python Newsletter</a></li>
<li><a href="https://dbader.org/">Dan's Python Tutorials</a></li>
</ul>
================================================
FILE: docs/background-execution.rst
================================================
Run in the background
=====================
Out of the box it is not possible to run the schedule in the background.
However, you can create a thread yourself and use it to run jobs without blocking the main thread.
This is an example of how you could do this:
.. code-block:: python
import threading
import time
import schedule
def run_continuously(interval=1):
"""Continuously run, while executing pending jobs at each
elapsed time interval.
@return cease_continuous_run: threading. Event which can
be set to cease continuous run. Please note that it is
*intended behavior that run_continuously() does not run
missed jobs*. For example, if you've registered a job that
should run every minute and you set a continuous run
interval of one hour then your job won't be run 60 times
at each interval but only once.
"""
cease_continuous_run = threading.Event()
class ScheduleThread(threading.Thread):
@classmethod
def run(cls):
while not cease_continuous_run.is_set():
schedule.run_pending()
time.sleep(interval)
continuous_thread = ScheduleThread()
continuous_thread.start()
return cease_continuous_run
def background_job():
print('Hello from the background thread')
schedule.every().second.do(background_job)
# Start the background thread
stop_run_continuously = run_continuously()
# Do some other things...
time.sleep(10)
# Stop the background thread
stop_run_continuously.set()
================================================
FILE: docs/changelog.rst
================================================
.. include:: ../HISTORY.rst
================================================
FILE: docs/conf.py
================================================
# -*- coding: utf-8 -*-
#
# schedule documentation build configuration file, created by
# sphinx-quickstart on Mon Nov 7 15:14:48 2016.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# (schedule modules lives up one level from docs/)
#
import os
import sys
sys.path.insert(0, os.path.abspath(".."))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.todo",
"sphinx.ext.coverage",
"sphinx.ext.viewcode",
# 'sphinx.ext.githubpages', # This breaks the ReadTheDocs build
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = ".rst"
# The encoding of source files.
#
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = "index"
# General information about the project.
project = u"schedule"
copyright = u'2020, <a href="https://dbader.org/">Daniel Bader</a>'
author = u'<a href="https://dbader.org/">Daniel Bader</a>'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = u"1.2.2"
# The full version, including alpha/beta/rc tags.
release = u"1.2.2"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = "en"
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#
# today = ''
#
# Else, today_fmt is used as the format for a strftime call.
#
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# The reST default role (used for this markup: `text`) to use for all
# documents.
#
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
# pygments_style = 'flask_theme_support.FlaskyStyle'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
# keep_warnings = False
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "alabaster"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
html_theme_options = {
"show_powered_by": False,
"github_user": "dbader",
"github_repo": "schedule",
"github_banner": True,
"github_button": True,
"github_type": "star",
"show_related": False,
}
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
# The name for this set of Sphinx documents.
# "<project> v<release> documentation" by default.
#
# html_title = u'schedule v0.4.0'
# A shorter title for the navigation bar. Default is the same as html_title.
#
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#
# html_logo = None
# The name of an image file (relative to this directory) to use as a favicon of
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#
# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#
# html_extra_path = []
# If not None, a 'Last updated on:' timestamp is inserted at every page
# bottom, using the given strftime format.
# The empty string is equivalent to '%b %d, %Y'.
#
# html_last_updated_fmt = None
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#
html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#
html_sidebars = {
"**": [
"about.html",
"globaltoc.html",
"sidebarintro.html",
"relations.html",
"searchbox.html",
],
}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#
# html_additional_pages = {}
# If false, no module index is generated.
#
# html_domain_indices = True
# If false, no index is generated.
#
# html_use_index = True
# If true, the index is split into individual pages for each letter.
#
# html_split_index = False
# If true, links to the reST sources are added to the pages.
#
html_show_sourcelink = False
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#
html_show_sphinx = False
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#
html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None
# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh'
#
# html_search_language = 'en'
# A dictionary with options for the search language support, empty by default.
# 'ja' uses this config value.
# 'zh' user can custom change `jieba` dictionary path.
#
# html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
#
# html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = "scheduledoc"
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(
master_doc,
"schedule.tex",
u"schedule Documentation",
u"Daniel Bader",
"manual",
),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#
# latex_use_parts = False
# If true, show page references after internal links.
#
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
#
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
#
# latex_appendices = []
# It false, will not define \strong, \code, itleref, \crossref ... but only
# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added
# packages.
#
# latex_keep_old_macro_names = True
# If false, no module index is generated.
#
# latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [(master_doc, "schedule", u"schedule Documentation", [author], 1)]
# If true, show URL addresses after external links.
#
# man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(
master_doc,
"schedule",
u"schedule Documentation",
author,
"schedule",
"One line description of project.",
"Miscellaneous",
),
]
# Documents to append as an appendix to all manuals.
#
# texinfo_appendices = []
# If false, no module index is generated.
#
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#
# texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#
# texinfo_no_detailmenu = False
autodoc_member_order = "bysource"
# We're pulling in some external images like CI badges.
suppress_warnings = ["image.nonlocal_uri"]
================================================
FILE: docs/development.rst
================================================
Development
===========
These instructions are geared towards people who want to help develop this library.
Preparing for development
-------------------------
All required tooling and libraries can be installed using the ``requirements-dev.txt`` file:
.. code-block:: bash
pip install -r requirements-dev.txt
Running tests
-------------
``pytest`` is used to run tests. Run all tests with coverage and formatting checks:
.. code-block:: bash
py.test test_schedule.py --flake8 schedule -v --cov schedule --cov-report term-missing
Formatting the code
-------------------
This project uses `black formatter <https://black.readthedocs.io/en/stable/>`_.
To format the code, run:
.. code-block:: bash
black .
Make sure you use version 20.8b1 of black.
Compiling documentation
-----------------------
The documentation is written in `reStructuredText <https://docutils.sourceforge.io/rst.html>`_.
It is processed using `Sphinx <http://www.sphinx-doc.org/en/1.4.8/tutorial.html>`_ using the `alabaster <https://alabaster.readthedocs.io/en/latest/>`_ theme.
After installing the development requirements it is just a matter of running:
.. code-block:: bash
cd docs
make html
The resulting html can be found in ``docs/_build/html``
Publish a new version
---------------------
Update the ``HISTORY.rst`` and ``AUTHORS.rst`` files.
Bump the version in ``setup.py`` and ``docs/conf.py``.
Merge these changes into master. Finally:
.. code-block:: bash
git tag X.Y.Z -m "Release X.Y.Z"
git push --tags
pip install --upgrade setuptools twine wheel
python3 -m build --wheel
# For https://test.pypi.org/project/schedule/
twine upload --repository schedule-test dist/*
# For https://pypi.org/project/schedule/
twine upload --repository schedule dist/*
This project follows `semantic versioning <https://semver.org/>`_.`
================================================
FILE: docs/examples.rst
================================================
Examples
========
Eager to get started? This page gives a good introduction to Schedule.
It assumes you already have Schedule installed. If you do not, head over to :doc:`installation`.
Run a job every x minute
~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
import schedule
import time
def job():
print("I'm working...")
# Run job every 3 second/minute/hour/day/week,
# Starting 3 second/minute/hour/day/week from now
schedule.every(3).seconds.do(job)
schedule.every(3).minutes.do(job)
schedule.every(3).hours.do(job)
schedule.every(3).days.do(job)
schedule.every(3).weeks.do(job)
# Run job every minute at the 23rd second
schedule.every().minute.at(":23").do(job)
# Run job every hour at the 42nd minute
schedule.every().hour.at(":42").do(job)
# Run jobs every 5th hour, 20 minutes and 30 seconds in.
# If current time is 02:00, first execution is at 06:20:30
schedule.every(5).hours.at("20:30").do(job)
# Run job every day at specific HH:MM and next HH:MM:SS
schedule.every().day.at("10:30").do(job)
schedule.every().day.at("10:30:42").do(job)
schedule.every().day.at("12:42", "Europe/Amsterdam").do(job)
# Run job on a specific day of the week
schedule.every().monday.do(job)
schedule.every().wednesday.at("13:15").do(job)
while True:
schedule.run_pending()
time.sleep(1)
Use a decorator to schedule a job
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Use the ``@repeat`` to schedule a function.
Pass it an interval using the same syntax as above while omitting the ``.do()``.
.. code-block:: python
from schedule import every, repeat, run_pending
import time
@repeat(every(10).minutes)
def job():
print("I am a scheduled job")
while True:
run_pending()
time.sleep(1)
The ``@repeat`` decorator does not work on non-static class methods.
Pass arguments to a job
~~~~~~~~~~~~~~~~~~~~~~~
``do()`` passes extra arguments to the job function
.. code-block:: python
import schedule
def greet(name):
print('Hello', name)
schedule.every(2).seconds.do(greet, name='Alice')
schedule.every(4).seconds.do(greet, name='Bob')
from schedule import every, repeat
@repeat(every().second, "World")
@repeat(every().day, "Mars")
def hello(planet):
print("Hello", planet)
Cancel a job
~~~~~~~~~~~~
To remove a job from the scheduler, use the ``schedule.cancel_job(job)`` method
.. code-block:: python
import schedule
def some_task():
print('Hello world')
job = schedule.every().day.at('22:30').do(some_task)
schedule.cancel_job(job)
Run a job once
~~~~~~~~~~~~~~
Return ``schedule.CancelJob`` from a job to remove it from the scheduler.
.. code-block:: python
import schedule
import time
def job_that_executes_once():
# Do some work that only needs to happen once...
return schedule.CancelJob
schedule.every().day.at('22:30').do(job_that_executes_once)
while True:
schedule.run_pending()
time.sleep(1)
Get all jobs
~~~~~~~~~~~~
To retrieve all jobs from the scheduler, use ``schedule.get_jobs()``
.. code-block:: python
import schedule
def hello():
print('Hello world')
schedule.every().second.do(hello)
all_jobs = schedule.get_jobs()
Cancel all jobs
~~~~~~~~~~~~~~~
To remove all jobs from the scheduler, use ``schedule.clear()``
.. code-block:: python
import schedule
def greet(name):
print('Hello {}'.format(name))
schedule.every().second.do(greet)
schedule.clear()
Get several jobs, filtered by tags
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can retrieve a group of jobs from the scheduler, selecting them by a unique identifier.
.. code-block:: python
import schedule
def greet(name):
print('Hello {}'.format(name))
schedule.every().day.do(greet, 'Andrea').tag('daily-tasks', 'friend')
schedule.every().hour.do(greet, 'John').tag('hourly-tasks', 'friend')
schedule.every().hour.do(greet, 'Monica').tag('hourly-tasks', 'customer')
schedule.every().day.do(greet, 'Derek').tag('daily-tasks', 'guest')
friends = schedule.get_jobs('friend')
Will return a list of every job tagged as ``friend``.
Cancel several jobs, filtered by tags
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can cancel the scheduling of a group of jobs selecting them by a unique identifier.
.. code-block:: python
import schedule
def greet(name):
print('Hello {}'.format(name))
schedule.every().day.do(greet, 'Andrea').tag('daily-tasks', 'friend')
schedule.every().hour.do(greet, 'John').tag('hourly-tasks', 'friend')
schedule.every().hour.do(greet, 'Monica').tag('hourly-tasks', 'customer')
schedule.every().day.do(greet, 'Derek').tag('daily-tasks', 'guest')
schedule.clear('daily-tasks')
Will prevent every job tagged as ``daily-tasks`` from running again.
Run a job at random intervals
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
def my_job():
print('Foo')
# Run every 5 to 10 seconds.
schedule.every(5).to(10).seconds.do(my_job)
``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
import schedule
from datetime import datetime, timedelta, time
def job():
print('Boo')
# run job until a 18:30 today
schedule.every(1).hours.until("18:30").do(job)
# run job until a 2030-01-01 18:33 today
schedule.every(1).hours.until("2030-01-01 18:33").do(job)
# Schedule a job to run for the next 8 hours
schedule.every(1).hours.until(timedelta(hours=8)).do(job)
# Run my_job until today 11:33:42
schedule.every(1).hours.until(time(11, 33, 42)).do(job)
# run job until a specific datetime
schedule.every(1).hours.until(datetime(2020, 5, 17, 11, 36, 20)).do(job)
The ``until`` method sets the jobs deadline. The job will not run after the deadline.
Time until the next execution
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Use ``schedule.idle_seconds()`` to get the number of seconds until the next job is scheduled to run.
The returned value is negative if the next scheduled jobs was scheduled to run in the past.
Returns ``None`` if no jobs are scheduled.
.. code-block:: python
import schedule
import time
def job():
print('Hello')
schedule.every(5).seconds.do(job)
while 1:
n = schedule.idle_seconds()
if n is None:
# no more jobs
break
elif n > 0:
# sleep exactly the right amount of time
time.sleep(n)
schedule.run_pending()
Run all jobs now, regardless of their scheduling
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To run all jobs regardless if they are scheduled to run or not, use ``schedule.run_all()``.
Jobs are re-scheduled after finishing, just like they would if they were executed using ``run_pending()``.
.. code-block:: python
import schedule
def job_1():
print('Foo')
def job_2():
print('Bar')
schedule.every().monday.at("12:40").do(job_1)
schedule.every().tuesday.at("16:40").do(job_2)
schedule.run_all()
# Add the delay_seconds argument to run the jobs with a number
# of seconds delay in between.
schedule.run_all(delay_seconds=10)
================================================
FILE: docs/exception-handling.rst
================================================
Exception Handling
##################
Schedule doesn't catch exceptions that happen during job execution. Therefore any exceptions thrown during job execution will bubble up and interrupt schedule's run_xyz function.
If you want to guard against exceptions you can wrap your job function
in a decorator like this:
.. code-block:: python
import functools
def catch_exceptions(cancel_on_failure=False):
def catch_exceptions_decorator(job_func):
@functools.wraps(job_func)
def wrapper(*args, **kwargs):
try:
return job_func(*args, **kwargs)
except:
import traceback
print(traceback.format_exc())
if cancel_on_failure:
return schedule.CancelJob
return wrapper
return catch_exceptions_decorator
@catch_exceptions(cancel_on_failure=True)
def bad_task():
return 1 / 0
schedule.every(5).minutes.do(bad_task)
Another option would be to subclass Schedule like @mplewis did in `this example <https://gist.github.com/mplewis/8483f1c24f2d6259aef6>`_.
================================================
FILE: docs/faq.rst
================================================
Frequently Asked Questions
==========================
Frequently asked questions on the usage of schedule.
Did you get here using an 'old' link and expected to see more questions?
AttributeError: 'module' object has no attribute 'every'
--------------------------------------------------------
I'm getting
.. code-block:: text
AttributeError: 'module' object has no attribute 'every'
when I try to use schedule.
This happens if your code imports the wrong ``schedule`` module.
Make sure you don't have a ``schedule.py`` file in your project that overrides the ``schedule`` module provided by this library.
ModuleNotFoundError: No module named 'schedule'
-----------------------------------------------
It seems python can't find the schedule package. Let's check some common causes.
Did you install schedule? If not, follow :doc:`installation`. Validate installation:
* Did you install using pip? Run ``pip3 list | grep schedule``. This should return ``schedule 0.6.0`` (or a higher version number)
* Did you install using apt? Run ``dpkg -l | grep python3-schedule``. This should return something along the lines of ``python3-schedule 0.3.2-1.1 Job scheduling for humans (Python 3)`` (or a higher version number)
Are you used python 3 to install Schedule, and are running the script using python 3?
For example, if you installed schedule using a version of pip that uses Python 2, and your code runs in Python 3, the package won't be found.
In this case the solution is to install Schedule using pip3: ``pip3 install schedule``.
Are you using virtualenv? Check that you are running the script inside the same virtualenv where you installed schedule.
Is this problem occurring when running the program from inside and IDE like PyCharm or VSCode?
Try to run your program from a commandline outside of the IDE.
If it works there, the problem is with your IDE configuration.
It might be that your IDE uses a different Python interpreter installation.
Still having problems? Use Google and StackOverflow before submitting an issue.
ModuleNotFoundError: ModuleNotFoundError: No module named 'pytz'
----------------------------------------------------------------
This error happens when you try to set a timezone in ``.at()`` without having the `pytz <https://pypi.org/project/pytz/>`_ package installed.
Pytz is a required dependency when working with timezones.
To resolve this issue, install the ``pytz`` module by running ``pip install pytz``.
Does schedule support time zones?
---------------------------------
Yes! See :doc:`Timezones <timezones>`.
What if my task throws an exception?
------------------------------------
See :doc:`Exception Handling <exception-handling>`.
How can I run a job only once?
------------------------------
See :doc:`Examples <examples>`.
How can I cancel several jobs at once?
--------------------------------------
See :doc:`Examples <examples>`.
How to execute jobs in parallel?
--------------------------------
See :doc:`Parallel Execution <parallel-execution>`.
How to continuously run the scheduler without blocking the main thread?
-----------------------------------------------------------------------
:doc:`Background Execution<background-execution>`.
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.
Feel free to create a new issue if that's not the case. Thank you 😃
================================================
FILE: docs/index.rst
================================================
schedule
========
.. 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 3.7, 3.8, 3.9, 3.10, 3.11 and 3.12
:doc:`Example <examples>`
-------------------------
.. code-block:: bash
$ pip install schedule
.. code-block:: python
import schedule
import time
def job():
print("I'm working...")
schedule.every(10).minutes.do(job)
schedule.every().hour.do(job)
schedule.every().day.at("10:30").do(job)
schedule.every().monday.do(job)
schedule.every().wednesday.at("13:15").do(job)
schedule.every().day.at("12:42", "Europe/Amsterdam").do(job)
schedule.every().minute.at(":17").do(job)
while True:
schedule.run_pending()
time.sleep(1)
More :doc:`examples`
When **not** to use Schedule
----------------------------
Let's be honest, Schedule is not a 'one size fits all' scheduling library.
This library is designed to be a simple solution for simple scheduling problems.
You should probably look somewhere else if you need:
* Job persistence (remember schedule between restarts)
* Exact timing (sub-second precision execution)
* Concurrent execution (multiple threads)
* Localization (workdays or holidays)
**Schedule does not account for the time it takes for the job function to execute.**
To guarantee a stable execution schedule you need to move long-running jobs off the main-thread (where the scheduler runs).
See :doc:`parallel-execution` for a sample implementation.
Read More
---------
.. toctree::
:maxdepth: 2
installation
examples
background-execution
parallel-execution
timezones
exception-handling
logging
multiple-schedulers
faq
reference
development
.. toctree::
:maxdepth: 1
changelog
Issues
------
If you encounter any problems, please `file an issue <http://github.com/dbader/schedule/issues>`_ along with a detailed description.
Please also use the search feature in the issue tracker beforehand to avoid creating duplicates. Thank you 😃
About Schedule
--------------
Created by `Daniel Bader <https://dbader.org/>`__ - `@dbader_org <https://twitter.com/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`` for more information.
.. include:: ../AUTHORS.rst
================================================
FILE: docs/installation.rst
================================================
Installation
============
Python version support
######################
We recommend using the latest version of Python.
Schedule is tested on Python 3.7, 3.8, 3.9, 3.10, 3.11 and 3.12
Want to use Schedule on earlier Python versions? See the History.
Dependencies
############
Schedule has 1 optional dependency:
Only when you use ``.at()`` with a timezone, you must have `pytz <https://pypi.org/project/pytz/>`_ installed.
Installation instructions
#########################
Problems? Check out :doc:`faq`.
PIP (preferred)
***************
The recommended way to install this package is to use pip.
Use the following command to install it:
.. code-block:: bash
$ pip install schedule
Schedule is now installed.
Check out the :doc:`examples </examples>` or go to the :doc:`the documentation overview </index>`.
Using another package manager
*****************************
Schedule is available through some linux package managers.
These packages are not maintained by the maintainers of this project.
It cannot be guarantee that these packages are up-to-date (and will stay up-to-date) with the latest released version.
If you don't mind having an old version, you can use it.
Ubuntu
-------
**OUTDATED!** At the time of writing, the packages for 20.04LTS and below use version 0.3.2 (2015).
.. code-block:: bash
$ apt-get install python3-schedule
See `package page <https://packages.ubuntu.com/search?keywords=python3-schedule>`__.
Debian
------
**OUTDATED!** At the time of writing, the packages for buster and below use version 0.3.2 (2015).
.. code-block:: bash
$ apt-get install python3 schedule
See `package page <https://packages.debian.org/search?searchon=names&keywords=+python3-schedule>`__.
Arch
----
On the Arch Linux User repository (AUR) the package is available using the name `python-schedule`.
See the package page `here <https://aur.archlinux.org/packages/python-schedule/>`__.
For yay users, run:
.. code-block:: bash
$ yay -S python-schedule
Conda (Anaconda)
----------------
Schedule is `published <https://anaconda.org/conda-forge/schedule>`__ in conda (the Anaconda package manager).
For installation instructions, visit `the conda-forge Schedule repo <https://github.com/conda-forge/schedule-feedstock#installing-schedule>`__.
The release of Schedule on conda is maintained by the `conda-forge project <https://conda-forge.org/>`__.
Install manually
**************************
If you don't have access to a package manager or need more control, you can manually copy the library into your project.
This is easy as the schedule library consists of a single sourcefile MIT licenced.
However, this method is highly discouraged as you won't receive automatic updates.
1. Go to the `Github repo <https://github.com/dbader/schedule>`_.
2. Open file `schedule/__init__.py` and copy the code.
3. In your project, create a packaged named `schedule` and paste the code in a file named `__init__.py`.
================================================
FILE: docs/logging.rst
================================================
Logging
=======
Schedule logs messages to the Python logger named ``schedule`` at ``DEBUG`` level.
To receive logs from Schedule, set the logging level to ``DEBUG``.
.. code-block:: python
import schedule
import logging
logging.basicConfig()
schedule_logger = logging.getLogger('schedule')
schedule_logger.setLevel(level=logging.DEBUG)
def job():
print("Hello, Logs")
schedule.every().second.do(job)
schedule.run_all()
schedule.clear()
This will result in the following log messages:
.. code-block:: text
DEBUG:schedule:Running *all* 1 jobs with 0s delay in between
DEBUG:schedule:Running job Job(interval=1, unit=seconds, do=job, args=(), kwargs={})
Hello, Logs
DEBUG:schedule:Deleting *all* jobs
Customize logging
-----------------
The easiest way to add reusable logging to jobs is to implement a decorator that handles logging.
As an example, below code adds the ``print_elapsed_time`` decorator:
.. code-block:: python
import functools
import time
import schedule
# This decorator can be applied to any job function to log the elapsed time of each job
def print_elapsed_time(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_timestamp = time.time()
print('LOG: Running job "%s"' % func.__name__)
result = func(*args, **kwargs)
print('LOG: Job "%s" completed in %d seconds' % (func.__name__, time.time() - start_timestamp))
return result
return wrapper
@print_elapsed_time
def job():
print('Hello, Logs')
time.sleep(5)
schedule.every().second.do(job)
schedule.run_all()
This outputs:
.. code-block:: text
LOG: Running job "job"
Hello, Logs
LOG: Job "job" completed in 5 seconds
================================================
FILE: docs/multiple-schedulers.rst
================================================
Multiple schedulers
###################
You can run as many jobs from a single scheduler as you wish.
However, for larger installations it might be desirable to have multiple schedulers.
This is supported:
.. code-block:: python
import time
import schedule
def fooJob():
print("Foo")
def barJob():
print("Bar")
# Create a new scheduler
scheduler1 = schedule.Scheduler()
# Add jobs to the created scheduler
scheduler1.every().hour.do(fooJob)
scheduler1.every().hour.do(barJob)
# Create as many schedulers as you need
scheduler2 = schedule.Scheduler()
scheduler2.every().second.do(fooJob)
scheduler2.every().second.do(barJob)
while True:
# run_pending needs to be called on every scheduler
scheduler1.run_pending()
scheduler2.run_pending()
time.sleep(1)
================================================
FILE: docs/parallel-execution.rst
================================================
Parallel execution
==========================
*I am trying to execute 50 items every 10 seconds, but from the my logs it says it executes every item in 10 second schedule serially, is there a work around?*
By default, schedule executes all jobs serially. The reasoning behind this is that it would be difficult to find a model for parallel execution that makes everyone happy.
You can work around this limitation by running each of the jobs in its own thread:
.. code-block:: python
import threading
import time
import schedule
def job():
print("I'm running on thread %s" % threading.current_thread())
def run_threaded(job_func):
job_thread = threading.Thread(target=job_func)
job_thread.start()
schedule.every(10).seconds.do(run_threaded, job)
schedule.every(10).seconds.do(run_threaded, job)
schedule.every(10).seconds.do(run_threaded, job)
schedule.every(10).seconds.do(run_threaded, job)
schedule.every(10).seconds.do(run_threaded, job)
while 1:
schedule.run_pending()
time.sleep(1)
If you want tighter control on the number of threads use a shared jobqueue and one or more worker threads:
.. code-block:: python
import time
import threading
import schedule
import queue
def job():
print("I'm working")
def worker_main():
while 1:
job_func = jobqueue.get()
job_func()
jobqueue.task_done()
jobqueue = queue.Queue()
schedule.every(10).seconds.do(jobqueue.put, job)
schedule.every(10).seconds.do(jobqueue.put, job)
schedule.every(10).seconds.do(jobqueue.put, job)
schedule.every(10).seconds.do(jobqueue.put, job)
schedule.every(10).seconds.do(jobqueue.put, job)
worker_thread = threading.Thread(target=worker_main)
worker_thread.start()
while 1:
schedule.run_pending()
time.sleep(1)
This model also makes sense for a distributed application where the workers are separate processes that receive jobs from a distributed work queue. I like using beanstalkd with the beanstalkc Python library.
================================================
FILE: docs/reference.rst
================================================
Reference
=========
.. module:: schedule
This part of the documentation covers all the interfaces of schedule.
Main Interface
--------------
.. autodata:: default_scheduler
.. autodata:: jobs
.. autofunction:: every
.. autofunction:: run_pending
.. autofunction:: run_all
.. autofunction:: get_jobs
.. autofunction:: clear
.. autofunction:: cancel_job
.. autofunction:: next_run
.. autofunction:: idle_seconds
Classes
-------
.. autoclass:: schedule.Scheduler
:members:
:undoc-members:
.. autoclass:: schedule.Job
:members:
:undoc-members:
Exceptions
----------
.. autoexception:: schedule.CancelJob
================================================
FILE: docs/timezones.rst
================================================
Timezone & Daylight Saving Time
===============================
Timezone in .at()
~~~~~~~~~~~~~~~~~
Schedule supports setting the job execution time in another timezone using the ``.at`` method.
**To work with timezones** `pytz <https://pypi.org/project/pytz/>`_ **must be installed!** Get it:
.. code-block:: bash
pip install pytz
Timezones are only available in the ``.at`` function, like so:
.. code-block:: python
# Pass a timezone as a string
schedule.every().day.at("12:42", "Europe/Amsterdam").do(job)
# Pass an pytz timezone object
from pytz import timezone
schedule.every().friday.at("12:42", timezone("Africa/Lagos")).do(job)
Schedule uses the timezone to calculate the next runtime in local time.
All datetimes inside the library are stored `naive <https://docs.python.org/3/library/datetime.html>`_.
This causes the ``next_run`` and ``last_run`` to always be in Pythons local timezone.
Daylight Saving Time
~~~~~~~~~~~~~~~~~~~~
Scheduling jobs that do not specify a timezone do **not** take clock-changes into account.
Timezone unaware jobs will use naive local times to calculate the next run.
For example, a job that is set to run every 4 hours might execute after 3 realtime hours when DST goes into effect.
But when passing a timezone to ``.at()``, DST **is** taken into account.
The job will run at the specified time, even when the clock changes.
Example clock moves forward:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When a job is scheduled in the gap that occurs when the clock moves forward, the job is scheduled after the gap.
A job is scheduled ``.at("02:30", "Europe/Berlin")``.
When the clock moves from ``02:00`` to ``03:00``, the job will run once at ``03:30``.
The day after it will return to normal and run at ``02:30``.
A job is scheduled ``.at("01:00", "Europe/London")``.
When the clock moves from ``01:00`` to ``02:00``, the job will run once at ``02:00``.
Example clock moves backwards:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A job is scheduled ``.at("02:30", "Europe/Berlin")``.
When the clock moves from ``02:00`` to ``03:00``, the job will run once at ``02:30``.
It will run only at the first time the clock hits ``02:30``, but not the second time.
The day after, it will return to normal and run at ``02:30``.
Example scheduling across timezones
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Let's say we are in ``Europe/Berlin`` and local datetime is ``2022 march 20, 10:00:00``.
At this moment daylight saving time is not in effect in Berlin (UTC+1).
We schedule a job to run every day at 10:30:00 in America/New_York.
At this time, daylight saving time is in effect in New York (UTC-4).
.. code-block:: python
s = every().day.at("10:30", "America/New_York").do(job)
Because of the 5 hour time difference between Berlin and New York the job should effectively run at ``15:30:00``.
So the next run in Berlin time is ``2022 march 20, 15:30:00``:
.. code-block:: python
print(s.next_run)
# 2022-03-20 15:30:00
print(repr(s))
# Every 1 day at 10:30:00 do job() (last run: [never], next run: 2022-03-20 15:30:00)
================================================
FILE: pyproject.toml
================================================
[build-system]
requires = ["setuptools >= 61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "schedule"
description = "Job scheduling for humans."
dynamic = ["version", "classifiers", "keywords", "authors"]
readme = "README.rst"
license = {text = "MIT License"}
requires-python = ">= 3.7"
dependencies = []
maintainers = [
{name = "Sijmen Huizenga"}
]
[project.optional-dependencies]
timezone = ["pytz"]
[project.urls]
Documentation = "https://schedule.readthedocs.io"
Repository = "https://github.com/dbader/schedule.git"
Issues = "https://github.com/dbader/schedule/issues"
Changelog = "https://github.com/dbader/schedule/blob/master/HISTORY.rst"
================================================
FILE: requirements-dev.txt
================================================
docutils
Pygments
pytest
pytest-cov
pytest-flake8
Sphinx
black==20.8b1
click==8.0.4
mypy
pytz
types-pytz
================================================
FILE: schedule/__init__.py
================================================
"""
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.7, 3.8, 3.9, 3.10, 3.11 and 3.12
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:
"""
Can be returned from a job to unschedule itself.
"""
pass
class Scheduler:
"""
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)
def get_next_run(
self, tag: Optional[Hashable] = None
) -> Optional[datetime.datetime]:
"""
Datetime when the next job should run.
:param tag: Filter the next run for the given tag parameter
:return: A :class:`~datetime.datetime` object
or None if no jobs scheduled
"""
if not self.jobs:
return None
jobs_filtered = self.get_jobs(tag)
if not jobs_filtered:
return None
return min(jobs_filtered).next_run
next_run = property(get_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:
"""
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: Optional[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
# optional time zone of the self.at_time field. Only relevant when at_time is not None
self.at_time_zone = 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
# Weekday to run the job at. Only relevant when unit is 'weeks'.
# For example, when asking 'every week on tuesday' the start_day is 'tuesday'.
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)
if self.job_func is not None:
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) + ")"
else:
call_repr = "[None]"
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: str, tz: Optional[str] = None):
"""
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')`).
:param tz: The timezone that this timestamp refers to. Can be
a string that can be parsed by pytz.timezone(), or a pytz.BaseTzInfo object
: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 tz is not None:
import pytz
if isinstance(tz, str):
self.at_time_zone = pytz.timezone(tz) # type: ignore
elif isinstance(tz, pytz.BaseTzInfo):
self.at_time_zone = tz
else:
raise ScheduleValueError(
"Timezone must be string or pytz.timezone object"
)
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
hour = int(hour)
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
# Do all computation in the context of the requested timezone
now = datetime.datetime.now(self.at_time_zone)
next_run = now
if self.start_day is not None:
if self.unit != "weeks":
raise ScheduleValueError("`unit` should be 'weeks'")
next_run = _move_to_next_weekday(next_run, self.start_day)
if self.at_time is not None:
next_run = self._move_to_at_time(next_run)
period = datetime.timedelta(**{self.unit: interval})
if interval != 1:
next_run += period
while next_run <= now:
next_run += period
next_run = self._correct_utc_offset(
next_run, fixate_time=(self.at_time is not None)
)
# To keep the api consistent with older versions, we have to set the 'next_run' to a naive timestamp in the local timezone.
# Because we want to stay backwards compatible with older versions.
if self.at_time_zone is not None:
# Convert back to the local timezone
next_run = next_run.astimezone()
next_run = next_run.replace(tzinfo=None)
self.next_run = next_run
def _move_to_at_time(self, moment: datetime.datetime) -> datetime.datetime:
"""
Takes a datetime and moves the time-component to the job's at_time.
"""
if self.at_time is None:
return moment
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
moment = moment.replace(**kwargs) # type: ignore
# When we set the time elements, we might end up in a different UTC-offset than the current offset.
# This happens when we cross into or out of daylight saving time.
moment = self._correct_utc_offset(moment, fixate_time=True)
return moment
def _correct_utc_offset(
self, moment: datetime.datetime, fixate_time: bool
) -> datetime.datetime:
"""
Given a datetime, corrects any mistakes in the utc offset.
This is similar to pytz' normalize, but adds the ability to attempt
keeping the time-component at the same hour/minute/second.
"""
if self.at_time_zone is None:
return moment
# Normalize corrects the utc-offset to match the timezone
# For example: When a date&time&offset does not exist within a timezone,
# the normalization will change the utc-offset to where it is valid.
# It does this while keeping the moment in time the same, by moving the
# time component opposite of the utc-change.
offset_before_normalize = moment.utcoffset()
moment = self.at_time_zone.normalize(moment)
offset_after_normalize = moment.utcoffset()
if offset_before_normalize == offset_after_normalize:
# There was no change in the utc-offset, datetime didn't change.
return moment
# The utc-offset and time-component has changed
if not fixate_time:
# No need to fixate the time.
return moment
offset_diff = offset_after_normalize - offset_before_normalize
# Adjust the time to reset the date-time to have the same HH:mm components
moment -= offset_diff
# Check if moving the timestamp back by the utc-offset-difference made it end up
# in a moment that does not exist within the current timezone/utc-offset
re_normalized_offset = self.at_time_zone.normalize(moment).utcoffset()
if re_normalized_offset != offset_after_normalize:
# We ended up in a DST Gap. The requested 'at' time does not exist
# within the current timezone/utc-offset. As a best effort, we will
# schedule the job 1 offset later than possible.
# For example, if 02:23 does not exist (because DST moves from 02:00
# to 03:00), this will schedule the job at 03:23.
moment += offset_diff
return moment
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(tag: Optional[Hashable] = None) -> Optional[datetime.datetime]:
"""Calls :meth:`next_run <Scheduler.next_run>` on the
:data:`default scheduler instance <default_scheduler>`.
"""
return default_scheduler.get_next_run(tag)
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
def _move_to_next_weekday(moment: datetime.datetime, weekday: str):
"""
Move the given timestamp to the nearest given weekday. May be this week
or next week. If the timestamp is already at the given weekday, it is not
moved.
"""
weekday_index = _weekday_index(weekday)
days_ahead = weekday_index - moment.weekday()
if days_ahead < 0:
# Target day already happened this week, move to next week
days_ahead += 7
return moment + datetime.timedelta(days=days_ahead)
def _weekday_index(day: str) -> int:
weekdays = (
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
)
if day not in weekdays:
raise ScheduleValueError(
"Invalid start day (valid start days are {})".format(weekdays)
)
return weekdays.index(day)
================================================
FILE: schedule/py.typed
================================================
================================================
FILE: setup.cfg
================================================
[mypy]
files=schedule
================================================
FILE: setup.py
================================================
import codecs
from setuptools import setup
SCHEDULE_VERSION = "1.2.2"
SCHEDULE_DOWNLOAD_URL = "https://github.com/dbader/schedule/tarball/" + SCHEDULE_VERSION
def read_file(filename):
"""
Read a utf8 encoded text file and return its contents.
"""
with codecs.open(filename, "r", "utf8") as f:
return f.read()
setup(
name="schedule",
packages=["schedule"],
package_data={"schedule": ["py.typed"]},
version=SCHEDULE_VERSION,
description="Job scheduling for humans.",
long_description=read_file("README.rst"),
license="MIT",
author="Daniel Bader",
author_email="mail@dbader.org",
url="https://github.com/dbader/schedule",
download_url=SCHEDULE_DOWNLOAD_URL,
keywords=[
"schedule",
"periodic",
"jobs",
"scheduling",
"clockwork",
"cron",
"scheduler",
"job scheduling",
],
classifiers=[
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Development Status :: 5 - Production/Stable",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Natural Language :: English",
],
python_requires=">=3.7",
)
================================================
FILE: test_schedule.py
================================================
"""Unit tests for schedule.py"""
import datetime
import functools
from unittest import mock, TestCase
import os
import time
# Silence "missing docstring", "method could be a function",
# "class already defined", and "too many public methods" messages:
# pylint: disable-msg=R0201,C0111,E0102,R0904,R0901
import schedule
from schedule import (
every,
repeat,
ScheduleError,
ScheduleValueError,
IntervalError,
)
# POSIX TZ string format
TZ_BERLIN = "CET-1CEST,M3.5.0,M10.5.0/3"
TZ_AUCKLAND = "NZST-12NZDT,M9.5.0,M4.1.0/3"
TZ_CHATHAM = "<+1245>-12:45<+1345>,M9.5.0/2:45,M4.1.0/3:45"
TZ_UTC = "UTC0"
# Set timezone to Europe/Berlin (CEST) to ensure global reproducibility
os.environ["TZ"] = TZ_BERLIN
time.tzset()
def make_mock_job(name=None):
job = mock.Mock()
job.__name__ = name or "job"
return job
class mock_datetime:
"""
Monkey-patch datetime for predictable results
"""
def __init__(self, year, month, day, hour, minute, second=0, zone=None, fold=0):
self.year = year
self.month = month
self.day = day
self.hour = hour
self.minute = minute
self.second = second
self.zone = zone
self.fold = fold
self.original_datetime = None
self.original_zone = None
def __enter__(self):
class MockDate(datetime.datetime):
@classmethod
def today(cls):
return cls(self.year, self.month, self.day)
@classmethod
def now(cls, tz=None):
mock_date = cls(
self.year,
self.month,
self.day,
self.hour,
self.minute,
self.second,
fold=self.fold,
)
if tz:
return mock_date.astimezone(tz)
return mock_date
self.original_datetime = datetime.datetime
datetime.datetime = MockDate
self.original_zone = os.environ.get("TZ")
if self.zone:
os.environ["TZ"] = self.zone
time.tzset()
return MockDate(
self.year, self.month, self.day, self.hour, self.minute, self.second
)
def __exit__(self, *args, **kwargs):
datetime.datetime = self.original_datetime
if self.original_zone:
os.environ["TZ"] = self.original_zone
time.tzset()
class SchedulerTests(TestCase):
def setUp(self):
schedule.clear()
def make_tz_mock_job(self, name=None):
try:
import pytz
except ModuleNotFoundError:
self.skipTest("pytz unavailable")
return
return make_mock_job(name)
def test_time_units(self):
assert every().seconds.unit == "seconds"
assert every().minutes.unit == "minutes"
assert every().hours.unit == "hours"
assert every().days.unit == "days"
assert every().weeks.unit == "weeks"
job_instance = schedule.Job(interval=2)
# without a context manager, it incorrectly raises an error because
# it is not callable
with self.assertRaises(IntervalError):
job_instance.minute
with self.assertRaises(IntervalError):
job_instance.hour
with self.assertRaises(IntervalError):
job_instance.day
with self.assertRaises(IntervalError):
job_instance.week
with self.assertRaisesRegex(
IntervalError,
(
r"Scheduling \.monday\(\) jobs is only allowed for weekly jobs\. "
r"Using \.monday\(\) on a job scheduled to run every 2 or more "
r"weeks is not supported\."
),
):
job_instance.monday
with self.assertRaisesRegex(
IntervalError,
(
r"Scheduling \.tuesday\(\) jobs is only allowed for weekly jobs\. "
r"Using \.tuesday\(\) on a job scheduled to run every 2 or more "
r"weeks is not supported\."
),
):
job_instance.tuesday
with self.assertRaisesRegex(
IntervalError,
(
r"Scheduling \.wednesday\(\) jobs is only allowed for weekly jobs\. "
r"Using \.wednesday\(\) on a job scheduled to run every 2 or more "
r"weeks is not supported\."
),
):
job_instance.wednesday
with self.assertRaisesRegex(
IntervalError,
(
r"Scheduling \.thursday\(\) jobs is only allowed for weekly jobs\. "
r"Using \.thursday\(\) on a job scheduled to run every 2 or more "
r"weeks is not supported\."
),
):
job_instance.thursday
with self.assertRaisesRegex(
IntervalError,
(
r"Scheduling \.friday\(\) jobs is only allowed for weekly jobs\. "
r"Using \.friday\(\) on a job scheduled to run every 2 or more "
r"weeks is not supported\."
),
):
job_instance.friday
with self.assertRaisesRegex(
IntervalError,
(
r"Scheduling \.saturday\(\) jobs is only allowed for weekly jobs\. "
r"Using \.saturday\(\) on a job scheduled to run every 2 or more "
r"weeks is not supported\."
),
):
job_instance.saturday
with self.assertRaisesRegex(
IntervalError,
(
r"Scheduling \.sunday\(\) jobs is only allowed for weekly jobs\. "
r"Using \.sunday\(\) on a job scheduled to run every 2 or more "
r"weeks is not supported\."
),
):
job_instance.sunday
# test an invalid unit
job_instance.unit = "foo"
self.assertRaises(ScheduleValueError, job_instance.at, "1:0:0")
self.assertRaises(ScheduleValueError, job_instance._schedule_next_run)
# test start day exists but unit is not 'weeks'
job_instance.unit = "days"
job_instance.start_day = 1
self.assertRaises(ScheduleValueError, job_instance._schedule_next_run)
# test weeks with an invalid start day
job_instance.unit = "weeks"
job_instance.start_day = "bar"
self.assertRaises(ScheduleValueError, job_instance._schedule_next_run)
# test a valid unit with invalid hours/minutes/seconds
job_instance.unit = "days"
self.assertRaises(ScheduleValueError, job_instance.at, "25:00:00")
self.assertRaises(ScheduleValueError, job_instance.at, "00:61:00")
self.assertRaises(ScheduleValueError, job_instance.at, "00:00:61")
# test invalid time format
self.assertRaises(ScheduleValueError, job_instance.at, "25:0:0")
self.assertRaises(ScheduleValueError, job_instance.at, "0:61:0")
self.assertRaises(ScheduleValueError, job_instance.at, "0:0:61")
# test self.latest >= self.interval
job_instance.latest = 1
self.assertRaises(ScheduleError, job_instance._schedule_next_run)
job_instance.latest = 3
self.assertRaises(ScheduleError, job_instance._schedule_next_run)
def test_next_run_with_tag(self):
with mock_datetime(2014, 6, 28, 12, 0):
job1 = every(5).seconds.do(make_mock_job(name="job1")).tag("tag1")
job2 = every(2).hours.do(make_mock_job(name="job2")).tag("tag1", "tag2")
job3 = (
every(1)
.minutes.do(make_mock_job(name="job3"))
.tag("tag1", "tag3", "tag2")
)
assert schedule.next_run("tag1") == job1.next_run
assert schedule.default_scheduler.get_next_run("tag2") == job3.next_run
assert schedule.next_run("tag3") == job3.next_run
assert schedule.next_run("tag4") is None
def test_singular_time_units_match_plural_units(self):
assert every().second.unit == every().seconds.unit
assert every().minute.unit == every().minutes.unit
assert every().hour.unit == every().hours.unit
assert every().day.unit == every().days.unit
assert every().week.unit == every().weeks.unit
def test_time_range(self):
with mock_datetime(2014, 6, 28, 12, 0):
mock_job = make_mock_job()
# Choose a sample size large enough that it's unlikely the
# same value will be chosen each time.
minutes = set(
[
every(5).to(30).minutes.do(mock_job).next_run.minute
for i in range(100)
]
)
assert len(minutes) > 1
assert min(minutes) >= 5
assert max(minutes) <= 30
def test_time_range_repr(self):
mock_job = make_mock_job()
with mock_datetime(2014, 6, 28, 12, 0):
job_repr = repr(every(5).to(30).minutes.do(mock_job))
assert job_repr.startswith("Every 5 to 30 minutes do job()")
def test_at_time(self):
mock_job = make_mock_job()
assert every().day.at("10:30").do(mock_job).next_run.hour == 10
assert every().day.at("10:30").do(mock_job).next_run.minute == 30
assert every().day.at("20:59").do(mock_job).next_run.minute == 59
assert every().day.at("10:30:50").do(mock_job).next_run.second == 50
self.assertRaises(ScheduleValueError, every().day.at, "2:30:000001")
self.assertRaises(ScheduleValueError, every().day.at, "::2")
self.assertRaises(ScheduleValueError, every().day.at, ".2")
self.assertRaises(ScheduleValueError, every().day.at, "2")
self.assertRaises(ScheduleValueError, every().day.at, ":2")
self.assertRaises(ScheduleValueError, every().day.at, " 2:30:00")
self.assertRaises(ScheduleValueError, every().day.at, "59:59")
self.assertRaises(ScheduleValueError, every().do, lambda: 0)
self.assertRaises(TypeError, every().day.at, 2)
# without a context manager, it incorrectly raises an error because
# it is not callable
with self.assertRaises(IntervalError):
every(interval=2).second
with self.assertRaises(IntervalError):
every(interval=2).minute
with self.assertRaises(IntervalError):
every(interval=2).hour
with self.assertRaises(IntervalError):
every(interval=2).day
with self.assertRaises(IntervalError):
every(interval=2).week
with self.assertRaises(IntervalError):
every(interval=2).monday
with self.assertRaises(IntervalError):
every(interval=2).tuesday
with self.assertRaises(IntervalError):
every(interval=2).wednesday
with self.assertRaises(IntervalError):
every(interval=2).thursday
with self.assertRaises(IntervalError):
every(interval=2).friday
with self.assertRaises(IntervalError):
every(interval=2).saturday
with self.assertRaises(IntervalError):
every(interval=2).sunday
def test_until_time(self):
mock_job = make_mock_job()
# Check argument parsing
with mock_datetime(2020, 1, 1, 10, 0, 0) as m:
assert every().day.until(datetime.datetime(3000, 1, 1, 20, 30)).do(
mock_job
).cancel_after == datetime.datetime(3000, 1, 1, 20, 30, 0)
assert every().day.until(datetime.datetime(3000, 1, 1, 20, 30, 50)).do(
mock_job
).cancel_after == datetime.datetime(3000, 1, 1, 20, 30, 50)
assert every().day.until(datetime.time(12, 30)).do(
mock_job
).cancel_after == m.replace(hour=12, minute=30, second=0, microsecond=0)
assert every().day.until(datetime.time(12, 30, 50)).do(
mock_job
).cancel_after == m.replace(hour=12, minute=30, second=50, microsecond=0)
assert every().day.until(
datetime.timedelta(days=40, hours=5, minutes=12, seconds=42)
).do(mock_job).cancel_after == datetime.datetime(2020, 2, 10, 15, 12, 42)
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)
# Invalid argument types
self.assertRaises(TypeError, every().day.until, 123)
self.assertRaises(ScheduleValueError, every().day.until, "123")
self.assertRaises(ScheduleValueError, every().day.until, "01-01-3000")
# Using .until() with moments in the passed
self.assertRaises(
ScheduleValueError,
every().day.until,
datetime.datetime(2019, 12, 31, 23, 59),
)
self.assertRaises(
ScheduleValueError, every().day.until, datetime.timedelta(minutes=-1)
)
one_hour_ago = datetime.datetime.now() - datetime.timedelta(hours=1)
self.assertRaises(ScheduleValueError, every().day.until, one_hour_ago)
# Unschedule job after next_run passes the deadline
schedule.clear()
with mock_datetime(2020, 1, 1, 11, 35, 10):
mock_job.reset_mock()
every(5).seconds.until(datetime.time(11, 35, 20)).do(mock_job)
with mock_datetime(2020, 1, 1, 11, 35, 15):
schedule.run_pending()
assert mock_job.call_count == 1
assert len(schedule.jobs) == 1
with mock_datetime(2020, 1, 1, 11, 35, 20):
schedule.run_all()
assert mock_job.call_count == 2
assert len(schedule.jobs) == 0
# Unschedule job because current execution time has passed deadline
schedule.clear()
with mock_datetime(2020, 1, 1, 11, 35, 10):
mock_job.reset_mock()
every(5).seconds.until(datetime.time(11, 35, 20)).do(mock_job)
with mock_datetime(2020, 1, 1, 11, 35, 50):
schedule.run_pending()
assert mock_job.call_count == 0
assert len(schedule.jobs) == 0
def test_weekday_at_todady(self):
mock_job = make_mock_job()
# This date is a wednesday
with mock_datetime(2020, 11, 25, 22, 38, 5):
job = every().wednesday.at("22:38:10").do(mock_job)
assert job.next_run.hour == 22
assert job.next_run.minute == 38
assert job.next_run.second == 10
assert job.next_run.year == 2020
assert job.next_run.month == 11
assert job.next_run.day == 25
job = every().wednesday.at("22:39").do(mock_job)
assert job.next_run.hour == 22
assert job.next_run.minute == 39
assert job.next_run.second == 00
assert job.next_run.year == 2020
assert job.next_run.month == 11
assert job.next_run.day == 25
def test_at_time_hour(self):
with mock_datetime(2010, 1, 6, 12, 20):
mock_job = make_mock_job()
assert every().hour.at(":30").do(mock_job).next_run.hour == 12
assert every().hour.at(":30").do(mock_job).next_run.minute == 30
assert every().hour.at(":30").do(mock_job).next_run.second == 0
assert every().hour.at(":10").do(mock_job).next_run.hour == 13
assert every().hour.at(":10").do(mock_job).next_run.minute == 10
assert every().hour.at(":10").do(mock_job).next_run.second == 0
assert every().hour.at(":00").do(mock_job).next_run.hour == 13
assert every().hour.at(":00").do(mock_job).next_run.minute == 0
assert every().hour.at(":00").do(mock_job).next_run.second == 0
self.assertRaises(ScheduleValueError, every().hour.at, "2:30:00")
self.assertRaises(ScheduleValueError, every().hour.at, "::2")
self.assertRaises(ScheduleValueError, every().hour.at, ".2")
self.assertRaises(ScheduleValueError, every().hour.at, "2")
self.assertRaises(ScheduleValueError, every().hour.at, " 2:30")
self.assertRaises(ScheduleValueError, every().hour.at, "61:00")
self.assertRaises(ScheduleValueError, every().hour.at, "00:61")
self.assertRaises(ScheduleValueError, every().hour.at, "01:61")
self.assertRaises(TypeError, every().hour.at, 2)
# test the 'MM:SS' format
assert every().hour.at("30:05").do(mock_job).next_run.hour == 12
assert every().hour.at("30:05").do(mock_job).next_run.minute == 30
assert every().hour.at("30:05").do(mock_job).next_run.second == 5
assert every().hour.at("10:25").do(mock_job).next_run.hour == 13
assert every().hour.at("10:25").do(mock_job).next_run.minute == 10
assert every().hour.at("10:25").do(mock_job).next_run.second == 25
assert every().hour.at("00:40").do(mock_job).next_run.hour == 13
assert every().hour.at("00:40").do(mock_job).next_run.minute == 0
assert every().hour.at("00:40").do(mock_job).next_run.second == 40
def test_at_time_minute(self):
with mock_datetime(2010, 1, 6, 12, 20, 30):
mock_job = make_mock_job()
assert every().minute.at(":40").do(mock_job).next_run.hour == 12
assert every().minute.at(":40").do(mock_job).next_run.minute == 20
assert every().minute.at(":40").do(mock_job).next_run.second == 40
assert every().minute.at(":10").do(mock_job).next_run.hour == 12
assert every().minute.at(":10").do(mock_job).next_run.minute == 21
assert every().minute.at(":10").do(mock_job).next_run.second == 10
self.assertRaises(ScheduleValueError, every().minute.at, "::2")
self.assertRaises(ScheduleValueError, every().minute.at, ".2")
self.assertRaises(ScheduleValueError, every().minute.at, "2")
self.assertRaises(ScheduleValueError, every().minute.at, "2:30:00")
self.assertRaises(ScheduleValueError, every().minute.at, "2:30")
self.assertRaises(ScheduleValueError, every().minute.at, " :30")
self.assertRaises(TypeError, every().minute.at, 2)
def test_next_run_time(self):
with mock_datetime(2010, 1, 6, 12, 15):
mock_job = make_mock_job()
assert schedule.next_run() is None
assert every().minute.do(mock_job).next_run.minute == 16
assert every(5).minutes.do(mock_job).next_run.minute == 20
assert every().hour.do(mock_job).next_run.hour == 13
assert every().day.do(mock_job).next_run.day == 7
assert every().day.at("09:00").do(mock_job).next_run.day == 7
assert every().day.at("12:30").do(mock_job).next_run.day == 6
assert every().week.do(mock_job).next_run.day == 13
assert every().monday.do(mock_job).next_run.day == 11
assert every().tuesday.do(mock_job).next_run.day == 12
assert every().wednesday.do(mock_job).next_run.day == 13
assert every().thursday.do(mock_job).next_run.day == 7
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(datetime.time(12, 17)).do(mock_job).next_run.minute
== 16
)
def test_next_run_time_day_end(self):
mock_job = make_mock_job()
# At day 1, schedule job to run at daily 23:30
with mock_datetime(2010, 12, 1, 23, 0, 0):
job = every().day.at("23:30").do(mock_job)
# first occurrence same day
assert job.next_run.day == 1
assert job.next_run.hour == 23
# Running the job 01:00 on day 2, afterwards the job should be
# scheduled at 23:30 the same day. This simulates a job that started
# on day 1 at 23:30 and took 1,5 hours to finish
with mock_datetime(2010, 12, 2, 1, 0, 0):
job.run()
assert job.next_run.day == 2
assert job.next_run.hour == 23
# Run the job at 23:30 on day 2, afterwards the job should be
# scheduled at 23:30 the next day
with mock_datetime(2010, 12, 2, 23, 30, 0):
job.run()
assert job.next_run.day == 3
assert job.next_run.hour == 23
def test_next_run_time_hour_end(self):
try:
import pytz
except ModuleNotFoundError:
self.skipTest("pytz unavailable")
self.tst_next_run_time_hour_end(None, 0)
def test_next_run_time_hour_end_london(self):
try:
import pytz
except ModuleNotFoundError:
self.skipTest("pytz unavailable")
self.tst_next_run_time_hour_end("Europe/London", 0)
def test_next_run_time_hour_end_katmandu(self):
try:
import pytz
except ModuleNotFoundError:
self.skipTest("pytz unavailable")
# 12:00 in Berlin is 15:45 in Kathmandu
# this test schedules runs at :10 minutes, so job runs at
# 16:10 in Kathmandu, which is 13:25 in Berlin
# in local time we don't run at :10, but at :25, offset of 15 minutes
self.tst_next_run_time_hour_end("Asia/Kathmandu", 15)
def tst_next_run_time_hour_end(self, tz, offsetMinutes):
mock_job = make_mock_job()
# So a job scheduled to run at :10 in Kathmandu, runs always 25 minutes
with mock_datetime(2010, 10, 10, 12, 0, 0):
job = every().hour.at(":10", tz).do(mock_job)
assert job.next_run.hour == 12
assert job.next_run.minute == 10 + offsetMinutes
with mock_datetime(2010, 10, 10, 13, 0, 0):
job.run()
assert job.next_run.hour == 13
assert job.next_run.minute == 10 + offsetMinutes
with mock_datetime(2010, 10, 10, 13, 30, 0):
job.run()
assert job.next_run.hour == 14
assert job.next_run.minute == 10 + offsetMinutes
def test_next_run_time_minute_end(self):
self.tst_next_run_time_minute_end(None)
def test_next_run_time_minute_end_london(self):
try:
import pytz
except ModuleNotFoundError:
self.skipTest("pytz unavailable")
self.tst_next_run_time_minute_end("Europe/London")
def test_next_run_time_minute_end_katmhandu(self):
try:
import pytz
except ModuleNotFoundError:
self.skipTest("pytz unavailable")
self.tst_next_run_time_minute_end("Asia/Kathmandu")
def tst_next_run_time_minute_end(self, tz):
mock_job = make_mock_job()
with mock_datetime(2010, 10, 10, 10, 10, 0):
job = every().minute.at(":15", tz).do(mock_job)
assert job.next_run.minute == 10
assert job.next_run.second == 15
with mock_datetime(2010, 10, 10, 10, 10, 59):
job.run()
assert job.next_run.minute == 11
assert job.next_run.second == 15
with mock_datetime(2010, 10, 10, 10, 12, 14):
job.run()
assert job.next_run.minute == 12
assert job.next_run.second == 15
with mock_datetime(2010, 10, 10, 10, 12, 16):
job.run()
assert job.next_run.minute == 13
assert job.next_run.second == 15
def test_tz(self):
mock_job = self.make_tz_mock_job()
with mock_datetime(2022, 2, 1, 23, 15):
# Current Berlin time: feb-1 23:15 (local)
# Current India time: feb-2 03:45
# Expected to run India time: feb-2 06:30
# Next run Berlin time: feb-2 02:00
next = every().day.at("06:30", "Asia/Kolkata").do(mock_job).next_run
assert next.day == 2
assert next.hour == 2
assert next.minute == 0
def test_tz_daily_midnight(self):
mock_job = self.make_tz_mock_job()
with mock_datetime(2023, 4, 14, 4, 50):
# Current Berlin time: april-14 04:50 (local) (during daylight saving)
# Current US/Central time: april-13 21:50
# Expected to run US/Central time: april-14 00:00
# Next run Berlin time: april-14 07:00
next = every().day.at("00:00", "US/Central").do(mock_job).next_run
assert next.day == 14
assert next.hour == 7
assert next.minute == 0
def test_tz_daily_half_hour_offset(self):
mock_job = self.make_tz_mock_job()
with mock_datetime(2022, 4, 8, 10, 0):
# Current Berlin time: 10:00 (local) (during daylight saving)
# Current NY time: 04:00
# Expected to run NY time: 10:30
# Next run Berlin time: 16:30
next = every().day.at("10:30", "America/New_York").do(mock_job).next_run
assert next.hour == 16
assert next.minute == 30
def test_tz_daily_dst(self):
mock_job = self.make_tz_mock_job()
import pytz
with mock_datetime(2022, 3, 20, 10, 0):
# Current Berlin time: 10:00 (local) (NOT during daylight saving)
# Current NY time: 04:00 (during daylight saving)
# Expected to run NY time: 10:30
# Next run Berlin time: 15:30
tz = pytz.timezone("America/New_York")
next = every().day.at("10:30", tz).do(mock_job).next_run
assert next.hour == 15
assert next.minute == 30
def test_tz_daily_dst_skip_hour(self):
mock_job = self.make_tz_mock_job()
# Test the DST-case that is described in the documentation
with mock_datetime(2023, 3, 26, 1, 30):
# Current Berlin time: 01:30 (NOT during daylight saving)
# Expected to run: 02:30 - this time doesn't exist
# because clock moves from 02:00 to 03:00
# Next run: 03:30
job = every().day.at("02:30", "Europe/Berlin").do(mock_job)
assert job.next_run.day == 26
assert job.next_run.hour == 3
assert job.next_run.minute == 30
with mock_datetime(2023, 3, 27, 1, 30):
# the next day the job shall again run at 02:30
job.run()
assert job.next_run.day == 27
assert job.next_run.hour == 2
assert job.next_run.minute == 30
def test_tz_daily_dst_overlap_hour(self):
mock_job = self.make_tz_mock_job()
# Test the DST-case that is described in the documentation
with mock_datetime(2023, 10, 29, 1, 30):
# Current Berlin time: 01:30 (during daylight saving)
# Expected to run: 02:30 - this time exists twice
# because clock moves from 03:00 to 02:00
# Next run should be at the first occurrence of 02:30
job = every().day.at("02:30", "Europe/Berlin").do(mock_job)
assert job.next_run.day == 29
assert job.next_run.hour == 2
assert job.next_run.minute == 30
with mock_datetime(2023, 10, 29, 2, 35):
# After the job runs, the next run should be scheduled on the next day at 02:30
job.run()
assert job.next_run.day == 30
assert job.next_run.hour == 2
assert job.next_run.minute == 30
def test_tz_daily_exact_future_scheduling(self):
mock_job = self.make_tz_mock_job()
with mock_datetime(2022, 3, 20, 10, 0):
# Current Berlin time: 10:00 (local) (NOT during daylight saving)
# Current Krasnoyarsk time: 16:00
# Expected to run Krasnoyarsk time: mar-21 11:00
# Next run Berlin time: mar-21 05:00
# Expected idle seconds: 68400
schedule.clear()
every().day.at("11:00", "Asia/Krasnoyarsk").do(mock_job)
expected_delta = (
datetime.datetime(2022, 3, 21, 5, 0) - datetime.datetime.now()
)
assert schedule.idle_seconds() == expected_delta.total_seconds()
def test_tz_daily_utc(self):
mock_job = self.make_tz_mock_job()
with mock_datetime(2023, 9, 18, 10, 59, 0, TZ_AUCKLAND):
# Testing issue #598
# Current Auckland time: 10:59 (local) (NOT during daylight saving)
# Current UTC time: 21:59 (17 september)
# Expected to run UTC time: sept-18 00:00
# Next run Auckland time: sept-18 12:00
schedule.clear()
next = every().day.at("00:00", "UTC").do(mock_job).next_run
assert next.day == 18
assert next.hour == 12
assert next.minute == 0
# Test that .day.at() and .monday.at() are equivalent in this case
schedule.clear()
next = every().monday.at("00:00", "UTC").do(mock_job).next_run
assert next.day == 18
assert next.hour == 12
assert next.minute == 0
def test_tz_daily_issue_592(self):
mock_job = self.make_tz_mock_job()
with mock_datetime(2023, 7, 15, 13, 0, 0, TZ_UTC):
# Testing issue #592
# Current UTC time: 13:00
# Expected to run US East time: 9:45 (daylight saving active)
# Next run UTC time: july-15 13:45
schedule.clear()
next = every().day.at("09:45", "US/Eastern").do(mock_job).next_run
assert next.day == 15
assert next.hour == 13
assert next.minute == 45
def test_tz_daily_exact_seconds_precision(self):
mock_job = self.make_tz_mock_job()
with mock_datetime(2023, 10, 19, 15, 0, 0, TZ_UTC):
# Testing issue #603
# Current UTC: oktober-19 15:00
# Current Amsterdam: oktober-19 17:00 (daylight saving active)
# Expected run Amsterdam: oktober-20 00:00:20 (daylight saving active)
# Next run UTC time: oktober-19 22:00:20
schedule.clear()
next = every().day.at("00:00:20", "Europe/Amsterdam").do(mock_job).next_run
assert next.day == 19
assert next.hour == 22
assert next.minute == 00
assert next.second == 20
def test_tz_weekly_sunday_conversion(self):
mock_job = self.make_tz_mock_job()
with mock_datetime(2023, 10, 22, 23, 0, 0, TZ_UTC):
# Current UTC: sunday 22-okt 23:00
# Current Amsterdam: monday 23-okt 01:00 (daylight saving active)
# Expected run Amsterdam: sunday 29 oktober 23:00 (daylight saving NOT active)
# Next run UTC time: oktober-29 22:00
schedule.clear()
next = every().sunday.at("23:00", "Europe/Amsterdam").do(mock_job).next_run
assert next.day == 29
assert next.hour == 22
assert next.minute == 00
def test_tz_daily_new_year_offset(self):
mock_job = self.make_tz_mock_job()
with mock_datetime(2023, 12, 31, 23, 0, 0):
# Current Berlin time: dec-31 23:00 (local)
# Current Sydney time: jan-1 09:00 (next day)
# Expected to run Sydney time: jan-1 12:00
# Next run Berlin time: jan-1 02:00
next = every().day.at("12:00", "Australia/Sydney").do(mock_job).next_run
assert next.day == 1
assert next.hour == 2
assert next.minute == 0
def test_tz_daily_end_year_cross_continent(self):
mock_job = self.make_tz_mock_job()
with mock_datetime(2023, 12, 31, 23, 50):
# End of the year in Berlin
# Current Berlin time: dec-31 23:50
# Current Tokyo time: jan-1 07:50 (next day)
# Expected to run Tokyo time: jan-1 09:00
# Next run Berlin time: jan-1 01:00
next = every().day.at("09:00", "Asia/Tokyo").do(mock_job).next_run
assert next.day == 1
assert next.hour == 1
assert next.minute == 0
def test_tz_daily_end_month_offset(self):
mock_job = self.make_tz_mock_job()
with mock_datetime(2023, 2, 28, 23, 50):
# End of the month (non-leap year) in Berlin
# Current Berlin time: feb-28 23:50
# Current Sydney time: mar-1 09:50 (next day)
# Expected to run Sydney time: mar-1 10:00
# Next run Berlin time: mar-1 00:00
next = every().day.at("10:00", "Australia/Sydney").do(mock_job).next_run
assert next.day == 1
assert next.hour == 0
assert next.minute == 0
def test_tz_daily_leap_year(self):
mock_job = self.make_tz_mock_job()
with mock_datetime(2024, 2, 28, 23, 50):
# End of the month (leap year) in Berlin
# Current Berlin time: feb-28 23:50
# Current Dubai time: feb-29 02:50
# Expected to run Dubai time: feb-29 04:00
# Next run Berlin time: feb-29 01:00
next = every().day.at("04:00", "Asia/Dubai").do(mock_job).next_run
assert next.month == 2
assert next.day == 29
assert next.hour == 1
assert next.minute == 0
def test_tz_daily_issue_605(self):
mock_job = self.make_tz_mock_job()
with mock_datetime(2023, 9, 18, 10, 00, 0, TZ_AUCKLAND):
schedule.clear()
# Testing issue #605
# Current time: Monday 18 September 10:00 NZST
# Current time UTC: Sunday 17 September 22:00
# We expect the job to run at 23:00 on Sunday 17 September NZST
# That is an expected idle time of 1 hour
# Expected next run in NZST: 2023-09-18 11:00:00
next = schedule.every().day.at("23:00", "UTC").do(mock_job).next_run
assert round(schedule.idle_seconds() / 3600) == 1
assert next.day == 18
assert next.hour == 11
assert next.minute == 0
def test_tz_daily_dst_starting_point(self):
mock_job = self.make_tz_mock_job()
with mock_datetime(2023, 3, 26, 1, 30):
# Daylight Saving Time starts in Berlin
# In Berlin, 26 March 2023, 02:00:00 clocks were turned forward 1 hour
# In London, 26 March 2023, 01:00:00 clocks were turned forward 1 hour
# Current Berlin time: 26 March 01:30 (UTC+1)
# Current London time: 26 March 00:30 (UTC+0)
# Expected London time: 26 March 02:00 (UTC+1)
# Expected Berlin time: 26 March 03:00 (UTC+2)
next = every().day.at("01:00", "Europe/London").do(mock_job).next_run
assert next.day == 26
assert next.hour == 3
assert next.minute == 0
def test_tz_daily_dst_ending_point(self):
mock_job = self.make_tz_mock_job()
with mock_datetime(2023, 10, 29, 2, 30, fold=1):
# Daylight Saving Time ends in Berlin
# Current Berlin time: oct-29 02:30 (after moving back to 02:00 due to DST end)
# Current Istanbul time: oct-29 04:30
# Expected to run Istanbul time: oct-29 06:00
# Next run Berlin time: oct-29 04:00
next = every().day.at("06:00", "Europe/Istanbul").do(mock_job).next_run
assert next.hour == 4
assert next.minute == 0
def test_tz_daily_issue_608_pre_dst(self):
mock_job = self.make_tz_mock_job()
with mock_datetime(2023, 9, 18, 10, 00, 0, TZ_AUCKLAND):
# See ticket #608
# Testing timezone conversion the week before daylight saving comes into effect
# Current time: Monday 18 September 10:00 NZST
# Current time UTC: Sunday 17 September 22:00
# Expected next run in NZST: 2023-09-18 11:00:00
schedule.clear()
next = schedule.every().day.at("23:00", "UTC").do(mock_job).next_run
assert next.day == 18
assert next.hour == 11
assert next.minute == 0
def test_tz_daily_issue_608_post_dst(self):
mock_job = self.make_tz_mock_job()
with mock_datetime(2024, 4, 8, 10, 00, 0, TZ_AUCKLAND):
# See ticket #608
# Testing timezone conversion the week after daylight saving ends
# Current time: Monday 8 April 10:00 NZST
# Current time UTC: Sunday 7 April 22:00
# Expected next run in NZDT: 2023-04-08 11:00:00
schedule.clear()
next = schedule.every().day.at("23:00", "UTC").do(mock_job).next_run
assert next.day == 8
assert next.hour == 11
assert next.minute == 0
def test_tz_daily_issue_608_mid_dst(self):
mock_job = self.make_tz_mock_job()
with mock_datetime(2023, 9, 25, 10, 00, 0, TZ_AUCKLAND):
# See ticket #608
# Testing timezone conversion during the week after daylight saving comes into effect
# Current time: Monday 25 September 10:00 NZDT
# Current time UTC: Sunday 24 September 21:00
# Expected next run in UTC: 2023-09-24 23:00
# Expected next run in NZDT: 2023-09-25 12:00
schedule.clear()
next = schedule.every().day.at("23:00", "UTC").do(mock_job).next_run
assert next.month == 9
assert next.day == 25
assert next.hour == 12
assert next.minute == 0
def test_tz_daily_issue_608_before_dst_end(self):
mock_job = self.make_tz_mock_job()
with mock_datetime(2024, 4, 1, 10, 00, 0, TZ_AUCKLAND):
# See ticket #608
# Testing timezone conversion during the week before daylight saving ends
# Current time: Monday 1 April 10:00 NZDT
# Current time UTC: Friday 31 March 21:00
# Expected next run in UTC: 2023-03-31 23:00
# Expected next run in NZDT: 2024-04-01 12:00
schedule.clear()
next = schedule.every().day.at("23:00", "UTC").do(mock_job).next_run
assert next.month == 4
assert next.day == 1
assert next.hour == 12
assert next.minute == 0
def test_tz_hourly_intermediate_conversion(self):
mock_job = self.make_tz_mock_job()
with mock_datetime(2024, 5, 4, 14, 37, 22, TZ_CHATHAM):
# Crurent time: 14:37:22 New Zealand, Chatham Islands (UTC +12:45)
# Current time: 3 may, 23:22:22 Canada, Newfoundland (UTC -2:30)
# Exected next run in Newfoundland: 4 may, 09:14:45
# Expected next run in Chatham: 5 may, 00:29:45
schedule.clear()
next = (
schedule.every(10)
.hours.at("14:45", "Canada/Newfoundland")
.do(mock_job)
.next_run
)
assert next.day == 5
assert next.hour == 0
assert next.minute == 29
assert next.second == 45
def test_tz_minutes_year_round(self):
mock_job = self.make_tz_mock_job()
# Test a full year of scheduling across timezones, where one timezone
# is in the northern hemisphere and the other in the southern hemisphere
# These two timezones are also a bit exotic (not the usual UTC+1, UTC-1)
# Local timezone: Newfoundland, Canada: UTC-2:30 / DST UTC-3:30
# Remote timezone: Chatham Islands, New Zealand: UTC+12:45 / DST UTC+13:45
schedule.clear()
job = schedule.every(20).minutes.at(":13", "Canada/Newfoundland").do(mock_job)
with mock_datetime(2024, 9, 29, 2, 20, 0, TZ_CHATHAM):
# First run, nothing special, no utc-offset change
# Current time: 29 sept, 02:20:00 Chatham
# Current time: 28 sept, 11:05:00 Newfoundland
# Expected time: 28 sept, 11:20:13 Newfoundland
# Expected time: 29 sept, 02:40:13 Chatham
job.run()
assert job.next_run.day == 29
assert job.next_run.hour == 2
assert job.next_run.minute == 40
assert job.next_run.second == 13
with mock_datetime(2024, 9, 29, 2, 40, 14, TZ_CHATHAM):
# next-schedule happens 1 second behind schedule
job.run()
# On 29 Sep, 02:45 2024, in Chatham, the clock is moved +1 hour
# Thus, the next run happens AFTER the local timezone exits DST
# Current time: 29 sept, 02:40:14 Chatham (UTC +12:45)
# Current time: 28 sept, 11:25:14 Newfoundland (UTC -2:30)
# Expected time: 28 sept, 11:45:13 Newfoundland (UTC -2:30)
# Expected time: 29 sept, 04:00:13 Chatham (UTC +13:45)
assert job.next_run.day == 29
assert job.next_run.hour == 4
assert job.next_run.minute == 00
assert job.next_run.second == 13
with mock_datetime(2024, 11, 3, 2, 23, 55, TZ_CHATHAM, fold=0):
# Time is right before Newfoundland exits DST
# Local time will move 1 hour back at 03:00
job.run()
# There are no timezone switches yet, nothing special going on:
# Current time: 3 Nov, 02:23:55 Chatham
# Expected time: 3 Nov, 02:43:13 Chatham
assert job.next_run.day == 3
assert job.next_run.hour == 2
assert job.next_run.minute == 43 # Within the fold, first occurrence
assert job.next_run.second == 13
with mock_datetime(2024, 11, 3, 2, 23, 55, TZ_CHATHAM, fold=1):
# Time is during the fold. Local time has moved back 1 hour, this is
# the second occurrence of the 02:23 time.
job.run()
# Current time: 3 Nov, 02:23:55 Chatham
# Expected time: 3 Nov, 02:43:13 Chatham
assert job.next_run.day == 3
assert job.next_run.hour == 2
assert job.next_run.minute == 43
assert job.next_run.second == 13
with mock_datetime(2025, 3, 9, 19, 00, 00, TZ_CHATHAM):
# Time is right before Newfoundland enters DST
# At 02:00, the remote clock will move forward 1 hour
job.run()
# Current time: 9 March, 19:00:00 Chatham (UTC +13:45)
# Current time: 9 March, 01:45:00 Newfoundland (UTC -3:30)
# Expected time: 9 March, 03:05:13 Newfoundland (UTC -2:30)
# Expected time 9 March, 19:20:13 Chatham (UTC +13:45)
assert job.next_run.day == 9
assert job.next_run.hour == 19
assert job.next_run.minute == 20
assert job.next_run.second == 13
with mock_datetime(2025, 4, 7, 17, 55, 00, TZ_CHATHAM):
# Time is within the few hours before Catham exits DST
# At 03:45, the local clock moves back 1 hour
job.run()
# Current time: 7 April, 17:55:00 Chatham
# Current time: 7 April, 02:40:00 Newfoundland
# Expected time: 7 April, 03:00:13 Newfoundland
# Expected time 7 April, 18:15:13 Chatham
assert job.next_run.day == 7
assert job.next_run.hour == 18
assert job.next_run.minute == 15
assert job.next_run.second == 13
with mock_datetime(2025, 4, 7, 18, 55, 00, TZ_CHATHAM):
# Schedule the next run exactly when the clock moved backwards
# Curren time is before the clock-move, next run is after the clock change
job.run()
# Current time: 7 April, 18:55:00 Chatham
# Current time: 7 April, 03:40:00 Newfoundland
# Expected time: 7 April, 03:00:13 Newfoundland (clock moved back)
# Expected time 7 April, 19:15:13 Chatham
assert job.next_run.day == 7
assert job.next_run.hour == 19
assert job.next_run.minute == 15
assert job.next_run.second == 13
with mock_datetime(2025, 4, 7, 19, 15, 13, TZ_CHATHAM):
# Schedule during the fold in the remote timezone
job.run()
# Current time: 7 April, 19:15:13 Chatham
# Current time: 7 April, 03:00:13 Newfoundland (fold)
# Expected time: 7 April, 03:20:13 Newfoundland (fold)
# Expected time: 7 April, 19:35:13 Chatham
assert job.next_run.day == 7
assert job.next_run.hour == 19
assert job.next_run.minute == 35
assert job.next_run.second == 13
def test_tz_weekly_large_interval_forward(self):
mock_job = self.make_tz_mock_job()
# Testing scheduling large intervals that skip over clock move forward
with mock_datetime(2024, 3, 28, 11, 0, 0, TZ_BERLIN):
# At March 31st 2024, 02:00:00 clocks were turned forward 1 hour
schedule.clear()
next = (
schedule.every(7)
.days.at("11:00", "Europe/Berlin")
.do(mock_job)
.next_run
)
assert next.month == 4
assert next.day == 4
assert next.hour == 11
assert next.minute == 0
assert next.second == 0
def test_tz_weekly_large_interval_backward(self):
mock_job = self.make_tz_mock_job()
import pytz
# Testing scheduling large intervals that skip over clock move back
with mock_datetime(2024, 10, 25, 11, 0, 0, TZ_BERLIN):
# At March 31st 2024, 02:00:00 clocks were turned forward 1 hour
schedule.clear()
next = (
schedule.every(7)
.days.at("11:00", "Europe/Berlin")
.do(mock_job)
.next_run
)
assert next.month == 11
assert next.day == 1
assert next.hour == 11
assert next.minute == 0
assert next.second == 0
def test_tz_daily_skip_dst_change(self):
mock_job = self.make_tz_mock_job()
with mock_datetime(2024, 11, 3, 10, 0):
# At 3 November 2024, 02:00:00 clocks are turned backward 1 hour
# The job skips the whole DST change becaus it runs at 14:00
# Current time Berlin: 3 Nov, 10:00
# Current time Anchorage: 3 Nov, 00:00 (UTC-08:00)
# Expected time Anchorage: 3 Nov, 14:00 (UTC-09:00)
# Expected time Berlin: 4 Nov, 00:00
schedule.clear()
next = (
schedule.every()
.day.at("14:00", "America/Anchorage")
.do(mock_job)
.next_run
)
assert next.day == 4
assert next.hour == 0
assert next.minute == 00
def test_tz_daily_different_simultaneous_dst_change(self):
mock_job = self.make_tz_mock_job()
# TZ_BERLIN_EXTRA is the same as Berlin, but during summer time
# moves the clock 2 hours forward instead of 1
# This is a fictional timezone
TZ_BERLIN_EXTRA = "CET-01CEST-03,M3.5.0,M10.5.0/3"
with mock_datetime(2024, 3, 31, 0, 0, 0, TZ_BERLIN_EXTRA):
# In Berlin at March 31 2024, 02:00:00 clocks were turned forward 1 hour
# In Berlin Extra, the clocks move forward 2 hour at the same time
# Current time Berlin Extra: 31 Mar, 00:00 (UTC+01:00)
# Current time Berlin: 31 Mar, 00:00 (UTC+01:00)
# Expected time Berlin: 31 Mar, 10:00 (UTC+02:00)
# Expected time Berlin Extra: 31 Mar, 11:00 (UTC+03:00)
schedule.clear()
next = (
schedule.every().day.at("10:00", "Europe/Berlin").do(mock_job).next_run
)
assert next.day == 31
assert next.hour == 11
assert next.minute == 00
def test_tz_daily_opposite_dst_change(self):
mock_job = self.make_tz_mock_job()
# TZ_BERLIN_INVERTED changes in the opposite direction of Berlin
# This is a fictional timezone
TZ_BERLIN_INVERTED = "CET-1CEST,M10.5.0/3,M3.5.0"
with mock_datetime(2024, 3, 31, 0, 0, 0, TZ_BERLIN_INVERTED):
# In Berlin at March 31 2024, 02:00:00 clocks were turned forward 1 hour
# In Berlin Inverted, the clocks move back 1 hour at the same time
# Current time Berlin Inverted: 31 Mar, 00:00 (UTC+02:00)
# Current time Berlin: 31 Mar, 00:00 (UTC+01:00)
# Expected time Berlin: 31 Mar, 10:00 (UTC+02:00) +9 hour
# Expected time Berlin Inverted: 31 Mar, 09:00 (UTC+01:00)
schedule.clear()
next = (
schedule.every().day.at("10:00", "Europe/Berlin").do(mock_job).next_run
)
assert next.day == 31
assert next.hour == 9
assert next.minute == 00
def test_tz_invalid_timezone_exceptions(self):
mock_job = self.make_tz_mock_job()
import pytz
with self.assertRaises(pytz.exceptions.UnknownTimeZoneError):
every().day.at("10:30", "FakeZone").do(mock_job)
with self.assertRaises(ScheduleValueError):
every().day.at("10:30", 43).do(mock_job)
def test_align_utc_offset_no_timezone(self):
job = schedule.every().day.at("10:00").do(make_mock_job())
now = datetime.datetime(2024, 5, 11, 10, 30, 55, 0)
aligned_time = job._correct_utc_offset(now, fixate_time=True)
self.assertEqual(now, aligned_time)
def setup_utc_offset_test(self):
try:
import pytz
except ModuleNotFoundError:
self.skipTest("pytz unavailable")
job = (
schedule.every()
.day.at("10:00", "Europe/Berlin")
.do(make_mock_job("tz-test"))
)
tz = pytz.timezone("Europe/Berlin")
return (job, tz)
def test_align_utc_offset_no_change(self):
(job, tz) = self.setup_utc_offset_test()
now = tz.localize(datetime.datetime(2023, 3, 26, 1, 30))
aligned_time = job._correct_utc_offset(now, fixate_time=False)
self.assertEqual(now, aligned_time)
def test_align_utc_offset_with_dst_gap(self):
(job, tz) = self.setup_utc_offset_test()
# Non-existent time in Berlin timezone
gap_time = tz.localize(datetime.datetime(2024, 3, 31, 2, 30, 0))
aligned_time = job._correct_utc_offset(gap_time, fixate_time=True)
assert aligned_time.utcoffset() == datetime.timedelta(hours=2)
assert aligned_time.day == 31
assert aligned_time.hour == 3
assert aligned_time.minute == 30
def test_align_utc_offset_with_dst_fold(self):
(job, tz) = self.setup_utc_offset_test()
# This time exists twice, this is the first occurance
overlap_time = tz.localize(datetime.datetime(2024, 10, 27, 2, 30))
aligned_time = job._correct_utc_offset(overlap_time, fixate_time=False)
# Since the time exists twice, no fixate_time flag should yield the first occurrence
first_occurrence = tz.localize(datetime.datetime(2024, 10, 27, 2, 30, fold=0))
self.assertEqual(first_occurrence, aligned_time)
def test_align_utc_offset_with_dst_fold_fixate_1(self):
(job, tz) = self.setup_utc_offset_test()
# This time exists twice, this is the 1st occurance
overlap_time = tz.localize(datetime.datetime(2024, 10, 27, 1, 30), is_dst=True)
overlap_time += datetime.timedelta(
hours=1
) # puts it at 02:30+02:00 (Which exists once)
aligned_time = job._correct_utc_offset(overlap_time, fixate_time=True)
# The time should not have moved, because the original time is valid
assert aligned_time.utcoffset() == datetime.timedelta(hours=2)
assert aligned_time.hour == 2
assert aligned_time.minute == 30
assert aligned_time.day == 27
def test_align_utc_offset_with_dst_fold_fixate_2(self):
(job, tz) = self.setup_utc_offset_test()
# 02:30 exists twice, this is the 2nd occurance
overlap_time = tz.localize(datetime.datetime(2024, 10, 27, 2, 30), is_dst=False)
# The time 2024-10-27 02:30:00+01:00 exists once
aligned_time = job._correct_utc_offset(overlap_time, fixate_time=True)
# The time was valid, should not have been moved
assert aligned_time.utcoffset() == datetime.timedelta(hours=1)
assert aligned_time.hour == 2
assert aligned_time.minute == 30
assert aligned_time.day == 27
def test_align_utc_offset_after_fold_fixate(self):
(job, tz) = self.setup_utc_offset_test()
# This time is 30 minutes after a folded hour.
duplicate_time = tz.localize(datetime.datetime(2024, 10, 27, 2, 30))
duplicate_time += datetime.timedelta(hours=1)
aligned_time = job._correct_utc_offset(duplicate_time, fixate_time=False)
assert aligned_time.utcoffset() == datetime.timedelta(hours=1)
assert aligned_time.hour == 3
assert aligned_time.minute == 30
assert aligned_time.day == 27
def test_daylight_saving_time(self):
mock_job = make_mock_job()
# 27 March 2022, 02:00:00 clocks were turned forward 1 hour
with mock_datetime(2022, 3, 27, 0, 0):
assert every(4).hours.do(mock_job).next_run.hour == 4
# Sunday, 30 October 2022, 03:00:00 clocks were turned backward 1 hour
with mock_datetime(2022, 10, 30, 0, 0):
assert every(4).hours.do(mock_job).next_run.hour == 4
def test_move_to_next_weekday_today(self):
monday = datetime.datetime(2024, 5, 13, 10, 27, 54)
tuesday = schedule._move_to_next_weekday(monday, "monday")
assert tuesday.day == 13 # today! Time didn't change.
assert tuesday.hour == 10
assert tuesday.minute == 27
def test_move_to_next_weekday_tommorrow(self):
monday = datetime.datetime(2024, 5, 13, 10, 27, 54)
tuesday = schedule._move_to_next_weekday(monday, "tuesday")
assert tuesday.day == 14 # 1 day ahead
assert tuesday.hour == 10
assert tuesday.minute == 27
def test_move_to_next_weekday_nextweek(self):
wednesday = datetime.datetime(2024, 5, 15, 10, 27, 54)
tuesday = schedule._move_to_next_weekday(wednesday, "tuesday")
assert tuesday.day == 21 # next week monday
assert tuesday.hour == 10
assert tuesday.minute == 27
def test_run_all(self):
mock_job = make_mock_job()
every().minute.do(mock_job)
every().hour.do(mock_job)
every().day.at("11:00").do(mock_job)
schedule.run_all()
assert mock_job.call_count == 3
def test_run_all_with_decorator(self):
mock_job = make_mock_job()
@repeat(every().minute)
def job1():
mock_job()
@repeat(every().hour)
def job2():
mock_job()
@repeat(every().day.at("11:00"))
def job3():
mock_job()
schedule.run_all()
assert mock_job.call_count == 3
def test_run_all_with_decorator_args(self):
mock_job = make_mock_job()
@repeat(every().minute, 1, 2, "three", foo=23, bar={})
def job(*args, **kwargs):
mock_job(*args, **kwargs)
schedule.run_all()
mock_job.assert_called_once_with(1, 2, "three", foo=23, bar={})
def test_run_all_with_decorator_defaultargs(self):
mock_job = make_mock_job()
@repeat(every().minute)
def job(nothing=None):
mock_job(nothing)
schedule.run_all()
mock_job.assert_called_once_with(None)
def test_job_func_args_are_passed_on(self):
mock_job = make_mock_job()
every().second.do(mock_job, 1, 2, "three", foo=23, bar={})
schedule.run_all()
mock_job.assert_called_once_with(1, 2, "three", foo=23, bar={})
def test_to_string(self):
def job_fun():
pass
s = str(every().minute.do(job_fun, "foo", bar=23))
assert s == (
"Job(interval=1, unit=minutes, do=job_fun, "
"args=('foo',), kwargs={'bar': 23})"
)
assert "job_fun" in s
assert "foo" in s
assert "{'bar': 23}" in s
def test_to_repr(self):
def job_fun():
pass
s = repr(every().minute.do(job_fun, "foo", bar=23))
assert s.startswith(
"Every 1 minute do job_fun('foo', bar=23) (last run: [never], next run: "
)
assert "job_fun" in s
assert "foo" in s
assert "bar=23" in s
# test repr when at_time is not None
s2 = repr(every().day.at("00:00").do(job_fun, "foo", bar=23))
assert s2.startswith(
(
"Every 1 day at 00:00:00 do job_fun('foo', "
"bar=23) (last run: [never], next run: "
)
)
# Ensure Job.__repr__ does not throw exception on a partially-composed Job
s3 = repr(schedule.every(10))
assert s3 == "Every 10 None do [None] (last run: [never], next run: [never])"
def test_to_string_lambda_job_func(self):
assert len(str(every().minute.do(lambda: 1))) > 1
assert len(str(every().day.at("10:30").do(lambda: 1))) > 1
def test_repr_functools_partial_job_func(self):
def job_fun(arg):
pass
job_fun = functools.partial(job_fun, "foo")
job_repr = repr(every().minute.do(job_fun, bar=True, somekey=23))
assert "functools.partial" in job_repr
assert "bar=True" in job_repr
assert "somekey=23" in job_repr
def test_to_string_functools_partial_job_func(self):
def job_fun(arg):
pass
job_fun = functools.partial(job_fun, "foo")
job_str = str(every().minute.do(job_fun, bar=True, somekey=23))
assert "functools.partial" in job_str
assert "bar=True" in job_str
assert "somekey=23" in job_str
def test_run_pending(self):
"""Check that run_pending() runs pending jobs.
We do this by overriding datetime.datetime with mock objects
that represent increasing system times.
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.
"""
mock_job = make_mock_job()
with mock_datetime(2010, 1, 6, 12, 15):
every().minute.do(mock_job)
every().hour.do(mock_job)
every().day.do(mock_job)
every().sunday.do(mock_job)
schedule.run_pending()
assert mock_job.call_count == 0
with mock_datetime(2010, 1, 6, 12, 16):
schedule.run_pending()
assert mock_job.call_count == 1
with mock_datetime(2010, 1, 6, 13, 16):
mock_job.reset_mock()
schedule.run_pending()
assert mock_job.call_count == 2
with mock_datetime(2010, 1, 7, 13, 16):
mock_job.reset_mock()
schedule.run_pending()
assert mock_job.call_count == 3
with mock_datetime(2010, 1, 10, 13, 16):
mock_job.reset_mock()
schedule.run_pending()
assert mock_job.call_count == 4
def test_run_every_weekday_at_specific_time_today(self):
mock_job = make_mock_job()
with mock_datetime(2010, 1, 6, 13, 16): # january 6 2010 == Wednesday
every().wednesday.at("14:12").do(mock_job)
schedule.run_pending()
assert mock_job.call_count == 0
with mock_datetime(2010, 1, 6, 14, 16):
schedule.run_pending()
assert mock_job.call_count == 1
def test_run_every_weekday_at_specific_time_past_today(self):
mock_job = make_mock_job()
with mock_datetime(2010, 1, 6, 13, 16):
every().wednesday.at("13:15").do(mock_job)
schedule.run_pending()
assert mock_job.call_count == 0
with mock_datetime(2010, 1, 13, 13, 14):
schedule.run_pending()
assert mock_job.call_count == 0
with mock_datetime(2010, 1, 13, 13, 16):
schedule.run_pending()
assert mock_job.call_count == 1
def test_run_every_n_days_at_specific_time(self):
mock_job = make_mock_job()
with mock_datetime(2010, 1, 6, 11, 29):
every(2).days.at("11:30").do(mock_job)
schedule.run_pending()
assert mock_job.call_count == 0
with mock_datetime(2010, 1, 6, 11, 31):
schedule.run_pending()
assert mock_job.call_count == 0
with mock_datetime(2010, 1, 7, 11, 31):
schedule.run_pending()
assert mock_job.call_count == 0
with mock_datetime(2010, 1, 8, 11, 29):
schedule.run_pending()
assert mock_job.call_count == 0
with mock_datetime(2010, 1, 8, 11, 31):
schedule.run_pending()
assert mock_job.call_count == 1
with mock_datetime(2010, 1, 10, 11, 31):
schedule.run_pending()
assert mock_job.call_count == 2
def test_next_run_property(self):
original_datetime = datetime.datetime
with mock_datetime(2010, 1, 6, 13, 16):
hourly_job = make_mock_job("hourly")
daily_job = make_mock_job("daily")
every().day.do(daily_job)
every().hour.do(hourly_job)
assert len(schedule.jobs) == 2
# Make sure the hourly job is first
assert schedule.next_run() == original_datetime(2010, 1, 6, 14, 16)
def test_idle_seconds(self):
assert schedule.default_scheduler.next_run is None
assert schedule.idle_seconds() is None
mock_job = make_mock_job()
with mock_datetime(2020, 12, 9, 21, 46):
job = every().hour.do(mock_job)
assert schedule.idle_seconds() == 60 * 60
schedule.cancel_job(job)
assert schedule.next_run() is None
assert schedule.idle_seconds() is None
def test_cancel_job(self):
def stop_job():
return schedule.CancelJob
mock_job = make_mock_job()
every().second.do(stop_job)
mj = every().second.do(mock_job)
assert len(schedule.jobs) == 2
schedule.run_all()
assert len(schedule.jobs) == 1
assert schedule.jobs[0] == mj
schedule.cancel_job("Not a job")
assert len(schedule.jobs) == 1
schedule.default_scheduler.cancel_job("Not a job")
assert len(schedule.jobs) == 1
schedule.cancel_job(mj)
assert len(schedule.jobs) == 0
def test_cancel_jobs(self):
def stop_job():
return schedule.CancelJob
every().second.do(stop_job)
every().second.do(stop_job)
every().second.do(stop_job)
assert len(schedule.jobs) == 3
schedule.run_all()
assert len(schedule.jobs) == 0
def test_tag_type_enforcement(self):
job1 = every().second.do(make_mock_job(name="job1"))
self.assertRaises(TypeError, job1.tag, {})
self.assertRaises(TypeError, job1.tag, 1, "a", [])
job1.tag(0, "a", True)
assert len(job1.tags) == 3
def test_get_by_tag(self):
every().second.do(make_mock_job()).tag("job1", "tag1")
every().second.do(make_mock_job()).tag("job2", "tag2", "tag4")
every().second.do(make_mock_job()).tag("job3", "tag3", "tag4")
# Test None input yields all 3
jobs = schedule.get_jobs()
assert len(jobs) == 3
assert {"job1", "job2", "job3"}.issubset(
{*jobs[0].tags, *jobs[1].tags, *jobs[2].tags}
)
# Test each 1:1 tag:job
jobs = schedule.get_jobs("tag1")
assert len(jobs) == 1
assert "job1" in jobs[0].tags
# Test multiple jobs found.
jobs = schedule.get_jobs("tag4")
assert len(jobs) == 2
assert "job1" not in {*jobs[0].tags, *jobs[1].tags}
# Test no tag.
jobs = schedule.get_jobs("tag5")
assert len(jobs) == 0
schedule.clear()
assert len(schedule.jobs) == 0
def test_clear_by_tag(self):
every().second.do(make_mock_job(name="job1")).tag("tag1")
every().second.do(make_mock_job(name="job2")).tag("tag1", "tag2")
every().second.do(make_mock_job(name="job3")).tag(
"tag3", "tag3", "tag3", "tag2"
)
assert len(schedule.jobs) == 3
schedule.run_all()
assert len(schedule.jobs) == 3
schedule.clear("tag3")
assert len(schedule.jobs) == 2
schedule.clear("tag1")
assert len(schedule.jobs) == 0
every().second.do(make_mock_job(name="job1"))
every().second.do(make_mock_job(name="job2"))
every().second.do(make_mock_job(name="job3"))
schedule.clear()
assert len(schedule.jobs) == 0
def test_misconfigured_job_wont_break_scheduler(self):
"""
Ensure an interrupted job definition chain won't break
the scheduler instance permanently.
"""
scheduler = schedule.Scheduler()
scheduler.every()
scheduler.every(10).seconds
scheduler.run_pending()
================================================
FILE: tox.ini
================================================
[tox]
envlist = py3{7,8,9,10,11,12}{,-pytz}
skip_missing_interpreters = true
[gh-actions]
python =
3.7: py37, py37-pytz
3.8: py38, py38-pytz
3.9: py39, py39-pytz
3.10: py310, py310-pytz
3.11: py311, py311-pytz
3.12: py312, py312-pytz
[testenv]
deps =
pytest
pytest-cov
mypy
types-pytz
pytz: pytz
commands =
py.test test_schedule.py schedule -v --cov schedule --cov-report term-missing
python -m mypy -p schedule --install-types --non-interactive
[testenv:docs]
changedir = docs
deps = -rrequirements-dev.txt
commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
[testenv:format]
deps = -rrequirements-dev.txt
commands = black --check .
[testenv:setuppy]
deps = -rrequirements-dev.txt
commands =
python setup.py check --strict --metadata --restructuredtext
gitextract_ic2muaty/ ├── .github/ │ └── workflows/ │ └── ci.yml ├── .gitignore ├── AUTHORS.rst ├── HISTORY.rst ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── docs/ │ ├── Makefile │ ├── _static/ │ │ └── custom.css │ ├── _templates/ │ │ └── sidebarintro.html │ ├── background-execution.rst │ ├── changelog.rst │ ├── conf.py │ ├── development.rst │ ├── examples.rst │ ├── exception-handling.rst │ ├── faq.rst │ ├── index.rst │ ├── installation.rst │ ├── logging.rst │ ├── multiple-schedulers.rst │ ├── parallel-execution.rst │ ├── reference.rst │ └── timezones.rst ├── pyproject.toml ├── requirements-dev.txt ├── schedule/ │ ├── __init__.py │ └── py.typed ├── setup.cfg ├── setup.py ├── test_schedule.py └── tox.ini
SYMBOL INDEX (153 symbols across 3 files)
FILE: schedule/__init__.py
class ScheduleError (line 53) | class ScheduleError(Exception):
class ScheduleValueError (line 59) | class ScheduleValueError(ScheduleError):
class IntervalError (line 65) | class IntervalError(ScheduleValueError):
class CancelJob (line 71) | class CancelJob:
class Scheduler (line 79) | class Scheduler:
method __init__ (line 86) | def __init__(self) -> None:
method run_pending (line 89) | def run_pending(self) -> None:
method run_all (line 103) | def run_all(self, delay_seconds: int = 0) -> None:
method get_jobs (line 122) | def get_jobs(self, tag: Optional[Hashable] = None) -> List["Job"]:
method clear (line 135) | def clear(self, tag: Optional[Hashable] = None) -> None:
method cancel_job (line 150) | def cancel_job(self, job: "Job") -> None:
method every (line 162) | def every(self, interval: int = 1) -> "Job":
method _run_job (line 172) | def _run_job(self, job: "Job") -> None:
method get_next_run (line 177) | def get_next_run(
method idle_seconds (line 198) | def idle_seconds(self) -> Optional[float]:
class Job (line 209) | class Job:
method __init__ (line 227) | def __init__(self, interval: int, scheduler: Optional[Scheduler] = None):
method __lt__ (line 257) | def __lt__(self, other) -> bool:
method __str__ (line 264) | def __str__(self) -> str:
method __repr__ (line 278) | def __repr__(self):
method second (line 326) | def second(self):
method seconds (line 332) | def seconds(self):
method minute (line 337) | def minute(self):
method minutes (line 343) | def minutes(self):
method hour (line 348) | def hour(self):
method hours (line 354) | def hours(self):
method day (line 359) | def day(self):
method days (line 365) | def days(self):
method week (line 370) | def week(self):
method weeks (line 376) | def weeks(self):
method monday (line 381) | def monday(self):
method tuesday (line 392) | def tuesday(self):
method wednesday (line 403) | def wednesday(self):
method thursday (line 414) | def thursday(self):
method friday (line 425) | def friday(self):
method saturday (line 436) | def saturday(self):
method sunday (line 447) | def sunday(self):
method tag (line 457) | def tag(self, *tags: Hashable):
method at (line 471) | def at(self, time_str: str, tz: Optional[str] = None):
method to (line 561) | def to(self, latest: int):
method until (line 576) | def until(
method do (line 644) | def do(self, job_func: Callable, *args, **kwargs):
method should_run (line 667) | def should_run(self) -> bool:
method run (line 674) | def run(self):
method _schedule_next_run (line 700) | def _schedule_next_run(self) -> None:
method _move_to_at_time (line 750) | def _move_to_at_time(self, moment: datetime.datetime) -> datetime.date...
method _correct_utc_offset (line 773) | def _correct_utc_offset(
method _is_overdue (line 819) | def _is_overdue(self, when: datetime.datetime):
method _decode_datetimestr (line 822) | def _decode_datetimestr(
function every (line 843) | def every(interval: int = 1) -> Job:
function run_pending (line 850) | def run_pending() -> None:
function run_all (line 857) | def run_all(delay_seconds: int = 0) -> None:
function get_jobs (line 864) | def get_jobs(tag: Optional[Hashable] = None) -> List[Job]:
function clear (line 871) | def clear(tag: Optional[Hashable] = None) -> None:
function cancel_job (line 878) | def cancel_job(job: Job) -> None:
function next_run (line 885) | def next_run(tag: Optional[Hashable] = None) -> Optional[datetime.dateti...
function idle_seconds (line 892) | def idle_seconds() -> Optional[float]:
function repeat (line 899) | def repeat(job, *args, **kwargs):
function _move_to_next_weekday (line 916) | def _move_to_next_weekday(moment: datetime.datetime, weekday: str):
function _weekday_index (line 931) | def _weekday_index(day: str) -> int:
FILE: setup.py
function read_file (line 9) | def read_file(filename):
FILE: test_schedule.py
function make_mock_job (line 33) | def make_mock_job(name=None):
class mock_datetime (line 39) | class mock_datetime:
method __init__ (line 44) | def __init__(self, year, month, day, hour, minute, second=0, zone=None...
method __enter__ (line 56) | def __enter__(self):
method __exit__ (line 89) | def __exit__(self, *args, **kwargs):
class SchedulerTests (line 96) | class SchedulerTests(TestCase):
method setUp (line 97) | def setUp(self):
method make_tz_mock_job (line 100) | def make_tz_mock_job(self, name=None):
method test_time_units (line 108) | def test_time_units(self):
method test_next_run_with_tag (line 222) | def test_next_run_with_tag(self):
method test_singular_time_units_match_plural_units (line 236) | def test_singular_time_units_match_plural_units(self):
method test_time_range (line 243) | def test_time_range(self):
method test_time_range_repr (line 260) | def test_time_range_repr(self):
method test_at_time (line 268) | def test_at_time(self):
method test_until_time (line 312) | def test_until_time(self):
method test_weekday_at_todady (line 390) | def test_weekday_at_todady(self):
method test_at_time_hour (line 411) | def test_at_time_hour(self):
method test_at_time_minute (line 445) | def test_at_time_minute(self):
method test_next_run_time (line 463) | def test_next_run_time(self):
method test_next_run_time_day_end (line 486) | def test_next_run_time_day_end(self):
method test_next_run_time_hour_end (line 510) | def test_next_run_time_hour_end(self):
method test_next_run_time_hour_end_london (line 518) | def test_next_run_time_hour_end_london(self):
method test_next_run_time_hour_end_katmandu (line 526) | def test_next_run_time_hour_end_katmandu(self):
method tst_next_run_time_hour_end (line 538) | def tst_next_run_time_hour_end(self, tz, offsetMinutes):
method test_next_run_time_minute_end (line 557) | def test_next_run_time_minute_end(self):
method test_next_run_time_minute_end_london (line 560) | def test_next_run_time_minute_end_london(self):
method test_next_run_time_minute_end_katmhandu (line 568) | def test_next_run_time_minute_end_katmhandu(self):
method tst_next_run_time_minute_end (line 576) | def tst_next_run_time_minute_end(self, tz):
method test_tz (line 598) | def test_tz(self):
method test_tz_daily_midnight (line 610) | def test_tz_daily_midnight(self):
method test_tz_daily_half_hour_offset (line 622) | def test_tz_daily_half_hour_offset(self):
method test_tz_daily_dst (line 633) | def test_tz_daily_dst(self):
method test_tz_daily_dst_skip_hour (line 647) | def test_tz_daily_dst_skip_hour(self):
method test_tz_daily_dst_overlap_hour (line 666) | def test_tz_daily_dst_overlap_hour(self):
method test_tz_daily_exact_future_scheduling (line 685) | def test_tz_daily_exact_future_scheduling(self):
method test_tz_daily_utc (line 700) | def test_tz_daily_utc(self):
method test_tz_daily_issue_592 (line 721) | def test_tz_daily_issue_592(self):
method test_tz_daily_exact_seconds_precision (line 734) | def test_tz_daily_exact_seconds_precision(self):
method test_tz_weekly_sunday_conversion (line 749) | def test_tz_weekly_sunday_conversion(self):
method test_tz_daily_new_year_offset (line 762) | def test_tz_daily_new_year_offset(self):
method test_tz_daily_end_year_cross_continent (line 774) | def test_tz_daily_end_year_cross_continent(self):
method test_tz_daily_end_month_offset (line 787) | def test_tz_daily_end_month_offset(self):
method test_tz_daily_leap_year (line 800) | def test_tz_daily_leap_year(self):
method test_tz_daily_issue_605 (line 814) | def test_tz_daily_issue_605(self):
method test_tz_daily_dst_starting_point (line 830) | def test_tz_daily_dst_starting_point(self):
method test_tz_daily_dst_ending_point (line 845) | def test_tz_daily_dst_ending_point(self):
method test_tz_daily_issue_608_pre_dst (line 857) | def test_tz_daily_issue_608_pre_dst(self):
method test_tz_daily_issue_608_post_dst (line 871) | def test_tz_daily_issue_608_post_dst(self):
method test_tz_daily_issue_608_mid_dst (line 885) | def test_tz_daily_issue_608_mid_dst(self):
method test_tz_daily_issue_608_before_dst_end (line 901) | def test_tz_daily_issue_608_before_dst_end(self):
method test_tz_hourly_intermediate_conversion (line 917) | def test_tz_hourly_intermediate_conversion(self):
method test_tz_minutes_year_round (line 936) | def test_tz_minutes_year_round(self):
method test_tz_weekly_large_interval_forward (line 1045) | def test_tz_weekly_large_interval_forward(self):
method test_tz_weekly_large_interval_backward (line 1063) | def test_tz_weekly_large_interval_backward(self):
method test_tz_daily_skip_dst_change (line 1083) | def test_tz_daily_skip_dst_change(self):
method test_tz_daily_different_simultaneous_dst_change (line 1103) | def test_tz_daily_different_simultaneous_dst_change(self):
method test_tz_daily_opposite_dst_change (line 1125) | def test_tz_daily_opposite_dst_change(self):
method test_tz_invalid_timezone_exceptions (line 1146) | def test_tz_invalid_timezone_exceptions(self):
method test_align_utc_offset_no_timezone (line 1156) | def test_align_utc_offset_no_timezone(self):
method setup_utc_offset_test (line 1162) | def setup_utc_offset_test(self):
method test_align_utc_offset_no_change (line 1175) | def test_align_utc_offset_no_change(self):
method test_align_utc_offset_with_dst_gap (line 1181) | def test_align_utc_offset_with_dst_gap(self):
method test_align_utc_offset_with_dst_fold (line 1192) | def test_align_utc_offset_with_dst_fold(self):
method test_align_utc_offset_with_dst_fold_fixate_1 (line 1201) | def test_align_utc_offset_with_dst_fold_fixate_1(self):
method test_align_utc_offset_with_dst_fold_fixate_2 (line 1216) | def test_align_utc_offset_with_dst_fold_fixate_2(self):
method test_align_utc_offset_after_fold_fixate (line 1229) | def test_align_utc_offset_after_fold_fixate(self):
method test_daylight_saving_time (line 1242) | def test_daylight_saving_time(self):
method test_move_to_next_weekday_today (line 1252) | def test_move_to_next_weekday_today(self):
method test_move_to_next_weekday_tommorrow (line 1259) | def test_move_to_next_weekday_tommorrow(self):
method test_move_to_next_weekday_nextweek (line 1266) | def test_move_to_next_weekday_nextweek(self):
method test_run_all (line 1273) | def test_run_all(self):
method test_run_all_with_decorator (line 1281) | def test_run_all_with_decorator(self):
method test_run_all_with_decorator_args (line 1299) | def test_run_all_with_decorator_args(self):
method test_run_all_with_decorator_defaultargs (line 1309) | def test_run_all_with_decorator_defaultargs(self):
method test_job_func_args_are_passed_on (line 1319) | def test_job_func_args_are_passed_on(self):
method test_to_string (line 1325) | def test_to_string(self):
method test_to_repr (line 1338) | def test_to_repr(self):
method test_to_string_lambda_job_func (line 1363) | def test_to_string_lambda_job_func(self):
method test_repr_functools_partial_job_func (line 1367) | def test_repr_functools_partial_job_func(self):
method test_to_string_functools_partial_job_func (line 1377) | def test_to_string_functools_partial_job_func(self):
method test_run_pending (line 1387) | def test_run_pending(self):
method test_run_every_weekday_at_specific_time_today (line 1427) | def test_run_every_weekday_at_specific_time_today(self):
method test_run_every_weekday_at_specific_time_past_today (line 1438) | def test_run_every_weekday_at_specific_time_past_today(self):
method test_run_every_n_days_at_specific_time (line 1453) | def test_run_every_n_days_at_specific_time(self):
method test_next_run_property (line 1480) | def test_next_run_property(self):
method test_idle_seconds (line 1491) | def test_idle_seconds(self):
method test_cancel_job (line 1503) | def test_cancel_job(self):
method test_cancel_jobs (line 1525) | def test_cancel_jobs(self):
method test_tag_type_enforcement (line 1537) | def test_tag_type_enforcement(self):
method test_get_by_tag (line 1544) | def test_get_by_tag(self):
method test_clear_by_tag (line 1572) | def test_clear_by_tag(self):
method test_misconfigured_job_wont_break_scheduler (line 1591) | def test_misconfigured_job_wont_break_scheduler(self):
Condensed preview — 32 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (174K chars).
[
{
"path": ".github/workflows/ci.yml",
"chars": 2330,
"preview": "name: Tests\n\non:\n push:\n branches:\n - master\n pull_request:\n branches:\n - master\n\njobs:\n test:\n ru"
},
{
"path": ".gitignore",
"chars": 404,
"preview": "*.py[cod]\n\n# C extensions\n*.so\n\n# Packages\n*.egg\n*.egg-info\ndist\nbuild\neggs\nparts\nbin\nvar\nsdist\ndevelop-eggs\n.installed."
},
{
"path": "AUTHORS.rst",
"chars": 2054,
"preview": "Thanks to all the wonderful folks who have contributed to schedule over the years:\n\n- mattss <https://github.com/mattss>"
},
{
"path": "HISTORY.rst",
"chars": 6743,
"preview": ".. :changelog:\n\nHistory\n-------\n\n1.2.2 (2024-05-25)\n++++++++++++++++++\n\n- Fix bugs in cross-timezone scheduling (#601, #"
},
{
"path": "LICENSE.txt",
"chars": 1099,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2013 Daniel Bader (http://dbader.org)\n\nPermission is hereby granted, free of charge"
},
{
"path": "MANIFEST.in",
"chars": 147,
"preview": "include README.rst\ninclude HISTORY.rst\ninclude LICENSE.txt\n\ninclude test_schedule.py\n\nrecursive-exclude * __pycache__\nre"
},
{
"path": "README.rst",
"chars": 2288,
"preview": "`schedule <https://schedule.readthedocs.io/>`__\n===============================================\n\n\n.. image:: https://git"
},
{
"path": "docs/Makefile",
"chars": 7614,
"preview": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS =\nSPHINXBUILD "
},
{
"path": "docs/_static/custom.css",
"chars": 40,
"preview": ".toctree-l1 {\n padding-bottom: 4px;\n}"
},
{
"path": "docs/_templates/sidebarintro.html",
"chars": 956,
"preview": "<h3>📰 Useful Links</h3>\n<ul>\n <li><a href=\"http://github.com/dbader/schedule\">Schedule @ GitHub</a></li>\n <li><a href="
},
{
"path": "docs/background-execution.rst",
"chars": 1652,
"preview": "Run in the background\n=====================\n\nOut of the box it is not possible to run the schedule in the background.\nHo"
},
{
"path": "docs/changelog.rst",
"chars": 28,
"preview": ".. include:: ../HISTORY.rst\n"
},
{
"path": "docs/conf.py",
"chars": 10641,
"preview": "# -*- coding: utf-8 -*-\n#\n# schedule documentation build configuration file, created by\n# sphinx-quickstart on Mon Nov "
},
{
"path": "docs/development.rst",
"chars": 1878,
"preview": "Development\n===========\n\nThese instructions are geared towards people who want to help develop this library.\n\nPreparing "
},
{
"path": "docs/examples.rst",
"chars": 7461,
"preview": "Examples\n========\n\nEager to get started? This page gives a good introduction to Schedule.\nIt assumes you already have Sc"
},
{
"path": "docs/exception-handling.rst",
"chars": 1162,
"preview": "Exception Handling\n##################\n\nSchedule doesn't catch exceptions that happen during job execution. Therefore any"
},
{
"path": "docs/faq.rst",
"chars": 3498,
"preview": "Frequently Asked Questions\n==========================\n\nFrequently asked questions on the usage of schedule.\nDid you get "
},
{
"path": "docs/index.rst",
"chars": 3157,
"preview": "schedule\n========\n\n\n.. image:: https://github.com/dbader/schedule/workflows/Tests/badge.svg\n :target: https://git"
},
{
"path": "docs/installation.rst",
"chars": 2963,
"preview": "Installation\n============\n\n\nPython version support\n######################\n\nWe recommend using the latest version of Pyth"
},
{
"path": "docs/logging.rst",
"chars": 1830,
"preview": "Logging\n=======\n\nSchedule logs messages to the Python logger named ``schedule`` at ``DEBUG`` level.\nTo receive logs from"
},
{
"path": "docs/multiple-schedulers.rst",
"chars": 865,
"preview": "Multiple schedulers\n###################\n\nYou can run as many jobs from a single scheduler as you wish.\nHowever, for larg"
},
{
"path": "docs/parallel-execution.rst",
"chars": 2125,
"preview": "Parallel execution\n==========================\n\n*I am trying to execute 50 items every 10 seconds, but from the my logs i"
},
{
"path": "docs/reference.rst",
"chars": 623,
"preview": "Reference\n=========\n\n.. module:: schedule\n\nThis part of the documentation covers all the interfaces of schedule.\n\nMain I"
},
{
"path": "docs/timezones.rst",
"chars": 3085,
"preview": "Timezone & Daylight Saving Time\n===============================\n\nTimezone in .at()\n~~~~~~~~~~~~~~~~~\n\nSchedule supports "
},
{
"path": "pyproject.toml",
"chars": 669,
"preview": "[build-system]\nrequires = [\"setuptools >= 61.0\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"schedule\"\nde"
},
{
"path": "requirements-dev.txt",
"chars": 104,
"preview": "docutils\nPygments\npytest\npytest-cov\npytest-flake8\nSphinx\nblack==20.8b1\nclick==8.0.4\nmypy\npytz\ntypes-pytz"
},
{
"path": "schedule/__init__.py",
"chars": 31983,
"preview": "\"\"\"\nPython job scheduling for humans.\n\ngithub.com/dbader/schedule\n\nAn in-process scheduler for periodic jobs that uses t"
},
{
"path": "schedule/py.typed",
"chars": 0,
"preview": ""
},
{
"path": "setup.cfg",
"chars": 22,
"preview": "[mypy]\nfiles=schedule\n"
},
{
"path": "setup.py",
"chars": 1588,
"preview": "import codecs\nfrom setuptools import setup\n\n\nSCHEDULE_VERSION = \"1.2.2\"\nSCHEDULE_DOWNLOAD_URL = \"https://github.com/dbad"
},
{
"path": "test_schedule.py",
"chars": 66565,
"preview": "\"\"\"Unit tests for schedule.py\"\"\"\n\nimport datetime\nimport functools\nfrom unittest import mock, TestCase\nimport os\nimport "
},
{
"path": "tox.ini",
"chars": 842,
"preview": "[tox]\nenvlist = py3{7,8,9,10,11,12}{,-pytz}\nskip_missing_interpreters = true\n\n\n[gh-actions]\npython =\n 3.7: py37, py37"
}
]
About this extraction
This page contains the full source code of the dbader/schedule GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 32 files (162.5 KB), approximately 43.1k tokens, and a symbol index with 153 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.