Skip to content

Commit

Permalink
adding initial code files
Browse files Browse the repository at this point in the history
  • Loading branch information
ivellios committed Sep 21, 2023
1 parent 26b2ffb commit a34d95a
Show file tree
Hide file tree
Showing 15 changed files with 3,784 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,4 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.idea/
31 changes: 31 additions & 0 deletions liccheck.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Authorized and unauthorized licenses in LOWER CASE
[Licenses]
authorized_licenses:
BSD
new BSD
BSD license
new BDS license
simplified BSD
Apache
Apache 2.0
Apache Software
Apache software license
gnu LGPL
LGPL with exceptions or zpl
ISC license
ISC license (ISCL)
MIT
MIT license
Python Software Foundation
python software foundation license
zpl 2.1

unauthorized_licenses:
GPL v3
GPL
GNU General Public License v2 or later (GPLv2+)


[Authorized Packages]
# Python software license (see http://zesty.ca/python/uuid.README.txt)
uuid: 1.25,>=1.30
17 changes: 17 additions & 0 deletions manage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys


def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.testapp.settings")

from django.core.management import execute_from_command_line

execute_from_command_line(sys.argv)


if __name__ == "__main__":
main()
9 changes: 9 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[mypy]
warn_return_any = True
warn_unused_configs = True

[mypy-unified_signals.*]
ignore_missing_imports = True

[mypy-celery.*]
ignore_missing_imports = True
3,427 changes: 3,427 additions & 0 deletions poetry.lock

Large diffs are not rendered by default.

57 changes: 57 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
[tool.poetry]
name = "celery-signal-receivers"
version = "0.1.0"
description = "Extension for the Django signal receivers to process them asynchronously as the Celery tasks."
authors = ["Janusz Kamieński <[email protected]>"]
license = "MIT"
readme = "README.md"
homepage = "https://github.com/ivellios/celery-signal-receivers"
repository = "https://github.com/ivellios/celery-signal-receivers"
keywords = ["django", "signals", "celery"]
classifiers = [
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Framework :: Django :: 4.0",
"Framework :: Django :: 4.1",
"Framework :: Django :: 4.2",
"Framework :: Celery",
]
packages = [
{ include = "celery_signals", from = "src" }
]

[tool.poetry.dependencies]
python = "^3.9"
Django = "^4.0"
celery = "^5.3"
django-stubs = "^4.2.4"
pytest-cov = "^4.1.0"
setuptools = "^68.2.2"
django-unified-signals = "^0.1.1"

[tool.poetry.group.dev.dependencies]
black = "^23.3.0"
botocore = "^1.31.49"
boto3 = "^1.28.49"
ipython = "^8.14.0"
ipdb = "^0.13.13"
liccheck = "^0.9.1"
pytest-django = "^4.5.2"
pytest-watch = "^4.2.0"
safety = "^2.3.5"
checkov = "^2.4.41"

[tool.mypy]
plugins = [
"mypy_django_plugin.main"
]

[tool.django-stubs]
django_settings_module = "tests.testapp.settings"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
4 changes: 4 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

[pytest]
DJANGO_SETTINGS_MODULE = tests.testapp.settings
python_files = tests.py test_*.py *_tests.py
1 change: 1 addition & 0 deletions src/celery_signals/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .receivers import receiver_task
67 changes: 67 additions & 0 deletions src/celery_signals/receivers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import importlib
import json
import typing
from functools import wraps

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.dispatch import receiver
from unified_signals import UnifiedSignal


def get_celery_app():
try:
app_path: str = settings.EVENT_SIGNALS_CELERY_APP
except AttributeError:
raise ImproperlyConfigured(
"EVENT_SIGNALS_CELERY_APP setting is not defined. "
"This should point to the celery app object "
"in the module (e.g. project.celery.app)"
)
else:
try:
module, app_name = app_path.rsplit(".", 1)
except ValueError:
raise ImproperlyConfigured(
"EVENT_SIGNALS_CELERY_APP should point "
"to the celery app object in the module "
"(e.g. project.celery.app)"
)
return getattr(importlib.import_module(module), app_name)


app = get_celery_app()


def receiver_task(
signal: UnifiedSignal, # TODO: also accept multiple signals as original Signal does
celery_task_options: typing.Optional[typing.Dict] = None,
**options,
):
if celery_task_options is None:
celery_task_options = dict()

def decorator(func):
@wraps(func)
def consumer_function(message_data: str = "{}", *args, **kwargs):
message = signal.message_class(**json.loads(message_data))
return func(sender=None, message=message, *args, **kwargs)

consumer = app.task(**celery_task_options)(consumer_function)
app.register_task(consumer)

