Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a398883
schedule: make datetime objs timezone-aware and UTC default
sarnold Nov 9, 2018
76483ce
Update utc class and add tests for the new code
sarnold Nov 13, 2018
f602fc0
schedule/__init__.py: make datetime objs timezone-aware and UTC default
sarnold Nov 16, 2018
935cf31
schedule/__init__.py: add class-level self.logging, update log calls
sarnold Nov 14, 2018
f48d1b8
schedule: add tag param for run_all and extra logging
sarnold Nov 14, 2018
4fceba0
schedule/__init__.py: rebase of add-properties on 0.5.0 plus patches
sarnold Nov 15, 2018
a66ad04
schedule/parent_logger.py: add class logging snippet
sarnold Nov 16, 2018
95d72c0
Post-rebase clenaups for flake8/pep8 and allow lines up to 95 chars
sarnold Nov 16, 2018
36db905
schedule/__init__.py: fix crufty datetime argument
sarnold Nov 16, 2018
14b51a3
__init__.py: simplify new properties, tested with get_job_info(tag)
sarnold Nov 21, 2018
09fd83d
test_schedule.py: use the custom utc argument for datetime() test
sarnold Nov 21, 2018
eb1deec
.travis.yml: disable coveralls post-build step
sarnold Nov 21, 2018
bed3f5a
Merge pull request #1 from sarnold/testing-051_pre
sarnold Nov 21, 2018
764b49f
Merge branch 'master' into utc-time
sarnold Nov 6, 2019
d0a2a9b
schedule/__init__.py: make datetime objs timezone-aware and UTC default
sarnold Nov 16, 2018
40391c6
Post-rebase clenaups for flake8/pep8 and allow lines up to 95 chars
sarnold Nov 16, 2018
6857a87
Merge pull request #1 from sarnold/testing-051_pre
sarnold Nov 21, 2018
b2714ea
test_schedule.py: fix UTC string expected result for python 2 vs 3
sarnold Nov 14, 2019
fa01b4b
test_schedule.py: (really) fix test for UTC time strings
sarnold Nov 14, 2019
88f5932
hacky test updates, make logfile path an argument for setup_logging()
sarnold Nov 22, 2018
40fcc3f
test_schedule.py: more test coverage, small wrinkle in utc tests
sarnold Nov 23, 2018
8e26274
test_schedule.py: add last new test for run_all(tag)
sarnold Nov 23, 2018
5f73a9b
.travis.yml: set TZ env variable (fix for test_time() failure)
sarnold Nov 23, 2018
4fc2813
.travis.yml: okay, let's see what travis says the date is...
sarnold Nov 23, 2018
5de0f5f
test_schedule.py: fix mock date for assert comparison
sarnold Nov 23, 2018
af02ea9
test_schedule.py: get rid of timezone change in logging test_time()
sarnold Nov 23, 2018
5da832d
test_schedule.py: try something really simple for travis
sarnold Nov 23, 2018
6b5cd8d
test_schedule.py: one more silly travis test of time_test()
sarnold Nov 23, 2018
471a169
test_schedule.py: try an even simpler test in travis...
sarnold Nov 23, 2018
85b6cef
test_schedule.py: yet another silly travis test
sarnold Nov 23, 2018
1759225
test_schedule.py,travis.yml: revert, try another silly timezone setting
sarnold Nov 23, 2018
ecf4766
test_schedule.py: remove problematic tests we don't need anyway
sarnold Nov 23, 2018
d84b3bb
schedule/__init__.py: minor fixes and test updates
sarnold Nov 24, 2018
be40e3f
test_schedule.py: remove spurious comment
sarnold Nov 25, 2018
50c15c4
test_schedule.py: pluck fix for UTC string comparison from utc branch
sarnold Nov 14, 2019
651e0ed
test_schedule.py: fix silly left-over merge cruft
sarnold Nov 14, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
dist: xenial
language: python

python:
- "2.7"
- "3.5"
- "3.6"
- "3.7"
- "3.8-dev"
- "nightly"

install: pip install tox-travis coveralls

before_install:
- export TZ=America/Los_Angeles
- date

script:
- tox
after_success:
Expand Down
99 changes: 83 additions & 16 deletions schedule/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@
import re
import time

logger = logging.getLogger('schedule')
try:
from datetime import timezone
utc = timezone.utc
except ImportError:
from schedule.timezone import UTC
utc = UTC()


