diff --git a/channel/__init__.py b/channel/__init__.py deleted file mode 100755 index d5e0e00..0000000 --- a/channel/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .consumer_registry import ConsumerRegistry - -# Make a site-wide registry -coreg = ConsumerRegistry() - -# Load an implementation of Channel -from .channels.memory import Channel diff --git a/channel/adapters.py b/channel/adapters.py deleted file mode 100755 index 7f3474e..0000000 --- a/channel/adapters.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.core.handlers.base import BaseHandler -from channel import Channel -from .response import encode_response -from .request import decode_request - - -class DjangoUrlAdapter(object): - """ - Adapts the channel-style HTTP requests to the URL-router/handler style - """ - - def __init__(self): - self.handler = BaseHandler() - self.handler.load_middleware() - - def __call__(self, request, response_channel): - response = self.handler.get_response(decode_request(request)) - Channel(response_channel).send(**encode_response(response)) diff --git a/channel/management/commands/__init__.py b/channel/management/commands/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/channels/__init__.py b/channels/__init__.py new file mode 100644 index 0000000..1aa77ce --- /dev/null +++ b/channels/__init__.py @@ -0,0 +1,11 @@ +from .consumer_registry import ConsumerRegistry + +# Make a site-wide registry +coreg = ConsumerRegistry() + +# Load an implementation of Channel +from .backends import InMemoryChannel as Channel + +# Ensure monkeypatching +from .hacks import monkeypatch_django +monkeypatch_django() diff --git a/channels/adapters.py b/channels/adapters.py new file mode 100644 index 0000000..03c02c9 --- /dev/null +++ b/channels/adapters.py @@ -0,0 +1,47 @@ +from django.core.handlers.base import BaseHandler +from django.http import HttpRequest, HttpResponse +from channels import Channel, coreg + + +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, **kwargs): + request = HttpRequest.channel_decode(kwargs) + try: + response = self.handler.get_response(request) + except HttpResponse.ResponseLater: + return + Channel(request.response_channel).send(**response.channel_encode()) + + +def view_producer(channel_name): + """ + Returns a new view function that actually writes the request to a channel + and abandons the response (with an exception the Worker will catch) + """ + def producing_view(request): + Channel(channel_name).send(**request.channel_encode()) + raise HttpResponse.ResponseLater() + return producing_view + + +def view_consumer(channel_name): + """ + Decorates a normal Django view to be a channel consumer. + Does not run any middleware. + """ + def inner(func): + def consumer(**kwargs): + request = HttpRequest.channel_decode(kwargs) + response = func(request) + Channel(request.response_channel).send(**response.channel_encode()) + coreg.add_consumer(consumer, [channel_name]) + return func + return inner diff --git a/channels/backends/__init__.py b/channels/backends/__init__.py new file mode 100644 index 0000000..0627c0a --- /dev/null +++ b/channels/backends/__init__.py @@ -0,0 +1,2 @@ +from .base import BaseChannel +from .memory import InMemoryChannel diff --git a/channel/channels/base.py b/channels/backends/base.py similarity index 78% rename from channel/channels/base.py rename to channels/backends/base.py index 7841d99..5341e0c 100644 --- a/channel/channels/base.py +++ b/channels/backends/base.py @@ -1,4 +1,4 @@ -class Channel(object): +class BaseChannel(object): """ Base class for all channel layer implementations. """ @@ -51,3 +51,12 @@ class Channel(object): response channels. """ raise NotImplementedError() + + def as_view(self): + """ + Returns a view version of this channel - one that takes + the request passed in and dispatches it to our channel, + serialized. + """ + from channels.adapters import view_producer + return view_producer(self.name) diff --git a/channel/channels/memory.py b/channels/backends/memory.py similarity index 92% rename from channel/channels/memory.py rename to channels/backends/memory.py index 6a66ceb..91bd1a2 100644 --- a/channel/channels/memory.py +++ b/channels/backends/memory.py @@ -2,12 +2,12 @@ import time import string import random from collections import deque -from .base import Channel as BaseChannel +from .base import BaseChannel queues = {} closed = set() -class Channel(BaseChannel): +class InMemoryChannel(BaseChannel): """ In-memory channel implementation. Intended only for use with threading, in low-throughput development environments. diff --git a/channel/consumer_registry.py b/channels/consumer_registry.py old mode 100755 new mode 100644 similarity index 100% rename from channel/consumer_registry.py rename to channels/consumer_registry.py diff --git a/channels/docs/integration-changes.rst b/channels/docs/integration-changes.rst new file mode 100644 index 0000000..2f6d204 --- /dev/null +++ b/channels/docs/integration-changes.rst @@ -0,0 +1,23 @@ +Message Standards +================= + +Some standardised message formats are used for common message types - they +are detailed below. + +HTTP Request +------------ + +Represents a full-fledged, single HTTP request coming in from a client. +Contains the following keys: + +* request: An encoded Django HTTP request +* response_channel: The channel name to write responses to + + +HTTP Response +------------- + +Sends a whole response to a client. +Contains the following keys: + +* response: An encoded Django HTTP response diff --git a/channels/docs/message-standards.rst b/channels/docs/message-standards.rst new file mode 100644 index 0000000..2f6d204 --- /dev/null +++ b/channels/docs/message-standards.rst @@ -0,0 +1,23 @@ +Message Standards +================= + +Some standardised message formats are used for common message types - they +are detailed below. + +HTTP Request +------------ + +Represents a full-fledged, single HTTP request coming in from a client. +Contains the following keys: + +* request: An encoded Django HTTP request +* response_channel: The channel name to write responses to + + +HTTP Response +------------- + +Sends a whole response to a client. +Contains the following keys: + +* response: An encoded Django HTTP response diff --git a/channels/hacks.py b/channels/hacks.py new file mode 100644 index 0000000..5d5e329 --- /dev/null +++ b/channels/hacks.py @@ -0,0 +1,27 @@ +from django.http.request import HttpRequest +from django.http.response import HttpResponseBase +from django.core.handlers.base import BaseHandler +from .request import encode_request, decode_request +from .response import encode_response, decode_response, ResponseLater + + +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 + + +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) diff --git a/channel/channels/__init__.py b/channels/management/__init__.py similarity index 100% rename from channel/channels/__init__.py rename to channels/management/__init__.py diff --git a/channel/management/__init__.py b/channels/management/commands/__init__.py similarity index 100% rename from channel/management/__init__.py rename to channels/management/commands/__init__.py diff --git a/channel/management/commands/runinterfaceserver.py b/channels/management/commands/runserver.py old mode 100755 new mode 100644 similarity index 64% rename from channel/management/commands/runinterfaceserver.py rename to channels/management/commands/runserver.py index 796606d..910bac9 --- a/channel/management/commands/runinterfaceserver.py +++ b/channels/management/commands/runserver.py @@ -2,11 +2,11 @@ import django import threading from django.core.management.commands.runserver import Command as RunserverCommand from django.core.handlers.wsgi import WSGIHandler -from channel import Channel, coreg -from channel.request import encode_request -from channel.response import decode_response -from channel.worker import Worker -from channel.utils import auto_import_consumers +from django.http import HttpResponse +from channels import Channel, coreg +from channels.worker import Worker +from channels.utils import auto_import_consumers +from channels.adapters import UrlConsumer class Command(RunserverCommand): @@ -24,7 +24,8 @@ class Command(RunserverCommand): # Check a handler is registered for http reqs auto_import_consumers() if not coreg.consumer_for_channel("django.wsgi.request"): - raise RuntimeError("No consumer registered for WSGI requests") + # Register the default one + coreg.add_consumer(UrlConsumer(), ["django.wsgi.request"]) # Launch a worker thread worker = WorkerThread() worker.daemon = True @@ -39,13 +40,10 @@ class WSGIInterfaceHandler(WSGIHandler): """ def get_response(self, request): - response_channel = Channel.new_name("django.wsgi.response") - Channel("django.wsgi.request").send( - request = encode_request(request), - response_channel = response_channel, - ) - channel, message = Channel.receive_many([response_channel]) - return decode_response(message) + request.response_channel = Channel.new_name("django.wsgi.response") + Channel("django.wsgi.request").send(**request.channel_encode()) + channel, message = Channel.receive_many([request.response_channel]) + return HttpResponse.channel_decode(message) class WorkerThread(threading.Thread): diff --git a/channel/request.py b/channels/request.py old mode 100755 new mode 100644 similarity index 87% rename from channel/request.py rename to channels/request.py index c319ac5..7062d62 --- a/channel/request.py +++ b/channels/request.py @@ -15,6 +15,7 @@ def encode_request(request): "path": request.path, "path_info": request.path_info, "method": request.method, + "response_channel": request.response_channel, } return value @@ -31,4 +32,5 @@ def decode_request(value): request.path = value['path'] request.method = value['method'] request.path_info = value['path_info'] + request.response_channel = value['response_channel'] return request diff --git a/channel/response.py b/channels/response.py old mode 100755 new mode 100644 similarity index 71% rename from channel/response.py rename to channels/response.py index 542482a..c71d333 --- a/channel/response.py +++ b/channels/response.py @@ -27,3 +27,12 @@ def decode_response(value): ) 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 doown 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 diff --git a/channel/utils.py b/channels/utils.py old mode 100755 new mode 100644 similarity index 81% rename from channel/utils.py rename to channels/utils.py index 2615e72..35f56d6 --- a/channel/utils.py +++ b/channels/utils.py @@ -10,5 +10,5 @@ def auto_import_consumers(): try: __import__(consumer_module_name) except ImportError as e: - if "no module named" not in str(e).lower(): + if "no module named consumers" not in str(e).lower(): raise diff --git a/channel/worker.py b/channels/worker.py old mode 100755 new mode 100644 similarity index 100% rename from channel/worker.py rename to channels/worker.py