def producer(
signal=signal,
sender=None,
message: typing.Optional[typing.Any] = None,
*_args,
**_kwargs,
):
message_data = json.dumps(message.__dict__) if message else "{}"
return consumer.delay(message_data, *_args, **_kwargs)

signal.connect(producer, **options)

return func

return decorator
Empty file added tests/__init__.py
Empty file.
79 changes: 79 additions & 0 deletions tests/test_receivers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import dataclasses
from unittest import mock

import pytest
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.test import override_settings
from unified_signals import UnifiedSignal

from celery_signals.receivers import get_celery_app, receiver_task


@dataclasses.dataclass
class DataMock:
field: int


class SenderMock:
pass


def test_celery_signal_receiver():
signal = UnifiedSignal(DataMock)

@receiver_task(signal)
def handle_signal(**kwargs):
assert True

signal.send(SenderMock(), DataMock(field=10))


@override_settings(
CELERY_TASK_ALWAYS_EAGER=True, EVENT_SIGNALS_CELERY_APP="tests.testapp.celery.app"
)
def test_celery_signal_receiver_creates_celery_task():
signal = UnifiedSignal(DataMock)

with mock.patch("tests.testapp.celery.app.register_task") as task_mock:

@receiver_task(signal)
def handle_signal(**kwargs):
...

signal.send(SenderMock(), DataMock(field=10))
task_mock.assert_called_once()


@override_settings(
CELERY_TASK_ALWAYS_EAGER=True, EVENT_SIGNALS_CELERY_APP="tests.testapp.celery.app"
)
def test_celery_signal_receiver_consumer_runs_receiver_function():
signal = UnifiedSignal(DataMock)

@receiver_task(signal, weak=False)
def handle_signal(sender, message, **kwargs):
assert message.field == 10
assert message.__class__ == DataMock

signal.send(SenderMock(), DataMock(field=10))


def test_receivers_import_without_celery_app_defined():
OLD_EVENT_SIGNALS_CELERY_APP = settings.EVENT_SIGNALS_CELERY_APP
del settings.EVENT_SIGNALS_CELERY_APP

with pytest.raises(ImproperlyConfigured):
get_celery_app()

settings.EVENT_SIGNALS_CELERY_APP = OLD_EVENT_SIGNALS_CELERY_APP


def test_receivers_import_with_celery_app_defined_incorrectly():
OLD_EVENT_SIGNALS_CELERY_APP = settings.EVENT_SIGNALS_CELERY_APP
settings.EVENT_SIGNALS_CELERY_APP = "bad_import"

with pytest.raises(ImproperlyConfigured):
get_celery_app()

settings.EVENT_SIGNALS_CELERY_APP = OLD_EVENT_SIGNALS_CELERY_APP
Empty file added tests/testapp/__init__.py
Empty file.
26 changes: 26 additions & 0 deletions tests/testapp/celery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import logging
import os

from celery import Celery

# Set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")

app = Celery("project")

# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object("django.conf:settings", namespace="CELERY")

# Load task modules from all registered Django apps.
app.autodiscover_tasks()


logger = logging.getLogger(__file__)


@app.task(bind=True, ignore_result=True)
def debug_task(self):
logger.info("PING")
7 changes: 7 additions & 0 deletions tests/testapp/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# minimal settings for running tests

SECRET_KEY = "Some secret key"

# CELERY_ALWAYS_EAGER = True

EVENT_SIGNALS_CELERY_APP = "tests.testapp.celery.app"
58 changes: 58 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
[tox]
isolated_build = true
envlist =
black
safety
liccheck
mypy
coverage
py{3.9,3.10,3.11}-dj{4.0,4.1,4.2}

[testenv]
allowlist_externals = poetry
deps =
coverage
commands_pre =
poetry install
poetry install --no-root --sync
dj4.0: pip install Django>=4.0,<4.1
dj4.1: pip install Django>=4.1,<4.2
dj4.2: pip install Django>=4.2,<4.3
commands =
poetry run python manage.py --version
poetry run pytest --cov=src/celery_signals tests/


[testenv:safety]
basepython = python3.11
deps = safety
commands = safety check


[testenv:mypy]
basepython = python3.11
commands =
poetry run mypy tests src/celery_signals


[testenv:black]
basepython = python3.11
commands =
black --check tests src/celery_signals


[testenv:liccheck]
basepython = python3.11
deps = liccheck
commands =
poetry export -f requirements.txt --output {envtmpdir}/requirements.txt
liccheck -r {envtmpdir}/requirements.txt -l PARANOID


[testenv:coverage]
basepython = python3.11
commands_pre =
poetry install
poetry install --no-root --sync
commands =
poetry run pytest --cov=src/celery_signals tests/

0 comments on commit a34d95a

Please sign in to comment.