mirror of
https://github.com/django/daphne.git
synced 2025-07-10 16:02:18 +03:00
Initial commit
This commit is contained in:
commit
821816f656
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
*.egg-info
|
7
channel/__init__.py
Executable file
7
channel/__init__.py
Executable file
|
@ -0,0 +1,7 @@
|
|||
from .consumer_registry import ConsumerRegistry
|
||||
|
||||
# Make a site-wide registry
|
||||
coreg = ConsumerRegistry()
|
||||
|
||||
# Load an implementation of Channel
|
||||
from .channels.memory import Channel
|
18
channel/adapters.py
Executable file
18
channel/adapters.py
Executable file
|
@ -0,0 +1,18 @@
|
|||
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))
|
0
channel/channels/__init__.py
Normal file
0
channel/channels/__init__.py
Normal file
53
channel/channels/base.py
Normal file
53
channel/channels/base.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
class Channel(object):
|
||||
"""
|
||||
Base class for all channel layer implementations.
|
||||
"""
|
||||
|
||||
class ClosedError(Exception):
|
||||
"""
|
||||
Raised when you try to send to a closed channel.
|
||||
"""
|
||||
pass
|
||||
|
||||
def __init__(self, name):
|
||||
"""
|
||||
Create an instance for the channel named "name"
|
||||
"""
|
||||
self.name = name
|
||||
|
||||
def send(self, **kwargs):
|
||||
"""
|
||||
Send a message over the channel, taken from the kwargs.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Closes the channel, allowing no more messages to be sent over it.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def closed(self):
|
||||
"""
|
||||
Says if the channel is closed.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def receive_many(self, channel_names):
|
||||
"""
|
||||
Block and return the first message available on one of the
|
||||
channels passed, as a (channel_name, message) tuple.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@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.
|
||||
"""
|
||||
raise NotImplementedError()
|
48
channel/channels/memory.py
Normal file
48
channel/channels/memory.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
import time
|
||||
import string
|
||||
import random
|
||||
from collections import deque
|
||||
from .base import Channel as BaseChannel
|
||||
|
||||
queues = {}
|
||||
closed = set()
|
||||
|
||||
class Channel(BaseChannel):
|
||||
"""
|
||||
In-memory channel implementation. Intended only for use with threading,
|
||||
in low-throughput development environments.
|
||||
"""
|
||||
|
||||
def send(self, **kwargs):
|
||||
# Don't allow if closed
|
||||
if self.name in closed:
|
||||
raise Channel.ClosedError("%s is closed" % self.name)
|
||||
# Add to the deque, making it if needs be
|
||||
queues.setdefault(self.name, deque()).append(kwargs)
|
||||
|
||||
@property
|
||||
def closed(self):
|
||||
# Check closed set
|
||||
return self.name in closed
|
||||
|
||||
def close(self):
|
||||
# Add to closed set
|
||||
closed.add(self.name)
|
||||
|
||||
@classmethod
|
||||
def receive_many(self, channel_names):
|
||||
while True:
|
||||
# Try to pop a message from each channel
|
||||
for channel_name in channel_names:
|
||||
try:
|
||||
# This doesn't clean up empty channels - OK for testing.
|
||||
# For later versions, have cleanup w/lock.
|
||||
return channel_name, queues[channel_name].popleft()
|
||||
except (IndexError, KeyError):
|
||||
pass
|
||||
# If all empty, sleep for a little bit
|
||||
time.sleep(0.01)
|
||||
|
||||
@classmethod
|
||||
def new_name(self, prefix):
|
||||
return "%s.%s" % (prefix, "".join(random.choice(string.ascii_letters) for i in range(16)))
|
40
channel/consumer_registry.py
Executable file
40
channel/consumer_registry.py
Executable file
|
@ -0,0 +1,40 @@
|
|||
import functools
|
||||
|
||||
class ConsumerRegistry(object):
|
||||
"""
|
||||
Manages the available consumers in the project and which channels they
|
||||
listen to.
|
||||
|
||||
Generally a single project-wide instance of this is used.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.consumers = {}
|
||||
|
||||
def add_consumer(self, consumer, channels):
|
||||
for channel in channels:
|
||||
if channel in self.consumers:
|
||||
raise ValueError("Cannot register consumer %s - channel %s already consumed by %s" % (
|
||||
consumer,
|
||||
channel,
|
||||
self.consumers[channel],
|
||||
))
|
||||
self.consumers[channel] = consumer
|
||||
|
||||
def consumer(self, channels):
|
||||
"""
|
||||
Decorator that registers a function as a consumer.
|
||||
"""
|
||||
def inner(func):
|
||||
self.add_consumer(func, channels)
|
||||
return func
|
||||
return inner
|
||||
|
||||
def all_channel_names(self):
|
||||
return self.consumers.keys()
|
||||
|
||||
def consumer_for_channel(self, channel):
|
||||
try:
|
||||
return self.consumers[channel]
|
||||
except KeyError:
|
||||
return None
|
0
channel/management/__init__.py
Normal file
0
channel/management/__init__.py
Normal file
0
channel/management/commands/__init__.py
Normal file
0
channel/management/commands/__init__.py
Normal file
60
channel/management/commands/runinterfaceserver.py
Executable file
60
channel/management/commands/runinterfaceserver.py
Executable file
|
@ -0,0 +1,60 @@
|
|||
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
|
||||
|
||||
|
||||
class Command(RunserverCommand):
|
||||
|
||||
def get_handler(self, *args, **options):
|
||||
"""
|
||||
Returns the default WSGI handler for the runner.
|
||||
"""
|
||||
django.setup()
|
||||
return WSGIInterfaceHandler()
|
||||
|
||||
def run(self, *args, **options):
|
||||
# Force disable reloader for now
|
||||
options['use_reloader'] = False
|
||||
# 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")
|
||||
# Launch a worker thread
|
||||
worker = WorkerThread()
|
||||
worker.daemon = True
|
||||
worker.start()
|
||||
# Run the rest
|
||||
return super(Command, self).run(*args, **options)
|
||||
|
||||
|
||||
class WSGIInterfaceHandler(WSGIHandler):
|
||||
"""
|
||||
New WSGI handler that pushes requests to channels.
|
||||
"""
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class WorkerThread(threading.Thread):
|
||||
"""
|
||||
Class that runs a worker
|
||||
"""
|
||||
|
||||
def run(self):
|
||||
Worker(
|
||||
consumer_registry = coreg,
|
||||
channel_class = Channel,
|
||||
).run()
|
34
channel/request.py
Executable file
34
channel/request.py
Executable file
|
@ -0,0 +1,34 @@
|
|||
from django.http import HttpRequest
|
||||
from django.utils.datastructures import MultiValueDict
|
||||
|
||||
|
||||
def encode_request(request):
|
||||
"""
|
||||
Encodes a request to JSON-compatible datastructures
|
||||
"""
|
||||
# TODO: More stuff
|
||||
value = {
|
||||
"GET": request.GET.items(),
|
||||
"POST": request.POST.items(),
|
||||
"COOKIES": request.COOKIES,
|
||||
"META": {k: v for k, v in request.META.items() if not k.startswith("wsgi")},
|
||||
"path": request.path,
|
||||
"path_info": request.path_info,
|
||||
"method": request.method,
|
||||
}
|
||||
return value
|
||||
|
||||
|
||||
def decode_request(value):
|
||||
"""
|
||||
Decodes a request JSONish value to a HttpRequest object.
|
||||
"""
|
||||
request = HttpRequest()
|
||||
request.GET = MultiValueDict(value['GET'])
|
||||
request.POST = MultiValueDict(value['POST'])
|
||||
request.COOKIES = value['COOKIES']
|
||||
request.META = value['META']
|
||||
request.path = value['path']
|
||||
request.method = value['method']
|
||||
request.path_info = value['path_info']
|
||||
return request
|
29
channel/response.py
Executable file
29
channel/response.py
Executable file
|
@ -0,0 +1,29 @@
|
|||
from django.http import HttpResponse
|
||||
|
||||
|
||||
def encode_response(response):
|
||||
"""
|
||||
Encodes a response to JSON-compatible datastructures
|
||||
"""
|
||||
# TODO: Entirely useful things like cookies
|
||||
value = {
|
||||
"content_type": getattr(response, "content_type", None),
|
||||
"content": response.content,
|
||||
"status_code": response.status_code,
|
||||
"headers": response._headers.values(),
|
||||
}
|
||||
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_code'],
|
||||
)
|
||||
response._headers = {k.lower: (k, v) for k, v in value['headers']}
|
||||
return response
|
14
channel/utils.py
Executable file
14
channel/utils.py
Executable file
|
@ -0,0 +1,14 @@
|
|||
from django.apps import apps
|
||||
|
||||
|
||||
def auto_import_consumers():
|
||||
"""
|
||||
Auto-import consumers modules in apps
|
||||
"""
|
||||
for app_config in apps.get_app_configs():
|
||||
consumer_module_name = "%s.consumers" % (app_config.name,)
|
||||
try:
|
||||
__import__(consumer_module_name)
|
||||
except ImportError as e:
|
||||
if "no module named" not in str(e).lower():
|
||||
raise
|
19
channel/worker.py
Executable file
19
channel/worker.py
Executable file
|
@ -0,0 +1,19 @@
|
|||
class Worker(object):
|
||||
"""
|
||||
A "worker" process that continually looks for available messages to run
|
||||
and runs their consumers.
|
||||
"""
|
||||
|
||||
def __init__(self, consumer_registry, channel_class):
|
||||
self.consumer_registry = consumer_registry
|
||||
self.channel_class = channel_class
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Tries to continually dispatch messages to consumers.
|
||||
"""
|
||||
channels = self.consumer_registry.all_channel_names()
|
||||
while True:
|
||||
channel, message = self.channel_class.receive_many(channels)
|
||||
consumer = self.consumer_registry.consumer_for_channel(channel)
|
||||
consumer(**message)
|
12
setup.py
Normal file
12
setup.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from setuptools import find_packages, setup
|
||||
|
||||
setup(
|
||||
name='django-channel',
|
||||
version="0.1",
|
||||
url='http://github.com/andrewgodwin/django-channel',
|
||||
author='Andrew Godwin',
|
||||
author_email='andrew@aeracode.org',
|
||||
license='BSD',
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
)
|
Loading…
Reference in New Issue
Block a user