diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5734b3d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,56 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Run Ruff + run: | + uv pip install --system ruff + ruff check . + ruff format --check . + + test: + needs: lint + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.12", "3.13", "3.14"] + django-version: ["6.0"] + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + uv pip install --system "django==${{ matrix.django-version }}" + uv pip install --system -e ".[test]" || uv pip install --system -e . pytest pytest-django huey + + - name: Run Tests + run: PYTHONPATH=. pytest \ No newline at end of file diff --git a/README.md b/README.md index dad54a4..13bd101 100644 --- a/README.md +++ b/README.md @@ -15,14 +15,14 @@ Log and monitor [Huey](https://huey.readthedocs.io/en/latest/) task attempts dir 1. Install the package using [uv](https://docs.astral.sh/uv/): ```bash - uv add django-huey-log + uv add git+https://github.com/PythonUnited/django-huey-log.git ``` 2. Add `huey_log` to your `INSTALLED_APPS` in `settings.py`: ```python INSTALLED_APPS = [ # ... - "huey_log", + "django_huey_log", # ... ] ``` diff --git a/example/manage.py b/example/manage.py index 37c9914..80a63d1 100644 --- a/example/manage.py +++ b/example/manage.py @@ -1,12 +1,13 @@ #!/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', 'settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +19,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': - main() \ No newline at end of file +if __name__ == "__main__": + main() diff --git a/example/settings.py b/example/settings.py index 99e1fe3..7c59bd6 100644 --- a/example/settings.py +++ b/example/settings.py @@ -1,4 +1,3 @@ -import os from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent @@ -55,7 +54,7 @@ STATIC_URL = "static/" HUEY = { - 'huey_class': 'huey.SqliteHuey', - 'name': 'test-huey', - 'filename': BASE_DIR / 'huey_db.sqlite3', -} \ No newline at end of file + "huey_class": "huey.SqliteHuey", + "name": "test-huey", + "filename": BASE_DIR / "huey_db.sqlite3", +} diff --git a/example/tasks.py b/example/tasks.py index 0e5c61a..1c062ab 100644 --- a/example/tasks.py +++ b/example/tasks.py @@ -1,13 +1,16 @@ -from huey.contrib.djhuey import task, db_task import time +from huey.contrib.djhuey import task + + @task() def success_task(name): print(f"Hello {name}!") time.sleep(1) return f"Done {name}" + @task() def failure_task(): time.sleep(0.5) - raise ValueError("This task was designed to fail!") \ No newline at end of file + raise ValueError("This task was designed to fail!") diff --git a/example/urls.py b/example/urls.py index 0757f2b..083932c 100644 --- a/example/urls.py +++ b/example/urls.py @@ -3,4 +3,4 @@ urlpatterns = [ path("admin/", admin.site.urls), -] \ No newline at end of file +] diff --git a/pyproject.toml b/pyproject.toml index 9dd55ad..a0eed26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "Log and monitor Huey task attempts in the Django admin" readme = "README.md" requires-python = ">=3.10" dependencies = [ - "django>=4.2", + "django>=5.0", "huey>=2.5.0", ] @@ -17,4 +17,33 @@ build-backend = "hatchling.build" packages = ["src/django_huey_log"] [tool.hatch.build.targets.wheel.sources] -"src" = "django_huey_log" \ No newline at end of file +"src" = "django_huey_log" + +[project.optional-dependencies] +test = [ + "pytest>=7.0", + "pytest-django>=4.5", +] +dev = [ + "ruff==0.14.13", +] + +[tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "example.settings" +python_files = ["tests.py", "test_*.py"] +pythonpath = ["."] + +[tool.ruff] +# Target version for the generated code +target-version = "py310" +line-length = 88 + +[tool.ruff.lint] +# Enable Pyflakes (F) and pycodestyle (E, W) codes by default. +# Also common: I (isort), B (flake8-bugbear), UP (pyupgrade) +select = ["E", "F", "W", "I", "B", "UP"] +ignore = [] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" \ No newline at end of file diff --git a/src/django_huey_log/admin.py b/src/django_huey_log/admin.py index d35871d..a731c60 100644 --- a/src/django_huey_log/admin.py +++ b/src/django_huey_log/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin + from .models import HueyTaskAttempt diff --git a/src/django_huey_log/migrations/0001_initial.py b/src/django_huey_log/migrations/0001_initial.py index fa588d0..3d6326b 100644 --- a/src/django_huey_log/migrations/0001_initial.py +++ b/src/django_huey_log/migrations/0001_initial.py @@ -5,7 +5,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [] diff --git a/src/django_huey_log/signals.py b/src/django_huey_log/signals.py index 4282328..6e91ac5 100644 --- a/src/django_huey_log/signals.py +++ b/src/django_huey_log/signals.py @@ -1,7 +1,8 @@ from __future__ import annotations import traceback as tb -from datetime import datetime, timezone as dt_timezone +from datetime import datetime +from datetime import timezone as dt_timezone from django.utils import timezone from huey.contrib.djhuey import HUEY @@ -48,7 +49,8 @@ def _task_eta(task): def _upsert_attempt(task, status: str, **fields): # Single-row “current attempt” strategy keyed by task_id + started_at bucket: - # For simplicity: just create new rows for EXECUTING, then update the latest row for completion/error. + # For simplicity: just create new rows for EXECUTING, then update the latest + # row for completion/error. task_id = str(getattr(task, "id", "") or getattr(task, "task_id", "")) if status == HueyTaskAttempt.Status.EXECUTING: diff --git a/src/django_huey_log/tests/__init__.py b/src/django_huey_log/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/django_huey_log/tests/test_log_tasks.py b/src/django_huey_log/tests/test_log_tasks.py new file mode 100644 index 0000000..8b7b5f7 --- /dev/null +++ b/src/django_huey_log/tests/test_log_tasks.py @@ -0,0 +1,42 @@ +import pytest + +from example.tasks import failure_task, success_task + +from ..models import HueyTaskAttempt + + +@pytest.mark.django_db +def test_task_logging_success(): + # Trigger the task (calling .task_id usually triggers huey logic in testing) + success_task("test-user") + + # Check if an attempt was logged + attempt = HueyTaskAttempt.objects.first() + assert attempt is not None + assert attempt.task_name == "success_task" + + +@pytest.mark.django_db +def test_task_logging_failure(): + # Trigger a failing task. + # We don't necessarily need pytest.raises if Huey handles the exception internally, + # we just need to verify that the SIGNAL_ERROR was caught and logged. + failure_task() + + # Check if a failure attempt was logged + attempt = HueyTaskAttempt.objects.filter(task_name="failure_task").first() + assert attempt is not None + assert attempt.status == HueyTaskAttempt.Status.ERROR + assert attempt.exc_type == "ValueError" + assert "This task was designed to fail!" in attempt.exc_message + assert len(attempt.traceback) > 0 + + +@pytest.mark.django_db +def test_task_logging_arguments(): + # Test complex argument capturing. + # Note: 'name' is a keyword argument here, so it goes into kwargs_repr. + success_task(name={"complex": [1, 2, 3]}) + + attempt = HueyTaskAttempt.objects.filter(task_name="success_task").first() + assert "{'complex': [1, 2, 3]}" in attempt.kwargs_repr