Repository: bokeh/bokeh

Issue:
Pass HTTP request arguments to autoload_server and pull_session
#4858 makes it possible to pass HTTP requests arguments to app documents. However, the functions `autoload_server` and `pull_session` do not allow for sending request parameters, because they already add a query string to the URL.

For instance, `autoload_server` adds `/autoload.js?bokeh-autoload-element=...&bokeh-session-id=...` to the end of the URL. If I add the query string `?N=10` to the end of my `app_path`, then the URL will result in `?N=10/autoload.js?bokeh-autoload-element=...&bokeh-session-id=...` which is not a valid URL.

It would be great if there was a way to pass request arguments to these functions so that they automatically add it to the URL in a correct way.


Discussion:
I ran into this wall also.  My use-case is having flask call `autoload_server`, and I wanted it to simply forward any request parameters through to bokeh.  It then used the return `script` variable as an input to a flask template.  Here is my workaround, if you simply must be able to do this before the feature is added: 

```
@app.route('/amped')
def amped():
    script = autoload_server(model = None, app_path="/amped")
    # this is pretty hacky -- flask can't pass its GET request parameters directly, so we have to do it this way
    # `script` is a string that looks like this (the first character is a newline):
    """
<script
    src="http://localhost:5006/amped/autoload.js?bokeh-autoload-element=6b813263-05df-45a5-bd91-e25c5e53c020"
    id="6b813263-05df-45a5-bd91-e25c5e53c020"
    data-bokeh-model-id=""
    data-bokeh-doc-id=""
></script>
"""
    # so to add on the necessary parameters, we have to insert them manually.  hopefully we won't need to urlencode anything.
    # note that request.args = a MultiDict, so be careful of duplicate params
    # http://werkzeug.pocoo.org/docs/0.11/datastructures/#werkzeug.datastructures.MultiDict
    
    script_list = script.split("\n")
    script_list[2] = script_list[2][:-1]
    for key in request.args:
        script_list[2] = script_list[2] + "&{}={}".format(key, request.args[key])
    script_list[2] = script_list[2] + '"'
    script = "\n".join(script_list)
    return render_template("amped.html", script = script)
```
This goes in and manually edits the script tag returned by `autoload_server`.  It doesn't url-encode any of the request arguments, so beware!  In my specific case, this is acceptable, but in general it may not be.  Hopefully this is useful to other people with this problem. 
I've looked at the source code for `autoload_server` and it seems like this can be implemented quite easily, at least for GET request parameters.  Just allowing an optional argument of a list of (key, value) tuples and then having something like this near the bottom of the function:

```
for key, value in request_parameters:
    src_path = src_path + "&{}={}".format(key, value)
```
would be most of the additions.  URL-encoding the keys and values would be good too.  I would be happy to do the work for this, but I have no experience with contributing to other people's repos on github.  If someone could point me to an explanation of the process of making a PR, adding code tests, documentation, etc, I'd love to contribute this to Bokeh!
Nice moves @kevinsa5 , I see what you're doing, modifying the src url tucked into the script tag's string. I too would like to see the source updated on autoload_server to permit pushing parameters down into the Bokeh documents.

Unpacking my simple passed string "?paramName=someParam" went like this...

```
def modify_doc(doc) :
    args      = doc.session_context.request.arguments
    paramName = str( args['paramName'][0].decode('utf-8') )

```
Too late for `0.12.6` but I have scheduled for the following milestone. 

Resolving patch:
diff --git a/bokeh/embed.py b/bokeh/embed.py
--- a/bokeh/embed.py
+++ b/bokeh/embed.py
@@ -483,7 +483,8 @@ def autoload_static(model, resources, script_path):
 
     return encode_utf8(js), encode_utf8(tag)
 
-def _connect_session_or_document(model=None, app_path=None, session_id=None, url="default", relative_urls=False, resources="default"):
+
+def _connect_session_or_document(model=None, app_path=None, session_id=None, url="default", relative_urls=False, resources="default", arguments=None):
     ''' Return a script tag that embeds content from a Bokeh server. This is
     a private method not meant to be called directly. Instead it is meant to be
     called by other methods that thinly wrap around it for different use cases:
@@ -541,6 +542,10 @@ def _connect_session_or_document(model=None, app_path=None, session_id=None, url
             files you'll load separately are of the same version as that of the
             server's, otherwise the rendering may not work correctly.
 
+        arguments (dict[str, str], optional) : A dictionary of the arguments to
+            be passed to Bokeh (default: None)
+
+
     Returns:
         A ``<script>`` tag that will embed content from a Bokeh Server.
 
@@ -594,6 +599,8 @@ def _connect_session_or_document(model=None, app_path=None, session_id=None, url
             typically not desired.
 
     '''
+    from .client.session import _encode_query_param
+
     if app_path is not None:
         deprecated((0, 12, 5), "app_path", "url", "Now pass entire app URLS in the url arguments, e.g. 'url=http://foo.com:5010/bar/myapp'")
         if not app_path.startswith("/"):
