├── example ├── __init__.py ├── jobs │ ├── __init__.py │ ├── hello_job.py │ └── time_job.py ├── job_listener_test.py ├── cron_trigger_test.py └── simple_trigger_test.py ├── task_test ├── __init__.py ├── cron_parser_test.py └── date_helper_test.py ├── task ├── job │ ├── __init__.py │ ├── job.py │ └── job_listener.py ├── trigger │ ├── __init__.py │ ├── simple_trigger.py │ ├── date_helper.py │ ├── trigger.py │ └── cron_trigger.py ├── __init__.py ├── task.py ├── task_runner.py └── task_container.py ├── MANIFEST.in ├── setup.cfg ├── .gitignore ├── README.md ├── setup.py ├── README.rst ├── PKG-INFO └── LICENSE /example/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /task_test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/jobs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /task/job/__init__.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 -------------------------------------------------------------------------------- /task/trigger/__init__.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 -------------------------------------------------------------------------------- /task/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | __version__ = "1.1" 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft task 2 | graft task_test 3 | include *.md *.rst *.cfg 4 | global-exclude *.pyc 5 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | 4 | [egg_info] 5 | tag_build = 6 | tag_svn_revision = 0 7 | tag_date = 0 8 | 9 | -------------------------------------------------------------------------------- /task/job/job.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | ''' 3 | job 4 | 5 | @author: Huiyugeng 6 | ''' 7 | 8 | class Job(object): 9 | 10 | def __init__(self): 11 | pass 12 | 13 | def execute(self): 14 | pass 15 | 16 | -------------------------------------------------------------------------------- /example/jobs/hello_job.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import time 4 | 5 | from task.job import job 6 | 7 | class HelloJob(job.Job): 8 | 9 | def __init__(self): 10 | pass 11 | 12 | def execute(self): 13 | print 'Just say Hello at %s' % str(time.time()) 14 | -------------------------------------------------------------------------------- /task/trigger/simple_trigger.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | ''' 3 | simple trigger 4 | 5 | @author: Huiyugeng 6 | ''' 7 | import trigger 8 | 9 | class SimpleTrigger(trigger.Trigger): 10 | 11 | def __init__(self, repeat_count = 0, repeat_interval = 1): 12 | trigger.Trigger.__init__(self, repeat_count, repeat_interval); 13 | 14 | def _is_match(self): 15 | return True 16 | 17 | -------------------------------------------------------------------------------- /task/job/job_listener.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | ''' 3 | job listener 4 | 5 | @author: Huiyugeng 6 | ''' 7 | 8 | class JobListener(object): 9 | 10 | def __init__(self): 11 | pass 12 | 13 | def init(self): 14 | pass 15 | 16 | def before_execute(self): 17 | pass 18 | 19 | def after_execute(self): 20 | pass 21 | 22 | def destory(self): 23 | pass 24 | 25 | def status_changed(self, status): 26 | pass 27 | 28 | def execute_exception(self): 29 | pass 30 | 31 | def execute_fail(self, exception): 32 | pass 33 | -------------------------------------------------------------------------------- /example/job_listener_test.py: -------------------------------------------------------------------------------- 1 | ''' 2 | job listener test case 3 | 4 | @author: Huiyugeng 5 | ''' 6 | 7 | import time 8 | 9 | from task import task 10 | from task import task_container 11 | 12 | from task.trigger import simple_trigger 13 | from example.jobs import time_job 14 | 15 | container = task_container.TaskContainer() 16 | 17 | def test_job_listener(): 18 | 19 | trigger1 = simple_trigger.SimpleTrigger(1, 2) 20 | time_task1 = task.Task('TimeTask1', time_job.TimeJob(), trigger1, time_job.TimeJobListener('time-job')) 21 | container.add_task(time_task1) 22 | 23 | trigger2 = simple_trigger.SimpleTrigger(1, 2) 24 | time_task2 = task.Task('TimeTask2', time_job.TimeJobWithException(), trigger2, time_job.TimeJobListener('exception-job')) 25 | container.add_task(time_task2) 26 | 27 | container.start_all(True) 28 | 29 | time.sleep(2) # stop container 30 | 31 | container.stop_all() 32 | 33 | test_job_listener() -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | .pydevproject 56 | .project 57 | .settings/org.eclipse.core.resources.prefs 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #py-task# 2 | 3 | py-task is a task scheduling tools for Python. 4 | 5 | - Support repeat trigger 6 | - Support cron trigger 7 | - Easy to use in Python 8 | 9 | ## Installation ## 10 | 11 | The lastest stable is py-task-1.1.tar.gz 12 | 13 | python setup.py install 14 | 15 | ## Getting Start ## 16 | 17 | demo for py-task: 18 | 19 | from task import task 20 | from task import task_container 21 | from task.job import job 22 | from task.trigger import cron_trigger 23 | 24 | class MyJob(job.Job): 25 | 26 | def __init__(self): 27 | pass 28 | 29 | def execute(self): 30 | print 'Hello now is ' + str(time.time()) 31 | 32 | cron = '0-59/5 10,15,20 * * * * 2015' 33 | new_task = task.Task('Task', MyJob(), cron_trigger.CronTrigger(cron)) 34 | container.add_task(new_task) 35 | container.start_all() 36 | 37 | ## Documentation ## 38 | 39 | Full documentation is hosted on [HERE](). 40 | Sources are available in the `docs/` directory. 41 | 42 | ## License ## 43 | 44 | py-task is licensed under the Apache License, Version 2.0. See LICENSE for full license text 45 | 46 | -------------------------------------------------------------------------------- /task/task.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | ''' 3 | task 4 | 5 | @author: Huiyugeng 6 | ''' 7 | 8 | import time 9 | 10 | from job import job_listener 11 | 12 | class Task(): 13 | 14 | def __init__(self, name, job, trigger, job_listener = None): 15 | self.serial = str(time.time()) + name 16 | self.name = name 17 | self.job = job 18 | self.job_listener = job_listener 19 | self.trigger = trigger 20 | 21 | self.status = 0 # -1:exception, 0:stop, 1:running, 2:pause 22 | 23 | def get_name(self): 24 | return self.name 25 | 26 | def get_serial(self): 27 | return self.serial 28 | 29 | def set_status(self, status): 30 | self.status = status 31 | if self.job_listener != None and isinstance(self.job_listener, job_listener.JobListener): 32 | self.job_listener.status_changed(status) 33 | 34 | def get_status(self): 35 | return self.status 36 | 37 | def get_job(self): 38 | return self.job 39 | 40 | def get_job_listener(self): 41 | return self.job_listener 42 | 43 | def get_trigger(self): 44 | return self.trigger 45 | 46 | -------------------------------------------------------------------------------- /example/cron_trigger_test.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Cron trigger test case 3 | 4 | @author: Huiyugeng 5 | ''' 6 | 7 | import datetime 8 | 9 | from task import task 10 | from task import task_container 11 | 12 | from task.trigger import cron_trigger 13 | 14 | from example.jobs import time_job 15 | from example.jobs import hello_job 16 | 17 | container = task_container.TaskContainer() 18 | 19 | def test_cron_trigger(): 20 | 21 | #start time_job hour=* minute=10-50 second=every 5 seconds 22 | trigger_1 = cron_trigger.CronTrigger('0-59/5 10-50 * * * * *') 23 | trigger_1.set_date(datetime.datetime(2015, 7, 9, 1, 25), datetime.datetime(2015, 7, 10, 1, 40)); 24 | 25 | task_1 = task.Task('Task_1', time_job.TimeJob(), trigger_1) 26 | container.add_task(task_1) 27 | 28 | #start hello_job hour=* minute=15-45 second=every 22 seconds 29 | trigger_2 = cron_trigger.CronTrigger('0-59/22 15-45 * * * * *') 30 | trigger_2.set_date(datetime.datetime(2015, 7, 9, 1, 25)); 31 | 32 | task_2 = task.Task('Task_2', hello_job.HelloJob(), trigger_2) 33 | container.add_task(task_2) 34 | 35 | print '---------Start---------' 36 | container.start_all() 37 | 38 | if __name__ == '__main__': 39 | test_cron_trigger() -------------------------------------------------------------------------------- /task_test/cron_parser_test.py: -------------------------------------------------------------------------------- 1 | ''' 2 | cron parser unit test case 3 | 4 | @author: Huiyugeng 5 | ''' 6 | 7 | import datetime 8 | 9 | import unittest 10 | from task.trigger import cron_trigger 11 | 12 | class CronParserTest(unittest.TestCase): 13 | 14 | def test_parse_cron(self): 15 | 16 | parser = cron_trigger.CronParser('0-59/5 15-35 * * * * *') 17 | _date = datetime.datetime(2015, 5, 12, 20, 20, 10) 18 | self.assertEqual(parser._is_match(_date.date(), _date.time()), True) 19 | 20 | parser = cron_trigger.CronParser('0-59/5 15-35 20 10-20 MAY * *') 21 | _date = datetime.datetime(2015, 5, 12, 20, 20, 10) 22 | self.assertEqual(parser._is_match(_date.date(), _date.time()), True) 23 | 24 | parser = cron_trigger.CronParser('0-59/5 15-35 20 * MAY SUN *') 25 | _date = datetime.datetime(2015, 5, 17, 20, 20, 10) 26 | self.assertEqual(parser._is_match(_date.date(), _date.time()), True) 27 | 28 | parser = cron_trigger.CronParser('0-59/5 15-35 20 * MAY SUN 2013-2015') 29 | _date = datetime.datetime(2015, 5, 17, 20, 20, 10) 30 | self.assertEqual(parser._is_match(_date.date(), _date.time()), True) -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | 2 | import task 3 | 4 | try: 5 | from setuptools import setup 6 | except ImportError: 7 | from distutils.core import setup 8 | 9 | 10 | with open('README.md', 'r', 'utf-8') as fp: 11 | long_description = fp.read() 12 | 13 | packages = ['task', 'task.job', 'task.trigger'] 14 | 15 | classifiers = [ 16 | 'Intended Audience :: Developers', 17 | 'Development Status :: 5 - Production/Stable', 18 | 'Natural Language :: English', 19 | 'License :: OSI Approved :: Apache Software License', 20 | 'Programming Language :: Python', 21 | 'Programming Language :: Python :: 2.6', 22 | 'Programming Language :: Python :: 2.7', 23 | 'Topic :: Software Development :: Libraries', 24 | 'Topic :: Utilities' 25 | ] 26 | 27 | setup( 28 | name = 'py-task', 29 | version = task.__version__, 30 | author = 'Yugeng Hui', 31 | author_email = 'hyg@pinae.org', 32 | url = 'https://github.com/PinaeOS/py-task', 33 | packages = packages, 34 | description = 'Task scheduling tools for python', 35 | long_description = long_description, 36 | license = 'Apache 2.0', 37 | classifiers = classifiers 38 | ) 39 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | py-task 2 | ============================================ 3 | 4 | py-task is a task scheduling tools for Python. 5 | 6 | - Support repeat trigger 7 | - Support cron trigger 8 | - Easy to use in Python 9 | 10 | Installation 11 | -------------- 12 | 13 | The lastest stable is py-task-1.1.tar.gz 14 | 15 | .. code-block:: shell 16 | 17 | python setup.py install 18 | 19 | Getting Start 20 | -------------- 21 | 22 | demo for py-task: 23 | 24 | .. code-block:: python 25 | 26 | from task import task 27 | from task import task_container 28 | from task.job import job 29 | from task.trigger import cron_trigger 30 | 31 | class MyJob(job.Job): 32 | 33 | def __init__(self): 34 | pass 35 | 36 | def execute(self): 37 | print 'Hello now is ' + str(time.time()) 38 | 39 | cron = '0-59/5 10,15,20 * * * * 2015' 40 | new_task = task.Task('Task', MyJob(), cron_trigger.CronTrigger(cron)) 41 | container.add_task(new_task) 42 | container.start_all() 43 | 44 | Documentation 45 | -------------- 46 | 47 | Full documentation is hosted on [HERE](). 48 | Sources are available in the ``docs/`` directory. 49 | 50 | License 51 | -------------- 52 | 53 | py-task is licensed under the Apache License, Version 2.0. See LICENSE for full license text -------------------------------------------------------------------------------- /task/trigger/date_helper.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | ''' 3 | date helper 4 | 5 | @author: Huiyugeng 6 | ''' 7 | import datetime 8 | 9 | def next_seconds(seconds = 1): 10 | now = datetime.datetime.now() 11 | time_delta = datetime.timedelta(seconds = seconds) 12 | 13 | return now + time_delta 14 | 15 | def next_minutes(minutes = 1): 16 | now = datetime.datetime.now() 17 | time_delta = datetime.timedelta(minutes = minutes) 18 | 19 | return now + time_delta 20 | 21 | def next_hours(hours = 1): 22 | now = datetime.datetime.now() 23 | time_delta = datetime.timedelta(hours = hours) 24 | 25 | return now + time_delta 26 | 27 | def today(hours = 0, minutes = 0, seconds = 0): 28 | _date = datetime.date.today() 29 | 30 | return datetime.datetime(_date.year, _date.month, _date.day, 31 | hours, minutes, seconds) 32 | 33 | def tomrrow(hours = 0, minutes = 0, seconds = 0): 34 | return next_days(1, hours, minutes, seconds) 35 | 36 | def next_days(days = 1, hours = 0, minutes = 0, seconds = 0): 37 | now = datetime.datetime.now() 38 | time_delta = datetime.timedelta(days = days) 39 | 40 | _date = now + time_delta 41 | return datetime.datetime(_date.year, _date.month, _date.day, 42 | hours, minutes, seconds) 43 | -------------------------------------------------------------------------------- /task_test/date_helper_test.py: -------------------------------------------------------------------------------- 1 | ''' 2 | date helper unit test case 3 | 4 | @author: Huiyugeng 5 | ''' 6 | 7 | import unittest 8 | 9 | import datetime 10 | from task.trigger import date_helper 11 | 12 | class DateHelperTest(unittest.TestCase): 13 | 14 | def test_date_helper(self): 15 | '''next_seconds test''' 16 | _time = date_helper.next_seconds(10) 17 | _now = datetime.datetime.now() 18 | delta = (_time - _now).seconds 19 | self.assertEqual(int(round(delta)), 10) 20 | 21 | '''next_minutes test''' 22 | _time = date_helper.next_minutes(10) 23 | _now = datetime.datetime.now() 24 | delta = (_time - _now).seconds 25 | self.assertEqual(int(round(delta)), 10 * 60) 26 | 27 | '''next_hours test''' 28 | _time = date_helper.next_hours(10) 29 | _now = datetime.datetime.now() 30 | delta = (_time - _now).seconds 31 | self.assertEqual(int(round(delta)), 10 * 60 * 60) 32 | 33 | '''today test''' 34 | _time = date_helper.today() 35 | _now = datetime.date.today() 36 | self.assertEqual(_now.year, _time.year) 37 | self.assertEqual(_now.month, _time.month) 38 | self.assertEqual(_now.day, _time.day) 39 | 40 | '''next_days test''' 41 | _now = datetime.datetime.now() 42 | _time = date_helper.next_days(3, _now.hour, _now.minute, _now.second) 43 | delta = (_time - _now).days 44 | self.assertEqual(int(round(delta)), 2) 45 | 46 | -------------------------------------------------------------------------------- /example/simple_trigger_test.py: -------------------------------------------------------------------------------- 1 | ''' 2 | simple trigger test case 3 | 4 | @author: Huiyugeng 5 | ''' 6 | 7 | import time 8 | 9 | from task import task 10 | from task import task_container 11 | 12 | from task.trigger import simple_trigger 13 | 14 | from example.jobs import time_job 15 | 16 | container = task_container.TaskContainer() 17 | 18 | def get_now(): 19 | time_stamp = time.time() 20 | time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time_stamp)) 21 | return time_now 22 | 23 | def test_simple_trigger(): 24 | job1 = time_job.JobA() 25 | trigger1 = simple_trigger.SimpleTrigger(0, 2) 26 | task1 = task.Task('TaskA', job1, trigger1) 27 | container.add_task(task1) 28 | 29 | job2 = time_job.JobB() 30 | trigger2 = simple_trigger.SimpleTrigger(0, 5) 31 | trigger2.set_delay(5, 0) 32 | task2 = task.Task('TaskB', job2, trigger2) 33 | container.add_task(task2) 34 | 35 | print 'start at %s' % get_now() 36 | print '----Start (With Daemon)----' 37 | container.start_all(True) 38 | 39 | time.sleep(11) # pause container 40 | print '---------Pause All---------' 41 | container.pasuse_all() 42 | 43 | print container.stat_tasks() 44 | 45 | time.sleep(10) # restart container 46 | print '--------Restart All--------' 47 | container.start_all() 48 | 49 | print container.stat_tasks() 50 | 51 | time.sleep(11) # stop task 52 | print '--------Stop Task A--------' 53 | container.stop('TaskA') 54 | 55 | print container.stat_tasks() 56 | 57 | time.sleep(10) # restart task 58 | print '------Start Task A--------' 59 | container.start('TaskA') 60 | 61 | time.sleep(11) # remove task 62 | print '---------Remove A---------' 63 | container.remove_task('TaskA') 64 | 65 | time.sleep(10) # stop all 66 | print '---------Stop All---------' 67 | container.stop_all() 68 | 69 | test_simple_trigger() -------------------------------------------------------------------------------- /task/trigger/trigger.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | ''' 4 | trigger 5 | 6 | @author: Huiyugeng 7 | ''' 8 | import datetime 9 | import time 10 | 11 | class Trigger(object): 12 | 13 | def __init__(self, repeat_count = 0, repeat_interval = 1): 14 | self.repeat_count = repeat_count 15 | self.repeat_interval = repeat_interval 16 | 17 | self.start_delay = 0 18 | self.end_delay = 0 19 | 20 | self.start_date = None 21 | self.end_date = None 22 | 23 | self.execute_count = 0 24 | 25 | def set_delay(self, start_delay = 0, end_delay = 0): 26 | self.start_delay = start_delay 27 | self.end_delay = end_delay 28 | 29 | def set_date(self, start_date = None, end_date = None): 30 | if start_date != None and isinstance(start_date, datetime.datetime): 31 | self.start_date = time.mktime(start_date.timetuple()) 32 | if end_date != None and isinstance(end_date, datetime.datetime): 33 | self.end_date = time.mktime(start_date.timetuple()) 34 | 35 | def _get_repeat_interval(self): 36 | return self.repeat_interval 37 | 38 | def _get_start_delay(self): 39 | if self.start_delay == 0 and self.start_date != None: 40 | now = time.time() 41 | self.start_delay = self.start_date - now 42 | return self.start_delay 43 | 44 | def _get_end_delay(self): 45 | if self.end_delay == 0 and self.end_date != None: 46 | now = time.time() 47 | self.end_delay = self.end_date - now 48 | return self.end_delay 49 | 50 | def _inc_execute_count(self): 51 | self.execute_count = self.execute_count + 1 52 | 53 | def _is_finish(self): 54 | # is reach repeat counter 55 | if self.repeat_count != 0 and self.repeat_count <= self.execute_count: 56 | return True 57 | 58 | # is match endtime 59 | now = time.time() 60 | if self.end_date != None and now >= self.end_date: 61 | return True 62 | 63 | return False 64 | -------------------------------------------------------------------------------- /example/jobs/time_job.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import time 4 | 5 | from task.job import job 6 | 7 | class TimeJob(job.Job): 8 | 9 | def __init__(self): 10 | pass 11 | 12 | def execute(self): 13 | time_stamp = time.time() 14 | time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time_stamp)) 15 | 16 | print 'Time-Job : Now is ' + time_now 17 | 18 | return False 19 | 20 | class TimeJobWithException(job.Job): 21 | 22 | def __init__(self): 23 | pass 24 | 25 | def execute(self): 26 | time_stamp = time.time() 27 | time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time_stamp)) 28 | 29 | print 'Time-Job (With Exception) : Now is ' + time_now 30 | 31 | raise RuntimeError('Throws exception from Job') 32 | 33 | from task.job import job_listener 34 | 35 | class TimeJobListener(job_listener.JobListener): 36 | 37 | def __init__(self, name): 38 | self.name = name 39 | 40 | def init(self): 41 | print '%s init' % self.name 42 | 43 | def before_execute(self): 44 | print '%s before-execute' % self.name 45 | 46 | def after_execute(self): 47 | print '%s after-execute' % self.name 48 | 49 | def destory(self): 50 | print '%s destory' % self.name 51 | 52 | def status_changed(self, status): 53 | print '%s status: %s' % (self.name, str(status)) 54 | 55 | def execute_fail(self): 56 | print '%s fail' % self.name 57 | 58 | def execute_exception(self, exception): 59 | print '%s exception: %s' % (self.name, exception.message) 60 | 61 | class JobA(job.Job): 62 | 63 | def __init__(self): 64 | pass 65 | 66 | def execute(self): 67 | time_stamp = time.time() 68 | time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time_stamp)) 69 | 70 | print 'JobA : Now is ' + time_now 71 | 72 | class JobB(job.Job): 73 | 74 | def execute(self): 75 | time_stamp = time.time() 76 | time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time_stamp)) 77 | 78 | print 'JobB : Now is ' + time_now -------------------------------------------------------------------------------- /PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 1.1 2 | Name: py-task 3 | Version: 1.1 4 | Author: Yugeng Hui 5 | Author-email: hyg at pinae org 6 | Home-page: https://github.com/PinaeOS/py-task 7 | Summary: Task scheduling tools for python 8 | License: Apache 2.0 9 | Description: py-task 10 | ============================================ 11 | 12 | py-task is a task scheduling tools for Python. 13 | 14 | - Support repeat trigger 15 | - Support cron trigger 16 | - Easy to use in Python 17 | 18 | Installation 19 | -------------- 20 | 21 | The lastest stable is py-task-1.1.tar.gz 22 | 23 | .. code-block:: shell 24 | 25 | python setup.py install 26 | 27 | Getting Start 28 | -------------- 29 | 30 | demo for py-task: 31 | 32 | .. code-block:: python 33 | 34 | from task import task 35 | from task import task_container 36 | from task.job import job 37 | from task.trigger import cron_trigger 38 | 39 | class MyJob(job.Job): 40 | 41 | def __init__(self): 42 | pass 43 | 44 | def execute(self): 45 | print 'Hello now is ' + str(time.time()) 46 | 47 | cron = '0-59/5 10,15,20 * * * * 2015' 48 | new_task = task.Task('Task', MyJob(), cron_trigger.CronTrigger(cron)) 49 | container.add_task(new_task) 50 | container.start_all() 51 | 52 | Documentation 53 | -------------- 54 | 55 | Full documentation is hosted on [HERE](). 56 | Sources are available in the ``docs/`` directory. 57 | 58 | License 59 | -------------- 60 | 61 | py-task is licensed under the Apache License, Version 2.0. See LICENSE for full license text 62 | Keywords: python cron scheduling 63 | Classifier: Development Status :: 5 - Production/Stable 64 | Classifier: License :: OSI Approved :: Apache Software License 65 | Classifier: Programming Language :: Python 66 | Classifier: Programming Language :: Python :: 2.6 67 | Classifier: Programming Language :: Python :: 2.7 68 | Classifier: Topic :: Software Development 69 | Classifier: Topic :: Utilities 70 | -------------------------------------------------------------------------------- /task/trigger/cron_trigger.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | ''' 3 | cron trigger 4 | 5 | @author: Huiyugeng 6 | ''' 7 | import datetime 8 | 9 | import trigger 10 | 11 | class CronTrigger(trigger.Trigger): 12 | 13 | def __init__(self, cron): 14 | trigger.Trigger.__init__(self, 0, 1); 15 | self.cron = cron 16 | 17 | def _is_match(self): 18 | parser = CronParser(self.cron) 19 | 20 | _date = datetime.date.today() 21 | _time = datetime.datetime.now() 22 | 23 | return parser._is_match(_date, _time) 24 | 25 | class CronParser(): 26 | 27 | def __init__(self, cron): 28 | cron_item = cron.split(' ') 29 | if len(cron_item) == 6 or len(cron_item) == 7: 30 | 31 | self.second_set = self._parse_integer(cron_item[0], 0, 59) 32 | self.minute_set = self._parse_integer(cron_item[1], 0, 59) 33 | self.hour_set = self._parse_integer(cron_item[2], 0, 23) 34 | self.day_of_month_set = self._parse_integer(cron_item[3], 1, 31) 35 | self.month_set = self._parse_month(cron_item[4]) 36 | self.day_of_week_set = self._parse_day_of_week(cron_item[5]) 37 | if len(cron_item) == 7: 38 | self.year_set = self._parse_integer(cron_item[6], 1970, 2100) 39 | 40 | def _parse_integer(self, value, min_val, max_val): 41 | result = [] 42 | 43 | range_items = [] 44 | if ',' in value: 45 | range_items = value.split(',') 46 | else: 47 | range_items.append(value) 48 | 49 | for range_item in range_items: 50 | temp_result = [] 51 | 52 | interval = 1 53 | 54 | if '/' in range_item: 55 | temp = range_item.split('/') 56 | range_item = temp[0] 57 | interval = int(temp[1]) 58 | 59 | if interval < 1: 60 | interval = 1 61 | 62 | if '*' in range_item: 63 | temp_result.extend(self._add_to_set(min_val, max_val)) 64 | elif '-' in range_item: 65 | item = range_item.split('-') 66 | temp_result.extend(self._add_to_set(int(item[0]), int(item[1]))) 67 | else: 68 | temp_result.append(int(range_item)) 69 | 70 | count = 0 71 | for item in temp_result: 72 | if count % interval == 0: 73 | result.append(item) 74 | count = count + 1 75 | 76 | return result 77 | 78 | def _add_to_set(self, start, end): 79 | result = [i for i in range(start, end + 1)] 80 | return result 81 | 82 | def _parse_month(self, value): 83 | months = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"] 84 | for i in range(0, 12): 85 | value = value.replace(months[i], str(i + 1)) 86 | return self._parse_integer(value, 1, 12); 87 | 88 | def _parse_day_of_week(self, value): 89 | day_of_weeks = ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"] 90 | for i in range(0, 7): 91 | value = value.replace(day_of_weeks[i], str(i + 1)); 92 | return self._parse_integer(value, 1, 7); 93 | 94 | def _is_match(self, _date, _time): 95 | 96 | # In Python datetime's weekday Monday is 0 and Sunday is 6 97 | day_of_week = _date.weekday() + 1 98 | 99 | result = True and \ 100 | _time.second in self.second_set and \ 101 | _time.minute in self.minute_set and \ 102 | _time.hour in self.hour_set and \ 103 | _date.day in self.day_of_month_set and \ 104 | _date.month in self.month_set and \ 105 | _date.year in self.year_set and \ 106 | day_of_week in self.day_of_week_set 107 | 108 | return result 109 | -------------------------------------------------------------------------------- /task/task_runner.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | ''' 3 | task runner 4 | 5 | @author: Huiyugeng 6 | ''' 7 | import time 8 | import threading 9 | import logging 10 | 11 | from job import job, job_listener 12 | from trigger import trigger 13 | 14 | class TaskRunner(threading.Thread): 15 | 16 | def __init__(self, _task): 17 | 18 | threading.Thread.__init__(self) 19 | 20 | self._task = _task 21 | 22 | self.name = _task.get_name() 23 | self.job = _task.get_job() 24 | self.job_listener = _task.get_job_listener() 25 | self.trigger = _task.get_trigger() 26 | 27 | self.started = False 28 | self.stop_flag = True 29 | self.pause_flag = False 30 | 31 | self.interval = 1 32 | 33 | if self.job == None or isinstance(self.job, job.Job) is False: 34 | raise AttributeError('Job is None') 35 | if self.trigger == None or isinstance(self.trigger, trigger.Trigger) is False: 36 | raise AttributeError('Trigger is None') 37 | if self.job_listener != None and isinstance(self.job_listener, job_listener.JobListener): 38 | self.job_listener.init() 39 | 40 | 41 | def run(self): 42 | start_delay = self.trigger._get_start_delay() 43 | if start_delay > 0: 44 | time.sleep(start_delay) 45 | 46 | while self.stop_flag is False: 47 | if self.pause_flag is False and self.trigger._is_match(): 48 | self.trigger._inc_execute_count() # executer counter 49 | try: 50 | if self.job_listener != None and isinstance(self.job_listener, job_listener.JobListener): 51 | self.job_listener.before_execute() 52 | 53 | result = self.job.execute() # execute job 54 | if result != None and result == False: 55 | if self.job_listener != None and isinstance(self.job_listener, job_listener.JobListener): 56 | self.job_listener.execute_fail() 57 | 58 | if self.job_listener != None and isinstance(self.job_listener, job_listener.JobListener): 59 | self.job_listener.after_execute() 60 | 61 | except Exception, ex: 62 | if self.job_listener != None and isinstance(self.job_listener, job_listener.JobListener): 63 | self.job_listener.execute_exception(ex) 64 | self._task.set_status(-1) 65 | 66 | if self.trigger._is_finish() is False: 67 | time.sleep(self.interval) 68 | else: 69 | break 70 | 71 | end_delay = self.trigger._get_end_delay() 72 | if end_delay > 0: 73 | time.sleep(end_delay) 74 | if self.job_listener != None and isinstance(self.job_listener, job_listener.JobListener): 75 | self.job_listener.destory() 76 | 77 | def start_task(self): 78 | self.started = True 79 | if self.pause_flag is True: 80 | self.pause_flag = False 81 | self._task.set_status(1) 82 | elif self.trigger != None and isinstance(self.trigger, trigger.Trigger): 83 | 84 | self.interval = self.trigger._get_repeat_interval() 85 | 86 | if self.stop_flag is True: 87 | try: 88 | self._task.set_status(1) 89 | self.stop_flag = False 90 | self.start() 91 | except Exception, ex: 92 | self._task.set_status(-1) 93 | logging.error('Start task: %s Exception: %s' % (self.name, ex.message)) 94 | 95 | def stop_task(self): 96 | self.stop_flag = True 97 | self._task.set_status(0) 98 | 99 | def pause_task(self): 100 | self.pause_flag = True 101 | self._task.set_status(2) 102 | 103 | class DaemonTask(threading.Thread): 104 | def __init__(self): 105 | threading.Thread.__init__(self, name = 'Daemon-Task') 106 | self.stop_flag = False 107 | 108 | def run(self): 109 | while self.stop_flag is False: 110 | time.sleep(1) 111 | 112 | def start_task(self): 113 | self.start() 114 | 115 | def stop_task(self): 116 | self.stop_flag = True 117 | -------------------------------------------------------------------------------- /task/task_container.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | ''' 3 | task container 4 | 5 | @author: Huiyugeng 6 | ''' 7 | import types 8 | import logging 9 | 10 | import task 11 | import task_runner 12 | 13 | class TaskContainer(object): 14 | 15 | def __init__(self, max_task = 100): 16 | self.max_task = max_task 17 | 18 | self.tasks = {} 19 | self.task_runners = {} 20 | 21 | self.daemon = task_runner.DaemonTask() 22 | 23 | def _check_task(self, _task): 24 | if _task != None and isinstance(_task, task.Task): 25 | name = _task.get_name() 26 | if name != None: 27 | return True 28 | return False 29 | 30 | def add_task(self, _task): 31 | logging.info('add task: %s' % _task.get_name()) 32 | if self._check_task(_task): 33 | if len(self.tasks) < self.max_task: 34 | self.tasks[_task.get_name()] = _task 35 | else: 36 | logging.error('task constainer is FULL, max size is %d' % self.max_task) 37 | else: 38 | logging.error('task is NOT instance of task.Task') 39 | 40 | def remove_task(self, _task): 41 | if type(_task) == types.StringType: 42 | name = str(_task) 43 | elif self._check_task(_task): 44 | name = _task.get_name() 45 | 46 | if name != None: 47 | if self.task_runners.has_key(name): 48 | r = self.task_runners.get(name) 49 | if r != None: 50 | r.stop_task() 51 | del(self.task_runners[name]) 52 | del(self.tasks[name]) 53 | 54 | def get_task(self, name): 55 | if name != None: 56 | return self.tasks.get(name) 57 | return None 58 | 59 | def get_tasks(self): 60 | return self.tasks 61 | 62 | def stat_tasks(self): 63 | 64 | status = {-1 : 'exception', 0 : 'stop', 1 : 'running', 2 : 'pause'} 65 | 66 | status_stat = {'exception' : 0, 'stop' : 0, 'running' : 0, 'pause' : 0} 67 | 68 | for name in self.tasks: 69 | try: 70 | flag = self.tasks.get(name).get_status() 71 | status_stat[status[flag]] = status_stat[status[flag]] + 1 72 | except: 73 | pass 74 | 75 | return status_stat 76 | 77 | def _new_task_runner(self, name): 78 | r = None 79 | _task = self.tasks.get(name) 80 | if _task != None and isinstance(_task, task.Task): 81 | r = task_runner.TaskRunner(_task) 82 | self.task_runners[name] = r 83 | else: 84 | logging.error('task %s is NOT instance of task.Task' % name) 85 | 86 | return r 87 | 88 | def start(self, name): 89 | 90 | logging.info('start task: %s' % name) 91 | if name != None: 92 | if self.task_runners.has_key(name): 93 | r = self.task_runners.get(name) 94 | else: 95 | r = self._new_task_runner(name) 96 | 97 | if r != None: 98 | if r.started is True and r.stop_flag is True: 99 | r = self._new_task_runner(name) 100 | r.start_task() 101 | else: 102 | logging.error('Task Runner is NONE') 103 | else: 104 | logging.error('task name is NONE') 105 | 106 | def start_all(self, daemon = False): 107 | for name in self.tasks: 108 | self.start(name) 109 | if daemon: 110 | self.daemon.start_task() 111 | 112 | 113 | def pause(self, name): 114 | logging.info('pause task: %s' % name) 115 | if name != None: 116 | r = self.task_runners.get(name) 117 | r.pause_task() 118 | else: 119 | logging.error('No such running task %s' % name) 120 | 121 | def pasuse_all(self): 122 | for name in self.tasks: 123 | self.pause(name) 124 | 125 | def stop(self, name): 126 | logging.info('stop task: %s' % name) 127 | if name != None: 128 | r = self.task_runners.get(name) 129 | r.stop_task() 130 | else: 131 | logging.error('No such running task %s' % name) 132 | 133 | def stop_all(self): 134 | for name in self.tasks: 135 | self.stop(name) 136 | self.daemon.stop_task() 137 | 138 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The Artistic License 2.0 2 | 3 | Copyright (c) 2015 Yugeng Hui 4 | 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | This license establishes the terms under which a given free software 11 | Package may be copied, modified, distributed, and/or redistributed. 12 | The intent is that the Copyright Holder maintains some artistic 13 | control over the development of that Package while still keeping the 14 | Package available as open source and free software. 15 | 16 | You are always permitted to make arrangements wholly outside of this 17 | license directly with the Copyright Holder of a given Package. If the 18 | terms of this license do not permit the full use that you propose to 19 | make of the Package, you should contact the Copyright Holder and seek 20 | a different licensing arrangement. 21 | 22 | Definitions 23 | 24 | "Copyright Holder" means the individual(s) or organization(s) 25 | named in the copyright notice for the entire Package. 26 | 27 | "Contributor" means any party that has contributed code or other 28 | material to the Package, in accordance with the Copyright Holder's 29 | procedures. 30 | 31 | "You" and "your" means any person who would like to copy, 32 | distribute, or modify the Package. 33 | 34 | "Package" means the collection of files distributed by the 35 | Copyright Holder, and derivatives of that collection and/or of 36 | those files. A given Package may consist of either the Standard 37 | Version, or a Modified Version. 38 | 39 | "Distribute" means providing a copy of the Package or making it 40 | accessible to anyone else, or in the case of a company or 41 | organization, to others outside of your company or organization. 42 | 43 | "Distributor Fee" means any fee that you charge for Distributing 44 | this Package or providing support for this Package to another 45 | party. It does not mean licensing fees. 46 | 47 | "Standard Version" refers to the Package if it has not been 48 | modified, or has been modified only in ways explicitly requested 49 | by the Copyright Holder. 50 | 51 | "Modified Version" means the Package, if it has been changed, and 52 | such changes were not explicitly requested by the Copyright 53 | Holder. 54 | 55 | "Original License" means this Artistic License as Distributed with 56 | the Standard Version of the Package, in its current version or as 57 | it may be modified by The Perl Foundation in the future. 58 | 59 | "Source" form means the source code, documentation source, and 60 | configuration files for the Package. 61 | 62 | "Compiled" form means the compiled bytecode, object code, binary, 63 | or any other form resulting from mechanical transformation or 64 | translation of the Source form. 65 | 66 | 67 | Permission for Use and Modification Without Distribution 68 | 69 | (1) You are permitted to use the Standard Version and create and use 70 | Modified Versions for any purpose without restriction, provided that 71 | you do not Distribute the Modified Version. 72 | 73 | 74 | Permissions for Redistribution of the Standard Version 75 | 76 | (2) You may Distribute verbatim copies of the Source form of the 77 | Standard Version of this Package in any medium without restriction, 78 | either gratis or for a Distributor Fee, provided that you duplicate 79 | all of the original copyright notices and associated disclaimers. At 80 | your discretion, such verbatim copies may or may not include a 81 | Compiled form of the Package. 82 | 83 | (3) You may apply any bug fixes, portability changes, and other 84 | modifications made available from the Copyright Holder. The resulting 85 | Package will still be considered the Standard Version, and as such 86 | will be subject to the Original License. 87 | 88 | 89 | Distribution of Modified Versions of the Package as Source 90 | 91 | (4) You may Distribute your Modified Version as Source (either gratis 92 | or for a Distributor Fee, and with or without a Compiled form of the 93 | Modified Version) provided that you clearly document how it differs 94 | from the Standard Version, including, but not limited to, documenting 95 | any non-standard features, executables, or modules, and provided that 96 | you do at least ONE of the following: 97 | 98 | (a) make the Modified Version available to the Copyright Holder 99 | of the Standard Version, under the Original License, so that the 100 | Copyright Holder may include your modifications in the Standard 101 | Version. 102 | 103 | (b) ensure that installation of your Modified Version does not 104 | prevent the user installing or running the Standard Version. In 105 | addition, the Modified Version must bear a name that is different 106 | from the name of the Standard Version. 107 | 108 | (c) allow anyone who receives a copy of the Modified Version to 109 | make the Source form of the Modified Version available to others 110 | under 111 | 112 | (i) the Original License or 113 | 114 | (ii) a license that permits the licensee to freely copy, 115 | modify and redistribute the Modified Version using the same 116 | licensing terms that apply to the copy that the licensee 117 | received, and requires that the Source form of the Modified 118 | Version, and of any works derived from it, be made freely 119 | available in that license fees are prohibited but Distributor 120 | Fees are allowed. 121 | 122 | 123 | Distribution of Compiled Forms of the Standard Version 124 | or Modified Versions without the Source 125 | 126 | (5) You may Distribute Compiled forms of the Standard Version without 127 | the Source, provided that you include complete instructions on how to 128 | get the Source of the Standard Version. Such instructions must be 129 | valid at the time of your distribution. If these instructions, at any 130 | time while you are carrying out such distribution, become invalid, you 131 | must provide new instructions on demand or cease further distribution. 132 | If you provide valid instructions or cease distribution within thirty 133 | days after you become aware that the instructions are invalid, then 134 | you do not forfeit any of your rights under this license. 135 | 136 | (6) You may Distribute a Modified Version in Compiled form without 137 | the Source, provided that you comply with Section 4 with respect to 138 | the Source of the Modified Version. 139 | 140 | 141 | Aggregating or Linking the Package 142 | 143 | (7) You may aggregate the Package (either the Standard Version or 144 | Modified Version) with other packages and Distribute the resulting 145 | aggregation provided that you do not charge a licensing fee for the 146 | Package. Distributor Fees are permitted, and licensing fees for other 147 | components in the aggregation are permitted. The terms of this license 148 | apply to the use and Distribution of the Standard or Modified Versions 149 | as included in the aggregation. 150 | 151 | (8) You are permitted to link Modified and Standard Versions with 152 | other works, to embed the Package in a larger work of your own, or to 153 | build stand-alone binary or bytecode versions of applications that 154 | include the Package, and Distribute the result without restriction, 155 | provided the result does not expose a direct interface to the Package. 156 | 157 | 158 | Items That are Not Considered Part of a Modified Version 159 | 160 | (9) Works (including, but not limited to, modules and scripts) that 161 | merely extend or make use of the Package, do not, by themselves, cause 162 | the Package to be a Modified Version. In addition, such works are not 163 | considered parts of the Package itself, and are not subject to the 164 | terms of this license. 165 | 166 | 167 | General Provisions 168 | 169 | (10) Any use, modification, and distribution of the Standard or 170 | Modified Versions is governed by this Artistic License. By using, 171 | modifying or distributing the Package, you accept this license. Do not 172 | use, modify, or distribute the Package, if you do not accept this 173 | license. 174 | 175 | (11) If your Modified Version has been derived from a Modified 176 | Version made by someone other than you, you are nevertheless required 177 | to ensure that your Modified Version complies with the requirements of 178 | this license. 179 | 180 | (12) This license does not grant you the right to use any trademark, 181 | service mark, tradename, or logo of the Copyright Holder. 182 | 183 | (13) This license includes the non-exclusive, worldwide, 184 | free-of-charge patent license to make, have made, use, offer to sell, 185 | sell, import and otherwise transfer the Package with respect to any 186 | patent claims licensable by the Copyright Holder that are necessarily 187 | infringed by the Package. If you institute patent litigation 188 | (including a cross-claim or counterclaim) against any party alleging 189 | that the Package constitutes direct or contributory patent 190 | infringement, then this Artistic License to you shall terminate on the 191 | date that such litigation is filed. 192 | 193 | (14) Disclaimer of Warranty: 194 | THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS 195 | IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED 196 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR 197 | NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL 198 | LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL 199 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 200 | DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF 201 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 202 | 203 | --------------------------------------------------------------------------------