Repository: bokeh/bokeh

Issue:
Feature: Make GET arguments available for bokeh server apps
GET arguments are an important feature of the web, so I think they should be available for server apps. 
They are complementary to session variables (#3349) and also mentioned [here](https://github.com/bokeh/bokeh/wiki/Use-of-the-query-string-in-url-to-control-plot-properties), but for the purpose of plot configuration.

I already have [working code](https://github.com/bkueng/bokeh/commit/3dfe97f6ad64e79f7658c9393a1afd290a4361f4) for it, but as per guidelines I open an issue first.



Discussion:
@bkueng I think alot of people will be happy to see such an addition! 

Here are some minor comments to keep in mind for a full PR :
- `get_arguments` is very similar to a typical "getter" name, and possibly confusing. Maybe `request_args` ? or similar
- `curdoc` seems like an OK place at a glance but at least a quick discussion about whether any other place is more appropriate. ping @havocp any comments?
- will need a test and some minimal documentation (can help with both)
- Discuss making this configurable, e.g. with a `--no-request-args` command line option or something? I don't know for certain, I am just imaging some people might want to precent access to the request args in some situations, but I may be wrong.  


Resolving patch:
diff --git a/bokeh/document.py b/bokeh/document.py
--- a/bokeh/document.py
+++ b/bokeh/document.py
@@ -141,6 +141,7 @@ def __init__(self, document, title):
         super(TitleChangedEvent, self).__init__(document)
         self.title = title
 
+
 class RootAddedEvent(DocumentPatchedEvent):
     def __init__(self, document, model):
         super(RootAddedEvent, self).__init__(document)
@@ -296,6 +297,7 @@ def __init__(self, **kwargs):
         self._all_models_by_name = _MultiValuedDict()
         self._callbacks = {}
         self._session_callbacks = {}
+        self._session_context = None
 
     def clear(self):
         ''' Remove all content from the document (including roots, vars, stores) but do not reset title'''
@@ -372,6 +374,10 @@ def _recompute_all_models(self):
     def roots(self):
         return list(self._roots)
 
+    @property
+    def session_context(self):
+        return self._session_context
+
     @property
     def title(self):
         return self._title
@@ -416,6 +422,7 @@ def theme(self, theme):
         for model in self._all_models.values():
             self._theme.apply_to_model(model)
 
+
     def add_root(self, model):
         ''' Add a model as a root model to this Document.
 
diff --git a/bokeh/server/application_context.py b/bokeh/server/application_context.py
--- a/bokeh/server/application_context.py
+++ b/bokeh/server/application_context.py
@@ -58,6 +58,8 @@ def __init__(self, session_id, server_context, document):
         self._session = None
         super(BokehSessionContext, self).__init__(server_context,
                                                   session_id)
+        # request arguments used to instantiate this session
+        self._request = None
 
     def _set_session(self, session):
         self._session = session
@@ -79,6 +81,11 @@ def destroyed(self):
         else:
             return self._session.destroyed
 
+    @property
+    def request(self):
+        return self._request
+
+
 class ApplicationContext(object):
     ''' Server-side holder for bokeh.application.Application plus any associated data.
         This holds data that's global to all sessions, while ServerSession holds
@@ -137,7 +144,7 @@ def run_unload_hook(self):
         self.server_context._remove_all_callbacks()
 
     @gen.coroutine
-    def create_session_if_needed(self, session_id):
+    def create_session_if_needed(self, session_id, request=None):
         # this is because empty session_ids would be "falsey" and
         # potentially open up a way for clients to confuse us
         if len(session_id) == 0:
@@ -149,9 +156,18 @@ def create_session_if_needed(self, session_id):
 
             doc = Document()
 
+
             session_context = BokehSessionContext(session_id,
                                                   self.server_context,
                                                   doc)
+            # using private attr so users only have access to a read-only property
+            session_context._request = request
+
+            # expose the session context to the document
+            # use the _attribute to set the public property .session_context
+            doc._session_context = session_context
+
+
             try:
                 yield yield_for_all_futures(self._application.on_session_created(session_context))
             except Exception as e:
diff --git a/bokeh/server/views/session_handler.py b/bokeh/server/views/session_handler.py
--- a/bokeh/server/views/session_handler.py
+++ b/bokeh/server/views/session_handler.py
@@ -37,6 +37,6 @@ def get_session(self):
             log.error("Session id had invalid signature: %r", session_id)
             raise HTTPError(status_code=403, reason="Invalid session ID")
 
-        session = yield self.application_context.create_session_if_needed(session_id)
+        session = yield self.application_context.create_session_if_needed(session_id, self.request)
 
         raise gen.Return(session)


Test patch:
diff --git a/bokeh/server/tests/test_server.py b/bokeh/server/tests/test_server.py
--- a/bokeh/server/tests/test_server.py
+++ b/bokeh/server/tests/test_server.py
@@ -226,6 +226,50 @@ def on_done():
     assert client_hook_list.hooks == ["session_created", "modify"]
     assert server_hook_list.hooks == ["session_created", "modify", "session_destroyed"]
 
+def test__request_in_session_context():
+    application = Application()
+    with ManagedServerLoop(application) as server:
+        response = http_get(server.io_loop,
+                            url(server) + "?foo=10")
+        html = response.body
+        sessionid = extract_sessionid_from_json(html)
+
+        server_session = server.get_session('/', sessionid)
+        server_doc = server_session.document
+        session_context = server_doc.session_context
+        # do we have a request
+        assert session_context.request is not None
+
+
+def test__request_in_session_context_has_arguments():
+    application = Application()
+    with ManagedServerLoop(application) as server:
+        response = http_get(server.io_loop,
+                            url(server) + "?foo=10")
+        html = response.body
+        sessionid = extract_sessionid_from_json(html)
+
+        server_session = server.get_session('/', sessionid)
+        server_doc = server_session.document
+        session_context = server_doc.session_context
+        # test if we can get the argument from the request
+        assert session_context.request.arguments['foo'] == [b'10']
+
+def test__no_request_arguments_in_session_context():
+    application = Application()
+    with ManagedServerLoop(application) as server:
+        response = http_get(server.io_loop,
+                            url(server))
+        html = response.body
+        sessionid = extract_sessionid_from_json(html)
+
+        server_session = server.get_session('/', sessionid)
+        server_doc = server_session.document
+        session_context = server_doc.session_context
+        # if we do not pass any arguments to the url, the request arguments
+        # should be empty
+        assert len(session_context.request.arguments) == 0
+
 # examples:
 # "sessionid" : "NzlNoPfEYJahnPljE34xI0a5RSTaU1Aq1Cx5"
 # 'sessionid':'NzlNoPfEYJahnPljE34xI0a5RSTaU1Aq1Cx5'