Oleksandr Rehush,

python developer

  • I'm Oleksandr Rehush
  • Have ~5 years experience in IT
  • Full stack developer at bvblogic
  • Key technologies: Python, React.js, React Navite 
  • I don't spend time on social networks surfing
  • t.me/o_rehush

Bio

django 

django-channels 1.x

django-channels 2.x

sync code

async code

sync server

async server

http

websockets

DEP 0009

...

 

What we will be talking about?

What is Django?

Django components

  • Model layer (DB connections, ORM, migrations, etc)
  • View layer (URL resolver, request/response classes, middlewares, views classes, etc)
  • Template layer (templating, tags, filters)
  • Forms                                                                                     
  • Admin site
  • Security
  • i18n, l10n
  • GeoDjango
  • Authentication
  • Caching
  • Logging
  • Paginations
  • Sessions
  • Sending emails
  • Signals
  • ...

What Is Django And Why Is It So Popular?

  • It is time-tested

  • You have access to enough Django packages

  • Django has wonderful documentation

  • The Django community is hugely supportive

  • Django advocates best practices for SEO

  • Scalability

  • Security

Source: https://medium.com/swlh/what-is-django-and-why-is-it-so-popular-2b225620cca0

How django works?

Source: https://blog.heroku.com/in_deep_with_django_channels_the_future_of_real_time_apps_in_django

Django channels

Django Channels 1.0

Channels is a project to make Django able to handle more than just plain HTTP requests, including WebSockets and HTTP2, as well as the ability to run code after a response has been sent for things like thumbnailing or background calculation.

 

It’s an easy-to-understand extension of the Django view model, and easy to integrate and deploy.

How channels 1.0 works?

Source: https://blog.heroku.com/in_deep_with_django_channels_the_future_of_real_time_apps_in_django

Django Channels 2.0+

Channels is a project that takes Django and extends its abilities beyond HTTP - to handle WebSockets, chat protocols, IoT protocols, and more. It’s built on a Python specification called ASGI.

 

It does this by taking the core of Django and layering a fully asynchronous layer underneath, running Django itself in a synchronous mode but handling connections and sockets asynchronously, and giving you the choice to write in either style.

Channels is comprised of several packages:

 

  • Channels, the Django integration layer
  • Daphne, the HTTP and Websocket termination server
  • asgiref, the base ASGI library
  • channels_redis, the Redis channel layer backend (optional)

How channels 2.0 works?

How channels 2.0 works?

Sync / Async programming

Sync programming Async programming
Sync
threads
Run synchronous code in synchronous threads Run asynchronous code as synchronous blocking function
Async
event loop
Run synchronous code as coroutine in the event loop Run asynchronous code in the event loop

asgiref

sync_to_async

 

async_to_sync

Example 1. Real-time chat and notifications

routing.py

from channels.routing import ProtocolTypeRouter, URLRouter
from django.urls import path

from .middleware import TokenAuthMiddlewareStack
from .consumers import RoomConversationConsumer, NotificationConsumer

application = ProtocolTypeRouter({
    'websocket': TokenAuthMiddlewareStack(
        URLRouter([
            path('notifications', NotificationConsumer),
            path('conversations/<int:room_id>', RoomConversationConsumer),
        ])
    )
})

middleware.py

from channels.auth import UserLazyObject
from channels.db import database_sync_to_async
from channels.middleware import BaseMiddleware
from channels.sessions import SessionMiddlewareStack
from django.contrib.auth.models import AnonymousUser
from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework_simplejwt.exceptions import TokenError, AuthenticationFailed


class TokenAuthMiddleware(BaseMiddleware):
    jwt_auth = JWTAuthentication()

    def populate_scope(self, scope):
        if "user" not in scope:
            scope["user"] = UserLazyObject()

    async def resolve_scope(self, scope):
        scope["user"]._wrapped = await self.get_user(scope)

    @database_sync_to_async
    def get_user(self, scope):
        token_key = self.get_token(scope)
        try:
            return self.jwt_auth.get_user(self.jwt_auth.get_validated_token(token_key))
        except (TokenError, AuthenticationFailed):
            pass
        return AnonymousUser()

TokenAuthMiddlewareStack = lambda inner: SessionMiddlewareStack(TokenAuthMiddleware(inner))

websocket/base.py

from channels.exceptions import AcceptConnection, DenyConnection
from channels.generic.websocket import AsyncJsonWebsocketConsumer


class AsyncBaseConsumer(AsyncJsonWebsocketConsumer):
    async def resolve_groups(self):
        self.groups = []

    async def connect(self):
        user = self.scope['user']
        if user.is_authenticated:
            raise AcceptConnection
        else:
            raise DenyConnection

    async def websocket_connect(self, message):
        await self.resolve_groups()
        await super(AsyncBaseConsumer, self).websocket_connect(message)

consumers.py

from channels.db import database_sync_to_async

from ..models import Room
from .base import AsyncBaseConsumer


