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