From bd586fe7b27bfeeb272efaf9b39fa94c245c948f Mon Sep 17 00:00:00 2001 From: Joren Date: Fri, 14 Jul 2023 21:05:02 +0200 Subject: [PATCH 1/8] fixed undocumented `django-cron` requirement --- django_cron/cron.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/django_cron/cron.py b/django_cron/cron.py index 8d65f49..60d24b9 100644 --- a/django_cron/cron.py +++ b/django_cron/cron.py @@ -1,6 +1,5 @@ from django.conf import settings - -from django_common.helper import send_mail +from django.core.mail import send_mail from django_cron import CronJobBase, Schedule, get_class from django_cron.models import CronJobLog @@ -44,13 +43,12 @@ def do(self): if failures >= min_failures: send_mail( - '%s%s failed %s times in a row!' - % ( + '%s%s failed %s times in a row!' % ( failed_runs_cronjob_email_prefix, cron.code, min_failures, ), message, - settings.DEFAULT_FROM_EMAIL, + None, emails, ) From d30600fedb1ea986e8d7ce97d20a139248f0e024 Mon Sep 17 00:00:00 2001 From: jorenham Date: Sun, 28 Jul 2024 18:24:30 +0200 Subject: [PATCH 2/8] modernized project structure and test fixes --- .coveragerc | 2 - .github/workflows/django.yml | 42 ++--- .travis.yml | 46 ----- AUTHORS | 1 - MANIFEST.in | 4 - demo/requirements.txt | 2 - django_cron/core.py | 12 +- django_cron/test/__init__.py | 0 test_crons.py => django_cron/test/cron.py | 0 .../test/settings.py | 97 +++++++---- test_urls.py => django_cron/test/urls.py | 4 +- django_cron/tests.py | 145 ++++++++-------- flake8 | 4 - helpers.py | 32 ---- poetry.lock | 164 ++++++++++++++++++ pyproject.toml | 33 ++++ runtests.py | 27 --- settings_mysql.py | 11 -- settings_postgres.py | 11 -- settings_sqllite.py | 10 -- setup.py | 42 ----- test_requirements.txt | 4 - testmanage.py | 18 ++ 23 files changed, 376 insertions(+), 335 deletions(-) delete mode 100644 .coveragerc delete mode 100644 .travis.yml delete mode 100644 AUTHORS delete mode 100644 MANIFEST.in delete mode 100644 demo/requirements.txt create mode 100644 django_cron/test/__init__.py rename test_crons.py => django_cron/test/cron.py (100%) rename settings_base.py => django_cron/test/settings.py (57%) rename test_urls.py => django_cron/test/urls.py (53%) delete mode 100644 flake8 delete mode 100644 helpers.py create mode 100644 poetry.lock create mode 100644 pyproject.toml delete mode 100644 runtests.py delete mode 100644 settings_mysql.py delete mode 100644 settings_postgres.py delete mode 100644 settings_sqllite.py delete mode 100644 setup.py delete mode 100644 test_requirements.txt create mode 100755 testmanage.py diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 6bfd21b..0000000 --- a/.coveragerc +++ /dev/null @@ -1,2 +0,0 @@ -[report] -omit = */migrations/* diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 747f8cc..eaeab95 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -1,33 +1,35 @@ -name: Django CI +name: CI on: push: - branches: [ master ] + branches: + - master pull_request: - branches: [ master ] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true jobs: build: - runs-on: ubuntu-latest strategy: max-parallel: 4 matrix: - python-version: [3.7, 3.8, 3.9] + python: [3.8, 3.12] steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Install Dependencies - run: | - python -m pip install --upgrade pip - pip install . - - name: Install Test Dependencies - run: | - pip install -r test_requirements.txt - - name: Run Tests - run: | - python runtests.py runtests + - uses: actions/checkout@v4 + - run: pipx install poetry + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: poetry check + run: poetry check + - name: poetry install + run: poetry install + - name: django-admin check + run: python run python testmanage.py check + - name: django-admin test + run: python run python testmanage.py test diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 64f4ee6..0000000 --- a/.travis.yml +++ /dev/null @@ -1,46 +0,0 @@ -dist: bionic -os: linux -language: python -jobs: - include: - - python: "2.7" - env: DJANGO=1.11.* DJANGO_SETTINGS_MODULE='settings_sqllite' - - python: "2.7" - env: DJANGO=1.11.* DJANGO_SETTINGS_MODULE='settings_postgres' - services: - - postgresql - before_script: - - psql -c 'create database travis_test;' -U postgres - - python: "2.7" - env: DJANGO=1.11.* DJANGO_SETTINGS_MODULE='settings_mysql' - services: - - mysql - before_script: - - mysql -e 'create database travis_test;' - - python: "3.7" - env: DJANGO=1.11.* DJANGO_SETTINGS_MODULE='settings_sqllite' - - python: "3.7" - env: DJANGO=2.0.* DJANGO_SETTINGS_MODULE='settings_sqllite' - - python: "3.7" - env: DJANGO=3.0.* DJANGO_SETTINGS_MODULE='settings_sqllite' - - python: "3.7" - env: DJANGO=3.0.* DJANGO_SETTINGS_MODULE='settings_postgres' - services: - - postgresql - before_script: - - psql -c 'create database travis_test;' -U postgres - - python: "3.7" - env: DJANGO=3.0.* DJANGO_SETTINGS_MODULE='settings_mysql' - services: - - mysql - before_script: - - mysql -e 'create database travis_test;' -install: - - pip install -q Django==$DJANGO - - pip install coveralls - - pip install -r test_requirements.txt -script: - - flake8 . --config=flake8 - - coverage run --source=django_cron setup.py test -after_success: - - coveralls \ No newline at end of file diff --git a/AUTHORS b/AUTHORS deleted file mode 100644 index 63c1c49..0000000 --- a/AUTHORS +++ /dev/null @@ -1 +0,0 @@ -http://github.com/Tivix/django-cron/contributors diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 351752d..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -include AUTHORS -include LICENSE -include MANIFEST.in -include README.rst \ No newline at end of file diff --git a/demo/requirements.txt b/demo/requirements.txt deleted file mode 100644 index af315c8..0000000 --- a/demo/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -Django==4.0.4 --e ../ diff --git a/django_cron/core.py b/django_cron/core.py index 9d5a7e6..d7a945b 100644 --- a/django_cron/core.py +++ b/django_cron/core.py @@ -1,12 +1,12 @@ import logging -from datetime import datetime, timedelta -import traceback -import time import sys +import time +import traceback +from datetime import datetime, timedelta from django.conf import settings -from django.utils.timezone import now as utc_now from django.db.models import Q +from django.utils.timezone import now as utc_now from django_cron.helpers import get_class, get_current_time @@ -109,11 +109,11 @@ def should_run_now(self, force=False): return True if cron_job.schedule.run_monthly_on_days is not None: - if not datetime.today().day in cron_job.schedule.run_monthly_on_days: + if datetime.today().day not in cron_job.schedule.run_monthly_on_days: return False if cron_job.schedule.run_weekly_on_days is not None: - if not datetime.today().weekday() in cron_job.schedule.run_weekly_on_days: + if datetime.today().weekday() not in cron_job.schedule.run_weekly_on_days: return False if cron_job.schedule.retry_after_failure_mins: diff --git a/django_cron/test/__init__.py b/django_cron/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test_crons.py b/django_cron/test/cron.py similarity index 100% rename from test_crons.py rename to django_cron/test/cron.py diff --git a/settings_base.py b/django_cron/test/settings.py similarity index 57% rename from settings_base.py rename to django_cron/test/settings.py index ecefc99..4d887f5 100644 --- a/settings_base.py +++ b/django_cron/test/settings.py @@ -1,3 +1,11 @@ +from pathlib import Path + +PROJECT_DIR = Path(__file__).resolve().parent.parent +BASE_DIR = PROJECT_DIR.parent + +SECRET_KEY = 'not-a-secure-key' +DEBUG = True + INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', @@ -5,30 +13,58 @@ 'django.contrib.contenttypes', 'django.contrib.messages', 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.sitemaps', 'django.contrib.staticfiles', 'django_cron', ] -SECRET_KEY = "wknfgl34qtnjo&Yk3jqfjtn2k3jtnk4wtnk" - - -CRON_CLASSES = [ - 'test_crons.TestSuccessCronJob', - 'test_crons.TestErrorCronJob', - 'test_crons.Test5minsCronJob', - 'test_crons.TestRunAtTimesCronJob', - 'test_crons.Wait3secCronJob', - 'django_cron.cron.FailedRunsNotificationCronJob', -] - MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'django_cron.test.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, ] +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + }, +} + +# since django is meant for developers with deadlines... +PASSWORD_HASHERS = ('django.contrib.auth.hashers.MD5PasswordHasher',) + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'USER': 'djangocron', + 'NAME': 'djangocron', + 'TEST_NAME': 'djangocron_test', + } +} + +DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' + LOGGING = { 'version': 1, 'disable_existing_loggers': False, @@ -47,29 +83,14 @@ }, } -CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', - 'LOCATION': 'django_cache', - } -} +STATIC_URL = '/static/' -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, +_CRON_PATH = 'django_cron.test.cron.' +CRON_CLASSES = [ + f'{_CRON_PATH}TestSuccessCronJob', + f'{_CRON_PATH}TestErrorCronJob', + f'{_CRON_PATH}Test5minsCronJob', + f'{_CRON_PATH}TestRunAtTimesCronJob', + f'{_CRON_PATH}Wait3secCronJob', + 'django_cron.cron.FailedRunsNotificationCronJob', ] - -ROOT_URLCONF = 'test_urls' -SITE_ID = 1 -STATIC_URL = '/static/' diff --git a/test_urls.py b/django_cron/test/urls.py similarity index 53% rename from test_urls.py rename to django_cron/test/urls.py index def9be0..82a6152 100644 --- a/test_urls.py +++ b/django_cron/test/urls.py @@ -1,9 +1,9 @@ # urls.py from django.contrib import admin -from django.urls import re_path +from django.urls import path admin.autodiscover() urlpatterns = [ - re_path(r'^admin/', admin.site.urls), + path('admin/', admin.site.urls), ] diff --git a/django_cron/tests.py b/django_cron/tests.py index 9691b4f..7728515 100644 --- a/django_cron/tests.py +++ b/django_cron/tests.py @@ -1,23 +1,26 @@ import datetime import threading -from time import sleep from datetime import timedelta +from time import sleep from unittest import skip -from mock import patch -from freezegun import freeze_time - from django import db -from django.test import TransactionTestCase +from django.contrib.auth.models import User from django.core.management import call_command -from django.test.utils import override_settings +from django.test import TransactionTestCase from django.test.client import Client +from django.test.utils import override_settings from django.urls import reverse -from django.contrib.auth.models import User +from freezegun import freeze_time +from mock import patch from django_cron.helpers import humanize_duration -from django_cron.models import CronJobLog, CronJobLock -import test_crons +from django_cron.models import CronJobLock, CronJobLog + +from .test import cron + +_MAIN_CRON_MODULE = 'django_cron.cron' +_TEST_CRON_MODULE = 'django_cron.test.cron' class OutBuffer(object): @@ -48,20 +51,18 @@ def call(command, *args, **kwargs): class TestRunCrons(TransactionTestCase): - success_cron = 'test_crons.TestSuccessCronJob' - error_cron = 'test_crons.TestErrorCronJob' - five_mins_cron = 'test_crons.Test5minsCronJob' - five_mins_with_tolerance_cron = 'test_crons.Test5minsWithToleranceCronJob' - run_at_times_cron = 'test_crons.TestRunAtTimesCronJob' - wait_3sec_cron = 'test_crons.Wait3secCronJob' - run_on_wkend_cron = 'test_crons.RunOnWeekendCronJob' - does_not_exist_cron = 'ThisCronObviouslyDoesntExist' - no_code_cron = 'test_crons.NoCodeCronJob' - test_failed_runs_notification_cron = ( - 'django_cron.cron.FailedRunsNotificationCronJob' - ) - run_on_month_days = 'test_crons.RunOnMonthDaysCronJob' - run_and_remove_old_logs = 'test_crons.RunEveryMinuteAndRemoveOldLogs' + cron_success = f'{_TEST_CRON_MODULE}.TestSuccessCronJob' + cron_error = f'{_TEST_CRON_MODULE}.TestErrorCronJob' + cron_5mins = f'{_TEST_CRON_MODULE}.Test5minsCronJob' + cron_5mins_with_tolerance = f'{_TEST_CRON_MODULE}.Test5minsWithToleranceCronJob' + cron_run_at_times = f'{_TEST_CRON_MODULE}.TestRunAtTimesCronJob' + cron_wait_3sec = f'{_TEST_CRON_MODULE}.Wait3secCronJob' + cron_run_on_weekend = f'{_TEST_CRON_MODULE}.RunOnWeekendCronJob' + cron_obviously_doesnt_exist = 'ThisCronJobDoesntExost' + cron_no_code = f'{_TEST_CRON_MODULE}.NoCodeCronJob' + cron_failed_runs_notification = f'{_MAIN_CRON_MODULE}.FailedRunsNotificationCronJob' + run_on_month_days = f'{_TEST_CRON_MODULE}.RunOnMonthDaysCronJob' + run_and_remove_old_logs = f'{_TEST_CRON_MODULE}.RunEveryMinuteAndRemoveOldLogs' def _call(self, *args, **kwargs): return call('runcrons', *args, **kwargs) @@ -82,23 +83,23 @@ def assertReportedFail(self, job_cls, response): self.assertIn(expected_log, response) def test_success_cron(self): - self._call(self.success_cron, force=True) + self._call(self.cron_success, force=True) self.assertEqual(CronJobLog.objects.all().count(), 1) def test_failed_cron(self): - response = self._call(self.error_cron, force=True) - self.assertReportedFail(test_crons.TestErrorCronJob, response) + response = self._call(self.cron_error, force=True) + self.assertReportedFail(cron.TestErrorCronJob, response) self.assertEqual(CronJobLog.objects.all().count(), 1) def test_not_exists_cron(self): - response = self._call(self.does_not_exist_cron, force=True) + response = self._call(self.cron_obviously_doesnt_exist, force=True) self.assertIn('Make sure these are valid cron class names', response) - self.assertIn(self.does_not_exist_cron, response) + self.assertIn(self.cron_obviously_doesnt_exist, response) self.assertEqual(CronJobLog.objects.all().count(), 0) @patch('django_cron.core.logger') def test_requires_code(self, mock_logger): - response = self._call(self.no_code_cron, force=True) + response = self._call(self.cron_no_code, force=True) self.assertIn('does not have a code attribute', response) mock_logger.info.assert_called() @@ -106,7 +107,7 @@ def test_requires_code(self, mock_logger): DJANGO_CRON_LOCK_BACKEND='django_cron.backends.lock.file.FileLock' ) def test_file_locking_backend(self): - self._call(self.success_cron, force=True) + self._call(self.cron_success, force=True) self.assertEqual(CronJobLog.objects.all().count(), 1) @override_settings( @@ -116,23 +117,23 @@ def test_database_locking_backend(self): # TODO: to test it properly we would need to run multiple jobs at the same time cron_job_locks = CronJobLock.objects.all().count() for _ in range(3): - self._call(self.success_cron, force=True) + self._call(self.cron_success, force=True) self.assertEqual(CronJobLog.objects.all().count(), 3) self.assertEqual(CronJobLock.objects.all().count(), cron_job_locks + 1) self.assertEqual(CronJobLock.objects.first().locked, False) - @patch.object(test_crons.TestSuccessCronJob, 'do') + @patch.object(cron.TestSuccessCronJob, 'do') def test_dry_run_does_not_perform_task(self, mock_do): - response = self._call(self.success_cron, dry_run=True) - self.assertReportedRun(test_crons.TestSuccessCronJob, response) + response = self._call(self.cron_success, dry_run=True) + self.assertReportedRun(cron.TestSuccessCronJob, response) mock_do.assert_not_called() self.assertFalse(CronJobLog.objects.exists()) - @patch.object(test_crons.TestSuccessCronJob, 'do') + @patch.object(cron.TestSuccessCronJob, 'do') def test_non_dry_run_performs_task(self, mock_do): mock_do.return_value = 'message' - response = self._call(self.success_cron) - self.assertReportedRun(test_crons.TestSuccessCronJob, response) + response = self._call(self.cron_success) + self.assertReportedRun(cron.TestSuccessCronJob, response) mock_do.assert_called_once() self.assertEqual(1, CronJobLog.objects.count()) log = CronJobLog.objects.get() @@ -143,66 +144,66 @@ def test_non_dry_run_performs_task(self, mock_do): def test_runs_every_mins(self): with freeze_time("2014-01-01 00:00:00"): - response = self._call(self.five_mins_cron) - self.assertReportedRun(test_crons.Test5minsCronJob, response) + response = self._call(self.cron_5mins) + self.assertReportedRun(cron.Test5minsCronJob, response) self.assertEqual(CronJobLog.objects.all().count(), 1) with freeze_time("2014-01-01 00:04:59"): - response = self._call(self.five_mins_cron) - self.assertReportedNoRun(test_crons.Test5minsCronJob, response) + response = self._call(self.cron_5mins) + self.assertReportedNoRun(cron.Test5minsCronJob, response) self.assertEqual(CronJobLog.objects.all().count(), 1) with freeze_time("2014-01-01 00:05:01"): - response = self._call(self.five_mins_cron) - self.assertReportedRun(test_crons.Test5minsCronJob, response) + response = self._call(self.cron_5mins) + self.assertReportedRun(cron.Test5minsCronJob, response) self.assertEqual(CronJobLog.objects.all().count(), 2) def test_runs_every_mins_with_tolerance(self): with freeze_time("2014-01-01 00:00:00"): - call_command('runcrons', self.five_mins_with_tolerance_cron) + call_command('runcrons', self.cron_5mins_with_tolerance) self.assertEqual(CronJobLog.objects.all().count(), 1) with freeze_time("2014-01-01 00:04:59"): - call_command('runcrons', self.five_mins_with_tolerance_cron) + call_command('runcrons', self.cron_5mins_with_tolerance) self.assertEqual(CronJobLog.objects.all().count(), 2) with freeze_time("2014-01-01 00:05:01"): - call_command('runcrons', self.five_mins_with_tolerance_cron) + call_command('runcrons', self.cron_5mins_with_tolerance) self.assertEqual(CronJobLog.objects.all().count(), 2) with freeze_time("2014-01-01 00:09:40"): - call_command('runcrons', self.five_mins_with_tolerance_cron) + call_command('runcrons', self.cron_5mins_with_tolerance) self.assertEqual(CronJobLog.objects.all().count(), 2) with freeze_time("2014-01-01 00:09:54"): - call_command('runcrons', self.five_mins_with_tolerance_cron) + call_command('runcrons', self.cron_5mins_with_tolerance) self.assertEqual(CronJobLog.objects.all().count(), 2) with freeze_time("2014-01-01 00:09:55"): - call_command('runcrons', self.five_mins_with_tolerance_cron) + call_command('runcrons', self.cron_5mins_with_tolerance) self.assertEqual(CronJobLog.objects.all().count(), 3) def test_runs_at_time(self): with freeze_time("2014-01-01 00:00:01"): - response = self._call(self.run_at_times_cron) - self.assertReportedRun(test_crons.TestRunAtTimesCronJob, response) + response = self._call(self.cron_run_at_times) + self.assertReportedRun(cron.TestRunAtTimesCronJob, response) self.assertEqual(CronJobLog.objects.all().count(), 1) with freeze_time("2014-01-01 00:04:50"): - response = self._call(self.run_at_times_cron) - self.assertReportedNoRun(test_crons.TestRunAtTimesCronJob, response) + response = self._call(self.cron_run_at_times) + self.assertReportedNoRun(cron.TestRunAtTimesCronJob, response) self.assertEqual(CronJobLog.objects.all().count(), 1) with freeze_time("2014-01-01 00:05:01"): - response = self._call(self.run_at_times_cron) - self.assertReportedRun(test_crons.TestRunAtTimesCronJob, response) + response = self._call(self.cron_run_at_times) + self.assertReportedRun(cron.TestRunAtTimesCronJob, response) self.assertEqual(CronJobLog.objects.all().count(), 2) def test_run_on_weekend(self): for test_date in ("2017-06-17", "2017-06-18"): # Saturday and Sunday logs_count = CronJobLog.objects.all().count() with freeze_time(test_date): - call_command('runcrons', self.run_on_wkend_cron) + call_command('runcrons', self.cron_run_on_weekend) self.assertEqual(CronJobLog.objects.all().count(), logs_count + 1) for test_date in ( @@ -214,7 +215,7 @@ def test_run_on_weekend(self): ): # Mon-Fri logs_count = CronJobLog.objects.all().count() with freeze_time(test_date): - call_command('runcrons', self.run_on_wkend_cron) + call_command('runcrons', self.cron_run_on_weekend) self.assertEqual(CronJobLog.objects.all().count(), logs_count) def test_run_on_month_days(self): @@ -237,23 +238,23 @@ def test_run_on_month_days(self): self.assertEqual(CronJobLog.objects.all().count(), logs_count) def test_silent_produces_no_output_success(self): - response = self._call(self.success_cron, silent=True) + response = self._call(self.cron_success, silent=True) self.assertEqual(1, CronJobLog.objects.count()) self.assertEqual('', response) def test_silent_produces_no_output_no_run(self): with freeze_time("2014-01-01 00:00:00"): - response = self._call(self.run_at_times_cron, silent=True) + response = self._call(self.cron_run_at_times, silent=True) self.assertEqual(1, CronJobLog.objects.count()) self.assertEqual('', response) with freeze_time("2014-01-01 00:00:01"): - response = self._call(self.run_at_times_cron, silent=True) + response = self._call(self.cron_run_at_times, silent=True) self.assertEqual(1, CronJobLog.objects.count()) self.assertEqual('', response) def test_silent_produces_no_output_failure(self): - response = self._call(self.error_cron, silent=True) + response = self._call(self.cron_error, silent=True) self.assertEqual('', response) def test_admin(self): @@ -263,14 +264,14 @@ def test_admin(self): self.client.login(username=user.username, password=password) # edit CronJobLog object - self._call(self.success_cron, force=True) + self._call(self.cron_success, force=True) log = CronJobLog.objects.all()[0] url = reverse('admin:django_cron_cronjoblog_change', args=(log.id,)) response = self.client.get(url) self.assertIn('Cron job logs', str(response.content)) def run_cronjob_in_thread(self, logs_count): - self._call(self.wait_3sec_cron) + self._call(self.cron_wait_3sec) self.assertEqual(CronJobLog.objects.all().count(), logs_count + 1) db.close_old_connections() @@ -283,7 +284,7 @@ def test_cache_locking_backend(self): t.start() # this shouldn't get running sleep(0.1) # to avoid race condition - self._call(self.wait_3sec_cron) + self._call(self.cron_wait_3sec) t.join(10) self.assertEqual(CronJobLog.objects.all().count(), 1) @@ -312,8 +313,8 @@ def test_failed_runs_notification(self): CronJobLog.objects.all().delete() for i in range(10): - self._call(self.error_cron, force=True) - self._call(self.test_failed_runs_notification_cron) + self._call(self.cron_error, force=True) + self._call(self.cron_failed_runs_notification) self.assertEqual(CronJobLog.objects.all().count(), 11) @@ -342,31 +343,29 @@ def test_remove_old_succeeded_job_logs(self): def test_run_job_with_logs_in_future(self): mock_date_in_future = datetime.datetime(2222, 5, 1, 12, 0, 0) with freeze_time(mock_date_in_future): - call_command('runcrons', self.five_mins_cron) + call_command('runcrons', self.cron_5mins) self.assertEqual(CronJobLog.objects.all().count(), 1) self.assertEqual(CronJobLog.objects.all().first().end_time, mock_date_in_future) mock_date_in_past = mock_date_in_future - timedelta(days=1000) with freeze_time(mock_date_in_past): - call_command('runcrons', self.five_mins_cron) + call_command('runcrons', self.cron_5mins) self.assertEqual(CronJobLog.objects.all().count(), 2) self.assertEqual(CronJobLog.objects.all().earliest('start_time').end_time, mock_date_in_past) mock_date_in_past_plus_one_min = mock_date_in_future + timedelta(minutes=1) with freeze_time(mock_date_in_past_plus_one_min): - call_command('runcrons', self.five_mins_cron) + call_command('runcrons', self.cron_5mins) self.assertEqual(CronJobLog.objects.all().count(), 2) self.assertEqual(CronJobLog.objects.all().earliest('start_time').end_time, mock_date_in_past) class TestCronLoop(TransactionTestCase): - success_cron = 'test_crons.TestSuccessCronJob' + success_cron = f'{_TEST_CRON_MODULE}.TestSuccessCronJob' def _call(self, *args, **kwargs): return call('cronloop', *args, **kwargs) def test_repeat_twice(self): - self._call( - cron_classes=[self.success_cron, self.success_cron], repeat=2, sleep=1 - ) + self._call(cron_classes=[self.success_cron, self.success_cron], repeat=2, sleep=1) self.assertEqual(CronJobLog.objects.all().count(), 4) diff --git a/flake8 b/flake8 deleted file mode 100644 index dd1cc08..0000000 --- a/flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -max-line-length = 160 -exclude = docs/*,lib/* -ignore = F403 diff --git a/helpers.py b/helpers.py deleted file mode 100644 index 6e90e3c..0000000 --- a/helpers.py +++ /dev/null @@ -1,32 +0,0 @@ -from django.utils.translation import gettext_lazy as _ - - -def humanize_duration(duration): - """ - Returns a humanized string representing time difference - - For example: 2 days 1 hour 25 minutes 10 seconds - """ - days = duration.days - hours = duration.seconds / 3600 - minutes = duration.seconds % 3600 / 60 - seconds = duration.seconds % 3600 % 60 - - parts = [] - if days > 0: - parts.append(u'%s %s' % (days, _('day') if days == 1 else _('days'))) - - if hours > 0: - parts.append(u'%s %s' % (hours, _('hour') if hours == 1 else _('hours'))) - - if minutes > 0: - parts.append( - u'%s %s' % (minutes, _('minute') if minutes == 1 else _('minutes')) - ) - - if seconds > 0: - parts.append( - u'%s %s' % (seconds, _('second') if seconds == 1 else _('seconds')) - ) - - return ' '.join(parts) if len(parts) != 0 else _('< 1 second') diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..8e177e2 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,164 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "asgiref" +version = "3.8.1" +description = "ASGI specs, helper code, and adapters" +optional = false +python-versions = ">=3.8" +files = [ + {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, + {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} + +[package.extras] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] + +[[package]] +name = "backports-zoneinfo" +version = "0.2.1" +description = "Backport of the standard library zoneinfo module" +optional = false +python-versions = ">=3.6" +files = [ + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, + {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, +] + +[package.extras] +tzdata = ["tzdata"] + +[[package]] +name = "django" +version = "4.2.14" +description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Django-4.2.14-py3-none-any.whl", hash = "sha256:3ec32bc2c616ab02834b9cac93143a7dc1cdcd5b822d78ac95fc20a38c534240"}, + {file = "Django-4.2.14.tar.gz", hash = "sha256:fc6919875a6226c7ffcae1a7d51e0f2ceaf6f160393180818f6c95f51b1e7b96"}, +] + +[package.dependencies] +asgiref = ">=3.6.0,<4" +"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} +sqlparse = ">=0.3.1" +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +argon2 = ["argon2-cffi (>=19.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +name = "freezegun" +version = "1.5.1" +description = "Let your Python tests travel through time" +optional = false +python-versions = ">=3.7" +files = [ + {file = "freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1"}, + {file = "freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9"}, +] + +[package.dependencies] +python-dateutil = ">=2.7" + +[[package]] +name = "mock" +version = "5.1.0" +description = "Rolling backport of unittest.mock for all Pythons" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mock-5.1.0-py3-none-any.whl", hash = "sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744"}, + {file = "mock-5.1.0.tar.gz", hash = "sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d"}, +] + +[package.extras] +build = ["blurb", "twine", "wheel"] +docs = ["sphinx"] +test = ["pytest", "pytest-cov"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "sqlparse" +version = "0.5.1" +description = "A non-validating SQL parser." +optional = false +python-versions = ">=3.8" +files = [ + {file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, + {file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, +] + +[package.extras] +dev = ["build", "hatch"] +doc = ["sphinx"] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "tzdata" +version = "2024.1" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.8" +content-hash = "ea0b5d905038e4a0d3853700b59ccdb32fa5dd5d1c5d5b959e56b45a5fcfb679" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e3138ff --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,33 @@ +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] +name = "django-cron" +version = "0.6.0" +description = "Running python crons in a Django project" +authors = [ + "Sumit Chachra ", + "Joren Hammudoglu ", +] +readme = "README.rst" +classifiers = [ + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "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", + "Framework :: Django", +] +repository = "https://github.com/jorenham/django-cron" + +[tool.poetry.dependencies] +python = "^3.8" +django = ">=4.2,<6.0" + +[tool.poetry.group.dev.dependencies] +freezegun = "^1.5.1" +mock = "^5.1.0" diff --git a/runtests.py b/runtests.py deleted file mode 100644 index 5ae04d6..0000000 --- a/runtests.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file mainly exists to allow python setup.py test to work. -# flake8: noqa -import os -import sys - -if 'DJANGO_SETTINGS_MODULE' not in os.environ: - os.environ['DJANGO_SETTINGS_MODULE'] = 'settings_sqllite' - -test_dir = os.path.dirname(__file__) -sys.path.insert(0, test_dir) - -import django -from django.test.utils import get_runner -from django.conf import settings - - -def runtests(): - TestRunner = get_runner(settings) - test_runner = TestRunner(verbosity=1, interactive=False) - if hasattr(django, 'setup'): - django.setup() - failures = test_runner.run_tests(['django_cron']) - sys.exit(bool(failures)) - - -if __name__ == '__main__': - globals()[sys.argv[1]]() diff --git a/settings_mysql.py b/settings_mysql.py deleted file mode 100644 index f574741..0000000 --- a/settings_mysql.py +++ /dev/null @@ -1,11 +0,0 @@ -from settings_base import * - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'USER': 'travis', - 'PASSWORD': '', - 'NAME': 'travis', - 'TEST_NAME': 'travis_test', - } -} diff --git a/settings_postgres.py b/settings_postgres.py deleted file mode 100644 index 4e64bc4..0000000 --- a/settings_postgres.py +++ /dev/null @@ -1,11 +0,0 @@ -from settings_base import * - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'USER': 'postgres', - 'PASSWORD': '', - 'NAME': 'travis', - 'TEST_NAME': 'travis_test', - } -} diff --git a/settings_sqllite.py b/settings_sqllite.py deleted file mode 100644 index 65b6927..0000000 --- a/settings_sqllite.py +++ /dev/null @@ -1,10 +0,0 @@ -from settings_base import * - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'USER': 'travis', - 'NAME': 'djangocron', - 'TEST_NAME': 'djangocron_test', - } -} diff --git a/setup.py b/setup.py deleted file mode 100644 index 3d057dc..0000000 --- a/setup.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python -# flake8: noqa - -try: - from setuptools import setup, find_packages - from setuptools.command.test import test -except ImportError: - from ez_setup import use_setuptools - - use_setuptools() - from setuptools import setup, find_packages - from setuptools.command.test import test -import os - - -here = os.path.dirname(os.path.abspath(__file__)) -f = open(os.path.join(here, 'README.rst')) -long_description = f.read().strip() -f.close() - -setup( - name='django-cron', - version='0.6.0', - author='Sumit Chachra', - author_email='chachra@tivix.com', - url='http://github.com/tivix/django-cron', - description='Running python crons in a Django project', - packages=find_packages(), - long_description=long_description, - keywords='django cron', - zip_safe=False, - install_requires=['Django>=3.2'], - test_suite='runtests.runtests', - include_package_data=True, - classifiers=[ - 'Framework :: Django', - 'Intended Audience :: Developers', - 'Intended Audience :: System Administrators', - 'Operating System :: OS Independent', - 'Topic :: Software Development', - ], -) diff --git a/test_requirements.txt b/test_requirements.txt deleted file mode 100644 index 5e6be87..0000000 --- a/test_requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -freezegun==0.2.8 -mock==2.0.0 -psycopg2==2.9.3 -flake8==2.4.0 diff --git a/testmanage.py b/testmanage.py new file mode 100755 index 0000000..c5fd447 --- /dev/null +++ b/testmanage.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +import os +import sys + +from django.core.management import execute_from_command_line + + +def main(): + os.environ.setdefault( + 'DJANGO_SETTINGS_MODULE', + 'django_cron.test.settings', + ) + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() From 7abdad48489be91dfc2669cdd530f618148c1d8f Mon Sep 17 00:00:00 2001 From: jorenham Date: Sun, 28 Jul 2024 18:46:11 +0200 Subject: [PATCH 3/8] replace deprecated `index_together` with `indexes` --- .../0004_alter_cronjoblog_options_and_more.py | 68 +++++++++++++++++++ django_cron/models.py | 37 +++++----- 2 files changed, 86 insertions(+), 19 deletions(-) create mode 100644 django_cron/migrations/0004_alter_cronjoblog_options_and_more.py diff --git a/django_cron/migrations/0004_alter_cronjoblog_options_and_more.py b/django_cron/migrations/0004_alter_cronjoblog_options_and_more.py new file mode 100644 index 0000000..99893fd --- /dev/null +++ b/django_cron/migrations/0004_alter_cronjoblog_options_and_more.py @@ -0,0 +1,68 @@ +# Generated by Django 4.2.14 on 2024-07-28 11:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_cron', '0003_cronjoblock'), + ] + + operations = [ + migrations.AlterModelOptions( + name='cronjoblog', + options={'get_latest_by': 'start_time'}, + ), + migrations.RenameIndex( + model_name='cronjoblog', + new_name='django_cron_code_966ed3_idx', + old_fields=('code', 'start_time'), + ), + migrations.RenameIndex( + model_name='cronjoblog', + new_name='django_cron_code_21f381_idx', + old_fields=('code', 'start_time', 'ran_at_time'), + ), + migrations.RenameIndex( + model_name='cronjoblog', + new_name='django_cron_code_89ad04_idx', + old_fields=('code', 'is_success', 'ran_at_time'), + ), + migrations.AlterField( + model_name='cronjoblog', + name='code', + field=models.CharField(max_length=64), + ), + migrations.AlterField( + model_name='cronjoblog', + name='end_time', + field=models.DateTimeField(), + ), + migrations.AlterField( + model_name='cronjoblog', + name='ran_at_time', + field=models.TimeField(blank=True, editable=False, null=True), + ), + migrations.AlterField( + model_name='cronjoblog', + name='start_time', + field=models.DateTimeField(), + ), + migrations.AddIndex( + model_name='cronjoblog', + index=models.Index(fields=['code'], name='django_cron_code_26aea9_idx'), + ), + migrations.AddIndex( + model_name='cronjoblog', + index=models.Index(fields=['start_time'], name='django_cron_start_t_9e0b8f_idx'), + ), + migrations.AddIndex( + model_name='cronjoblog', + index=models.Index(fields=['end_time'], name='django_cron_end_tim_c3cfdc_idx'), + ), + migrations.AddIndex( + model_name='cronjoblog', + index=models.Index(fields=['ran_at_time'], name='django_cron_ran_at__aa3be6_idx'), + ), + ] diff --git a/django_cron/models.py b/django_cron/models.py index c109e09..c7413ca 100644 --- a/django_cron/models.py +++ b/django_cron/models.py @@ -6,33 +6,32 @@ class CronJobLog(models.Model): Keeps track of the cron jobs that ran etc. and any error messages if they failed. """ - - code = models.CharField(max_length=64, db_index=True) - start_time = models.DateTimeField(db_index=True) - end_time = models.DateTimeField(db_index=True) + code = models.CharField(max_length=64) + start_time = models.DateTimeField() + end_time = models.DateTimeField() is_success = models.BooleanField(default=False) - message = models.TextField(default='', blank=True) # TODO: db_index=True + message = models.TextField(default="", blank=True) # This field is used to mark jobs executed in exact time. # Jobs that run every X minutes, have this field empty. - ran_at_time = models.TimeField(null=True, blank=True, db_index=True, editable=False) - - def __unicode__(self): - return '%s (%s)' % (self.code, 'Success' if self.is_success else 'Fail') + ran_at_time = models.TimeField(null=True, blank=True, editable=False) def __str__(self): - return "%s (%s)" % (self.code, "Success" if self.is_success else "Fail") + status = "Success" if self.is_success else "Fail" + return f"{self.code} ({status})" class Meta: - index_together = [ - ('code', 'is_success', 'ran_at_time'), - ('code', 'start_time', 'ran_at_time'), - ( - 'code', - 'start_time', - ), # useful when finding latest run (order by start_time) of cron - ] - app_label = 'django_cron' + app_label = "django_cron" + get_latest_by = "start_time" + indexes = ( + models.Index(fields=["code"]), + models.Index(fields=["start_time"]), + models.Index(fields=["end_time"]), + models.Index(fields=["ran_at_time"]), + models.Index(fields=["code", "start_time"]), + models.Index(fields=["code", "start_time", "ran_at_time"]), + models.Index(fields=["code", "is_success", "ran_at_time"]), + ) class CronJobLock(models.Model): From e31ddc129584fee0414eb5d322b75fab464969ab Mon Sep 17 00:00:00 2001 From: jorenham Date: Sun, 28 Jul 2024 18:54:36 +0200 Subject: [PATCH 4/8] ruff formatting --- django_cron/__init__.py | 19 ++- django_cron/admin.py | 54 ++++--- django_cron/backends/lock/base.py | 8 +- django_cron/backends/lock/cache.py | 6 +- django_cron/backends/lock/database.py | 8 +- django_cron/backends/lock/file.py | 10 +- django_cron/core.py | 111 ++++++++------ django_cron/cron.py | 5 +- django_cron/helpers.py | 12 +- django_cron/management/commands/cronloop.py | 4 +- django_cron/management/commands/runcrons.py | 33 ++-- django_cron/test/cron.py | 26 ++-- django_cron/test/settings.py | 108 +++++++------- django_cron/test/urls.py | 2 +- django_cron/tests.py | 157 +++++++++++--------- djangocron | 0 16 files changed, 306 insertions(+), 257 deletions(-) create mode 100644 djangocron diff --git a/django_cron/__init__.py b/django_cron/__init__.py index b5f77b8..f5b8374 100644 --- a/django_cron/__init__.py +++ b/django_cron/__init__.py @@ -1,2 +1,17 @@ -from django_cron.core import * -from django_cron.helpers import get_class, get_current_time +from .core import ( + DEFAULT_LOCK_BACKEND, + DJANGO_CRON_OUTPUT_ERRORS, + BadCronJobError, + CronJobBase, + CronJobManager, + Schedule, +) + +__all__ = ( + "DEFAULT_LOCK_BACKEND", + "DJANGO_CRON_OUTPUT_ERRORS", + "BadCronJobError", + "CronJobBase", + "CronJobManager", + "Schedule", +) diff --git a/django_cron/admin.py b/django_cron/admin.py index c43365d..5c1254a 100644 --- a/django_cron/admin.py +++ b/django_cron/admin.py @@ -4,50 +4,56 @@ from django.db.models import DurationField, ExpressionWrapper, F from django.utils.translation import gettext_lazy as _ -from django_cron.models import CronJobLog, CronJobLock -from django_cron.helpers import humanize_duration +from .helpers import humanize_duration +from .models import CronJobLock, CronJobLog class DurationFilter(admin.SimpleListFilter): - title = _('duration') - parameter_name = 'duration' + title = _("duration") + parameter_name = "duration" def lookups(self, request, model_admin): return ( - ('lte_minute', _('<= 1 minute')), - ('gt_minute', _('> 1 minute')), - ('gt_hour', _('> 1 hour')), - ('gt_day', _('> 1 day')), + ("lte_minute", _("<= 1 minute")), + ("gt_minute", _("> 1 minute")), + ("gt_hour", _("> 1 hour")), + ("gt_day", _("> 1 day")), ) def queryset(self, request, queryset): - if self.value() == 'lte_minute': - return queryset.filter(end_time__lte=F('start_time') + timedelta(minutes=1)) - if self.value() == 'gt_minute': - return queryset.filter(end_time__gt=F('start_time') + timedelta(minutes=1)) - if self.value() == 'gt_hour': - return queryset.filter(end_time__gt=F('start_time') + timedelta(hours=1)) - if self.value() == 'gt_day': - return queryset.filter(end_time__gt=F('start_time') + timedelta(days=1)) + if self.value() == "lte_minute": + return queryset.filter(end_time__lte=F("start_time") + timedelta(minutes=1)) + if self.value() == "gt_minute": + return queryset.filter(end_time__gt=F("start_time") + timedelta(minutes=1)) + if self.value() == "gt_hour": + return queryset.filter(end_time__gt=F("start_time") + timedelta(hours=1)) + if self.value() == "gt_day": + return queryset.filter(end_time__gt=F("start_time") + timedelta(days=1)) class CronJobLogAdmin(admin.ModelAdmin): class Meta: model = CronJobLog - search_fields = ('code', 'message') - ordering = ('-start_time',) - list_display = ('code', 'start_time', 'end_time', 'humanize_duration', 'is_success') - list_filter = ('code', 'start_time', 'is_success', DurationFilter) + search_fields = ("code", "message") + ordering = ("-start_time",) + list_display = ("code", "start_time", "end_time", "humanize_duration", "is_success") + list_filter = ("code", "start_time", "is_success", DurationFilter) def get_queryset(self, request): - return super().get_queryset(request).annotate( - duration=ExpressionWrapper(F('end_time') - F('start_time'), DurationField()), + return ( + super() + .get_queryset(request) + .annotate( + duration=ExpressionWrapper( + F("end_time") - F("start_time"), DurationField() + ), + ) ) def get_readonly_fields(self, request, obj=None): if not request.user.is_superuser and obj is not None: - names = [f.name for f in CronJobLog._meta.fields if f.name != 'id'] + names = [f.name for f in CronJobLog._meta.fields if f.name != "id"] return self.readonly_fields + tuple(names) return self.readonly_fields @@ -55,7 +61,7 @@ def humanize_duration(self, obj): return humanize_duration(obj.end_time - obj.start_time) humanize_duration.short_description = _("Duration") - humanize_duration.admin_order_field = 'duration' + humanize_duration.admin_order_field = "duration" admin.site.register(CronJobLog, CronJobLogAdmin) diff --git a/django_cron/backends/lock/base.py b/django_cron/backends/lock/base.py index a003b4a..7e62bc6 100644 --- a/django_cron/backends/lock/base.py +++ b/django_cron/backends/lock/base.py @@ -27,9 +27,9 @@ def __init__(self, cron_class, silent, *args, **kwargs): * self.silent for you. The rest is backend-specific. """ - self.job_name = '.'.join([cron_class.__module__, cron_class.__name__]) + self.job_name = ".".join([cron_class.__module__, cron_class.__name__]) self.job_code = cron_class.code - self.parallel = getattr(cron_class, 'ALLOW_PARALLEL_RUNS', False) + self.parallel = getattr(cron_class, "ALLOW_PARALLEL_RUNS", False) self.silent = silent def lock(self): @@ -41,7 +41,7 @@ def lock(self): Here you can optionally call self.notice_lock_failed(). """ raise NotImplementedError( - 'You have to implement lock(self) method for your class' + "You have to implement lock(self) method for your class" ) def release(self): @@ -51,7 +51,7 @@ def release(self): No need to return anything currently. """ raise NotImplementedError( - 'You have to implement release(self) method for your class' + "You have to implement release(self) method for your class" ) def lock_failed_message(self): diff --git a/django_cron/backends/lock/cache.py b/django_cron/backends/lock/cache.py index 00c9353..046dfec 100644 --- a/django_cron/backends/lock/cache.py +++ b/django_cron/backends/lock/cache.py @@ -2,7 +2,7 @@ from django.core.cache import caches from django.utils import timezone -from django_cron.backends.lock.base import DjangoCronJobLock +from .base import DjangoCronJobLock class CacheLock(DjangoCronJobLock): @@ -60,9 +60,9 @@ def get_lock_name(self): def get_cache_timeout(self, cron_class): try: timeout = getattr( - cron_class, 'DJANGO_CRON_LOCK_TIME', settings.DJANGO_CRON_LOCK_TIME + cron_class, "DJANGO_CRON_LOCK_TIME", settings.DJANGO_CRON_LOCK_TIME ) - except: + except Exception: timeout = self.DEFAULT_LOCK_TIME return timeout diff --git a/django_cron/backends/lock/database.py b/django_cron/backends/lock/database.py index 3bf78a1..f72c829 100644 --- a/django_cron/backends/lock/database.py +++ b/django_cron/backends/lock/database.py @@ -1,7 +1,9 @@ -from django_cron.backends.lock.base import DjangoCronJobLock -from django_cron.models import CronJobLock from django.db import transaction +from django_cron.models import CronJobLock + +from .base import DjangoCronJobLock + class DatabaseLock(DjangoCronJobLock): """ @@ -11,7 +13,7 @@ class DatabaseLock(DjangoCronJobLock): @transaction.atomic def lock(self): - lock, created = CronJobLock.objects.get_or_create(job_name=self.job_name) + lock, _ = CronJobLock.objects.get_or_create(job_name=self.job_name) if lock.locked: return False else: diff --git a/django_cron/backends/lock/file.py b/django_cron/backends/lock/file.py index 1d14d31..d0868b3 100644 --- a/django_cron/backends/lock/file.py +++ b/django_cron/backends/lock/file.py @@ -3,7 +3,7 @@ from django.conf import settings from django.core.files import locks -from django_cron.backends.lock.base import DjangoCronJobLock +from .base import DjangoCronJobLock class FileLock(DjangoCronJobLock): @@ -16,7 +16,7 @@ class FileLock(DjangoCronJobLock): def lock(self): lock_name = self.get_lock_name() try: - self.__lock_fd = open(lock_name, 'w+b', 1) + self.__lock_fd = open(lock_name, "w+b", 1) locks.lock(self.__lock_fd, locks.LOCK_EX | locks.LOCK_NB) except IOError: return False @@ -27,11 +27,11 @@ def release(self): self.__lock_fd.close() def get_lock_name(self): - default_path = '/tmp' - path = getattr(settings, 'DJANGO_CRON_LOCKFILE_PATH', default_path) + default_path = "/tmp" + path = getattr(settings, "DJANGO_CRON_LOCKFILE_PATH", default_path) if not os.path.isdir(path): # let it die if failed, can't run further anyway os.makedirs(path, exist_ok=True) - filename = self.job_name + '.lock' + filename = self.job_name + ".lock" return os.path.join(path, filename) diff --git a/django_cron/core.py b/django_cron/core.py index d7a945b..2ec3145 100644 --- a/django_cron/core.py +++ b/django_cron/core.py @@ -8,11 +8,20 @@ from django.db.models import Q from django.utils.timezone import now as utc_now -from django_cron.helpers import get_class, get_current_time - -DEFAULT_LOCK_BACKEND = 'django_cron.backends.lock.cache.CacheLock' +from .helpers import get_class, get_current_time + +__all__ = [ + "DEFAULT_LOCK_BACKEND", + "DJANGO_CRON_OUTPUT_ERRORS", + "BadCronJobError", + "CronJobBase", + "CronJobManager", + "Schedule", +] + +DEFAULT_LOCK_BACKEND = "django_cron.backends.lock.cache.CacheLock" DJANGO_CRON_OUTPUT_ERRORS = False -logger = logging.getLogger('django_cron') +logger = logging.getLogger("django_cron") class BadCronJobError(AssertionError): @@ -21,13 +30,13 @@ class BadCronJobError(AssertionError): class Schedule(object): def __init__( - self, - run_every_mins=None, - run_at_times=None, - retry_after_failure_mins=None, - run_weekly_on_days=None, - run_monthly_on_days=None, - run_tolerance_seconds=0, + self, + run_every_mins=None, + run_at_times=None, + retry_after_failure_mins=None, + run_weekly_on_days=None, + run_monthly_on_days=None, + run_tolerance_seconds=0, ): if run_at_times is None: run_at_times = [] @@ -65,13 +74,13 @@ def get_time_until_run(cls): from django_cron.models import CronJobLog try: - last_job = CronJobLog.objects.filter(code=cls.code).latest('start_time') + last_job = CronJobLog.objects.filter(code=cls.code).latest("start_time") except CronJobLog.DoesNotExist: return timedelta() return ( - last_job.start_time - + timedelta(minutes=cls.schedule.run_every_mins) - - utc_now() + last_job.start_time + + timedelta(minutes=cls.schedule.run_every_mins) + - utc_now() ) @@ -91,7 +100,7 @@ def __init__(self, cron_job_class, silent=False, dry_run=False, stdout=None): self.lock_class = self.get_lock_class() self.previously_ran_successful_cron = None self.write_log = getattr( - settings, 'DJANGO_CRON_OUTPUT_ERRORS', DJANGO_CRON_OUTPUT_ERRORS + settings, "DJANGO_CRON_OUTPUT_ERRORS", DJANGO_CRON_OUTPUT_ERRORS ) def should_run_now(self, force=False): @@ -120,32 +129,35 @@ def should_run_now(self, force=False): # We check last job - success or not last_job = ( CronJobLog.objects.filter(code=cron_job.code) - .order_by('-start_time') - .exclude(start_time__gt=datetime.today()) - .first() + .order_by("-start_time") + .exclude(start_time__gt=datetime.today()) + .first() ) if ( - last_job - and not last_job.is_success - and get_current_time() + timedelta(seconds=cron_job.schedule.run_tolerance_seconds) - <= last_job.start_time - + timedelta(minutes=cron_job.schedule.retry_after_failure_mins) + last_job + and not last_job.is_success + and get_current_time() + + timedelta(seconds=cron_job.schedule.run_tolerance_seconds) + <= last_job.start_time + + timedelta(minutes=cron_job.schedule.retry_after_failure_mins) ): return False if cron_job.schedule.run_every_mins is not None: try: - self.previously_ran_successful_cron = CronJobLog.objects.filter( - code=cron_job.code, is_success=True - ).exclude(start_time__gt=datetime.today()).latest('start_time') + self.previously_ran_successful_cron = ( + CronJobLog.objects.filter(code=cron_job.code, is_success=True) + .exclude(start_time__gt=datetime.today()) + .latest("start_time") + ) except CronJobLog.DoesNotExist: pass if self.previously_ran_successful_cron: - if ( - get_current_time() + timedelta(seconds=cron_job.schedule.run_tolerance_seconds) - > self.previously_ran_successful_cron.start_time - + timedelta(minutes=cron_job.schedule.run_every_mins) + if get_current_time() + timedelta( + seconds=cron_job.schedule.run_tolerance_seconds + ) > self.previously_ran_successful_cron.start_time + timedelta( + minutes=cron_job.schedule.run_every_mins ): return True else: @@ -176,12 +188,12 @@ def should_run_now(self, force=False): def make_log(self, *messages, **kwargs): cron_log = self.cron_log - cron_job = getattr(self, 'cron_job', self.cron_job_class) + cron_job = getattr(self, "cron_job", self.cron_job_class) cron_log.code = cron_job.code - cron_log.is_success = kwargs.get('success', True) + cron_log.is_success = kwargs.get("success", True) cron_log.message = self.make_log_msg(messages) - cron_log.ran_at_time = getattr(self, 'user_time', None) + cron_log.ran_at_time = getattr(self, "user_time", None) cron_log.end_time = get_current_time() cron_log.save() @@ -189,12 +201,12 @@ def make_log(self, *messages, **kwargs): logger.error("%s cronjob error:\n%s" % (cron_log.code, cron_log.message)) def make_log_msg(self, messages): - full_message = '' + full_message = "" if messages: for message in messages: if len(message): full_message += message - full_message += '\n' + full_message += "\n" return full_message @@ -218,7 +230,7 @@ def __exit__(self, ex_type, ex_value, ex_traceback): else: if not self.silent: self.stdout.write( - u"[\N{HEAVY BALLOT X}] {0}\n".format(self.cron_job_class.code) + "[\N{HEAVY BALLOT X}] {0}\n".format(self.cron_job_class.code) ) try: trace = "".join( @@ -242,10 +254,10 @@ def run(self, force=False): if not issubclass(cron_job_class, CronJobBase): raise BadCronJobError( - 'The cron_job to be run must be a subclass of %s' % CronJobBase.__name__ + "The cron_job to be run must be a subclass of %s" % CronJobBase.__name__ ) - if not hasattr(cron_job_class, 'code'): + if not hasattr(cron_job_class, "code"): raise BadCronJobError( "Cron class '{0}' does not have a code attribute".format( cron_job_class.__name__ @@ -262,7 +274,7 @@ def run(self, force=False): cron_job_class.__name__, self.cron_job.code, ) - self.make_log('Job in progress', success=True) + self.make_log("Job in progress", success=True) self.msg = self.cron_job.do() self.make_log(self.msg, success=True) self.cron_job.set_prev_success_cron( @@ -270,14 +282,14 @@ def run(self, force=False): ) if not self.silent: self.stdout.write( - u"[\N{HEAVY CHECK MARK}] {0}\n".format(self.cron_job.code) + "[\N{HEAVY CHECK MARK}] {0}\n".format(self.cron_job.code) ) self._remove_old_success_job_logs(cron_job_class) elif not self.silent: - self.stdout.write(u"[ ] {0}\n".format(self.cron_job.code)) + self.stdout.write("[ ] {0}\n".format(self.cron_job.code)) def get_lock_class(self): - name = getattr(settings, 'DJANGO_CRON_LOCK_BACKEND', DEFAULT_LOCK_BACKEND) + name = getattr(settings, "DJANGO_CRON_LOCK_BACKEND", DEFAULT_LOCK_BACKEND) try: return get_class(name) except Exception as err: @@ -285,15 +297,20 @@ def get_lock_class(self): @property def msg(self): - return getattr(self, '_msg', '') + return getattr(self, "_msg", "") @msg.setter def msg(self, msg): if msg is None: - msg = '' + msg = "" self._msg = msg def _remove_old_success_job_logs(self, job_class): - if job_class.remove_successful_cron_logs or getattr(settings, 'REMOVE_SUCCESSFUL_CRON_LOGS', False): + if job_class.remove_successful_cron_logs or getattr( + settings, "REMOVE_SUCCESSFUL_CRON_LOGS", False + ): from django_cron.models import CronJobLog - CronJobLog.objects.filter(code=job_class.code, is_success=True).exclude(pk=self.cron_log.pk).delete() + + CronJobLog.objects.filter(code=job_class.code, is_success=True).exclude( + pk=self.cron_log.pk + ).delete() diff --git a/django_cron/cron.py b/django_cron/cron.py index 60d24b9..762449c 100644 --- a/django_cron/cron.py +++ b/django_cron/cron.py @@ -1,8 +1,9 @@ from django.conf import settings from django.core.mail import send_mail -from django_cron import CronJobBase, Schedule, get_class -from django_cron.models import CronJobLog +from .core import CronJobBase, Schedule +from .helpers import get_class +from .models import CronJobLog class FailedRunsNotificationCronJob(CronJobBase): diff --git a/django_cron/helpers.py b/django_cron/helpers.py index 1c3ecdb..7b54dbe 100644 --- a/django_cron/helpers.py +++ b/django_cron/helpers.py @@ -16,18 +16,18 @@ def humanize_duration(duration): parts = [] if days > 0: - parts.append(u'%s %s' % (days, pluralize(days, _('day,days')))) + parts.append("%s %s" % (days, pluralize(days, _("day,days")))) if hours > 0: - parts.append(u'%s %s' % (hours, pluralize(hours, _('hour,hours')))) + parts.append("%s %s" % (hours, pluralize(hours, _("hour,hours")))) if minutes > 0: - parts.append(u'%s %s' % (minutes, pluralize(minutes, _('minute,minutes')))) + parts.append("%s %s" % (minutes, pluralize(minutes, _("minute,minutes")))) if seconds > 0: - parts.append(u'%s %s' % (seconds, pluralize(seconds, _('second,seconds')))) + parts.append("%s %s" % (seconds, pluralize(seconds, _("second,seconds")))) - return ', '.join(parts) if len(parts) != 0 else _('< 1 second') + return ", ".join(parts) if len(parts) != 0 else _("< 1 second") def get_class(kls): @@ -35,7 +35,7 @@ def get_class(kls): 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('.') + parts = kls.split(".") if len(parts) == 1: raise ImportError("'{0}'' is not a valid import path".format(kls)) diff --git a/django_cron/management/commands/cronloop.py b/django_cron/management/commands/cronloop.py index 6e88622..05a2679 100644 --- a/django_cron/management/commands/cronloop.py +++ b/django_cron/management/commands/cronloop.py @@ -1,4 +1,4 @@ -from time import sleep +import time from django.core.management import BaseCommand, call_command @@ -46,6 +46,6 @@ def handle(self, *args, **options): def _call_command_or_return_true(self, command, classes, s): try: call_command(command, *classes) - sleep(s) + time.sleep(s) except KeyboardInterrupt: return True diff --git a/django_cron/management/commands/runcrons.py b/django_cron/management/commands/runcrons.py index 07fa567..55224c0 100644 --- a/django_cron/management/commands/runcrons.py +++ b/django_cron/management/commands/runcrons.py @@ -1,28 +1,27 @@ -from __future__ import print_function import traceback from datetime import timedelta -from django.core.management.base import BaseCommand from django.conf import settings +from django.core.management.base import BaseCommand from django.db import close_old_connections -from django_cron import CronJobManager, get_class, get_current_time +from django_cron.core import CronJobManager +from django_cron.helpers import get_class, get_current_time from django_cron.models import CronJobLog - DEFAULT_LOCK_TIME = 24 * 60 * 60 # 24 hours class Command(BaseCommand): def add_arguments(self, parser): - parser.add_argument('cron_classes', nargs='*') - parser.add_argument('--force', action='store_true', help='Force cron runs') + parser.add_argument("cron_classes", nargs="*") + parser.add_argument("--force", action="store_true", help="Force cron runs") parser.add_argument( - '--silent', action='store_true', help='Do not push any message on console' + "--silent", action="store_true", help="Do not push any message on console" ) parser.add_argument( - '--dry-run', - action='store_true', + "--dry-run", + action="store_true", help="Just show what crons would be run; don't actually run them", ) @@ -31,22 +30,22 @@ 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']: + if not options["silent"]: self.stdout.write("Running Crons\n") self.stdout.write("{0}\n".format("=" * 40)) - cron_classes = options['cron_classes'] + cron_classes = options["cron_classes"] if cron_classes: cron_class_names = cron_classes else: - cron_class_names = getattr(settings, 'CRON_CLASSES', []) + cron_class_names = getattr(settings, "CRON_CLASSES", []) try: crons_to_run = [get_class(x) for x in cron_class_names] except ImportError: error = traceback.format_exc() self.stdout.write( - 'ERROR: Make sure these are valid cron class names: %s\n\n%s' + "ERROR: Make sure these are valid cron class names: %s\n\n%s" % (cron_class_names, error) ) return @@ -54,9 +53,9 @@ def handle(self, *args, **options): for cron_class in crons_to_run: run_cron_with_cache_check( cron_class, - force=options['force'], - silent=options['silent'], - dry_run=options['dry_run'], + force=options["force"], + silent=options["silent"], + dry_run=options["dry_run"], stdout=self.stdout, ) @@ -86,6 +85,6 @@ def clear_old_log_entries(): """ Removes older log entries, if the appropriate setting has been set """ - if hasattr(settings, 'DJANGO_CRON_DELETE_LOGS_OLDER_THAN'): + if hasattr(settings, "DJANGO_CRON_DELETE_LOGS_OLDER_THAN"): delta = timedelta(days=settings.DJANGO_CRON_DELETE_LOGS_OLDER_THAN) CronJobLog.objects.filter(end_time__lt=get_current_time() - delta).delete() diff --git a/django_cron/test/cron.py b/django_cron/test/cron.py index 87ebdb8..68bff4b 100644 --- a/django_cron/test/cron.py +++ b/django_cron/test/cron.py @@ -1,10 +1,10 @@ from time import sleep -from django_cron import CronJobBase, Schedule +from django_cron.core import CronJobBase, Schedule class TestSuccessCronJob(CronJobBase): - code = 'test_success_cron_job' + code = "test_success_cron_job" schedule = Schedule(run_every_mins=0) def do(self): @@ -12,7 +12,7 @@ def do(self): class TestErrorCronJob(CronJobBase): - code = 'test_error_cron_job' + code = "test_error_cron_job" schedule = Schedule(run_every_mins=0) def do(self): @@ -20,7 +20,7 @@ def do(self): class Test5minsCronJob(CronJobBase): - code = 'test_run_every_mins' + code = "test_run_every_mins" schedule = Schedule(run_every_mins=5) def do(self): @@ -28,7 +28,7 @@ def do(self): class Test5minsWithToleranceCronJob(CronJobBase): - code = 'test_run_every_mins' + code = "test_run_every_mins" schedule = Schedule(run_every_mins=5, run_tolerance_seconds=5) def do(self): @@ -36,15 +36,15 @@ def do(self): class TestRunAtTimesCronJob(CronJobBase): - code = 'test_run_at_times' - schedule = Schedule(run_at_times=['0:00', '0:05']) + code = "test_run_at_times" + schedule = Schedule(run_at_times=["0:00", "0:05"]) def do(self): pass class Wait3secCronJob(CronJobBase): - code = 'test_wait_3_seconds' + code = "test_wait_3_seconds" schedule = Schedule(run_every_mins=5) def do(self): @@ -52,11 +52,11 @@ def do(self): class RunOnWeekendCronJob(CronJobBase): - code = 'run_on_weekend' + code = "run_on_weekend" schedule = Schedule( run_weekly_on_days=[5, 6], run_at_times=[ - '0:00', + "0:00", ], ) @@ -70,11 +70,11 @@ def do(self): class RunOnMonthDaysCronJob(CronJobBase): - code = 'run_on_month_days' + code = "run_on_month_days" schedule = Schedule( run_monthly_on_days=[1, 10, 20], run_at_times=[ - '0:00', + "0:00", ], ) @@ -83,7 +83,7 @@ def do(self): class RunEveryMinuteAndRemoveOldLogs(CronJobBase): - code = 'run_and_remove_old_logs' + code = "run_and_remove_old_logs" schedule = Schedule(run_every_mins=1) remove_successful_cron_logs = True diff --git a/django_cron/test/settings.py b/django_cron/test/settings.py index 4d887f5..8028bb5 100644 --- a/django_cron/test/settings.py +++ b/django_cron/test/settings.py @@ -3,94 +3,94 @@ PROJECT_DIR = Path(__file__).resolve().parent.parent BASE_DIR = PROJECT_DIR.parent -SECRET_KEY = 'not-a-secure-key' +SECRET_KEY = "not-a-secure-key" DEBUG = True INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.humanize', - 'django.contrib.contenttypes', - 'django.contrib.messages', - 'django.contrib.sessions', - 'django.contrib.staticfiles', - 'django_cron', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.humanize", + "django.contrib.contenttypes", + "django.contrib.messages", + "django.contrib.sessions", + "django.contrib.staticfiles", + "django_cron", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'django_cron.test.urls' +ROOT_URLCONF = "django_cron.test.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", }, } # since django is meant for developers with deadlines... -PASSWORD_HASHERS = ('django.contrib.auth.hashers.MD5PasswordHasher',) +PASSWORD_HASHERS = ("django.contrib.auth.hashers.MD5PasswordHasher",) DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'USER': 'djangocron', - 'NAME': 'djangocron', - 'TEST_NAME': 'djangocron_test', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "USER": "djangocron", + "NAME": "djangocron", + "TEST_NAME": "djangocron_test", } } -DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'handlers': { - 'null': { - 'level': 'DEBUG', - 'class': 'logging.NullHandler', + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "null": { + "level": "DEBUG", + "class": "logging.NullHandler", }, }, - 'loggers': { - 'django_cron': { - 'handlers': ['null'], - 'level': 'INFO', - 'propagate': True, + "loggers": { + "django_cron": { + "handlers": ["null"], + "level": "INFO", + "propagate": True, }, }, } -STATIC_URL = '/static/' +STATIC_URL = "/static/" -_CRON_PATH = 'django_cron.test.cron.' +_CRON_PATH = "django_cron.test.cron." CRON_CLASSES = [ - f'{_CRON_PATH}TestSuccessCronJob', - f'{_CRON_PATH}TestErrorCronJob', - f'{_CRON_PATH}Test5minsCronJob', - f'{_CRON_PATH}TestRunAtTimesCronJob', - f'{_CRON_PATH}Wait3secCronJob', - 'django_cron.cron.FailedRunsNotificationCronJob', + f"{_CRON_PATH}TestSuccessCronJob", + f"{_CRON_PATH}TestErrorCronJob", + f"{_CRON_PATH}Test5minsCronJob", + f"{_CRON_PATH}TestRunAtTimesCronJob", + f"{_CRON_PATH}Wait3secCronJob", + "django_cron.cron.FailedRunsNotificationCronJob", ] diff --git a/django_cron/test/urls.py b/django_cron/test/urls.py index 82a6152..ce109cd 100644 --- a/django_cron/test/urls.py +++ b/django_cron/test/urls.py @@ -5,5 +5,5 @@ admin.autodiscover() urlpatterns = [ - path('admin/', admin.site.urls), + path("admin/", admin.site.urls), ] diff --git a/django_cron/tests.py b/django_cron/tests.py index 7728515..55c7da0 100644 --- a/django_cron/tests.py +++ b/django_cron/tests.py @@ -14,18 +14,17 @@ from freezegun import freeze_time from mock import patch -from django_cron.helpers import humanize_duration -from django_cron.models import CronJobLock, CronJobLog - +from .helpers import humanize_duration +from .models import CronJobLock, CronJobLog from .test import cron -_MAIN_CRON_MODULE = 'django_cron.cron' -_TEST_CRON_MODULE = 'django_cron.test.cron' +_MAIN_CRON_MODULE = "django_cron.cron" +_TEST_CRON_MODULE = "django_cron.test.cron" class OutBuffer(object): def __init__(self): - self._str_cache = '' + self._str_cache = "" self.content = [] self.modified = False @@ -35,7 +34,7 @@ def write(self, *args): def str_content(self): if self.modified: - self._str_cache = ''.join((str(x) for x in self.content)) + self._str_cache = "".join((str(x) for x in self.content)) self.modified = False return self._str_cache @@ -51,35 +50,35 @@ def call(command, *args, **kwargs): class TestRunCrons(TransactionTestCase): - cron_success = f'{_TEST_CRON_MODULE}.TestSuccessCronJob' - cron_error = f'{_TEST_CRON_MODULE}.TestErrorCronJob' - cron_5mins = f'{_TEST_CRON_MODULE}.Test5minsCronJob' - cron_5mins_with_tolerance = f'{_TEST_CRON_MODULE}.Test5minsWithToleranceCronJob' - cron_run_at_times = f'{_TEST_CRON_MODULE}.TestRunAtTimesCronJob' - cron_wait_3sec = f'{_TEST_CRON_MODULE}.Wait3secCronJob' - cron_run_on_weekend = f'{_TEST_CRON_MODULE}.RunOnWeekendCronJob' - cron_obviously_doesnt_exist = 'ThisCronJobDoesntExost' - cron_no_code = f'{_TEST_CRON_MODULE}.NoCodeCronJob' - cron_failed_runs_notification = f'{_MAIN_CRON_MODULE}.FailedRunsNotificationCronJob' - run_on_month_days = f'{_TEST_CRON_MODULE}.RunOnMonthDaysCronJob' - run_and_remove_old_logs = f'{_TEST_CRON_MODULE}.RunEveryMinuteAndRemoveOldLogs' + cron_success = f"{_TEST_CRON_MODULE}.TestSuccessCronJob" + cron_error = f"{_TEST_CRON_MODULE}.TestErrorCronJob" + cron_5mins = f"{_TEST_CRON_MODULE}.Test5minsCronJob" + cron_5mins_with_tolerance = f"{_TEST_CRON_MODULE}.Test5minsWithToleranceCronJob" + cron_run_at_times = f"{_TEST_CRON_MODULE}.TestRunAtTimesCronJob" + cron_wait_3sec = f"{_TEST_CRON_MODULE}.Wait3secCronJob" + cron_run_on_weekend = f"{_TEST_CRON_MODULE}.RunOnWeekendCronJob" + cron_obviously_doesnt_exist = "ThisCronJobDoesntExost" + cron_no_code = f"{_TEST_CRON_MODULE}.NoCodeCronJob" + cron_failed_runs_notification = f"{_MAIN_CRON_MODULE}.FailedRunsNotificationCronJob" + run_on_month_days = f"{_TEST_CRON_MODULE}.RunOnMonthDaysCronJob" + run_and_remove_old_logs = f"{_TEST_CRON_MODULE}.RunEveryMinuteAndRemoveOldLogs" def _call(self, *args, **kwargs): - return call('runcrons', *args, **kwargs) + return call("runcrons", *args, **kwargs) def setUp(self): CronJobLog.objects.all().delete() def assertReportedRun(self, job_cls, response): - expected_log = u"[\N{HEAVY CHECK MARK}] {0}".format(job_cls.code) + expected_log = "[\N{HEAVY CHECK MARK}] {0}".format(job_cls.code) self.assertIn(expected_log, response) def assertReportedNoRun(self, job_cls, response): - expected_log = u"[ ] {0}".format(job_cls.code) + expected_log = "[ ] {0}".format(job_cls.code) self.assertIn(expected_log, response) def assertReportedFail(self, job_cls, response): - expected_log = u"[\N{HEAVY BALLOT X}] {0}".format(job_cls.code) + expected_log = "[\N{HEAVY BALLOT X}] {0}".format(job_cls.code) self.assertIn(expected_log, response) def test_success_cron(self): @@ -93,25 +92,25 @@ def test_failed_cron(self): def test_not_exists_cron(self): response = self._call(self.cron_obviously_doesnt_exist, force=True) - self.assertIn('Make sure these are valid cron class names', response) + self.assertIn("Make sure these are valid cron class names", response) self.assertIn(self.cron_obviously_doesnt_exist, response) self.assertEqual(CronJobLog.objects.all().count(), 0) - @patch('django_cron.core.logger') + @patch("django_cron.core.logger") def test_requires_code(self, mock_logger): response = self._call(self.cron_no_code, force=True) - self.assertIn('does not have a code attribute', response) + self.assertIn("does not have a code attribute", response) mock_logger.info.assert_called() @override_settings( - DJANGO_CRON_LOCK_BACKEND='django_cron.backends.lock.file.FileLock' + DJANGO_CRON_LOCK_BACKEND="django_cron.backends.lock.file.FileLock" ) def test_file_locking_backend(self): self._call(self.cron_success, force=True) self.assertEqual(CronJobLog.objects.all().count(), 1) @override_settings( - DJANGO_CRON_LOCK_BACKEND='django_cron.backends.lock.database.DatabaseLock' + DJANGO_CRON_LOCK_BACKEND="django_cron.backends.lock.database.DatabaseLock" ) def test_database_locking_backend(self): # TODO: to test it properly we would need to run multiple jobs at the same time @@ -122,23 +121,23 @@ def test_database_locking_backend(self): self.assertEqual(CronJobLock.objects.all().count(), cron_job_locks + 1) self.assertEqual(CronJobLock.objects.first().locked, False) - @patch.object(cron.TestSuccessCronJob, 'do') + @patch.object(cron.TestSuccessCronJob, "do") def test_dry_run_does_not_perform_task(self, mock_do): response = self._call(self.cron_success, dry_run=True) self.assertReportedRun(cron.TestSuccessCronJob, response) mock_do.assert_not_called() self.assertFalse(CronJobLog.objects.exists()) - @patch.object(cron.TestSuccessCronJob, 'do') + @patch.object(cron.TestSuccessCronJob, "do") def test_non_dry_run_performs_task(self, mock_do): - mock_do.return_value = 'message' + mock_do.return_value = "message" response = self._call(self.cron_success) self.assertReportedRun(cron.TestSuccessCronJob, response) mock_do.assert_called_once() self.assertEqual(1, CronJobLog.objects.count()) log = CronJobLog.objects.get() self.assertEqual( - 'message', log.message.strip() + "message", log.message.strip() ) # CronJobManager adds new line at the end of each message self.assertTrue(log.is_success) @@ -160,27 +159,27 @@ def test_runs_every_mins(self): def test_runs_every_mins_with_tolerance(self): with freeze_time("2014-01-01 00:00:00"): - call_command('runcrons', self.cron_5mins_with_tolerance) + call_command("runcrons", self.cron_5mins_with_tolerance) self.assertEqual(CronJobLog.objects.all().count(), 1) with freeze_time("2014-01-01 00:04:59"): - call_command('runcrons', self.cron_5mins_with_tolerance) + call_command("runcrons", self.cron_5mins_with_tolerance) self.assertEqual(CronJobLog.objects.all().count(), 2) with freeze_time("2014-01-01 00:05:01"): - call_command('runcrons', self.cron_5mins_with_tolerance) + call_command("runcrons", self.cron_5mins_with_tolerance) self.assertEqual(CronJobLog.objects.all().count(), 2) with freeze_time("2014-01-01 00:09:40"): - call_command('runcrons', self.cron_5mins_with_tolerance) + call_command("runcrons", self.cron_5mins_with_tolerance) self.assertEqual(CronJobLog.objects.all().count(), 2) with freeze_time("2014-01-01 00:09:54"): - call_command('runcrons', self.cron_5mins_with_tolerance) + call_command("runcrons", self.cron_5mins_with_tolerance) self.assertEqual(CronJobLog.objects.all().count(), 2) with freeze_time("2014-01-01 00:09:55"): - call_command('runcrons', self.cron_5mins_with_tolerance) + call_command("runcrons", self.cron_5mins_with_tolerance) self.assertEqual(CronJobLog.objects.all().count(), 3) def test_runs_at_time(self): @@ -203,72 +202,72 @@ def test_run_on_weekend(self): for test_date in ("2017-06-17", "2017-06-18"): # Saturday and Sunday logs_count = CronJobLog.objects.all().count() with freeze_time(test_date): - call_command('runcrons', self.cron_run_on_weekend) + call_command("runcrons", self.cron_run_on_weekend) self.assertEqual(CronJobLog.objects.all().count(), logs_count + 1) for test_date in ( - "2017-06-19", - "2017-06-20", - "2017-06-21", - "2017-06-22", - "2017-06-23", + "2017-06-19", + "2017-06-20", + "2017-06-21", + "2017-06-22", + "2017-06-23", ): # Mon-Fri logs_count = CronJobLog.objects.all().count() with freeze_time(test_date): - call_command('runcrons', self.cron_run_on_weekend) + call_command("runcrons", self.cron_run_on_weekend) self.assertEqual(CronJobLog.objects.all().count(), logs_count) def test_run_on_month_days(self): for test_date in ("2010-10-1", "2010-10-10", "2010-10-20"): logs_count = CronJobLog.objects.all().count() with freeze_time(test_date): - call_command('runcrons', self.run_on_month_days) + call_command("runcrons", self.run_on_month_days) self.assertEqual(CronJobLog.objects.all().count(), logs_count + 1) for test_date in ( - "2010-10-2", - "2010-10-9", - "2010-10-11", - "2010-10-19", - "2010-10-21", + "2010-10-2", + "2010-10-9", + "2010-10-11", + "2010-10-19", + "2010-10-21", ): logs_count = CronJobLog.objects.all().count() with freeze_time(test_date): - call_command('runcrons', self.run_on_month_days) + call_command("runcrons", self.run_on_month_days) self.assertEqual(CronJobLog.objects.all().count(), logs_count) def test_silent_produces_no_output_success(self): response = self._call(self.cron_success, silent=True) self.assertEqual(1, CronJobLog.objects.count()) - self.assertEqual('', response) + self.assertEqual("", response) def test_silent_produces_no_output_no_run(self): with freeze_time("2014-01-01 00:00:00"): response = self._call(self.cron_run_at_times, silent=True) self.assertEqual(1, CronJobLog.objects.count()) - self.assertEqual('', response) + self.assertEqual("", response) with freeze_time("2014-01-01 00:00:01"): response = self._call(self.cron_run_at_times, silent=True) self.assertEqual(1, CronJobLog.objects.count()) - self.assertEqual('', response) + self.assertEqual("", response) def test_silent_produces_no_output_failure(self): response = self._call(self.cron_error, silent=True) - self.assertEqual('', response) + self.assertEqual("", response) def test_admin(self): - password = 'test' - user = User.objects.create_superuser('test', 'test@tivix.com', password) + password = "test" + user = User.objects.create_superuser("test", "test@tivix.com", password) self.client = Client() self.client.login(username=user.username, password=password) # edit CronJobLog object self._call(self.cron_success, force=True) log = CronJobLog.objects.all()[0] - url = reverse('admin:django_cron_cronjoblog_change', args=(log.id,)) + url = reverse("admin:django_cron_cronjoblog_change", args=(log.id,)) response = self.client.get(url) - self.assertIn('Cron job logs', str(response.content)) + self.assertIn("Cron job logs", str(response.content)) def run_cronjob_in_thread(self, logs_count): self._call(self.cron_wait_3sec) @@ -322,11 +321,11 @@ def test_humanize_duration(self): test_subjects = ( ( timedelta(days=1, hours=1, minutes=1, seconds=1), - '1 day, 1 hour, 1 minute, 1 second', + "1 day, 1 hour, 1 minute, 1 second", ), - (timedelta(days=2), '2 days'), - (timedelta(days=15, minutes=4), '15 days, 4 minutes'), - (timedelta(), '< 1 second'), + (timedelta(days=2), "2 days"), + (timedelta(days=15, minutes=4), "15 days, 4 minutes"), + (timedelta(), "< 1 second"), ) for duration, humanized in test_subjects: @@ -336,36 +335,46 @@ def test_remove_old_succeeded_job_logs(self): mock_date = datetime.datetime(2022, 5, 1, 12, 0, 0) for _ in range(5): with freeze_time(mock_date): - call_command('runcrons', self.run_and_remove_old_logs) + call_command("runcrons", self.run_and_remove_old_logs) self.assertEqual(CronJobLog.objects.all().count(), 1) self.assertEqual(CronJobLog.objects.all().first().end_time, mock_date) def test_run_job_with_logs_in_future(self): mock_date_in_future = datetime.datetime(2222, 5, 1, 12, 0, 0) with freeze_time(mock_date_in_future): - call_command('runcrons', self.cron_5mins) + call_command("runcrons", self.cron_5mins) self.assertEqual(CronJobLog.objects.all().count(), 1) - self.assertEqual(CronJobLog.objects.all().first().end_time, mock_date_in_future) + self.assertEqual( + CronJobLog.objects.all().first().end_time, mock_date_in_future + ) mock_date_in_past = mock_date_in_future - timedelta(days=1000) with freeze_time(mock_date_in_past): - call_command('runcrons', self.cron_5mins) + call_command("runcrons", self.cron_5mins) self.assertEqual(CronJobLog.objects.all().count(), 2) - self.assertEqual(CronJobLog.objects.all().earliest('start_time').end_time, mock_date_in_past) + self.assertEqual( + CronJobLog.objects.all().earliest("start_time").end_time, + mock_date_in_past, + ) mock_date_in_past_plus_one_min = mock_date_in_future + timedelta(minutes=1) with freeze_time(mock_date_in_past_plus_one_min): - call_command('runcrons', self.cron_5mins) + call_command("runcrons", self.cron_5mins) self.assertEqual(CronJobLog.objects.all().count(), 2) - self.assertEqual(CronJobLog.objects.all().earliest('start_time').end_time, mock_date_in_past) + self.assertEqual( + CronJobLog.objects.all().earliest("start_time").end_time, + mock_date_in_past, + ) class TestCronLoop(TransactionTestCase): - success_cron = f'{_TEST_CRON_MODULE}.TestSuccessCronJob' + success_cron = f"{_TEST_CRON_MODULE}.TestSuccessCronJob" def _call(self, *args, **kwargs): - return call('cronloop', *args, **kwargs) + return call("cronloop", *args, **kwargs) def test_repeat_twice(self): - self._call(cron_classes=[self.success_cron, self.success_cron], repeat=2, sleep=1) + self._call( + cron_classes=[self.success_cron, self.success_cron], repeat=2, sleep=1 + ) self.assertEqual(CronJobLog.objects.all().count(), 4) diff --git a/djangocron b/djangocron new file mode 100644 index 0000000..e69de29 From 0575c2fe2d291f3982b87fcd48e028f59ad3a158 Mon Sep 17 00:00:00 2001 From: jorenham Date: Sun, 28 Jul 2024 19:03:18 +0200 Subject: [PATCH 5/8] remove empty file --- djangocron | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 djangocron diff --git a/djangocron b/djangocron deleted file mode 100644 index e69de29..0000000 From 2017e512b42183a28ee94a3bc84566e3e93f5142 Mon Sep 17 00:00:00 2001 From: jorenham Date: Wed, 7 Aug 2024 16:16:43 +0200 Subject: [PATCH 6/8] fix migrations for django 5.1 support --- django_cron/migrations/0001_initial.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/django_cron/migrations/0001_initial.py b/django_cron/migrations/0001_initial.py index 5cf8d85..bfbd456 100644 --- a/django_cron/migrations/0001_initial.py +++ b/django_cron/migrations/0001_initial.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): @@ -34,14 +34,14 @@ class Migration(migrations.Migration): ), ], ), - migrations.AlterIndexTogether( - name='cronjoblog', - index_together=set( - [ - ('code', 'is_success', 'ran_at_time'), - ('code', 'start_time', 'ran_at_time'), - ('code', 'start_time'), - ] - ), - ), + # migrations.AlterIndexTogether( + # name='cronjoblog', + # index_together=set( + # [ + # ('code', 'is_success', 'ran_at_time'), + # ('code', 'start_time', 'ran_at_time'), + # ('code', 'start_time'), + # ] + # ), + # ), ] From 5f4c82307608a03547eebb821ec6aa2c6e3a2b70 Mon Sep 17 00:00:00 2001 From: jorenham Date: Wed, 14 Aug 2024 14:19:19 +0200 Subject: [PATCH 7/8] restore original initial migration --- django_cron/migrations/0001_initial.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/django_cron/migrations/0001_initial.py b/django_cron/migrations/0001_initial.py index bfbd456..26ef67b 100644 --- a/django_cron/migrations/0001_initial.py +++ b/django_cron/migrations/0001_initial.py @@ -34,14 +34,14 @@ class Migration(migrations.Migration): ), ], ), - # migrations.AlterIndexTogether( - # name='cronjoblog', - # index_together=set( - # [ - # ('code', 'is_success', 'ran_at_time'), - # ('code', 'start_time', 'ran_at_time'), - # ('code', 'start_time'), - # ] - # ), - # ), + migrations.AlterIndexTogether( + name='cronjoblog', + index_together=set( + [ + ('code', 'is_success', 'ran_at_time'), + ('code', 'start_time', 'ran_at_time'), + ('code', 'start_time'), + ] + ), + ), ] From b4aafd801d35f31429471fc6b98572a43e6e2e0c Mon Sep 17 00:00:00 2001 From: jorenham Date: Wed, 14 Aug 2024 14:19:31 +0200 Subject: [PATCH 8/8] squash migrations on django 5.0.8 --- ..._0004_alter_cronjoblog_options_and_more.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 django_cron/migrations/0001_squashed_0004_alter_cronjoblog_options_and_more.py diff --git a/django_cron/migrations/0001_squashed_0004_alter_cronjoblog_options_and_more.py b/django_cron/migrations/0001_squashed_0004_alter_cronjoblog_options_and_more.py new file mode 100644 index 0000000..95ee318 --- /dev/null +++ b/django_cron/migrations/0001_squashed_0004_alter_cronjoblog_options_and_more.py @@ -0,0 +1,40 @@ +# Generated by Django 5.0.8 on 2024-08-14 14:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + replaces = [('django_cron', '0001_initial'), ('django_cron', '0002_remove_max_length_from_CronJobLog_message'), ('django_cron', '0003_cronjoblock'), ('django_cron', '0004_alter_cronjoblog_options_and_more')] + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='CronJobLock', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('job_name', models.CharField(max_length=200, unique=True)), + ('locked', models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name='CronJobLog', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=64)), + ('start_time', models.DateTimeField()), + ('end_time', models.DateTimeField()), + ('is_success', models.BooleanField(default=False)), + ('message', models.TextField(blank=True, default='')), + ('ran_at_time', models.TimeField(blank=True, editable=False, null=True)), + ], + options={ + 'get_latest_by': 'start_time', + 'indexes': [models.Index(fields=['code', 'start_time'], name='django_cron_code_966ed3_idx'), models.Index(fields=['code', 'start_time', 'ran_at_time'], name='django_cron_code_21f381_idx'), models.Index(fields=['code', 'is_success', 'ran_at_time'], name='django_cron_code_89ad04_idx'), models.Index(fields=['code'], name='django_cron_code_26aea9_idx'), models.Index(fields=['start_time'], name='django_cron_start_t_9e0b8f_idx'), models.Index(fields=['end_time'], name='django_cron_end_tim_c3cfdc_idx'), models.Index(fields=['ran_at_time'], name='django_cron_ran_at__aa3be6_idx')], + }, + ), + ]