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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
strategy:
max-parallel: 6
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12']
python-version: ['3.10', '3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Python job scheduling for humans. Run Python functions (or any other callable) p
- In-process scheduler for periodic jobs. No extra processes needed!
- Very lightweight and no external dependencies.
- Excellent test coverage.
- Tested on Python and 3.7, 3.8, 3.9, 3.10, 3.11, 3.12
- Tested on Python and 3.10, 3.11, 3.12, 3.13

Usage
-----
Expand Down
6 changes: 0 additions & 6 deletions docs/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,6 @@ It might be that your IDE uses a different Python interpreter installation.

Still having problems? Use Google and StackOverflow before submitting an issue.

ModuleNotFoundError: ModuleNotFoundError: No module named 'pytz'
----------------------------------------------------------------

This error happens when you try to set a timezone in ``.at()`` without having the `pytz <https://pypi.org/project/pytz/>`_ package installed.
Pytz is a required dependency when working with timezones.
To resolve this issue, install the ``pytz`` module by running ``pip install pytz``.

Does schedule support time zones?
---------------------------------
Expand Down
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Python job scheduling for humans. Run Python functions (or any other callable) p
- In-process scheduler for periodic jobs. No extra processes needed!
- Very lightweight and no external dependencies.
- Excellent test coverage.
- Tested on Python 3.7, 3.8, 3.9, 3.10, 3.11 and 3.12
- Tested on Python 3.10, 3.11, 3.12 and 3.13


:doc:`Example <examples>`
Expand Down
6 changes: 2 additions & 4 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,15 @@ Python version support
######################

We recommend using the latest version of Python.
Schedule is tested on Python 3.7, 3.8, 3.9, 3.10, 3.11 and 3.12
Schedule is tested on Python 3.10, 3.11, 3.12 and 3.13

Want to use Schedule on earlier Python versions? See the History.


Dependencies
############

Schedule has 1 optional dependency:

Only when you use ``.at()`` with a timezone, you must have `pytz <https://pypi.org/project/pytz/>`_ installed.
Schedule has no external dependencies. Timezone support uses Python's built-in :mod:`zoneinfo` module.

Installation instructions
#########################
Expand Down
12 changes: 3 additions & 9 deletions docs/timezones.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,16 @@ Timezone in .at()

Schedule supports setting the job execution time in another timezone using the ``.at`` method.

**To work with timezones** `pytz <https://pypi.org/project/pytz/>`_ **must be installed!** Get it:

.. code-block:: bash

pip install pytz

Timezones are only available in the ``.at`` function, like so:

.. code-block:: python

# Pass a timezone as a string
schedule.every().day.at("12:42", "Europe/Amsterdam").do(job)

# Pass an pytz timezone object
from pytz import timezone
schedule.every().friday.at("12:42", timezone("Africa/Lagos")).do(job)
# Pass a ZoneInfo object
from zoneinfo import ZoneInfo
schedule.every().friday.at("12:42", ZoneInfo("Africa/Lagos")).do(job)

Schedule uses the timezone to calculate the next runtime in local time.
All datetimes inside the library are stored `naive <https://docs.python.org/3/library/datetime.html>`_.
Expand Down
4 changes: 1 addition & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,13 @@ dynamic = ["version", "classifiers", "keywords", "authors"]
readme = "README.rst"
license = {text = "MIT License"}

requires-python = ">= 3.7"
requires-python = ">= 3.10"
dependencies = []

maintainers = [
{name = "Sijmen Huizenga"}
]

[project.optional-dependencies]
timezone = ["pytz"]

[project.urls]
Documentation = "https://schedule.readthedocs.io"
Expand Down
4 changes: 1 addition & 3 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,4 @@ pytest-flake8
Sphinx
black==20.8b1
click==8.0.4
mypy
pytz
types-pytz
mypy
62 changes: 10 additions & 52 deletions schedule/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
- A simple to use API for scheduling jobs.
- Very lightweight and no external dependencies.
- Excellent test coverage.
- Tested on Python 3.7, 3.8, 3.9, 3.10, 3.11 and 3.12
- Tested on Python 3.10, 3.11, 3.12 and 3.13

Usage:
>>> import schedule
Expand Down Expand Up @@ -46,6 +46,7 @@
import re
import time
from typing import Set, List, Optional, Callable, Union
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError

logger = logging.getLogger("schedule")

Expand Down Expand Up @@ -486,7 +487,7 @@ def at(self, time_str: str, tz: Optional[str] = None):
`every().minute.at(':30')`).

:param tz: The timezone that this timestamp refers to. Can be
a string that can be parsed by pytz.timezone(), or a pytz.BaseTzInfo object
a string accepted by zoneinfo.ZoneInfo(), or a ZoneInfo object

:return: The invoked job instance
"""
Expand All @@ -496,16 +497,12 @@ def at(self, time_str: str, tz: Optional[str] = None):
)

if tz is not None:
import pytz

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

if not isinstance(time_str, str):
raise TypeError("at() should be passed a string")
Expand Down Expand Up @@ -733,9 +730,7 @@ def _schedule_next_run(self) -> None:
while next_run <= now:
next_run += period

next_run = self._correct_utc_offset(
next_run, fixate_time=(self.at_time is not None)
)
next_run = self._correct_utc_offset(next_run)

# To keep the api consistent with older versions, we have to set the 'next_run' to a naive timestamp in the local timezone.
# Because we want to stay backwards compatible with older versions.
Expand Down Expand Up @@ -766,55 +761,18 @@ def _move_to_at_time(self, moment: datetime.datetime) -> datetime.datetime:

# When we set the time elements, we might end up in a different UTC-offset than the current offset.
# This happens when we cross into or out of daylight saving time.
moment = self._correct_utc_offset(moment, fixate_time=True)
moment = self._correct_utc_offset(moment)

return moment

def _correct_utc_offset(
self, moment: datetime.datetime, fixate_time: bool
) -> datetime.datetime:
def _correct_utc_offset(self, moment: datetime.datetime) -> datetime.datetime:
"""
Given a datetime, corrects any mistakes in the utc offset.
This is similar to pytz' normalize, but adds the ability to attempt
keeping the time-component at the same hour/minute/second.
"""
if self.at_time_zone is None:
return moment
# Normalize corrects the utc-offset to match the timezone
# For example: When a date&time&offset does not exist within a timezone,
# the normalization will change the utc-offset to where it is valid.
# It does this while keeping the moment in time the same, by moving the
# time component opposite of the utc-change.
offset_before_normalize = moment.utcoffset()
moment = self.at_time_zone.normalize(moment)
offset_after_normalize = moment.utcoffset()

if offset_before_normalize == offset_after_normalize:
# There was no change in the utc-offset, datetime didn't change.
return moment

# The utc-offset and time-component has changed

if not fixate_time:
# No need to fixate the time.
return moment

offset_diff = offset_after_normalize - offset_before_normalize

# Adjust the time to reset the date-time to have the same HH:mm components
moment -= offset_diff

# Check if moving the timestamp back by the utc-offset-difference made it end up
# in a moment that does not exist within the current timezone/utc-offset
re_normalized_offset = self.at_time_zone.normalize(moment).utcoffset()
if re_normalized_offset != offset_after_normalize:
# We ended up in a DST Gap. The requested 'at' time does not exist
# within the current timezone/utc-offset. As a best effort, we will
# schedule the job 1 offset later than possible.
# For example, if 02:23 does not exist (because DST moves from 02:00
# to 03:00), this will schedule the job at 03:23.
moment += offset_diff
return moment
return moment.astimezone(datetime.timezone.utc).astimezone(self.at_time_zone)

def _is_overdue(self, when: datetime.datetime):
return self.cancel_after is not None and when > self.cancel_after
Expand Down
5 changes: 1 addition & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,10 @@ def read_file(filename):
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Natural Language :: English",
],
python_requires=">=3.7",
python_requires=">=3.10",
)
Loading