@@ -637,6 +644,11 @@ def _connect_session_or_document(model=None, app_path=None, session_id=None, url
     if resources is None:
         src_path = src_path + "&resources=none"
 
+    if arguments is not None:
+        for key, value in arguments.items():
+            if not key.startswith("bokeh-"):
+                src_path = src_path + "&{}={}".format(_encode_query_param(str(key)), _encode_query_param(str(value)))
+
     tag = AUTOLOAD_TAG.render(
         src_path = src_path,
         app_path = app_path,
@@ -648,27 +660,33 @@ def _connect_session_or_document(model=None, app_path=None, session_id=None, url
 
 _connect_session_or_document.__doc__ = format_docstring(_connect_session_or_document.__doc__, DEFAULT_SERVER_HTTP_URL=DEFAULT_SERVER_HTTP_URL)
 
-def autoload_server(model=None, app_path=None, session_id=None, url="default", relative_urls=False):
+def autoload_server(model=None, app_path=None, session_id=None, url="default", relative_urls=False, arguments=None):
     ''' Return a script tag that embeds content from a Bokeh server and loads
-    all the necessary JS/CSS resource files.
+    all the necessary JS/CSS resource files. Also accepts an optional dictionary
+    of arguments to be passed to Bokeh.
     '''
     deprecated((0, 12, 7), 'bokeh.embed.autoload_server', 'bokeh.embed.server_document or bokeh.embed.server_session')
-    return _connect_session_or_document(model=model, app_path=app_path, session_id=session_id, url=url, relative_urls=relative_urls, resources="default")
+    return _connect_session_or_document(model=model, app_path=app_path, session_id=session_id, url=url, relative_urls=relative_urls,
+                                        resources="default", arguments=arguments)
 
-def server_document(url="default", relative_urls=False, resources="default"):
+def server_document(url="default", relative_urls=False, resources="default", arguments=None):
     ''' Works similarly to ``autoload_server`` except a new session will be
-    systematically generated and an entire document will be returned. Also the
+    systematically generated and an entire document will be returned. Also
+    accepts an optional dictionary of arguments to be passed to Bokeh, and
     resources files may optionally be omitted from loading by passing
     resources="none".
     '''
-    return _connect_session_or_document(model=None, session_id=None, url=url, relative_urls=relative_urls, resources=resources)
+    return _connect_session_or_document(model=None, session_id=None, url=url, relative_urls=relative_urls,
+                                        resources=resources, arguments=arguments)
 
-def server_session(model, session_id, url="default", relative_urls=False, resources="default"):
+def server_session(model, session_id, url="default", relative_urls=False, resources="default", arguments=None):
     ''' Works similarly to ``autoload_server`` except an existing session id and
-    model must be provided. Also the resources files may optionally be omitted
-    from loading by passing resources="none".
+    model must be provided. Also accepts an optional dictionary of arguments to
+    be passed to Bokeh, and resources files may optionally be omitted from
+    loading by passing resources="none".
     '''
-    return _connect_session_or_document(model, session_id=session_id, url=url, relative_urls=relative_urls, resources=resources)
+    return _connect_session_or_document(model, session_id=session_id, url=url, relative_urls=relative_urls,
+                                        resources=resources, arguments=arguments)
 
 def _script_for_render_items(docs_json, render_items, app_path=None, absolute_url=None):
     js = DOC_JS.render(
diff --git a/examples/embed/arguments/bokeh_server.py b/examples/embed/arguments/bokeh_server.py
new file mode 100644
--- /dev/null
+++ b/examples/embed/arguments/bokeh_server.py
@@ -0,0 +1,82 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+'''This example demonstrates embedding an autoloaded Bokeh server
+into a simple Flask application, and passing arguments to Bokeh.
+
+To view the example, run:
+
+    python flask_server.py
+
+in this directory, and navigate to:
+
+    http://localhost:5000
+
+'''
+import numpy as np
+
+from bokeh.io import curdoc
+from bokeh.layouts import row, widgetbox
+from bokeh.models import ColumnDataSource
+from bokeh.models.widgets import Slider, TextInput
+from bokeh.plotting import figure
+
+# Retrieving the arguments
+args = curdoc().session_context.request.arguments
+
+try:
+    batchid = int(args.get('batchid')[0])
+except (ValueError, TypeError):
+    batchid = 1
+
+dispatch = {1 : np.sin,
+            2 : np.cos,
+            3 : np.tan}
+
+# Set up data
+N = 200
+x = np.linspace(0, 4*np.pi, N)
+y = dispatch[batchid](x)
+source = ColumnDataSource(data=dict(x=x, y=y))
+
+# Set up plot
+plot = figure(plot_height=400, plot_width=400, title="my wave",
+              tools="crosshair,pan,reset,save,wheel_zoom",
+              x_range=[0, 4*np.pi], y_range=[-2.5, 2.5])
+
+plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)
+
+# Set up widgets
+text = TextInput(title="title", value="Batch n°{}".format(batchid))
+offset = Slider(title="offset", value=0.0, start=-5.0, end=5.0, step=0.1)
+amplitude = Slider(title="amplitude", value=1.0, start=-5.0, end=5.0)
+phase = Slider(title="phase", value=0.0, start=0.0, end=2*np.pi)
+freq = Slider(title="frequency", value=1.0, start=0.1, end=5.1)
+
+# Set up callbacks
+def update_title(attrname, old, new):
+    plot.title.text = text.value
+
+text.on_change('value', update_title)
+
+def update_data(attrname, old, new):
+
+    # Get the current slider values
+    a = amplitude.value
+    b = offset.value
+    w = phase.value
+    k = freq.value
+
+    # Generate the new curve
+    x = np.linspace(0, 4*np.pi, N)
+    y = a*np.sin(k*x + w) + b
+
+    source.data = dict(x=x, y=y)
+
+for w in [offset, amplitude, phase, freq]:
+    w.on_change('value', update_data)
+
+# Set up layouts and add to document
+inputs = widgetbox(text, offset, amplitude, phase, freq)
+
+curdoc().add_root(row(inputs, plot, width=800))
diff --git a/examples/embed/arguments/flask_server.py b/examples/embed/arguments/flask_server.py
new file mode 100644
--- /dev/null
+++ b/examples/embed/arguments/flask_server.py
@@ -0,0 +1,65 @@
+'''This example demonstrates embedding an autoloaded Bokeh server
+into a simple Flask application, and passing arguments to Bokeh.
+
+To view the example, run:
+
+    python flask_server.py
+
+in this directory, and navigate to:
+
+    http://localhost:5000
+
+'''
+import atexit
+import subprocess
+
+from flask import render_template_string, Flask
+from bokeh.embed import server_document
+
+home_html = """
+<!DOCTYPE html>
+<html lang="en">
+  <body>
+    <div class="bk-root">
+      <h1><a href="/batch/1"> Batch 1 (cos)</a></h1>
+      <h1><a href="/batch/2"> Batch 2 (sin)</a></h1>
+      <h1><a href="/batch/3"> Batch 3 (tan)</a></h1>
+    </div>
+  </body>
+</html>
+"""
+
+app_html = """
+<!DOCTYPE html>
+<html lang="en">
+  <body>
+    <div>
+        <h2><a href="/batch/1">Batch 1 (cos)</a> - <a href="/batch/2">Batch 2 (sin)</a> - <a href="/batch/3">Batch 3 (tan)</a></h2>
+    </div>
+    <div class="bk-root">
+      {{ bokeh_script|safe }}
+    </div>
+  </body>
+</html>
+"""
+
+app = Flask(__name__)
+
+bokeh_process = subprocess.Popen(
+    ['bokeh', 'serve', '--allow-websocket-origin=localhost:5000', 'bokeh_server.py'], stdout=subprocess.PIPE)
+
+@atexit.register
+def kill_server():
+    bokeh_process.kill()
+
+@app.route('/')
+def home():
+    return render_template_string(home_html)
+
+@app.route('/batch/<int:batchid>')
+def visualization(batchid):
+    bokeh_script = server_document(url='http://localhost:5006/bokeh_server', arguments=dict(batchid=batchid))
+    return render_template_string(app_html, bokeh_script=bokeh_script)
+
+if __name__ == '__main__':
+    app.run(debug=True)


Test patch:
diff --git a/bokeh/tests/test_embed.py b/bokeh/tests/test_embed.py
--- a/bokeh/tests/test_embed.py
+++ b/bokeh/tests/test_embed.py
@@ -431,6 +431,29 @@ def test_script_attrs_url_and_app_path_provided(self):
                                    'src' : src },
                                  attrs)
 
+    def test_script_attrs_arguments_provided(self):
+        r = embed.server_document(arguments=dict(foo=10))
+        self.assertTrue('foo=10' in r)
+        html = bs4.BeautifulSoup(r)
+        scripts = html.findAll(name='script')
+        self.assertEqual(len(scripts), 1)
+        attrs = scripts[0].attrs
+        self.assertTrue(set(attrs), set([
+            'src',
+            'data-bokeh-doc-id',
+            'data-bokeh-model-id',
+            'id'
+        ]))
+        divid = attrs['id']
+        src = "%s/autoload.js?bokeh-autoload-element=%s&bokeh-absolute-url=%s&foo=10" % \
+              ("http://localhost:5006", divid, "http://localhost:5006")
+        self.assertDictEqual({ 'data-bokeh-doc-id' : '',
+                               'data-bokeh-model-id' : '',
+                               'id' : divid,
+                               'src' : src },
+                             attrs)
+
+
 @mock.patch('bokeh.document.check_integrity')
 def test_modelindocument_validates_document_by_default(check_integrity):
     p = figure()