1
"""
2
Utilities for creating bokeh Server instances.
3
"""
4 7
from __future__ import absolute_import, division, unicode_literals
5

6 7
import datetime as dt
7 7
import os
8 7
import signal
9 7
import sys
10 7
import threading
11 7
import uuid
12

13 7
from collections import OrderedDict
14 7
from contextlib import contextmanager
15 7
from functools import partial
16 7
from types import FunctionType, MethodType
17

18 7
from bokeh.document.events import ModelChangedEvent
19 7
from bokeh.embed.bundle import extension_dirs
20 7
from bokeh.io import curdoc
21 7
from bokeh.server.server import Server
22 7
from tornado.ioloop import IOLoop
23 7
from tornado.websocket import WebSocketHandler
24 7
from tornado.web import RequestHandler, StaticFileHandler
25 7
from tornado.wsgi import WSGIContainer
26

27 7
from .state import state
28

29
#---------------------------------------------------------------------
30
# Private API
31
#---------------------------------------------------------------------
32

33
# Handle serving of the panel extension before session is loaded
34 7
extension_dirs['panel'] = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'dist'))
35

36 7
INDEX_HTML = os.path.join(os.path.dirname(__file__), '..', '_templates', "index.html")
37

38 7
def _origin_url(url):
39 0
    if url.startswith("http"):
40 0
        url = url.split("//")[1]
41 0
    return url
42

43

44 7
def _server_url(url, port):
45 0
    if url.startswith("http"):
46 0
        return '%s:%d%s' % (url.rsplit(':', 1)[0], port, "/")
47
    else:
48 0
        return 'http://%s:%d%s' % (url.split(':')[0], port, "/")
49

50

51 7
def init_doc(doc):
52 7
    doc = doc or curdoc()
53 7
    if not doc.session_context:
54 7
        return doc
55

56 7
    from ..config import config
57 7
    session_id = doc.session_context.id
58 7
    sessions = state.session_info['sessions']
59 7
    if config.session_history == 0 or session_id in sessions:
60 7
        return doc
61

62 7
    state.session_info['total'] += 1
63 7
    if config.session_history > 0 and len(sessions) >= config.session_history:
64 0
        old_history = list(sessions.items())
65 0
        sessions = OrderedDict(old_history[-(config.session_history-1):])
66 0
        state.session_info['sessions'] = sessions
67 7
    sessions[session_id] = {
68
        'started': dt.datetime.now().timestamp(),
69
        'rendered': None,
70
        'ended': None,
71
        'user_agent': state.headers.get('User-Agent')
72
    }
73 7
    doc.on_event('document_ready', state._init_session)
74 7
    return doc
75

76

77 7
@contextmanager
78 2
def set_curdoc(doc):
79 7
    state.curdoc = doc
80 7
    yield
81 7
    state.curdoc = None
82

83

84 7
def _eval_panel(panel, server_id, title, location, doc):
85 7
    from ..template import BaseTemplate
86 7
    from ..pane import panel as as_panel
87

88 7
    with set_curdoc(doc):
89 7
        if isinstance(panel, (FunctionType, MethodType)):
90 0
            panel = panel()
91 7
        if isinstance(panel, BaseTemplate):
92 0
            doc = panel._modify_doc(server_id, title, doc, location)
93
        else:
94 7
            doc = as_panel(panel)._modify_doc(server_id, title, doc, location)
95 7
        return doc
96

97
#---------------------------------------------------------------------
98
# Public API
99
#---------------------------------------------------------------------
100

101

102 7
@contextmanager
103 2
def unlocked():
104
    """
105
    Context manager which unlocks a Document and dispatches
106
    ModelChangedEvents triggered in the context body to all sockets
107
    on current sessions.
108
    """
109 7
    curdoc = state.curdoc
110 7
    if curdoc is None or curdoc.session_context is None or curdoc.session_context.session is None:
111 7
        yield
112 7
        return
113 0
    connections = curdoc.session_context.session._subscribed_connections
114

115 0
    hold = curdoc._hold
116 0
    if hold:
117 0
        old_events = list(curdoc._held_events)
118
    else:
119 0
        old_events = []
120 0
        curdoc.hold()
121 0
    try:
122 0
        yield
123 0
        events = []
124 0
        for conn in connections:
