Skip to content

Commit 258c4bc

Browse files
committed
Add DjangoApplicationContext to handle sync_to_async transition for handler function
1 parent 1ee386c commit 258c4bc

File tree

4 files changed

+78
-5
lines changed

4 files changed

+78
-5
lines changed

bokeh_django/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
# Bokeh imports
55
from .apps import DjangoBokehConfig
6+
from .consumers import AutoloadJsConsumer, WSConsumer
67
from .routing import autoload, directory, document
78
from .static import static_extensions
89

bokeh_django/consumers.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,10 @@ async def _get_session(self) -> ServerSession:
128128
signed=False,
129129
expiration=300,
130130
extra_payload=payload)
131-
session = await self.application_context.create_session_if_needed(session_id, self.request, token)
131+
try:
132+
session = await self.application_context.create_session_if_needed(session_id, self.request, token)
133+
except Exception as e:
134+
log.exception(e)
132135
return session
133136

134137

bokeh_django/routing.py

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,25 @@
2121
import re
2222
from pathlib import Path
2323
from typing import Callable, List, Union
24+
import weakref
2425

2526
# External imports
2627
from django.core.asgi import get_asgi_application
2728
from django.urls import re_path
2829
from django.urls.resolvers import URLPattern
30+
from channels.db import database_sync_to_async
31+
from tornado import gen
2932

3033
# Bokeh imports
3134
from bokeh.application import Application
3235
from bokeh.application.handlers.document_lifecycle import DocumentLifecycleHandler
3336
from bokeh.application.handlers.function import FunctionHandler
3437
from bokeh.command.util import build_single_handler_application, build_single_handler_applications
35-
from bokeh.server.contexts import ApplicationContext
38+
from bokeh.server.contexts import ApplicationContext, BokehSessionContext, _RequestProxy, ServerSession
39+
from bokeh.document import Document
40+
from bokeh.util.token import get_token_payload
3641

37-
# Bokeh imports
42+
# Local imports
3843
from .consumers import AutoloadJsConsumer, DocConsumer, WSConsumer
3944

4045
# -----------------------------------------------------------------------------
@@ -51,6 +56,70 @@
5156
# General API
5257
# -----------------------------------------------------------------------------
5358

59+
class DjangoApplicationContext(ApplicationContext):
60+
async def create_session_if_needed(self, session_id: ID, request: HTTPServerRequest | None = None,
61+
token: str | None = None) -> ServerSession:
62+
# this is because empty session_ids would be "falsey" and
63+
# potentially open up a way for clients to confuse us
64+
if len(session_id) == 0:
65+
raise ProtocolError("Session ID must not be empty")
66+
67+
if session_id not in self._sessions and \
68+
session_id not in self._pending_sessions:
69+
future = self._pending_sessions[session_id] = gen.Future()
70+
71+
doc = Document()
72+
73+
session_context = BokehSessionContext(session_id,
74+
self.server_context,
75+
doc,
76+
logout_url=self._logout_url)
77+
if request is not None:
78+
payload = get_token_payload(token) if token else {}
79+
if ('cookies' in payload and 'headers' in payload
80+
and not 'Cookie' in payload['headers']):
81+
# Restore Cookie header from cookies dictionary
82+
payload['headers']['Cookie'] = '; '.join([
83+
f'{k}={v}' for k, v in payload['cookies'].items()
84+
])
85+
# using private attr so users only have access to a read-only property
86+
session_context._request = _RequestProxy(request,
87+
cookies=payload.get('cookies'),
88+
headers=payload.get('headers'))
89+
session_context._token = token
90+
91+
# expose the session context to the document
92+
# use the _attribute to set the public property .session_context
93+
doc._session_context = weakref.ref(session_context)
94+
95+
try:
96+
await self._application.on_session_created(session_context)
97+
except Exception as e:
98+
log.error("Failed to run session creation hooks %r", e, exc_info=True)
99+
100+
# This needs to be wrapped in the database_sync_to_async wrapper just in case the handler function accesses
101+
# Django ORM.
102+
103+
await database_sync_to_async(self._application.initialize_document)(doc)
104+
105+
session = ServerSession(session_id, doc, io_loop=self._loop, token=token)
106+
del self._pending_sessions[session_id]
107+
self._sessions[session_id] = session
108+
session_context._set_session(session)
109+
self._session_contexts[session_id] = session_context
110+
111+
# notify anyone waiting on the pending session
112+
future.set_result(session)
113+
114+
if session_id in self._pending_sessions:
115+
# another create_session_if_needed is working on
116+
# creating this session
117+
session = await self._pending_sessions[session_id]
118+
else:
119+
session = self._sessions[session_id]
120+
121+
return session
122+
54123

55124
class Routing:
56125
url: str
@@ -62,7 +131,7 @@ class Routing:
62131
def __init__(self, url: str, app: ApplicationLike, *, document: bool = False, autoload: bool = False) -> None:
63132
self.url = url
64133
self.app = self._fixup(self._normalize(app))
65-
self.app_context = ApplicationContext(self.app, url=self.url)
134+
self.app_context = DjangoApplicationContext(self.app, url=self.url)
66135
self.document = document
67136
self.autoload = autoload
68137

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
setup(
66
name='bokeh-django',
7-
version='0.0.0',
7+
version='0.1.0a2',
88
description='',
99
long_description='',
1010
keywords='',

0 commit comments

Comments
 (0)