2121import re
2222from pathlib import Path
2323from typing import Callable , List , Union
24+ import weakref
2425
2526# External imports
2627from django .core .asgi import get_asgi_application
2728from django .urls import re_path
2829from django .urls .resolvers import URLPattern
30+ from channels .db import database_sync_to_async
31+ from tornado import gen
2932
3033# Bokeh imports
3134from bokeh .application import Application
3235from bokeh .application .handlers .document_lifecycle import DocumentLifecycleHandler
3336from bokeh .application .handlers .function import FunctionHandler
3437from 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
3843from .consumers import AutoloadJsConsumer , DocConsumer , WSConsumer
3944
4045# -----------------------------------------------------------------------------
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
55124class 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
0 commit comments