pax_global_header00006660000000000000000000000064146244302410014512gustar00rootroot0000000000000052 comment=82a43db1b938d8fdf60103bd41f329e06c8d3651 schedule-1.2.2/000077500000000000000000000000001462443024100133105ustar00rootroot00000000000000schedule-1.2.2/.github/000077500000000000000000000000001462443024100146505ustar00rootroot00000000000000schedule-1.2.2/.github/workflows/000077500000000000000000000000001462443024100167055ustar00rootroot00000000000000schedule-1.2.2/.github/workflows/ci.yml000066400000000000000000000044321462443024100200260ustar00rootroot00000000000000name: 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 schedule-1.2.2/.gitignore000066400000000000000000000006241462443024100153020ustar00rootroot00000000000000*.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 schedule-1.2.2/AUTHORS.rst000066400000000000000000000040061462443024100151670ustar00rootroot00000000000000Thanks to all the wonderful folks who have contributed to schedule over the years: - mattss - mrhwick - cfrco - matrixise - abultman - mplewis - WoLfulus - dylwhich - fkromer - alaingilbert - Zerrossetto - yetingsky - schnepp - grampajoe - gilbsgilbs - Nathan Wailes - Connor Skees - qmorek - aisk - MichaelCorleoneLi - sijmenhuizenga - eladbi - chankeypathak - vubon - gaguirregabiria - rhagenaars - Skenvy - zcking - Martin Thoma - ebllg - fredthomsen - biggerfisch - sosolidkk - rudSarkar - chrimaho - jweijers - Akuli - NaelsonDouglas - SergBobrovsky - CPickens42 - emollier - sunpro108 - kurtasov - AnezeR - a-detiste schedule-1.2.2/HISTORY.rst000066400000000000000000000151271462443024100152110ustar00rootroot00000000000000.. :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 schedule-1.2.2/LICENSE.txt000066400000000000000000000021131462443024100151300ustar00rootroot00000000000000The 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. schedule-1.2.2/MANIFEST.in000066400000000000000000000002231462443024100150430ustar00rootroot00000000000000include README.rst include HISTORY.rst include LICENSE.txt include test_schedule.py recursive-exclude * __pycache__ recursive-exclude * *.py[co] schedule-1.2.2/README.rst000066400000000000000000000043601462443024100150020ustar00rootroot00000000000000`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 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 `_. Meta ---- Daniel Bader - `@dbader_org `_ - mail@dbader.org Inspired by `Adam Wiggins' `_ article `"Rethinking Cron" `_ and the `clockwork `_ Ruby module. Distributed under the MIT license. See `LICENSE.txt `_ for more information. https://github.com/dbader/schedule schedule-1.2.2/docs/000077500000000000000000000000001462443024100142405ustar00rootroot00000000000000schedule-1.2.2/docs/Makefile000066400000000000000000000166761462443024100157200ustar00rootroot00000000000000# 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 ' where 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." schedule-1.2.2/docs/_static/000077500000000000000000000000001462443024100156665ustar00rootroot00000000000000schedule-1.2.2/docs/_static/custom.css000066400000000000000000000000501462443024100177050ustar00rootroot00000000000000.toctree-l1 { padding-bottom: 4px; }schedule-1.2.2/docs/_templates/000077500000000000000000000000001462443024100163755ustar00rootroot00000000000000schedule-1.2.2/docs/_templates/sidebarintro.html000066400000000000000000000017021462443024100217500ustar00rootroot00000000000000

📰 Useful Links

🐍 More Python