125 0
            socket = conn._socket
126 0
            if hasattr(socket, 'write_lock') and socket.write_lock._block._value == 0:
127 0
                state._locks.add(socket)
128 0
            locked = socket in state._locks
129 0
            for event in curdoc._held_events:
130 0
                if (isinstance(event, ModelChangedEvent) and event not in old_events
131
                    and hasattr(socket, 'write_message') and not locked):
132 0
                    msg = conn.protocol.create('PATCH-DOC', [event])
133 0
                    WebSocketHandler.write_message(socket, msg.header_json)
134 0
                    WebSocketHandler.write_message(socket, msg.metadata_json)
135 0
                    WebSocketHandler.write_message(socket, msg.content_json)
136 0
                    for header, payload in msg._buffers:
137 0
                        WebSocketHandler.write_message(socket, header)
138 0
                        WebSocketHandler.write_message(socket, payload, binary=True)
139 0
                elif event not in events:
140 0
                    events.append(event)
141 0
        curdoc._held_events = events
142
    finally:
143 0
        if not hold:
144 0
            curdoc.unhold()
145

146

147 7
def serve(panels, port=0, address=None, websocket_origin=None, loop=None,
148
          show=True, start=True, title=None, verbose=True, location=True,
149
          threaded=False, **kwargs):
150
    """
151
    Allows serving one or more panel objects on a single server.
152
    The panels argument should be either a Panel object or a function
153
    returning a Panel object or a dictionary of these two. If a 
154
    dictionary is supplied the keys represent the slugs at which
155
    each app is served, e.g. `serve({'app': panel1, 'app2': panel2})`
156
    will serve apps at /app and /app2 on the server.
157

158
    Arguments
159
    ---------
160
    panel: Viewable, function or {str: Viewable or function}
161
      A Panel object, a function returning a Panel object or a
162
      dictionary mapping from the URL slug to either.
163
    port: int (optional, default=0)
164
      Allows specifying a specific port
165
    address : str
166
      The address the server should listen on for HTTP requests.
167
    websocket_origin: str or list(str) (optional)
168
      A list of hosts that can connect to the websocket.
169

170
      This is typically required when embedding a server app in
171
      an external web site.
172

173
      If None, "localhost" is used.
174
    loop : tornado.ioloop.IOLoop (optional, default=IOLoop.current())
175
      The tornado IOLoop to run the Server on
176
    show : boolean (optional, default=False)
177
      Whether to open the server in a new browser tab on start
178
    start : boolean(optional, default=False)
179
      Whether to start the Server
180
    title: str or {str: str} (optional, default=None)
181
      An HTML title for the application or a dictionary mapping
182
      from the URL slug to a customized title
183
    verbose: boolean (optional, default=True)
184
      Whether to print the address and port
185
    location : boolean or panel.io.location.Location
186
      Whether to create a Location component to observe and
187
      set the URL location.
188
    threaded: boolean (default=False)
189
      Whether to start the server on a new Thread
190
    kwargs: dict
191
      Additional keyword arguments to pass to Server instance
192
    """
193 7
    kwargs = dict(kwargs, **dict(
194
        port=port, address=address, websocket_origin=websocket_origin,
195
        loop=loop, show=show, start=start, title=title, verbose=verbose,
196
        location=location
197
    ))
198 7
    if threaded:
199 7
        from tornado.ioloop import IOLoop
200 7
        kwargs['loop'] = loop = IOLoop() if loop is None else loop
201 7
        server = StoppableThread(
202
            target=get_server, io_loop=loop, args=(panels,), kwargs=kwargs
203
        )
204 7
        server.start()
205
    else:
206 7
        server = get_server(panels, **kwargs)
207 7
    return server
208

209

210 7
class ProxyFallbackHandler(RequestHandler):
211
    """A `RequestHandler` that wraps another HTTP server callback and
212
    proxies the subpath.
213
    """
214

215 7
    def initialize(self, fallback, proxy=None):
216 0
        self.fallback = fallback
217 0
        self.proxy = proxy
218

219 7
    def prepare(self):
220 0
        if self.proxy:
221 0
            self.request.path = self.request.path.replace(self.proxy, '')
222 0
        self.fallback(self.request)
223 0
        self._finished = True
224 0
        self.on_finish()
