Skip to content
This repository has been archived by the owner on Oct 17, 2018. It is now read-only.

Commit

Permalink
Initial commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
bcho committed Feb 3, 2015
0 parents commit be05d09
Show file tree
Hide file tree
Showing 11 changed files with 326 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
build/
*.egg-info/
dist/
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2.7.9
Empty file added CHANGES.md
Empty file.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2015 hbc

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Donkey [WIP]

A simple cron-like library for executing scheduled jobs.


Donkey is inspired by [Cron][cron-go].

[cron-go]: https://github.com/robfig/cron


```python
from datetime import datetime
from donkey import JobQueue, Worker


q = JobQueue()


@q.job(3)
def this_job_runs_every_3_seconds():
print('Fuzz', datetime.now())


@q.job(5)
def this_job_runs_every_5_seconds():
print('Buzz', datetime.now())


Worker().run(q)
# Fuzz 2015-02-03 16:41:01.408136
# Buzz 2015-02-03 16:41:03.404123
# Fuzz 2015-02-03 16:41:04.406813
# Fuzz 2015-02-03 16:41:07.408426
# Buzz 2015-02-03 16:41:08.406851
# Fuzz 2015-02-03 16:41:10.408415
# Fuzz 2015-02-03 16:41:13.403260
# Buzz 2015-02-03 16:41:13.403319
```

## TODO

- [ ] tests.
- [ ] add jobs at run time.
- [ ] job states & stats (see [rq][rq]).
- [ ] other backend (namely `thread`, `stackless`) support.


[rq]: http://python-rq.org/


## License

[MIT](LICENSE)
17 changes: 17 additions & 0 deletions donkey/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# coding: utf-8

'''
donkey
~~~~~~~
A simple cron-like library for executing scheduled jobs.
'''

from .worker import Worker
from .job import Schedule, Job, JobQueue


__all__ = ['Worker', 'Schedule', 'Job', 'JobQueue']


__version__ = '0.0.1'
28 changes: 28 additions & 0 deletions donkey/compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# coding: utf-8

'''
donkey.compat
~~~~~~~~~~~~~
Compatibility.
'''

import gevent


def spawn(func, *args, **kwargs):
'''Spawn a function.
By now only `gevent` is supported.
:param func: function to be ran.
'''
return gevent.spawn(func, *args, **kwargs)


def sleep(seconds):
'''Sleep for some times.
:param seconds: seonds you want to sleep.
'''
return gevent.sleep(seconds)
134 changes: 134 additions & 0 deletions donkey/job.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# coding: utf-8

'''
donkey.job
~~~~~~~~~~
Donkey job data structure.
'''

from .compat import spawn


class Schedule(object):
'''Job schedule defination.
:param interval: execute interval (in seconds).
'''

def __init__(self, interval):
# TODO support fuzzy language / crontab format
self.interval = int(interval)

def next(self, now):
'''Calculate next runnable time (in unix timestamp).
:param now: current unix timestamp.
'''
return now + self.interval


class Job(object):
'''Scheduled job.
TODO job state
:param exec_: the actual job.
:param schedule: a :class:`donkey.Schedule` instance.
'''

def __init__(self, exec_, schedule):
self.exec_ = exec_
self.schedule = schedule

self._last_run_at = None
self._next_run_at = None

def run(self):
'''Execute the job'''
rv = self.exec_()

return rv

def reschedule(self, now):
'''Reschedule the job base on current timestamp.
:param now: current unix timestamp.
'''
self._last_run_at = self.next_run_at
self._next_run_at = self.schedule.next(now)

@property
def last_run_at(self):
'''Last ran timestamp.'''
return self._last_run_at

@property
def next_run_at(self):
'''Next ran timestamp.'''
return self._next_run_at


class JobQueue(object):

def __init__(self):
# TODO race condition
self.jobs = []

def add(self, job):
'''Add a job to the queue.
:param job: a :class:`donkey.Job` instance.
'''
self.jobs.append(job)

def job(self, interval):
'''Return a decorator for adding a function as job:
@queue.job(300)
def my_job():
print('I am working...')
:param interval: execute interval (in seconds).
'''
def wrapper(func):
job = Job(func, Schedule(interval))

return self.add(job)

return wrapper

def get_runnable_jobs(self):
'''Get runnable jobs.'''
# Sort by next run time, sooner the better.
self.jobs.sort(key=lambda j: j.next_run_at)

return self.jobs

@property
def next_run_at(self):
'''Get next running unix timestamp.'''
jobs = self.get_runnable_jobs()

if not jobs:
return None
return jobs[0].next_run_at

def run_jobs(self, now):
'''Execute jobs run at this moment.
:param now: current unix timestamp.
'''
for job in self.jobs:
if job.next_run_at != now:
return
spawn(job.run)
job.reschedule(now)

def reschedule_jobs(self, now):
'''Reschedule all jobs base on current timestamp.
:param now: current unix timestamp.
'''
for job in self.jobs:
job.reschedule(now)
32 changes: 32 additions & 0 deletions donkey/worker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# coding: utf-8

'''
donkey.worker
~~~~~~~~~~~~~
Donkey worker.
'''

from time import time as get_now_timestamp

from .compat import sleep


A_LONG_TIME = 10 * 365 * 24 * 3600 # 10 years


class Worker(object):

def run(self, queue):
'''Let the worker run.
:param queue: :class:`donkey.JobQueue` instance.
'''
queue.reschedule_jobs(get_now_timestamp())

while True:
effective = queue.next_run_at or A_LONG_TIME

# Wait until next schedule comes...
sleep(effective - get_now_timestamp())
queue.run_jobs(effective)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
gevent
36 changes: 36 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# coding: utf-8

from setuptools import setup, find_packages

README = open('README.md').read()
CHANGES = open('CHANGES.md').read()


setup(
name='donkey',
version='0.0.1',

author='hbc',
author_email='[email protected]',
url='https://github.com/bcho/donkey',

description='A simple cron-like library for executing scheduled jobs.',
long_description='\n'.join((README, CHANGES)),
license='MIT',

packages=find_packages(exclude=['tests']),
include_package_data=True,
install_requires=[
'gevent',
],

classifiers=[
'Development Status :: 4 - Beta',
'Operating System :: POSIX :: Linux',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 2',
'Topic :: Software Development :: Libraries :: Python Modules',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
]
)

0 comments on commit be05d09

Please sign in to comment.