Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,8 @@ jobs:
fail-fast: false
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.10", "3.11", "3.12", "3.13"]
django-version: ["4.2", "5.0", "5.1", "5.2"]
exclude:
- django-version: "5.0"
python-version: "3.9"
- django-version: "5.1"
python-version: "3.9"
- django-version: "5.2"
python-version: "3.9"

steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
Expand Down
3 changes: 1 addition & 2 deletions django_tasks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
django_stubs_ext.monkeypatch()

import importlib.metadata
from typing import Optional

from django.utils.connection import BaseConnectionHandler, ConnectionProxy
from django.utils.module_loading import import_string
Expand Down Expand Up @@ -40,7 +39,7 @@ class TasksHandler(BaseConnectionHandler[BaseTaskBackend]):
settings_name = "TASKS"
exception_class = InvalidTaskBackendError

def configure_settings(self, settings: Optional[dict]) -> dict:
def configure_settings(self, settings: dict | None) -> dict:
try:
return super().configure_settings(settings)
except AttributeError:
Expand Down
3 changes: 1 addition & 2 deletions django_tasks/apps.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
from django.apps import AppConfig
from django.core import checks

from django_tasks.checks import check_tasks


class TasksAppConfig(AppConfig):
name = "django_tasks"

def ready(self) -> None:
from . import signal_handlers # noqa
from .checks import check_tasks

checks.register(check_tasks)
10 changes: 4 additions & 6 deletions django_tasks/backends/database/admin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from typing import Optional

from django.contrib import admin
from django.http import HttpRequest

Expand All @@ -22,21 +20,21 @@ class DBTaskResultAdmin(admin.ModelAdmin):
ordering = ["-enqueued_at"]

def has_add_permission(
self, request: HttpRequest, obj: Optional[DBTaskResult] = None
self, request: HttpRequest, obj: DBTaskResult | None = None
) -> bool:
return False

def has_delete_permission(
self, request: HttpRequest, obj: Optional[DBTaskResult] = None
self, request: HttpRequest, obj: DBTaskResult | None = None
) -> bool:
return False

def has_change_permission(
self, request: HttpRequest, obj: Optional[DBTaskResult] = None
self, request: HttpRequest, obj: DBTaskResult | None = None
) -> bool:
return False

def get_readonly_fields(
self, request: HttpRequest, obj: Optional[DBTaskResult] = None
self, request: HttpRequest, obj: DBTaskResult | None = None
) -> list[str]:
return [f.name for f in self.model._meta.fields]
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import time
from argparse import ArgumentParser, ArgumentTypeError, BooleanOptionalAction
from types import FrameType
from typing import Optional

