Fading Coder

One Final Commit for the Last Sprint

Home > Tools > Content

Integrating Celery 4 with Django 3 for Periodic News Ranking Aggregation

Tools Aug 27 8

Environment Setup

  • OS: Ubuntu
  • Editor: PyCharm
  • Python: 3.6 (bundled with Celery 4.3.0)
  • Dependencies: Django 3.0.8

Celery 4 can drive scheduled jobs in Django without extra plugins like djcelery. Using the native celery beat keeps the setup lightweight. Trade-offs include losing plugin-based task dashboards and simplified task management, which is acceptable when changes to schedules are infrequent.

Project Initialization

django-admin startproject Newsite
cd Newsite
python3 manage.py startapp aggregator
touch ./Newsite/celery_app.py ./aggregator/jobs.py start_worker.sh

Resulting layout:

Newsite/
├── aggregator/
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── migrations/
│   │   └── __init__.py
│   ├── models.py
│   ├── jobs.py
│   ├── tests.py
│   └── views.py
├── manage.py
├── start_worker.sh
└── Newsite/
    ├── asgi.py
    ├── celery_app.py
    ├── __init__.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

aggregator handles news collection; Newsite is the project container.

Data Model

For persistnet ranking history, define a model to store entries:

# aggregator/models.py
from django.db import models

class RankItem(models.Model):
    INACTIVE = 0
    ACTIVE = 1
    STATE_CHOICES = ((INACTIVE, 'Inactive'), (ACTIVE, 'Active'))

    source_name = models.CharField('Source', max_length=256, null=True)
    category = models.CharField('Category', max_length=256, null=True)
    payload = models.TextField('Data', null=True)
    created = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now=True)
    state = models.SmallIntegerField(choices=STATE_CHOICES, default=ACTIVE)
    rank_order = models.SmallIntegerField('Order', default=0)

    def __str__(self):
        return f"{self.source_name}{self.category}"

    class Meta:
        ordering = ['-rank_order']

View Layer

# aggregator/views.py
from django.shortcuts import render
from .models import RankItem

def show_rankings(request):
    items = RankItem.objects.filter(state=RankItem.ACTIVE)
    context = {'title': 'Trending News', 'items': items}
    return render(request, 'aggregator/rankings.html', context)

Task Definition

# aggregator/jobs.py
import sys
import os
import django
from concurrent.futures import ThreadPoolExecutor

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Newsite.settings')
django.setup()

from celery import shared_task
from aggregator.scrapers import fetch_github_trending
from aggregator.models import RankItem

@shared_task
def refresh_rankings():
    scrapers = [fetch_github_trending]

    def handle_result(fut):
        outcome = fut.result()
        src = outcome.get('source_name', '')
        cat = outcome.get('category', '')
        data = outcome.get('payload', '')
        if src:
            rec = RankItem.objects.filter(source_name=src).first()
            if not rec:
                RankItem.objects.create(source_name=src, category=cat, payload=data)
            else:
                rec.payload = data
                rec.save()

    with ThreadPoolExecutor(max_workers=4) as executor:
        for job in scrapers:
            executor.submit(job).add_done_callback(handle_result)
    print('Refresh completed')

Example Scraper

# aggregator/scrapers.py
import requests
from lxml import etree
import sys

def fetch_github_trending():
    endpoint = 'https://github.com/trending'
    hdr = {'Host': 'github.com', 'Referer': 'https://github.com/explore'}
    resp = requests.get(endpoint, headers=hdr, timeout=5)
    entries = []
    if resp:
        doc = etree.HTML(resp.text)
        articles = doc.xpath("//article[@class='Box-row']")
        for art in articles:
            title = art.xpath('string(./h1/a)').strip()
            link = 'https://github.com' + art.xpath('./h1/a/@href')[0]
            summary = art.xpath('string(./p)').strip()
            entries.append({'title': f'{title}---{summary}', 'url': link})
    return {
        'source_name': 'GitHub',
        'category': 'Trending',
        'scraper': sys._getframe().f_code.co_name,
        'payload': entries
    }

URL Configuration

# aggregator/urls.py
from django.urls import path
from . import views

app_name = 'aggregator'
urlpatterns = [
    path('rankings', views.show_rankings, name='show_rankings'),
]
# Newsite/urls.py
from django.urls import path, include
from django.contrib import admin

urlpatterns = [
    path('admin/', admin.site.urls),
    path('news/', include('aggregator.urls', namespace='aggregator')),
]

Celery Integration

Celery App Factory

# Newsite/celery_app.py
import os
from celery import Celery
from datetime import timedelta

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Newsite.settings')
celery_instance = Celery('NewsiteWorker')
celery_instance.config_from_object('django.conf:settings', namespace='CELERY')
celery_instance.autodiscover_tasks()

celery_instance.conf.update(
    beat_schedule={
        'update-rankings': {
            'task': 'aggregator.jobs.refresh_rankings',
            'schedule': timedelta(seconds=600),
        }
    }
)

Django Import Hook

# Newsite/__init__.py
from .celery_app import celery_instance as celery_app
__all__ = ('celery_app',)

Setttings Adjustments

# Newsite/settings.py
import os

REDIS_HOST = os.environ.get('REDIS_ADDR', 'localhost')
REDIS_PORT = 6379
REDIS_DB = 0

CELERY_ENABLE_UTC = True
CELERY_TIMEZONE = 'UTC'
CELERY_BROKER_URL = f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}'
CELERY_RESULT_BACKEND = f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}'
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'

Scheduler Activation

Launch worker and beat together:

# start_worker.sh
export REDIS_ADDR=127.0.0.1
celery -A Newsite worker -l info -B -f /var/log/celery.log

Avoid running as root. Monitor /var/log/celery.log to verify periodic execution of refresh_rankings.

Tags: Celery

Related Articles

Efficient Usage of HTTP Client in IntelliJ IDEA

IntelliJ IDEA incorporates a versatile HTTP client tool, enabling developres to interact with RESTful services and APIs effectively with in the editor. This functionality streamlines workflows, replac...

Installing CocoaPods on macOS Catalina (10.15) Using a User-Managed Ruby

System Ruby on macOS 10.15 frequently fails to build native gems required by CocoaPods (for example, ffi), leading to errors like: ERROR: Failed to build gem native extension checking for ffi.h... no...

Resolve PhpStorm "Interpreter is not specified or invalid" on WAMP (Windows)

Symptom PhpStorm displays: "Interpreter is not specified or invalid. Press ‘Fix’ to edit your project configuration." This occurs when the IDE cannot locate a valid PHP CLI executable or when the debu...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.