195 lines
6.9 KiB
Python
195 lines
6.9 KiB
Python
import importlib
|
|
import warnings
|
|
|
|
from asgiref.compatibility import guarantee_single_callable
|
|
from django.conf import settings
|
|
from django.core.exceptions import ImproperlyConfigured
|
|
from django.urls.exceptions import Resolver404
|
|
from django.urls.resolvers import URLResolver
|
|
|
|
from channels.http import AsgiHandler
|
|
|
|
"""
|
|
All Routing instances inside this file are also valid ASGI applications - with
|
|
new Channels routing, whatever you end up with as the top level object is just
|
|
served up as the "ASGI application".
|
|
"""
|
|
|
|
|
|
def get_default_application():
|
|
"""
|
|
Gets the default application, set in the ASGI_APPLICATION setting.
|
|
"""
|
|
try:
|
|
path, name = settings.ASGI_APPLICATION.rsplit(".", 1)
|
|
except (ValueError, AttributeError):
|
|
raise ImproperlyConfigured("Cannot find ASGI_APPLICATION setting.")
|
|
try:
|
|
module = importlib.import_module(path)
|
|
except ImportError:
|
|
raise ImproperlyConfigured("Cannot import ASGI_APPLICATION module %r" % path)
|
|
try:
|
|
value = getattr(module, name)
|
|
except AttributeError:
|
|
raise ImproperlyConfigured(
|
|
"Cannot find %r in ASGI_APPLICATION module %s" % (name, path)
|
|
)
|
|
return value
|
|
|
|
|
|
DEPRECATION_MSG = """
|
|
Using ProtocolTypeRouter without an explicit "http" key is deprecated.
|
|
Given that you have not passed the "http" you likely should use Django's
|
|
get_asgi_application():
|
|
|
|
from django.core.asgi import get_asgi_application
|
|
|
|
application = ProtocolTypeRouter(
|
|
"http": get_asgi_application()
|
|
# Other protocols here.
|
|
)
|
|
"""
|
|
|
|
|
|
class ProtocolTypeRouter:
|
|
"""
|
|
Takes a mapping of protocol type names to other Application instances,
|
|
and dispatches to the right one based on protocol name (or raises an error)
|
|
"""
|
|
|
|
def __init__(self, application_mapping):
|
|
self.application_mapping = application_mapping
|
|
if "http" not in self.application_mapping:
|
|
warnings.warn(DEPRECATION_MSG, DeprecationWarning, stacklevel=2)
|
|
self.application_mapping["http"] = AsgiHandler()
|
|
|
|
async def __call__(self, scope, receive, send):
|
|
if scope["type"] in self.application_mapping:
|
|
application = guarantee_single_callable(
|
|
self.application_mapping[scope["type"]]
|
|
)
|
|
return await application(scope, receive, send)
|
|
else:
|
|
raise ValueError(
|
|
"No application configured for scope type %r" % scope["type"]
|
|
)
|
|
|
|
|
|
def route_pattern_match(route, path):
|
|
"""
|
|
Backport of RegexPattern.match for Django versions before 2.0. Returns
|
|
the remaining path and positional and keyword arguments matched.
|
|
"""
|
|
if hasattr(route, "pattern"):
|
|
match = route.pattern.match(path)
|
|
if match:
|
|
path, args, kwargs = match
|
|
kwargs.update(route.default_args)
|
|
return path, args, kwargs
|
|
return match
|
|
|
|
# Django<2.0. No converters... :-(
|
|
match = route.regex.search(path)
|
|
if match:
|
|
# If there are any named groups, use those as kwargs, ignoring
|
|
# non-named groups. Otherwise, pass all non-named arguments as
|
|
# positional arguments.
|
|
kwargs = match.groupdict()
|
|
args = () if kwargs else match.groups()
|
|
if kwargs is not None:
|
|
kwargs.update(route.default_args)
|
|
return path[match.end() :], args, kwargs
|
|
return None
|
|
|
|
|
|
class URLRouter:
|
|
"""
|
|
Routes to different applications/consumers based on the URL path.
|
|
|
|
Works with anything that has a ``path`` key, but intended for WebSocket
|
|
and HTTP. Uses Django's django.conf.urls objects for resolution -
|
|
url() or path().
|
|
"""
|
|
|
|
#: This router wants to do routing based on scope[path] or
|
|
#: scope[path_remaining]. ``path()`` entries in URLRouter should not be
|
|
#: treated as endpoints (ended with ``$``), but similar to ``include()``.
|
|
_path_routing = True
|
|
|
|
def __init__(self, routes):
|
|
self.routes = routes
|
|
|
|
for route in self.routes:
|
|
# The inner ASGI app wants to do additional routing, route
|
|
# must not be an endpoint
|
|
if getattr(route.callback, "_path_routing", False) is True:
|
|
route.pattern._is_endpoint = False
|
|
|
|
if not route.callback and isinstance(route, URLResolver):
|
|
raise ImproperlyConfigured(
|
|
"%s: include() is not supported in URLRouter. Use nested"
|
|
" URLRouter instances instead." % (route,)
|
|
)
|
|
|
|
async def __call__(self, scope, receive, send):
|
|
# Get the path
|
|
path = scope.get("path_remaining", scope.get("path", None))
|
|
if path is None:
|
|
raise ValueError("No 'path' key in connection scope, cannot route URLs")
|
|
# Remove leading / to match Django's handling
|
|
path = path.lstrip("/")
|
|
# Run through the routes we have until one matches
|
|
for route in self.routes:
|
|
try:
|
|
match = route_pattern_match(route, path)
|
|
if match:
|
|
new_path, args, kwargs = match
|
|
# Add args or kwargs into the scope
|
|
outer = scope.get("url_route", {})
|
|
application = guarantee_single_callable(route.callback)
|
|
return await application(
|
|
dict(
|
|
scope,
|
|
path_remaining=new_path,
|
|
url_route={
|
|
"args": outer.get("args", ()) + args,
|
|
"kwargs": {**outer.get("kwargs", {}), **kwargs},
|
|
},
|
|
),
|
|
receive,
|
|
send,
|
|
)
|
|
except Resolver404:
|
|
pass
|
|
else:
|
|
if "path_remaining" in scope:
|
|
raise Resolver404("No route found for path %r." % path)
|
|
# We are the outermost URLRouter
|
|
raise ValueError("No route found for path %r." % path)
|
|
|
|
|
|
class ChannelNameRouter:
|
|
"""
|
|
Maps to different applications based on a "channel" key in the scope
|
|
(intended for the Channels worker mode)
|
|
"""
|
|
|
|
def __init__(self, application_mapping):
|
|
self.application_mapping = application_mapping
|
|
|
|
async def __call__(self, scope, receive, send):
|
|
if "channel" not in scope:
|
|
raise ValueError(
|
|
"ChannelNameRouter got a scope without a 'channel' key. "
|
|
+ "Did you make sure it's only being used for 'channel' type messages?"
|
|
)
|
|
if scope["channel"] in self.application_mapping:
|
|
application = guarantee_single_callable(
|
|
self.application_mapping[scope["channel"]]
|
|
)
|
|
return await application(scope, receive, send)
|
|
else:
|
|
raise ValueError(
|
|
"No application configured for channel name %r" % scope["channel"]
|
|
)
|