├── .gitignore ├── .travis.yml ├── .vscode └── settings.json ├── AUTHORS.rst ├── FAQ.rst ├── HISTORY.rst ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── README.rst ├── VERSION ├── Vagrantfile ├── aioschedule ├── __init__.py └── package.json ├── config.mk ├── dev └── vim.conf ├── docs ├── Makefile ├── _static │ └── placeholder.txt ├── _templates │ └── sidebarintro.html ├── api.rst ├── conf.py ├── faq.rst └── index.rst ├── ops ├── gitlab │ ├── defaults.yml │ ├── user-defined.yml │ └── variables.yml └── make │ ├── docker.mk │ ├── python-docs.mk │ ├── python-gitlab.mk │ ├── python-package.mk │ ├── python-parent.mk │ └── python.mk ├── requirements-dev.txt ├── setup.py ├── test_schedule.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | .lib 3 | 4 | # C extensions 5 | *.so 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | MANIFEST 22 | 23 | # Installer logs 24 | pip-log.txt 25 | 26 | # Unit test / coverage reports 27 | .coverage 28 | .tox 29 | nosetests.xml 30 | 31 | # Translations 32 | *.mo 33 | 34 | # Mr Developer 35 | .mr.developer.cfg 36 | .project 37 | .pydevproject 38 | 39 | env 40 | env3 41 | __pycache__ 42 | venv 43 | 44 | .cache 45 | docs/_build 46 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - "3.5" 5 | - "3.6" 6 | install: pip install tox-travis coveralls 7 | script: 8 | - tox 9 | - if [ $TRAVIS_TEST_RESULT -eq 0 ]; then coveralls; fi 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "terminal.integrated.fontSize": 14, 3 | "python.analysis.typeCheckingMode": "strict", 4 | "python.analysis.extraPaths": [ 5 | "${workspaceFolder}/src/libcanonical", 6 | "${workspaceFolder}/src/canonical/ext/google", 7 | "${workspaceFolder}/src/canonical/ext/repository" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Thanks to all the wonderful folks who have contributed to schedule over the years: 2 | 3 | - mattss 4 | - mrhwick 5 | - cfrco 6 | - matrixise 7 | - abultman 8 | - mplewis 9 | - WoLfulus 10 | - dylwhich 11 | - fkromer 12 | - alaingilbert 13 | - Zerrossetto 14 | - yetingsky 15 | - schnepp 16 | - grampajoe 17 | - gilbsgilbs 18 | -------------------------------------------------------------------------------- /FAQ.rst: -------------------------------------------------------------------------------- 1 | .. _frequently-asked-questions: 2 | 3 | Frequently Asked Questions 4 | ========================== 5 | 6 | Frequently asked questions on the usage of ``aioschedule``. 7 | 8 | How to continuously run the scheduler without blocking the main thread? 9 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 10 | 11 | Run the scheduler in a separate thread. Mrwhick wrote up a nice solution in to this problem `here `__ (look for ``run_continuously()``) 12 | 13 | Does schedule support timezones? 14 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 15 | 16 | Vanilla schedule doesn't support timezones at the moment. If you need this functionality please check out @imiric's work `here `__. He added timezone support to schedule using python-dateutil. 17 | 18 | What if my task throws an exception? 19 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 20 | 21 | 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. 22 | 23 | If you want to guard against exceptions you can wrap your job function 24 | in a decorator like this: 25 | 26 | .. code-block:: python 27 | 28 | import functools 29 | 30 | def catch_exceptions(job_func, cancel_on_failure=False): 31 | @functools.wraps(job_func) 32 | def wrapper(*args, **kwargs): 33 | try: 34 | return job_func(*args, **kwargs) 35 | except: 36 | import traceback 37 | print(traceback.format_exc()) 38 | if cancel_on_failure: 39 | return schedule.CancelJob 40 | return wrapper 41 | 42 | @catch_exceptions(cancel_on_failure=True) 43 | def bad_task(): 44 | return 1 / 0 45 | 46 | schedule.every(5).minutes.do(bad_task) 47 | 48 | Another option would be to subclass Schedule like @mplewis did in `this example `_. 49 | 50 | How can I run a job only once? 51 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 52 | 53 | .. code-block:: python 54 | 55 | def job_that_executes_once(): 56 | # Do some work ... 57 | return schedule.CancelJob 58 | 59 | schedule.every().day.at('22:30').do(job_that_executes_once) 60 | 61 | 62 | How can I cancel several jobs at once? 63 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 64 | 65 | You can cancel the scheduling of a group of jobs selecting them by a unique identifier. 66 | 67 | .. code-block:: python 68 | 69 | def greet(name): 70 | print('Hello {}'.format(name)) 71 | 72 | schedule.every().day.do(greet, 'Andrea').tag('daily-tasks', 'friend') 73 | schedule.every().hour.do(greet, 'John').tag('hourly-tasks', 'friend') 74 | schedule.every().hour.do(greet, 'Monica').tag('hourly-tasks', 'customer') 75 | schedule.every().day.do(greet, 'Derek').tag('daily-tasks', 'guest') 76 | 77 | schedule.clear('daily-tasks') 78 | 79 | Will prevent every job tagged as ``daily-tasks`` from running again. 80 | 81 | 82 | I'm getting an ``AttributeError: 'module' object has no attribute 'every'`` when I try to use schedule. How can I fix this? 83 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 84 | 85 | 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. 86 | 87 | How can I add generic logging to my scheduled jobs? 88 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 89 | 90 | The easiest way to add generic logging functionality to your schedule 91 | job functions is to implement a decorator that handles logging 92 | in a reusable way: 93 | 94 | .. code-block:: python 95 | 96 | import functools 97 | import time 98 | 99 | import schedule 100 | 101 | 102 | # This decorator can be applied to 103 | def with_logging(func): 104 | @functools.wraps(func) 105 | def wrapper(*args, **kwargs): 106 | print('LOG: Running job "%s"' % func.__name__) 107 | result = func(*args, **kwargs) 108 | print('LOG: Job "%s" completed' % func.__name__) 109 | return result 110 | return wrapper 111 | 112 | @with_logging 113 | def job(): 114 | print('Hello, World.') 115 | 116 | schedule.every(3).seconds.do(job) 117 | 118 | while 1: 119 | schedule.run_pending() 120 | time.sleep(1) 121 | 122 | How to run a job at random intervals? 123 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 124 | 125 | .. code-block:: python 126 | 127 | def my_job(): 128 | # This job will execute every 5 to 10 seconds. 129 | print('Foo') 130 | 131 | schedule.every(5).to(10).seconds.do(my_job) 132 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | History 4 | ------- 5 | 6 | 0.5.0 (2017-11-16) 7 | ++++++++++++++++++ 8 | 9 | - Keep partially scheduled jobs from breaking the scheduler (#125) 10 | - Add support for random intervals (Thanks @grampajoe and @gilbsgilbs) 11 | 12 | 13 | 0.4.3 (2017-06-10) 14 | ++++++++++++++++++ 15 | 16 | - Improve docs & clean up docstrings 17 | 18 | 19 | 0.4.2 (2016-11-29) 20 | ++++++++++++++++++ 21 | 22 | - Publish to PyPI as a universal (py2/py3) wheel 23 | 24 | 25 | 0.4.0 (2016-11-28) 26 | ++++++++++++++++++ 27 | 28 | - Add proper HTML (Sphinx) docs available at https://schedule.readthedocs.io/ 29 | - CI builds now run against Python 2.7 and 3.5 (3.3 and 3.4 should work fine but are untested) 30 | - Fixed an issue with ``run_all()`` and having more than one job that deletes itself in the same iteration. Thanks @alaingilbert. 31 | - Add ability to tag jobs and to cancel jobs by tag. Thanks @Zerrossetto. 32 | - Improve schedule docs. Thanks @Zerrossetto. 33 | - Additional docs fixes by @fkromer and @yetingsky. 34 | 35 | 0.3.2 (2015-07-02) 36 | ++++++++++++++++++ 37 | 38 | - Fixed issues where scheduling a job with a functools.partial as the job function fails. Thanks @dylwhich. 39 | - 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. 40 | - Added a FAQ item to describe how to schedule a job that runs only once. 41 | 42 | 0.3.1 (2014-09-03) 43 | ++++++++++++++++++ 44 | 45 | - 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. 46 | - Added an FAQ item to describe how to deal with job functions that throw exceptions. Thanks @mplewis. 47 | 48 | 0.3.0 (2014-06-14) 49 | ++++++++++++++++++ 50 | 51 | - 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.) 52 | - 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. 53 | 54 | 0.2.1 (2013-11-20) 55 | ++++++++++++++++++ 56 | 57 | - Fixed history (no code changes). 58 | 59 | 0.2.0 (2013-11-09) 60 | ++++++++++++++++++ 61 | 62 | - This release introduces two new features in a backwards compatible way: 63 | - 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.) 64 | - 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.) 65 | - Refactored unit tests to mock ``datetime`` in a cleaner way. (Thanks @matts.) 66 | 67 | 0.1.11 (2013-07-30) 68 | +++++++++++++++++++ 69 | 70 | - 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. 71 | 72 | 0.1.10 (2013-06-07) 73 | +++++++++++++++++++ 74 | 75 | - Fixed issue with ``at_time`` jobs not running on the same day the job is created (Thanks to @mattss) 76 | 77 | 0.1.9 (2013-05-27) 78 | ++++++++++++++++++ 79 | 80 | - Added ``schedule.next_run()`` 81 | - Added ``schedule.idle_seconds()`` 82 | - Args passed into ``do()`` are forwarded to the job function at call time 83 | - Increased test coverage to 100% 84 | 85 | 86 | 0.1.8 (2013-05-21) 87 | ++++++++++++++++++ 88 | 89 | - Changed default ``delay_seconds`` for ``schedule.run_all()`` to 0 (from 60) 90 | - Increased test coverage 91 | 92 | 0.1.7 (2013-05-20) 93 | ++++++++++++++++++ 94 | 95 | - API change: renamed ``schedule.run_all_jobs()`` to ``schedule.run_all()`` 96 | - API change: renamed ``schedule.run_pending_jobs()`` to ``schedule.run_pending()`` 97 | - API change: renamed ``schedule.clear_all_jobs()`` to ``schedule.clear()`` 98 | - Added ``schedule.jobs`` 99 | 100 | 0.1.6 (2013-05-20) 101 | ++++++++++++++++++ 102 | 103 | - Fix packaging 104 | - README fixes 105 | 106 | 0.1.4 (2013-05-20) 107 | ++++++++++++++++++ 108 | 109 | - API change: renamed ``schedule.tick()`` to ``schedule.run_pending_jobs()`` 110 | - Updated README and ``setup.py`` packaging 111 | 112 | 0.1.0 (2013-05-19) 113 | ++++++++++++++++++ 114 | 115 | - Initial release 116 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Daniel Bader (http://dbader.org) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include HISTORY.rst 3 | include LICENSE.txt 4 | include test_schedule.py 5 | include aioschedule/package.json 6 | 7 | recursive-exclude * __pycache__ 8 | recursive-exclude * *.py[co] 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # UNIMAKE MASTER MAKEFILE (PYTHON) 4 | # 5 | # 6 | ############################################################################### 7 | ifneq ($(wildcard config.mk),) 8 | include config.mk 9 | endif 10 | ifneq ($(wildcard local.mk),) 11 | include local.mk 12 | endif 13 | APP_RUNDIR ?= . 14 | APP_SECDIR ?= var/run/secrets 15 | APP_PKIDIR ?= pki 16 | APP_PKIDIR_SERVER ?= $(APP_PKIDIR)/server 17 | APP_PKIDIR_PKCS ?= $(APP_PKIDIR)/pkcs 18 | COVERAGE_CONFIG=.coveragerc 19 | COVERAGE_FILE = .coverage-$(TEST_STAGE) 20 | ifdef CI_JOB_ID 21 | COVERAGE_FILE = .coverage-$(CI_JOB_ID) 22 | endif 23 | CURDIR ?= $(shell pwd) 24 | DEBUG=1 25 | DEPLOYMENT_ENV=local 26 | DEPSCHECKSUMFILES += config.mk 27 | DOCKER_BASE_TAG ?= $(PYTHON_VERSION)-$(OS_RELEASE_ID)$(OS_RELEASE_VERSION) 28 | DOCKER_BASE_IMAGE ?= python:$(DOCKER_BASE_TAG) 29 | HTML_COVERAGE_DIR=$(HTML_DOCUMENT_ROOT)/coverage 30 | HTML_DOCUMENT_ROOT=public 31 | MAKEFILE=Makefile 32 | ifdef CI_JOB_ID 33 | # It is assumed here that in a CI/CD pipeline environment, there is only one 34 | # Python version installed. 35 | PYTHON = python3 36 | endif 37 | ifndef PYTHON_VERSION 38 | PYTHON_VERSION = 3.9 39 | endif 40 | PYTHON ?= python$(PYTHON_VERSION) 41 | PIP ?= $(PYTHON) -m pip 42 | PIP_INSTALL=$(PIP) install 43 | PYTEST_ROOT_CONFTEST = $(PYTHON_PKG_WORKDIR)/conftest.py 44 | PYTHONPATH=$(CURDIR):$(CURDIR)/$(PYTHON_RUNTIME_LIBS):$(CURDIR)/$(PYTHON_TESTING_LIBS):$(CURDIR)/.lib/python/docs:$(CURDIR)/$(PYTHON_INFRA_LIBS) 45 | PYTHON_INFRA_LIBS=.lib/python/infra 46 | PYTHON_PKG_NAME ?= $(error Define PYTHON_PKG_NAME in config.mk) 47 | PYTHON_REQUIREMENTS ?= requirements.txt 48 | PYTHON_RUNTIME_LIBS=.lib/python/runtime 49 | PYTHON_RUNTIME_PACKAGES += 'unimatrix>=0.2.1' python-ioc 50 | PYTHON_RUNTIME_PKG ?= $(PYTHON_PKG_WORKDIR)/runtime 51 | PYTHON_SEED_URL=$(SEED_URL)/python 52 | PYTHON_SHELL ?= $(PYTHON) 53 | PYTHON_SUBPKG_DIR ?= $(PYTHON_PKG_NAME)/ext 54 | PYTHON_SUBPKG_PATH=$(PYTHON_SUBPKG_DIR)/$(PYTHON_SUBPKG_NAME) 55 | PYTHON_TEST_PACKAGES += bandit safety yamllint pylint twine sphinx 56 | PYTHON_TEST_PACKAGES += pytest pytest-cov pytest-asyncio semver piprot doc8 57 | PYTHON_TEST_PACKAGES += watchdog argh 'chardet<4,>=3.0.2' 58 | PYTHON_TEST_PACKAGES += flake8 flake8-print 59 | PYTHON_TESTING_LIBS=.lib/python/testing 60 | PYTHON_WATCH = $(PYTHON) -c 'from watchdog.watchmedo import main; main()' 61 | PYTHON_WATCH += auto-restart --directory=$(PYTHON_PKG_WORKDIR) --pattern=*.py --recursive 62 | SECRET_KEY=0000000000000000000000000000000000000000000000000000000000000000 63 | SEED_URL=https://gitlab.com/unimatrixone/seed/-/raw/master 64 | SEMVER_FILE ?= VERSION 65 | TWINE_USERNAME ?= $(PYPI_USERNAME) 66 | TWINE_PASSWORD ?= $(PYPI_PASSWORD) 67 | UNIMAKE_INCLUDE_DIR=ops/make 68 | UNIMAKE_TEMPLATE_DIR=.lib/templates 69 | UNIMAKE=$(PYTHON) -m unimake 70 | ifneq ($(wildcard .git),) 71 | GIT_REMOTE=$(shell git remote get-url origin) 72 | GIT_COMMIT_HASH=$(shell git rev-parse --short HEAD | tr -d "\n") 73 | VCS_COMMIT_HASH=$(GIT_COMMIT_HASH) 74 | endif 75 | ifneq ($(wildcard ./ops/make/*.mk),) 76 | include ops/make/*.mk 77 | endif 78 | PYTHON ?= python3 79 | OS_RELEASE_ID ?= alpine 80 | OS_RELEASE_VERSION ?= 3.12 81 | TEST_STAGE ?= any 82 | export 83 | build.alpine.packages += curl gcc g++ git libc-dev libffi-dev libressl-dev make 84 | build.debian.packages += curl gcc g++ git libc-dev libffi-dev libssl-dev make 85 | cmd.curl=curl --fail --silent -H 'Cache-Control: no-cache' 86 | cmd.git=git 87 | cmd.git.add=$(cmd.git) add 88 | cmd.openssl = openssl 89 | cmd.screen = screen -c screen.conf 90 | cmd.semver=$(PYTHON) -c "import semver; semver.main()" 91 | cmd.sh=bash 92 | cmd.sha1sum=sha1sum 93 | cmd.sha256sum=sha256sum 94 | mk.configure += python-$(PROJECT_SCOPE) 95 | os.alpine.pkg.install ?= apk add --no-cache 96 | os.debian.pkg.install ?= apt-get install -y 97 | os.alpine.pkg.update ?= apk update 98 | os.debian.pkg.update ?= apt-get update -y 99 | ifndef project.title 100 | project.title = Enter a Project Title 101 | endif 102 | unimake.template.ctx += -v project_title=$(project_title) 103 | vcs.defaults.mainline ?= origin/mainline 104 | vcs.defaults.stable ?= origin/stable 105 | 106 | 107 | # Block until the required infrastructure services are available. 108 | awaitservices: 109 | @$(cmd.awaitservices) 110 | 111 | 112 | bash: 113 | @PS1="env: λ " $(cmd.sh) 114 | 115 | 116 | # Bootstraps a project. The implementations in ./ops should add their targets 117 | # as a dependency. 118 | bootstrap: 119 | @$(MAKE) post-bootstrap 120 | @$(cmd.git.add) . 121 | @$(cmd.git) commit -m "Bootstrap project with Unimake" 122 | 123 | 124 | # Check if the packaging mechanics are ok. 125 | check-package: 126 | @$(cmd.check-package) 127 | 128 | 129 | # Check if there are CVEs in the project dependencies. 130 | check-requirements-cve: 131 | @$(cmd.check-requirements-cve) 132 | 133 | 134 | # Check for outdated requirements/dependencies. 135 | check-requirements-outdated: 136 | @$(cmd.check-requirements-outdated) 137 | 138 | 139 | ci-runner-id: 140 | @echo $(OS_RELEASE_ID)$(OS_RELEASE_VERSION) 141 | 142 | 143 | # Completely cleans the working tree. 144 | clean: depsclean distclean envclean pkgclean docsclean htmlclean testclean destroyinfra 145 | @rm -rf .lib 146 | ifneq ($(wildcard .git),) 147 | @$(cmd.git) clean -fdx 148 | endif 149 | 150 | 151 | # Ensures that all includes specified in mk.configure are present inthe 152 | # ./ops/mk directory. 153 | configure: 154 | @mkdir -p $(UNIMAKE_INCLUDE_DIR) 155 | @echo "Configuring $(mk.configure)" 156 | @$(MAKE) $(addprefix $(UNIMAKE_INCLUDE_DIR)/, $(addsuffix .mk, $(mk.configure))) 157 | 158 | 159 | # Spawn an interactive interpreter if the language supports it. 160 | console: 161 | @$(cmd.console) 162 | 163 | 164 | # Create a CHECKSUMS file that contains a checksum of the source dependencies 165 | # for this project. This file is used by the CI to determine if a testing 166 | # environment should be rebuilt. 167 | depschecksums: 168 | ifdef DEPSCHECKSUMFILES 169 | @$(cmd.sha256sum) $(DEPSCHECKSUMFILES) > CHECKSUMS 170 | else 171 | @touch CHECKSUMS 172 | endif 173 | 174 | 175 | # Build the documentation 176 | documentation: 177 | @$(cmd.documentation) 178 | 179 | 180 | # Destroys the local infrastructure used for development. 181 | destroyinfra: killinfra 182 | @rm -rf ./var 183 | 184 | 185 | # Remove documentation build artifacts. 186 | docsclean: 187 | @$(cmd.docsclean) 188 | 189 | 190 | # Remove dependencies from the source tree. 191 | depsclean: 192 | @$(cmd.depsclean) 193 | 194 | 195 | # Installs the project dependencies, relative to the current working 196 | # directory. 197 | depsinstall: 198 | 199 | 200 | # Remove dependencies from the source tree and rebuild them or download from 201 | # packaging services. 202 | depsrebuild: depsclean 203 | @$(MAKE) depsinstall 204 | 205 | 206 | # Remove artifacts created by packaging. 207 | distclean: 208 | @$(cmd.distclean) 209 | 210 | 211 | # Setup the local development environment. 212 | env: 213 | 214 | 215 | # Cleans the local development environment. 216 | envclean: 217 | 218 | 219 | # Remove HTML artifacts 220 | htmlclean: 221 | @$(cmd.htmlclean) 222 | 223 | 224 | # Kill the local infrastructure 225 | killinfra: 226 | @$(cmd.killinfra) 227 | 228 | 229 | # Run documentation linting tools. 230 | lint-docs: 231 | @$(cmd.lint-docs) 232 | 233 | 234 | # Lint exceptions 235 | lint-exceptions: 236 | @$(cmd.lint-exceptions) 237 | 238 | 239 | # Exit nonzero if the source code contains files that have an inappropriate 240 | # import orde. 241 | lint-import-order: 242 | @$(or $(cmd.lint.import-order), $(error Set cmd.lint.import-order)) 243 | 244 | 245 | # Exit nonzero if a maximum line length is exceeded by source code. 246 | lint-line-length: 247 | @$(or $(cmd.lint.line-length), $(error Set cmd.lint.line-length)) 248 | 249 | 250 | # Ensures that there are no print statements or other unwanted calls. 251 | lint-nodebug: 252 | @$(or $(cmd.lint-nodebug), $(error Set cmd.lint-nodebug)) 253 | 254 | 255 | lint-security: 256 | @$(or $(cmd.lint-security), $(error Set cmd.lint-security)) 257 | 258 | 259 | # Exit nonzero if there is trailing whitespace. 260 | lint-trailing-whitespace: 261 | @$(or $(cmd.lint.trailing-whitespace), $(error Set cmd.lint.trailing-whitespace)) 262 | 263 | 264 | # Exit nonzero if the source code contains unused imports. 265 | lint-unused: 266 | @$(or $(cmd.lint.unused), $(error Set cmd.lint.unused)) 267 | 268 | 269 | # Exit nonzero if any YAML file in the source tree violates the linting 270 | # requirements. 271 | lint-yaml: 272 | @$(PYTHON) -m yamllint . 273 | 274 | 275 | # Ensures that database migrations are ran. 276 | migrate: 277 | @$(cmd.migrate) 278 | 279 | 280 | # Render database migrations 281 | migrations: 282 | @$(cmd.migrations) 283 | 284 | 285 | # Render database migrations for a specific module. 286 | migrations-%: 287 | @$(MAKE) migrations MODULE_NAME=$(*) 288 | 289 | 290 | # Rebuild the environment 291 | rebuild: killinfra 292 | @$(MAKE) clean && $(MAKE) env 293 | 294 | 295 | # Remove temporary files from the package source tree. 296 | pkgclean: 297 | @$(cmd.pkgclean) 298 | 299 | 300 | # Publish the package to a package registry. 301 | publish: distclean 302 | @$(cmd.dist) 303 | @$(cmd.publish) 304 | 305 | 306 | rebase: 307 | @$(cmd.git) remote update && $(cmd.git) rebase $(vcs.defaults.mainline) 308 | 309 | 310 | # Resets the infrastructure and its storage to a pristine state. 311 | resetinfra: destroyinfra 312 | @$(MAKE) runinfra DAEMONIZE_SERVICES=1 313 | 314 | 315 | # Run all application components and infrastructure service dependencies 316 | # in a single process. Kills existing infrastructure to run it in the 317 | # foreground. 318 | run: screen.conf killinfra 319 | @$(cmd.screen) 320 | 321 | 322 | # For server applications, such as HTTP, starts the program and binds 323 | # it to a well-known local port. If the application also exposes a 324 | # websocket, then it is assumed that during development it is served 325 | # by the same process as the main application. 326 | runhttp: $(APP_PKIDIR_SERVER)/tls.crt 327 | @$(cmd.runhttp) 328 | 329 | 330 | # Runs infrastructure services on the local machine. 331 | runinfra: 332 | @$(cmd.runinfra) 333 | 334 | 335 | # Runs the infrastructure services daemonized. 336 | runinfra-daemon: 337 | @$(MAKE) runinfra DAEMONIZE_SERVICES=1 338 | 339 | 340 | # Run a script 341 | runscript: 342 | ifdef path 343 | @$(PYTHON) $(path) 344 | endif 345 | 346 | 347 | # Run the worker. 348 | runworker: 349 | @$(cmd.runworker) 350 | 351 | 352 | # Run tests, excluding system tests. 353 | runtests: 354 | @$(MAKE) test-unit test-integration 355 | @$(MAKE) testcoverage test.coverage=$(test.coverage.nonsystem) 356 | 357 | 358 | # Print the current version. 359 | semantic-version: 360 | @cat $(SEMVER_FILE) 361 | 362 | 363 | # Clean artifacts created during tests. 364 | testclean: 365 | 366 | 367 | testcoverage: 368 | @$(cmd.testcoverage) 369 | 370 | 371 | test-%: 372 | @$(MAKE) pkgclean 373 | @$(MAKE) test TEST_STAGE=$(*) 374 | 375 | 376 | test: pkgclean 377 | @$(cmd.runtests) 378 | 379 | 380 | testall: runinfra-daemon 381 | @$(MAKE) testclean 382 | @$(MAKE) -j3 test-unit test-integration test-system 383 | @$(MAKE) testcoverage 384 | 385 | 386 | # Invoke when the bootstrap target finishes. 387 | post-bootstrap: 388 | 389 | 390 | # Updates the UniMake includes. 391 | update: 392 | @$(cmd.curl) $(SEED_URL)/Makefile > $(MAKEFILE) 393 | @$(cmd.git.add) $(MAKEFILE) 394 | @rm -f $(addprefix $(UNIMAKE_INCLUDE_DIR)/, $(addsuffix .mk, $(mk.configure))) 395 | @$(MAKE) $(addprefix $(UNIMAKE_INCLUDE_DIR)/, $(addsuffix .mk, $(mk.configure))) 396 | @$(cmd.git.add) -u && $(cmd.git) commit -m "Update GNU Make includes" 397 | 398 | 399 | # Watch source code for changes and run tests. 400 | watch: 401 | @$(or $(cmd.watch), $(shell echo "make watch is not implemented.")) 402 | 403 | 404 | # Export environment variables 405 | .env: 406 | @env | sort > .env 407 | 408 | 409 | # Creates the Git ignore rules. 410 | .gitignore: 411 | @$(cmd.curl) $(or $(seed.git.ignore), $(error Set seed.git.ignore))\ 412 | > .gitignore 413 | @$(cmd.git.add) .gitignore && $(cmd.git) commit -m "Add Git ignore rules" 414 | 415 | 416 | $(APP_PKIDIR): 417 | @mkdir -p $(APP_PKIDIR) 418 | 419 | 420 | $(APP_PKIDIR_SERVER): $(APP_PKIDIR) 421 | @mkdir -p $(APP_PKIDIR_SERVER) 422 | 423 | 424 | $(APP_PKIDIR_PKCS): $(APP_PKIDIR) 425 | @mkdir -p $(APP_PKIDIR_PKCS) 426 | 427 | 428 | $(APP_PKIDIR_SERVER)/tls.crt: $(APP_PKIDIR_SERVER) 429 | @$(cmd.openssl) req -new -newkey rsa:2048 -days 365 -nodes -x509\ 430 | -keyout $(APP_PKIDIR_SERVER)/tls.key -out $(APP_PKIDIR_SERVER)/tls.crt\ 431 | -subj "/C=NL/ST=Zuid-Holland/L=Den Haag/O=Unimatrix One/CN=localhost" 432 | @$(cmd.git) add $(APP_PKIDIR_SERVER)/*\ 433 | && $(cmd.git) commit -m "Add local development TLS certificate and key" 434 | 435 | 436 | $(APP_PKIDIR_PKCS)/noop.rsa: $(APP_PKIDIR_PKCS) 437 | @$(cmd.openssl) genrsa -out $(APP_PKIDIR_PKCS)/noop.rsa 438 | @$(cmd.git) add $(APP_PKIDIR_PKCS)/*\ 439 | && $(cmd.git) commit -m "Add local development private key" 440 | 441 | 442 | $(APP_PKIDIR_PKCS)/noop.pub: $(APP_PKIDIR_PKCS)/noop.rsa 443 | @$(cmd.openssl) rsa -pubout -in $(APP_PKIDIR_PKCS)/noop.rsa\ 444 | > $(APP_PKIDIR_PKCS)/noop.pub 445 | @$(cmd.git) add $(APP_PKIDIR_PKCS)/*\ 446 | && $(cmd.git) commit -m "Add local development public key" 447 | 448 | 449 | # Provide the bump-major, bump-minor and bump-patch targets if 450 | # SEMVER_FILE is defined. 451 | ifdef SEMVER_FILE 452 | bump-%: 453 | @$(cmd.semver) bump $(*) $$(cat $(SEMVER_FILE)) > $(SEMVER_FILE) 454 | @git add $(SEMVER_FILE) && git commit -m "Bump $(*) version" 455 | 456 | 457 | $(SEMVER_FILE): 458 | ifeq ($(wildcard $(SEMVER_FILE)),) 459 | @echo '0.0.1' | tr -d '\n' > $(SEMVER_FILE) 460 | @$(cmd.git.add) $(SEMVER_FILE) 461 | endif 462 | 463 | ifneq ($(wildcard $(SEMVER_FILE)),) 464 | SEMVER_RELEASE=$(shell cat $(SEMVER_FILE)) 465 | endif 466 | endif 467 | 468 | 469 | # Fetch UniMake include. 470 | $(UNIMAKE_INCLUDE_DIR)/%.mk: 471 | @echo "Updating $(UNIMAKE_INCLUDE_DIR)/$(*).mk" 472 | @$(cmd.curl) $(SEED_URL)/ops/$(*).mk > $(UNIMAKE_INCLUDE_DIR)/$(*).mk 473 | @$(MAKE) configure-$(*) 474 | @$(cmd.git.add) $(UNIMAKE_INCLUDE_DIR)/$(*).mk 475 | 476 | 477 | bootstrap: .gitignore 478 | configure: 479 | lint: lint-unused lint-security lint-line-length lint-import-order lint-trailing-whitespace 480 | prepush: lint testall 481 | run: env 482 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | aioschedule 2 | =========== 3 | 4 | 5 | .. image:: https://api.travis-ci.org/ibrb/python-aioschedule.svg?branch=master 6 | :target: https://travis-ci.org/ibrb/python-aioschedule 7 | 8 | .. image:: https://coveralls.io/repos/ibrb/python-aioschedule/badge.svg?branch=master 9 | :target: https://coveralls.io/r/ibrb/python-aioschedule 10 | 11 | .. image:: https://img.shields.io/pypi/v/aioschedule.svg 12 | :target: https://pypi.python.org/pypi/aioschedule 13 | 14 | .. image:: https://media.ibrb.org/ibr/images/logos/landscape1200.png 15 | :target: https://media.ibrb.org/ibr/images/logos/landscape1200.png 16 | 17 | 18 | Python job scheduling for humans. Forked and modified from github.com/dbader/schedule. 19 | 20 | An in-process scheduler for periodic jobs that uses the builder pattern 21 | for configuration. Schedule lets you run Python functions (or any other 22 | callable) periodically at pre-determined intervals using a simple, 23 | human-friendly syntax. 24 | 25 | Inspired by `Adam Wiggins' `_ article `"Rethinking Cron" `_ and the `clockwork `_ Ruby module. 26 | 27 | Features 28 | -------- 29 | - A simple to use API for scheduling jobs. 30 | - Very lightweight and no external dependencies. 31 | - Excellent test coverage. 32 | - Tested on Python 3.5, and 3.6 33 | 34 | Usage 35 | ----- 36 | 37 | .. code-block:: bash 38 | 39 | $ pip install aioschedule 40 | 41 | .. code-block:: python 42 | 43 | import asyncio 44 | import aioschedule as schedule 45 | import time 46 | 47 | async def job(message: str = 'stuff', n: int = 1): 48 | print("Asynchronous invocation (%s) of I'm working on:" % n, message) 49 | await asyncio.sleep(1) 50 | 51 | for i in range(1,3): 52 | schedule.every(1).seconds.do(job, n=i) 53 | schedule.every(5).to(10).days.do(job) 54 | schedule.every().hour.do(job, message='things') 55 | schedule.every().day.at("10:30").do(job) 56 | 57 | loop = asyncio.get_event_loop() 58 | while True: 59 | loop.run_until_complete(schedule.run_pending()) 60 | time.sleep(0.1) 61 | 62 | Documentation 63 | ------------- 64 | 65 | Schedule's documentation lives at `schedule.readthedocs.io `_. 66 | 67 | Please also check the FAQ there with common questions. 68 | 69 | 70 | Development 71 | ----------- 72 | Run `vagrant up` to spawn a virtual machine containing the development 73 | environment. Make sure to set the `IBR_GIT_COMMITTER_NAME` and 74 | `IBR_GIT_COMMITTER_EMAIL` environment variables. 75 | 76 | 77 | Meta 78 | ---- 79 | 80 | - Daniel Bader - `@dbader_org `_ - mail@dbader.org 81 | - Cochise Ruhulessin - `@magicalcochise `_ - c.ruhulessin@ibrb.org 82 | 83 | Distributed under the MIT license. See ``LICENSE.txt`` for more information. 84 | 85 | https://github.com/ibrb/python-aioschedule 86 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.2 2 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | require 'etc' 4 | 5 | provision = <

