21
21
import re
22
22
from pathlib import Path
23
23
from typing import Callable , List , Union
24
+ import weakref
24
25
25
26
# External imports
26
27
from django .core .asgi import get_asgi_application
27
28
from django .urls import re_path
28
29
from django .urls .resolvers import URLPattern
30
+ from channels .db import database_sync_to_async
31
+ from tornado import gen
29
32
30
33
# Bokeh imports
31
34
from bokeh .application import Application
32
35
from bokeh .application .handlers .document_lifecycle import DocumentLifecycleHandler
33
36
from bokeh .application .handlers .function import FunctionHandler
34
37
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
36
41
37
- # Bokeh imports
42
+ # Local imports
38
43
from .consumers import AutoloadJsConsumer , DocConsumer , WSConsumer
39
44
40
45
# -----------------------------------------------------------------------------
51
56
# General API
52
57
# -----------------------------------------------------------------------------
53
58
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
+
54
123
55
124
class Routing :
56
125
url : str
@@ -62,7 +131,7 @@ class Routing:
62
131
def __init__ (self , url : str , app : ApplicationLike , * , document : bool = False , autoload : bool = False ) -> None :
63
132
self .url = url
64
133
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 )
66
135
self .document = document
67
136
self .autoload = autoload
68
137
0 commit comments