class ScheduleError(Exception):
Expand Down Expand Up @@ -81,6 +86,7 @@ class Scheduler(object):
"""
def __init__(self):
self.jobs = []
self.logger = logging.getLogger('schedule.Scheduler')

def run_pending(self):
"""
Expand All @@ -96,19 +102,30 @@ def run_pending(self):
for job in sorted(runnable_jobs):
self._run_job(job)

def run_all(self, delay_seconds=0):
def run_all(self, delay_seconds=0, tag=None):
"""
Run all jobs regardless if they are scheduled to run or not.
Run all jobs regardless if they are scheduled to run or not,
optionally matching one or more tags.

A delay of `delay` seconds is added between each job. This helps
distribute system load generated by the jobs more evenly
over time.

:param delay_seconds: A delay added between every executed job

:param tag: An identifier used to identify a subset of
jobs to run
"""
logger.info('Running *all* %i jobs with %is delay inbetween',
len(self.jobs), delay_seconds)
for job in self.jobs[:]:

if tag is None:
runnable_jobs = self.jobs[:]
else:
runnable_jobs = (job for job in self.jobs if tag in job.tags)

self.logger.info('Running *all* %i jobs with %is delay inbetween',
len(self.jobs), delay_seconds)

for job in sorted(runnable_jobs):
self._run_job(job)
time.sleep(delay_seconds)

Expand All @@ -121,8 +138,10 @@ def clear(self, tag=None):
jobs to delete
"""
if tag is None:
self.logger.info('Deleting *all* jobs')
del self.jobs[:]
else:
self.logger.info('Deleting all jobs tagged "%s"', tag)
self.jobs[:] = (job for job in self.jobs if tag not in job.tags)

def cancel_job(self, job):
Expand Down Expand Up @@ -168,7 +187,28 @@ def idle_seconds(self):
:return: Number of seconds until
:meth:`next_run <Scheduler.next_run>`.
"""
return (self.next_run - datetime.datetime.now()).total_seconds()
return (self.next_run - datetime.datetime.now(utc)).total_seconds()

@property
def last_run(self):
"""
Datetime when the last job ran (check for NoneType before using).

:return: A :class:`~datetime.datetime` object
"""
if not self.jobs:
return None
return max(self.jobs).last_run

@property
def idle_seconds_since(self):
"""
:return: Number of seconds since (check for NoneType before using).
:meth:`next_run <Scheduler.next_run>`.
"""
if self.last_run is None:
return None
return (datetime.datetime.now(utc) - self.last_run).total_seconds()


class Job(object):
Expand All @@ -192,6 +232,8 @@ def __init__(self, interval, scheduler=None):
self.interval = interval # pause interval * unit between runs
self.latest = None # upper limit to the interval
self.job_func = None # the job job_func to run
self.job_name = None # the name of job_func to run
self.job_info = None # the job timestats (see below)
self.unit = None # time units, e.g. 'minutes', 'hours', ...
self.at_time = None # optional time at which this job runs
self.last_run = None # datetime of the last run
Expand All @@ -200,6 +242,7 @@ def __init__(self, interval, scheduler=None):
self.start_day = None # Specific day of the week to start on
self.tags = set() # unique set of tags for the job
self.scheduler = scheduler # scheduler to register with
self.logger = logging.getLogger('schedule.Job')

def __lt__(self, other):
"""
Expand All @@ -223,7 +266,7 @@ def __str__(self):

def __repr__(self):
def format_time(t):
return t.strftime('%Y-%m-%d %H:%M:%S') if t else '[never]'
return t.strftime('%Y-%m-%d %H:%M:%S %Z') if t else '[never]'

def is_repr(j):
return not isinstance(j, Job)
Expand All @@ -240,6 +283,9 @@ def is_repr(j):
for k, v in self.job_func.keywords.items()]
call_repr = job_func_name + '(' + ', '.join(args + kwargs) + ')'

self.job_name = call_repr
self.job_info = timestats

if self.at_time is not None:
return 'Every %s %s at %s do %s %s' % (
self.interval,
Expand Down Expand Up @@ -473,17 +519,24 @@ def should_run(self):
"""
:return: ``True`` if the job should be run now.
"""
return datetime.datetime.now() >= self.next_run
return datetime.datetime.now(utc) >= self.next_run

@property
def info(self):
"""
:return: ``string`` with `job_func` name and timestats
"""
return self.job_name + self.job_info

def run(self):
"""
Run the job and immediately reschedule it.

