Oleksandr Rehush,
python developer
django
django-channels 1.x
django-channels 2.x
sync code
async code
sync server
async server
http
websockets
DEP 0009
...
What is Django?
Django components
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:
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
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)
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
Django Enhancement Proposals (DEPs)
are a formal way of proposing large feature additions
to the Django Web framework
Sequencing