Compare commits

...
Author SHA1 Message Date
Stan b0c6753462 Release v1.11.1 2026-08-26 04:46:29 +02:00
David Macario d97b27901b Fix cluster requesting hardcoded unix-only fork context (#347) 2026-08-26 04:39:33 +02:00
Stan Triepels fa935f89ab Show test coverage in github pull request comment (#344) 2026-08-14 17:26:55 +02:00
Stan Triepels 9e576b85e6 Swap localstorage to ministack (#343) 2026-08-14 05:37:10 +02:00
Stan 1941f1e3f0 Release v1.11.0 2026-08-10 04:17:05 +02:00
tugofpeasandTug of Peas 97ea3c89a5 Postpone SECRET_KEY evaluation to the qcluster command execution (#332)
* Suppress ImproperlyConfigured within Conf, allowing the exception to be raised specifically during the 'qcluster' command instead.

This ensures build scripts and workflows can freely execute other Django commands (e.g., 'python manage.py collectstatic') without failing due to a missing/empty secret key.

* Closes #279

Co-authored-by: Tug of Peas <19413623+tugofpeas@users.noreply.github.com>
2026-08-09 03:20:51 +02:00
Stan Triepels b2651ce86c Add django 6.1 support (#340) 2026-08-06 03:00:32 +02:00
15 changed files with 139 additions and 72 deletions
+38 -24
View File
@@ -6,6 +6,21 @@ on:
pull_request: pull_request:
branches: branches:
- master - master
# for code coverage comment
permissions:
contents: read
pull-requests: write
env:
MONGO_HOST: "127.0.0.1"
REDIS_HOST: "127.0.0.1"
AWS_ENDPOINT_URL: http://localhost:4566
AWS_REGION: "us-east-1"
AWS_ACCESS_KEY_ID: "test"
AWS_SECRET_ACCESS_KEY: "test"
AWS_DEFAULT_REGION: "us-east-1"
jobs: jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -35,12 +50,18 @@ jobs:
django: django:
- "5.2" - "5.2"
- "6.0" - "6.0"
- "6.1"
exclude: exclude:
# django 6.0 does not support earlier than 3.12 # django 6.0 does not support earlier than 3.12
- python-version: "3.10" - python-version: "3.10"
django: "6.0" django: "6.0"
- python-version: "3.11" - python-version: "3.11"
django: "6.0" django: "6.0"
# django 6.1 does not support earlier than 3.12
- python-version: "3.10"
django: "6.1"
- python-version: "3.11"
django: "6.1"
services: services:
mongodb: mongodb:
@@ -62,6 +83,13 @@ jobs:
ports: ports:
- 6379:6379 - 6379:6379
options: --entrypoint redis-server options: --entrypoint redis-server
aws:
image: ministackorg/ministack:1.4.16
env:
SERVICES: sqs
MINISTACK_HOST: aws
ports:
- 4566:4566
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
@@ -73,30 +101,16 @@ jobs:
python -m pip install uv==0.12.1 python -m pip install uv==0.12.1
uv sync --extra testing --python ${{ matrix.python-version }} uv sync --extra testing --python ${{ matrix.python-version }}
uv pip install "django~=${{ matrix.django }}a1" uv pip install "django~=${{ matrix.django }}a1"
- name: Create SQS queue
run: aws sqs create-queue --queue-name testing
- name: Run Tests - name: Run Tests
run: | run: |
uv run pytest --cov=./django_q --cov-report=xml uv run pytest --cov=./django_q --cov-report=xml --junitxml=pytest.xml --cov-report=term-missing:skip-covered | tee pytest-coverage.txt
env:
MONGO_HOST: "127.0.0.1"
REDIS_HOST: "127.0.0.1"
- name: Upload to coveralls
run: |
python -m pip install coveralls
coveralls --service=github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_FLAG_NAME: python-${{ matrix.python-version }}-django-${{ matrix.django }}
COVERALLS_PARALLEL: true
finish: - name: Coverage comment
needs: test if: matrix.python-version == '3.13' && matrix.django == '5.2' && github.event_name == 'pull_request'
runs-on: ubuntu-latest uses: MishaKav/pytest-coverage-comment@v1
container: python:3.11-bookworm with:
steps: pytest-coverage-path: ./pytest-coverage.txt
- name: Upload to coveralls junitxml-path: ./pytest.xml
run: | report-only-changed-files: true
python -m pip install --upgrade pip
python -m pip install coveralls
coveralls --service=github --finish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+16
View File
@@ -1,5 +1,21 @@
# Changelog # Changelog
## [v1.11.1](https://github.com/django-q2/django-q2/tree/v1.11.1) (2026-08-26)
- Swap localstorage to ministack (#343) https://github.com/django-q2/django-q2/pull/343
- Show test coverage in github pull request comment (#344) https://github.com/django-q2/django-q2/pull/344
- Fix cluster requesting hardcoded unix-only fork context (#347) https://github.com/django-q2/django-q2/pull/347
## [v1.11.0](https://github.com/django-q2/django-q2/tree/v1.11.0) (2026-08-10)
- AttributeError when start_event is None, and guard process faster stop (#305) https://github.com/django-q2/django-q2/pull/305
- Add croniter as optional extra. (#336) https://github.com/django-q2/django-q2/pull/336
- Migrate poetry to uv (#337) https://github.com/django-q2/django-q2/pull/337
- Add support for python 3.13 and 3.14 (#338) https://github.com/django-q2/django-q2/pull/338
- Add django 6.1 support (#340) https://github.com/django-q2/django-q2/pull/340
- Postpone SECRET_KEY evaluation to the qcluster command execution (#332) https://github.com/django-q2/django-q2/pull/332
## [v1.10.0](https://github.com/django-q2/django-q2/tree/v1.10.0) (2026-05-01) ## [v1.10.0](https://github.com/django-q2/django-q2/tree/v1.10.0) (2026-05-01)
- fix: Fix incorrect signal import (#308) https://github.com/django-q2/django-q2/pull/308 - fix: Fix incorrect signal import (#308) https://github.com/django-q2/django-q2/pull/308
+2 -2
View File
@@ -35,13 +35,13 @@ See the `changelog <https://github.com/GDay/django-q2/blob/master/CHANGELOG.md>`
Requirements Requirements
~~~~~~~~~~~~ ~~~~~~~~~~~~
- `Django <https://www.djangoproject.com>`__ 5.2 and 6.0 - `Django <https://www.djangoproject.com>`__ 5.2, 6.0 and 6.1
- `Django-picklefield <https://github.com/gintas/django-picklefield>`__ - `Django-picklefield <https://github.com/gintas/django-picklefield>`__
Tested with: Tested with:
* Python 3.10 to 3.14. * Python 3.10 to 3.14.
* Django 5.2 and 6.0. * Django 5.2, 6.0 and 6.1.
Brokers Brokers
~~~~~~~ ~~~~~~~
-19
View File
@@ -1,19 +0,0 @@
#!/bin/bash
# Note that this file needs to have the executable bit set for it to work with later localstack implementations.
export DEFAULT_REGION=us-west-2
create_sqs() {
QUEUE_NAME="$1"
TIMEOUT=${2:-60}
DL_QUEUE_URL=$(awslocal sqs create-queue --queue-name "dl-$QUEUE_NAME" --query QueueUrl --output text)
echo ">>> Created $DL_QUEUE_URL queue!"
DL_QUEUE_ARN=$(awslocal sqs get-queue-attributes --queue-url "$DL_QUEUE_URL" --attribute-names QueueArn --query Attributes.QueueArn --output text)
awslocal sqs create-queue --queue-name "$QUEUE_NAME" --attributes '{
"RedrivePolicy": "{\"deadLetterTargetArn\": \"'"$DL_QUEUE_ARN"'\",\"maxReceiveCount\":\"3\"}",
"VisibilityTimeout": "'"$TIMEOUT"'"
}'
}
# Create SQS queues
create_sqs testing
@@ -0,0 +1 @@
aws sqs create-queue --queue-name testing
+1 -1
View File
@@ -1,3 +1,3 @@
VERSION = (1, 10, 0) VERSION = (1, 11, 1)
__all__ = ["conf", "cluster", "models", "tasks"] __all__ = ["conf", "cluster", "models", "tasks"]
+4 -1
View File
@@ -41,7 +41,10 @@ from django_q.worker import worker
def get_mp_context(): def get_mp_context():
return multiprocessing.get_context("fork") if "fork" in multiprocessing.get_all_start_methods():
return multiprocessing.get_context("fork")
else:
return multiprocessing.get_context()
class Cluster: class Cluster:
+7 -1
View File
@@ -7,6 +7,7 @@ from multiprocessing import cpu_count
from warnings import warn from warnings import warn
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_q.queues import Queue from django_q.queues import Queue
@@ -211,7 +212,12 @@ class Conf:
# Use the secret key for package signing # Use the secret key for package signing
# Django itself should raise an error if it's not configured # Django itself should raise an error if it's not configured
SECRET_KEY = settings.SECRET_KEY # but suppress the exception early to allow other parts of the app to still run.
# For example any "python manage.py ..." command still runs even if SECRET_KEY is not set.
try:
SECRET_KEY = settings.SECRET_KEY
except ImproperlyConfigured:
SECRET_KEY = None
# The redis stats key # The redis stats key
Q_STAT = f"django_q:{PREFIX}:cluster" Q_STAT = f"django_q:{PREFIX}:cluster"
+3
View File
@@ -1,5 +1,6 @@
import os import os
from django.conf import settings
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
@@ -28,6 +29,8 @@ class Command(BaseCommand):
) )
def handle(self, *args, **options): def handle(self, *args, **options):
# Ensure that the cluster starts only if the SECRET_KEY is set, as it is required for signing.
settings.SECRET_KEY
# Set alternative cluster_name before creating the cluster (cluster_name is broker's queue_name, too) # Set alternative cluster_name before creating the cluster (cluster_name is broker's queue_name, too)
cluster_name = options.get("cluster_name") cluster_name = options.get("cluster_name")
if cluster_name: if cluster_name:
+3 -6
View File
@@ -172,9 +172,6 @@ def test_ironmq(monkeypatch):
broker.delete_queue() broker.delete_queue()
@pytest.mark.skipif(
not os.getenv("AWS_ACCESS_KEY_ID"), reason="requires AWS credentials"
)
def test_sqs(monkeypatch): def test_sqs(monkeypatch):
monkeypatch.setattr( monkeypatch.setattr(
Conf, Conf,
@@ -188,6 +185,7 @@ def test_sqs(monkeypatch):
) )
# check broker # check broker
broker = get_broker(list_key="testing") broker = get_broker(list_key="testing")
broker.purge_queue()
assert "receive_message_wait_time_seconds" in Conf.SQS assert "receive_message_wait_time_seconds" in Conf.SQS
assert "aws_region" in Conf.SQS assert "aws_region" in Conf.SQS
assert broker.ping() is True assert broker.ping() is True
@@ -224,8 +222,9 @@ def test_sqs(monkeypatch):
assert broker.dequeue() is None assert broker.dequeue() is None
# fail # fail
broker.enqueue("test") broker.enqueue("test")
task = None
while task is None: while task is None:
task = broker.dequeue()[0] task = broker.dequeue()
broker.fail(task[0][0]) broker.fail(task[0][0])
# bulk test # bulk test
for _ in range(10): for _ in range(10):
@@ -235,8 +234,6 @@ def test_sqs(monkeypatch):
for task in tasks: for task in tasks:
assert task is not None assert task is not None
broker.acknowledge(task[0]) broker.acknowledge(task[0])
# duplicate acknowledge
broker.acknowledge(task[0])
assert broker.lock_size() == 0 assert broker.lock_size() == 0
# delete queue # delete queue
broker.enqueue("test") broker.enqueue("test")
+49 -1
View File
@@ -1,3 +1,4 @@
import multiprocessing
import os import os
import signal import signal
import sys import sys
@@ -13,7 +14,7 @@ import pytest
from django.utils import timezone from django.utils import timezone
from django_q.brokers import Broker, get_broker from django_q.brokers import Broker, get_broker
from django_q.cluster import Cluster, Sentinel from django_q.cluster import Cluster, Sentinel, get_mp_context
from django_q.conf import Conf from django_q.conf import Conf
from django_q.humanhash import DEFAULT_WORDLIST, uuid from django_q.humanhash import DEFAULT_WORDLIST, uuid
from django_q.models import Success, Task from django_q.models import Success, Task
@@ -59,6 +60,53 @@ def broker(monkeypatch):
return get_broker() return get_broker()
def test_get_mp_context_prefers_fork_when_available(monkeypatch):
monkeypatch.setattr(
multiprocessing,
"get_all_start_methods",
lambda: ["fork", "spawn", "forkserver"],
)
calls = []
class DummyContext:
def get_start_method(self):
return "fork"
def fake_get_context(method=None):
calls.append(method)
return DummyContext()
monkeypatch.setattr(multiprocessing, "get_context", fake_get_context)
assert get_mp_context().get_start_method() == "fork"
assert calls == ["fork"]
def test_get_mp_context_falls_back_to_platform_default_without_fork(monkeypatch):
"""
Regression test: get_mp_context() used to hardcode the unix-only "fork"
context, which raises ValueError on platforms (e.g. Windows) that don't
support it. It should instead defer to the platform's default context
whenever "fork" isn't available.
"""
monkeypatch.setattr(multiprocessing, "get_all_start_methods", lambda: ["spawn"])
calls = []
real_get_context = multiprocessing.get_context
def fake_get_context(method=None):
calls.append(method)
return real_get_context(method)
monkeypatch.setattr(multiprocessing, "get_context", fake_get_context)
get_mp_context()
# Must ask for the platform default (no explicit method), should never be equal to "fork"
assert calls == [None]
def test_redis_connection(broker): def test_redis_connection(broker):
assert broker.ping() is True assert broker.ping() is True
+2 -2
View File
@@ -73,9 +73,9 @@ author = "Ilan Steemers, Stan Triepels"
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = "1.10" version = "1.11"
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = "1.10.0" release = "1.11.1"
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
+1 -1
View File
@@ -27,7 +27,7 @@ Features
- Rollbar and Sentry support - Rollbar and Sentry support
Django Q2 is tested with: Python 3.8, 3.9, 3.10, 3.11 and 3.12. Works with Django 4.2.x, 5.x and 6.0.x Django Q2 is tested with: python 3.10, 3.11, 3.12, 3.13 and 3.14. Works with Django 5.2, 6.0 and 6.1
Currently available in English, German and French. Currently available in English, German and French.
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "uv_build"
[project] [project]
name = "django-q2" name = "django-q2"
version = "1.10.0" version = "1.11.1"
packages = [ packages = [
{ include = "django_q" }, { include = "django_q" },
] ]
@@ -33,11 +33,11 @@ classifiers = [
"Operating System :: MacOS", "Operating System :: MacOS",
"Programming Language :: Python", "Programming Language :: Python",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP",
"Topic :: System :: Distributed Computing", "Topic :: System :: Distributed Computing",
"Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries :: Python Modules",
+10 -12
View File
@@ -15,20 +15,18 @@ services:
aws: aws:
container_name: aws container_name: aws
image: localstack/localstack:3.4.0 image: ministackorg/ministack:1.4.16
ports: ports:
- "127.0.0.1:4566:4566" # LocalStack Gateway - "127.0.0.1:4566:4566"
- "127.0.0.1:4510-4559:4510-4559" # External services port range
environment: environment:
AWS_DEFAULT_REGION: ${AWS_DEFAULT_REGION:-us-west-2} MINISTACK_HOST: aws
DEFAULT_REGION: ${AWS_DEFAULT_REGION:-us-west-2}
SQS_ENDPOINT_STRATEGY: path
SERVICES: sqs SERVICES: sqs
LOCALSTACK_HOST: aws
DEBUG: 1
LS_LOG: trace
volumes: volumes:
- ./containers/localstack:/etc/localstack/init/ready.d - ./containers/ministack:/docker-entrypoint-initaws.d
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:4566/_ministack/ready"]
interval: 2s
retries: 15
networks: networks:
- main - main
@@ -38,10 +36,10 @@ services:
context: . context: .
environment: environment:
AWS_ENDPOINT_URL: http://aws:4566 AWS_ENDPOINT_URL: http://aws:4566
AWS_REGION: ${AWS_REGION:-us-west-2} AWS_REGION: ${AWS_REGION:-us-east-1}
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-test} AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-test}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-test} AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-test}
AWS_DEFAULT_REGION: ${AWS_DEFAULT_REGION:-us-west-2} AWS_DEFAULT_REGION: ${AWS_DEFAULT_REGION:-us-east-1}
volumes: volumes:
- .:/app - .:/app
depends_on: depends_on: