mirror of
https://github.com/django/daphne.git
synced 2025-04-21 17:22:03 +03:00
Start making channels work to ASGI spec.
This commit is contained in:
parent
e78f75288d
commit
b9464ca149
|
@ -1,21 +1,7 @@
|
|||
__version__ = "0.8"
|
||||
|
||||
# Load backends, using settings if available (else falling back to a default)
|
||||
DEFAULT_CHANNEL_BACKEND = "default"
|
||||
|
||||
from .backends import BackendManager # isort:skip
|
||||
from django.conf import settings # isort:skip
|
||||
|
||||
channel_backends = BackendManager(
|
||||
getattr(settings, "CHANNEL_BACKENDS", {
|
||||
DEFAULT_CHANNEL_BACKEND: {
|
||||
"BACKEND": "channels.backends.memory.InMemoryChannelBackend",
|
||||
"ROUTING": {},
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
default_app_config = 'channels.apps.ChannelsConfig'
|
||||
DEFAULT_CHANNEL_LAYER = 'default'
|
||||
|
||||
# Promote channel to top-level (down here to avoid circular import errs)
|
||||
from .asgi import channel_layers # NOQA isort:skip
|
||||
from .channel import Channel, Group # NOQA isort:skip
|
||||
|
|
|
@ -1,29 +1,9 @@
|
|||
import functools
|
||||
|
||||
from django.core.handlers.base import BaseHandler
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
|
||||
from channels import Channel
|
||||
|
||||
|
||||
class UrlConsumer(object):
|
||||
"""
|
||||
Dispatches channel HTTP requests into django's URL system.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.handler = BaseHandler()
|
||||
self.handler.load_middleware()
|
||||
|
||||
def __call__(self, message):
|
||||
request = HttpRequest.channel_decode(message.content)
|
||||
try:
|
||||
response = self.handler.get_response(request)
|
||||
except HttpResponse.ResponseLater:
|
||||
return
|
||||
message.reply_channel.send(response.channel_encode())
|
||||
|
||||
|
||||
def view_producer(channel_name):
|
||||
"""
|
||||
Returns a new view function that actually writes the request to a channel
|
||||
|
|
81
channels/asgi.py
Normal file
81
channels/asgi.py
Normal file
|
@ -0,0 +1,81 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import django
|
||||
from django.conf import settings
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
from .consumer_registry import ConsumerRegistry
|
||||
|
||||
|
||||
class InvalidChannelLayerError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class ChannelLayerManager(object):
|
||||
"""
|
||||
Takes a settings dictionary of backends and initialises them on request.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.backends = {}
|
||||
|
||||
@property
|
||||
def configs(self):
|
||||
# Lazy load settings so we can be imported
|
||||
return getattr(settings, "CHANNEL_LAYERS", {})
|
||||
|
||||
def make_backend(self, name):
|
||||
# Load the backend class
|
||||
try:
|
||||
backend_class = import_string(self.configs[name]['BACKEND'])
|
||||
except KeyError:
|
||||
raise InvalidChannelLayerError("No BACKEND specified for %s" % name)
|
||||
except ImportError:
|
||||
raise InvalidChannelLayerError(
|
||||
"Cannot import BACKEND %r specified for %s" % (self.configs[name]['BACKEND'], name)
|
||||
)
|
||||
# Get routing
|
||||
try:
|
||||
routing = self.configs[name]['ROUTING']
|
||||
except KeyError:
|
||||
raise InvalidChannelLayerError("No ROUTING specified for %s" % name)
|
||||
# Initialise and pass config
|
||||
asgi_layer = backend_class(**self.configs[name].get("CONFIG", {}))
|
||||
return ChannelLayerWrapper(
|
||||
channel_layer=asgi_layer,
|
||||
alias=name,
|
||||
routing=routing,
|
||||
)
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key not in self.backends:
|
||||
self.backends[key] = self.make_backend(key)
|
||||
return self.backends[key]
|
||||
|
||||
|
||||
class ChannelLayerWrapper(object):
|
||||
"""
|
||||
Top level channel layer wrapper, which contains both the ASGI channel
|
||||
layer object as well as alias and routing information specific to Django.
|
||||
"""
|
||||
|
||||
def __init__(self, channel_layer, alias, routing):
|
||||
self.channel_layer = channel_layer
|
||||
self.alias = alias
|
||||
self.routing = routing
|
||||
self.registry = ConsumerRegistry(self.routing)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.channel_layer, name)
|
||||
|
||||
|
||||
def get_channel_layer(alias="default"):
|
||||
"""
|
||||
Returns the raw ASGI channel layer for this project.
|
||||
"""
|
||||
django.setup(set_prefix=False)
|
||||
return channel_layers[alias].channel_layer
|
||||
|
||||
|
||||
# Default global instance of the channel layer manager
|
||||
channel_layers = ChannelLayerManager()
|
|
@ -1,7 +1,7 @@
|
|||
import random
|
||||
import string
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from channels import DEFAULT_CHANNEL_BACKEND, channel_backends
|
||||
from django.utils import six
|
||||
from channels import DEFAULT_CHANNEL_LAYER, channel_layers
|
||||
|
||||
|
||||
class Channel(object):
|
||||
|
@ -16,15 +16,17 @@ class Channel(object):
|
|||
"default" one by default.
|
||||
"""
|
||||
|
||||
def __init__(self, name, alias=DEFAULT_CHANNEL_BACKEND, channel_backend=None):
|
||||
def __init__(self, name, alias=DEFAULT_CHANNEL_LAYER, channel_layer=None):
|
||||
"""
|
||||
Create an instance for the channel named "name"
|
||||
"""
|
||||
if isinstance(name, six.binary_type):
|
||||
name = name.decode("ascii")
|
||||
self.name = name
|
||||
if channel_backend:
|
||||
self.channel_backend = channel_backend
|
||||
if channel_layer:
|
||||
self.channel_layer = channel_layer
|
||||
else:
|
||||
self.channel_backend = channel_backends[alias]
|
||||
self.channel_layer = channel_layers[alias]
|
||||
|
||||
def send(self, content):
|
||||
"""
|
||||
|
@ -32,17 +34,7 @@ class Channel(object):
|
|||
"""
|
||||
if not isinstance(content, dict):
|
||||
raise ValueError("You can only send dicts as content on channels.")
|
||||
self.channel_backend.send(self.name, content)
|
||||
|
||||
@classmethod
|
||||
def new_name(self, prefix):
|
||||
"""
|
||||
Returns a new channel name that's unique and not closed
|
||||
with the given prefix. Does not need to be called before sending
|
||||
on a channel name - just provides a way to avoid clashing for
|
||||
response channels.
|
||||
"""
|
||||
return "%s.%s" % (prefix, "".join(random.choice(string.ascii_letters) for i in range(32)))
|
||||
self.channel_layer.send(self.name, content)
|
||||
|
||||
def as_view(self):
|
||||
"""
|
||||
|
@ -63,27 +55,29 @@ class Group(object):
|
|||
of the group after an expiry time (keep re-adding to keep them in).
|
||||
"""
|
||||
|
||||
def __init__(self, name, alias=DEFAULT_CHANNEL_BACKEND, channel_backend=None):
|
||||
def __init__(self, name, alias=DEFAULT_CHANNEL_LAYER, channel_layer=None):
|
||||
if isinstance(name, six.binary_type):
|
||||
name = name.decode("ascii")
|
||||
self.name = name
|
||||
if channel_backend:
|
||||
self.channel_backend = channel_backend
|
||||
if channel_layer:
|
||||
self.channel_layer = channel_layer
|
||||
else:
|
||||
self.channel_backend = channel_backends[alias]
|
||||
self.channel_layer = channel_layers[alias]
|
||||
|
||||
def add(self, channel):
|
||||
if isinstance(channel, Channel):
|
||||
channel = channel.name
|
||||
self.channel_backend.group_add(self.name, channel)
|
||||
self.channel_layer.group_add(self.name, channel)
|
||||
|
||||
def discard(self, channel):
|
||||
if isinstance(channel, Channel):
|
||||
channel = channel.name
|
||||
self.channel_backend.group_discard(self.name, channel)
|
||||
self.channel_layer.group_discard(self.name, channel)
|
||||
|
||||
def channels(self):
|
||||
return self.channel_backend.group_channels(self.name)
|
||||
return self.channel_layer.group_channels(self.name)
|
||||
|
||||
def send(self, content):
|
||||
if not isinstance(content, dict):
|
||||
raise ValueError("You can only send dicts as content on channels.")
|
||||
self.channel_backend.send_group(self.name, content)
|
||||
self.channel_layer.send_group(self.name, content)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import importlib
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
@ -35,6 +37,11 @@ class ConsumerRegistry(object):
|
|||
# Upconvert if you just pass in a string for channels
|
||||
if isinstance(channels, six.string_types):
|
||||
channels = [channels]
|
||||
# Make sure all channels are byte strings
|
||||
channels = [
|
||||
channel.decode("ascii") if isinstance(channel, six.binary_type) else channel
|
||||
for channel in channels
|
||||
]
|
||||
# Import any consumer referenced as string
|
||||
if isinstance(consumer, six.string_types):
|
||||
module_name, variable_name = consumer.rsplit(".", 1)
|
||||
|
|
|
@ -1,33 +1,11 @@
|
|||
from django.core.handlers.base import BaseHandler
|
||||
from django.http.request import HttpRequest
|
||||
from django.http.response import HttpResponseBase
|
||||
|
||||
from .request import decode_request, encode_request
|
||||
from .response import ResponseLater, decode_response, encode_response
|
||||
|
||||
|
||||
def monkeypatch_django():
|
||||
"""
|
||||
Monkeypatches support for us into parts of Django.
|
||||
"""
|
||||
# Request encode/decode
|
||||
HttpRequest.channel_encode = encode_request
|
||||
HttpRequest.channel_decode = staticmethod(decode_request)
|
||||
# Response encode/decode
|
||||
HttpResponseBase.channel_encode = encode_response
|
||||
HttpResponseBase.channel_decode = staticmethod(decode_response)
|
||||
HttpResponseBase.ResponseLater = ResponseLater
|
||||
# Allow ResponseLater to propagate above handler
|
||||
BaseHandler.old_handle_uncaught_exception = BaseHandler.handle_uncaught_exception
|
||||
BaseHandler.handle_uncaught_exception = new_handle_uncaught_exception
|
||||
# Ensure that the staticfiles version of runserver bows down to us
|
||||
# This one is particularly horrible
|
||||
from django.contrib.staticfiles.management.commands.runserver import Command as StaticRunserverCommand
|
||||
from .management.commands.runserver import Command as RunserverCommand
|
||||
StaticRunserverCommand.__bases__ = (RunserverCommand, )
|
||||
|
||||
|
||||
def new_handle_uncaught_exception(self, request, resolver, exc_info):
|
||||
if exc_info[0] is ResponseLater:
|
||||
raise
|
||||
return BaseHandler.old_handle_uncaught_exception(self, request, resolver, exc_info)
|
||||
|
|
191
channels/handler.py
Normal file
191
channels/handler.py
Normal file
|
@ -0,0 +1,191 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import sys
|
||||
import logging
|
||||
from threading import Lock
|
||||
|
||||
from django import http
|
||||
from django.core.handlers import base
|
||||
from django.core import signals
|
||||
from django.core.urlresolvers import set_script_prefix
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
logger = logging.getLogger('django.request')
|
||||
|
||||
|
||||
class AsgiRequest(http.HttpRequest):
|
||||
"""
|
||||
Custom request subclass that decodes from an ASGI-standard request
|
||||
dict, and wraps request body handling.
|
||||
"""
|
||||
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
self.reply_channel = self.message['reply_channel']
|
||||
self._content_length = 0
|
||||
# Path info
|
||||
self.path = self.message['path']
|
||||
self.script_name = self.message.get('root_path', '')
|
||||
if self.script_name:
|
||||
# TODO: Better is-prefix checking, slash handling?
|
||||
self.path_info = self.path[len(self.script_name):]
|
||||
else:
|
||||
self.path_info = self.path
|
||||
# HTTP basics
|
||||
self.method = self.message['method'].upper()
|
||||
self.META = {
|
||||
"REQUEST_METHOD": self.method,
|
||||
"QUERY_STRING": self.message.get('query_string', ''),
|
||||
}
|
||||
if self.message.get('client', None):
|
||||
self.META['REMOTE_ADDR'] = self.message['client'][0]
|
||||
self.META['REMOTE_HOST'] = self.META['REMOTE_ADDR']
|
||||
self.META['REMOTE_PORT'] = self.message['client'][1]
|
||||
if self.message.get('server', None):
|
||||
self.META['SERVER_NAME'] = self.message['server'][0]
|
||||
self.META['SERVER_PORT'] = self.message['server'][1]
|
||||
# Headers go into META
|
||||
for name, value in self.message.get('headers', {}).items():
|
||||
if name == "content_length":
|
||||
corrected_name = "CONTENT_LENGTH"
|
||||
elif name == "content_type":
|
||||
corrected_name = "CONTENT_TYPE"
|
||||
else:
|
||||
corrected_name = 'HTTP_%s' % name.upper().replace("-", "_")
|
||||
self.META[corrected_name] = value
|
||||
# Pull out content length info
|
||||
if self.META.get('CONTENT_LENGTH', None):
|
||||
try:
|
||||
self._content_length = int(self.META['CONTENT_LENGTH'])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
# TODO: body handling
|
||||
self._body = ""
|
||||
# Other bits
|
||||
self.resolver_match = None
|
||||
|
||||
@cached_property
|
||||
def GET(self):
|
||||
return http.QueryDict(
|
||||
self.message.get('query_string', ''),
|
||||
encoding=self._encoding,
|
||||
)
|
||||
|
||||
def _get_post(self):
|
||||
if not hasattr(self, '_post'):
|
||||
self._load_post_and_files()
|
||||
return self._post
|
||||
|
||||
def _set_post(self, post):
|
||||
self._post = post
|
||||
|
||||
POST = property(_get_post, _set_post)
|
||||
|
||||
@cached_property
|
||||
def COOKIES(self):
|
||||
return http.parse_cookie(self.META.get('HTTP_COOKIE', ''))
|
||||
|
||||
|
||||
class AsgiHandler(base.BaseHandler):
|
||||
"""
|
||||
Handler for ASGI requests for the view system only (it will have got here
|
||||
after traversing the dispatch-by-channel-name system, which decides it's
|
||||
a HTTP request)
|
||||
"""
|
||||
|
||||
initLock = Lock()
|
||||
request_class = AsgiRequest
|
||||
|
||||
def __call__(self, message):
|
||||
# Set up middleware if needed. We couldn't do this earlier, because
|
||||
# settings weren't available.
|
||||
if self._request_middleware is None:
|
||||
with self.initLock:
|
||||
# Check that middleware is still uninitialized.
|
||||
if self._request_middleware is None:
|
||||
self.load_middleware()
|
||||
# Set script prefix from message root_path
|
||||
set_script_prefix(message.get('root_path', ''))
|
||||
signals.request_started.send(sender=self.__class__, message=message)
|
||||
# Run request through view system
|
||||
try:
|
||||
request = self.request_class(message)
|
||||
except UnicodeDecodeError:
|
||||
logger.warning(
|
||||
'Bad Request (UnicodeDecodeError)',
|
||||
exc_info=sys.exc_info(),
|
||||
extra={
|
||||
'status_code': 400,
|
||||
}
|
||||
)
|
||||
response = http.HttpResponseBadRequest()
|
||||
else:
|
||||
response = self.get_response(request)
|
||||
# Transform response into messages, which we yield back to caller
|
||||
for message in self.encode_response(response):
|
||||
# TODO: file_to_stream
|
||||
yield message
|
||||
|
||||
def encode_response(self, response):
|
||||
"""
|
||||
Encodes a Django HTTP response into an ASGI http.response message(s).
|
||||
"""
|
||||
# Collect cookies into headers
|
||||
response_headers = [(str(k), str(v)) for k, v in response.items()]
|
||||
for c in response.cookies.values():
|
||||
response_headers.append((str('Set-Cookie'), str(c.output(header=''))))
|
||||
# Make initial response message
|
||||
message = {
|
||||
"status": response.status_code,
|
||||
"status_text": response.reason_phrase,
|
||||
"headers": response_headers,
|
||||
}
|
||||
# Streaming responses need to be pinned to their iterator
|
||||
if response.streaming:
|
||||
for part in response.streaming_content:
|
||||
for chunk in self.chunk_bytes(part):
|
||||
message['content'] = chunk
|
||||
message['more_content'] = True
|
||||
yield message
|
||||
message = {}
|
||||
# Final closing message
|
||||
yield {
|
||||
"more_content": False,
|
||||
}
|
||||
# Other responses just need chunking
|
||||
else:
|
||||
# Yield chunks of response
|
||||
for chunk, last in self.chunk_bytes(response.content):
|
||||
message['content'] = chunk
|
||||
message['more_content'] = not last
|
||||
yield message
|
||||
message = {}
|
||||
|
||||
def chunk_bytes(self, data):
|
||||
"""
|
||||
Chunks some data into chunks based on the current ASGI channel layer's
|
||||
message size and reasonable defaults.
|
||||
|
||||
Yields (chunk, last_chunk) tuples.
|
||||
"""
|
||||
CHUNK_SIZE = 512 * 1024
|
||||
position = 0
|
||||
while position < len(data):
|
||||
yield (
|
||||
data[position:position+CHUNK_SIZE],
|
||||
(position + CHUNK_SIZE) >= len(data),
|
||||
)
|
||||
position += CHUNK_SIZE
|
||||
|
||||
|
||||
class ViewConsumer(object):
|
||||
"""
|
||||
Dispatches channel HTTP requests into django's URL/View system.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.handler = AsgiHandler()
|
||||
|
||||
def __call__(self, message):
|
||||
for reply_message in self.handler(message.content):
|
||||
message.reply_channel.send(reply_message)
|
|
@ -3,8 +3,8 @@ import threading
|
|||
from django.core.management.commands.runserver import \
|
||||
Command as RunserverCommand
|
||||
|
||||
from channels import DEFAULT_CHANNEL_BACKEND, channel_backends
|
||||
from channels.adapters import UrlConsumer
|
||||
from channels import DEFAULT_CHANNEL_LAYER, channel_layers
|
||||
from channels.handler import ViewConsumer
|
||||
from channels.interfaces.wsgi import WSGIInterface
|
||||
from channels.log import setup_logger
|
||||
from channels.worker import Worker
|
||||
|
@ -21,7 +21,7 @@ class Command(RunserverCommand):
|
|||
"""
|
||||
Returns the default WSGI handler for the runner.
|
||||
"""
|
||||
return WSGIInterface(self.channel_backend)
|
||||
return WSGIInterface(self.channel_layer)
|
||||
|
||||
def run(self, *args, **options):
|
||||
# Run the rest
|
||||
|
@ -29,16 +29,16 @@ class Command(RunserverCommand):
|
|||
|
||||
def inner_run(self, *args, **options):
|
||||
# Check a handler is registered for http reqs
|
||||
self.channel_backend = channel_backends[DEFAULT_CHANNEL_BACKEND]
|
||||
if not self.channel_backend.registry.consumer_for_channel("http.request"):
|
||||
self.channel_layer = channel_layers[DEFAULT_CHANNEL_LAYER]
|
||||
if not self.channel_layer.registry.consumer_for_channel("http.request"):
|
||||
# Register the default one
|
||||
self.channel_backend.registry.add_consumer(UrlConsumer(), ["http.request"])
|
||||
self.channel_layer.registry.add_consumer(ViewConsumer(), ["http.request"])
|
||||
# Note that this is the right one on the console
|
||||
self.logger.info("Worker thread running, channels enabled")
|
||||
if self.channel_backend.local_only:
|
||||
if self.channel_layer.local_only:
|
||||
self.logger.info("Local channel backend detected, no remote channels support")
|
||||
# Launch a worker thread
|
||||
worker = WorkerThread(self.channel_backend)
|
||||
worker = WorkerThread(self.channel_layer)
|
||||
worker.daemon = True
|
||||
worker.start()
|
||||
# Run rest of inner run
|
||||
|
@ -50,9 +50,9 @@ class WorkerThread(threading.Thread):
|
|||
Class that runs a worker
|
||||
"""
|
||||
|
||||
def __init__(self, channel_backend):
|
||||
def __init__(self, channel_layer):
|
||||
super(WorkerThread, self).__init__()
|
||||
self.channel_backend = channel_backend
|
||||
self.channel_layer = channel_layer
|
||||
|
||||
def run(self):
|
||||
Worker(channel_backend=self.channel_backend).run()
|
||||
Worker(channel_layer=self.channel_layer).run()
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.management import BaseCommand, CommandError
|
||||
from channels import channel_backends, DEFAULT_CHANNEL_BACKEND
|
||||
from channels import channel_layers, DEFAULT_CHANNEL_LAYER
|
||||
from channels.log import setup_logger
|
||||
from channels.adapters import UrlConsumer
|
||||
from channels.handler import ViewConsumer
|
||||
from channels.worker import Worker
|
||||
|
||||
|
||||
|
@ -12,25 +13,20 @@ class Command(BaseCommand):
|
|||
# Get the backend to use
|
||||
self.verbosity = options.get("verbosity", 1)
|
||||
self.logger = setup_logger('django.channels', self.verbosity)
|
||||
channel_backend = channel_backends[DEFAULT_CHANNEL_BACKEND]
|
||||
if channel_backend.local_only:
|
||||
raise CommandError(
|
||||
"You have a process-local channel backend configured, and so cannot run separate workers.\n"
|
||||
"Configure a network-based backend in CHANNEL_BACKENDS to use this command."
|
||||
)
|
||||
channel_layer = channel_layers[DEFAULT_CHANNEL_LAYER]
|
||||
# Check a handler is registered for http reqs
|
||||
if not channel_backend.registry.consumer_for_channel("http.request"):
|
||||
if not channel_layer.registry.consumer_for_channel("http.request"):
|
||||
# Register the default one
|
||||
channel_backend.registry.add_consumer(UrlConsumer(), ["http.request"])
|
||||
channel_layer.registry.add_consumer(ViewConsumer(), ["http.request"])
|
||||
# Launch a worker
|
||||
self.logger.info("Running worker against backend %s", channel_backend)
|
||||
self.logger.info("Running worker against backend %s", channel_layer.alias)
|
||||
# Optionally provide an output callback
|
||||
callback = None
|
||||
if self.verbosity > 1:
|
||||
callback = self.consumer_called
|
||||
# Run the worker
|
||||
try:
|
||||
Worker(channel_backend=channel_backend, callback=callback).run()
|
||||
Worker(channel_layer=channel_layer, callback=callback).run()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from .channel import Channel
|
||||
|
||||
|
||||
|
@ -17,9 +19,9 @@ class Message(object):
|
|||
"""
|
||||
pass
|
||||
|
||||
def __init__(self, content, channel, channel_backend, reply_channel=None):
|
||||
def __init__(self, content, channel, channel_layer, reply_channel=None):
|
||||
self.content = content
|
||||
self.channel = channel
|
||||
self.channel_backend = channel_backend
|
||||
self.channel_layer = channel_layer
|
||||
if reply_channel:
|
||||
self.reply_channel = Channel(reply_channel, channel_backend=self.channel_backend)
|
||||
self.reply_channel = Channel(reply_channel, channel_layer=self.channel_layer)
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
from django.http import HttpRequest
|
||||
from django.http.request import QueryDict
|
||||
from django.utils.datastructures import MultiValueDict
|
||||
|
||||
|
||||
def encode_request(request):
|
||||
"""
|
||||
Encodes a request to JSON-compatible datastructures
|
||||
"""
|
||||
# TODO: More stuff
|
||||
value = {
|
||||
"get": dict(request.GET.lists()),
|
||||
"post": dict(request.POST.lists()),
|
||||
"cookies": request.COOKIES,
|
||||
"headers": {
|
||||
k[5:].lower(): v
|
||||
for k, v in request.META.items()
|
||||
if k.lower().startswith("http_")
|
||||
},
|
||||
"path": request.path,
|
||||
"root_path": request.META.get("SCRIPT_NAME", ""),
|
||||
"method": request.method,
|
||||
"reply_channel": request.reply_channel,
|
||||
"server": [
|
||||
request.META.get("SERVER_NAME", None),
|
||||
request.META.get("SERVER_PORT", None),
|
||||
],
|
||||
"client": [
|
||||
request.META.get("REMOTE_ADDR", None),
|
||||
request.META.get("REMOTE_PORT", None),
|
||||
],
|
||||
}
|
||||
return value
|
||||
|
||||
|
||||
def decode_request(value):
|
||||
"""
|
||||
Decodes a request JSONish value to a HttpRequest object.
|
||||
"""
|
||||
request = HttpRequest()
|
||||
request.GET = CustomQueryDict(value['get'])
|
||||
request.POST = CustomQueryDict(value['post'])
|
||||
request.COOKIES = value['cookies']
|
||||
request.path = value['path']
|
||||
request.method = value['method']
|
||||
request.reply_channel = value['reply_channel']
|
||||
# Channels requests are more high-level than the dumping ground that is
|
||||
# META; re-combine back into it
|
||||
request.META = {
|
||||
"REQUEST_METHOD": value["method"],
|
||||
"SERVER_NAME": value["server"][0],
|
||||
"SERVER_PORT": value["server"][1],
|
||||
"REMOTE_ADDR": value["client"][0],
|
||||
"REMOTE_HOST": value["client"][0], # Not the DNS name, hopefully fine.
|
||||
"SCRIPT_NAME": value["root_path"],
|
||||
}
|
||||
for header, header_value in value.get("headers", {}).items():
|
||||
request.META["HTTP_%s" % header.upper()] = header_value
|
||||
# Derive path_info from script root
|
||||
request.path_info = request.path
|
||||
if request.META.get("SCRIPT_NAME", ""):
|
||||
request.path_info = request.path_info[len(request.META["SCRIPT_NAME"]):]
|
||||
return request
|
||||
|
||||
|
||||
class CustomQueryDict(QueryDict):
|
||||
"""
|
||||
Custom override of QueryDict that sets things directly.
|
||||
"""
|
||||
|
||||
def __init__(self, values, mutable=False, encoding=None):
|
||||
""" mutable and encoding are ignored :( """
|
||||
MultiValueDict.__init__(self, values)
|
|
@ -1,43 +0,0 @@
|
|||
from django.http import HttpResponse
|
||||
from django.utils.six import PY3
|
||||
|
||||
|
||||
def encode_response(response):
|
||||
"""
|
||||
Encodes a response to JSON-compatible datastructures
|
||||
"""
|
||||
value = {
|
||||
"content_type": getattr(response, "content_type", None),
|
||||
"content": response.content,
|
||||
"status": response.status_code,
|
||||
"headers": list(response._headers.values()),
|
||||
"cookies": [v.output(header="") for _, v in response.cookies.items()]
|
||||
}
|
||||
if PY3:
|
||||
value["content"] = value["content"].decode('utf8')
|
||||
response.close()
|
||||
return value
|
||||
|
||||
|
||||
def decode_response(value):
|
||||
"""
|
||||
Decodes a response JSONish value to a HttpResponse object.
|
||||
"""
|
||||
response = HttpResponse(
|
||||
content=value['content'],
|
||||
content_type=value['content_type'],
|
||||
status=value['status'],
|
||||
)
|
||||
for cookie in value['cookies']:
|
||||
response.cookies.load(cookie)
|
||||
response._headers = {k.lower(): (k, v) for k, v in value['headers']}
|
||||
return response
|
||||
|
||||
|
||||
class ResponseLater(Exception):
|
||||
"""
|
||||
Class that represents a response which will be sent down the response
|
||||
channel later. Used to move a django view-based segment onto the next
|
||||
task, as otherwise we'd need to write some kind of fake response.
|
||||
"""
|
||||
pass
|
|
@ -1,4 +1,7 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
from .message import Message
|
||||
from .utils import name_that_thing
|
||||
|
@ -12,30 +15,35 @@ class Worker(object):
|
|||
and runs their consumers.
|
||||
"""
|
||||
|
||||
def __init__(self, channel_backend, callback=None):
|
||||
self.channel_backend = channel_backend
|
||||
def __init__(self, channel_layer, callback=None):
|
||||
self.channel_layer = channel_layer
|
||||
self.callback = callback
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Tries to continually dispatch messages to consumers.
|
||||
"""
|
||||
channels = self.channel_backend.registry.all_channel_names()
|
||||
channels = self.channel_layer.registry.all_channel_names()
|
||||
while True:
|
||||
channel, content = self.channel_backend.receive_many_blocking(channels)
|
||||
channel, content = self.channel_layer.receive_many(channels, block=True)
|
||||
# If no message, stall a little to avoid busy-looping then continue
|
||||
if channel is None:
|
||||
time.sleep(0.01)
|
||||
continue
|
||||
# Create message wrapper
|
||||
message = Message(
|
||||
content=content,
|
||||
channel=channel,
|
||||
channel_backend=self.channel_backend,
|
||||
channel_layer=self.channel_layer,
|
||||
reply_channel=content.get("reply_channel", None),
|
||||
)
|
||||
# Handle the message
|
||||
consumer = self.channel_backend.registry.consumer_for_channel(channel)
|
||||
consumer = self.channel_layer.registry.consumer_for_channel(channel)
|
||||
if self.callback:
|
||||
self.callback(channel, message)
|
||||
try:
|
||||
consumer(message)
|
||||
except Message.Requeue:
|
||||
self.channel_backend.send(channel, content)
|
||||
self.channel_layer.send(channel, content)
|
||||
except:
|
||||
logger.exception("Error processing message with consumer %s:", name_that_thing(consumer))
|
||||
|
|
|
@ -101,12 +101,18 @@ Channels and Messages
|
|||
---------------------
|
||||
|
||||
All communication in an ASGI stack uses *messages* sent over *channels*.
|
||||
All messages must be a ``dict`` at the top level of the object, and be
|
||||
serializable by the built-in ``json`` serializer module (though the
|
||||
actual serialization a channel layer uses is up to the implementation;
|
||||
we use ``json`` as the lowest common denominator).
|
||||
All messages must be a ``dict`` at the top level of the object, and
|
||||
contain only the following types to ensure serializability:
|
||||
|
||||
Channels are identified by a byte string name consisting only of ASCII
|
||||
* Byte strings
|
||||
* Unicode strings
|
||||
* Integers (no longs)
|
||||
* Lists (tuples should be treated as lists)
|
||||
* Dicts (keys must be unicode strings)
|
||||
* Booleans
|
||||
* None
|
||||
|
||||
Channels are identified by a unicode string name consisting only of ASCII
|
||||
letters, numbers, numerical digits, periods (``.``), dashes (``-``)
|
||||
and underscores (``_``), plus an optional prefix character (see below).
|
||||
|
||||
|
@ -270,20 +276,20 @@ A *channel layer* should provide an object with these attributes
|
|||
(all function arguments are positional):
|
||||
|
||||
* ``send(channel, message)``, a callable that takes two arguments; the
|
||||
channel to send on, as a byte string, and the message
|
||||
channel to send on, as a unicode string, and the message
|
||||
to send, as a serializable ``dict``.
|
||||
|
||||
* ``receive_many(channels, block=False)``, a callable that takes a list of channel
|
||||
names as byte strings, and returns with either ``(None, None)``
|
||||
names as unicode strings, and returns with either ``(None, None)``
|
||||
or ``(channel, message)`` if a message is available. If ``block`` is True, then
|
||||
it will not return until after a built-in timeout or a message arrives; if
|
||||
``block`` is false, it will always return immediately. It is perfectly
|
||||
valid to ignore ``block`` and always return immediately.
|
||||
|
||||
* ``new_channel(pattern)``, a callable that takes a byte string pattern,
|
||||
* ``new_channel(pattern)``, a callable that takes a unicode string pattern,
|
||||
and returns a new valid channel name that does not already exist, by
|
||||
substituting any occurrences of the question mark character ``?`` in
|
||||
``pattern`` with a single random byte string and checking for
|
||||
``pattern`` with a single random unicode string and checking for
|
||||
existence of that name in the channel layer. This is NOT called prior to
|
||||
a message being sent on a channel, and should not be used for channel
|
||||
initialization.
|
||||
|
@ -298,14 +304,14 @@ A *channel layer* should provide an object with these attributes
|
|||
A channel layer implementing the ``groups`` extension must also provide:
|
||||
|
||||
* ``group_add(group, channel)``, a callable that takes a ``channel`` and adds
|
||||
it to the group given by ``group``. Both are byte strings. If the channel
|
||||
it to the group given by ``group``. Both are unicode strings. If the channel
|
||||
is already in the group, the function should return normally.
|
||||
|
||||
* ``group_discard(group, channel)``, a callable that removes the ``channel``
|
||||
from the ``group`` if it is in it, and does nothing otherwise.
|
||||
|
||||
* ``send_group(group, message)``, a callable that takes two positional
|
||||
arguments; the group to send to, as a byte string, and the message
|
||||
arguments; the group to send to, as a unicode string, and the message
|
||||
to send, as a serializable ``dict``.
|
||||
|
||||
A channel layer implementing the ``statistics`` extension must also provide:
|
||||
|
@ -423,11 +429,11 @@ Keys:
|
|||
* ``reply_channel``: Channel name for responses and server pushes, in
|
||||
format ``http.response.?``
|
||||
|
||||
* ``http_version``: Byte string, one of ``1.0``, ``1.1`` or ``2``.
|
||||
* ``http_version``: Unicode string, one of ``1.0``, ``1.1`` or ``2``.
|
||||
|
||||
* ``method``: Byte string HTTP method name, uppercased.
|
||||
* ``method``: Unicode string HTTP method name, uppercased.
|
||||
|
||||
* ``scheme``: Byte string URL scheme portion (likely ``http`` or ``https``).
|
||||
* ``scheme``: Unicode string URL scheme portion (likely ``http`` or ``https``).
|
||||
Optional (but must not be empty), default is ``http``.
|
||||
|
||||
* ``path``: Byte string HTTP path from URL.
|
||||
|
@ -442,20 +448,46 @@ Keys:
|
|||
* ``headers``: Dict of ``{name: value}``, where ``name`` is the lowercased
|
||||
HTTP header name as byte string and ``value`` is the header value as a byte
|
||||
string. If multiple headers with the same name are received, they should
|
||||
be concatenated into a single header as per .
|
||||
be concatenated into a single header as per RFC 2616.
|
||||
|
||||
* ``body``: Body of the request, as a byte string. Optional, defaults to empty
|
||||
string.
|
||||
string. If ``body_channel`` is set, treat as start of body and concatenate
|
||||
on further chunks.
|
||||
|
||||
* ``client``: List of ``[host, port]`` where ``host`` is a byte string of the
|
||||
* ``body_channel``: Single-reader unicode string channel name that contains
|
||||
Request Body Chunk messages representing a large request body.
|
||||
Optional, defaults to None. Chunks append to ``body`` if set. Presence of
|
||||
a channel indicates at least one Request Body Chunk message needs to be read,
|
||||
and then further consumption keyed off of the ``more_content`` key in those
|
||||
messages.
|
||||
|
||||
* ``client``: List of ``[host, port]`` where ``host`` is a unicode string of the
|
||||
remote host's IPv4 or IPv6 address, and ``port`` is the remote port as an
|
||||
integer. Optional, defaults to ``None``.
|
||||
|
||||
* ``server``: List of ``[host, port]`` where ``host`` is the listening address
|
||||
for this server as a byte string, and ``port`` is the integer listening port.
|
||||
for this server as a unicode string, and ``port`` is the integer listening port.
|
||||
Optional, defaults to ``None``.
|
||||
|
||||
|
||||
Request Body Chunk
|
||||
''''''''''''''''''
|
||||
|
||||
Must be sent after an initial Response.
|
||||
|
||||
Channel: ``http.request.body.?``
|
||||
|
||||
Keys:
|
||||
|
||||
* ``content``: Byte string of HTTP body content, will be concatenated onto
|
||||
previously received ``content`` values and ``body`` key in Request.
|
||||
|
||||
* ``more_content``: Boolean value signifying if there is additional content
|
||||
to come (as part of a Request Body Chunk message). If ``False``, request will
|
||||
be taken as complete, and any further messages on the channel
|
||||
will be ignored. Optional, defaults to ``False``.
|
||||
|
||||
|
||||
Response
|
||||
''''''''
|
||||
|
||||
|
@ -475,7 +507,8 @@ Keys:
|
|||
string header name, and ``value`` is the byte string header value. Order
|
||||
should be preserved in the HTTP response.
|
||||
|
||||
* ``content``: Byte string of HTTP body content
|
||||
* ``content``: Byte string of HTTP body content.
|
||||
Optional, defaults to empty string.
|
||||
|
||||
* ``more_content``: Boolean value signifying if there is additional content
|
||||
to come (as part of a Response Chunk message). If ``False``, response will
|
||||
|
@ -534,7 +567,7 @@ Keys:
|
|||
* ``reply_channel``: Channel name for sending data, in
|
||||
format ``websocket.send.?``
|
||||
|
||||
* ``scheme``: Byte string URL scheme portion (likely ``ws`` or ``wss``).
|
||||
* ``scheme``: Unicode string URL scheme portion (likely ``ws`` or ``wss``).
|
||||
Optional (but must not be empty), default is ``ws``.
|
||||
|
||||
* ``path``: Byte string HTTP path from URL.
|
||||
|
@ -551,12 +584,12 @@ Keys:
|
|||
string. If multiple headers with the same name are received, they should
|
||||
be concatenated into a single header as per .
|
||||
|
||||
* ``client``: List of ``[host, port]`` where ``host`` is a byte string of the
|
||||
* ``client``: List of ``[host, port]`` where ``host`` is a unicode string of the
|
||||
remote host's IPv4 or IPv6 address, and ``port`` is the remote port as an
|
||||
integer. Optional, defaults to ``None``.
|
||||
|
||||
* ``server``: List of ``[host, port]`` where ``host`` is the listening address
|
||||
for this server as a byte string, and ``port`` is the integer listening port.
|
||||
for this server as a unicode string, and ``port`` is the integer listening port.
|
||||
Optional, defaults to ``None``.
|
||||
|
||||
|
||||
|
@ -644,12 +677,12 @@ Keys:
|
|||
|
||||
* ``data``: Byte string of UDP datagram payload.
|
||||
|
||||
* ``client``: List of ``[host, port]`` where ``host`` is a byte string of the
|
||||
* ``client``: List of ``[host, port]`` where ``host`` is a unicode string of the
|
||||
remote host's IPv4 or IPv6 address, and ``port`` is the remote port as an
|
||||
integer.
|
||||
|
||||
* ``server``: List of ``[host, port]`` where ``host`` is the listening address
|
||||
for this server as a byte string, and ``port`` is the integer listening port.
|
||||
for this server as a unicode string, and ``port`` is the integer listening port.
|
||||
Optional, defaults to ``None``.
|
||||
|
||||
|
||||
|
@ -700,8 +733,13 @@ underlying implementation, then any values should be kept within the lower
|
|||
This document will never specify just *string* - all strings are one of the
|
||||
two types.
|
||||
|
||||
Channel and group names are always byte strings, with the additional limitation
|
||||
that they only use the following characters:
|
||||
Some serializers, such as ``json``, cannot differentiate between byte
|
||||
strings and unicode strings; these should include logic to box one type as
|
||||
the other (for example, encoding byte strings as base64 unicode strings with
|
||||
a preceding special character, e.g. U+FFFF).
|
||||
|
||||
Channel and group names are always unicode strings, with the additional
|
||||
limitation that they only use the following characters:
|
||||
|
||||
* ASCII letters
|
||||
* The digits ``0`` through ``9``
|
||||
|
@ -747,7 +785,7 @@ WSGI's ``environ`` variable to the Request message:
|
|||
* ``CONTENT_TYPE`` can be extracted from ``headers``
|
||||
* ``CONTENT_LENGTH`` can be extracted from ``headers``
|
||||
* ``SERVER_NAME`` and ``SERVER_PORT`` are in ``server``
|
||||
* ``REMOTE_HOST`` and ``REMOTE_PORT`` are in ``client``
|
||||
* ``REMOTE_HOST``/``REMOTE_ADDR`` and ``REMOTE_PORT`` are in ``client``
|
||||
* ``SERVER_PROTOCOL`` is encoded in ``http_version``
|
||||
* ``wsgi.url_scheme`` is ``scheme``
|
||||
* ``wsgi.input`` is a StringIO around ``body``
|
||||
|
|
Loading…
Reference in New Issue
Block a user