# -----------------------------------------------------------------------------# Copyright (c) Anaconda, Inc., and Bokeh Contributors.# All rights reserved.## The full license is in the file LICENSE.txt, distributed with this software.# -----------------------------------------------------------------------------""" The resources module provides the Resources class for easily configuringhow BokehJS code and CSS resources should be located, loaded, and embedded inBokeh documents.Additionally, functions for retrieving `Subresource Integrity`_ hashes forBokeh JavaScript files are provided here.Some pre-configured Resources objects are made available as attributes.Attributes: CDN : load minified BokehJS from CDN INLINE : provide minified BokehJS from library static directory.. _Subresource Integrity: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity"""# -----------------------------------------------------------------------------# Boilerplate# -----------------------------------------------------------------------------from__future__importannotationsimportlogging# isort:skiplog=logging.getLogger(__name__)# -----------------------------------------------------------------------------# Imports# -----------------------------------------------------------------------------# Standard library importsimportjsonimportosimportrefromos.pathimportrelpathfrompathlibimportPathfromtypingimport(TYPE_CHECKING,Callable,ClassVar,Literal,Protocol,TypedDict,Union,cast,get_args,)# Bokeh importsfrom.import__version__from.core.templatesimportCSS_RESOURCES,JS_RESOURCESfrom.core.typesimportID,PathLikefrom.modelimportModelfrom.settingsimportLogLevel,settingsfrom.util.dataclassesimportdataclass,fieldfrom.util.pathsimportROOT_DIRfrom.util.tokenimportgenerate_session_idfrom.util.versionimportis_full_releaseifTYPE_CHECKING:fromtyping_extensionsimportTypeAlias# -----------------------------------------------------------------------------# Globals and constants# -----------------------------------------------------------------------------DEFAULT_SERVER_HOST=settings.default_server_host()DEFAULT_SERVER_PORT=settings.default_server_port()defserver_url(host:str|None=None,port:int|None=None,ssl:bool=False)->str:protocol="https"ifsslelse"http"returnf"{protocol}://{hostorDEFAULT_SERVER_HOST}:{portorDEFAULT_SERVER_PORT}/"DEFAULT_SERVER_HTTP_URL=server_url()BaseMode:TypeAlias=Literal["inline","cdn","server","relative","absolute"]DevMode:TypeAlias=Literal["server-dev","relative-dev","absolute-dev"]ResourcesMode:TypeAlias=Union[BaseMode,DevMode]Component=Literal["bokeh","bokeh-gl","bokeh-widgets","bokeh-tables","bokeh-mathjax","bokeh-api"]classComponentDefs(TypedDict):js:list[Component]css:list[Component]# __all__ defined at the bottom on the class module# -----------------------------------------------------------------------------# General API# -----------------------------------------------------------------------------# -----------------------------------------------------------------------------# Dev API# -----------------------------------------------------------------------------Hashes:TypeAlias=dict[str,str]_ALL_SRI_HASHES:dict[str,Hashes]={}
[docs]defget_all_sri_versions()->tuple[str,...]:""" Report all versions that have SRI hashes. Returns: tuple """files=(ROOT_DIR/"_sri").glob("*.json")returnset(file.stemforfileinfiles)
[docs]defget_sri_hashes_for_version(version:str)->Hashes:""" Report SRI script hashes for a specific version of BokehJS. Bokeh provides `Subresource Integrity`_ hashes for all JavaScript files that are published to CDN for full releases. This function returns a dictionary that maps JavaScript filenames to their hashes, for a single version of Bokeh. Args: version (str) : The Bokeh version to return SRI hashes for. Hashes are only provided for full releases, e.g "1.4.0", and not for "dev" builds or release candidates. Returns: dict Raises: ValueError: if the specified version does not exist Example: The returned dict for a single version will map filenames for that version to their SRI hashes: .. code-block:: python { 'bokeh-1.4.0.js': 'vn/jmieHiN+ST+GOXzRU9AFfxsBp8gaJ/wvrzTQGpIKMsdIcyn6U1TYtvzjYztkN', 'bokeh-1.4.0.min.js': 'mdMpUZqu5U0cV1pLU9Ap/3jthtPth7yWSJTu1ayRgk95qqjLewIkjntQDQDQA5cZ', 'bokeh-api-1.4.0.js': 'Y3kNQHt7YjwAfKNIzkiQukIOeEGKzUU3mbSrraUl1KVfrlwQ3ZAMI1Xrw5o3Yg5V', 'bokeh-api-1.4.0.min.js': '4oAJrx+zOFjxu9XLFp84gefY8oIEr75nyVh2/SLnyzzg9wR+mXXEi+xyy/HzfBLM', 'bokeh-tables-1.4.0.js': 'I2iTMWMyfU/rzKXWJ2RHNGYfsXnyKQ3YjqQV2RvoJUJCyaGBrp0rZcWiTAwTc9t6', 'bokeh-tables-1.4.0.min.js': 'pj14Cq5ZSxsyqBh+pnL2wlBS3UX25Yz1gVxqWkFMCExcnkN3fl4mbOF8ZUKyh7yl', 'bokeh-widgets-1.4.0.js': 'scpWAebHEUz99AtveN4uJmVTHOKDmKWnzyYKdIhpXjrlvOwhIwEWUrvbIHqA0ke5', 'bokeh-widgets-1.4.0.min.js': 'xR3dSxvH5hoa9txuPVrD63jB1LpXhzFoo0ho62qWRSYZVdyZHGOchrJX57RwZz8l' } .. _Subresource Integrity: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity """ifversionnotin_ALL_SRI_HASHES:try:withopen(ROOT_DIR/"_sri"/f"{version}.json")asf:_ALL_SRI_HASHES[version]=json.load(f)exceptExceptionase:raiseValueError(f"Missing SRI hash for version {version}")fromereturn_ALL_SRI_HASHES[version]
[docs]defverify_sri_hashes()->None:""" Verify the SRI hashes in a full release package. This function compares the computed SRI hashes for the BokehJS files in a full release package to the values in the SRI manifest file. Returns None if all hashes match, otherwise an exception will be raised. .. note:: This function can only be called on full release (e.g "1.2.3") packages. Returns: None Raises: ValueError If called outside a full release package RuntimeError If there are missing, extra, or mismatched files """ifnotis_full_release():raiseValueError("verify_sri_hashes() can only be used with full releases")paths=list((settings.bokehjs_path()/"js").glob("bokeh*.js"))hashes=get_sri_hashes_for_version(__version__)iflen(hashes)<len(paths):raiseRuntimeError("There are unexpected 'bokeh*.js' files in the package")iflen(hashes)>len(paths):raiseRuntimeError("There are 'bokeh*.js' files missing in the package")bad:list[Path]=[]forpathinpaths:name,suffix=str(path.name).split(".",1)filename=f"{name}-{__version__}.{suffix}"sri_hash=_compute_single_hash(path)ifhashes[filename]!=sri_hash:bad.append(path)ifbad:raiseRuntimeError(f"SRI Hash mismatches in the package: {bad!r}")
[docs]classResources:""" 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. """_default_root_dir=Path(os.curdir)_default_root_url=DEFAULT_SERVER_HTTP_URLmode:BaseModemessages:list[RuntimeMessage]_log_level:LogLevelcomponents:list[Component]_component_defs:ClassVar[ComponentDefs]={"js":["bokeh","bokeh-gl","bokeh-widgets","bokeh-tables","bokeh-mathjax","bokeh-api"],"css":[],}_default_components:ClassVar[list[Component]]=["bokeh","bokeh-gl","bokeh-widgets","bokeh-tables","bokeh-mathjax"]def__init__(self,mode:ResourcesMode|None=None,*,version:str|None=None,root_dir:PathLike|None=None,dev:bool|None=None,minified:bool|None=None,log_level:LogLevel|None=None,root_url:str|None=None,path_versioner:PathVersioner|None=None,components:list[Component]|None=None,base_dir:PathLike|None=None,):self.components=componentsifcomponentsisnotNoneelselist(self._default_components)mode=settings.resources(mode)mode_dev=mode.endswith("-dev")self.dev=devifdevisnotNoneelsesettings.devormode_devself.mode=cast(BaseMode,mode[:-4]ifmode_develsemode)ifself.modenotinget_args(BaseMode):raiseValueError("wrong value for 'mode' parameter, expected "f"'inline', 'cdn', 'server(-dev)', 'relative(-dev)' or 'absolute(-dev)', got {mode}",)ifroot_dirandnotself.mode.startswith("relative"):raiseValueError("setting 'root_dir' makes sense only when 'mode' is set to 'relative'")ifversionandnotself.mode.startswith("cdn"):raiseValueError("setting 'version' makes sense only when 'mode' is set to 'cdn'")ifroot_urlandnotself.mode.startswith("server"):raiseValueError("setting 'root_url' makes sense only when 'mode' is set to 'server'")self.root_dir=settings.rootdir(root_dir)delroot_dirself.version=settings.cdn_version(version)delversionself.minified=settings.minified(minifiedifminifiedisnotNoneelsenotself.dev)delminifiedself.log_level=settings.log_level(log_level)dellog_levelself.path_versioner=path_versionerdelpath_versionerifroot_urlandnotroot_url.endswith("/"):# root_url should end with a /, adding oneroot_url=root_url+"/"self._root_url=root_urlself.messages=[]ifself.mode=="cdn":cdn=self._cdn_urls()self.messages.extend(cdn.messages)elifself.mode=="server":server=self._server_urls()self.messages.extend(server.messages)self.base_dir=Path(base_dir)ifbase_dirisnotNoneelsesettings.bokehjs_path()
[docs]defclone(self,*,components:list[Component]|None=None)->Resources:""" Make a clone of a resources instance allowing to override its components. """returnResources(mode=self.mode,version=self.version,root_dir=self.root_dir,dev=self.dev,minified=self.minified,log_level=self.log_level,root_url=self._root_url,path_versioner=self.path_versioner,components=componentsifcomponentsisnotNoneelselist(self.components),base_dir=self.base_dir,)
def__repr__(self)->str:args=[f"mode={self.mode!r}"]ifself.dev:args.append("dev=True")ifself.components!=self._default_components:args.append(f"components={self.components!r}")returnf"Resources({', '.join(args)})"__str__=__repr__@classmethoddefbuild(cls,resources:ResourcesLike|None=None)->Resources:ifisinstance(resources,Resources):returnresourceselse:returnResources(mode=settings.resources(resources))# Properties --------------------------------------------------------------@propertydeflog_level(self)->LogLevel:returnself._log_level@log_level.setterdeflog_level(self,level:LogLevel)->None:valid_levels=get_args(LogLevel)ifnot(levelisNoneorlevelinvalid_levels):raiseValueError(f"Unknown log level '{level}', valid levels are: {valid_levels}")self._log_level=level@propertydefroot_url(self)->str:ifself._root_urlisnotNone:returnself._root_urlelse:returnself._default_root_url# Public methods ----------------------------------------------------------defcomponents_for(self,kind:Kind)->list[Component]:return[compforcompinself.componentsifcompinself._component_defs[kind]]def_file_paths(self,kind:Kind)->list[Path]:minified=".min"ifself.minifiedelse""files=[f"{component}{minified}.{kind}"forcomponentinself.components_for(kind)]paths=[self.base_dir/kind/fileforfileinfiles]returnpathsdef_collect_external_resources(self,resource_attr:ResourceAttr)->list[str]:""" Collect external resources set on resource_attr attribute of all models."""external_resources:list[str]=[]for_,clsinsorted(Model.model_class_reverse_map.items(),key=lambdaarg:arg[0]):external:list[str]|str|None=getattr(cls,resource_attr,None)ifisinstance(external,str):ifexternalnotinexternal_resources:external_resources.append(external)elifisinstance(external,list):foreinexternal:ifenotinexternal_resources:external_resources.append(e)returnexternal_resourcesdef_cdn_urls(self)->Urls:return_get_cdn_urls(self.version,self.minified)def_server_urls(self)->Urls:return_get_server_urls(self.root_url,self.minified,self.path_versioner)def_resolve(self,kind:Kind)->tuple[list[str],list[str],Hashes]:paths=self._file_paths(kind)files,raw=[],[]hashes={}ifself.mode=="inline":raw=[self._inline(path)forpathinpaths]elifself.mode=="relative":root_dir=self.root_dirorself._default_root_dirfiles=[str(relpath(path,root_dir))forpathinpaths]elifself.mode=="absolute":files=list(map(str,paths))elifself.mode=="cdn":cdn=self._cdn_urls()files=list(cdn.urls(self.components_for(kind),kind))ifcdn.hashes:hashes=cdn.hashes(self.components_for(kind),kind)elifself.mode=="server":server=self._server_urls()files=list(server.urls(self.components_for(kind),kind))return(files,raw,hashes)@staticmethoddef_inline(path:Path)->str:filename=path.namebegin=f"/* BEGIN {filename} */"withopen(path,"rb")asf:middle=f.read().decode("utf-8")end=f"/* END {filename} */"returnf"{begin}\n{middle}\n{end}"@propertydefjs_files(self)->list[str]:files,_,_=self._resolve("js")external_resources=self._collect_external_resources("__javascript__")returnexternal_resources+files@propertydefjs_raw(self)->list[str]:_,raw,_=self._resolve("js")ifself.log_levelisnotNone:raw.append(f'Bokeh.set_log_level("{self.log_level}");')ifself.dev:raw.append("Bokeh.settings.dev = true")returnraw@propertydefhashes(self)->Hashes:_,_,hashes=self._resolve("js")returnhashesdefrender_js(self)->str:returnJS_RESOURCES.render(js_raw=self.js_raw,js_files=self.js_files,hashes=self.hashes)@propertydefcss_files(self)->list[str]:files,_,_=self._resolve("css")external_resources=self._collect_external_resources("__css__")returnexternal_resources+files@propertydefcss_raw(self)->list[str]:_,raw,_=self._resolve("css")returnraw@propertydefcss_raw_str(self)->list[str]:return[json.dumps(css)forcssinself.css_raw]defrender_css(self)->str:returnCSS_RESOURCES.render(css_raw=self.css_raw,css_files=self.css_files)defrender(self)->str:css,js=self.render_css(),self.render_js()returnf"{css}\n{js}"
classSessionCoordinates:""" Internal class used to parse kwargs for server URL, app_path, and session_id."""_url:str_session_id:ID|Nonedef__init__(self,*,url:str=DEFAULT_SERVER_HTTP_URL,session_id:ID|None=None)->None:self._url=urlifself._url=="default":self._url=DEFAULT_SERVER_HTTP_URLifself._url.startswith("ws"):raiseValueError("url should be the http or https URL for the server, not the websocket URL")self._url=self._url.rstrip("/")# we lazy-generate the session_id so we can generate it server-side when appropriateself._session_id=session_id# Properties --------------------------------------------------------------@propertydefurl(self)->str:returnself._url@propertydefsession_id(self)->ID:""" Session ID derived from the kwargs provided."""ifself._session_idisNone:self._session_id=generate_session_id()returnself._session_id@propertydefsession_id_allowing_none(self)->ID|None:""" 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. """returnself._session_id# -----------------------------------------------------------------------------# Private API# -----------------------------------------------------------------------------_DEV_PAT=re.compile(r"^(\d)+\.(\d)+\.(\d)+(\.dev|rc)")def_cdn_base_url()->str:return"https://cdn.bokeh.org"def_get_cdn_urls(version:str|None=None,minified:bool=True)->Urls:ifversionisNone:docs_cdn=settings.docs_cdn()version=docs_cdnifdocs_cdnelse__version__.split("+")[0]base_url=_cdn_base_url()container="bokeh/dev"if_DEV_PAT.match(version)else"bokeh/release"defmk_filename(comp:str,kind:Kind)->str:returnf"{comp}-{version}{'.min'ifminifiedelse''}.{kind}"defmk_url(comp:str,kind:Kind)->str:returnf"{base_url}/{container}/"+mk_filename(comp,kind)result=Urls(urls=lambdacomponents,kind:[mk_url(component,kind)forcomponentincomponents])iflen(__version__.split("+"))>1:result.messages.append(RuntimeMessage(type="warn",text=(f"Requesting CDN BokehJS version '{version}' from local development version '{__version__}'. ""This configuration is unsupported and may not work!"),))ifis_full_release(version):# TODO: TypeGuard?assertversionisnotNonesri_hashes=get_sri_hashes_for_version(version)result.hashes=lambdacomponents,kind:{mk_url(component,kind):sri_hashes[mk_filename(component,kind)]forcomponentincomponents}returnresultdef_get_server_urls(root_url:str=DEFAULT_SERVER_HTTP_URL,minified:bool=True,path_versioner:PathVersioner|None=None,)->Urls:_minified=".min"ifminifiedelse""defmk_url(comp:str,kind:Kind)->str:path=f"{kind}/{comp}{_minified}.{kind}"ifpath_versionerisnotNone:path=path_versioner(path)returnf"{root_url}static/{path}"returnUrls(urls=lambdacomponents,kind:[mk_url(component,kind)forcomponentincomponents])def_compute_single_hash(path:Path)->str:assertpath.suffix==".js"fromsubprocessimportPIPE,Popendigest=f"openssl dgst -sha384 -binary {path}".split()p1=Popen(digest,stdout=PIPE)b64="openssl base64 -A".split()p2=Popen(b64,stdin=p1.stdout,stdout=PIPE)out,_=p2.communicate()returnout.decode("utf-8").strip()# -----------------------------------------------------------------------------# Code# -----------------------------------------------------------------------------ResourcesLike:TypeAlias=Union[Resources,ResourcesMode]CDN=Resources(mode="cdn")INLINE=Resources(mode="inline")__all__=("CDN","INLINE","Resources","get_all_sri_versions","get_sri_hashes_for_version","verify_sri_hashes",)