├── .gitignore ├── README.markdown ├── django_ztask ├── __init__.py ├── conf │ ├── __init__.py │ └── settings.py ├── context.py ├── decorators.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── ztaskd.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto__add_field_task_created.py │ └── __init__.py └── models.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | IMPORTANT: READ ME 2 | ================== 3 | 4 | In version 0.1.4, we are introducing two major changes: 5 | 6 | 1. Tasks now have a `created` datetime field. This was added to make sure `--replayfailed` replayed tasks in the appropriate order 7 | 2. Introduced [South](http://south.aeracode.org/) migrations. 8 | 9 | IF YOU HAVE ALREADY INSTALLED `django-ztask` - you can "fake" the first migration, and then run the second migration: 10 | 11 | ./manage.py migrate django_ztask --fake 0001 12 | ./manage.py migrate django_ztask 13 | 14 | If you are not using [South](http://south.aeracode.org/) in your Django project, it is strongly recommended you do. If you 15 | are not, you will have to add the "created" field to your database manually. 16 | 17 | Installing 18 | ========== 19 | 20 | Download and install 0MQ version 2.1.3 or better from [http://www.zeromq.org](http://www.zeromq.org) 21 | 22 | Install pyzmq and django-ztask using PIP: 23 | 24 | pip install pyzmq 25 | pip install -e git+git@github.com:dmgctrl/django-ztask.git#egg=django_ztask 26 | 27 | Add `django_ztask` to your `INSTALLED_APPS` setting in `settings.py` 28 | 29 | INSTALLED_APPS = ( 30 | ..., 31 | 'django_ztask', 32 | ) 33 | 34 | Then run `syncdb` 35 | 36 | python manage.py syncdb 37 | 38 | 39 | Running the server 40 | ================== 41 | 42 | Run django-ztask using the manage.py command: 43 | 44 | python manage.py ztaskd 45 | 46 | 47 | Command-line arguments 48 | ---------------------- 49 | 50 | The `ztaskd` command takes a series of command-line arguments: 51 | 52 | - `--noreload` 53 | 54 | By default, `ztaskd` will use the built-in Django reloader 55 | to reload the server whenever a change is made to a python file. Passing 56 | in `--noreload` will prevent it from listening for changed files. 57 | (Good to use in production.) 58 | 59 | - `-l` or `--loglevel` 60 | 61 | Choose from the standard `CRITICAL`, `ERROR`, `WARNING`, 62 | `INFO`, `DEBUG`, or `NOTSET`. If this argument isn't passed 63 | in, `INFO` is used by default. 64 | 65 | - `-f` or `--logfile` 66 | 67 | The file to log messages to. By default, all messages are logged 68 | to `stdout` 69 | 70 | - `--replayfailed` 71 | 72 | If a command has failed more times than allowed in the 73 | `ZTASKD_RETRY_COUNT` (see below for more), the task is 74 | logged as failed. Passing in `--replayfailed` will cause all 75 | failed tasks to be re-run. 76 | 77 | 78 | Settings 79 | -------- 80 | 81 | There are several settings that you can put in your `settings.py` file in 82 | your Django project. These are the settings and their defaults 83 | 84 | ZTASKD_URL = 'tcp://127.0.0.1:5555' 85 | 86 | By default, `ztaskd` will run over TCP, listening on 127.0.0.1 port 5555. 87 | 88 | ZTASKD_ALWAYS_EAGER = False 89 | 90 | If set to `True`, all `.async` and `.after` tasks will be run in-process and 91 | not sent to the `ztaskd` process. Good for task debugging. 92 | 93 | ZTASKD_DISABLED = False 94 | 95 | If set, all tasks will be logged, but not executed. This setting is often 96 | used during testing runs. If you set `ZTASKD_DISABLED` before running 97 | `python manage.py test`, tasks will be logged, but not executed. 98 | 99 | ZTASKD_RETRY_COUNT = 5 100 | 101 | The number of times a task should be reattempted before it is considered failed. 102 | 103 | ZTASKD_RETRY_AFTER = 5 104 | 105 | The number, in seconds, to wait in-between task retries. 106 | 107 | ZTASKD_ON_LOAD = () 108 | 109 | This is a list of callables - either classes or functions - that are called when the server first 110 | starts. This is implemented to support several possible Django setup scenarios when launching 111 | `ztask` - for an example, see the section below called **Implementing with Johnny Cache**. 112 | 113 | 114 | Running in production 115 | --------------------- 116 | 117 | A recommended way to run in production would be to put something similar to 118 | the following in to your `rc.local` file. This example has been tested on 119 | Ubuntu 10.04 and Ubuntu 10.10: 120 | 121 | #!/bin/bash -e 122 | pushd /var/www/path/to/site 123 | sudo -u www-data python manage.py ztaskd --noreload -f /var/log/ztaskd.log & 124 | popd 125 | 126 | 127 | Making functions in to tasks 128 | ============================ 129 | 130 | Decorators and function extensions make tasks able to run. 131 | Unlike some solutions, tasks can be in any file anywhere. 132 | When the file is imported, `ztaskd` will register the task for running. 133 | 134 | **Important note: all functions and their arguments must be able to be pickled.** 135 | 136 | ([Read more about pickling here](http://docs.python.org/tutorial/inputoutput.html#the-pickle-module)) 137 | 138 | It is a recommended best practice that instead of passing a Django model object 139 | to a task, you intead pass along the model's ID or primary key, and re-get 140 | the object in the task function. 141 | 142 | The @task Decorator 143 | ------------------- 144 | 145 | from django_ztask.decorators import task 146 | 147 | The `@task()` decorator will turn any normal function in to a 148 | `django_ztask` task if called using one of the function extensions. 149 | 150 | Function extensions 151 | ------------------- 152 | 153 | Any function can be called in one of three ways: 154 | 155 | - `func(*args, *kwargs)` 156 | 157 | Calling a function normally will bypass the decorator and call the function directly 158 | 159 | - `func.async(*args, **kwargs)` 160 | 161 | Calling a function with `.async` will cause the function task to be called asyncronously 162 | on the ztaskd server. For backwards compatability, `.delay` will do the same thing as `.async`, but is deprecated. 163 | 164 | - `func.after(seconds, *args, **kwargs)` 165 | 166 | This will cause the task to be sent to the `ztaskd` server, which will wait `seconds` 167 | seconds to execute. 168 | 169 | 170 | Example 171 | ------- 172 | 173 | from django_ztask.decorators import task 174 | 175 | @task() 176 | def print_this(what_to_print): 177 | print what_to_print 178 | 179 | if __name__ == '__main__': 180 | 181 | # Call the function directly 182 | print_this('Hello world!') 183 | 184 | # Call the function asynchronously 185 | print_this.async('This will print to the ztaskd log') 186 | 187 | # Call the function asynchronously 188 | # after a 5 second delay 189 | print_this.after(5, 'This will print to the ztaskd log') 190 | 191 | 192 | Implementing with Johnny Cache 193 | ============================== 194 | 195 | Because [Johnny Cache](http://packages.python.org/johnny-cache/) monkey-patches all the Django query compilers, 196 | any changes to models in django-ztask that aren't properly patched won't reflect on your site until the cache 197 | is cleared. Since django-ztask doesn't concern itself with Middleware, you must put Johnny Cache's query cache 198 | middleware in as a callable in the `ZTASKD_ON_LOAD` setting. 199 | 200 | ZTASKD_ON_LOAD = ( 201 | 'johnny.middleware.QueryCacheMiddleware', 202 | ... 203 | ) 204 | 205 | If you wanted to do this and other things, you could write your own function, and pass that in to 206 | `ZTASKD_ON_LOAD`, as in this example: 207 | 208 | **myutilities.py** 209 | 210 | def ztaskd_startup_stuff(): 211 | ''' 212 | Stuff to run every time the ztaskd server 213 | is started or reloaded 214 | ''' 215 | from johnny import middleware 216 | middleware.QueryCacheMiddleware() 217 | ... # Other setup stuff 218 | 219 | **settings.py** 220 | 221 | ZTASKD_ON_LOAD = ( 222 | 'myutilities.ztaskd_startup_stuff', 223 | ... 224 | ) 225 | 226 | 227 | TODOs and BUGS 228 | ============== 229 | See: [http://github.com/dmgctrl/django-ztask/issues](http://github.com/dmgctrl/django-ztask/issues) 230 | -------------------------------------------------------------------------------- /django_ztask/__init__.py: -------------------------------------------------------------------------------- 1 | """Django ZTask.""" 2 | import os 3 | 4 | VERSION = (0, 1, 5) 5 | 6 | __version__ = ".".join(map(str, VERSION[0:3])) + "".join(VERSION[3:]) 7 | __author__ = "Jason Allum and Dave Martorana" 8 | __contact__ = "jason@dmgctrl.com" 9 | __homepage__ = "http://github.com/dmgctrl/django-ztask" 10 | __docformat__ = "markdown" 11 | __license__ = "BSD (3 clause)" 12 | -------------------------------------------------------------------------------- /django_ztask/conf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmgctrl/django-ztask/ea5210060be4c9280d7cf3e4a30ad9142f76c47d/django_ztask/conf/__init__.py -------------------------------------------------------------------------------- /django_ztask/conf/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | ZTASKD_URL = getattr(settings, 'ZTASKD_URL', 'tcp://127.0.0.1:5555') 4 | ZTASKD_ALWAYS_EAGER = getattr(settings, 'ZTASKD_ALWAYS_EAGER', False) 5 | ZTASKD_DISABLED = getattr(settings, 'ZTASKD_DISABLED', False) 6 | ZTASKD_RETRY_COUNT = getattr(settings, 'ZTASKD_RETRY_COUNT', 5) 7 | ZTASKD_RETRY_AFTER = getattr(settings, 'ZTASKD_RETRY_AFTER', 5) 8 | 9 | ZTASKD_ON_LOAD = getattr(settings, 'ZTASKD_ON_LOAD', ()) 10 | #ZTASKD_ON_CALL_COMPLETE = getattr(settings, 'ZTASKD_ON_COMPLETE', ()) -------------------------------------------------------------------------------- /django_ztask/context.py: -------------------------------------------------------------------------------- 1 | import zmq 2 | 3 | shared_context = zmq.Context() 4 | -------------------------------------------------------------------------------- /django_ztask/decorators.py: -------------------------------------------------------------------------------- 1 | from django.utils.decorators import available_attrs 2 | from functools import wraps 3 | 4 | import logging 5 | import types 6 | 7 | def task(): 8 | from django_ztask.conf import settings 9 | try: 10 | from zmq import PUSH 11 | except: 12 | from zmq import DOWNSTREAM as PUSH 13 | def wrapper(func): 14 | function_name = '%s.%s' % (func.__module__, func.__name__) 15 | 16 | logger = logging.getLogger('ztaskd') 17 | logger.info('Registered task: %s' % function_name) 18 | 19 | from django_ztask.context import shared_context as context 20 | socket = context.socket(PUSH) 21 | socket.connect(settings.ZTASKD_URL) 22 | @wraps(func) 23 | def _func(*args, **kwargs): 24 | after = kwargs.pop('__ztask_after', 0) 25 | if settings.ZTASKD_DISABLED: 26 | try: 27 | socket.send_pyobj(('ztask_log', ('Would have called but ZTASKD_DISABLED is True', function_name), None, 0)) 28 | except: 29 | logger.info('Would have sent %s but ZTASKD_DISABLED is True' % function_name) 30 | return 31 | elif settings.ZTASKD_ALWAYS_EAGER: 32 | logger.info('Running %s in ZTASKD_ALWAYS_EAGER mode' % function_name) 33 | if after > 0: 34 | logger.info('Ignoring timeout of %d seconds because ZTASKD_ALWAYS_EAGER is set' % after) 35 | func(*args, **kwargs) 36 | else: 37 | try: 38 | socket.send_pyobj((function_name, args, kwargs, after)) 39 | except Exception, e: 40 | if after > 0: 41 | logger.info('Ignoring timeout of %s seconds because function is being run in-process' % after) 42 | func(*args, **kwargs) 43 | 44 | def _func_delay(*args, **kwargs): 45 | try: 46 | socket.send_pyobj(('ztask_log', ('.delay is depricated... use.async instead', function_name), None, 0)) 47 | except: 48 | pass 49 | _func(*args, **kwargs) 50 | 51 | def _func_after(*args, **kwargs): 52 | try: 53 | after = args[0] 54 | if type(after) != types.IntType: 55 | raise TypeError('The first argument of .after must be an integer representing seconds to wait') 56 | kwargs['__ztask_after'] = after 57 | _func(*args[1:], **kwargs) 58 | except Exception, e: 59 | logger.info('Error adding delayed task:\n%s' % e) 60 | 61 | setattr(func, 'async', _func) 62 | setattr(func, 'delay', _func_delay) 63 | setattr(func, 'after', _func_after) 64 | return func 65 | 66 | return wrapper 67 | -------------------------------------------------------------------------------- /django_ztask/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmgctrl/django-ztask/ea5210060be4c9280d7cf3e4a30ad9142f76c47d/django_ztask/management/__init__.py -------------------------------------------------------------------------------- /django_ztask/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmgctrl/django-ztask/ea5210060be4c9280d7cf3e4a30ad9142f76c47d/django_ztask/management/commands/__init__.py -------------------------------------------------------------------------------- /django_ztask/management/commands/ztaskd.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from django.utils import autoreload 3 | # 4 | from django_ztask.models import * 5 | # 6 | from django_ztask.conf import settings 7 | from django_ztask.context import shared_context as context 8 | # 9 | import zmq 10 | from zmq.eventloop import ioloop 11 | try: 12 | from zmq import PULL 13 | except: 14 | from zmq import UPSTREAM as PULL 15 | # 16 | from optparse import make_option 17 | import sys 18 | import traceback 19 | 20 | import logging 21 | import pickle 22 | import datetime, time 23 | 24 | class Command(BaseCommand): 25 | option_list = BaseCommand.option_list + ( 26 | make_option('--noreload', action='store_false', dest='use_reloader', default=True, help='Tells Django to NOT use the auto-reloader.'), 27 | make_option('-f', '--logfile', action='store', dest='logfile', default=None, help='Tells ztaskd where to log information. Leaving this blank logs to stderr'), 28 | make_option('-l', '--loglevel', action='store', dest='loglevel', default='info', help='Tells ztaskd what level of information to log'), 29 | make_option('--replayfailed', action='store_true', dest='replay_failed', default=False, help='Replays all failed calls in the db'), 30 | ) 31 | args = '' 32 | help = 'Start the ztaskd server' 33 | func_cache = {} 34 | io_loop = None 35 | 36 | def handle(self, *args, **options): 37 | self._setup_logger(options.get('logfile', None), options.get('loglevel', 'info')) 38 | use_reloader = options.get('use_reloader', True) 39 | replay_failed = options.get('replay_failed', False) 40 | if use_reloader: 41 | autoreload.main(lambda: self._handle(use_reloader, replay_failed)) 42 | else: 43 | self._handle(use_reloader, replay_failed) 44 | 45 | def _handle(self, use_reloader, replay_failed): 46 | self.logger.info("%sServer starting on %s." % ('Development ' if use_reloader else '', settings.ZTASKD_URL)) 47 | self._on_load() 48 | 49 | socket = context.socket(PULL) 50 | socket.bind(settings.ZTASKD_URL) 51 | 52 | def _queue_handler(socket, *args, **kwargs): 53 | try: 54 | function_name, args, kwargs, after = socket.recv_pyobj() 55 | if function_name == 'ztask_log': 56 | self.logger.warn('%s: %s' % (args[0], args[1])) 57 | return 58 | task = Task.objects.create( 59 | function_name=function_name, 60 | args=pickle.dumps(args), 61 | kwargs=pickle.dumps(kwargs), 62 | retry_count=settings.ZTASKD_RETRY_COUNT, 63 | next_attempt=time.time() + after 64 | ) 65 | 66 | if after: 67 | ioloop.DelayedCallback(lambda: self._call_function(task.pk, function_name=function_name, args=args, kwargs=kwargs), after * 1000, io_loop=self.io_loop).start() 68 | else: 69 | self._call_function(task.pk, function_name=function_name, args=args, kwargs=kwargs) 70 | except Exception, e: 71 | self.logger.error('Error setting up function. Details:\n%s' % e) 72 | traceback.print_exc(e) 73 | 74 | # Reload tasks if necessary 75 | if replay_failed: 76 | replay_tasks = Task.objects.all().order_by('created') 77 | else: 78 | replay_tasks = Task.objects.filter(retry_count__gt=0).order_by('created') 79 | for task in replay_tasks: 80 | if task.next_attempt < time.time(): 81 | ioloop.DelayedCallback(lambda: self._call_function(task.pk), 5000, io_loop=self.io_loop).start() 82 | else: 83 | after = task.next_attempt - time.time() 84 | ioloop.DelayedCallback(lambda: self._call_function(task.pk), after * 1000, io_loop=self.io_loop).start() 85 | 86 | self.io_loop = ioloop.IOLoop.instance() 87 | self.io_loop.add_handler(socket, _queue_handler, self.io_loop.READ) 88 | self.io_loop.start() 89 | 90 | def p(self, txt): 91 | print txt 92 | 93 | def _call_function(self, task_id, function_name=None, args=None, kwargs=None): 94 | try: 95 | if not function_name: 96 | try: 97 | task = Task.objects.get(pk=task_id) 98 | function_name = task.function_name 99 | args = pickle.loads(str(task.args)) 100 | kwargs = pickle.loads(str(task.kwargs)) 101 | except Exception, e: 102 | self.logger.info('Count not get task with id %s:\n%s' % (task_id, e)) 103 | return 104 | 105 | self.logger.info('Calling %s' % function_name) 106 | #self.logger.info('Task ID: %s' % task_id) 107 | try: 108 | function = self.func_cache[function_name] 109 | except KeyError: 110 | parts = function_name.split('.') 111 | module_name = '.'.join(parts[:-1]) 112 | member_name = parts[-1] 113 | if not module_name in sys.modules: 114 | __import__(module_name) 115 | function = getattr(sys.modules[module_name], member_name) 116 | self.func_cache[function_name] = function 117 | function(*args, **kwargs) 118 | self.logger.info('Called %s successfully' % function_name) 119 | Task.objects.get(pk=task_id).delete() 120 | except Exception, e: 121 | self.logger.error('Error calling %s. Details:\n%s' % (function_name, e)) 122 | try: 123 | task = Task.objects.get(pk=task_id) 124 | if task.retry_count > 0: 125 | task.retry_count = task.retry_count - 1 126 | task.next_attempt = time.time() + settings.ZTASKD_RETRY_AFTER 127 | ioloop.DelayedCallback(lambda: self._call_function(task.pk), settings.ZTASKD_RETRY_AFTER * 1000, io_loop=self.io_loop).start() 128 | task.failed = datetime.datetime.utcnow() 129 | task.last_exception = '%s' % e 130 | task.save() 131 | except Exception, e2: 132 | self.logger.error('Error capturing exception in _call_function. Details:\n%s' % e2) 133 | traceback.print_exc(e) 134 | 135 | def _setup_logger(self, logfile, loglevel): 136 | LEVELS = { 137 | 'debug': logging.DEBUG, 138 | 'info': logging.INFO, 139 | 'warning': logging.WARNING, 140 | 'error': logging.ERROR, 141 | 'critical': logging.CRITICAL 142 | } 143 | 144 | self.logger = logging.getLogger('ztaskd') 145 | self.logger.setLevel(LEVELS[loglevel.lower()]) 146 | if logfile: 147 | handler = logging.FileHandler(logfile, delay=True) 148 | else: 149 | handler = logging.StreamHandler() 150 | 151 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 152 | handler.setFormatter(formatter) 153 | self.logger.addHandler(handler) 154 | 155 | def _on_load(self): 156 | for callable_name in settings.ZTASKD_ON_LOAD: 157 | self.logger.info("ON_LOAD calling %s" % callable_name) 158 | parts = callable_name.split('.') 159 | module_name = '.'.join(parts[:-1]) 160 | member_name = parts[-1] 161 | if not module_name in sys.modules: 162 | __import__(module_name) 163 | callable_fn = getattr(sys.modules[module_name], member_name) 164 | callable_fn() 165 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /django_ztask/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding model 'Task' 12 | db.create_table('django_ztask_task', ( 13 | ('uuid', self.gf('django.db.models.fields.CharField')(max_length=36, primary_key=True)), 14 | ('function_name', self.gf('django.db.models.fields.CharField')(max_length=255)), 15 | ('args', self.gf('django.db.models.fields.TextField')()), 16 | ('kwargs', self.gf('django.db.models.fields.TextField')()), 17 | ('retry_count', self.gf('django.db.models.fields.IntegerField')(default=0)), 18 | ('last_exception', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), 19 | ('next_attempt', self.gf('django.db.models.fields.FloatField')(null=True, blank=True)), 20 | ('failed', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), 21 | )) 22 | db.send_create_signal('django_ztask', ['Task']) 23 | 24 | 25 | def backwards(self, orm): 26 | 27 | # Deleting model 'Task' 28 | db.delete_table('django_ztask_task') 29 | 30 | 31 | models = { 32 | 'django_ztask.task': { 33 | 'Meta': {'object_name': 'Task'}, 34 | 'args': ('django.db.models.fields.TextField', [], {}), 35 | 'failed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 36 | 'function_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 37 | 'kwargs': ('django.db.models.fields.TextField', [], {}), 38 | 'last_exception': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 39 | 'next_attempt': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), 40 | 'retry_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), 41 | 'uuid': ('django.db.models.fields.CharField', [], {'max_length': '36', 'primary_key': 'True'}) 42 | } 43 | } 44 | 45 | complete_apps = ['django_ztask'] 46 | -------------------------------------------------------------------------------- /django_ztask/migrations/0002_auto__add_field_task_created.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding field 'Task.created' 12 | db.add_column('django_ztask_task', 'created', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True), keep_default=False) 13 | 14 | 15 | def backwards(self, orm): 16 | 17 | # Deleting field 'Task.created' 18 | db.delete_column('django_ztask_task', 'created') 19 | 20 | 21 | models = { 22 | 'django_ztask.task': { 23 | 'Meta': {'object_name': 'Task'}, 24 | 'args': ('django.db.models.fields.TextField', [], {}), 25 | 'created': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 26 | 'failed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 27 | 'function_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 28 | 'kwargs': ('django.db.models.fields.TextField', [], {}), 29 | 'last_exception': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 30 | 'next_attempt': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), 31 | 'retry_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), 32 | 'uuid': ('django.db.models.fields.CharField', [], {'max_length': '36', 'primary_key': 'True'}) 33 | } 34 | } 35 | 36 | complete_apps = ['django_ztask'] 37 | -------------------------------------------------------------------------------- /django_ztask/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmgctrl/django-ztask/ea5210060be4c9280d7cf3e4a30ad9142f76c47d/django_ztask/migrations/__init__.py -------------------------------------------------------------------------------- /django_ztask/models.py: -------------------------------------------------------------------------------- 1 | from django.db.models import * 2 | 3 | import uuid 4 | import datetime 5 | 6 | class QuerySetManager(Manager): 7 | def __getattr__(self, attr, *args): 8 | try: 9 | return getattr(self.__class__, attr, *args) 10 | except AttributeError: 11 | return getattr(self.get_query_set(), attr, *args) 12 | 13 | def get_query_set(self): 14 | return self.model.QuerySet(self.model) 15 | 16 | 17 | # 18 | # 19 | class Task(Model): 20 | uuid = CharField(max_length=36, primary_key=True) 21 | function_name = CharField(max_length=255) 22 | args = TextField() 23 | kwargs = TextField() 24 | retry_count = IntegerField(default=0) 25 | last_exception = TextField(blank=True, null=True) 26 | next_attempt = FloatField(blank=True, null=True) 27 | created = DateTimeField(blank=True, null=True) 28 | failed = DateTimeField(blank=True, null=True) 29 | 30 | def save(self, *args, **kwargs): 31 | if not self.uuid: 32 | self.created = datetime.datetime.utcnow() 33 | self.uuid = uuid.uuid4() 34 | super(Task, self).save(*args, **kwargs) 35 | 36 | class Meta: 37 | db_table = 'django_ztask_task' 38 | 39 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | try: 3 | from setuptools import setup, find_packages 4 | except: 5 | from distutils.core import setup, find_packages 6 | import django_ztask as distmeta 7 | 8 | setup( 9 | version=distmeta.__version__, 10 | description=distmeta.__doc__, 11 | author=distmeta.__author__, 12 | author_email=distmeta.__contact__, 13 | url=distmeta.__homepage__, 14 | # 15 | name='django-ztask', 16 | packages=find_packages(), 17 | install_requires=[ 18 | 'pyzmq', 19 | ] 20 | ) 21 | --------------------------------------------------------------------------------