├── .codeclimate.yml ├── .gitignore ├── .noserc ├── .project ├── .pydevproject ├── .travis.yml ├── LICENSE ├── README.rst ├── appveyor.yml ├── examples ├── Dockerfile ├── decorator_style_task_app.py ├── every_minute_task_app.py └── supervisord.conf ├── requirements.txt ├── setup.py ├── tests └── crontab_test.py └── tornado_crontab ├── __init__.py └── _crontab.py /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | # https://codeclimate.com/github/gaujin/tornado-crontab 2 | engines: 3 | radon: 4 | enabled: true 5 | config: 6 | threshold: 'B' 7 | pep8: 8 | enabled: true 9 | ratings: 10 | paths: 11 | - "tornado_crontab/**.py" 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | .coverage 4 | .noseids 5 | /.vscode/ 6 | /build/ 7 | /cover/ 8 | /dist/ 9 | /tornado_crontab.egg-info/ 10 | -------------------------------------------------------------------------------- /.noserc: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | with-doctest=1 3 | with-coverage=1 4 | cover-package=tornado_crontab 5 | cover-html=1 6 | verbosity=2 -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | tornado-crontab 4 | 5 | 6 | 7 | 8 | 9 | org.python.pydev.PyDevBuilder 10 | 11 | 12 | 13 | 14 | 15 | org.python.pydev.pythonNature 16 | 17 | 18 | -------------------------------------------------------------------------------- /.pydevproject: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | /${PROJECT_DIR_NAME} 5 | 6 | python 2.7 7 | Default 8 | 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # https://travis-ci.org/gaujin/tornado-crontab 2 | language: python 3 | python: 4 | - "2.7" 5 | - "pypy2.7-5.8.0" 6 | - "3.4" 7 | - "3.5" 8 | - "3.6" 9 | - "3.7-dev" 10 | - "nightly" 11 | - "pypy3.5-5.8.0" 12 | install: "pip install -r requirements.txt" 13 | script: nosetests -v 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Takehito Yamada 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 all 13 | 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 THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | tornado-crontab 3 | =============== 4 | 5 | tornado-crontab is a library that can make the task apps like crontab. 6 | 7 | |travis| |appveyor| |codeclimate| |requires| 8 | 9 | Installation 10 | ============ 11 | 12 | Automatic installation:: 13 | 14 | $ pip install tornado-crontab 15 | 16 | torando-crontab is listed in `PyPI `_ and can be installed with pip or easy_install. 17 | 18 | Manual installation:: 19 | 20 | $ git clone https://github.com/gaujin/tornado-crontab.git 21 | $ cd tornado-crontab 22 | $ python setup.py install 23 | 24 | tornado-crontab source code is `hosted on GitHub `_ 25 | 26 | Usage 27 | ===== 28 | 29 | Here is an example every minute task app:: 30 | 31 | from __future__ import print_function 32 | import functools 33 | import tornado.ioloop 34 | import tornado_crontab 35 | 36 | def hello_crontab(value): 37 | 38 | print("Hello, {0}".format(value)) 39 | 40 | if __name__ == "__main__": 41 | 42 | _func = functools.partial(hello_crontab, *["crontab"]) 43 | tornado_crontab.CronTabCallback(_func, "* * * * *").start() 44 | tornado.ioloop.IOLoop.current().start() 45 | 46 | decorator style task app:: 47 | 48 | from __future__ import print_function 49 | import tornado.ioloop 50 | from tornado_crontab import crontab 51 | 52 | @crontab("* * * * *") 53 | def hello_crontab(value): 54 | 55 | print("Hello, {0}".format(value)) 56 | 57 | if __name__ == "__main__": 58 | 59 | hello_crontab("crontab") 60 | tornado.ioloop.IOLoop.current().start() 61 | 62 | Prerequisites 63 | ============= 64 | 65 | tornado-crontab 0.4.x or earlier runs on Tornado 4.x or earlier. 66 | 67 | Future policy of io_loop argument 68 | ================================= 69 | 70 | | ``io_loop`` argument to function and constructor is deprecated for 0.4.0 and removed for 0.5.0. 71 | | About this policy is based on the policy already indicated in Tornado, tornado-crontab also made the same policy. 72 | 73 | Using 74 | ===== 75 | 76 | * `Tornado `_ 77 | * `crontab `_ 78 | 79 | License 80 | ======= 81 | 82 | * tornado-crontab license under the `MIT license `_. 83 | * `Tornado is licensed under the Apache license `_. 84 | * `crontab is licensed under the LGPL license version 2.1 `_. 85 | 86 | See the LICENSE file for specific terms. 87 | 88 | .. |travis| image:: https://travis-ci.org/gaujin/tornado-crontab.svg?branch=master 89 | :target: https://travis-ci.org/gaujin/tornado-crontab 90 | :alt: Travis CI 91 | 92 | .. |appveyor| image:: https://ci.appveyor.com/api/projects/status/2wsrfhy8sx100hwq?svg=true 93 | :target: https://ci.appveyor.com/project/gaujin/tornado-crontab 94 | :alt: AppVeyor 95 | 96 | .. |codeclimate| image:: https://codeclimate.com/github/gaujin/tornado-crontab/badges/gpa.svg 97 | :target: https://codeclimate.com/github/gaujin/tornado-crontab 98 | :alt: Code Climate 99 | 100 | .. |requires| image:: https://requires.io/github/gaujin/tornado-crontab/requirements.svg?branch=master 101 | :target: https://requires.io/github/gaujin/tornado-crontab/requirements/?branch=master 102 | :alt: Requirements Status 103 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # https://ci.appveyor.com/project/gaujin/tornado-crontab 2 | environment: 3 | matrix: 4 | - PYTHON: "C:/Python27" 5 | - PYTHON: "C:/Python27-x64" 6 | - PYTHON: "C:/Python34" 7 | - PYTHON: "C:/Python34-x64" 8 | - PYTHON: "C:/Python35" 9 | - PYTHON: "C:/Python35-x64" 10 | - PYTHON: "C:/Python36" 11 | - PYTHON: "C:/Python36-x64" 12 | - PYTHON: "C:/Python37" 13 | - PYTHON: "C:/Python37-x64" 14 | 15 | init: 16 | - "ECHO %PYTHON%" 17 | 18 | version: '#{build}' 19 | 20 | install: 21 | - "%PYTHON%/Scripts/pip.exe install -r requirements.txt" 22 | 23 | build: false 24 | 25 | test_script: 26 | - "%PYTHON%/Scripts/nosetests.exe -vv" 27 | -------------------------------------------------------------------------------- /examples/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2-alpine 2 | LABEL maintainer "tornado-crontab@gaujin.jp" 3 | 4 | RUN set -x && \ 5 | adduser -S spam-ham && \ 6 | easy_install -U tornado-crontab supervisor 7 | 8 | COPY ["decorator_style_task_app.py", "supervisord.conf", "/home/spam-ham/"] 9 | 10 | CMD ["/usr/local/bin/supervisord", "-c", "/home/spam-ham/supervisord.conf"] 11 | -------------------------------------------------------------------------------- /examples/decorator_style_task_app.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import tornado.ioloop 3 | from tornado_crontab import crontab 4 | 5 | 6 | @crontab("* * * * *") 7 | def hello_crontab(value): 8 | 9 | print("Hello, {0}".format(value)) 10 | 11 | 12 | if __name__ == "__main__": 13 | 14 | hello_crontab("crontab") 15 | tornado.ioloop.IOLoop.current().start() 16 | -------------------------------------------------------------------------------- /examples/every_minute_task_app.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import functools 3 | import tornado.ioloop 4 | import tornado_crontab 5 | 6 | 7 | def hello_crontab(value): 8 | 9 | print("Hello, {0}".format(value)) 10 | 11 | 12 | if __name__ == "__main__": 13 | 14 | _func = functools.partial(hello_crontab, *["crontab"]) 15 | tornado_crontab.CronTabCallback(_func, "* * * * *").start() 16 | tornado.ioloop.IOLoop.current().start() 17 | -------------------------------------------------------------------------------- /examples/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | 4 | [program:example] 5 | command=python /home/spam-ham/decorator_style_task_app.py 6 | user=spam-ham 7 | stdout_logfile=/dev/fd/1 8 | stdout_logfile_maxbytes=0 9 | redirect_stderr=true -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | coverage==4.5.1 2 | crontab==0.22.2 3 | freezegun==0.3.10 4 | mock==2.0 5 | nose==1.3.7 6 | tornado==4.5.3 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from encodings import codecs 2 | import io 3 | import os 4 | import re 5 | 6 | from setuptools import setup 7 | 8 | 9 | here = os.path.dirname(__file__) 10 | 11 | 12 | def read(*names, **kwargs): 13 | with io.open(os.path.join(here, *names), 14 | encoding=kwargs.get("encoding", "utf8")) as f: 15 | return f.read() 16 | 17 | 18 | def find_version(*file_paths): 19 | version_file = read(*file_paths) 20 | version_match = re.search(r"^version = ['\"]([^'\"]*)['\"]", 21 | version_file, re.M) 22 | if version_match: 23 | return version_match.group(1) 24 | raise RuntimeError("Unable to find version string.") 25 | 26 | 27 | with codecs.open(os.path.join(here, "README.rst"), encoding="utf8") as f: 28 | long_description = f.read() 29 | 30 | 31 | setup( 32 | name="tornado-crontab", 33 | version=find_version("tornado_crontab", "__init__.py"), 34 | packages=["tornado_crontab"], 35 | install_requires=["tornado<5", "crontab"], 36 | author="Takehito Yamada", 37 | author_email="tornado-crontab@gaujin.jp", 38 | url="https://github.com/gaujin/tornado-crontab", 39 | license="MIT", 40 | description="CronTab callback for Tornado", 41 | long_description=long_description, 42 | classifiers=[ 43 | "Development Status :: 4 - Beta", 44 | "License :: OSI Approved :: MIT License", 45 | "Programming Language :: Python :: 2", 46 | "Programming Language :: Python :: 2.7", 47 | "Programming Language :: Python :: 3", 48 | "Programming Language :: Python :: 3.4", 49 | "Programming Language :: Python :: 3.5", 50 | "Programming Language :: Python :: 3.6", 51 | "Programming Language :: Python :: Implementation :: CPython", 52 | "Programming Language :: Python :: Implementation :: PyPy", 53 | ], 54 | ) 55 | -------------------------------------------------------------------------------- /tests/crontab_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | try: 4 | from StringIO import StringIO 5 | except ImportError: 6 | from io import StringIO 7 | 8 | from datetime import datetime 9 | import functools 10 | import logging 11 | import os 12 | import time 13 | import unittest 14 | 15 | from freezegun import freeze_time 16 | 17 | 18 | class FakeTimeIOLoop(object): 19 | 20 | def __init__(self, freezer): 21 | self._offset = 0 22 | self._freezer = freezer 23 | 24 | def time(self): 25 | return time.time() + self._offset 26 | 27 | def add_timeout(self, timeout, callback): 28 | self._callback = (timeout, callback) 29 | 30 | def call_later(self, timeout, callback): 31 | self._max = timeout 32 | 33 | def close(self): 34 | pass 35 | 36 | def start(self): 37 | for _ in range(self._max): 38 | _timeout, _callback = self._callback 39 | self._freezer.stop() 40 | self._freezer.time_to_freeze = datetime.utcfromtimestamp(_timeout) 41 | self._freezer.start() 42 | _callback() 43 | self._offset = _timeout - time.time() 44 | 45 | def stop(self): 46 | pass 47 | 48 | 49 | class TestCronTabCallback(unittest.TestCase): 50 | 51 | BEGIN_TIME = "2015-11-08T00:00:00Z" 52 | 53 | def __init__(self, *args, **kwargs): 54 | super(TestCronTabCallback, self).__init__(*args, **kwargs) 55 | 56 | def setUp(self): 57 | unittest.TestCase.setUp(self) 58 | 59 | self._freezer = freeze_time(self.BEGIN_TIME) 60 | self._freezer.start() 61 | self.io_loop = FakeTimeIOLoop(self._freezer) 62 | self.calls = [] 63 | 64 | def tearDown(self): 65 | unittest.TestCase.tearDown(self) 66 | self._freezer.stop() 67 | 68 | def crontab_task(self): 69 | self.calls.append(datetime.utcfromtimestamp( 70 | self.io_loop.time()).strftime("%Y-%m-%dT%H:%M:%S")) 71 | 72 | def _target(self, schedule): 73 | 74 | from tornado_crontab import CronTabCallback 75 | 76 | pc = CronTabCallback(self.crontab_task, schedule, self.io_loop) 77 | pc.start() 78 | 79 | def _test(self, schedule, asserts): 80 | 81 | self._target(schedule) 82 | self.io_loop.call_later(10, self.io_loop.stop) 83 | self.io_loop.start() 84 | 85 | self.assertEqual(self.calls, asserts) 86 | 87 | def test_every_minute(self): 88 | 89 | self._test("* * * * *", ["2015-11-08T00:01:00", "2015-11-08T00:02:00", 90 | "2015-11-08T00:03:00", "2015-11-08T00:04:00", 91 | "2015-11-08T00:05:00", "2015-11-08T00:06:00", 92 | "2015-11-08T00:07:00", "2015-11-08T00:08:00", 93 | "2015-11-08T00:09:00", "2015-11-08T00:10:00"]) 94 | 95 | def test_every_hour(self): 96 | 97 | self._test("0 * * * *", ["2015-11-08T01:00:00", "2015-11-08T02:00:00", 98 | "2015-11-08T03:00:00", "2015-11-08T04:00:00", 99 | "2015-11-08T05:00:00", "2015-11-08T06:00:00", 100 | "2015-11-08T07:00:00", "2015-11-08T08:00:00", 101 | "2015-11-08T09:00:00", "2015-11-08T10:00:00"]) 102 | 103 | def test_every_day(self): 104 | 105 | self._test("0 0 * * *", ["2015-11-09T00:00:00", "2015-11-10T00:00:00", 106 | "2015-11-11T00:00:00", "2015-11-12T00:00:00", 107 | "2015-11-13T00:00:00", "2015-11-14T00:00:00", 108 | "2015-11-15T00:00:00", "2015-11-16T00:00:00", 109 | "2015-11-17T00:00:00", "2015-11-18T00:00:00"]) 110 | 111 | def test_every_monday(self): 112 | 113 | self._test("0 0 * * 1", ["2015-11-09T00:00:00", "2015-11-16T00:00:00", 114 | "2015-11-23T00:00:00", "2015-11-30T00:00:00", 115 | "2015-12-07T00:00:00", "2015-12-14T00:00:00", 116 | "2015-12-21T00:00:00", "2015-12-28T00:00:00", 117 | "2016-01-04T00:00:00", "2016-01-11T00:00:00"]) 118 | 119 | def test_every_month(self): 120 | 121 | self._test("0 0 1 * *", ["2015-12-01T00:00:00", "2016-01-01T00:00:00", 122 | "2016-02-01T00:00:00", "2016-03-01T00:00:00", 123 | "2016-04-01T00:00:00", "2016-05-01T00:00:00", 124 | "2016-06-01T00:00:00", "2016-07-01T00:00:00", 125 | "2016-08-01T00:00:00", "2016-09-01T00:00:00"]) 126 | 127 | def test_every_year(self): 128 | 129 | self._test("0 0 1 1 *", ["2016-01-01T00:00:00", "2017-01-01T00:00:00", 130 | "2018-01-01T00:00:00", "2019-01-01T00:00:00", 131 | "2020-01-01T00:00:00", "2021-01-01T00:00:00", 132 | "2022-01-01T00:00:00", "2023-01-01T00:00:00", 133 | "2024-01-01T00:00:00", "2025-01-01T00:00:00"]) 134 | 135 | 136 | class TestCrontabDecorator(TestCronTabCallback): 137 | 138 | def __init__(self, *args, **kwargs): 139 | super(TestCrontabDecorator, self).__init__(*args, **kwargs) 140 | 141 | def _target(self, schedule): 142 | 143 | from tornado_crontab import crontab 144 | 145 | @crontab(schedule, self.io_loop) 146 | def decorate_task(): 147 | self.crontab_task() 148 | 149 | decorate_task() 150 | 151 | 152 | class TestCrontabLogging(unittest.TestCase): 153 | 154 | def test__get_func_spec_no_args(self): 155 | 156 | from tornado_crontab import CronTabCallback 157 | 158 | def _func(): 159 | pass 160 | 161 | _crontab = CronTabCallback(_func, "* * * * *") 162 | self.assertEqual((_func, [], {}), _crontab._get_func_spec()) 163 | 164 | def test__get_func_spec_args(self): 165 | 166 | from tornado_crontab import CronTabCallback 167 | 168 | def _func(arg1, arg2): 169 | pass 170 | 171 | _args = ["value1", "value2"] 172 | _crontab = CronTabCallback( 173 | functools.partial(_func, *_args), "* * * * *") 174 | self.assertEqual((_func, _args, {}), _crontab._get_func_spec()) 175 | 176 | _crontab = CronTabCallback( 177 | functools.partial( 178 | functools.partial(_func, _args[0]), _args[1]), "* * * * *") 179 | self.assertEqual((_func, _args, {}), _crontab._get_func_spec()) 180 | 181 | def test__get_func_spec_kwargs(self): 182 | 183 | from tornado_crontab import CronTabCallback 184 | 185 | def _func(arg1, arg2): 186 | pass 187 | 188 | _kwargs = {"arg1": "value1", "arg2": "value2"} 189 | _crontab = CronTabCallback( 190 | functools.partial(_func, **_kwargs), "* * * * *") 191 | self.assertEqual((_func, [], _kwargs), _crontab._get_func_spec()) 192 | 193 | _crontab = CronTabCallback( 194 | functools.partial( 195 | functools.partial(_func, arg1="value1"), arg2="value2"), 196 | "* * * * *") 197 | self.assertEqual((_func, [], _kwargs), _crontab._get_func_spec()) 198 | 199 | def test__logging(self): 200 | 201 | from tornado_crontab import CronTabCallback 202 | from tornado_crontab._crontab import log_crontab 203 | 204 | def _func(arg1, arg2): 205 | pass 206 | 207 | _crontab = CronTabCallback( 208 | functools.partial(_func, "value1", arg2="value2"), "* * * * *") 209 | _crontab._running = True 210 | 211 | _stream = StringIO() 212 | log_crontab.addHandler(logging.StreamHandler(_stream)) 213 | _crontab._logging(logging.DEBUG) 214 | _log = _stream.getvalue() 215 | 216 | # for windows 217 | if os.name == "nt": 218 | user = os.environ.get("USERNAME") 219 | 220 | # for other os 221 | else: 222 | import pwd 223 | user = pwd.getpwuid(os.geteuid()).pw_name 224 | 225 | self.assertEqual( 226 | " ".join(["tornado-crontab[%d]:" % os.getpid(), "(%s)" % user, 227 | "FUNC (_func ['value1'] {'arg2': 'value2'})\n"]), _log) 228 | 229 | 230 | if __name__ == "__main__": 231 | unittest.main() 232 | -------------------------------------------------------------------------------- /tornado_crontab/__init__.py: -------------------------------------------------------------------------------- 1 | from ._crontab import CronTabCallback, crontab 2 | 3 | __all__ = ["CronTabCallback", "crontab", "version", "version_info"] 4 | 5 | version = "0.4.0.dev1" 6 | version_info = tuple([int(v) for v in version.split(".")[:3]]) 7 | -------------------------------------------------------------------------------- /tornado_crontab/_crontab.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import inspect 3 | import logging 4 | import math 5 | import os 6 | import warnings 7 | 8 | from crontab import CronTab 9 | from tornado import version_info as tornado_version_info 10 | from tornado.ioloop import PeriodicCallback 11 | 12 | 13 | log_crontab = logging.getLogger("tornado-crontab.crontab") 14 | FORMAT_LOG_CRONTAB = " ".join(["tornado-crontab[%(pid)d]:", 15 | "(%(user)s)", 16 | "FUNC", 17 | "(%(funcname)s", 18 | "%(args)s %(kwargs)s)"]) 19 | 20 | IS_TZ_SUPPORTED = "default_utc" in inspect.getargspec(CronTab.next).args 21 | UNSUPPORTED_TZ_MESSAGE = """\ 22 | Since crontab package version is low, UTC can not be used. 23 | When using it with UTC, upgrade the crontab package to 0.22.0+.""" 24 | UNSUPPORTED_IOLOOP_MESSAGE = """\ 25 | Since tornado package version is high, io_loop can not be used. 26 | When using it with io_loop, downgrade the tornado package to 4.5.3 or earlier.""" 27 | 28 | 29 | class CronTabCallback(PeriodicCallback): 30 | """ Crontab Callback Class 31 | 32 | Schedule execution of the function. 33 | Timezone is local time by default. 34 | If you want to schedule with UTC, set `is_utc` to `True`. 35 | 36 | .. versionchanged:: 0.3.3 37 | Supported UTC. 38 | If Timezone is not supported and `is_utc` is set to `True`, 39 | a warning is output and `is_utc` is ignored. 40 | 41 | .. versionchanged:: 0.4 42 | Supported Tornado 5, warning is output and `io_loop` is ignored. 43 | The `io_loop` argument is deprecated. 44 | """ 45 | 46 | def __init__(self, callback, schedule, io_loop=None, is_utc=False): 47 | """ CrontabCallback initializer 48 | 49 | :type callback: func 50 | :param callback: target schedule function 51 | :type schedule: str 52 | :param schedule: crotab expression 53 | :type io_loop: tornado.ioloop.IOLoop 54 | :param io_loop: tornado IOLoop 55 | :type is_utc: bool 56 | :param is_utc: schedule timezone is UTC. (True:UTC, False:Local Timezone) 57 | """ 58 | 59 | # If Timezone is not supported and `is_utc` is set to `True`, 60 | # a warning is output and `is_utc` is ignored. 61 | if not IS_TZ_SUPPORTED and is_utc: 62 | warnings.warn(UNSUPPORTED_TZ_MESSAGE) 63 | is_utc = False 64 | 65 | self.__crontab = CronTab(schedule) 66 | self.__is_utc = is_utc 67 | 68 | arguments = dict( 69 | callback=callback, callback_time=self._calc_callbacktime()) 70 | 71 | if tornado_version_info >= (5,): 72 | if io_loop is not None: 73 | warnings.warn(UNSUPPORTED_IOLOOP_MESSAGE) 74 | else: 75 | arguments.update(io_loop=io_loop) 76 | 77 | super(CronTabCallback, self).__init__(**arguments) 78 | 79 | self.pid = os.getpid() 80 | 81 | if os.name == "nt": 82 | self.user = os.environ.get("USERNAME") 83 | else: 84 | import pwd 85 | self.user = pwd.getpwuid(os.geteuid()).pw_name 86 | 87 | def _calc_callbacktime(self, now=None): 88 | 89 | _kwargs = dict(now=now) 90 | 91 | if IS_TZ_SUPPORTED: 92 | _kwargs.update(dict(default_utc=self.__is_utc)) 93 | 94 | return math.ceil( 95 | self.__crontab.next(**_kwargs)) * 1000.0 96 | 97 | def _get_func_spec(self): 98 | 99 | _args = [] 100 | _kwargs = {} 101 | 102 | def _get_func(_func): 103 | 104 | if not isinstance(_func, functools.partial): 105 | return _func 106 | 107 | for _arg in reversed(_func.args): 108 | _args.insert(0, _arg) 109 | 110 | if _func.keywords: 111 | _kwargs.update(_func.keywords) 112 | 113 | return _get_func(_func.func) 114 | 115 | _func = _get_func(self.callback) 116 | return _func, _args, _kwargs 117 | 118 | def _logging(self, level): 119 | 120 | if self._running and log_crontab.isEnabledFor(level): 121 | 122 | _func, _args, _kwargs = self._get_func_spec() 123 | 124 | log_crontab.log(level, 125 | FORMAT_LOG_CRONTAB % dict(pid=self.pid, 126 | user=self.user, 127 | funcname=_func.__name__, 128 | args=_args, 129 | kwargs=_kwargs)) 130 | 131 | def _run(self): 132 | 133 | self._logging(logging.INFO) 134 | 135 | try: 136 | PeriodicCallback._run(self) 137 | 138 | finally: 139 | 140 | self._logging(logging.DEBUG) 141 | 142 | def _schedule_next(self): 143 | self.callback_time = self._calc_callbacktime() 144 | super(CronTabCallback, self)._schedule_next() 145 | 146 | 147 | def crontab(schedule, io_loop=None, is_utc=False): 148 | """ Crontab Decorator 149 | 150 | Decorate this function to the function you want to execute as scheduled. 151 | Timezone is local time by default. 152 | If you want to schedule with UTC, set `is_utc` to `True`. 153 | 154 | .. versionchanged:: 0.3.3 155 | Supported UTC. 156 | If Timezone is not supported and `is_utc` is set to `True`, 157 | a warning is output and `is_utc` is ignored. 158 | 159 | .. versionchanged:: 0.4 160 | Supported Tornado 5, warning is output and `io_loop` is ignored. 161 | The `io_loop` argument is deprecated. 162 | 163 | :type schedule: str 164 | :param schedule: crotab expression 165 | :type io_loop: tornado.ioloop.IOLoop 166 | :param io_loop: tornado IOLoop 167 | :type is_utc: bool 168 | :param is_utc: schedule timezone is UTC. (True:UTC, False:Local Timezone) 169 | :rtype: func 170 | :return: scheduled execute function 171 | """ 172 | 173 | def receive_func(func): 174 | 175 | @functools.wraps(func) 176 | def wrapper(*args, **kwargs): 177 | 178 | _func = functools.partial(func, *args, **kwargs) 179 | CronTabCallback(_func, schedule=schedule, 180 | io_loop=io_loop, is_utc=is_utc).start() 181 | 182 | return wrapper 183 | return receive_func 184 | --------------------------------------------------------------------------------