Compare commits

...
15 Commits
Author SHA1 Message Date
Stan 2c456cd0aa Release v1.7.2 2024-09-10 00:16:18 +02:00
Stan Triepels 8f54e5d5ae Add twine check and fix readme (#216)
* Add twine check

* add poetry to install

* move up remove black/flake formatting

* fix path

* fix?

* fix rst?

* add rstcheck

* test all files?

* all

* remove complaining image

* only check readme

* remove not needed dependencies
2024-09-10 00:10:14 +02:00
Stan 689c881178 Release v1.7.1 2024-09-08 04:20:13 +02:00
Stan 923c5daa73 Release v1.7.0 2024-09-08 03:52:37 +02:00
Stan Triepels 69e794f1af Add index on succeeded tasks (#164)
* Add index on task table

* also add models file

* add make commands and limit index

* format
2024-09-08 03:37:28 +02:00
Stan Triepels 2191897825 Add example project (#215)
* add example project

* format

* fix format
2024-09-08 03:13:49 +02:00
Andrii Yurchuk af505e8d70 Call mark_process_dead on worker pid if prometheus_client is installed (#212) 2024-09-03 22:29:00 +02:00
kimihito 48123af09d chore: Add Support Django 5.1 (#207) 2024-08-13 14:14:40 +02:00
kimihito faaa57be93 chore: Update CI for Django 4.2 Python 3.12 support (#208) 2024-08-13 14:14:11 +02:00
jv3ga 5015d8ee4d fix: Oracle ORM backend compatibility #180 (#186) 2024-08-13 04:40:54 +02:00
Stan Triepels cb253357fb Fix repeating task after timeout (#184) 2024-06-23 22:36:29 +02:00
GDay efa0d6d9e4 ignore fuff formating 2024-05-11 03:06:58 +02:00
Stan Triepels ad4d24e17c Replace black/isort with ruff (#188)
* Replace black/isort with ruff

* fix typo
2024-05-11 03:04:32 +02:00
Stan Triepels 1d1d0a9aa5 Fix max attempts for value 1 (#185) 2024-05-06 17:28:27 +02:00
Stan Triepels 5e4aa65f62 Remove support for Django 3.2 and 4.1 (#183) 2024-05-05 00:42:09 +02:00
59 changed files with 1798 additions and 1642 deletions
+2
View File
@@ -1,2 +1,4 @@
# flake8, black, isort # flake8, black, isort
b1d000d007f3f77069719523268a0c6256dc0860 b1d000d007f3f77069719523268a0c6256dc0860
# move to ruff formatting/linting
ad4d24e17c9424b17cd8ae65c2def7ecc74e63c1
+25 -20
View File
@@ -7,29 +7,38 @@ on:
branches: branches:
- master - master
jobs: jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Lint with ruff
run: |
pipx install ruff==0.4.10
ruff format . --check && ruff check .
- name: Check twine
run: |
python -m pip install twine poetry rstcheck
poetry build
rstcheck README.rst
twine check dist/*
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12.0-rc.3" ] python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ]
django: [ "3.2", "4.1", "4.2", "5.0a1"] django: [ "4.2", "5.0", "5.1"]
exclude: exclude:
# django 5.1 does not support 3.8 and 3.9
- python-version: "3.8"
django: "5.1"
- python-version: "3.9"
django: "5.1"
# django 5.0 does not support 3.8 and 3.9 # django 5.0 does not support 3.8 and 3.9
- python-version: "3.8" - python-version: "3.8"
django: "5.0a1" django: "5.0"
- python-version: "3.9" - python-version: "3.9"
django: "5.0a1" django: "5.0"
# django 4.2 does not support 3.12
- python-version: "3.12.0-rc.3"
django: "4.2"
# django 4.1 does not support 3.12
- python-version: "3.12.0-rc.3"
django: "4.1"
# django 3.2 does not support 3.11 and 3.12
- python-version: "3.11"
django: "3.2"
- python-version: "3.12.0-rc.3"
django: "3.2"
services: services:
disque: disque:
@@ -90,11 +99,7 @@ jobs:
- name: Upload to coveralls - name: Upload to coveralls
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
python -m pip install coveralls flake8 black python -m pip install coveralls
coveralls --service=github --finish coveralls --service=github --finish
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check flake8/black
run: |
flake8 .
black --check .
+24
View File
@@ -1,5 +1,29 @@
# Changelog # Changelog
## [v1.7.2](https://github.com/django-q2/django-q2/tree/v1.7.2) (2024-09-09)
- Fix twine check
## [v1.7.1](https://github.com/django-q2/django-q2/tree/v1.7.1) (2024-09-08)
- Fixed date of v1.7.0
- Fixed README.rst formatting which is blocking release of latest version
## [v1.7.0](https://github.com/django-q2/django-q2/tree/v1.7.0) (2024-09-08)
**Merged pull requests:**
- Remove support for Django 3.2 and 4.1 https://github.com/django-q2/django-q2/pull/183
- Fix max attempts for value 1 https://github.com/django-q2/django-q2/pull/185
- Replace black/isort with ruff https://github.com/django-q2/django-q2/pull/188
- Fix repeating task after timeout https://github.com/django-q2/django-q2/pull/184
- fix: Oracle ORM backend compatibility #180 https://github.com/django-q2/django-q2/pull/186
- chore: Update CI for Django 4.2 Python 3.12 support https://github.com/django-q2/django-q2/pull/208
- chore: Add Support Django 5.1 https://github.com/django-q2/django-q2/pull/207
- Call mark_process_dead on worker pid if prometheus_client is installed https://github.com/django-q2/django-q2/pull/212
- Add example project https://github.com/django-q2/django-q2/pull/215
- Add index on succeeded tasks https://github.com/django-q2/django-q2/pull/164
## [v1.6.2](https://github.com/django-q2/django-q2/tree/v1.6.2) (2024-03-05) ## [v1.6.2](https://github.com/django-q2/django-q2/tree/v1.6.2) (2024-03-05)
**Merged pull requests:** **Merged pull requests:**
+1 -1
View File
@@ -1,4 +1,4 @@
FROM python:3.9 FROM python:3.12
ENV PYTHONUNBUFFERED 1 ENV PYTHONUNBUFFERED 1
RUN mkdir -p /app RUN mkdir -p /app
+2 -2
View File
@@ -8,10 +8,10 @@ ENV PYTHONUNBUFFERED 1
ENV SHELL /bin/bash ENV SHELL /bin/bash
# Upgrades pip # Upgrades pip
RUN pip install --upgrade pip RUN pip install -U pip setuptools
# Install poetry # Install poetry
RUN pip install poetry RUN pip install poetry==1.8.2
WORKDIR /app WORKDIR /app
+21
View File
@@ -0,0 +1,21 @@
dev:
docker compose -f web-docker-compose.yaml up
test:
docker-compose -f test-services-docker-compose.yaml run --rm django-q2 poetry run pytest
shell:
docker-compose -f test-services-docker-compose.yaml run --rm django-q2 poetry run python manage.py shell
makemigrations:
docker-compose -f test-services-docker-compose.yaml run --rm django-q2 poetry run python manage.py makemigrations
migrate:
docker-compose -f test-services-docker-compose.yaml run --rm django-q2 poetry run python manage.py migrate
createsuperuser:
docker compose -f web-docker-compose.yaml run --rm web python manage.py createsuperuser
format:
docker compose -f test-services-docker-compose.yaml run --rm django-q2 poetry run ruff format .
docker compose -f test-services-docker-compose.yaml run --rm django-q2 poetry run ruff check . --fix
+21 -14
View File
@@ -1,7 +1,7 @@
A multiprocessing distributed task queue for Django A multiprocessing distributed task queue for Django
--------------------------------------------------- ---------------------------------------------------
|image0| |image1| |docs| |downloads| |image0| |image1| |downloads|
Django Q2 is a fork of Django Q. Big thanks to Ilan Steemers for starting this project. Unfortunately, development has stalled since June 2021. Django Q2 is the new updated version of Django Q, with dependencies updates, docs updates and several bug fixes. Original repository: https://github.com/Koed00/django-q Django Q2 is a fork of Django Q. Big thanks to Ilan Steemers for starting this project. Unfortunately, development has stalled since June 2021. Django Q2 is the new updated version of Django Q, with dependencies updates, docs updates and several bug fixes. Original repository: https://github.com/Koed00/django-q
@@ -35,10 +35,10 @@ See the `changelog <https://github.com/GDay/django-q2/blob/master/CHANGELOG.md>`
Requirements Requirements
~~~~~~~~~~~~ ~~~~~~~~~~~~
- `Django <https://www.djangoproject.com>`__ > = 3.2 - `Django <https://www.djangoproject.com>`__ > = 4.2
- `Django-picklefield <https://github.com/gintas/django-picklefield>`__ - `Django-picklefield <https://github.com/gintas/django-picklefield>`__
Tested with: Python 3.8, 3.9, 3.10, 3.11 and 3.12. Works with Django 3.2.X, 4.1.X, 4.2.X and 5.0.X Tested with: Python 3.8, 3.9, 3.10, 3.11 and 3.12. Works with Django 4.2.X and 5.0.X
Brokers Brokers
~~~~~~~ ~~~~~~~
@@ -197,6 +197,23 @@ Admin page or directly from your code:
For more info check the `Schedules <https://django-q2.readthedocs.org/en/latest/schedules.html>`__ documentation. For more info check the `Schedules <https://django-q2.readthedocs.org/en/latest/schedules.html>`__ documentation.
Development
~~~~~~~~~~~
There is an example project that you can use to develop with. Docker (compose) is being used to set everything up.
Please note that you will have to restart the django-q container when changes have been made to tasks or django-q.
You can start the example project with:
.. code:: bash
make dev
Create a superuser with:
.. code:: bash
make createsuperuser
Testing Testing
~~~~~~~ ~~~~~~~
@@ -204,7 +221,7 @@ Running tests is easy with docker compose, it will also start the necessary data
.. code:: bash .. code:: bash
docker-compose -f test-services-docker-compose.yaml run --rm django-q2 poetry run pytest make test
Locale Locale
~~~~~~ ~~~~~~
@@ -212,12 +229,6 @@ Locale
Currently available in English, German, Turkish, and French. Currently available in English, German, Turkish, and French.
Translation pull requests are always welcome. Translation pull requests are always welcome.
Todo
~~~~
- Better tests and coverage
- Less dependencies?
Acknowledgements Acknowledgements
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~
@@ -234,9 +245,5 @@ Acknowledgements
:target: https://github.com/GDay/django-q2/actions?query=workflow%3Atests :target: https://github.com/GDay/django-q2/actions?query=workflow%3Atests
.. |image1| image:: https://coveralls.io/repos/github/GDay/django-q2/badge.svg?branch=master .. |image1| image:: https://coveralls.io/repos/github/GDay/django-q2/badge.svg?branch=master
:target: https://coveralls.io/github/GDay/django-q2?branch=master :target: https://coveralls.io/github/GDay/django-q2?branch=master
.. |docs| image:: https://readthedocs.org/projects/docs/badge/?version=latest
:alt: Documentation Status
:scale: 100
:target: https://django-q2.readthedocs.org/
.. |downloads| image:: https://img.shields.io/pypi/dm/django-q2 .. |downloads| image:: https://img.shields.io/pypi/dm/django-q2
:target: https://img.shields.io/pypi/dm/django-q2 :target: https://img.shields.io/pypi/dm/django-q2
+1 -1
View File
@@ -1,6 +1,6 @@
import django import django
VERSION = (1, 6, 2) VERSION = (1, 7, 2)
if django.VERSION < (3, 2): if django.VERSION < (3, 2):
default_app_config = "django_q.apps.DjangoQConfig" default_app_config = "django_q.apps.DjangoQConfig"
+1
View File
@@ -1,4 +1,5 @@
"""Admin module for Django.""" """Admin module for Django."""
from django.contrib import admin from django.contrib import admin
from django.db.models.expressions import OuterRef, Subquery from django.db.models.expressions import OuterRef, Subquery
from django.urls import reverse from django.urls import reverse
+1 -1
View File
@@ -182,7 +182,7 @@ def get_broker(list_key: str = None) -> Broker:
return ironmq.IronMQBroker(list_key=list_key) return ironmq.IronMQBroker(list_key=list_key)
# SQS # SQS
elif type(Conf.SQS) == dict: elif isinstance(Conf.SQS, dict):
from django_q.brokers import aws_sqs from django_q.brokers import aws_sqs
return aws_sqs.Sqs(list_key=list_key) return aws_sqs.Sqs(list_key=list_key)
+10 -1
View File
@@ -21,7 +21,14 @@ from django.utils.translation import gettext_lazy as _
# Local # Local
from django_q.brokers import Broker, get_broker from django_q.brokers import Broker, get_broker
from django_q.conf import Conf, get_ppid, logger, psutil, setproctitle from django_q.conf import (
Conf,
get_ppid,
logger,
prometheus_multiprocess,
psutil,
setproctitle,
)
from django_q.humanhash import humanize from django_q.humanhash import humanize
from django_q.monitor import monitor from django_q.monitor import monitor
from django_q.pusher import pusher from django_q.pusher import pusher
@@ -223,6 +230,8 @@ class Sentinel:
% {"name": process.name} % {"name": process.name}
) )
else: else:
if prometheus_multiprocess:
prometheus_multiprocess.mark_process_dead(process.pid)
self.pool.remove(process) self.pool.remove(process)
self.spawn_worker() self.spawn_worker()
if process.timer.value == 0: if process.timer.value == 0:
+5 -1
View File
@@ -38,6 +38,11 @@ try:
except ModuleNotFoundError: except ModuleNotFoundError:
setproctitle = None setproctitle = None
try:
from prometheus_client import multiprocess as prometheus_multiprocess
except ModuleNotFoundError:
prometheus_multiprocess = None
class Conf: class Conf:
""" """
@@ -263,7 +268,6 @@ if not logger.hasHandlers():
# Error Reporting Interface # Error Reporting Interface
class ErrorReporter: class ErrorReporter:
# initialize with iterator of reporters (better name, targets?) # initialize with iterator of reporters (better name, targets?)
def __init__(self, reporters): def __init__(self, reporters):
self.targets = [target for target in reporters] self.targets = [target for target in reporters]
+8 -3
View File
@@ -2,17 +2,22 @@ import datetime
import time import time
import zlib import zlib
import django from django.core.signing import (
from django.core.signing import BadSignature, JSONSerializer, SignatureExpired BadSignature,
JSONSerializer,
SignatureExpired,
b64_decode,
dumps,
)
from django.core.signing import Signer as Sgnr from django.core.signing import Signer as Sgnr
from django.core.signing import TimestampSigner as TsS from django.core.signing import TimestampSigner as TsS
from django.core.signing import b64_decode, dumps
try: try:
from django.core.signing import b62_decode from django.core.signing import b62_decode
except ImportError: except ImportError:
# fallback for django 3.x # fallback for django 3.x
from django.utils.baseconv import base62 from django.utils.baseconv import base62
b62_decode = base62.decode b62_decode = base62.decode
from django.utils.crypto import constant_time_compare from django.utils.crypto import constant_time_compare
+7
View File
@@ -0,0 +1,7 @@
class TimeoutException(SystemExit):
"""
Exception for when a worker takes too long to complete a task
Raising SystemExit will make sure the function terminates gracefully.
"""
pass
+1 -4
View File
@@ -4,6 +4,7 @@ humanhash: Human-readable representations of digests.
The simplest ways to use this module are the :func:`humanize` and :func:`uuid` The simplest ways to use this module are the :func:`humanize` and :func:`uuid`
functions. For tighter control over the output, see :class:`HumanHasher`. functions. For tighter control over the output, see :class:`HumanHasher`.
""" """
import operator import operator
import uuid as uuidlib import uuid as uuidlib
from argparse import ArgumentError from argparse import ArgumentError
@@ -270,7 +271,6 @@ DEFAULT_WORDLIST = (
class HumanHasher: class HumanHasher:
""" """
Transforms hex digests to human-readable strings. Transforms hex digests to human-readable strings.
@@ -290,7 +290,6 @@ class HumanHasher:
self.wordlist = wordlist self.wordlist = wordlist
def humanize(self, hexdigest, words=4, separator="-"): def humanize(self, hexdigest, words=4, separator="-"):
""" """
Humanize a given hexadecimal digest. Humanize a given hexadecimal digest.
@@ -314,7 +313,6 @@ class HumanHasher:
@staticmethod @staticmethod
def compress(bytes, target): def compress(bytes, target):
""" """
Compress a list of byte values to a fixed target length. Compress a list of byte values to a fixed target length.
@@ -353,7 +351,6 @@ class HumanHasher:
return checksums return checksums
def uuid(self, **params): def uuid(self, **params):
""" """
Generate a UUID with a human-readable representation. Generate a UUID with a human-readable representation.
-1
View File
@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [] dependencies = []
operations = [ operations = [
@@ -2,7 +2,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("django_q", "0001_initial"), ("django_q", "0001_initial"),
] ]
@@ -2,7 +2,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("django_q", "0002_auto_20150630_1624"), ("django_q", "0002_auto_20150630_1624"),
] ]
@@ -2,7 +2,6 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("django_q", "0003_auto_20150708_1326"), ("django_q", "0003_auto_20150708_1326"),
] ]
@@ -2,7 +2,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("django_q", "0004_auto_20150710_1043"), ("django_q", "0004_auto_20150710_1043"),
] ]
@@ -2,7 +2,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("django_q", "0005_auto_20150718_1506"), ("django_q", "0005_auto_20150718_1506"),
] ]
-1
View File
@@ -2,7 +2,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("django_q", "0006_auto_20150805_1817"), ("django_q", "0006_auto_20150805_1817"),
] ]
@@ -2,7 +2,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("django_q", "0007_ormq"), ("django_q", "0007_ormq"),
] ]
@@ -2,7 +2,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("django_q", "0008_auto_20160224_1026"), ("django_q", "0008_auto_20160224_1026"),
] ]
@@ -3,7 +3,6 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("django_q", "0009_auto_20171009_0915"), ("django_q", "0009_auto_20171009_0915"),
] ]
@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("django_q", "0010_auto_20200610_0856"), ("django_q", "0010_auto_20200610_0856"),
] ]
@@ -6,7 +6,6 @@ import django_q.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("django_q", "0011_auto_20200628_1055"), ("django_q", "0011_auto_20200628_1055"),
] ]
@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("django_q", "0012_auto_20200702_1608"), ("django_q", "0012_auto_20200702_1608"),
] ]
@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("django_q", "0013_task_attempt_count"), ("django_q", "0013_task_attempt_count"),
] ]
@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("django_q", "0014_schedule_cluster"), ("django_q", "0014_schedule_cluster"),
] ]
@@ -6,7 +6,6 @@ import django_q.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("django_q", "0015_alter_schedule_schedule_type"), ("django_q", "0015_alter_schedule_schedule_type"),
] ]
@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("django_q", "0016_schedule_intended_date_kwarg"), ("django_q", "0016_schedule_intended_date_kwarg"),
] ]
@@ -0,0 +1,20 @@
# Generated by Django 4.2.7 on 2024-03-05 17:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("django_q", "0017_task_cluster_alter"),
]
operations = [
migrations.AddIndex(
model_name="task",
index=models.Index(
condition=models.Q(("success", True)),
fields=["group", "name", "func"],
name="success_index",
),
),
]
+9 -2
View File
@@ -5,12 +5,12 @@ from keyword import iskeyword
from django import get_version from django import get_version
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models import Q
from django.template.defaultfilters import truncatechars from django.template.defaultfilters import truncatechars
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.timezone import is_aware
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
# External # External
@@ -18,7 +18,7 @@ from picklefield import PickledObjectField
from picklefield.fields import dbsafe_decode from picklefield.fields import dbsafe_decode
# Local # Local
from django_q.conf import Conf, croniter from django_q.conf import croniter
from django_q.signing import SignedPackage from django_q.signing import SignedPackage
from django_q.utils import add_months, add_years, localtime from django_q.utils import add_months, add_years, localtime
@@ -112,6 +112,13 @@ class Task(models.Model):
class Meta: class Meta:
app_label = "django_q" app_label = "django_q"
ordering = ["-stopped"] ordering = ["-stopped"]
indexes = [
models.Index(
name="success_index",
fields=["group", "name", "func"],
condition=Q(success=True),
),
]
class SuccessManager(models.Manager): class SuccessManager(models.Manager):
+15 -17
View File
@@ -2,8 +2,8 @@ from multiprocessing.process import current_process
from multiprocessing.queues import Queue from multiprocessing.queues import Queue
from django import core, db from django import core, db
from django.utils.translation import gettext_lazy as _
from django.apps.registry import apps from django.apps.registry import apps
from django.utils.translation import gettext_lazy as _
try: try:
apps.check_apps_ready() apps.check_apps_ready()
@@ -108,34 +108,28 @@ def save_task(task, broker: Broker):
filters[Conf.SAVE_LIMIT_PER] = value filters[Conf.SAVE_LIMIT_PER] = value
with db.transaction.atomic(using=db.router.db_for_write(Success)): with db.transaction.atomic(using=db.router.db_for_write(Success)):
last = Success.objects.filter(**filters).select_for_update().last() list(Success.objects.filter(**filters).select_for_update())
if ( if (
task["success"] task["success"]
and 0 < Conf.SAVE_LIMIT <= Success.objects.filter(**filters).count() and 0 < Conf.SAVE_LIMIT <= Success.objects.filter(**filters).count()
): ):
last.delete() Success.objects.filter(**filters).last().delete()
# check if this task has previous results # check if this task has previous results
try: try:
existing_task = Task.objects.get(id=task["id"], name=task["name"]) task_obj = Task.objects.get(id=task["id"], name=task["name"])
# only update the result if it hasn't succeeded yet # only update the result if it hasn't succeeded yet
if not existing_task.success: if not task_obj.success:
existing_task.stopped = task["stopped"] task_obj.stopped = task["stopped"]
existing_task.result = task["result"] task_obj.result = task["result"]
existing_task.success = task["success"] task_obj.success = task["success"]
existing_task.attempt_count = existing_task.attempt_count + 1 task_obj.attempt_count = task_obj.attempt_count + 1
existing_task.save() task_obj.save()
if (
Conf.MAX_ATTEMPTS > 0
and existing_task.attempt_count >= Conf.MAX_ATTEMPTS
):
broker.acknowledge(task["ack_id"])
except Task.DoesNotExist: except Task.DoesNotExist:
# convert func to string # convert func to string
func = get_func_repr(task["func"]) func = get_func_repr(task["func"])
Task.objects.create( task_obj = Task.objects.create(
id=task["id"], id=task["id"],
name=task["name"], name=task["name"],
func=func, func=func,
@@ -150,6 +144,10 @@ def save_task(task, broker: Broker):
success=task["success"], success=task["success"],
attempt_count=1, attempt_count=1,
) )
if Conf.MAX_ATTEMPTS > 0 and task_obj.attempt_count >= Conf.MAX_ATTEMPTS:
broker.acknowledge(task["ack_id"])
except Exception: except Exception:
logger.exception("Could not save task result") logger.exception("Could not save task result")
+1 -3
View File
@@ -397,9 +397,7 @@ def memory(run_once=False, workers=False, broker=None):
2, 2,
) )
# memory available (MB) # memory available (MB)
memory_available = round( memory_available = round(psutil.virtual_memory().available / 1024**2, 2)
psutil.virtual_memory().available / 1024**2, 2
)
if memory_available_percentage < MEMORY_AVAILABLE_LOWEST_PERCENTAGE: if memory_available_percentage < MEMORY_AVAILABLE_LOWEST_PERCENTAGE:
MEMORY_AVAILABLE_LOWEST_PERCENTAGE = memory_available_percentage MEMORY_AVAILABLE_LOWEST_PERCENTAGE = memory_available_percentage
MEMORY_AVAILABLE_LOWEST_PERCENTAGE_AT = timezone.now() MEMORY_AVAILABLE_LOWEST_PERCENTAGE_AT = timezone.now()
+4 -4
View File
@@ -4,8 +4,8 @@ from multiprocessing.queues import Queue
from time import sleep from time import sleep
from django import core from django import core
from django.utils.translation import gettext_lazy as _
from django.apps.registry import apps from django.apps.registry import apps
from django.utils.translation import gettext_lazy as _
try: try:
apps.check_apps_ready() apps.check_apps_ready()
@@ -58,9 +58,9 @@ def pusher(task_queue: Queue, event: Event, broker: Broker = None):
logger.exception("Failed to push task to queue") logger.exception("Failed to push task to queue")
broker.fail(ack_id) broker.fail(ack_id)
continue continue
task[ task["cluster"] = (
"cluster" Conf.CLUSTER_NAME
] = Conf.CLUSTER_NAME # save actual cluster name to orm task table ) # save actual cluster name to orm task table
task["ack_id"] = ack_id task["ack_id"] = ack_id
task_queue.put(task) task_queue.put(task)
logger.debug( logger.debug(
+1
View File
@@ -2,6 +2,7 @@
The code is derived from The code is derived from
https://github.com/althonos/pronto/commit/3384010dfb4fc7c66a219f59276adef3288a886b https://github.com/althonos/pronto/commit/3384010dfb4fc7c66a219f59276adef3288a886b
""" """
import multiprocessing import multiprocessing
import multiprocessing.queues import multiprocessing.queues
import sys import sys
+2 -3
View File
@@ -2,11 +2,10 @@ import ast
from multiprocessing.process import current_process from multiprocessing.process import current_process
from django import core, db from django import core, db
from django.apps.registry import apps
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.apps.registry import apps
try: try:
apps.check_apps_ready() apps.check_apps_ready()
except core.exceptions.AppRegistryNotReady: except core.exceptions.AppRegistryNotReady:
@@ -86,7 +85,7 @@ def scheduler(broker: Broker = None):
# Little Fix for already broken numbers # Little Fix for already broken numbers
if s.repeats < -1: if s.repeats < -1:
s.repeats = -1 s.repeats = -1
# Check if the value is not zero # Check if the value is not zero
if s.repeats > 0: if s.repeats > 0:
s.repeats -= 1 s.repeats -= 1
+1
View File
@@ -1,4 +1,5 @@
"""Package signing.""" """Package signing."""
import pickle import pickle
from django_q import core_signing as signing from django_q import core_signing as signing
+4 -5
View File
@@ -1,4 +1,5 @@
"""Provides task functionality.""" """Provides task functionality."""
# Standard # Standard
from multiprocessing import Value from multiprocessing import Value
from time import sleep, time from time import sleep, time
@@ -569,7 +570,9 @@ class Chain:
A sequential chain of tasks A sequential chain of tasks
""" """
def __init__(self, chain=None, group=None, cached=Conf.CACHED, sync=Conf.SYNC, broker=None): def __init__(
self, chain=None, group=None, cached=Conf.CACHED, sync=Conf.SYNC, broker=None
):
self.chain = chain or [] self.chain = chain or []
self.group = group or "" self.group = group or ""
self.broker = broker or get_broker() self.broker = broker or get_broker()
@@ -729,17 +732,14 @@ class AsyncTask:
return self.id return self.id
def result(self, wait=0): def result(self, wait=0):
if self.started: if self.started:
return result(self.id, wait=wait, cached=self.cached) return result(self.id, wait=wait, cached=self.cached)
def fetch(self, wait=0): def fetch(self, wait=0):
if self.started: if self.started:
return fetch(self.id, wait=wait, cached=self.cached) return fetch(self.id, wait=wait, cached=self.cached)
def result_group(self, failures=False, wait=0, count=None): def result_group(self, failures=False, wait=0, count=None):
if self.started and self.group: if self.started and self.group:
return result_group( return result_group(
self.group, self.group,
@@ -750,7 +750,6 @@ class AsyncTask:
) )
def fetch_group(self, failures=True, wait=0, count=None): def fetch_group(self, failures=True, wait=0, count=None):
if self.started and self.group: if self.started and self.group:
return fetch_group( return fetch_group(
self.group, self.group,
+38
View File
@@ -0,0 +1,38 @@
import signal
from django.utils.translation import gettext_lazy as _
from django_q.conf import logger
from .exceptions import TimeoutException
class TimeoutHandler:
def __init__(self, timeout: int):
self._timeout = timeout
def raise_timeout_exception(self, signum, frame):
raise TimeoutException(
f"Task exceeded maximum timeout value ({self._timeout} seconds)"
)
def __enter__(self):
# if the timeout is -1, then there is no timeout and the task will always keep running until it's done or manually killed
if self._timeout == -1:
return
try:
signal.signal(signal.SIGALRM, self.raise_timeout_exception)
except ValueError: # ValueError is raised for Windows users
logger.debug(_("SIGALARM is not available on your platform"))
signal.alarm(self._timeout)
def __exit__(self, exc_type, exc_value, traceback):
if self._timeout == -1:
return
"""When getting out of the timeout, reset the alarm, so it won't trigger"""
try:
signal.alarm(0)
signal.signal(signal.SIGALRM, signal.SIG_DFL)
except ValueError: # ValueError is raised for Windows users
logger.debug(_("SIGALARM is not available on your platform"))
+17 -3
View File
@@ -5,9 +5,9 @@ from multiprocessing.process import current_process
from multiprocessing.queues import Queue from multiprocessing.queues import Queue
from django import core from django import core
from django.apps.registry import apps
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.apps.registry import apps
try: try:
apps.check_apps_ready() apps.check_apps_ready()
@@ -17,7 +17,9 @@ except core.exceptions.AppRegistryNotReady:
django.setup() django.setup()
from django_q.conf import Conf, error_reporter, logger, resource, setproctitle from django_q.conf import Conf, error_reporter, logger, resource, setproctitle
from django_q.exceptions import TimeoutException
from django_q.signals import post_spawn, pre_execute from django_q.signals import post_spawn, pre_execute
from django_q.timeout import TimeoutHandler
from django_q.utils import close_old_django_connections, get_func_repr from django_q.utils import close_old_django_connections, get_func_repr
try: try:
@@ -89,25 +91,37 @@ def worker(
pre_execute.send(sender="django_q", func=f, task=task) pre_execute.send(sender="django_q", func=f, task=task)
# execute the payload # execute the payload
timer.value = timer_value # Busy timer.value = timer_value # Busy
if timer.value != -1:
timer.value += 3 # Add buffer so that guard doesn't kill the process on timeout before it gets processed
timeout_error = False
try: try:
if f is None: if f is None:
# raise a meaningfull error if task["func"] is not a valid function # raise a meaningfull error if task["func"] is not a valid function
raise ValueError(f"Function {task['func']} is not defined") raise ValueError(f"Function {task['func']} is not defined")
res = f(*task["args"], **task["kwargs"]) with TimeoutHandler(timer_value):
res = f(*task["args"], **task["kwargs"])
result = (res, True) result = (res, True)
except Exception as e: except (Exception, TimeoutException) as e:
if isinstance(e, TimeoutException):
timeout_error = True
result = (f"{e} : {traceback.format_exc()}", False) result = (f"{e} : {traceback.format_exc()}", False)
if error_reporter: if error_reporter:
error_reporter.report() error_reporter.report()
if task.get("sync", False): if task.get("sync", False):
raise raise
with timer.get_lock(): with timer.get_lock():
# Process result # Process result
task["result"] = result[0] task["result"] = result[0]
task["success"] = result[1] task["success"] = result[1]
task["stopped"] = timezone.now() task["stopped"] = timezone.now()
result_queue.put(task) result_queue.put(task)
if timeout_error:
# force destroy process due to timeout
timer.value = 0
break
timer.value = -1 # Idle timer.value = -1 # Idle
if setproctitle: if setproctitle:
setproctitle.setproctitle(f"qcluster {proc_name} idle") setproctitle.setproctitle(f"qcluster {proc_name} idle")
+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.6" version = "1.7"
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = "1.6.2" release = "1.7.2"
# 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.
+2
View File
@@ -79,6 +79,8 @@ timeout
The number of seconds a worker is allowed to spend on a task before it's terminated. Defaults to ``None``, meaning it will never time out. The number of seconds a worker is allowed to spend on a task before it's terminated. Defaults to ``None``, meaning it will never time out.
Set this to something that makes sense for your project. Can be overridden for individual tasks. Set this to something that makes sense for your project. Can be overridden for individual tasks.
Note: for systems that don't have `SIGALRM` available (e.g. Windows), it will not raise an error properly. It will kill the task, but it will keep retrying until it finishes within the given time.
See :ref:`retry` for details how to set values for timeout and retry. See :ref:`retry` for details how to set values for timeout and retry.
.. _time_zone: .. _time_zone:
-1
View File
@@ -1,4 +1,3 @@
version: "3.0"
services: services:
docs: docs:
container_name: djangoq2-docs container_name: djangoq2-docs
+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 3.2.x, 4.1.x, 4.2.x and 5.0.x Django Q2 is tested with: Python 3.8, 3.9, 3.10, 3.11 and 3.12. Works with Django 4.2.x and 5.0.x
Currently available in English, German and French. Currently available in English, German and French.
View File
+16
View File
@@ -0,0 +1,16 @@
"""
ASGI config for exampleproject project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "exampleproject.settings")
application = get_asgi_application()
+134
View File
@@ -0,0 +1,134 @@
"""
Django settings for exampleproject project.
Generated by 'django-admin startproject' using Django 5.1.1.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-)oouh93bjg+c=b!l5-*w)7et1l+!3nmp223rr^1r4v#jn&ow3f"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ["*"]
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django_q",
]
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 = "exampleproject.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",
],
},
},
]
WSGI_APPLICATION = "exampleproject.wsgi.application"
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/
STATIC_URL = "static/"
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
Q_CLUSTER = {
"name": "DjangORM",
"workers": 4,
"timeout": 90,
"retry": 120,
"queue_limit": 50,
"bulk": 10,
"orm": "default",
}
+27
View File
@@ -0,0 +1,27 @@
"""
URL configuration for exampleproject project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from exampleproject import views
urlpatterns = [
path("admin/", admin.site.urls),
path("new_task/", views.add_task, name="add_task"),
path("result/<slug:task_id>/", views.get_result, name="get_result"),
]
+31
View File
@@ -0,0 +1,31 @@
import time
from django.http import HttpResponse
from django.urls import reverse
from django_q import tasks
# internal function to be called with django_q
def new_task(run_for_minutes):
print("Task started")
time.sleep(run_for_minutes)
print("Task done")
return True
def add_task(request):
task_id = tasks.async_task(new_task, 5)
result_url = reverse("get_result", args=[task_id])
return HttpResponse(
f"Added async task with <a href='{result_url}'>Go to results</a>"
)
def get_result(request, task_id):
task = tasks.fetch(task_id)
if not task:
msg = "Task running... please refresh after some time"
else:
msg = f"Async task result: {task.result}"
return HttpResponse(msg)
+16
View File
@@ -0,0 +1,16 @@
"""
WSGI config for exampleproject project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "exampleproject.settings")
application = get_wsgi_application()
Generated
+1274 -1487
View File
File diff suppressed because it is too large Load Diff
+3 -4
View File
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry] [tool.poetry]
name = "django-q2" name = "django-q2"
version = "1.6.2" version = "1.7.2"
packages = [ packages = [
{ include = "django_q" }, { include = "django_q" },
] ]
@@ -50,7 +50,7 @@ include = ['CHANGELOG.md']
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = ">=3.8,<4" python = ">=3.8,<4"
django = { version = ">=3.2, <6", allow-prereleases = true} django = ">=4.2, <6"
django-picklefield = "^3.1" django-picklefield = "^3.1"
blessed = { version = "^1.19.1", optional = true } blessed = { version = "^1.19.1", optional = true }
@@ -73,8 +73,7 @@ pytest = "^7.1.3"
pytest-django = "^4.5.2" pytest-django = "^4.5.2"
Sphinx = "^4.0.2" Sphinx = "^4.0.2"
pytest-cov = "^4.0.0" pytest-cov = "^4.0.0"
black = "^22.10.0" ruff = "^0.4.4"
isort = {extras = ["requirements_deprecated_finder"], version = "^5.10.1"}
[tool.poetry.extras] [tool.poetry.extras]
requires = ["poetry_core"] requires = ["poetry_core"]
+41 -40
View File
@@ -1,40 +1,41 @@
ansicon==1.89.0; platform_system == "Windows" and python_version >= "2.7" \ asgiref==3.8.1 ; python_version >= "3.8" and python_version < "4" \
--hash=sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec \ --hash=sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47 \
--hash=sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1 --hash=sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590
arrow==1.1.1; python_version >= "3.6" \ backports-zoneinfo==0.2.1 ; python_version >= "3.8" and python_version < "3.9" \
--hash=sha256:77a60a4db5766d900a2085ce9074c5c7b8e2c99afeaa98ad627637ff6f292510 \ --hash=sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf \
--hash=sha256:dee7602f6c60e3ec510095b5e301441bc56288cb8f51def14dcb3079f623823a --hash=sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328 \
asgiref==3.3.4; python_version >= "3.6" \ --hash=sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546 \
--hash=sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee \ --hash=sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6 \
--hash=sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78 --hash=sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570 \
blessed==1.18.1; python_version >= "2.7" \ --hash=sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9 \
--hash=sha256:dd7c0d33db9a2e7f597b446996484d0ed46e1586239db064fb5025008937dcae \ --hash=sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7 \
--hash=sha256:8b09936def6bc06583db99b65636b980075733e13550cb6af262ce724a55da23 --hash=sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987 \
django-picklefield==3.0.1; python_version >= "3" \ --hash=sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722 \
--hash=sha256:15ccba592ca953b9edf9532e64640329cd47b136b7f8f10f2939caa5f9ce4287 \ --hash=sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582 \
--hash=sha256:3c702a54fde2d322fe5b2f39b8f78d9f655b8f77944ab26f703be6c0ed335a35 --hash=sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc \
django==3.2.4; python_version >= "3.6" \ --hash=sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b \
--hash=sha256:ea735cbbbb3b2fba6d4da4784a0043d84c67c92f1fdf15ad6db69900e792c10f \ --hash=sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1 \
--hash=sha256:66c9d8db8cc6fe938a28b7887c1596e42d522e27618562517cc8929eb7e7f296 --hash=sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08 \
jinxed==1.1.0; platform_system == "Windows" and python_version >= "2.7" \ --hash=sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac \
--hash=sha256:6a61ccf963c16aa885304f27e6e5693783676897cea0c7f223270c8b8e78baf8 \ --hash=sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2
--hash=sha256:d8f1731f134e9e6b04d95095845ae6c10eb15cb223a5f0cabdea87d4a279c305 django-picklefield==3.2 ; python_version >= "3.8" and python_version < "4" \
python-dateutil==2.8.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" \ --hash=sha256:aa463f5d79d497dbe789f14b45180f00a51d0d670067d0729f352a3941cdfa4d \
--hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ --hash=sha256:e9a73539d110f69825d9320db18bcb82e5189ff48dbed41821c026a20497764c
--hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a django==4.2.11 ; python_version >= "3.8" and python_version < "4" \
pytz==2021.1; python_version >= "3.6" \ --hash=sha256:6e6ff3db2d8dd0c986b4eec8554c8e4f919b5c1ff62a5b4390c17aff2ed6e5c4 \
--hash=sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798 \ --hash=sha256:ddc24a0a8280a0430baa37aff11f28574720af05888c62b7cfe71d219f4599d3
--hash=sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da importlib-metadata==7.1.0 ; python_version >= "3.8" and python_version < "3.10" \
six==1.16.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" \ --hash=sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570 \
--hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 \ --hash=sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2
--hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 sqlparse==0.5.0 ; python_version >= "3.8" and python_version < "4" \
sqlparse==0.4.1; python_version >= "3.6" \ --hash=sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93 \
--hash=sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0 \ --hash=sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663
--hash=sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8 typing-extensions==4.11.0 ; python_version >= "3.8" and python_version < "3.11" \
typing-extensions==3.10.0.0; python_version < "3.8" and python_version >= "3.6" \ --hash=sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0 \
--hash=sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497 \ --hash=sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a
--hash=sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84 \ tzdata==2024.1 ; python_version >= "3.8" and python_version < "4" and sys_platform == "win32" \
--hash=sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342 --hash=sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd \
wcwidth==0.2.5; python_version >= "2.7" \ --hash=sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252
--hash=sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784 \ zipp==3.18.1 ; python_version >= "3.8" and python_version < "3.10" \
--hash=sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83 --hash=sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b \
--hash=sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715
+1
View File
@@ -0,0 +1 @@
lint.extend-select = ["I"]
-2
View File
@@ -1,5 +1,3 @@
version: '3'
services: services:
redis: redis:
image: redis:latest image: redis:latest
+8 -3
View File
@@ -1,11 +1,16 @@
version: '3'
services: services:
web: web:
restart: always restart: always
command: python manage.py runserver 0.0.0.0:8000 command: bash -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000"
ports: ports:
- "127.0.0.1:8000:8000" - "127.0.0.1:8000:8000"
build: . build: .
volumes: volumes:
- .:/app - .:/app
django-q:
restart: always
command: bash -c "python manage.py migrate && python manage.py qcluster"
build: .
volumes:
- .:/app