diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 922a7939..f8c5f66e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,13 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.7', '3.8', '3.9', 'pypy-3.9'] + python-version: ['3.7', '3.8', '3.9', '3.10', 'pypy-3.9'] + + services: + rabbitmq: + image: rabbitmq + ports: + - "5672:5672" steps: - uses: actions/checkout@v3 diff --git a/README.rst b/README.rst index dbe7517a..143f7930 100644 --- a/README.rst +++ b/README.rst @@ -46,7 +46,7 @@ Important Warning about Time Zones -.. note:: +.. note:: This will reset the state as if the periodic tasks have never run before. @@ -95,7 +95,7 @@ create the interval object: .. code-block:: Python >>> from django_celery_beat.models import PeriodicTask, IntervalSchedule - + # executes every 10 seconds. >>> schedule, created = IntervalSchedule.objects.get_or_create( ... every=10, @@ -181,7 +181,7 @@ of a ``30 * * * *`` (execute every 30 minutes) crontab entry you specify: ... day_of_week='*', ... day_of_month='*', ... month_of_year='*', - ... timezone=pytz.timezone('Canada/Pacific') + ... timezone=zoneinfo.ZoneInfo('Canada/Pacific') ... ) The crontab schedule is linked to a specific timezone using the 'timezone' input parameter. @@ -288,7 +288,7 @@ After installation, add ``django_celery_beat`` to Django's settings module: Run the ``django_celery_beat`` migrations using: .. code-block:: bash - + $ python manage.py migrate django_celery_beat diff --git a/django_celery_beat/migrations/0016_alter_crontabschedule_timezone.py b/django_celery_beat/migrations/0016_alter_crontabschedule_timezone.py new file mode 100644 index 00000000..ccff6ea3 --- /dev/null +++ b/django_celery_beat/migrations/0016_alter_crontabschedule_timezone.py @@ -0,0 +1,25 @@ +# Generated by Django 4.0.3 on 2022-03-21 20:20 +# flake8: noqa +from django.db import migrations +import django_celery_beat.models +import timezone_field.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_celery_beat', '0015_edit_solarschedule_events_choices'), + ] + + operations = [ + migrations.AlterField( + model_name='crontabschedule', + name='timezone', + field=timezone_field.fields.TimeZoneField( + default= + django_celery_beat.models.crontab_schedule_celery_timezone, + help_text= + 'Timezone to Run the Cron Schedule on. Default is UTC.', + use_pytz=False, verbose_name='Cron Timezone'), + ), + ] diff --git a/django_celery_beat/models.py b/django_celery_beat/models.py index 1f87bbc5..7cb8d0b4 100644 --- a/django_celery_beat/models.py +++ b/django_celery_beat/models.py @@ -1,4 +1,8 @@ """Database models.""" +try: + from zoneinfo import available_timezones +except ImportError: + from backports.zoneinfo import available_timezones from datetime import timedelta import timezone_field @@ -66,10 +70,9 @@ def crontab_schedule_celery_timezone(): settings, '%s_TIMEZONE' % current_app.namespace) except AttributeError: return 'UTC' - return CELERY_TIMEZONE if CELERY_TIMEZONE in [ - choice[0].zone for choice in timezone_field. - TimeZoneField.default_choices - ] else 'UTC' + if CELERY_TIMEZONE in available_timezones(): + return CELERY_TIMEZONE + return 'UTC' class SolarSchedule(models.Model): @@ -297,6 +300,7 @@ class CrontabSchedule(models.Model): timezone = timezone_field.TimeZoneField( default=crontab_schedule_celery_timezone, + use_pytz=False, verbose_name=_('Cron Timezone'), help_text=_( 'Timezone to Run the Cron Schedule on. Default is UTC.'), @@ -357,8 +361,8 @@ def from_schedule(cls, schedule): class PeriodicTasks(models.Model): """Helper table for tracking updates to periodic tasks. - This stores a single row with ``ident=1``. ``last_update`` is updated - via django signals whenever anything is changed in the :class:`~.PeriodicTask` model. + This stores a single row with ``ident=1``. ``last_update`` is updated via + signals whenever anything changes in the :class:`~.PeriodicTask` model. Basically this acts like a DB data audit trigger. Doing this so we also track deletions, and not just insert/update. """ diff --git a/django_celery_beat/schedulers.py b/django_celery_beat/schedulers.py index 1a509e0d..176b95ec 100644 --- a/django_celery_beat/schedulers.py +++ b/django_celery_beat/schedulers.py @@ -136,12 +136,8 @@ def is_due(self): return self.schedule.is_due(last_run_at_in_tz) def _default_now(self): - # The PyTZ datetime must be localised for the Django-Celery-Beat - # scheduler to work. Keep in mind that timezone arithmatic - # with a localized timezone may be inaccurate. if getattr(settings, 'DJANGO_CELERY_BEAT_TZ_AWARE', True): - now = self.app.now() - now = now.tzinfo.localize(now.replace(tzinfo=None)) + now = datetime.datetime.now(self.app.timezone) else: # this ends up getting passed to maybe_make_aware, which expects # all naive datetime objects to be in utc time. diff --git a/django_celery_beat/tzcrontab.py b/django_celery_beat/tzcrontab.py index 49d88920..236299a7 100644 --- a/django_celery_beat/tzcrontab.py +++ b/django_celery_beat/tzcrontab.py @@ -2,8 +2,7 @@ from celery import schedules from collections import namedtuple -from datetime import datetime -import pytz +from datetime import datetime, timezone schedstate = namedtuple('schedstate', ('is_due', 'next')) @@ -14,7 +13,7 @@ class TzAwareCrontab(schedules.crontab): def __init__( self, minute='*', hour='*', day_of_week='*', - day_of_month='*', month_of_year='*', tz=pytz.utc, app=None + day_of_month='*', month_of_year='*', tz=timezone.utc, app=None ): """Overwrite Crontab constructor to include a timezone argument.""" self.tz = tz @@ -28,9 +27,7 @@ def __init__( ) def nowfunc(self): - return self.tz.normalize( - pytz.utc.localize(datetime.utcnow()) - ) + return datetime.now(self.tz) def is_due(self, last_run_at): """Calculate when the next run will take place. diff --git a/docs/conf.py b/docs/conf.py index 140f36f8..f1e04801 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -24,12 +24,12 @@ ], extlinks={ 'github_project': ( - f'https://github.com/%s', - 'GitHub project', + 'https://github.com/%s', + 'GitHub project %s', ), 'github_pr': ( - f'https://github.com/celery/django-celery-beat/pull/%s', - 'GitHub PR #', + 'https://github.com/celery/django-celery-beat/pull/%s', + 'GitHub PR #%s', ), }, extra_intersphinx_mapping={ diff --git a/requirements/default.txt b/requirements/default.txt index c627d527..ef0b5b51 100644 --- a/requirements/default.txt +++ b/requirements/default.txt @@ -1,3 +1,5 @@ celery>=5.2.3,<6.0 -django-timezone-field>=4.2.3 +django-timezone-field>=5.0 +backports.zoneinfo; python_version<"3.9" +tzdata python-crontab>=2.3.4 diff --git a/requirements/test-django22.txt b/requirements/test-django22.txt deleted file mode 100644 index d3f4408e..00000000 --- a/requirements/test-django22.txt +++ /dev/null @@ -1 +0,0 @@ -django>=2.2.28,<3.0 diff --git a/requirements/test.txt b/requirements/test.txt index cc59b865..98e1967e 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,6 +1,5 @@ case>=1.3.1 -pytest-django<5.0 -pytz>dev -pytest<8.0 +pytest-django>=2.2,<5.0 +pytest>=6.2.5,<8.0 pytest-timeout ephem diff --git a/setup.py b/setup.py index bc16a11f..429fa5eb 100644 --- a/setup.py +++ b/setup.py @@ -38,11 +38,12 @@ def _pyimp(): Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Framework :: Django - Framework :: Django :: 4.0 Framework :: Django :: 3.2 + Framework :: Django :: 4.0 Operating System :: OS Independent Topic :: Communications Topic :: System :: Distributed Computing diff --git a/t/proj/urls.py b/t/proj/urls.py index 907c9eb5..dfc73621 100644 --- a/t/proj/urls.py +++ b/t/proj/urls.py @@ -2,5 +2,5 @@ from django.urls import path urlpatterns = [ - path('admin/', admin.site.urls), + path('admin/', admin.site.urls), ] diff --git a/t/unit/test_models.py b/t/unit/test_models.py index db92ad8d..fd592ade 100644 --- a/t/unit/test_models.py +++ b/t/unit/test_models.py @@ -1,4 +1,9 @@ +import datetime import os +try: + from zoneinfo import available_timezones, ZoneInfo +except ImportError: + from backports.zoneinfo import available_timezones, ZoneInfo from celery import schedules from django.test import TestCase, override_settings @@ -9,8 +14,6 @@ from django.db.migrations.questioner import NonInteractiveMigrationQuestioner from django.utils import timezone from django.conf import settings -import pytz, datetime -import timezone_field from django_celery_beat import migrations as beat_migrations from django_celery_beat.models import ( @@ -84,8 +87,7 @@ def _test_duplicate_schedules(self, cls, kwargs, schedule=None): class CrontabScheduleTestCase(TestCase, TestDuplicatesMixin): - FIRST_VALID_TIMEZONE = timezone_field.\ - TimeZoneField.default_choices[0][0].zone + FIRST_VALID_TIMEZONE = available_timezones().pop() def test_default_timezone_without_settings_config(self): assert crontab_schedule_celery_timezone() == "UTC" @@ -148,11 +150,12 @@ def test_duplicate_schedules(self): kwargs = {'clocked_time': timezone.now()} self._test_duplicate_schedules(ClockedSchedule, kwargs) - # IMPORTANT: we must have a valid time-zone (not UTC) to do an accurate testing - @override_settings(TIME_ZONE='Africa/Cairo') + # IMPORTANT: we must have a valid timezone (not UTC) for accurate testing + @override_settings(TIME_ZONE='Africa/Cairo') def test_timezone_format(self): - """Make sure the scheduled time is not shown in UTC when time zone is used""" - tz_info = pytz.timezone(settings.TIME_ZONE).localize(datetime.datetime.utcnow()) - schedule, created = ClockedSchedule.objects.get_or_create(clocked_time=tz_info) + """Ensure scheduled time is not shown in UTC when timezone is used""" + tz_info = datetime.datetime.now(ZoneInfo(settings.TIME_ZONE)) + schedule, created = ClockedSchedule.objects.get_or_create( + clocked_time=tz_info) # testnig str(schedule) calls make_aware() internally assert str(schedule.clocked_time) == str(schedule) diff --git a/tox.ini b/tox.ini index da37f1f5..f5393c46 100644 --- a/tox.ini +++ b/tox.ini @@ -3,18 +3,20 @@ python = 3.7: py37 3.8: py38, apicheck, linkcheck 3.9: py39, flake8, pydocstyle, cov + 3.10: py310 pypy-3.9: pypy3 [gh-actions:env] DJANGO = 3.2: django32 4.0: django40 - + [tox] envlist = py37-django{32} py38-django{32,40} py39-django{32,40} + py310-django{32,40} pypy3-django{32} flake8 apicheck @@ -43,12 +45,12 @@ commands = [testenv:apicheck] -basepython = python3.9 +basepython = python3.8 commands = sphinx-build -W -b apicheck -d {envtmpdir}/doctrees docs docs/_build/apicheck [testenv:linkcheck] -basepython = python3.9 +basepython = python3.8 commands = sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees docs docs/_build/linkcheck