#-----------------------------------------------------------------------------# Copyright (c) Anaconda, Inc., and Bokeh Contributors.# All rights reserved.## The full license is in the file LICENSE.txt, distributed with this software.#-----------------------------------------------------------------------------''' Provides the Application, Server, and Session context classes.'''#-----------------------------------------------------------------------------# Boilerplate#-----------------------------------------------------------------------------from__future__importannotationsimportlogging# isort:skiplog=logging.getLogger(__name__)#-----------------------------------------------------------------------------# Imports#-----------------------------------------------------------------------------# Standard library importsimportweakreffromtypingimport(TYPE_CHECKING,Any,Awaitable,Callable,Iterable,)# External importsfromtornadoimportgenifTYPE_CHECKING:fromtornado.httputilimportHTTPServerRequestfromtornado.ioloopimportIOLoop# Bokeh importsfrom..application.applicationimportServerContext,SessionContextfrom..documentimportDocumentfrom..protocol.exceptionsimportProtocolErrorfrom..util.tokenimportget_token_payloadfrom.sessionimportServerSessionifTYPE_CHECKING:from..application.applicationimportApplicationfrom..core.typesimportIDfrom..util.tokenimportTokenPayload#-----------------------------------------------------------------------------# Globals and constants#-----------------------------------------------------------------------------__all__=('ApplicationContext','BokehServerContext','BokehSessionContext',)#-----------------------------------------------------------------------------# Setup#-----------------------------------------------------------------------------#-----------------------------------------------------------------------------# General API#-----------------------------------------------------------------------------#-----------------------------------------------------------------------------# Dev API#-----------------------------------------------------------------------------
[docs]classBokehSessionContext(SessionContext):_session:ServerSession|None_request:_RequestProxy|None_token:str|Nonedef__init__(self,session_id:ID,server_context:ServerContext,document:Document,logout_url:str|None=None)->None:self._document=documentself._session=Noneself._logout_url=logout_urlsuper().__init__(server_context,session_id)# request arguments used to instantiate this sessionself._request=Noneself._token=Nonedef_set_session(self,session:ServerSession)->None:self._session=session
[docs]asyncdefwith_locked_document(self,func:Callable[[Document],Awaitable[None]])->None:ifself._sessionisNone:# this means we are in on_session_created, so no locking yet,# we have exclusive accessawaitfunc(self._document)else:awaitself._session.with_document_locked(func,self._document)
@propertydefdestroyed(self)->bool:ifself._sessionisNone:# this means we are in on_session_createdreturnFalseelse:returnself._session.destroyed@propertydeflogout_url(self)->str|None:returnself._logout_url@propertydefrequest(self)->_RequestProxy|None:returnself._request@propertydeftoken_payload(self)->TokenPayload:assertself._tokenisnotNonereturnget_token_payload(self._token)@propertydefsession(self)->ServerSession|None:returnself._session
[docs]classApplicationContext:''' Server-side holder for ``bokeh.application.Application`` plus any associated data. This holds data that's global to all sessions, while ``ServerSession`` holds data specific to an "instance" of the application. '''_sessions:dict[ID,ServerSession]_pending_sessions:dict[ID,gen.Future[ServerSession]]_session_contexts:dict[ID,SessionContext]_server_context:BokehServerContextdef__init__(self,application:Application,io_loop:IOLoop|None=None,url:str|None=None,logout_url:str|None=None):self._application=applicationself._loop=io_loopself._sessions={}self._pending_sessions={}self._session_contexts={}self._server_context=BokehServerContext(self)self._url=urlself._logout_url=logout_url@propertydefio_loop(self)->IOLoop|None:returnself._loop@propertydefapplication(self)->Application:returnself._application@propertydefurl(self)->str|None:returnself._url@propertydefserver_context(self)->BokehServerContext:returnself._server_context@propertydefsessions(self)->Iterable[ServerSession]:returnself._sessions.values()defrun_load_hook(self)->None:try:self._application.on_server_loaded(self.server_context)exceptExceptionase:log.error(f"Error in server loaded hook {e!r}",exc_info=True)defrun_unload_hook(self)->None:try:self._application.on_server_unloaded(self.server_context)exceptExceptionase:log.error(f"Error in server unloaded hook {e!r}",exc_info=True)asyncdefcreate_session_if_needed(self,session_id:ID,request:HTTPServerRequest|None=None,token:str|None=None)->ServerSession:# this is because empty session_ids would be "falsey" and# potentially open up a way for clients to confuse usiflen(session_id)==0:raiseProtocolError("Session ID must not be empty")ifsession_idnotinself._sessionsand \
session_idnotinself._pending_sessions:future=self._pending_sessions[session_id]=gen.Future()doc=Document()session_context=BokehSessionContext(session_id,self.server_context,doc,logout_url=self._logout_url)ifrequestisnotNone:payload=get_token_payload(token)iftokenelse{}if('cookies'inpayloadand'headers'inpayloadand'Cookie'notinpayload['headers']):# Restore Cookie header from cookies dictionarypayload['headers']['Cookie']='; '.join([f'{k}={v}'fork,vinpayload['cookies'].items()])# using private attr so users only have access to a read-only propertysession_context._request=_RequestProxy(request,arguments=payload.get('arguments'),cookies=payload.get('cookies'),headers=payload.get('headers'))session_context._token=token# expose the session context to the document# use the _attribute to set the public property .session_contextdoc._session_context=weakref.ref(session_context)try:awaitself._application.on_session_created(session_context)exceptExceptionase:log.error("Failed to run session creation hooks %r",e,exc_info=True)self._application.initialize_document(doc)session=ServerSession(session_id,doc,io_loop=self._loop,token=token)delself._pending_sessions[session_id]self._sessions[session_id]=sessionsession_context._set_session(session)self._session_contexts[session_id]=session_context# notify anyone waiting on the pending sessionfuture.set_result(session)ifsession_idinself._pending_sessions:# another create_session_if_needed is working on# creating this sessionsession=awaitself._pending_sessions[session_id]else:session=self._sessions[session_id]returnsessiondefget_session(self,session_id:ID)->ServerSession:ifsession_idinself._sessions:session=self._sessions[session_id]returnsessionelse:raiseProtocolError("No such session "+session_id)asyncdef_discard_session(self,session:ServerSession,should_discard:Callable[[ServerSession],bool])->None:ifsession.connection_count>0:raiseRuntimeError("Should not be discarding a session with open connections")log.debug("Discarding session %r last in use %r milliseconds ago",session.id,session.milliseconds_since_last_unsubscribe)session_context=self._session_contexts[session.id]# session.destroy() wants the document lock so it can shut down the document# callbacks.defdo_discard()->None:# while we awaited for the document lock, the discard-worthiness of the# session may have changed.# However, since we have the document lock, our own lock will cause the# block count to be 1. If there's any other block count besides our own,# we want to skip session destruction though.ifshould_discard(session)andsession.expiration_blocked_count==1:session.destroy()delself._sessions[session.id]delself._session_contexts[session.id]log.trace(f"Session {session.id!r} was successfully discarded")else:log.warning(f"Session {session.id!r} was scheduled to discard but came back to life")awaitsession.with_document_locked(do_discard)# session lifecycle hooks are supposed to be called outside the document lock,# we only run these if we actually ended up destroying the session.ifsession_context.destroyed:try:awaitself._application.on_session_destroyed(session_context)exceptExceptionase:log.error("Failed to run session destroy hooks %r",e,exc_info=True)returnNoneasyncdef_cleanup_sessions(self,unused_session_linger_milliseconds:int)->None:defshould_discard_ignoring_block(session:ServerSession)->bool:returnsession.connection_count==0and \
(session.milliseconds_since_last_unsubscribe>unused_session_linger_millisecondsor \
session.expiration_requested)# build a temp list to avoid trouble from self._sessions changesto_discard:list[ServerSession]=[]forsessioninself._sessions.values():ifshould_discard_ignoring_block(session)andnotsession.expiration_blocked:to_discard.append(session)iflen(to_discard)>0:log.debug(f"Scheduling {len(to_discard)} sessions to discard")# asynchronously reconsider each sessionforsessioninto_discard:ifshould_discard_ignoring_block(session)andnotsession.expiration_blocked:awaitself._discard_session(session,should_discard_ignoring_block)returnNone
#-----------------------------------------------------------------------------# Private API#-----------------------------------------------------------------------------class_RequestProxy:_arguments:dict[str,list[bytes]]_cookies:dict[str,str]_headers:dict[str,str|list[str]]def__init__(self,request:HTTPServerRequest,arguments:dict[str,bytes|list[bytes]]|None=None,cookies:dict[str,str]|None=None,headers:dict[str,str|list[str]]|None=None,)->None:self._request=requestifargumentsisnotNone:self._arguments=argumentselifhasattr(request,'arguments'):self._arguments=dict(request.arguments)else:self._arguments={}if'bokeh-session-id'inself._arguments:delself._arguments['bokeh-session-id']ifcookiesisnotNone:self._cookies=cookieselifhasattr(request,'cookies'):# Django cookies are plain strings, tornado cookies are objects with a valueself._cookies={k:vifisinstance(v,str)elsev.valuefork,vinrequest.cookies.items()}else:self._cookies={}ifheadersisnotNone:self._headers=headerselifhasattr(request,'headers'):self._headers=dict(request.headers)else:self._headers={}@propertydefarguments(self)->dict[str,list[bytes]]:returnself._arguments@propertydefcookies(self)->dict[str,str]:returnself._cookies@propertydefheaders(self)->dict[str,str|list[str]]:returnself._headersdef__getattr__(self,name:str)->Any:ifnotname.startswith("_"):val=getattr(self._request,name,None)ifvalisnotNone:returnvalreturnsuper().__getattr__(name)#-----------------------------------------------------------------------------# Code#-----------------------------------------------------------------------------