from django.conf import settings
from django.core.exceptions import SuspiciousOperation
Expand Down Expand Up @@ -38,7 +37,7 @@ def __init__(
batch: bool,
backend_name: str,
startup_delay: bool,
max_tasks: Optional[int],
max_tasks: int | None,
worker_id: str,
):
self.queue_names = queue_names
Expand All @@ -55,7 +54,7 @@ def __init__(

self.worker_id = worker_id

def shutdown(self, signum: int, frame: Optional[FrameType]) -> None:
def shutdown(self, signum: int, frame: FrameType | None) -> None:
if not self.running:
logger.warning(
"Received %s - terminating current task.", signal.strsignal(signum)
Expand Down Expand Up @@ -307,7 +306,7 @@ def handle(
backend_name: str,
startup_delay: bool,
reload: bool,
max_tasks: Optional[int],
max_tasks: int | None,
worker_id: str,
**options: dict,
) -> None:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import logging
from argparse import ArgumentParser, ArgumentTypeError
from datetime import timedelta
from typing import Optional

from django.core.management.base import BaseCommand
from django.db.models import Q
Expand Down Expand Up @@ -91,7 +90,7 @@ def handle(
verbosity: int,
backend: DatabaseBackend,
min_age_days: int,
failed_min_age_days: Optional[int],
failed_min_age_days: int | None,
queue_name: str,
dry_run: bool,
**options: dict,
Expand Down
6 changes: 3 additions & 3 deletions django_tasks/backends/database/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from collections.abc import Generator
from contextlib import contextmanager
from typing import Any, Optional, Union
from typing import Any
from uuid import UUID

import django
Expand Down Expand Up @@ -30,7 +30,7 @@ def connection_requires_manual_exclusive_transaction(


@contextmanager
def exclusive_transaction(using: Optional[str] = None) -> Generator[Any, Any, Any]:
def exclusive_transaction(using: str | None = None) -> Generator[Any, Any, Any]:
"""
Wrapper around `transaction.atomic` which ensures transactions on SQLite are exclusive.

Expand All @@ -50,7 +50,7 @@ def exclusive_transaction(using: Optional[str] = None) -> Generator[Any, Any, An
yield


def normalize_uuid(val: Union[str, UUID]) -> str:
def normalize_uuid(val: str | UUID) -> str:
"""
Normalize a UUID into its dashed representation.

Expand Down
8 changes: 4 additions & 4 deletions django_tasks/backends/rq.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from collections.abc import Iterable
from types import TracebackType
from typing import Any, Optional, TypeVar
from typing import Any, TypeVar

import django_rq
from django.apps import apps
Expand Down Expand Up @@ -140,7 +140,7 @@ def task_result(self) -> TaskResult:

def failed_callback(
job: Job,
connection: Optional[Redis],
connection: Redis | None,
exception_class: type[Exception],
exception_value: Exception,
traceback: TracebackType,
Expand All @@ -158,7 +158,7 @@ def failed_callback(
task_finished.send(type(task_result.task.get_backend()), task_result=task_result)


def success_callback(job: Job, connection: Optional[Redis], result: Any) -> None:
def success_callback(job: Job, connection: Redis | None, result: Any) -> None:
task_result = job.task_result

object.__setattr__(task_result, "status", ResultStatus.SUCCEEDED)
Expand Down Expand Up @@ -233,7 +233,7 @@ def save_result() -> None:
def _get_queues(self) -> list[django_rq.queues.DjangoRQ]:
return django_rq.queues.get_queues(*self.queues, job_class=Job) # type: ignore[no-any-return,no-untyped-call]

def _get_job(self, job_id: str) -> Optional[Job]:
def _get_job(self, job_id: str) -> Job | None:
for queue in self._get_queues():
job = queue.fetch_job(job_id)
if job is not None:
Expand Down
49 changes: 24 additions & 25 deletions django_tasks/task.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
from collections.abc import Callable
from dataclasses import dataclass, field, replace
from datetime import datetime
from inspect import isclass, iscoroutinefunction
from typing import (
TYPE_CHECKING,
Any,
Callable,
Concatenate,
Generic,
Literal,
Optional,
TypeVar,
Union,
cast,
overload,
)
Expand All @@ -18,7 +17,7 @@
from django.db.models.enums import TextChoices
from django.utils.module_loading import import_string
from django.utils.translation import gettext_lazy as _
from typing_extensions import Concatenate, ParamSpec, Self
from typing_extensions import ParamSpec, Self

from .exceptions import ResultDoesNotExist
from .utils import (
Expand Down Expand Up @@ -79,10 +78,10 @@ class Task(Generic[P, T]):
queue_name: str = DEFAULT_QUEUE_NAME
"""The name of the queue the task will run on"""

run_after: Optional[datetime] = None
run_after: datetime | None = None
"""The earliest this task will run"""

enqueue_on_commit: Optional[bool] = None
enqueue_on_commit: bool | None = None
"""
Whether the task will be enqueued when the current transaction commits,
immediately, or whatever the backend decides
Expand All @@ -106,10 +105,10 @@ def name(self) -> str:
def using(
self,
*,
priority: Optional[int] = None,
queue_name: Optional[str] = None,
run_after: Optional[datetime] = None,
backend: Optional[str] = None,
priority: int | None = None,
queue_name: str | None = None,
run_after: datetime | None = None,
backend: str | None = None,
) -> Self:
"""
Create a new task with modified defaults
Expand Down Expand Up @@ -202,7 +201,7 @@ def task(
priority: int = DEFAULT_PRIORITY,
queue_name: str = DEFAULT_QUEUE_NAME,
backend: str = DEFAULT_TASK_BACKEND_ALIAS,
enqueue_on_commit: Optional[bool] = None,
enqueue_on_commit: bool | None = None,
takes_context: Literal[False] = False,
) -> Callable[[Callable[P, T]], Task[P, T]]: ...

Expand All @@ -215,25 +214,25 @@ def task(
priority: int = DEFAULT_PRIORITY,
queue_name: str = DEFAULT_QUEUE_NAME,
backend: str = DEFAULT_TASK_BACKEND_ALIAS,
enqueue_on_commit: Optional[bool] = None,
enqueue_on_commit: bool | None = None,
takes_context: Literal[True],
) -> Callable[[Callable[Concatenate["TaskContext", P], T]], Task[P, T]]: ...


# Implementation
def task( # type: ignore[misc]
function: Optional[Callable[P, T]] = None,
function: Callable[P, T] | None = None,
*,
priority: int = DEFAULT_PRIORITY,
queue_name: str = DEFAULT_QUEUE_NAME,
backend: str = DEFAULT_TASK_BACKEND_ALIAS,
enqueue_on_commit: Optional[bool] = None,
enqueue_on_commit: bool | None = None,
takes_context: bool = False,
) -> Union[
Task[P, T],
Callable[[Callable[P, T]], Task[P, T]],
Callable[[Callable[Concatenate["TaskContext", P], T]], Task[P, T]],
]:
) -> (
Task[P, T]
| Callable[[Callable[P, T]], Task[P, T]]
| Callable[[Callable[Concatenate["TaskContext", P], T]], Task[P, T]]
):
"""
A decorator used to create a task.
"""
Expand Down Expand Up @@ -286,16 +285,16 @@ class TaskResult(Generic[T]):
status: ResultStatus
"""The status of the running task"""

enqueued_at: Optional[datetime]
enqueued_at: datetime | None
"""The time this task was enqueued"""

started_at: Optional[datetime]
started_at: datetime | None
"""The time this task was started"""

finished_at: Optional[datetime]
finished_at: datetime | None
"""The time this task was finished"""

last_attempted_at: Optional[datetime]
last_attempted_at: datetime | None
"""The time this task was last attempted to be run"""

args: list
Expand All @@ -313,10 +312,10 @@ class TaskResult(Generic[T]):
worker_ids: list[str]
"""The workers which have processed the task"""

_return_value: Optional[T] = field(init=False, default=None)
_return_value: T | None = field(init=False, default=None)

@property
def return_value(self) -> Optional[T]:
def return_value(self) -> T | None:
"""
The return value of the task.

Expand Down
11 changes: 4 additions & 7 deletions django_tasks/utils.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import inspect
import json
import random
import time
from collections.abc import Callable
from functools import wraps
from traceback import format_exception
from typing import Any, Callable, TypeVar
from typing import Any, TypeVar

from django.utils.crypto import RANDOM_STRING_CHARS
from django.utils.crypto import get_random_string
from typing_extensions import ParamSpec

T = TypeVar("T")
Expand Down Expand Up @@ -69,8 +69,5 @@ def get_random_id() -> str:
Return a random string for use as a task or worker id.

Whilst 64 characters is the max, just use 32 as a sensible middle-ground.

This should be much faster than Django's `get_random_string`, since
it's not cryptographically secure.
"""
return "".join(random.choices(RANDOM_STRING_CHARS, k=32))
return get_random_string(32)
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ license-files = ["LICENSE"]
classifiers = [
"Development Status :: 3 - Alpha",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Expand All @@ -36,7 +35,7 @@ classifiers = [
"Topic :: Internet :: WWW/HTTP",
"Typing :: Typed"
]
requires-python = ">=3.9"
requires-python = ">=3.10"
dependencies = [
"Django>=4.2",
"typing_extensions",
Expand Down