:return: The return value returned by the `job_func`
"""
logger.info('Running job %s', self)
self.logger.info('Running job %s', self)
ret = self.job_func()
self.last_run = datetime.datetime.now()
self.last_run = datetime.datetime.now(utc)
self._schedule_next_run()
return ret

Expand All @@ -502,7 +555,7 @@ def _schedule_next_run(self):
interval = self.interval

self.period = datetime.timedelta(**{self.unit: interval})
self.next_run = datetime.datetime.now() + self.period
self.next_run = datetime.datetime.now(utc) + self.period
if self.start_day is not None:
if self.unit != 'weeks':
raise ScheduleValueError('`unit` should be \'weeks\'')
Expand Down Expand Up @@ -539,7 +592,7 @@ def _schedule_next_run(self):
# If we are running for the first time, make sure we run
# at the specified time *today* (or *this hour*) as well
if not self.last_run:
now = datetime.datetime.now()
now = datetime.datetime.now(utc)
if (self.unit == 'days' and self.at_time > now.time() and
self.interval == 1):
self.next_run = self.next_run - datetime.timedelta(days=1)
Expand All @@ -554,7 +607,7 @@ def _schedule_next_run(self):
datetime.timedelta(minutes=1)
if self.start_day is not None and self.at_time is not None:
# Let's see if we will still make that time we specified today
if (self.next_run - datetime.datetime.now()).days >= 7:
if (self.next_run - datetime.datetime.now(utc)).days >= 7:
self.next_run -= self.period


Expand Down Expand Up @@ -582,11 +635,11 @@ def run_pending():
default_scheduler.run_pending()


def run_all(delay_seconds=0):
def run_all(delay_seconds=0, tag=None):
"""Calls :meth:`run_all <Scheduler.run_all>` on the
:data:`default scheduler instance <default_scheduler>`.
"""
default_scheduler.run_all(delay_seconds=delay_seconds)
default_scheduler.run_all(delay_seconds, tag)


def clear(tag=None):
Expand Down Expand Up @@ -615,3 +668,17 @@ def idle_seconds():
:data:`default scheduler instance <default_scheduler>`.
"""
return default_scheduler.idle_seconds


def last_run():
"""Calls :meth:`last_run <Scheduler.last_run>` on the
:data:`default scheduler instance <default_scheduler>`.
"""
return default_scheduler.last_run


def idle_seconds_since():
"""Calls :meth:`idle_seconds_since <Scheduler.idle_seconds_since>` on the
:data:`default scheduler instance <default_scheduler>`.
"""
return default_scheduler.idle_seconds_since
32 changes: 32 additions & 0 deletions schedule/parent_logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env python
# coding: utf-8

import time
import logging


def setup_logging(debug, filename):
"""
Can be imported by ``<my_package>`` to create a log file for current
scheduler and job class logging. In this example we use a ``debug``
flag set in ``<my_package>`` to change the Log Level and ``filename``
to set log path. We also use UTC time and force the name in ``datefmt``.
"""
if debug:
log_level = logging.getLevelName('DEBUG')
else:
log_level = logging.getLevelName('INFO')

logging.basicConfig(level=log_level,
format="%(asctime)s %(name)s[%(process)d] %(levelname)s - %(message)s",
datefmt='%Y-%m-%d %H:%M:%S UTC',
filename=filename)

# BUG: This does not print the TZ name because logging module uses
# time instead of tz-aware datetime objects (so we force the
# correct name in datefmt above).
logging.Formatter.converter = time.gmtime

# To also log parent info, try something like this
# global logger
# logger = logging.getLogger("my_package")
19 changes: 19 additions & 0 deletions schedule/timezone.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import datetime


class UTC(datetime.tzinfo):
"""tzinfo derived concrete class named "UTC" with offset of 0"""
# can be changed to another timezone name/offset
def __init__(self):
self.__offset = datetime.timedelta(seconds=0)
self.__dst = datetime.timedelta(0)
self.__name = "UTC"

def utcoffset(self, dt):
return self.__offset

def dst(self, dt):
return self.__dst

def tzname(self, dt):
return self.__name
Loading