Source code for bokeh.resources

''' The resources module provides the Resources class for easily configuring
how BokehJS code and CSS resources should be located, loaded, and embedded in
Bokeh documents.

Also provides some pre-configured Resources objects.

Attributes:
    CDN : load minified BokehJS from CDN
    INLINE : provide minified BokehJS from library static directory

'''

from __future__ import absolute_import

import logging
logger = logging.getLogger(__name__)

import re
import json
from os.path import basename, join, relpath

from six import string_types

from . import __version__
from .core.templates import JS_RESOURCES, CSS_RESOURCES
from .settings import settings

from .util.paths import bokehjsdir
from .util.session_id import generate_session_id
from .util.compiler import gen_custom_models_static
from .model import Model

DEFAULT_SERVER_HOST = "localhost"
DEFAULT_SERVER_PORT = 5006
DEFAULT_SERVER_HTTP_URL = "http://%s:%d/" % (DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT)

def websocket_url_for_server_url(url):
    if url.startswith("http:"):
        reprotocoled = "ws" + url[4:]
    elif url.startswith("https:"):
        reprotocoled = "wss" + url[5:]
    else:
        raise ValueError("URL has unknown protocol " + url)
    if reprotocoled.endswith("/"):
        return reprotocoled + "ws"
    else:
        return reprotocoled + "/ws"


def server_url_for_websocket_url(url):
    if url.startswith("ws:"):
        reprotocoled = "http" + url[2:]
    elif url.startswith("wss:"):
        reprotocoled = "https" + url[3:]
    else:
        raise ValueError("URL has non-websocket protocol " + url)
    if not reprotocoled.endswith("/ws"):
        raise ValueError("websocket URL does not end in /ws")
    return reprotocoled[:-2]

class _SessionCoordinates(object):
    """ Internal class used to parse kwargs for server URL, app_path, and session_id."""
    def __init__(self, kwargs):
        """ Using kwargs which may have extra stuff we don't care about, compute websocket url and session ID."""

        self._base_url = kwargs.get('url', DEFAULT_SERVER_HTTP_URL)
        if self._base_url is None:
            raise ValueError("url cannot be None")
        if self._base_url == 'default':
            self._base_url = DEFAULT_SERVER_HTTP_URL
        if self._base_url.startswith("ws"):
            raise ValueError("url should be the http or https URL for the server, not the websocket URL")

        # base_url always has trailing slash, host:port/{prefix/}
        if not self._base_url.endswith("/"):
            self._base_url = self._base_url + "/"

        self._app_path = kwargs.get('app_path', '/')
        if self._app_path is None:
            raise ValueError("app_path cannot be None")
        if not self._app_path.startswith("/"):
            raise ValueError("app_path should start with a '/' character")
        if self._app_path != '/' and self._app_path.endswith("/"):
            self._app_path = self._app_path[:-1] # chop off trailing slash

        self._session_id = kwargs.get('session_id')
        # we lazy-generate the session_id so we can generate
        # it server-side when appropriate

        # server_url never has trailing slash because it's
        # prettier like host:port/app_path without a slash
        if self._app_path == '/':
            self._server_url = self._base_url[:-1] # chop off trailing slash
        else:
            self._server_url = self._base_url + self._app_path[1:]

    @property
    def websocket_url(self):
        """ Websocket URL derived from the kwargs provided."""
        return websocket_url_for_server_url(self._server_url)

    @property
    def server_url(self):
        """ Server URL including app path derived from the kwargs provided."""
        return self._server_url

    @property
    def url(self):
        """ Server base URL derived from the kwargs provided (no app path)."""
        return self._base_url

    @property
    def session_id(self):
        """ Session ID derived from the kwargs provided."""
        if self._session_id is None:
            self._session_id = generate_session_id()
        return self._session_id

    @property
    def session_id_allowing_none(self):
        """ Session ID provided in kwargs, keeping it None if it hasn't been generated yet.

        The purpose of this is to preserve ``None`` as long as possible... in some cases
        we may never generate the session ID because we generate it on the server.
        """
        return self._session_id

    @property
    def app_path(self):
        """ App path derived from the kwargs provided."""
        return self._app_path