16 | 21 | 22 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | Developer Interface 4 | =================== 5 | 6 | .. module:: aioschedule 7 | 8 | This part of the documentation covers all the interfaces of schedule. For 9 | parts where schedule depends on external libraries, we document the most 10 | important right here and provide links to the canonical documentation. 11 | 12 | Main Interface 13 | -------------- 14 | 15 | .. autodata:: default_scheduler 16 | .. autodata:: jobs 17 | 18 | .. autofunction:: every 19 | .. autofunction:: run_pending 20 | .. autofunction:: run_all 21 | .. autofunction:: clear 22 | .. autofunction:: cancel_job 23 | .. autofunction:: next_run 24 | .. autofunction:: idle_seconds 25 | 26 | Exceptions 27 | ---------- 28 | 29 | .. autoexception:: aioschedule.CancelJob 30 | 31 | 32 | Classes 33 | ------- 34 | 35 | .. autoclass:: aioschedule.Scheduler 36 | :members: 37 | :undoc-members: 38 | 39 | .. autoclass:: aioschedule.Job 40 | :members: 41 | :undoc-members: 42 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # schedule documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Nov 7 15:14:48 2016. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | # (schedule modules lives up one level from docs/) 20 | # 21 | import os 22 | import sys 23 | sys.path.insert(0, os.path.abspath('..')) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | 'sphinx.ext.autodoc', 36 | 'sphinx.ext.todo', 37 | 'sphinx.ext.coverage', 38 | 'sphinx.ext.viewcode', 39 | # 'sphinx.ext.githubpages', # This breaks the ReadTheDocs build 40 | ] 41 | 42 | # Add any paths that contain templates here, relative to this directory. 43 | templates_path = ['_templates'] 44 | 45 | # The suffix(es) of source filenames. 46 | # You can specify multiple suffix as a list of string: 47 | # 48 | # source_suffix = ['.rst', '.md'] 49 | source_suffix = '.rst' 50 | 51 | # The encoding of source files. 52 | # 53 | # source_encoding = 'utf-8-sig' 54 | 55 | # The master toctree document. 56 | master_doc = 'index' 57 | 58 | # General information about the project. 59 | project = u'schedule' 60 | copyright = u'2016, Daniel Bader' 61 | author = u'Daniel Bader' 62 | 63 | # The version info for the project you're documenting, acts as replacement for 64 | # |version| and |release|, also used in various other places throughout the 65 | # built documents. 66 | # 67 | # The short X.Y version. 68 | version = u'0.4.0' 69 | # The full version, including alpha/beta/rc tags. 70 | release = u'0.4.0' 71 | 72 | # The language for content autogenerated by Sphinx. Refer to documentation 73 | # for a list of supported languages. 74 | # 75 | # This is also used if you do content translation via gettext catalogs. 76 | # Usually you set "language" from the command line for these cases. 77 | language = None 78 | 79 | # There are two options for replacing |today|: either, you set today to some 80 | # non-false value, then it is used: 81 | # 82 | # today = '' 83 | # 84 | # Else, today_fmt is used as the format for a strftime call. 85 | # 86 | # today_fmt = '%B %d, %Y' 87 | 88 | # List of patterns, relative to source directory, that match files and 89 | # directories to ignore when looking for source files. 90 | # This patterns also effect to html_static_path and html_extra_path 91 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 92 | 93 | # The reST default role (used for this markup: `text`) to use for all 94 | # documents. 95 | # 96 | # default_role = None 97 | 98 | # If true, '()' will be appended to :func: etc. cross-reference text. 99 | # 100 | # add_function_parentheses = True 101 | 102 | # If true, the current module name will be prepended to all description 103 | # unit titles (such as .. function::). 104 | # 105 | # add_module_names = True 106 | 107 | # If true, sectionauthor and moduleauthor directives will be shown in the 108 | # output. They are ignored by default. 109 | # 110 | # show_authors = False 111 | 112 | # The name of the Pygments (syntax highlighting) style to use. 113 | # pygments_style = 'flask_theme_support.FlaskyStyle' 114 | 115 | # A list of ignored prefixes for module index sorting. 116 | # modindex_common_prefix = [] 117 | 118 | # If true, keep warnings as "system message" paragraphs in the built documents. 119 | # keep_warnings = False 120 | 121 | # If true, `todo` and `todoList` produce output, else they produce nothing. 122 | todo_include_todos = True 123 | 124 | 125 | # -- Options for HTML output ---------------------------------------------- 126 | 127 | # The theme to use for HTML and HTML Help pages. See the documentation for 128 | # a list of builtin themes. 129 | # 130 | html_theme = 'alabaster' 131 | 132 | # Theme options are theme-specific and customize the look and feel of a theme 133 | # further. For a list of options available for each theme, see the 134 | # documentation. 135 | # 136 | html_theme_options = { 137 | 'show_powered_by': False, 138 | 'github_user': 'dbader', 139 | 'github_repo': 'schedule', 140 | 'github_banner': True, 141 | 'show_related': False 142 | } 143 | 144 | # Add any paths that contain custom themes here, relative to this directory. 145 | # html_theme_path = [] 146 | 147 | # The name for this set of Sphinx documents. 148 | # " v documentation" by default. 149 | # 150 | # html_title = u'schedule v0.4.0' 151 | 152 | # A shorter title for the navigation bar. Default is the same as html_title. 153 | # 154 | # html_short_title = None 155 | 156 | # The name of an image file (relative to this directory) to place at the top 157 | # of the sidebar. 158 | # 159 | # html_logo = None 160 | 161 | # The name of an image file (relative to this directory) to use as a favicon of 162 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 163 | # pixels large. 164 | # 165 | # html_favicon = None 166 | 167 | # Add any paths that contain custom static files (such as style sheets) here, 168 | # relative to this directory. They are copied after the builtin static files, 169 | # so a file named "default.css" will overwrite the builtin "default.css". 170 | html_static_path = ['_static'] 171 | 172 | # Add any extra paths that contain custom files (such as robots.txt or 173 | # .htaccess) here, relative to this directory. These files are copied 174 | # directly to the root of the documentation. 175 | # 176 | # html_extra_path = [] 177 | 178 | # If not None, a 'Last updated on:' timestamp is inserted at every page 179 | # bottom, using the given strftime format. 180 | # The empty string is equivalent to '%b %d, %Y'. 181 | # 182 | # html_last_updated_fmt = None 183 | 184 | # If true, SmartyPants will be used to convert quotes and dashes to 185 | # typographically correct entities. 186 | # 187 | html_use_smartypants = True 188 | 189 | # Custom sidebar templates, maps document names to template names. 190 | # 191 | html_sidebars = { 192 | 'index': ['sidebarintro.html', 'sourcelink.html', 'searchbox.html'], 193 | } 194 | 195 | # Additional templates that should be rendered to pages, maps page names to 196 | # template names. 197 | # 198 | # html_additional_pages = {} 199 | 200 | # If false, no module index is generated. 201 | # 202 | # html_domain_indices = True 203 | 204 | # If false, no index is generated. 205 | # 206 | # html_use_index = True 207 | 208 | # If true, the index is split into individual pages for each letter. 209 | # 210 | # html_split_index = False 211 | 212 | # If true, links to the reST sources are added to the pages. 213 | # 214 | html_show_sourcelink = False 215 | 216 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 217 | # 218 | html_show_sphinx = False 219 | 220 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 221 | # 222 | html_show_copyright = True 223 | 224 | # If true, an OpenSearch description file will be output, and all pages will 225 | # contain a tag referring to it. The value of this option must be the 226 | # base URL from which the finished HTML is served. 227 | # 228 | # html_use_opensearch = '' 229 | 230 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 231 | # html_file_suffix = None 232 | 233 | # Language to be used for generating the HTML full-text search index. 234 | # Sphinx supports the following languages: 235 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 236 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' 237 | # 238 | # html_search_language = 'en' 239 | 240 | # A dictionary with options for the search language support, empty by default. 241 | # 'ja' uses this config value. 242 | # 'zh' user can custom change `jieba` dictionary path. 243 | # 244 | # html_search_options = {'type': 'default'} 245 | 246 | # The name of a javascript file (relative to the configuration directory) that 247 | # implements a search results scorer. If empty, the default will be used. 248 | # 249 | # html_search_scorer = 'scorer.js' 250 | 251 | # Output file base name for HTML help builder. 252 | htmlhelp_basename = 'scheduledoc' 253 | 254 | # -- Options for LaTeX output --------------------------------------------- 255 | 256 | latex_elements = { 257 | # The paper size ('letterpaper' or 'a4paper'). 258 | # 259 | # 'papersize': 'letterpaper', 260 | 261 | # The font size ('10pt', '11pt' or '12pt'). 262 | # 263 | # 'pointsize': '10pt', 264 | 265 | # Additional stuff for the LaTeX preamble. 266 | # 267 | # 'preamble': '', 268 | 269 | # Latex figure (float) alignment 270 | # 271 | # 'figure_align': 'htbp', 272 | } 273 | 274 | # Grouping the document tree into LaTeX files. List of tuples 275 | # (source start file, target name, title, 276 | # author, documentclass [howto, manual, or own class]). 277 | latex_documents = [ 278 | (master_doc, 'schedule.tex', u'schedule Documentation', 279 | u'Daniel Bader', 'manual'), 280 | ] 281 | 282 | # The name of an image file (relative to this directory) to place at the top of 283 | # the title page. 284 | # 285 | # latex_logo = None 286 | 287 | # For "manual" documents, if this is true, then toplevel headings are parts, 288 | # not chapters. 289 | # 290 | # latex_use_parts = False 291 | 292 | # If true, show page references after internal links. 293 | # 294 | # latex_show_pagerefs = False 295 | 296 | # If true, show URL addresses after external links. 297 | # 298 | # latex_show_urls = False 299 | 300 | # Documents to append as an appendix to all manuals. 301 | # 302 | # latex_appendices = [] 303 | 304 | # It false, will not define \strong, \code, itleref, \crossref ... but only 305 | # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added 306 | # packages. 307 | # 308 | # latex_keep_old_macro_names = True 309 | 310 | # If false, no module index is generated. 311 | # 312 | # latex_domain_indices = True 313 | 314 | 315 | # -- Options for manual page output --------------------------------------- 316 | 317 | # One entry per manual page. List of tuples 318 | # (source start file, name, description, authors, manual section). 319 | man_pages = [ 320 | (master_doc, 'schedule', u'schedule Documentation', 321 | [author], 1) 322 | ] 323 | 324 | # If true, show URL addresses after external links. 325 | # 326 | # man_show_urls = False 327 | 328 | 329 | # -- Options for Texinfo output ------------------------------------------- 330 | 331 | # Grouping the document tree into Texinfo files. List of tuples 332 | # (source start file, target name, title, author, 333 | # dir menu entry, description, category) 334 | texinfo_documents = [ 335 | (master_doc, 'schedule', u'schedule Documentation', 336 | author, 'schedule', 'One line description of project.', 337 | 'Miscellaneous'), 338 | ] 339 | 340 | # Documents to append as an appendix to all manuals. 341 | # 342 | # texinfo_appendices = [] 343 | 344 | # If false, no module index is generated. 345 | # 346 | # texinfo_domain_indices = True 347 | 348 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 349 | # 350 | # texinfo_show_urls = 'footnote' 351 | 352 | # If true, do not generate a @detailmenu in the "Top" node's menu. 353 | # 354 | # texinfo_no_detailmenu = False 355 | 356 | autodoc_member_order = 'bysource' 357 | 358 | # We're pulling in some external images like CI badges. 359 | suppress_warnings = ['image.nonlocal_uri'] 360 | -------------------------------------------------------------------------------- /docs/faq.rst: -------------------------------------------------------------------------------- 1 | .. At some point we'll want to migrate FAQ.rst to the docs folder but to 2 | .. breaking links on PyPI we need to leave it there until we prepare the 3 | .. next schedule release 4 | 5 | .. include:: ../FAQ.rst 6 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | schedule 2 | ======== 3 | 4 | 5 | .. image:: https://api.travis-ci.org/ibrb/python-aioschedule.svg?branch=master 6 | :target: https://travis-ci.org/ibrb/python-aioschedule 7 | 8 | .. image:: https://coveralls.io/repos/ibrb/python-aioschedule/badge.svg?branch=master 9 | :target: https://coveralls.io/r/ibrb/python-aioschedule 10 | 11 | .. image:: https://img.shields.io/pypi/v/aioschedule.svg 12 | :target: https://pypi.python.org/pypi/aioschedule 13 | 14 | .. image:: https://media.ibrb.org/ibr/images/logos/landscape1200.png 15 | :target: https://media.ibrb.org/ibr/images/logos/landscape1200.png 16 | 17 | 18 | Python job scheduling for humans. 19 | 20 | An in-process scheduler for periodic jobs that uses the builder pattern 21 | for configuration. Schedule lets you run Python functions (or any other 22 | callable) periodically at pre-determined intervals using a simple, 23 | human-friendly syntax. 24 | 25 | Inspired by `Adam Wiggins' `_ article `"Rethinking Cron" `_ and the `clockwork `_ Ruby module. 26 | 27 | Features 28 | -------- 29 | - A simple to use API for scheduling jobs. 30 | - Very lightweight and no external dependencies. 31 | - Excellent test coverage. 32 | - Tested on Python 2.7, 3.5, and 3.6 33 | 34 | Usage 35 | ----- 36 | 37 | .. code-block:: bash 38 | 39 | $ pip install aioschedule 40 | 41 | .. code-block:: python 42 | 43 | import asyncio 44 | import aioschedule as schedule 45 | import time 46 | 47 | async def job(message='stuff', n=1): 48 | print("Asynchronous invocation (%s) of I'm working on:" % n, message) 49 | asyncio.sleep(1) 50 | 51 | for i in range(1,3): 52 | schedule.every(1).seconds.do(job, n=i) 53 | schedule.every(5).to(10).days.do(job) 54 | schedule.every().hour.do(job, message='things') 55 | schedule.every().day.at("10:30").do(job) 56 | 57 | loop = asyncio.get_event_loop() 58 | while True: 59 | loop.run_until_complete(schedule.run_pending()) 60 | time.sleep(0.1) 61 | 62 | 63 | API Documentation 64 | ----------------- 65 | 66 | If you are looking for information on a specific function, class, or method, 67 | this part of the documentation is for you. 68 | 69 | .. toctree:: 70 | api 71 | 72 | Common Questions 73 | ---------------- 74 | 75 | Please check here before creating a new issue ticket. 76 | 77 | .. toctree:: 78 | faq 79 | 80 | 81 | Issues 82 | ------ 83 | 84 | If you encounter any problems, please `file an issue `_ along with a detailed description. Please also check the :ref:`frequently-asked-questions` and use the search feature in the issue tracker beforehand to avoid creating duplicates. Thank you 😃 85 | 86 | 87 | About Schedule 88 | -------------- 89 | 90 | Schedule was created by `Daniel Bader `__ - `@dbader_org `_ 91 | 92 | Distributed under the MIT license. See ``LICENSE.txt`` for more information. 93 | 94 | .. include:: ../AUTHORS.rst 95 | -------------------------------------------------------------------------------- /ops/gitlab/defaults.yml: -------------------------------------------------------------------------------- 1 | # These are the default variables for Python pipelines. This file should not 2 | # be added to a repository, but instead overridden by including a variables 3 | # block after this one. 4 | --- 5 | variables: 6 | DB_HOST: &DB_HOST rdbms 7 | DB_NAME: &DB_NAME rdbms 8 | DB_USERNAME: &DB_USERNAME rdbms 9 | DB_PASSWORD: &DB_PASSWORD rdbms 10 | DEBUG: 1 11 | PYTHON_VERSIONS: "3.8 3.9" 12 | OS_ALPINE_VERSIONS: "3.12 3.13" 13 | OS_CENTOS_VERSIONS: "7 8" 14 | OS_DEBIAN_VERSIONS: "9 10" 15 | OS_UBUNTU_VERSIONS: "16.04 18.04 20.04" 16 | POSTGRES_DB: *DB_NAME 17 | POSTGRES_PASSWORD: *DB_PASSWORD 18 | POSTGRES_USER: *DB_USERNAME 19 | SCM_DEVELOPMENT_BRANCH: development 20 | SCM_STABLE_BRANCH: stable 21 | SCM_MAINLINE_BRANCH: mainline 22 | SCM_CI_TESTING_BRANCH: ci/testing 23 | SECRET_KEY: unsafe-ci 24 | -------------------------------------------------------------------------------- /ops/gitlab/user-defined.yml: -------------------------------------------------------------------------------- 1 | --- 2 | variables: {} 3 | -------------------------------------------------------------------------------- /ops/gitlab/variables.yml: -------------------------------------------------------------------------------- 1 | --- 2 | variables: {} 3 | -------------------------------------------------------------------------------- /ops/make/docker.mk: -------------------------------------------------------------------------------- 1 | #DOCKER_IMAGE ?= alpine:3.12 2 | DOCKER ?= docker 3 | DOCKER_BASE_REPO ?= registry.gitlab.com/unimatrixone/docker/drone 4 | ifdef DOCKER_CACHE_IMAGE 5 | DOCKER_BUILD_ARGS += --cache-from $(DOCKER_CACHE_IMAGE) 6 | endif 7 | ifdef DOCKER_TARGET 8 | DOCKER_BUILD_ARGS += --target $(DOCKER_TARGET) 9 | endif 10 | DOCKER_COMPOSE_ARGS = 11 | ifeq ($(DAEMONIZE_SERVICES), 1) 12 | DOCKER_COMPOSE_ARGS += -d 13 | endif 14 | DOCKER_IMAGE_BASE ?= $(DOCKER_BASE_REPO)/runtime/$(DOCKER_BASE_IMAGE) 15 | DOCKER_IMAGE_BUILD ?= $(DOCKER_BASE_REPO)/build/$(DOCKER_BASE_IMAGE) 16 | export 17 | 18 | docker.build_dir ?= build/docker 19 | docker.build.files += 20 | docker.dockerfile = Dockerfile 21 | docker.entrypoint = bin/docker-entrypoint 22 | docker.entrypoint.shebang.alpine = \#!/bin/ash 23 | docker.entrypoint.shebang.debian = \#!/bin/bash 24 | ifneq ($(wildcard docker-compose.yml),) 25 | cmd.killinfra = docker-compose down 26 | cmd.runinfra = docker-compose up $(DOCKER_COMPOSE_ARGS) 27 | endif 28 | 29 | 30 | .dockerignore: 31 | ifneq ($(seed.docker.dockerignore),) 32 | @$(cmd.curl) $(seed.docker.dockerignore) > .dockerignore 33 | @$(cmd.git.add) .dockerignore 34 | endif 35 | 36 | 37 | # Invoked prior to building a Docker image. 38 | docker-prebuild: 39 | @mkdir -p $(docker.build_dir) 40 | 41 | 42 | docker-compose.yml: 43 | ifneq ($(seed.docker.compose),) 44 | @$(cmd.curl) $(seed.docker.compose) > docker-compose.yml 45 | endif 46 | 47 | 48 | docker-image: $(docker.dockerfile) $(docker.entrypoint) docker-prebuild 49 | @echo "Building Docker image with build image $(DOCKER_IMAGE_BUILD)" 50 | @docker build $(DOCKER_BUILD_ARGS) -t $(DOCKER_IMAGE_NAME)\ 51 | --build-arg BUILD_OS_PACKAGES="$(build.$(OS_RELEASE_ID).packages)"\ 52 | --build-arg BUILDKIT_INLINE_CACHE=1\ 53 | --build-arg DOCKER_BUILD_DIR="$(docker.build_dir)"\ 54 | --build-arg DOCKER_IMAGE_BASE="$(DOCKER_IMAGE_BASE)"\ 55 | --build-arg DOCKER_IMAGE_BUILD="$(DOCKER_IMAGE_BUILD)"\ 56 | --build-arg RUNTIME_OS_PACKAGES="$(runtime.$(OS_RELEASE_ID).packages)"\ 57 | --build-arg GIT_COMMIT_SHA="$(VCS_COMMIT_HASH)"\ 58 | $(docker.build.args) . 59 | 60 | 61 | docker-run%: 62 | @docker run -it $(addprefix -e , $(docker.run.env))\ 63 | $(DOCKER_IMAGE_NAME) run$(*) 64 | 65 | 66 | docker-push: docker-image 67 | ifdef DOCKER_QUALNAME 68 | @docker tag $(DOCKER_IMAGE_NAME) $(DOCKER_QUALNAME) 69 | @docker push $(DOCKER_QUALNAME) 70 | endif 71 | 72 | 73 | $(docker.build_dir): 74 | @mkdir -p $(docker.build_dir) 75 | 76 | 77 | $(docker.build_dir)/%: $(docker.build_dir) 78 | @mkdir -p $(docker.build_dir)/$(shell dirname $(*)) 79 | @cp -R $(*) $(docker.build_dir)/$(*) 80 | 81 | 82 | $(docker.dockerfile): 83 | @$(cmd.curl) $(or $(seed.docker.dockerfile), $(error seed.docker.dockerfile is not defined))\ 84 | -o $(docker.dockerfile) 85 | 86 | 87 | $(docker.entrypoint): 88 | @mkdir -p $(shell dirname $(docker.entrypoint)) 89 | @$(cmd.curl) $(or $(seed.docker.entrypoint), $(error seed.docker.entrypoint is not defined))\ 90 | -o $(docker.entrypoint) 91 | @chmod +x $(docker.entrypoint) 92 | @sed -i.bak "1s|.*|$(docker.entrypoint.shebang.$(OS_RELEASE_ID))|" $(docker.entrypoint)\ 93 | && rm $(docker.entrypoint).bak 94 | 95 | 96 | bootstrap: .dockerignore 97 | clean: killinfra 98 | configure-docker: 99 | runinfra-daemon: docker-compose.yml 100 | runinfra: docker-compose.yml 101 | ifneq ($(wildcard etc),) 102 | docker-prebuild: $(docker.build_dir)/etc 103 | endif 104 | ifneq ($(wildcard pki/pkcs),) 105 | docker-prebuild: $(docker.build_dir)/pki/pkcs 106 | endif 107 | -------------------------------------------------------------------------------- /ops/make/python-docs.mk: -------------------------------------------------------------------------------- 1 | PYTHON_DOCS_DIR ?= docs 2 | PYTHON_DOCS_PACKAGES += sphinx sphinxcontrib-napoleon sphinx-copybutton 3 | PYTHON_DOCS_PACKAGES += sphinxcontrib-makedomain insegel python-docs-theme sphinx-material 4 | PYTHON_DOCS_LIBS=.lib/python/docs 5 | SPHINXBUILD=$(PYTHON) -c "from sphinx.cmd.build import main; main()" 6 | export 7 | ifneq ($(wildcard $(PYTHON_DOCS_DIR)/requirements.txt),) 8 | DEPSCHECKSUMFILES += $(PYTHON_DOCS_DIR)/requirements.txt 9 | endif 10 | 11 | cmd.doc8 = $(PYTHON) -c "from doc8.main import main; main()" 12 | cmd.lint-docs = $(cmd.doc8) $(PYTHON_DOCS_DIR) 13 | cmd.sphinx-quickstart = $(PYTHON) -c "from sphinx.cmd.quickstart import main; main()" 14 | gitlab.pip.packages += $(PYTHON_DOCS_PACKAGES) 15 | 16 | 17 | $(PYTHON_DOCS_DIR)/Makefile: $(PYTHON_DOCS_DIR)/requirements.txt 18 | @$(cmd.sphinx-quickstart) $(PYTHON_DOCS_DIR) 19 | @$(cmd.git) add -f $(PYTHON_DOCS_DIR)/* 20 | 21 | 22 | $(PYTHON_DOCS_DIR): 23 | @mkdir -p $(PYTHON_DOCS_DIR) 24 | 25 | 26 | $(PYTHON_DOCS_LIBS): 27 | @$(PIP) install $(PYTHON_DOCS_PACKAGES) --target $(PYTHON_DOCS_LIBS) 28 | ifneq ($(wildcard $(PYTHON_DOCS_DIR)/requirements.txt),) 29 | @$(PIP) install -r $(PYTHON_DOCS_DIR)/requirements.txt --target $(PYTHON_DOCS_LIBS) 30 | endif 31 | 32 | 33 | $(PYTHON_DOCS_DIR)/requirements.txt: $(PYTHON_DOCS_DIR) $(PYTHON_DOCS_LIBS) 34 | @$(PIP) freeze --path $(PYTHON_DOCS_LIBS)\ 35 | > $(PYTHON_DOCS_DIR)/requirements.txt 36 | @$(cmd.git.add) $(PYTHON_DOCS_DIR)/requirements.txt 37 | 38 | 39 | $(PYTHON_DOCS_DIR)/build/dirhtml: $(PYTHON_DOCS_DIR) 40 | @cd $(PYTHON_DOCS_DIR) && $(MAKE) dirhtml 41 | 42 | 43 | build-python-docs: 44 | @$(MAKE) -C $(PYTHON_DOCS_DIR) dirhtml 45 | @mkdir -p $(HTML_DOCUMENT_ROOT) 46 | @cp -R $(PYTHON_DOCS_DIR)/build/dirhtml/* $(HTML_DOCUMENT_ROOT)/ 47 | 48 | 49 | bootstrap-python-docs: $(PYTHON_DOCS_DIR)/Makefile 50 | 51 | 52 | configure-python-docs: 53 | depsinstall: $(PYTHON_DOCS_LIBS) 54 | public: build-python-docs 55 | -------------------------------------------------------------------------------- /ops/make/python-gitlab.mk: -------------------------------------------------------------------------------- 1 | GITLAB_DOCKERFILE ?= ops/gitlab/Dockerfile 2 | GITLAB_RUNNER_IMAGE_TAG ?= $(GITLAB_RUNNER_IMAGE_URL) 3 | CI_DOCKERFILE ?= $(GITLAB_DOCKERFILE) 4 | DEPSCHECKSUMFILES += $(GITLAB_DOCKERFILE) 5 | export 6 | 7 | gitlab.alpine.packages += $(build.alpine.packages) 8 | gitlab.debian.packages += $(build.debian.packages) 9 | gitlab.pip.packages += $(PYTHON_TEST_PACKAGES) 10 | gitlab.runner.image ?= $(DOCKER_BASE_REPO)/gitlab-runner/python:$(DOCKER_BASE_TAG) 11 | 12 | 13 | gitlab-docker-tag: 14 | @echo -n $(DOCKER_BASE_TAG) 15 | 16 | 17 | gitlab-docker-image: $(GITLAB_DOCKERFILE) 18 | @echo "Building GitLab runner image with base $(gitlab.runner.image)" 19 | @$(DOCKER) build -t $(GITLAB_RUNNER_IMAGE_URL)\ 20 | -f $(CI_DOCKERFILE)\ 21 | --build-arg BASE_IMAGE=$(gitlab.runner.image)\ 22 | --build-arg OS_PKG_INSTALL="$(os.$(OS_RELEASE_ID).pkg.install)"\ 23 | --build-arg OS_PKG_UPDATE="$(os.$(OS_RELEASE_ID).pkg.update)"\ 24 | --build-arg OS_PACKAGES="$(gitlab.$(OS_RELEASE_ID).packages)"\ 25 | --build-arg "PIP_PKG_INSTALL=$(shell echo $(gitlab.pip.packages)|sed -e 's|["'\'']||g')"\ 26 | --build-arg PYTHON_SUBPKG_NAME=$(PYTHON_SUBPKG_NAME)\ 27 | . 28 | @$(DOCKER) push $(GITLAB_RUNNER_IMAGE_URL) 29 | 30 | 31 | .gitlab-ci.yml: 32 | @$(cmd.curl) $(or $(seed.gitlab.pipeline), $(error Define seed.gitlab.pipeline)) > .gitlab-ci.yml 33 | @$(cmd.git) add .gitlab-ci.yml 34 | 35 | 36 | ops/gitlab: 37 | @mkdir -p ./ops/gitlab 38 | 39 | 40 | ops/gitlab/defaults.yml: ops/gitlab 41 | ifeq ($(wildcard ops/gitlab/defaults.yml),) 42 | @$(cmd.curl) $(PYTHON_SEED_URL)/ops/gitlab/defaults.yml > ./ops/gitlab/defaults.yml 43 | @$(cmd.git.add) ./ops/gitlab/defaults.yml 44 | endif 45 | 46 | 47 | ops/gitlab/user-defined.yml: ops/gitlab 48 | ifeq ($(wildcard ops/gitlab/user-defined.yml),) 49 | @echo "---\nvariables: {}" > ./ops/gitlab/user-defined.yml 50 | @$(cmd.git.add) ./ops/gitlab/user-defined.yml 51 | endif 52 | 53 | 54 | ops/gitlab/variables.yml: ops/gitlab 55 | ifeq ($(wildcard ops/gitlab/variables.yml),) 56 | @echo "---\nvariables: {}" > ./ops/gitlab/variables.yml 57 | @$(cmd.git.add) ./ops/gitlab/variables.yml 58 | endif 59 | 60 | 61 | update-python-gitlab: 62 | @rm -f ./ops/gitlab/defaults.yml 63 | @$(MAKE) ops/gitlab/defaults.yml 64 | 65 | 66 | $(GITLAB_DOCKERFILE): ops/gitlab 67 | @$(cmd.curl) $(PYTHON_SEED_URL)/ops/gitlab/Dockerfile > $(GITLAB_DOCKERFILE) 68 | 69 | 70 | configure-python-gitlab: 71 | 72 | 73 | bootstrap-python-gitlab: 74 | @$(MAKE) ops/gitlab/defaults.yml 75 | @$(MAKE) ops/gitlab/variables.yml 76 | @$(MAKE) ops/gitlab/user-defined.yml 77 | @$(MAKE) .gitlab-ci.yml 78 | 79 | 80 | .gitlab-ci.yml: ops/gitlab/defaults.yml 81 | .gitlab-ci.yml: ops/gitlab/variables.yml 82 | .gitlab-ci.yml: ops/gitlab/user-defined.yml 83 | bootstrap: bootstrap-python-gitlab 84 | depschecksums: $(GITLAB_DOCKERFILE) 85 | update: update-python-gitlab 86 | -------------------------------------------------------------------------------- /ops/make/python-package.mk: -------------------------------------------------------------------------------- 1 | seed.gitlab.pipeline = $(PYTHON_SEED_URL)/.gitlab-ci.yml 2 | 3 | 4 | configure-python-package: 5 | -------------------------------------------------------------------------------- /ops/make/python-parent.mk: -------------------------------------------------------------------------------- 1 | DOCKER_IMAGE_NAME=$(PYTHON_PKG_NAME) 2 | PYTHON_BOOT_MODULE ?= $(PYTHON_PKG_WORKDIR)/runtime/boot.py 3 | PYTHON_PKG_WORKDIR ?= $(PYTHON_PKG_NAME) 4 | PYTHON_SETTINGS_PKG = $(PYTHON_PKG_WORKDIR)/runtime/settings 5 | PYTHON_SETUPTOOLS_PKG_FINDER=find_packages 6 | PYTHON_QUALNAME ?= $(PYTHON_PKG_NAME) 7 | PYPI_METADATA_FILE ?= $(PYTHON_PKG_NAME)/package.json 8 | 9 | 10 | configure-python-parent: 11 | -------------------------------------------------------------------------------- /ops/make/python.mk: -------------------------------------------------------------------------------- 1 | # Celery/RabbitMQ settings 2 | RABBITMQ_USERNAME ?= rabbitmq 3 | RABBITMQ_PASSWORD ?= rabbitmq 4 | RABBITMQ_PORT ?= 5672 5 | RABBITMQ_VHOST ?= / 6 | 7 | # Add the appropriate files to DEPSCHECKSUMFILES so that the CI rebuilds 8 | # the environment if any dependency has changed. 9 | DEPSCHECKSUMFILES += $(PYPI_METADATA_FILE) 10 | ifneq ($(wildcard $(PYTHON_REQUIREMENTS)),) 11 | DEPSCHECKSUMFILES += $(PYTHON_REQUIREMENTS) 12 | endif 13 | ifeq ($(PROJECT_KIND), application) 14 | UNIMATRIX_SETTINGS_MODULE ?= $(PYTHON_PKG_NAME).runtime.settings 15 | UNIMATRIX_BOOT_MODULE ?= $(PYTHON_PKG_NAME).runtime.boot 16 | endif 17 | 18 | # Command-definitions and parameters 19 | cmd.console ?= $(PYTHON) 20 | cmd.check-package = $(PYTHON) setup.py check 21 | ifneq ($(wildcard $(PYTHON_REQUIREMENTS)),) 22 | cmd.check-requirements-cve = $(PYTHON) -m safety check -r $(PYTHON_REQUIREMENTS) 23 | endif 24 | cmd.check-requirements-outdated = $(PYTHON) -c "from piprot.piprot import piprot; piprot()" -o 25 | cmd.dist = $(PYTHON) setup.py sdist 26 | cmd.distclean = rm -rf ./dist && rm -rf *.egg.info 27 | cmd.flake8 = $(PYTHON) -m flake8 28 | cmd.htmlclean ?= rm -rf $(HTML_DOCUMENT_ROOT) 29 | cmd.lint-exceptions ?= $(PYTHON) -m pylint --disable=all --enable=W0704,W0702,E0701,E0702,E0703,E0704,E0710,E0711,E0712,E1603,E1604,W0150,W0623,W0703,W0705,W0706,W0711,W0715 $(python.lint.packages) 30 | cmd.lint-nodebug ?= $(cmd.flake8) --ignore=all --select=T001,T002,T003,T004 $(python.lint.packages) 31 | cmd.lint-security ?= $(PYTHON) -m bandit 32 | ifneq ($(wildcard .bandit.yml),) 33 | cmd.lint-security += -c .bandit.yml 34 | endif 35 | cmd.lint-security += -r $(or $(BANDIT_DIRS), $(python.lint.packages)) 36 | cmd.lint.import-order ?= $(PYTHON) -m pylint --disable=all --enable=C0413 $(python.lint.packages) 37 | cmd.lint.unused ?= $(PYTHON) -m pylint --disable=all --enable=W0611,W0612 $(python.lint.packages) 38 | cmd.lint.line-length ?= $(PYTHON) -m pylint --disable=all --enable=C0301 $(python.lint.packages) 39 | cmd.lint.trailing-whitespace ?= $(PYTHON) -m pylint --disable=all --enable=C0303 $(python.lint.packages) 40 | cmd.localinstall ?= $(PIP_INSTALL) --target $(PYTHON_RUNTIME_LIBS) .[all] 41 | cmd.publish = $(cmd.twine) upload dist/* 42 | cmd.runtests = $(PYTHON) -m pytest -v 43 | cmd.runtests += --cov-report term-missing:skip-covered 44 | cmd.runtests += --cov=$(or $(cmd.test.cover-package), $(PYTHON_PKG_NAME)) 45 | cmd.runtests += --cov-append 46 | ifdef test.coverage.$(TEST_STAGE) 47 | cmd.runtests += --cov-fail-under=$(test.coverage.$(TEST_STAGE)) 48 | endif 49 | cmd.runtests += $(PYTEST_ARGS) 50 | cmd.testcoverage = $(PYTHON) -m coverage combine .coverage-* 51 | cmd.testcoverage += && $(PYTHON) -m coverage report -m --skip-covered --show-missing 52 | ifdef TEST_MIN_COVERAGE 53 | cmd.testcoverage += --fail-under $(TEST_MIN_COVERAGE) 54 | endif 55 | ifdef test.coverage 56 | cmd.testcoverage += --fail-under $(test.coverage) 57 | endif 58 | ifdef CI_JOB_ID 59 | cmd.testcoverage += && $(PYTHON) -m coverage html -d $(HTML_COVERAGE_DIR) 60 | endif 61 | cmd.twine = $(PYTHON) -m twine 62 | ifndef cmd.test.path 63 | ifeq ($(PROJECT_SCOPE), namespaced) 64 | cmd.runtests += $(shell find $(CURDIR)/tests | grep test_$(TEST_STAGE)_.*\.py$$) 65 | else 66 | cmd.runtests += $(shell find $(CURDIR)/tests | grep test_$(TEST_STAGE)_.*\.py$$) 67 | endif 68 | else 69 | cmd.runtests += $(shell find $(CURDIR)/$(cmd.test.path) | grep test_$(TEST_STAGE)_.*\.py$$) 70 | endif 71 | cmd.watch ?= fswatch -o $(PYTHON_PKG_NAME) | xargs -n1 -I{} $(MAKE) test-unit 72 | docker.build.args += --build-arg HTTP_WSGI_MODULE="$(HTTP_WSGI_MODULE)" 73 | docker.build.args += --build-arg PYTHON_PKG_NAME="$(PYTHON_PKG_NAME)" 74 | docker.build.args += --build-arg PYTHON_SUBPKG_NAME="$(PYTHON_SUBPKG_NAME)" 75 | python.lint.packages ?= $(PYTHON_PKG_NAME) 76 | seed.git.ignore = $(PYTHON_SEED_URL)/.gitignore 77 | 78 | 79 | cleanpythondeps: 80 | @rm -rf $(PYTHON_DOCS_LIBS) 81 | @rm -rf $(PYTHON_INFRA_LIBS) 82 | @rm -rf $(PYTHON_RUNTIME_LIBS) 83 | @rm -rf $(PYTHON_TESTING_LIBS) 84 | 85 | 86 | configure-python: pythonclean 87 | 88 | 89 | eggclean: 90 | @rm -rf *.egg-info 91 | 92 | 93 | python-install-%: 94 | @$(PIP) install $(*) --target $(PYTHON_RUNTIME_LIBS) 95 | @rm -rf $(PYTHON_REQUIREMENTS) && $(MAKE) $(PYTHON_REQUIREMENTS) 96 | 97 | 98 | pythonclean: 99 | @find . -type f -name '*.py[co]' -delete -o -type d -name __pycache__ -delete 100 | 101 | 102 | pythontestclean: 103 | @rm -rf .coverage 104 | @rm -rf .coverage-* 105 | @rm -rf .pytest_cache 106 | @rm -rf htmlcov 107 | 108 | 109 | $(COVERAGE_CONFIG): 110 | @$(cmd.curl) $(PYTHON_SEED_URL)/.coveragerc > $(COVERAGE_CONFIG) 111 | @$(cmd.git) add $(COVERAGE_CONFIG)\ 112 | && $(cmd.git) commit -m "Add Coverage configuration" 113 | 114 | 115 | $(PYTEST_ROOT_CONFTEST): 116 | @mkdir -p $(shell dirname $(PYTEST_ROOT_CONFTEST)) 117 | @$(cmd.curl) $(PYTHON_SEED_URL)/pkg/conftest.py > $(PYTEST_ROOT_CONFTEST) 118 | @$(cmd.git) add $(PYTEST_ROOT_CONFTEST)\ 119 | && $(cmd.git) commit -m "Add root PyTest configuration" 120 | 121 | 122 | $(PYTHON_BOOT_MODULE): 123 | @mkdir -p $(shell dirname $(PYTHON_BOOT_MODULE)) 124 | @$(cmd.curl) $(PYTHON_SEED_URL)/pkg/runtime/boot.py > $(PYTHON_BOOT_MODULE) 125 | @$(cmd.git) add $(PYTHON_BOOT_MODULE) && $(cmd.git) commit -m "Add boot module" 126 | 127 | 128 | $(PYTHON_INFRA_LIBS): 129 | ifdef PYTHON_INFRA_PACKAGES 130 | @$(PIP_INSTALL) --target $(PYTHON_INFRA_LIBS) $(PYTHON_INFRA_PACKAGES) 131 | endif 132 | 133 | 134 | $(PYTHON_RUNTIME_LIBS): setup.py 135 | ifeq ($(wildcard $(PYTHON_RUNTIME_LIBS)),) 136 | @$(cmd.localinstall) 137 | ifeq ($(PROJECT_SCOPE), namespaced) 138 | @rm -rf $(PYTHON_RUNTIME_LIBS)/$(PYTHON_PKG_NAME)/ext/$(PYTHON_SUBPKG_NAME) 139 | @rm -rf $(PYTHON_RUNTIME_LIBS)/$(PYTHON_PKG_NAME).ext.$(PYTHON_SUBPKG_NAME)* 140 | else 141 | @rm -rf $(PYTHON_RUNTIME_LIBS)/$(PYTHON_PKG_NAME) 142 | @rm -rf $(PYTHON_RUNTIME_LIBS)/$(PYTHON_PKG_NAME)-* 143 | @rm -rf $(PYTHON_RUNTIME_LIBS)/$(PYTHON_PKG_NAME).* 144 | endif 145 | endif 146 | 147 | 148 | $(PYTHON_TESTING_LIBS): 149 | ifeq ($(wildcard $(PYTHON_TESTING_LIBS)),) 150 | @$(PIP_INSTALL) --target $(PYTHON_TESTING_LIBS) $(PYTHON_TEST_PACKAGES) 151 | endif 152 | 153 | 154 | $(PYTHON_PKG_NAME): 155 | @mkdir -p $(PYTHON_PKG_NAME) 156 | 157 | 158 | ifeq ($(PROJECT_SCOPE), namespaced) 159 | 160 | 161 | $(PYTHON_SUBPKG_PATH): 162 | @mkdir -p $(PYTHON_SUBPKG_PATH) 163 | 164 | 165 | $(PYTHON_SUBPKG_PATH)/__init__.py: $(PYTHON_SUBPKG_PATH) 166 | @touch $(PYTHON_SUBPKG_PATH)/__init__.py 167 | 168 | bootstrap: $(PYTHON_SUBPKG_PATH)/__init__.py 169 | bootstrap: $(PYTHON_SUBPKG_PATH)/package.json 170 | endif 171 | 172 | 173 | bootstrap-python: 174 | @touch $(PYTHON_PKG_WORKDIR)/__init__.py\ 175 | && $(cmd.git.add) $(PYTHON_PKG_WORKDIR)/__init__.py 176 | ifdef PYTHON_RUNTIME_PACKAGES 177 | @$(PIP) install $(PYTHON_RUNTIME_PACKAGES)\ 178 | --target $(PYTHON_RUNTIME_LIBS) 179 | endif 180 | ifeq ($(PROJECT_KIND), application) 181 | @# Create some default test cases for application boot, settings load etc. 182 | @mkdir -p $(PYTHON_PKG_WORKDIR)/app 183 | @mkdir -p $(PYTHON_PKG_WORKDIR)/infra 184 | @mkdir -p $(PYTHON_PKG_WORKDIR)/runtime/tests 185 | @touch $(PYTHON_PKG_WORKDIR)/app/__init__.py 186 | @touch $(PYTHON_PKG_WORKDIR)/infra/__init__.py 187 | @touch $(PYTHON_PKG_WORKDIR)/runtime/__init__.py 188 | @touch $(PYTHON_PKG_WORKDIR)/runtime/tests/__init__.py 189 | @$(cmd.curl) $(PYTHON_SEED_URL)/pkg/runtime/tests/test_unit_settings.py\ 190 | > $(PYTHON_PKG_WORKDIR)/runtime/tests/test_unit_settings.py 191 | @$(cmd.curl) $(PYTHON_SEED_URL)/pkg/runtime/tests/test_system_boot.py\ 192 | > $(PYTHON_PKG_WORKDIR)/runtime/tests/test_system_boot.py 193 | @$(cmd.git.add) -A $(PYTHON_PKG_WORKDIR) 194 | else 195 | @mkdir -p $(PYTHON_PKG_WORKDIR)/tests 196 | @$(cmd.curl) $(PYTHON_SEED_URL)/pkg/tests/test_unit_noop.py\ 197 | > $(PYTHON_PKG_WORKDIR)/tests/test_unit_noop.py 198 | @$(cmd.curl) $(PYTHON_SEED_URL)/pkg/tests/test_integration_noop.py\ 199 | > $(PYTHON_PKG_WORKDIR)/tests/test_integration_noop.py 200 | @$(cmd.curl) $(PYTHON_SEED_URL)/pkg/tests/test_system_noop.py\ 201 | > $(PYTHON_PKG_WORKDIR)/tests/test_system_noop.py 202 | @$(cmd.git.add) -A $(PYTHON_PKG_WORKDIR)/tests 203 | endif 204 | 205 | 206 | MANIFEST.in: 207 | @$(cmd.curl) $(PYTHON_SEED_URL)/MANIFEST.in.tpl\ 208 | | sed 's|$$SEMVER_FILE|$(SEMVER_FILE)|g'\ 209 | | sed 's|$$PYTHON_SETUPTOOLS_PKG_FINDER|$(PYTHON_SETUPTOOLS_PKG_FINDER)|g'\ 210 | | sed 's|$$PYPI_METADATA_FILE|$(PYPI_METADATA_FILE)|g'\ 211 | | sed 's|$$PYTHON_PKG_NAME|$(PYTHON_PKG_NAME)|g'\ 212 | > MANIFEST.in 213 | @$(cmd.git.add) MANIFEST.in 214 | 215 | 216 | setup.py: $(SEMVER_FILE) $(PYPI_METADATA_FILE) MANIFEST.in 217 | ifeq ($(wildcard setup.py),) 218 | @$(cmd.curl) $(PYTHON_SEED_URL)/setup.py.tpl\ 219 | | sed 's|$$SEMVER_FILE|$(SEMVER_FILE)|g'\ 220 | | sed 's|$$PYTHON_REQUIREMENTS|$(PYTHON_REQUIREMENTS)|g'\ 221 | | sed 's|$$PYTHON_SETUPTOOLS_PKG_FINDER|$(PYTHON_SETUPTOOLS_PKG_FINDER)|g'\ 222 | | sed 's|$$PYTHON_QUALNAME|$(PYTHON_QUALNAME)|g'\ 223 | | sed 's|$$PYPI_METADATA_FILE|$(PYPI_METADATA_FILE)|g'\ 224 | > setup.py 225 | @$(cmd.git.add) setup.py 226 | endif 227 | 228 | 229 | $(HTML_COVERAGE_DIR): 230 | @mkdir -p $(HTML_COVERAGE_DIR) 231 | 232 | 233 | $(PYPI_METADATA_FILE): 234 | @mkdir -p $$(dirname $(PYPI_METADATA_FILE)) 235 | @$(cmd.curl) $(or $(seed.python.package), $(PYTHON_SEED_URL)/pkg/package.json)\ 236 | > $(PYPI_METADATA_FILE) 237 | @$(cmd.git.add) $(PYPI_METADATA_FILE) 238 | 239 | 240 | $(PYTHON_REQUIREMENTS): 241 | @$(PIP) freeze --path $(PYTHON_RUNTIME_LIBS) > $(PYTHON_REQUIREMENTS) 242 | 243 | 244 | # Create the application settings module. The core Python include does not 245 | # create the defaults - this is up to the specific implementation for a 246 | # framework, such as Django or FastAPI. 247 | ifeq ($(PROJECT_SCOPE), parent) 248 | $(PYTHON_RUNTIME_PKG): 249 | @mkdir -p $(PYTHON_RUNTIME_PKG) 250 | @touch $(PYTHON_RUNTIME_PKG)/__init__.py 251 | @$(cmd.git) add $(PYTHON_RUNTIME_PKG)/__init__.py 252 | 253 | 254 | $(PYTHON_SETTINGS_PKG): $(PYTHON_RUNTIME_PKG) 255 | @mkdir -p $(PYTHON_SETTINGS_PKG) 256 | 257 | 258 | $(PYTHON_SETTINGS_PKG)/__init__.py: 259 | @$(MAKE) $(PYTHON_SETTINGS_PKG)/defaults.py 260 | @$(cmd.curl) $(PYTHON_SEED_URL)/pkg/runtime/settings/__init__.py\ 261 | > $(PYTHON_SETTINGS_PKG)/__init__.py 262 | @$(cmd.git.add) -A $(PYTHON_SETTINGS_PKG) 263 | 264 | 265 | $(PYTHON_SETTINGS_PKG)/defaults.py: $(PYTHON_SETTINGS_PKG) 266 | @echo "$(or $(app.settings.defaults.seed), $(error Set app.settings.defaults.seed))" 267 | @$(cmd.curl) $(app.settings.defaults.seed)\ 268 | > $(PYTHON_SETTINGS_PKG)/defaults.py 269 | @$(cmd.git.add) $(PYTHON_SETTINGS_PKG)/* 270 | endif 271 | 272 | 273 | bootstrap: setup.py 274 | bootstrap: bootstrap-python 275 | bootstrap: $(COVERAGE_CONFIG) 276 | bootstrap: $(SEMVER_FILE) 277 | ifeq ($(PROJECT_KIND), application) 278 | bootstrap: $(PYTEST_ROOT_CONFTEST) 279 | bootstrap: $(PYTHON_BOOT_MODULE) 280 | post-bootstrap: $(PYTHON_REQUIREMENTS) 281 | endif 282 | configure-python: 283 | distclean: eggclean 284 | ifdef PYTHON_INFRA_PACKAGES 285 | depsinstall: $(PYTHON_INFRA_LIBS) 286 | endif 287 | depsinstall: $(PYTHON_RUNTIME_LIBS) 288 | depsinstall: $(PYTHON_TESTING_LIBS) 289 | docker-image: MANIFEST.in 290 | docker-image: setup.py 291 | docker-image: $(PYTHON_REQUIREMENTS) 292 | env: depsinstall 293 | depsclean: cleanpythondeps 294 | envclean: cleanpythondeps 295 | test: $(HTML_COVERAGE_DIR) 296 | testclean: pythontestclean 297 | testcoverage: $(HTML_COVERAGE_DIR) 298 | ifndef CI_JOB_ID 299 | test: $(PYTHON_RUNTIME_LIBS) $(PYTHON_TESTING_LIBS) 300 | shell: $(PYTHON_RUNTIME_LIBS) 301 | endif 302 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | docutils==0.13.1 2 | mock==2.0.0 3 | Pygments==2.2.0 4 | pytest-cov==2.5.1 5 | pytest-flake8==0.9.1 6 | pytest==3.2.5 7 | Sphinx==1.6.2 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (C) 2019-2020 Cochise Ruhulessin 4 | # 5 | # This file is part of canonical. 6 | # 7 | # canonical is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, version 3. 10 | # 11 | # canonical is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with canonical. If not, see . 18 | import json 19 | import os 20 | import sys 21 | from setuptools import find_namespace_packages 22 | from setuptools import setup 23 | 24 | curdir = os.path.abspath(os.path.dirname(__file__)) 25 | version = str.strip(open('VERSION').read()) 26 | opts = json.loads((open('aioschedule/package.json').read())) 27 | if os.path.exists(os.path.join(curdir, 'README.md')): 28 | with open(os.path.join(curdir, 'README.md'), encoding='utf-8') as f: 29 | opts['long_description'] = f.read() 30 | opts['long_description_content_type'] = "text/markdown" 31 | 32 | setup( 33 | name='aioschedule', 34 | version=version, 35 | packages=['aioschedule'], 36 | include_package_data=True, 37 | **opts) 38 | -------------------------------------------------------------------------------- /test_schedule.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Unit tests for schedule.py""" 3 | import asyncio 4 | import datetime 5 | import functools 6 | import mock 7 | import unittest 8 | 9 | # Silence "missing docstring", "method could be a function", 10 | # "class already defined", and "too many public methods" messages: 11 | # pylint: disable-msg=R0201,C0111,E0102,R0904,R0901 12 | 13 | import aioschedule as schedule 14 | from aioschedule import every 15 | 16 | 17 | def make_mock_job(name=None): 18 | job = Mock() 19 | job.__name__ = name or 'job' 20 | return job 21 | 22 | 23 | class Mock: 24 | invoked = None 25 | call_count = 0 26 | 27 | def reset_mock(self): 28 | self.invoked = None 29 | self.call_count = 0 30 | 31 | def assert_called_once_with(self, *args, **kwargs): 32 | self.invoked == (args, kwargs) 33 | assert self.call_count == 1 34 | 35 | async def __call__(self, *args, **kwargs): 36 | self.invoked = args, kwargs 37 | self.call_count += 1 38 | 39 | 40 | class mock_datetime(object): 41 | """ 42 | Monkey-patch datetime for predictable results 43 | """ 44 | def __init__(self, year, month, day, hour, minute): 45 | self.year = year 46 | self.month = month 47 | self.day = day 48 | self.hour = hour 49 | self.minute = minute 50 | 51 | def __enter__(self): 52 | class MockDate(datetime.datetime): 53 | @classmethod 54 | def today(cls): 55 | return cls(self.year, self.month, self.day) 56 | 57 | @classmethod 58 | def now(cls): 59 | return cls(self.year, self.month, self.day, 60 | self.hour, self.minute) 61 | self.original_datetime = datetime.datetime 62 | datetime.datetime = MockDate 63 | 64 | def __exit__(self, *args, **kwargs): 65 | datetime.datetime = self.original_datetime 66 | 67 | 68 | class SchedulerTests(unittest.TestCase): 69 | def setUp(self): 70 | schedule.clear() 71 | 72 | def run_async(self, func, *args, **kwargs): 73 | loop = asyncio.get_event_loop() 74 | fut = asyncio.ensure_future(func(*args, **kwargs)) 75 | loop.run_until_complete(fut) 76 | return fut.result() 77 | 78 | def test_time_units(self): 79 | assert every().seconds.unit == 'seconds' 80 | assert every().minutes.unit == 'minutes' 81 | assert every().hours.unit == 'hours' 82 | assert every().days.unit == 'days' 83 | assert every().weeks.unit == 'weeks' 84 | 85 | def test_singular_time_units_match_plural_units(self): 86 | assert every().second.unit == every().seconds.unit 87 | assert every().minute.unit == every().minutes.unit 88 | assert every().hour.unit == every().hours.unit 89 | assert every().day.unit == every().days.unit 90 | assert every().week.unit == every().weeks.unit 91 | 92 | def test_time_range(self): 93 | with mock_datetime(2014, 6, 28, 12, 0): 94 | mock_job = make_mock_job() 95 | 96 | # Choose a sample size large enough that it's unlikely the 97 | # same value will be chosen each time. 98 | minutes = set([ 99 | every(5).to(30).minutes.do(mock_job).next_run.minute 100 | for i in range(100) 101 | ]) 102 | 103 | assert len(minutes) > 1 104 | assert min(minutes) >= 5 105 | assert max(minutes) <= 30 106 | 107 | def test_time_range_repr(self): 108 | mock_job = make_mock_job() 109 | 110 | with mock_datetime(2014, 6, 28, 12, 0): 111 | job_repr = repr(every(5).to(30).minutes.do(mock_job)) 112 | 113 | assert job_repr.startswith('Every 5 to 30 minutes do job()') 114 | 115 | def test_at_time(self): 116 | mock_job = make_mock_job() 117 | assert every().day.at('10:30').do(mock_job).next_run.hour == 10 118 | assert every().day.at('10:30').do(mock_job).next_run.minute == 30 119 | 120 | def test_at_time_hour(self): 121 | with mock_datetime(2010, 1, 6, 12, 20): 122 | mock_job = make_mock_job() 123 | assert every().hour.at(':30').do(mock_job).next_run.hour == 12 124 | assert every().hour.at(':30').do(mock_job).next_run.minute == 30 125 | assert every().hour.at(':10').do(mock_job).next_run.hour == 13 126 | assert every().hour.at(':10').do(mock_job).next_run.minute == 10 127 | assert every().hour.at(':00').do(mock_job).next_run.hour == 13 128 | assert every().hour.at(':00').do(mock_job).next_run.minute == 0 129 | 130 | def test_next_run_time(self): 131 | with mock_datetime(2010, 1, 6, 12, 15): 132 | mock_job = make_mock_job() 133 | assert schedule.next_run() is None 134 | assert every().minute.do(mock_job).next_run.minute == 16 135 | assert every(5).minutes.do(mock_job).next_run.minute == 20 136 | assert every().hour.do(mock_job).next_run.hour == 13 137 | assert every().day.do(mock_job).next_run.day == 7 138 | assert every().day.at('09:00').do(mock_job).next_run.day == 7 139 | assert every().day.at('12:30').do(mock_job).next_run.day == 6 140 | assert every().week.do(mock_job).next_run.day == 13 141 | assert every().monday.do(mock_job).next_run.day == 11 142 | assert every().tuesday.do(mock_job).next_run.day == 12 143 | assert every().wednesday.do(mock_job).next_run.day == 13 144 | assert every().thursday.do(mock_job).next_run.day == 7 145 | assert every().friday.do(mock_job).next_run.day == 8 146 | assert every().saturday.do(mock_job).next_run.day == 9 147 | assert every().sunday.do(mock_job).next_run.day == 10 148 | 149 | def test_run_all(self): 150 | mock_job = make_mock_job() 151 | every().minute.do(mock_job) 152 | every().hour.do(mock_job) 153 | every().day.at('11:00').do(mock_job) 154 | self.run_async(schedule.run_all) 155 | assert mock_job.call_count == 3 156 | 157 | def test_job_func_args_are_passed_on(self): 158 | mock_job = make_mock_job() 159 | every().second.do(mock_job, 1, 2, 'three', foo=23, bar={}) 160 | self.run_async(schedule.run_all) 161 | mock_job.assert_called_once_with(1, 2, 'three', foo=23, bar={}) 162 | 163 | def test_to_string(self): 164 | def job_fun(): 165 | pass 166 | s = str(every().minute.do(job_fun, 'foo', bar=23)) 167 | assert 'job_fun' in s 168 | assert 'foo' in s 169 | assert 'bar=23' in s 170 | 171 | def test_to_string_lambda_job_func(self): 172 | assert len(str(every().minute.do(lambda: 1))) > 1 173 | assert len(str(every().day.at('10:30').do(lambda: 1))) > 1 174 | 175 | def test_to_string_functools_partial_job_func(self): 176 | def job_fun(arg): 177 | pass 178 | job_fun = functools.partial(job_fun, 'foo') 179 | job_repr = repr(every().minute.do(job_fun, bar=True, somekey=23)) 180 | assert 'functools.partial' in job_repr 181 | assert 'bar=True' in job_repr 182 | assert 'somekey=23' in job_repr 183 | 184 | def test_run_pending(self): 185 | """Check that run_pending() runs pending jobs. 186 | We do this by overriding datetime.datetime with mock objects 187 | that represent increasing system times. 188 | 189 | Please note that it is *intended behavior that run_pending() does not 190 | run missed jobs*. For example, if you've registered a job that 191 | should run every minute and you only call run_pending() in one hour 192 | increments then your job won't be run 60 times in between but 193 | only once. 194 | """ 195 | mock_job = make_mock_job() 196 | 197 | with mock_datetime(2010, 1, 6, 12, 15): 198 | every().minute.do(mock_job) 199 | every().hour.do(mock_job) 200 | every().day.do(mock_job) 201 | every().sunday.do(mock_job) 202 | self.run_async(schedule.run_pending) 203 | assert mock_job.call_count == 0 204 | 205 | with mock_datetime(2010, 1, 6, 12, 16): 206 | self.run_async(schedule.run_pending) 207 | assert mock_job.call_count == 1 208 | 209 | with mock_datetime(2010, 1, 6, 13, 16): 210 | mock_job.reset_mock() 211 | self.run_async(schedule.run_pending) 212 | assert mock_job.call_count == 2 213 | 214 | with mock_datetime(2010, 1, 7, 13, 16): 215 | mock_job.reset_mock() 216 | self.run_async(schedule.run_pending) 217 | assert mock_job.call_count == 3 218 | 219 | with mock_datetime(2010, 1, 10, 13, 16): 220 | mock_job.reset_mock() 221 | self.run_async(schedule.run_pending) 222 | assert mock_job.call_count == 4 223 | 224 | def test_run_every_weekday_at_specific_time_today(self): 225 | mock_job = make_mock_job() 226 | with mock_datetime(2010, 1, 6, 13, 16): 227 | every().wednesday.at('14:12').do(mock_job) 228 | self.run_async(schedule.run_pending) 229 | assert mock_job.call_count == 0 230 | 231 | with mock_datetime(2010, 1, 6, 14, 16): 232 | self.run_async(schedule.run_pending) 233 | assert mock_job.call_count == 1 234 | 235 | def test_run_every_weekday_at_specific_time_past_today(self): 236 | mock_job = make_mock_job() 237 | with mock_datetime(2010, 1, 6, 13, 16): 238 | every().wednesday.at('13:15').do(mock_job) 239 | self.run_async(schedule.run_pending) 240 | assert mock_job.call_count == 0 241 | 242 | with mock_datetime(2010, 1, 13, 13, 14): 243 | self.run_async(schedule.run_pending) 244 | assert mock_job.call_count == 0 245 | 246 | with mock_datetime(2010, 1, 13, 13, 16): 247 | self.run_async(schedule.run_pending) 248 | assert mock_job.call_count == 1 249 | 250 | def test_run_every_n_days_at_specific_time(self): 251 | mock_job = make_mock_job() 252 | with mock_datetime(2010, 1, 6, 11, 29): 253 | every(2).days.at('11:30').do(mock_job) 254 | self.run_async(schedule.run_pending) 255 | assert mock_job.call_count == 0 256 | 257 | with mock_datetime(2010, 1, 6, 11, 31): 258 | self.run_async(schedule.run_pending) 259 | assert mock_job.call_count == 0 260 | 261 | with mock_datetime(2010, 1, 7, 11, 31): 262 | self.run_async(schedule.run_pending) 263 | assert mock_job.call_count == 0 264 | 265 | with mock_datetime(2010, 1, 8, 11, 29): 266 | self.run_async(schedule.run_pending) 267 | assert mock_job.call_count == 0 268 | 269 | with mock_datetime(2010, 1, 8, 11, 31): 270 | self.run_async(schedule.run_pending) 271 | assert mock_job.call_count == 1 272 | 273 | with mock_datetime(2010, 1, 10, 11, 31): 274 | self.run_async(schedule.run_pending) 275 | assert mock_job.call_count == 2 276 | 277 | def test_next_run_property(self): 278 | original_datetime = datetime.datetime 279 | with mock_datetime(2010, 1, 6, 13, 16): 280 | hourly_job = make_mock_job('hourly') 281 | daily_job = make_mock_job('daily') 282 | every().day.do(daily_job) 283 | every().hour.do(hourly_job) 284 | assert len(schedule.jobs) == 2 285 | # Make sure the hourly job is first 286 | assert schedule.next_run() == original_datetime(2010, 1, 6, 14, 16) 287 | assert schedule.idle_seconds() == 60 * 60 288 | 289 | def test_cancel_job(self): 290 | async def stop_job(): 291 | return schedule.CancelJob 292 | mock_job = make_mock_job() 293 | 294 | every().second.do(stop_job) 295 | mj = every().second.do(mock_job) 296 | assert len(schedule.jobs) == 2 297 | 298 | self.run_async(schedule.run_all) 299 | assert len(schedule.jobs) == 1 300 | assert schedule.jobs[0] == mj 301 | 302 | schedule.cancel_job('Not a job') 303 | assert len(schedule.jobs) == 1 304 | schedule.default_scheduler.cancel_job('Not a job') 305 | assert len(schedule.jobs) == 1 306 | 307 | schedule.cancel_job(mj) 308 | assert len(schedule.jobs) == 0 309 | 310 | def test_cancel_jobs(self): 311 | async def stop_job(): 312 | return schedule.CancelJob 313 | 314 | every().second.do(stop_job) 315 | every().second.do(stop_job) 316 | every().second.do(stop_job) 317 | assert len(schedule.jobs) == 3 318 | 319 | self.run_async(schedule.run_all) 320 | assert len(schedule.jobs) == 0 321 | 322 | def test_tag_type_enforcement(self): 323 | job1 = every().second.do(make_mock_job(name='job1')) 324 | self.assertRaises(TypeError, job1.tag, {}) 325 | self.assertRaises(TypeError, job1.tag, 1, 'a', []) 326 | job1.tag(0, 'a', True) 327 | assert len(job1.tags) == 3 328 | 329 | def test_clear_by_tag(self): 330 | every().second.do(make_mock_job(name='job1')).tag('tag1') 331 | every().second.do(make_mock_job(name='job2')).tag('tag1', 'tag2') 332 | every().second.do(make_mock_job(name='job3')).tag('tag3', 'tag3', 333 | 'tag3', 'tag2') 334 | assert len(schedule.jobs) == 3 335 | self.run_async(schedule.run_all) 336 | assert len(schedule.jobs) == 3 337 | schedule.clear('tag3') 338 | assert len(schedule.jobs) == 2 339 | schedule.clear('tag1') 340 | assert len(schedule.jobs) == 0 341 | every().second.do(make_mock_job(name='job1')) 342 | every().second.do(make_mock_job(name='job2')) 343 | every().second.do(make_mock_job(name='job3')) 344 | schedule.clear() 345 | assert len(schedule.jobs) == 0 346 | 347 | def test_misconfigured_job_wont_break_scheduler(self): 348 | """ 349 | Ensure an interrupted job definition chain won't break 350 | the scheduler instance permanently. 351 | """ 352 | scheduler = schedule.Scheduler() 353 | scheduler.every() 354 | scheduler.every(10).seconds 355 | self.run_async(scheduler.run_pending) 356 | 357 | 358 | if __name__ == '__main__': 359 | unittest.main() 360 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py36, docs 3 | 4 | [tox:travis] 5 | 3.5 = py35, docs 6 | 3.6 = py36, docs 7 | 8 | [testenv] 9 | deps = -rrequirements-dev.txt 10 | commands = 11 | py.test test_schedule.py -v --cov aioschedule --cov-report term-missing 12 | python setup.py check --strict --metadata --restructuredtext 13 | 14 | [testenv:docs] 15 | changedir = docs 16 | deps = -rrequirements-dev.txt 17 | commands = 18 | sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html 19 | --------------------------------------------------------------------------------