225

226

227 7
def get_static_routes(static_dirs):
228
    """
229
    Returns a list of tornado routes of StaticFileHandlers given a
230
    dictionary of slugs and file paths to serve.
231
    """
232 7
    patterns = []
233 7
    for slug, path in static_dirs.items():
234 7
        if not slug.startswith('/'):
235 7
            slug = '/' + slug
236 7
        if slug == '/static':
237 0
            raise ValueError("Static file route may not use /static "
238
                             "this is reserved for internal use.")
239 7
        path = os.path.abspath(path)
240 7
        if not os.path.isdir(path):
241 0
            raise ValueError("Cannot serve non-existent path %s" % path)
242 7
        patterns.append(
243
            (r"%s/(.*)" % slug, StaticFileHandler, {"path": path})
244
        )
245 7
    return patterns
246

247

248 7
def get_server(panel, port=0, address=None, websocket_origin=None,
249
               loop=None, show=False, start=False, title=None,
250
               verbose=False, location=True, static_dirs={},
251
               oauth_provider=None, oauth_key=None, oauth_secret=None,
252
               oauth_extra_params={}, cookie_secret=None,
253
               oauth_encryption_key=None, **kwargs):
254
    """
255
    Returns a Server instance with this panel attached as the root
256
    app.
257

258
    Arguments
259
    ---------
260
    panel: Viewable, function or {str: Viewable}
261
      A Panel object, a function returning a Panel object or a
262
      dictionary mapping from the URL slug to either.
263
    port: int (optional, default=0)
264
      Allows specifying a specific port
265
    address : str
266
      The address the server should listen on for HTTP requests.
267
    websocket_origin: str or list(str) (optional)
268
      A list of hosts that can connect to the websocket.
269

270
      This is typically required when embedding a server app in
271
      an external web site.
272

273
      If None, "localhost" is used.
274
    loop : tornado.ioloop.IOLoop (optional, default=IOLoop.current())
275
      The tornado IOLoop to run the Server on.
276
    show : boolean (optional, default=False)
277
      Whether to open the server in a new browser tab on start.
278
    start : boolean(optional, default=False)
279
      Whether to start the Server.
280
    title : str or {str: str} (optional, default=None)
281
      An HTML title for the application or a dictionary mapping
282
      from the URL slug to a customized title.
283
    verbose: boolean (optional, default=False)
284
      Whether to report the address and port.
285
    location : boolean or panel.io.location.Location
286
      Whether to create a Location component to observe and
287
      set the URL location.
288
    static_dirs: dict (optional, default={})
289
      A dictionary of routes and local paths to serve as static file
290
      directories on those routes.
291
    oauth_provider: str
292
      One of the available OAuth providers
293
    oauth_key: str (optional, default=None)
294
      The public OAuth identifier
295
    oauth_secret: str (optional, default=None)
296
      The client secret for the OAuth provider
297
    oauth_extra_params: dict (optional, default={})
298
      Additional information for the OAuth provider
299
    cookie_secret: str (optional, default=None)
300
      A random secret string to sign cookies (required for OAuth)
301
    oauth_encryption_key: str (optional, default=False)
302
      A random encryption key used for encrypting OAuth user
303
      information and access tokens.
304
    kwargs: dict
305
      Additional keyword arguments to pass to Server instance.
306

307
    Returns
308
    -------
309
    server : bokeh.server.server.Server
310
      Bokeh Server instance running this panel
311
    """
312 7
    server_id = kwargs.pop('server_id', uuid.uuid4().hex)
313 7
    kwargs['extra_patterns'] = extra_patterns = kwargs.get('extra_patterns', [])
314 7
    if isinstance(panel, dict):
315 7
        apps = {}
316 7
        for slug, app in panel.items():
317 7
            if isinstance(title, dict):
318 7
                try:
319 7
                    title_ = title[slug]
320 7
                except KeyError:
321 7
                    raise KeyError(
322
                        "Keys of the title dictionnary and of the apps "
323
                        f"dictionary must match. No {slug} key found in the "
324
                        "title dictionnary.") 
325
            else:
326 0
                title_ = title
327 7
            slug = slug if slug.startswith('/') else '/'+slug
328 7
            if 'flask' in sys.modules:
329 0
                from flask import Flask
