Skip to content

Commit

Permalink
Merge 1c29a7b into 57bd912
Browse files Browse the repository at this point in the history
  • Loading branch information
evenicoulddoit committed Nov 30, 2016
2 parents 57bd912 + 1c29a7b commit 0e1cf7e
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 57 deletions.
54 changes: 41 additions & 13 deletions django_cron/__init__.py
Expand Up @@ -2,6 +2,7 @@
from datetime import timedelta
import traceback
import time
import sys

from django.conf import settings
from django.utils.timezone import now as utc_now, localtime, is_naive
Expand All @@ -12,13 +13,21 @@
logger = logging.getLogger('django_cron')


class BadCronJobError(AssertionError):
pass


def get_class(kls):
"""
TODO: move to django-common app.
Converts a string to a class.
Courtesy: http://stackoverflow.com/questions/452969/does-python-have-an-equivalent-to-java-class-forname/452981#452981
"""
parts = kls.split('.')

if len(parts) == 1:
raise ImportError("'{0}'' is not a valid import path".format(kls))

module = ".".join(parts[:-1])
m = __import__(module)
for comp in parts[1:]:
Expand Down Expand Up @@ -77,12 +86,11 @@ class CronJobManager(object):
Used as a context manager via 'with' statement to ensure
proper logger in cases of job failure.
"""

def __init__(self, cron_job_class, silent=False, *args, **kwargs):
super(CronJobManager, self).__init__(*args, **kwargs)

def __init__(self, cron_job_class, silent=False, dry_run=False, stdout=None):
self.cron_job_class = cron_job_class
self.silent = silent
self.dry_run = dry_run
self.stdout = stdout or sys.stdout
self.lock_class = self.get_lock_class()
self.previously_ran_successful_cron = None

Expand All @@ -92,7 +100,6 @@ def should_run_now(self, force=False):
"""
Returns a boolean determining whether this cron should run now or not!
"""

self.user_time = None
self.previously_ran_successful_cron = None

Expand Down Expand Up @@ -185,11 +192,20 @@ def __enter__(self):
return self

def __exit__(self, ex_type, ex_value, ex_traceback):
if ex_type == self.lock_class.LockFailedException:
if ex_type is None:
return True

non_logging_exceptions = [
BadCronJobError, self.lock_class.LockFailedException
]

if ex_type in non_logging_exceptions:
if not self.silent:
self.stdout.write("{0}\n".format(ex_value))
logger.info(ex_value)

elif ex_type is not None:
else:
if not self.silent:
self.stdout.write(u"[\N{HEAVY BALLOT X}] {0}\n".format(self.cron_job_class.code))
try:
trace = "".join(traceback.format_exception(ex_type, ex_value, ex_traceback))
self.make_log(self.msg, trace, success=False)
Expand All @@ -204,17 +220,29 @@ def run(self, force=False):
apply the logic of the schedule and call do() on the CronJobBase class
"""
cron_job_class = self.cron_job_class

if not issubclass(cron_job_class, CronJobBase):
raise Exception('The cron_job to be run must be a subclass of %s' % CronJobBase.__name__)
raise BadCronJobError('The cron_job to be run must be a subclass of %s' % CronJobBase.__name__)

if not hasattr(cron_job_class, 'code'):
raise BadCronJobError(
"Cron class '{0}' does not have a code attribute"
.format(cron_job_class.__name__)
)

with self.lock_class(cron_job_class, self.silent):
self.cron_job = cron_job_class()

if self.should_run_now(force):
logger.debug("Running cron: %s code %s", cron_job_class.__name__, self.cron_job.code)
self.msg = self.cron_job.do()
self.make_log(self.msg, success=True)
self.cron_job.set_prev_success_cron(self.previously_ran_successful_cron)
if not self.dry_run:
logger.debug("Running cron: %s code %s", cron_job_class.__name__, self.cron_job.code)
self.msg = self.cron_job.do()
self.make_log(self.msg, success=True)
self.cron_job.set_prev_success_cron(self.previously_ran_successful_cron)
if not self.silent:
self.stdout.write(u"[\N{HEAVY CHECK MARK}] {0}\n".format(self.cron_job.code))
elif not self.silent:
self.stdout.write(u"[ ] {0}\n".format(self.cron_job.code))

def get_lock_class(self):
name = getattr(settings, 'DJANGO_CRON_LOCK_BACKEND', DEFAULT_LOCK_BACKEND)
Expand Down
27 changes: 21 additions & 6 deletions django_cron/management/commands/runcrons.py
@@ -1,3 +1,4 @@
from __future__ import print_function
import traceback
from datetime import timedelta

Expand Down Expand Up @@ -30,12 +31,21 @@ def add_arguments(self, parser):
action='store_true',
help='Do not push any message on console'
)
parser.add_argument(
'--dry-run',
action='store_true',
help="Just show what crons would be run; don't actually run them"
)

def handle(self, *args, **options):
"""
Iterates over all the CRON_CLASSES (or if passed in as a commandline argument)
and runs them.
"""
if not options['silent']:
self.stdout.write("Running Crons\n")
self.stdout.write("{0}\n".format("=" * 40))

cron_classes = options['cron_classes']
if cron_classes:
cron_class_names = cron_classes
Expand All @@ -44,32 +54,37 @@ def handle(self, *args, **options):

try:
crons_to_run = [get_class(x) for x in cron_class_names]
except Exception:
except ImportError:
error = traceback.format_exc()
self.stdout.write('Make sure these are valid cron class names: %s\n%s' % (cron_class_names, error))
self.stdout.write('ERROR: Make sure these are valid cron class names: %s\n\n%s' % (cron_class_names, error))
return

for cron_class in crons_to_run:
run_cron_with_cache_check(
cron_class,
force=options['force'],
silent=options['silent']
silent=options['silent'],
dry_run=options['dry_run'],
stdout=self.stdout
)

clear_old_log_entries()
close_connection()


def run_cron_with_cache_check(cron_class, force=False, silent=False):
def run_cron_with_cache_check(
cron_class, force=False, silent=False, dry_run=False, stdout=None
):
"""
Checks the cache and runs the cron or not.
@cron_class - cron class to run.
@force - run job even if not scheduled
@silent - suppress notifications
@dryrun - don't actually perform the cron job
@stdout - where to write feedback to
"""

with CronJobManager(cron_class, silent) as manager:
with CronJobManager(cron_class, silent=silent, dry_run=dry_run, stdout=stdout) as manager:
manager.run(force)


Expand Down

0 comments on commit 0e1cf7e

Please sign in to comment.