DEFAULT_SERVER_WEBSOCKET_URL = websocket_url_for_server_url(DEFAULT_SERVER_HTTP_URL)

_DEV_PAT = re.compile(r"^(\d)+\.(\d)+\.(\d)+(dev|rc)")


def _cdn_base_url():
    return "https://cdn.bokeh.org"


def _get_cdn_urls(components, version=None, minified=True):
    if version is None:
        if settings.docs_cdn():
            version = settings.docs_cdn()
        else:
            version = __version__.split('-')[0]

    # check if we want minified js and css
    _min = ".min" if minified else ""

    base_url = _cdn_base_url()
    dev_container = 'bokeh/dev'
    rel_container = 'bokeh/release'

    # check the 'dev' fingerprint
    container = dev_container if _DEV_PAT.match(version) else rel_container

    if version.endswith(('dev', 'rc')):
        logger.debug("Getting CDN URL for local dev version will not produce usable URL")

    def mk_url(comp, kind):
        return '%s/%s/%s-%s%s.%s' % (base_url, container, comp, version, _min, kind)

    result = {
        'urls'     : lambda kind: [ mk_url(component, kind) for component in components ],
        'messages' : [],
    }

    if len(__version__.split('-')) > 1:
        result['messages'].append({
            "type" : "warn",
            "text" : ("Requesting CDN BokehJS version '%s' from Bokeh development version '%s'. "
                      "This configuration is unsupported and may not work!" % (version, __version__))
        })

    return result


def _get_server_urls(components, root_url, minified=True, path_versioner=None):
    _min = ".min" if minified else ""

    def mk_url(comp, kind):
        path = "%s/%s%s.%s" % (kind, comp, _min, kind)
        if path_versioner is not None:
            path = path_versioner(path)
        return '%sstatic/%s' % (root_url, path)

    return {
        'urls'     : lambda kind: [ mk_url(component, kind) for component in components ],
        'messages' : [],
    }


class BaseResources(object):
    _default_root_dir = "."
    _default_root_url = DEFAULT_SERVER_HTTP_URL

    def __init__(self, mode='inline', version=None, root_dir=None,
                 minified=True, log_level="info", root_url=None,
                 path_versioner=None, components=None):

        self.components = components if components is not None else ["bokeh", "bokeh-widgets"]

        self.mode = settings.resources(mode);           del mode
        self.root_dir = settings.rootdir(root_dir);     del root_dir
        self.version = settings.version(version);       del version
        self.minified = settings.minified(minified);    del minified
        self.log_level = settings.log_level(log_level); del log_level
        self.path_versioner = path_versioner;           del path_versioner

        if root_url and not root_url.endswith("/"):
            logger.warning("root_url should end with a /, adding one")
            root_url = root_url + "/"
        self._root_url = root_url
        if self.mode not in ['inline', 'cdn', 'server', 'server-dev', 'relative', 'relative-dev', 'absolute', 'absolute-dev']:
            raise ValueError("wrong value for 'mode' parameter, expected "
                             "'inline', 'cdn', 'server(-dev)', 'relative(-dev)' or 'absolute(-dev)', got %r" % self.mode)

        if self.root_dir and not self.mode.startswith("relative"):
            raise ValueError("setting 'root_dir' makes sense only when 'mode' is set to 'relative'")

        if self.version and not self.mode.startswith('cdn'):
            raise ValueError("setting 'version' makes sense only when 'mode' is set to 'cdn'")

        if root_url and not self.mode.startswith('server'):
            raise ValueError("setting 'root_url' makes sense only when 'mode' is set to 'server'")

        self.dev = self.mode.endswith('-dev')
        if self.dev:
            self.mode = self.mode[:-4]

        self.messages = []

        if self.mode == "cdn":
            cdn = self._cdn_urls()
            self.messages.extend(cdn['messages'])
        elif self.mode == "server":
            server = self._server_urls()
            self.messages.extend(server['messages'])

    @property
    def log_level(self):
        return self._log_level

    @log_level.setter
    def log_level(self, level):
        valid_levels = [
            "trace", "debug", "info", "warn", "error", "fatal"
        ]
        if not (level is None or level in valid_levels):
            raise ValueError("Unknown log level '%s', valid levels are: %s", str(valid_levels))
        self._log_level = level

    @property
    def root_url(self):
        if self._root_url:
            return self._root_url
        else:
            return self._default_root_url

    def _file_paths(self, kind):
        bokehjs_dir = bokehjsdir(self.dev)
        minified = ".min" if not self.dev and self.minified else ""
        files = [ "%s%s.%s" % (component, minified, kind) for component in self.components ]
        paths = [ join(bokehjs_dir, kind, file) for file in files ]
        return paths

    def _collect_external_resources(self, resource_attr):
        """ Collect external resources set on resource_attr attribute of all models."""

        external_resources = []

        for _, cls in sorted(Model.model_class_reverse_map.items(), key=lambda arg: arg[0]):
            external = getattr(cls, resource_attr, None)

            if isinstance(external, string_types):
                if external not in external_resources:
                    external_resources.append(external)
            elif isinstance(external, list):
                for e in external:
                    if e not in external_resources:
                        external_resources.append(e)

        return external_resources


    def _cdn_urls(self):
        return _get_cdn_urls(self.components, self.version, self.minified)

    def _server_urls(self):
        return _get_server_urls(self.components, self.root_url, False if self.dev else self.minified, self.path_versioner)

    def _resolve(self, kind):
        paths = self._file_paths(kind)
        files, raw = [], []

        if self.mode == "inline":
            raw = [ self._inline(path) for path in paths ]
        elif self.mode == "relative":
            root_dir = self.root_dir or self._default_root_dir
            files = [ relpath(path, root_dir) for path in paths ]
        elif self.mode == "absolute":
            files = list(paths)
        elif self.mode == "cdn":
            cdn = self._cdn_urls()
            files = list(cdn['urls'](kind))
        elif self.mode == "server":
            server = self._server_urls()
            files = list(server['urls'](kind))

        return (files, raw)

    def _inline(self, path):
        begin = "/* BEGIN %s */" % basename(path)
        try:
            with open(path, 'rb') as f:
                middle = f.read().decode("utf-8")
        except IOError:
            middle = ""
        end = "/* END %s */"  % basename(path)
        return "%s\n%s\n%s" % (begin, middle, end)

