Repository: bokeh/bokeh

Issue:
'ServerSession' object has no attribute 'session_context'
Using version 0.12.14 with python3.6. When there is at least one active session, accessing `server_context.sessions`, where `server_context` is a `bokeh.server.application_context.BokehServerContext` instance passed to my `on_server_loaded` handler, results in the following `AttributeError`:

```
  File "/usr/local/lib/python3.6/dist-packages/bokeh/server/application_context.py", line 40, in sessions
    result.append(session.session_context)
AttributeError: 'ServerSession' object has no attribute 'session_context'
```

The relevant lines of `server/application_context.py` are:

```
    @property
    def sessions(self):
        result = []
        for session in self.application_context.sessions:
            result.append(session.session_context)
        return result
```

Each item in the list `self.application_context.sessions` is a `bokeh.server.session.ServerSession` instance and does not have a `session_context` attribute.

It's not clear to me how one gets the session context (a `bokeh.server.application_context.BokehSessionContext` instance) from the session object. Also, should `server_context.sessions` return a list of `bokeh.server.session.ServerSession` instances or `bokeh.server.application_context.BokehSessionContext` instances?


Discussion:
What exactly are you trying to accomplish? By and large all the classes you've mentioned are not really useful or intended for general users.
I'm following the approach given [here](https://groups.google.com/a/continuum.io/d/msg/bokeh/qVVfCUu_eow/cDZjQcVbBQAJ)
to start a single ThreadPoolExecutor on_server_load and have it update all active sessions with the data that it gets. But I can't access the session contexts, hence their `with_locked_document` methods.

In any case, this looks like a bug.
@aayla-secura I'm happy to look at this if you can provide a Minimal Reproducing Example code. 
Also, to answer your question, I think one way is to go through `session.document.session_context`, which might just be the right change to make in general. But I'd like to try it out in your MRE.
@bryevdv , you are right, session.document.session_context works, I think that would be the fix. Here's a MRE:

```
# server_lifecycle.py

from time import sleep
from threading import Thread

def updater(server_context):
    while True:
        sleep (1)
        try:
            sessions = server_context.sessions  # this fails
        except AttributeError as e:
            print(str(e))
            sessions = server_context.application_context.sessions
        for s in sessions:
            try:
                session_context = s.doc.session_context  # this works
            except AttributeError as e:
                print(str(e))
            return

def on_server_loaded(server_context):
    thread = Thread(target=updater, args=[server_context])
    thread.daemon = True
    thread.start()
```

This in a directory with an empty `main.py` prints the error when a session if first created.

Resolving patch:
diff --git a/bokeh/server/application_context.py b/bokeh/server/contexts.py
similarity index 98%
rename from bokeh/server/application_context.py
rename to bokeh/server/contexts.py
--- a/bokeh/server/application_context.py
+++ b/bokeh/server/contexts.py
@@ -1,4 +1,4 @@
-''' Provides the ``ApplicationContext`` class.
+''' Provides the Application, Server, and Session context classes.
 
 '''
 from __future__ import absolute_import
@@ -37,7 +37,7 @@ def _remove_all_callbacks(self):
     def sessions(self):
         result = []
         for session in self.application_context.sessions:
-            result.append(session.session_context)
+            result.append(session)
         return result
 
     def add_next_tick_callback(self, callback):
@@ -91,6 +91,10 @@ def destroyed(self):
     def request(self):
         return self._request
 
+    @property
+    def session(self):
+        return self._session
+
 
 class ApplicationContext(object):
     ''' Server-side holder for bokeh.application.Application plus any associated data.
@@ -162,7 +166,6 @@ def create_session_if_needed(self, session_id, request=None):
 
             doc = Document()
 
-
             session_context = BokehSessionContext(session_id,
                                                   self.server_context,
                                                   doc)
diff --git a/bokeh/server/tornado.py b/bokeh/server/tornado.py
--- a/bokeh/server/tornado.py
+++ b/bokeh/server/tornado.py
@@ -19,7 +19,7 @@
 from ..settings import settings
 from ..util.dependencies import import_optional
 