class RoomConversationConsumer(AsyncBaseConsumer):
    async def resolve_groups(self):
        user = self.scope['user']
        room_id = self.scope['url_route']['kwargs']['room_id']
        room = await self.get_room(user, room_id)
        if room:
            self.groups.append('room_%s' % room_id)

    @database_sync_to_async
    def get_room(self, user, room_id):
        return Room.objects.filter(pk=room_id, members__id=user.id).first()


class NotificationConsumer(AsyncBaseConsumer):
    async def resolve_groups(self):
        user = self.scope['user']
        if user.is_authenticated:
            self.groups.append('notifications_%s' % user.pk)

signal_handlers.py

from django.db.models.signals import post_save
from django.dispatch import receiver
from djangorestframework_camel_case.util import camelize

from .serializers import NotificationSerializer, MessageDetailSerializer
from .models import Notification, Message
from .helpers import send_channel_notification, send_message_to_room, transaction_on_commit


@receiver(post_save, sender=Notification)
@transaction_on_commit
def send_notification_to_user(instance: Notification, created, **kwargs):
    if not created:
        return
    event = {
        'type': 'notification',
        'data': camelize(NotificationSerializer(instance=instance).data)
    }
    send_channel_notification(instance.user, event)

@receiver(post_save, sender=Message)
@transaction_on_commit
def send_message_to_channel(instance, created, **kwargs):
    if not created:
        return
    event = {
        'type': 'message',
        'data': camelize(MessageDetailSerializer(instance=instance).data)
    }
    send_message_to_room(instance.room_id, event)

helpers.py

from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer



def send_channel_message(group_name, message):
    channel_layer = get_channel_layer()
    async_to_sync(channel_layer.group_send)(
        group_name, {
            'type': 'channel_message',
            'message': message
        }
    )


def send_channel_notification(user, notification):
    group_name = 'notifications_%s' % user.pk
    send_channel_message(group_name, notification)



def send_message_to_room(room_id, message):
    group_name = 'room_%s' % room_id
    send_channel_message(group_name, message)

Example 2. Async http

views.py

import requests
from django.http import JsonResponse
from .constants import BLOGS


def news_collector_sync_view(request):
    """
    Synchronous HTTP fetcher
    """
    data = {}
    # Go through each blog and fetch it using requests
    for name, link in BLOGS.items():
        response = requests.get(link)
        if response.status_code != 200:
            data[name] = 'Download error'
        else:
            data[name] = response.content.decode("utf-8")
    return JsonResponse(data)

consumers.py

import asyncio
import json
from aiohttp import ClientSession
from channels.generic.http import AsyncHttpConsumer
from .constants import BLOGS


class NewsCollectorAsyncConsumer(AsyncHttpConsumer):
    async def handle(self, body):
        async def fetch(url, session):
            async with session.get(url) as response:
                return await response.read()
        tasks = []
        loop = asyncio.get_event_loop()
        # aiohttp allows a ClientSession object to link all requests together
        async with ClientSession() as session:
            for name, url in BLOGS.items():
                # Launch a coroutine for each URL fetch
                task = loop.create_task(fetch(url, session))
                tasks.append(task)
            # Wait on, and then gather, all responses
            responses = await asyncio.gather(*tasks)

        data = dict(zip(BLOGS.keys(), [r.decode('utf-8') for r in responses]))
        text = json.dumps(data)

        await self.send_response(200,
            text.encode(),
            headers=[
                (b"Content-Type", b"application/json"),
            ]
        )

routing.py

from django.urls import path, re_path
from channels.http import AsgiHandler
from channels.routing import ProtocolTypeRouter, URLRouter
from collector.consumers import NewsCollectorAsyncConsumer


# By default, the ProtocolTypeRouter sets the "http" route to just be Django.
# This overrides it to send it to our asynchronous consumer for a single path,
# and to Django for all other pages.
application = ProtocolTypeRouter({
    "http": URLRouter([
        # Our async news fetcher
        path("collector/collect_news_async/", NewsCollectorAsyncConsumer),

        # AsgiHandler is "the rest of Django" - send things here to have Django
        # views handle them.
        re_path("^", AsgiHandler),
    ]),
})

tests.py

import pytest
from channels.testing import HttpCommunicator
from .consumers import NewsCollectorAsyncConsumer


@pytest.mark.django_db
@pytest.mark.asyncio
async def test_news_collector_consumer():
    communicator = HttpCommunicator(
        NewsCollectorAsyncConsumer,
        "GET",
        "/collector/collect_news_async/"
    )
    response = await communicator.get_response()
    assert response["status"] == 200

Result

DEP 0009:

Async-capable Django

Django Enhancement Proposals (DEPs)

are a formal way of proposing large feature additions

to the Django Web framework

Sequencing

References

  • https://docs.djangoproject.com/en/3.0/releases/3.0/
  • https://asgi.readthedocs.io/en/latest/
  • https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
  • https://channels.readthedocs.io/en/latest/
  • https://github.com/django/deps/blob/master/accepted/0009-async.rst
  • https://habr.com/ru/post/461493/

Let's talk

How to use channels and can django die?

By Tutan Budok

How to use channels and can django die?

  • 402