├── .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 |
--------------------------------------------------------------------------------