Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 28 additions & 1 deletion schedule/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,24 @@ def tag(self, *tags: Hashable):
self.tags.update(tags)
return self

def with_time_zone(self, tz: str):
import pytz

if self.unit != "seconds":
raise ScheduleValueError(
"Timezone should be defined in at()"
)

if isinstance(tz, str):
self.at_time_zone = pytz.timezone(tz)
elif isinstance(tz, pytz.BaseTzInfo):
self.at_time_zone = tz
else:
raise ScheduleValueError(
"Timezone must be string or pytz.timezone object"
)
return self

def at(self, time_str: str, tz: Optional[str] = None):
"""
Specify a particular time that the job should be run at.
Expand Down Expand Up @@ -689,7 +707,7 @@ def run(self):

logger.debug("Running job %s", self)
ret = self.job_func()
self.last_run = datetime.datetime.now()
self.last_run = datetime.datetime.now(self.at_time_zone)
self._schedule_next_run()

if self._is_overdue(self.next_run):
Expand Down Expand Up @@ -789,6 +807,15 @@ def _correct_utc_offset(
moment = self.at_time_zone.normalize(moment)
offset_after_normalize = moment.utcoffset()

# Check fall back for DST
if self.last_run is not None:
last_execution_dst = self.last_run.dst()
moment_dst = moment.dst()
if last_execution_dst > moment_dst:
if self.unit in ["minutes", "hours"]:
moment -= last_execution_dst - moment_dst
return moment

if offset_before_normalize == offset_after_normalize:
# There was no change in the utc-offset, datetime didn't change.
return moment
Expand Down
55 changes: 55 additions & 0 deletions test_schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -1239,6 +1239,61 @@ def test_align_utc_offset_after_fold_fixate(self):
assert aligned_time.minute == 30
assert aligned_time.day == 27

def test_fall_back_for_daylight_saving_time(self):
mock_job = make_mock_job()
# 26 October 2025, 03:00:00 clocks were turned back 1 hour
with mock_datetime(2025, 10, 26, 2, 58, second=40, fold=0):
job_object = every().minute.at(":30", tz="Europe/Madrid").do(mock_job)
with mock_datetime(2025, 10, 26, 2, 59, second=40, fold=0):
schedule.run_pending()
assert job_object.next_run.hour == 2

def test_fall_back_for_daylight_saving_time_2(self):
mock_job = make_mock_job()
# 26 October 2025, 03:00:00 clocks were turned back 1 hour
with mock_datetime(2025, 10, 26, 2, 59, second=30, fold=1):
assert every().minute.at(":30").do(mock_job).next_run.hour == 3

def test_fall_back_for_daylight_saving_time_3(self):
mock_job = make_mock_job()
# 26 October 2025, 03:00:00 clocks were turned back 1 hour
with mock_datetime(2025, 10, 26, 1, 59, second=30, fold=0):
assert every().minute.at(":30").do(mock_job).next_run.hour == 2

def test_fall_back_for_daylight_saving_time_4(self):
mock_job = make_mock_job()
# 26 October 2025, 03:00:00 clocks were turned back 1 hour
with mock_datetime(2025, 10, 26, 1, 30, second=0, fold=0):
job_object = every().hour.at(":30", tz="Europe/Madrid").do(mock_job)
assert job_object.next_run.hour == 2
assert job_object.next_run.minute == 30
with mock_datetime(2025, 10, 26, 2, 59, second=40, fold=0):
schedule.run_pending()
assert job_object.next_run.hour == 2
assert job_object.next_run.minute == 30
with mock_datetime(2025, 10, 26, 2, 59, second=40, fold=1):
schedule.run_pending()
assert job_object.next_run.hour == 3
assert job_object.next_run.minute == 30

def test_fall_back_for_daylight_saving_time_5(self):
mock_job = make_mock_job()
# 26 October 2025, 03:00:00 clocks were turned back 1 hour
with mock_datetime(2025, 10, 26, 2, 59, second=52, fold=0):
job_object = every(5).seconds.with_time_zone(tz="Europe/Madrid").do(mock_job)
with mock_datetime(2025, 10, 26, 2, 59, second=59, fold=0):
schedule.run_pending()
assert job_object.next_run.hour == 2

def test_fall_back_for_daylight_saving_time_6(self):
mock_job = make_mock_job()
# 26 October 2025, 03:00:00 clocks were turned back 1 hour
with mock_datetime(2025, 10, 26, 2, 59, second=52, fold=1):
job_object = every(5).seconds.with_time_zone(tz="Europe/Madrid").do(mock_job)
with mock_datetime(2025, 10, 26, 2, 59, second=59, fold=1):
schedule.run_pending()
assert job_object.next_run.hour == 3

def test_daylight_saving_time(self):
mock_job = make_mock_job()
# 27 March 2022, 02:00:00 clocks were turned forward 1 hour
Expand Down