330 0
                if isinstance(app, Flask):
331 0
                    wsgi = WSGIContainer(app)
332 0
                    if slug == '/':
333 0
                        raise ValueError('Flask apps must be served on a subpath.')
334 0
                    if not slug.endswith('/'):
335 0
                        slug += '/'
336 0
                    extra_patterns.append(('^'+slug+'.*', ProxyFallbackHandler,
337
                                           dict(fallback=wsgi, proxy=slug)))
338 0
                    continue
339 7
            apps[slug] = partial(_eval_panel, app, server_id, title_, location)
340
    else:
341 7
        apps = {'/': partial(_eval_panel, panel, server_id, title, location)}
342

343 7
    extra_patterns += get_static_routes(static_dirs)
344

345 7
    opts = dict(kwargs)
346 7
    if loop:
347 7
        loop.make_current()
348 7
        opts['io_loop'] = loop
349 7
    elif opts.get('num_procs', 1) == 1:
350 7
        opts['io_loop'] = IOLoop.current()
351

352 7
    if 'index' not in opts:
353 7
        opts['index'] = INDEX_HTML
354

355 7
    if address is not None:
356 0
        opts['address'] = address
357

358 7
    if websocket_origin:
359 0
        if not isinstance(websocket_origin, list):
360 0
            websocket_origin = [websocket_origin]
361 0
        opts['allow_websocket_origin'] = websocket_origin
362

363
    # Configure OAuth
364 7
    from ..config import config
365 7
    if config.oauth_provider:
366 0
        from ..auth import OAuthProvider
367 0
        opts['auth_provider'] = OAuthProvider()
368 7
    if oauth_provider:
369 0
        config.oauth_provider = oauth_provider
370 7
    if oauth_key:
371 0
        config.oauth_key = oauth_key
372 7
    if oauth_extra_params:
373 0
        config.oauth_extra_params = oauth_extra_params
374 7
    if cookie_secret:
375 0
        config.cookie_secret = cookie_secret
376 7
    opts['cookie_secret'] = config.cookie_secret
377

378 7
    server = Server(apps, port=port, **opts)
379 7
    if verbose:
380 7
        address = server.address or 'localhost'
381 7
        print("Launching server at http://%s:%s" % (address, server.port))
382

383 7
    state._servers[server_id] = (server, panel, [])
384

385 7
    if show:
386 0
        def show_callback():
387 0
            server.show('/login' if config.oauth_provider else '/')
388 0
        server.io_loop.add_callback(show_callback)
389

390 7
    def sig_exit(*args, **kwargs):
391 0
        server.io_loop.add_callback_from_signal(do_stop)
392

393 7
    def do_stop(*args, **kwargs):
394 0
        server.io_loop.stop()
395

396 7
    try:
397 7
        signal.signal(signal.SIGINT, sig_exit)
398 7
    except ValueError:
399 7
        pass # Can't use signal on a thread
400

401 7
    if start:
402 7
        server.start()
403 7
        try:
404 7
            server.io_loop.start()
405 0
        except RuntimeError:
406 0
            pass
407 7
    return server
408

409

410 7
class StoppableThread(threading.Thread):
411
    """Thread class with a stop() method."""
412

413 7
    def __init__(self, io_loop=None, **kwargs):
414 7
        super(StoppableThread, self).__init__(**kwargs)
415 7
        self.io_loop = io_loop
416

417 7
    def run(self):
418 7
        if hasattr(self, '_target'):
419 7
            target, args, kwargs = self._target, self._args, self._kwargs
420
        else:
421 0
            target, args, kwargs = self._Thread__target, self._Thread__args, self._Thread__kwargs
422 7
        if not target:
423 0
            return
424 7
        bokeh_server = None
425 7
        try:
426 7
            bokeh_server = target(*args, **kwargs)
427
        finally:
428 7
            if isinstance(bokeh_server, Server):
429 7
                bokeh_server.stop()
430 7
            if hasattr(self, '_target'):
431 7
                del self._target, self._args, self._kwargs
432
            else:
433 0
                del self._Thread__target, self._Thread__args, self._Thread__kwargs
434

435 7
    def stop(self):
436 7
        self.io_loop.add_callback(self.io_loop.stop)

Read our documentation on viewing source code .

Loading