diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index b7c00b4..9a14c78 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -4,62 +4,60 @@ name: Python package on: - push: - branches: [ develop ] - pull_request: - branches: [ develop ] + push: + branches: [ develop ] + pull_request: + branches: [ develop ] jobs: - lint: - name: Check code style - runs-on: ubuntu-latest - container: python:3-slim - steps: - - uses: actions/checkout@v2 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install flake8 black - - name: Blacken code - run: black . --safe --quiet - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - test: - name: Test (Python, Django) - runs-on: ubuntu-latest - needs: lint - strategy: - matrix: - python-version: [ 3.8, 3.9, "3.10" ] - django-version: [ "3.2", "4.0" ] - env: - PYTHON: ${{ matrix.python-version }} - DJANGO: ${{ matrix.django-version }} - steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install --upgrade django~=${{ matrix.django-version }} - if [ -f requirements/local.txt ]; then pip install -r requirements/local.txt; fi - - name: Test with pytest and update coverage - run: | - coverage run -m pytest - coverage xml - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: coverage.xml - flags: unittests - env_vars: PYTHON, DJANGO - fail_ci_if_error: true - verbose: true + lint: + name: Check code style + runs-on: ubuntu-latest + container: python:3-slim + steps: + - uses: actions/checkout@v2 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install flake8 black + - name: Blacken code + run: black . --safe --quiet + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + + tests: + name: Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + matrix: + python-version: + - '3.9' + - '3.10' + - '3.11' + - '3.12' + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: 'requirements/*.txt' + - name: Upgrade packaging tools + run: python -m pip install --upgrade pip setuptools virtualenv wheel + - name: Install dependencies + run: python -m pip install --upgrade codecov tox + - name: Run tox targets for ${{ matrix.python-version }} + run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d . | cut -f 1 -d '-') + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: coverage.xml + flags: unittests + env_vars: TOXENV, PYTHON, DJANGO + fail_ci_if_error: true + verbose: true \ No newline at end of file diff --git a/AUTHORS b/AUTHORS index 55f0887..edef57f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,3 +1,4 @@ - Jarek Glowacki - Stas Kaledin - John Cass +- JAEGYUN JUNG \ No newline at end of file diff --git a/django_apscheduler/jobstores.py b/django_apscheduler/jobstores.py index 6c05434..cdca9f3 100644 --- a/django_apscheduler/jobstores.py +++ b/django_apscheduler/jobstores.py @@ -248,7 +248,7 @@ def update_job(self, job: AppSchedulerJob): # Acquire lock for update with transaction.atomic(): try: - db_job = DjangoJob.objects.get(id=job.id) + db_job = DjangoJob.objects.select_for_update().get(id=job.id) db_job.next_run_time = get_django_internal_datetime(job.next_run_time) db_job.job_state = pickle.dumps( @@ -262,10 +262,11 @@ def update_job(self, job: AppSchedulerJob): @util.retry_on_db_operational_error def remove_job(self, job_id: str): - try: - DjangoJob.objects.get(id=job_id).delete() - except DjangoJob.DoesNotExist: - raise JobLookupError(job_id) + with transaction.atomic(): + try: + DjangoJob.objects.select_for_update().get(id=job_id).delete() + except DjangoJob.DoesNotExist: + raise JobLookupError(job_id) @util.retry_on_db_operational_error def remove_all_jobs(self): diff --git a/docs/changelog.md b/docs/changelog.md index 02eae40..0824565 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,22 @@ This changelog is used to track all major changes to django-apscheduler. +## v0.7.0 (2024-09-28) + +**Enhancements** + +- Drop support for Python 3.8 and Django 3.2. +- Add support for Python 3.9 to 3.12, Django 4.2 to 5.1 +- Bump dependencies to latest available versions. +- Remove pytest-pythonpath as it is no longer needed. +- Add tox.ini file to run tests against all supported versions of Python and Django. +- Update CI configuration to use tox. + +**Fixes** + +- Take a database lock before updating / deleting job store entries to prevent duplicate key violation errors (thanks + @calledbert). + ## v0.6.2 (2022-03-06) **Fixes** diff --git a/pytest.ini b/pytest.ini index 1562108..8b53b59 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,9 +1,5 @@ [pytest] +pythonpath = tests/ python_files = tests.py test_*.py *_tests.py - -;https://github.com/bigsassy/pytest-pythonpath#usage -python_paths = tests/ - addopts = --ds=tests.settings - norecursedirs = django_apscheduler diff --git a/requirements/local.txt b/requirements/local.txt index fd1375c..c195e68 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -2,12 +2,12 @@ # Testing # ------------------------------------------------------------------------------ -pytest~=6.2 # https://github.com/pytest-dev/pytest +tox~=4.19 # https://github.com/tox-dev/tox +pytest~=7.0 # https://github.com/pytest-dev/pytest pytest-sugar~=0.9 # https://github.com/Frozenball/pytest-sugar -pytest-pythonpath~=0.7 # https://github.com/bigsassy/pytest-pythonpath # Django # ------------------------------------------------------------------------------ django # https://www.djangoproject.com/ -django-coverage-plugin~=2.0 # https://github.com/nedbat/django_coverage_plugin -pytest-django~=4.5 # https://github.com/pytest-dev/pytest-django +django-coverage-plugin~=3.1 # https://github.com/nedbat/django_coverage_plugin +pytest-django~=4.9 # https://github.com/pytest-dev/pytest-django diff --git a/setup.py b/setup.py index 511f0ab..ec9cf44 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name="django-apscheduler", - version="0.6.2", + version="0.7.0", description="APScheduler for Django", long_description=long_description, long_description_content_type="text/markdown", @@ -26,17 +26,19 @@ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "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", - "Framework :: Django :: 3.2", - "Framework :: Django :: 4.0", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", + "Framework :: Django :: 5.1", ], keywords="django apscheduler django-apscheduler", packages=find_packages(exclude=("tests",)), install_requires=[ - "django>=3.2", + "django>=4.2", "apscheduler>=3.2,<4.0", ], zip_safe=False, diff --git a/tests/test_jobstores.py b/tests/test_jobstores.py index b721e3a..c76a044 100644 --- a/tests/test_jobstores.py +++ b/tests/test_jobstores.py @@ -237,7 +237,7 @@ def test_update_job_does_retry_on_db_operational_error(self, jobstore, create_jo with mock.patch.object(db.connection, "close") as close_mock: with pytest.raises(db.OperationalError, match="Some DB-related error"): with mock.patch( - "django_apscheduler.jobstores.DjangoJob.objects.get", + "django_apscheduler.jobstores.DjangoJob.objects.select_for_update", side_effect=conftest.raise_db_operational_error, ): jobstore.update_job(job) @@ -249,7 +249,7 @@ def test_remove_job_does_retry_on_db_operational_error(self, jobstore): with mock.patch.object(db.connection, "close") as close_mock: with pytest.raises(db.OperationalError, match="Some DB-related error"): with mock.patch( - "django_apscheduler.jobstores.DjangoJob.objects.get", + "django_apscheduler.jobstores.DjangoJob.objects.select_for_update", side_effect=conftest.raise_db_operational_error, ): jobstore.remove_job("some job") diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..86030ab --- /dev/null +++ b/tox.ini @@ -0,0 +1,20 @@ +[tox] +envlist = + {py39}-{django42} + {py310}-{django42,django50,django51,djangomain} + {py311}-{django42,django50,django51,djangomain} + {py312}-{django42,django50,django51,djangomain} + +[testenv] +setenv = + PYTHONPATH = {toxinidir} +deps = + pytest + pytest-cov + pytest-django + django42: django>=4.2,<5.0 + django50: django>=5.0,<5.1 + django51: django>=5.1,<5.2 + djangomain: https://github.com/django/django/archive/main.tar.gz +commands = + pytest --cov=./ --cov-report=xml \ No newline at end of file