-from .application_context import ApplicationContext
+from .contexts import ApplicationContext
 from .connection import ServerConnection
 from .urls import per_app_patterns, toplevel_patterns
 from .views.root_handler import RootHandler


Test patch:
diff --git a/bokeh/server/tests/test_contexts.py b/bokeh/server/tests/test_contexts.py
new file mode 100644
--- /dev/null
+++ b/bokeh/server/tests/test_contexts.py
@@ -0,0 +1,99 @@
+import pytest
+
+from bokeh.application import Application
+import bokeh.server.contexts as bsc
+
+class TestBokehServerContext(object):
+
+    def test_init(self):
+        ac = bsc.ApplicationContext("app", io_loop="ioloop")
+        c = bsc.BokehServerContext(ac)
+        assert c.application_context == ac
+
+    def test_sessions(self):
+        ac = bsc.ApplicationContext("app", io_loop="ioloop")
+        ac._sessions = dict(foo=1, bar=2)
+        c = bsc.BokehServerContext(ac)
+        assert set(c.sessions) == set([1,2])
+
+class TestBokehSessionContext(object):
+
+    def test_init(self):
+        ac = bsc.ApplicationContext("app", io_loop="ioloop")
+        sc = bsc.BokehServerContext(ac)
+        c = bsc.BokehSessionContext("id", sc, "doc")
+        assert c.session is None
+        assert c.request is None
+        assert not c.destroyed
+
+    def test_destroyed(self):
+        class FakeSession(object):
+            destroyed = False
+        ac = bsc.ApplicationContext("app", io_loop="ioloop")
+        sc = bsc.BokehServerContext(ac)
+        c = bsc.BokehSessionContext("id", sc, "doc")
+        sess = FakeSession()
+        c._session = sess
+        assert not c.destroyed
+        sess.destroyed = True
+        assert c.destroyed
+
+class TestApplicationContext(object):
+
+    def test_init(self):
+        c = bsc.ApplicationContext("app", io_loop="ioloop")
+        assert c.io_loop == "ioloop"
+        assert c.application == "app"
+        assert c.url is None
+
+        c = bsc.ApplicationContext("app", io_loop="ioloop", url="url")
+        assert c.io_loop == "ioloop"
+        assert c.application == "app"
+        assert c.url == "url"
+
+    def test_sessions(self):
+        c = bsc.ApplicationContext("app", io_loop="ioloop")
+        c._sessions = dict(foo=1, bar=2)
+        assert set(c.sessions) == set([1,2])
+
+    def test_get_session_success(self):
+        c = bsc.ApplicationContext("app", io_loop="ioloop")
+        c._sessions = dict(foo=1, bar=2)
+        assert c.get_session("foo") == 1
+
+    def test_get_session_failure(self):
+        c = bsc.ApplicationContext("app", io_loop="ioloop")
+        c._sessions = dict(foo=1, bar=2)
+        with pytest.raises(bsc.ProtocolError) as e:
+            c.get_session("bax")
+        assert str(e).endswith("No such session bax")
+
+    def test_create_session_if_needed_new(self):
+        app = Application()
+        c = bsc.ApplicationContext(app, io_loop="ioloop")
+        class FakeRequest(object):
+            arguments = dict(foo=10)
+        req = FakeRequest()
+        s = c.create_session_if_needed("foo", request=req)
+        assert c.get_session("foo") == s.result()
+
+    def test_create_session_if_needed_exists(self):
+        app = Application()
+        c = bsc.ApplicationContext(app, io_loop="ioloop")
+        class FakeRequest(object):
+            arguments = dict(foo=10)
+        req = FakeRequest()
+        s1 = c.create_session_if_needed("foo", request=req)
+        s2 = c.create_session_if_needed("foo", request=req)
+        assert s1.result() == s2.result()
+
+    def test_create_session_if_needed_bad_sessionid(self):
+        app = Application()
+        c = bsc.ApplicationContext(app, io_loop="ioloop")
+        class FakeRequest(object):
+            arguments = dict(foo=10)
+        req = FakeRequest()
+        r = c.create_session_if_needed("", request=req)
+        with pytest.raises(bsc.ProtocolError) as e:
+            r.result()
+        assert str(e).endswith("Session ID must not be empty")