[docs]class JSResources(BaseResources): ''' The Resources class encapsulates information relating to loading or embedding Bokeh Javascript. Args: mode (str) : how should Bokeh JS be included in output See below for descriptions of available modes version (str, optional) : what version of Bokeh JS to load Only valid with the ``'cdn'`` mode root_dir (str, optional) : root directory for loading Bokeh JS assets Only valid with ``'relative'`` and ``'relative-dev'`` modes minified (bool, optional) : whether JavaScript should be minified or not (default: True) root_url (str, optional) : URL and port of Bokeh Server to load resources from Only valid with ``'server'`` and ``'server-dev'`` modes The following **mode** values are available for configuring a Resource object: * ``'inline'`` configure to provide entire Bokeh JS and CSS inline * ``'cdn'`` configure to load Bokeh JS and CSS from ``https://cdn.bokeh.org`` * ``'server'`` configure to load from a Bokeh Server * ``'server-dev'`` same as ``server`` but supports non-minified assets * ``'relative'`` configure to load relative to the given directory * ``'relative-dev'`` same as ``relative`` but supports non-minified assets * ``'absolute'`` configure to load from the installed Bokeh library static directory * ``'absolute-dev'`` same as ``absolute`` but supports non-minified assets Once configured, a Resource object exposes the following public attributes: Attributes: css_raw : any raw CSS that needs to be places inside ``<style>`` tags css_files : URLs of any CSS files that need to be loaded by ``<link>`` tags messages : any informational messages concerning this configuration These attributes are often useful as template parameters when embedding Bokeh plots. ''' def _autoload_path(self, elementid): return self.root_url + "bokeh/autoload.js/%s" % elementid @property def js_files(self): files, _ = self._resolve('js') external_resources = self._collect_external_resources('__javascript__') files.extend(external_resources) return files @property def js_raw(self): _, raw = self._resolve('js') if self.log_level is not None: raw.append('Bokeh.set_log_level("%s");' % self.log_level) custom_models = gen_custom_models_static() if custom_models is not None: raw.append(custom_models) return raw def render_js(self): return JS_RESOURCES.render(js_raw=self.js_raw, js_files=self.js_files)
[docs]class CSSResources(BaseResources): ''' The CSSResources class encapsulates information relating to loading or embedding Bokeh client-side CSS. Args: mode (str) : how should Bokeh CSS be included in output See below for descriptions of available modes version (str, optional) : what version of Bokeh CSS to load Only valid with the ``'cdn'`` mode root_dir (str, optional) : root directory for loading BokehJS resources Only valid with ``'relative'`` and ``'relative-dev'`` modes minified (bool, optional) : whether CSS should be minified or not (default: True) root_url (str, optional) : URL and port of Bokeh Server to load resources from Only valid with ``'server'`` and ``'server-dev'`` modes The following **mode** values are available for configuring a Resource object: * ``'inline'`` configure to provide entire BokehJS code and CSS inline * ``'cdn'`` configure to load Bokeh CSS from ``https://cdn.bokeh.org`` * ``'server'`` configure to load from a Bokeh Server * ``'server-dev'`` same as ``server`` but supports non-minified CSS * ``'relative'`` configure to load relative to the given directory * ``'relative-dev'`` same as ``relative`` but supports non-minified CSS * ``'absolute'`` configure to load from the installed Bokeh library static directory * ``'absolute-dev'`` same as ``absolute`` but supports non-minified CSS Once configured, a Resource object exposes the following public attributes: Attributes: css_raw : any raw CSS that needs to be places inside ``<style>`` tags css_files : URLs of any CSS files that need to be loaded by ``<link>`` tags messages : any informational messages concerning this configuration These attributes are often useful as template parameters when embedding Bokeh plots. ''' @property def css_files(self): files, _ = self._resolve('css') external_resources = self._collect_external_resources("__css__") files.extend(external_resources) return files @property def css_raw(self): _, raw = self._resolve('css') return raw @property def css_raw_str(self): return [ json.dumps(css) for css in self.css_raw ] def render_css(self): return CSS_RESOURCES.render(css_raw=self.css_raw, css_files=self.css_files)
[docs]class Resources(JSResources, CSSResources): ''' The Resources class encapsulates information relating to loading or embedding Bokeh Javascript and CSS. Args: mode (str) : how should Bokeh JS and CSS be included in output See below for descriptions of available modes version (str, optional) : what version of Bokeh JS and CSS to load Only valid with the ``'cdn'`` mode root_dir (str, optional) : root directory for loading Bokeh JS and CSS assets Only valid with ``'relative'`` and ``'relative-dev'`` modes minified (bool, optional) : whether JavaScript and CSS should be minified or not (default: True) root_url (str, optional) : URL and port of Bokeh Server to load resources from Only valid with ``'server'`` and ``'server-dev'`` modes The following **mode** values are available for configuring a Resource object: * ``'inline'`` configure to provide entire Bokeh JS and CSS inline * ``'cdn'`` configure to load Bokeh JS and CSS from ``https://cdn.bokeh.org`` * ``'server'`` configure to load from a Bokeh Server * ``'server-dev'`` same as ``server`` but supports non-minified assets * ``'relative'`` configure to load relative to the given directory * ``'relative-dev'`` same as ``relative`` but supports non-minified assets * ``'absolute'`` configure to load from the installed Bokeh library static directory * ``'absolute-dev'`` same as ``absolute`` but supports non-minified assets Once configured, a Resource object exposes the following public attributes: Attributes: js_raw : any raw JS that needs to be placed inside ``<script>`` tags css_raw : any raw CSS that needs to be places inside ``<style>`` tags js_files : URLs of any JS files that need to be loaded by ``<script>`` tags css_files : URLs of any CSS files that need to be loaded by ``<link>`` tags messages : any informational messages concerning this configuration These attributes are often useful as template parameters when embedding Bokeh plots. ''' def render(self): return "%s\n%s" % (self.render_css(), self.render_js())
CDN = Resources(mode="cdn") INLINE = Resources(mode="inline") EMPTY = Resources(mode="inline", components=[], log_level=None)