schedule-1.2.2/docs/background-execution.rst000066400000000000000000000031641462443024100211160ustar00rootroot00000000000000Run 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() schedule-1.2.2/docs/changelog.rst000066400000000000000000000000341462443024100167160ustar00rootroot00000000000000.. include:: ../HISTORY.rst schedule-1.2.2/docs/conf.py000066400000000000000000000246211462443024100155440ustar00rootroot00000000000000# -*- 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, Daniel Bader' author = u'Daniel Bader' # 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. # " v 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 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"] schedule-1.2.2/docs/development.rst000066400000000000000000000035261462443024100173220ustar00rootroot00000000000000Development =========== 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 `_. 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 `_. It is processed using `Sphinx `_ using the `alabaster `_ 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 `_.` schedule-1.2.2/docs/examples.rst000066400000000000000000000164451462443024100166220ustar00rootroot00000000000000Examples ======== 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) schedule-1.2.2/docs/exception-handling.rst000066400000000000000000000022121462443024100205470ustar00rootroot00000000000000Exception 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 `_. schedule-1.2.2/docs/faq.rst000066400000000000000000000066551462443024100155550ustar00rootroot00000000000000Frequently 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 `_ 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 `. What if my task throws an exception? ------------------------------------ See :doc:`Exception Handling `. How can I run a job only once? ------------------------------ See :doc:`Examples `. How can I cancel several jobs at once? -------------------------------------- See :doc:`Examples `. How to execute jobs in parallel? -------------------------------- See :doc:`Parallel Execution `. How to continuously run the scheduler without blocking the main thread? ----------------------------------------------------------------------- :doc:`Background Execution`. Another question? ----------------- If you are left with an unanswered question, `browse the issue tracker `_ to see if your question has been asked before. Feel free to create a new issue if that's not the case. Thank you 😃schedule-1.2.2/docs/index.rst000066400000000000000000000061301462443024100161010ustar00rootroot00000000000000schedule ======== .. 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 ` ------------------------- .. 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 `_ 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 `__ - `@dbader_org `_ Inspired by `Adam Wiggins' `_ article `"Rethinking Cron" `_ and the `clockwork `_ Ruby module. Distributed under the MIT license. See ``LICENSE.txt`` for more information. .. include:: ../AUTHORS.rst schedule-1.2.2/docs/installation.rst000066400000000000000000000056231462443024100175010ustar00rootroot00000000000000Installation ============ 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 `_ 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 ` or go to the :doc:`the documentation overview `. 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 `__. 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 `__. Arch ---- On the Arch Linux User repository (AUR) the package is available using the name `python-schedule`. See the package page `here `__. For yay users, run: .. code-block:: bash $ yay -S python-schedule Conda (Anaconda) ---------------- Schedule is `published `__ in conda (the Anaconda package manager). For installation instructions, visit `the conda-forge Schedule repo `__. The release of Schedule on conda is maintained by the `conda-forge project `__. 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 `_. 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`. schedule-1.2.2/docs/logging.rst000066400000000000000000000034461462443024100164270ustar00rootroot00000000000000Logging ======= 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 schedule-1.2.2/docs/multiple-schedulers.rst000066400000000000000000000015411462443024100207650ustar00rootroot00000000000000Multiple 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) schedule-1.2.2/docs/parallel-execution.rst000066400000000000000000000041151462443024100205700ustar00rootroot00000000000000Parallel 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. schedule-1.2.2/docs/reference.rst000066400000000000000000000011571462443024100167340ustar00rootroot00000000000000Reference ========= .. 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.CancelJobschedule-1.2.2/docs/timezones.rst000066400000000000000000000060151462443024100170110ustar00rootroot00000000000000Timezone & 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 `_ **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 `_. 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) schedule-1.2.2/pyproject.toml000066400000000000000000000012351462443024100162250ustar00rootroot00000000000000[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" schedule-1.2.2/requirements-dev.txt000066400000000000000000000001501462443024100173440ustar00rootroot00000000000000docutils Pygments pytest pytest-cov pytest-flake8 Sphinx black==20.8b1 click==8.0.4 mypy pytz types-pytzschedule-1.2.2/schedule/000077500000000000000000000000001462443024100151045ustar00rootroot00000000000000schedule-1.2.2/schedule/__init__.py000066400000000000000000000763571462443024100172370ustar00rootroot00000000000000""" 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 ` 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(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 ` 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 ` 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 ` * 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 ` object default_scheduler = Scheduler() #: Default :class:`Jobs ` list jobs = default_scheduler.jobs # todo: should this be a copy, e.g. jobs()? def every(interval: int = 1) -> Job: """Calls :meth:`every ` on the :data:`default scheduler instance `. """ return default_scheduler.every(interval) def run_pending() -> None: """Calls :meth:`run_pending ` on the :data:`default scheduler instance `. """ default_scheduler.run_pending() def run_all(delay_seconds: int = 0) -> None: """Calls :meth:`run_all ` on the :data:`default scheduler instance `. """ default_scheduler.run_all(delay_seconds=delay_seconds) def get_jobs(tag: Optional[Hashable] = None) -> List[Job]: """Calls :meth:`get_jobs ` on the :data:`default scheduler instance `. """ return default_scheduler.get_jobs(tag) def clear(tag: Optional[Hashable] = None) -> None: """Calls :meth:`clear ` on the :data:`default scheduler instance `. """ default_scheduler.clear(tag) def cancel_job(job: Job) -> None: """Calls :meth:`cancel_job ` on the :data:`default scheduler instance `. """ default_scheduler.cancel_job(job) def next_run(tag: Optional[Hashable] = None) -> Optional[datetime.datetime]: """Calls :meth:`next_run ` on the :data:`default scheduler instance `. """ return default_scheduler.get_next_run(tag) def idle_seconds() -> Optional[float]: """Calls :meth:`idle_seconds ` on the :data:`default scheduler instance `. """ 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 ` """ 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) schedule-1.2.2/schedule/py.typed000066400000000000000000000000001462443024100165710ustar00rootroot00000000000000schedule-1.2.2/setup.cfg000066400000000000000000000000261462443024100151270ustar00rootroot00000000000000[mypy] files=schedule schedule-1.2.2/setup.py000066400000000000000000000030641462443024100150250ustar00rootroot00000000000000import 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", ) schedule-1.2.2/test_schedule.py000066400000000000000000002020051462443024100165140ustar00rootroot00000000000000"""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() schedule-1.2.2/tox.ini000066400000000000000000000015121462443024100146220ustar00rootroot00000000000000[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