diff --git a/.buildinfo b/.buildinfo new file mode 100644 index 0000000..7d08cc5 --- /dev/null +++ b/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 30bb7cd181cdb7683f68dc9df09b2c07 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/.doctrees/changelog.doctree b/.doctrees/changelog.doctree new file mode 100644 index 0000000..b3e3978 Binary files /dev/null and b/.doctrees/changelog.doctree differ diff --git a/.doctrees/contact.doctree b/.doctrees/contact.doctree new file mode 100644 index 0000000..324b66b Binary files /dev/null and b/.doctrees/contact.doctree differ diff --git a/.doctrees/dev.doctree b/.doctrees/dev.doctree new file mode 100644 index 0000000..1ed5ecf Binary files /dev/null and b/.doctrees/dev.doctree differ diff --git a/.doctrees/environment.pickle b/.doctrees/environment.pickle new file mode 100644 index 0000000..5c5c3ec Binary files /dev/null and b/.doctrees/environment.pickle differ diff --git a/.doctrees/examples/asset_scripts.doctree b/.doctrees/examples/asset_scripts.doctree new file mode 100644 index 0000000..c0f6092 Binary files /dev/null and b/.doctrees/examples/asset_scripts.doctree differ diff --git a/.doctrees/examples/demo_reports.doctree b/.doctrees/examples/demo_reports.doctree new file mode 100644 index 0000000..f093cb2 Binary files /dev/null and b/.doctrees/examples/demo_reports.doctree differ diff --git a/.doctrees/examples/index.doctree b/.doctrees/examples/index.doctree new file mode 100644 index 0000000..051833d Binary files /dev/null and b/.doctrees/examples/index.doctree differ diff --git a/.doctrees/index.doctree b/.doctrees/index.doctree new file mode 100644 index 0000000..72b43d6 Binary files /dev/null and b/.doctrees/index.doctree differ diff --git a/.doctrees/installation.doctree b/.doctrees/installation.doctree new file mode 100644 index 0000000..a635035 Binary files /dev/null and b/.doctrees/installation.doctree differ diff --git a/.doctrees/intro.doctree b/.doctrees/intro.doctree new file mode 100644 index 0000000..d349bdc Binary files /dev/null and b/.doctrees/intro.doctree differ diff --git a/.doctrees/plugins/index.doctree b/.doctrees/plugins/index.doctree new file mode 100644 index 0000000..00b0446 Binary files /dev/null and b/.doctrees/plugins/index.doctree differ diff --git a/.doctrees/plugins/plugin.doctree b/.doctrees/plugins/plugin.doctree new file mode 100644 index 0000000..ff9d716 Binary files /dev/null and b/.doctrees/plugins/plugin.doctree differ diff --git a/.doctrees/reference/api.doctree b/.doctrees/reference/api.doctree new file mode 100644 index 0000000..639669f Binary files /dev/null and b/.doctrees/reference/api.doctree differ diff --git a/.doctrees/reference/assets.doctree b/.doctrees/reference/assets.doctree new file mode 100644 index 0000000..c91526d Binary files /dev/null and b/.doctrees/reference/assets.doctree differ diff --git a/.doctrees/reference/building.doctree b/.doctrees/reference/building.doctree new file mode 100644 index 0000000..1148825 Binary files /dev/null and b/.doctrees/reference/building.doctree differ diff --git a/.doctrees/reference/cli.doctree b/.doctrees/reference/cli.doctree new file mode 100644 index 0000000..3b6aae3 Binary files /dev/null and b/.doctrees/reference/cli.doctree differ diff --git a/.doctrees/reference/components.doctree b/.doctrees/reference/components.doctree new file mode 100644 index 0000000..a68d9f8 Binary files /dev/null and b/.doctrees/reference/components.doctree differ diff --git a/.doctrees/reference/directive.doctree b/.doctrees/reference/directive.doctree new file mode 100644 index 0000000..f8501f2 Binary files /dev/null and b/.doctrees/reference/directive.doctree differ diff --git a/.doctrees/reference/index.doctree b/.doctrees/reference/index.doctree new file mode 100644 index 0000000..792d131 Binary files /dev/null and b/.doctrees/reference/index.doctree differ diff --git a/.doctrees/reference/project_structure.doctree b/.doctrees/reference/project_structure.doctree new file mode 100644 index 0000000..98aa883 Binary files /dev/null and b/.doctrees/reference/project_structure.doctree differ diff --git a/.doctrees/reference/settings.doctree b/.doctrees/reference/settings.doctree new file mode 100644 index 0000000..d5010d1 Binary files /dev/null and b/.doctrees/reference/settings.doctree differ diff --git a/.doctrees/reference/templating.doctree b/.doctrees/reference/templating.doctree new file mode 100644 index 0000000..d2db249 Binary files /dev/null and b/.doctrees/reference/templating.doctree differ diff --git a/.doctrees/template_designer_guide.doctree b/.doctrees/template_designer_guide.doctree new file mode 100644 index 0000000..1e1cff3 Binary files /dev/null and b/.doctrees/template_designer_guide.doctree differ diff --git a/.doctrees/user_guide.doctree b/.doctrees/user_guide.doctree new file mode 100644 index 0000000..4e7c5c9 Binary files /dev/null and b/.doctrees/user_guide.doctree differ diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/_images/build_flow.svg b/_images/build_flow.svg new file mode 100644 index 0000000..f3fd92e --- /dev/null +++ b/_images/build_flow.svg @@ -0,0 +1,16 @@ + diff --git a/_images/chirping_crickets.gif b/_images/chirping_crickets.gif new file mode 100644 index 0000000..39fa040 Binary files /dev/null and b/_images/chirping_crickets.gif differ diff --git a/_images/generation_time_templating.svg b/_images/generation_time_templating.svg new file mode 100644 index 0000000..250435a --- /dev/null +++ b/_images/generation_time_templating.svg @@ -0,0 +1,16 @@ + diff --git a/_images/overview.png b/_images/overview.png new file mode 100644 index 0000000..cbd1fa4 Binary files /dev/null and b/_images/overview.png differ diff --git a/_images/pptx.png b/_images/pptx.png new file mode 100644 index 0000000..f0dbb2d Binary files /dev/null and b/_images/pptx.png differ diff --git a/_modules/index.html b/_modules/index.html new file mode 100644 index 0000000..2745a0c --- /dev/null +++ b/_modules/index.html @@ -0,0 +1,377 @@ + + +
+ + +
+# flake8: noqa: F401,F403
+"""
+This module contains API functions related to asset generation.
+When asset scripts are accessing those API functions, the functions can access the project information by themselves.
+"""
+# Don't remove unused imports. They may be unused here but maybe in user code!
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from pharaoh.assetlib.context import metadata_context
+from pharaoh.assetlib.generation import register_asset, register_templating_context
+from pharaoh.assetlib.matlab_engine import Matlab
+from pharaoh.assetlib.resource import *
+
+if TYPE_CHECKING:
+ from pharaoh.assetlib.finder import AssetFinder
+
+
+def __get_pharaoh_project():
+ import inspect
+ from pathlib import Path
+
+ import pharaoh
+ from pharaoh.project import get_project
+
+ from .util import is_relative_to
+
+ try:
+ return get_project()
+ except RuntimeError:
+ pass
+
+ pharaoh_src = Path(pharaoh.__file__).parents[1] # also works for site-packages
+ # todo: maybe we have to consider additional paths
+
+ # Find the first frame whose filename is not inside the pharaoh library
+ for frame in inspect.stack():
+ fname = Path(frame.filename)
+ if is_relative_to(fname, pharaoh_src) or "/pharaoh_contrib/" in fname.as_posix():
+ continue
+ return get_project(Path(frame.filename))
+ return None
+
+
+
+[docs]
+def get_resource(alias: str, component: str | None = None):
+ """
+ Get a Resource instance by its component name and resource alias.
+ """
+ proj = __get_pharaoh_project()
+ if not component:
+ component = get_current_component()
+ return proj.get_resource(alias, component)
+
+
+
+
+[docs]
+def find_components(expression: str = ""):
+ """
+ Find components by their metadata using an evaluated expression.
+
+ The expression must be a valid Python expression and following local variables may be used:
+
+ - name: The name of the component
+ - templates: A list of templates used to render the component
+ - metadata: A dict of metadata specified
+ - render_context: The Jinja rendering context specified
+ - resources: A list of resource definitions
+
+ :param expression: A Python expression that will be evaluated. If it evaluates to a truthy result,
+ the component name is included in the returned list.
+
+ Example: ``name == "dummy" and metadata.foo in (1,2,3)``.
+
+ A failing evaluation will be treated as False. An empty expression will always match.
+ """
+ proj = __get_pharaoh_project()
+ return proj.find_components(expression)
+
+
+
+
+[docs]
+def get_current_component() -> str:
+ """
+ If executed from within a script that is placed inside a Pharaoh component, the function returns the components
+ name by analyzing the call stack.
+ """
+ from pharaoh.assetlib import util
+
+ proj = __get_pharaoh_project()
+ return util.get_component_name_by_callstack(proj.sphinx_report_project_components)
+
+
+
+
+[docs]
+def get_asset_finder() -> AssetFinder:
+ """
+ Returns the :class:`AssetFinder <pharaoh.assetlib.finder.AssetFinder>` instance for the current project
+ """
+ proj = __get_pharaoh_project()
+ return proj.asset_finder
+
+
+from __future__ import annotations
+
+import hashlib
+import json
+import shutil
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+import omegaconf
+
+from pharaoh.log import log
+
+from .util import obj_groupby
+
+if TYPE_CHECKING:
+ from collections.abc import Iterable, Iterator
+
+
+class AssetFileLinkBrokenError(LookupError):
+ pass
+
+
+
+[docs]
+class Asset:
+ """
+ Holds information about a generated asset.
+
+ :ivar id: An MD5 hash of the asset's filename, prefixed with "__ID__".
+ Since a unique suffix is included in the filename, this ID hash is also unique.
+ Can be used to quickly find this Asset instance.
+ :ivar Path infofile: Absolute path to the ``*.assetinfo`` file
+ :ivar Path assetfile: Absolute path to the actual asset file
+ :ivar omegaconf.DictConfig context: The content of *infofile* parsed into a OmegaConf dict.
+ """
+
+ def __init__(self, info_file: Path):
+ assert info_file.suffix == ".assetinfo"
+ self.id: str = "__ID__" + hashlib.md5(bytes(info_file.name, "utf-8")).hexdigest()
+ self.infofile: Path = info_file
+ self.assetfile: Path | None = None
+ self.context = omegaconf.OmegaConf.create(json.loads(self.infofile.read_text()))
+ for file in self.infofile.parent.glob(f"{self.infofile.stem}*"):
+ if file.suffix != ".assetinfo":
+ self.assetfile = file
+ break
+ if self.assetfile is None:
+ msg = f"There is no asset for inventory file {self.infofile}!"
+ raise AssetFileLinkBrokenError(msg)
+
+ def __str__(self):
+ return repr(self)
+
+ def __repr__(self):
+ return f"Asset[{self.infofile.stem}]"
+
+ def __eq__(self, other: Asset):
+ return self.id == other.id and self.infofile == other.infofile and self.assetfile == other.assetfile
+
+ def __hash__(self):
+ return hash(self.infofile.name)
+
+ def __lt__(self, other: Asset):
+ return self.infofile < other.infofile
+
+ def copy_to(self, target_dir: Path) -> Path:
+ """
+ Copy the asset plus info-file.
+
+ :param target_dir: The target directory to copy to. Will be created if it does not exist.
+ :returns: True if files were copied, False otherwise (files already exist)
+ """
+ target_dir.mkdir(exist_ok=True, parents=True)
+ target_info_file = target_dir / self.infofile.name
+ if target_info_file.exists():
+ return target_dir / self.assetfile.name
+
+ log.debug(f"Copying asset {self} to build directory")
+ shutil.copy(self.infofile, target_info_file)
+
+ if Path(self.assetfile).is_file():
+ return Path(shutil.copy(self.assetfile, target_dir))
+
+ if Path(self.assetfile).is_dir():
+ shutil.copytree(self.assetfile, target_dir / self.assetfile.name)
+ return target_dir / self.assetfile.name
+
+ raise NotImplementedError
+
+ def read_json(self):
+ if self.assetfile.suffix.lower() != ".json":
+ msg = "Can only read .json files!"
+ raise Exception(msg)
+ return json.loads(self.assetfile.read_text("utf-8"))
+
+ def read_yaml(self):
+ import yaml
+
+ if self.assetfile.suffix.lower() != ".yaml":
+ msg = "Can only read .yaml files!"
+ raise Exception(msg)
+ with open(self.assetfile, encoding="utf-8") as fp:
+ return yaml.load(fp, yaml.Loader)
+
+ def read_text(self, encoding: str = "utf-8"):
+ return self.assetfile.read_text(encoding)
+
+ def read_bytes(self):
+ return self.assetfile.read_bytes()
+
+
+
+
+[docs]
+class AssetFinder:
+
+[docs]
+ def __init__(self, lookup_path: Path):
+ """
+ A class for discovering and searching generated assets.
+
+ An instance of this class will be created by the Pharaoh project, where ``lookup_path`` will be set to
+ ``report_project/.asset_build``.
+
+ :param lookup_path: The root directory to look for assets. It will be searched recursively for assets.
+ """
+ self._lookup_path = lookup_path
+ self._assets: dict[str, list[Asset]] = {}
+ self.discover_assets()
+
+
+
+[docs]
+ def discover_assets(self, components: list[str] | None = None) -> dict[str, list[Asset]]:
+ """
+ Discovers all assets by recursively searching for ``*.assetinfo`` files and stores
+ the collection as instance variable (`_assets`).
+
+ :param components: A list of components to search for assets.
+ If None (the default), all components will be searched.
+ :return: A dictionary that maps component names to a list of :class:`Asset` instances.
+ """
+ if isinstance(components, list) and len(components):
+ for component in components:
+ self._assets[component] = [Asset(file) for file in (self._lookup_path / component).glob("*.assetinfo")]
+ else:
+ self._assets.clear()
+ for asset in (Asset(file) for file in self._lookup_path.glob("*/*.assetinfo")):
+ component = asset.assetfile.parent.name
+ if component not in self._assets:
+ self._assets[component] = []
+ self._assets[component].append(asset)
+
+ return self._assets
+
+
+
+[docs]
+ def search_assets(self, condition: str, components: str | Iterable[str] | None = None) -> list[Asset]:
+ """
+ Searches already discovered assets (see :func:`discover_assets`) that match a condition.
+
+ :param condition: A Python expression that is evaluated using the content of the ``*.assetinfo`` JSON file
+ as namespace. If the evaluation returns a truthy result, the asset is returned.
+
+ Refer to :ref:`this example assetinfo file <example_asset_info>` to see the available default namespace.
+
+ Example::
+
+ # All HTML file where the "label" metadata ends with "_plot"
+ finder.search_assets('asset.suffix == ".html" and label.endswith("_plot")')
+
+ :param components: A list of component names to search. If None (the default), all components will be searched.
+ :return: A list of assets whose metadata match the condition.
+ """
+ if not condition.strip():
+ return []
+
+ code = compile(condition, "<string>", "eval")
+ found = []
+
+ for asset in self.iter_assets(components):
+ try:
+ result = eval(code, {}, asset.context.copy())
+ except Exception:
+ result = False
+ if result:
+ found.append(asset)
+
+ def sort_key(asset):
+ try:
+ return asset.context.asset.index
+ except AttributeError:
+ return 0
+
+ # Sort by asset index, which reflects the order in which the assets were generated in the asset script
+ return sorted(found, key=sort_key)
+
+
+
+[docs]
+ def iter_assets(self, components: str | Iterable[str] | None = None) -> Iterator[Asset]:
+ """
+ Iterates over all discovered assets.
+
+ :param components: A list of component names to search. If None (the default), all components will be searched.
+ :return: An iterator over all discovered assets.
+ """
+ if not self._assets:
+ self.discover_assets()
+
+ if isinstance(components, str):
+ components = [components]
+ components = components or list(self._assets.keys())
+ for component in components:
+ if component in self._assets:
+ yield from self._assets[component]
+
+
+
+[docs]
+ def get_asset_by_id(self, id: str) -> Asset | None:
+ """
+ Returns the corresponding :class:`Asset` instance for a certain ID.
+
+ :param id: The ID of the asset to return
+ :return: An :class:`Asset` instance if found, None otherwise.
+ """
+ for asset in self.iter_assets():
+ if asset.id == id:
+ return asset
+ return None
+
+
+
+
+
+[docs]
+def asset_groupby(
+ seq: Iterable[Asset], key: str, sort_reverse: bool = False, default: str | None = None
+) -> dict[str, list[Asset]]:
+ """
+ Groups an iterable of Assets by a certain metadata key.
+
+ During build-time rendering this function will be available as Jinja global function
+ ``asset_groupby`` and alias ``agroupby``.
+
+ Example:
+
+ .. code-block:: none
+
+ We have following 4 assets (simplified notation of specified metadata):
+ Asset[a="1", b="3"]
+ Asset[a="1", c="4"]
+ Asset[a="2", b="3"]
+ Asset[a="2", c="4"]
+
+ Grouping by "a":
+ asset_groupby(assets, "a")
+ will yield
+ {
+ "1": [Asset[a="1", b="3"], Asset[a="1", c="4"]],
+ "2": [Asset[a="2", b="3"], Asset[a="2", c="4"]],
+ }
+
+
+ Grouping by "b" and default "default":
+ asset_groupby(assets, "b", default="default")
+ will yield
+ {
+ "3": [Asset[a="1", b="3"], Asset[a="2", b="3"]],
+ "default": [Asset[a="1", c="4"], Asset[a="2", c="4"]],
+ }
+
+
+ :param seq: The iterable of assets to group
+ :param key: The nested attribute to use for grouping, e.g. "A.B.C"
+ :param sort_reverse: Reverse-sort the keys in the returned dictionary
+ :param default: Sort each item, where "key" is not an existing attribute, into this default group
+ :return: A dictionary that maps the group names (values of A.B.C) to a list of items out of the input iterable
+ """
+ return obj_groupby(seq=seq, key=key, sort_reverse=sort_reverse, attr="context", default=default)
+
+
+from __future__ import annotations
+
+import concurrent.futures
+import contextlib
+import io
+import json
+import logging.handlers
+import multiprocessing
+import os
+import re
+import shutil
+import traceback
+from functools import partial
+from pathlib import Path
+from types import ModuleType
+from typing import TYPE_CHECKING, Union
+
+import pharaoh.log
+from pharaoh import project
+from pharaoh.assetlib import patches
+from pharaoh.assetlib.context import context_stack
+from pharaoh.assetlib.finder import Asset
+from pharaoh.templating.second_level.sphinx_ext.asset_ext_templates import find_template
+from pharaoh.util.contextlib_chdir import chdir
+from pharaoh.util.json_encoder import CustomJSONEncoder
+
+if TYPE_CHECKING:
+ from collections.abc import Iterable
+ from queue import Queue
+
+log = pharaoh.log.log
+
+PathLike = Union[str, Path]
+
+
+def generate_assets(project_root: Path, asset_src: Path, component_name: str = "", mp_log_queue: Queue | None = None):
+ # Since this function is always called in a process by generate_assets_parallel,
+ # we need to remove at least all file handlers so child-processes don't log to the same file
+ # as the parent process, otherwise race conditions may occur.
+ # So we just remove all handlers and add a QueueHandler
+ # to send all log records to the parent in order to handle them.
+ if mp_log_queue is not None: # pragma: no cover
+ for hdl in log.handlers:
+ log.removeHandler(hdl)
+ log.addHandler(logging.handlers.QueueHandler(mp_log_queue))
+
+ # Also forbid the project instance to add loggers we just removed
+ proj = project.PharaohProject(project_root=project_root, logging_add_filehandler=False)
+ context_stack.reset()
+
+ try:
+ script_path = asset_src.relative_to(project_root).as_posix()
+ except ValueError:
+ script_path = asset_src.as_posix()
+
+ if asset_src.suffix.lower() == ".py":
+ script_ignore_pattern = proj.get_setting("asset_gen.script_ignore_pattern")
+
+ with patches.patch_3rd_party_libraries(), context_stack.new_context(
+ context_name="generate_assets",
+ asset={
+ "script_name": asset_src.name,
+ "script_path": asset_src,
+ "index": 0,
+ "component_name": component_name,
+ },
+ ):
+ code = asset_src.read_text(encoding="utf-8")
+
+ first_line = code.split("\n", maxsplit=1)[0].strip()
+ if re.fullmatch(script_ignore_pattern, asset_src.name) or re.fullmatch(
+ r"^# *pharaoh?: *ignore *", first_line, re.IGNORECASE
+ ):
+ log.info(f"Ignoring file {script_path}")
+ return
+
+ log.info(f"Generating assets from script {script_path!r}...")
+ asset_module = module_from_file(asset_src)
+
+ WAVEWATSON_LEGACY_INPLACE = os.environ.get("WAVEWATSON_LEGACY_INPLACE")
+ os.environ["WAVEWATSON_LEGACY_INPLACE"] = "0"
+ try:
+ run_module(asset_module, code)
+ except Exception as e:
+ msg = (
+ f"An exception was raised when executing module "
+ f"{str(asset_src)!r}:\n\n{e}\n\nTraceback:\n{traceback.format_exc()}"
+ )
+ raise Exception(msg) from None
+ finally:
+ if WAVEWATSON_LEGACY_INPLACE is None:
+ del os.environ["WAVEWATSON_LEGACY_INPLACE"]
+ else:
+ os.environ["WAVEWATSON_LEGACY_INPLACE"] = WAVEWATSON_LEGACY_INPLACE
+
+ elif asset_src.suffix.lower() == ".ipynb":
+ # import locally, otherwise Sphinx autodoc on pharaoh.assetlib.api fails because of nbformat package and this
+ # issue: https://github.com/sphinx-doc/sphinx/issues/11662
+ import nbformat
+ from nbconvert.preprocessors import CellExecutionError, ExecutePreprocessor
+
+ log.info(f"Generating assets from notebook {script_path!r}...")
+
+ with open(asset_src) as file:
+ nb = nbformat.read(file, as_version=4)
+ initial_node = nbformat.notebooknode.from_dict(
+ {
+ "cell_type": "code",
+ "execution_count": None,
+ "id": "000000",
+ "outputs": [],
+ "metadata": {},
+ "source": f"""
+import os
+from pharaoh.api import PharaohProject
+from pharaoh.assetlib.api import metadata_context
+from pharaoh.assetlib.patches import patch_3rd_party_libraries
+
+proj = PharaohProject(project_root="{project_root.as_posix()}")
+patcher = patch_3rd_party_libraries()
+patcher.__enter__()
+metadata_context(
+ context_name="generate_assets",
+ asset=dict(
+ script_name="{asset_src.name}",
+ script_path="{asset_src.as_posix()}",
+ component_name="{component_name}",
+ index=0
+ )
+).activate()
+
+os.environ["WAVEWATSON_LEGACY_INPLACE"] = "0"
+"""[1:-1],
+ }
+ )
+
+ nb.cells.insert(0, initial_node)
+
+ ep = ExecutePreprocessor(timeout=600)
+ subdir = component_name or "default"
+ try:
+ ep.preprocess(nb, {"metadata": {"path": str(asset_src.parent)}})
+
+ completed_notebooks_path = proj.asset_build_dir / "completed_notebooks" / subdir / asset_src.name
+ completed_notebooks_path.parent.mkdir(parents=True, exist_ok=True)
+ with open(completed_notebooks_path, "w") as file:
+ nbformat.write(nb, file)
+ except CellExecutionError as e:
+ failed_notebooks_path = proj.asset_build_dir / "failed_notebooks" / subdir / asset_src.name
+ failed_notebooks_path.parent.mkdir(parents=True, exist_ok=True)
+ with open(failed_notebooks_path, "w") as file:
+ nbformat.write(nb, file)
+ msg = (
+ f"An exception was raised when executing notebook '{asset_src.stem}': {e}\n"
+ f"Check the notebook for errors/traces: {failed_notebooks_path}"
+ )
+ raise Exception(msg) from e
+
+
+def generate_assets_parallel(
+ project_root: PathLike, asset_sources: Iterable[tuple[str, Path]], workers: str | int = "auto"
+):
+ project_root = Path(project_root)
+ if isinstance(workers, int):
+ workers = max(1, workers)
+ if isinstance(workers, str):
+ if workers.lower() == "auto":
+ workers = multiprocessing.cpu_count()
+ else:
+ msg = "Argument worker may only be an integer number or the string 'auto'!"
+ raise ValueError(msg)
+
+ log.info(f"Executing asset generation with {workers} worker processes")
+ generate_asset_partial = partial(generate_assets, project_root=project_root)
+
+ mp_manager = multiprocessing.Manager()
+ mp_log_queue = mp_manager.Queue(-1)
+
+ results = []
+ # The queue listener collects all log records handled via the queue handler (defined inside generate_assets)
+ # in order to log them in the parent process
+ ql = logging.handlers.QueueListener(mp_log_queue, *log.handlers, respect_handler_level=True)
+ ql.start()
+ with concurrent.futures.ProcessPoolExecutor(max_workers=workers) as executor:
+ futures_map = {
+ executor.submit(
+ generate_asset_partial, asset_src=asset_source, component_name=component_name, mp_log_queue=mp_log_queue
+ ): asset_source
+ for component_name, asset_source in asset_sources
+ }
+ for future in concurrent.futures.as_completed(futures_map.keys()):
+ result = None
+ try:
+ result = future.result()
+ results.append((futures_map[future], result))
+ except SystemExit as e:
+ if e.code == 0:
+ results.append((futures_map[future], result))
+ else:
+ results.append((futures_map[future], e))
+ except Exception as e:
+ results.append((futures_map[future], e))
+ ql.stop()
+ return results
+
+
+def module_from_file(path: str | Path) -> ModuleType:
+ """
+ Creates a module from file at runtime.
+
+ :param path: Path to the module source code.
+ :return: A fresh module
+ """
+
+ module_path = Path(path)
+ module_name = f"asset_module_{module_path.stem.replace(' ', '_').replace('-', '_')}"
+ module = ModuleType(module_name)
+ module.__dict__["__file__"] = str(module_path.absolute())
+ module.__path__ = [module_path.parent]
+ module.__dict__["__name__"] = "__main__"
+ module.__dict__["__module_name__"] = module_name
+
+ return module
+
+
+def run_module(module: ModuleType, code: str):
+ """
+ Execute the configured source code in a module.
+
+ :param module: A module object.
+ :param code: The code to be run inside the module
+ """
+ compiled_code = compile(code, module.__dict__["__file__"], "exec")
+ with chdir(Path(module.__dict__["__file__"]).parent):
+ exec(compiled_code, module.__dict__)
+
+
+
+[docs]
+def register_asset(
+ file: PathLike,
+ metadata: dict | None = None,
+ template: str | None = None,
+ data: io.BytesIO | None = None,
+ copy2build: bool = False,
+ **kwargs,
+) -> Asset:
+ """
+ Register an asset manually. The file will be copied (if data is None and 'file' is a real file) or
+ written (if data is given) to the asset build folder of the current Pharaoh project.
+
+ :param file: The filename. Must exist even if data is set, to have a filename to store the asset and to
+ automatically determine the template (if not set via template argument).
+ :param metadata: Additional metadata to store on the asset.
+ :param template: The template used to render the asset. If omitted, it is inferred by the file extension.
+
+ .. seealso:: :ref:`reference/directive:Asset Templates`
+
+ :param data: An io.BytesIO instance. Used if the asset is generated in memory and should be stored to disk by
+ this function.
+ :param copy2build: If True, the asset will be copied to the asset build directory,
+ even if not referenced in the template.
+
+ Background: Pharaoh stores all assets in the project directory and copies them to the build directory only if
+ copy2build is set to True or on-demand by Pharaoh. For example if an HTML file is rendered using an iframe,
+ the HTML file has to be copied to the build folder where the iframe can later include it.
+
+ :returns: The file path where the asset will be actually stored
+ """
+ component_name = kwargs.pop("component", None)
+ if kwargs:
+ raise Exception("Unknown keyword arguments " + ",".join(kwargs.keys()))
+
+ file = Path(file)
+ if not template:
+ suffix = file.suffix.lower()
+ template = {
+ ".html": "iframe", # todo: use raw_html if this is not a full HTML file, iframe otherwise
+ ".rst": "raw_rst",
+ ".txt": "raw_txt",
+ ".svg": "image",
+ ".png": "image",
+ ".jpg": "image",
+ ".jpeg": "image",
+ ".gif": "image",
+ ".md": "markdown",
+ }.get(suffix)
+
+ if template is not None:
+ find_template(template) # will fail if template does not exist
+
+ if template in ("iframe",):
+ copy2build = True
+
+ file = Path(file)
+ try:
+ active_app = project.get_project()
+ except Exception:
+ # If there is no Pharaoh application yet, the calling file is presumably executed standalone,
+ # so we have to skip exporting any files
+ return None
+
+ # If this function is used in an asset script that is executed directly, the component name is not added to the
+ # metadata context, so we have to find the component via the callstack and pass it to _build_asset_filepath
+ from pharaoh.assetlib.api import get_current_component
+
+ if component_name is None:
+ try:
+ component_name = get_current_component()
+ except LookupError: # raised if method is not executed from inside a component
+ msg = (
+ "When register_asset is called outside a component of a Pharaoh project, keyword argument "
+ "'component' must be set!"
+ )
+ raise Exception(msg) from None
+ asset_file_path = active_app._build_asset_filepath(file, component_name)
+
+ metadata = metadata or {}
+ metadata.pop("context_name", None)
+ metadata.pop("asset", None)
+ with context_stack.new_context(
+ context_name="manual_registry",
+ asset={
+ "user_filepath": str(file),
+ "file": str(asset_file_path),
+ "name": asset_file_path.name,
+ "stem": asset_file_path.stem,
+ "suffix": asset_file_path.suffix,
+ "template": template,
+ "copy2build": copy2build,
+ },
+ **metadata,
+ ):
+ with contextlib.suppress(LookupError):
+ # If asset scripts are executed directly, this context does not exist
+ context_stack.get_parent_context(name="generate_assets")["asset"]["index"] += 1
+ if isinstance(data, io.BytesIO):
+ with open(asset_file_path, "wb") as fp:
+ fp.write(bytes(data.getbuffer()))
+ else:
+ if file.is_file():
+ shutil.copy(file.absolute(), asset_file_path)
+ elif file.is_dir():
+ shutil.copytree(file, asset_file_path)
+ else:
+ msg = f"{file} does not exist!"
+ raise FileNotFoundError(msg)
+ info_file = context_stack.dump(asset_file_path)
+ return Asset(info_file)
+
+
+
+
+[docs]
+def register_templating_context(name: str, context: str | Path | dict | list, metadata: dict | None = None, **kwargs):
+ """
+ Register a data context for the build-time templating stage.
+ The data may be given directly as dict/list or via a json or yaml file.
+
+ This function is designed to be used within asset scripts, to easily register data you extract from resources
+ for the templating process.
+
+ Example::
+
+ from pharaoh.assetlib.api import register_templating_context
+
+ register_templating_context(name="foo", context={"bar": "baz"})
+ # will be accessed like this: {{ ctx.local.foo.bar.baz }}
+
+ :param name: The name under which the data context is available inside Jinja templates.
+ Access like this (name: mycontext)::
+
+ {% set mycontext = ctx.local.mycontext %}
+
+ :param context: Either a str or :external:class:`Path <pathlib.Path>` instance pointing to a json or yaml
+ file, or a dict or list. All data must contain only json-compatible types, otherwise the data cannot be stored.
+ :param metadata: The given context will be internally registered as an asset with following metadata:
+ ``dict(pharaoh_templating_context=name, **metadata)``
+ :param kwargs: Keyword arguments that are mostly (except ``component``) passed to ``json.dumps(...)``, in case
+ ``context`` is a dict or list.
+ """
+ component = kwargs.pop("component", None)
+ metadata = metadata or {}
+ metadata.pop("pharaoh_templating_context", None)
+ kwargs.setdefault("cls", CustomJSONEncoder)
+
+ if not name:
+ msg = "name must be a non-empty string!"
+ raise ValueError(msg)
+
+ if isinstance(context, (str, Path)):
+ file = Path(context)
+ if file.suffix.lower() not in (".json", ".yaml"):
+ msg = "If context is a file path, it's suffix must be either .json or .yaml!"
+ raise ValueError(msg)
+ register_asset(file, metadata=dict(pharaoh_templating_context=name, **metadata), component=component)
+ elif isinstance(context, (list, dict)):
+ data = io.BytesIO(json.dumps(context, **kwargs).encode("utf-8"))
+ register_asset(
+ "pharaoh_templating_context.json",
+ metadata=dict(pharaoh_templating_context=name, **metadata),
+ data=data,
+ component=component,
+ )
+ else:
+ msg = f"Unsupported type {type(context)}!"
+ raise TypeError(msg)
+
+
+from __future__ import annotations
+
+import datetime
+import io
+import os
+import typing as t
+from pathlib import Path
+
+from pharaoh.log import log
+
+if t.TYPE_CHECKING:
+ import matlab.engine as me
+
+
+def ensure_engine():
+ try:
+ import matlab.engine # noqa: F401
+ except Exception:
+ log.error(
+ "The Matlab Engine for Python is not installed.\n"
+ "Please install the corresponding version via PIP:\n"
+ " R2020B: pip install matlabengine==9.9.*\n"
+ " R2021A: pip install matlabengine==9.10.*\n"
+ " R2021B: pip install matlabengine==9.11.*\n"
+ " R2022A: pip install matlabengine==9.12.*\n"
+ " R2022B: pip install matlabengine==9.13.*\n"
+ " R2023A: pip install matlabengine==9.14.*\n"
+ " R2023B: pip install matlabengine==9.15.*\n"
+ )
+ raise
+
+
+PathLike = t.Union[str, Path]
+
+
+
+[docs]
+class Matlab:
+ """
+ Matlab engine for Pharaoh asset generation.
+
+ Usage::
+
+ from pharaoh.assetlib.api import Matlab
+
+ eng = Matlab()
+ with eng:
+ out, err = eng.execute_script("myscript.m")
+ result, out, err = eng.execute_function("myfunc", [800.0], nargout=1)
+
+ """
+
+
+[docs]
+ def __init__(self, start_options="-nodesktop"):
+ """
+ Matlab engine for Pharaoh asset generation.
+
+ :param start_options: See options at https://de.mathworks.com/help/matlab/ref/matlabwindows.html
+ """
+ self._engine: me.MatlabEngine | None = None
+ self._start_options = start_options
+
+
+ @property
+ def eng(self) -> me.MatlabEngine:
+ """
+ Returns the matlab engine instance
+ """
+ return self._engine
+
+
+[docs]
+ def __enter__(self):
+ """
+ Context manager enter. Connects to Matlab.
+ """
+ self.connect()
+ return self
+
+
+
+[docs]
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ """
+ Context manager exit. Disconnects from Matlab.
+ """
+ if self.eng is not None:
+ self.eng.exit()
+ self._engine = None
+
+
+
+[docs]
+ def connect(self):
+ """
+ Connects to a Matlab engine. If an engine is running it will be connected, otherwise a new Matlab instance
+ will be started and connected.
+ """
+ ensure_engine()
+ import matlab.engine as me
+
+ active_engines = me.find_matlab()
+ if len(active_engines):
+ self._engine = me.connect_matlab(name=active_engines[0], background=False)
+ else:
+ self._engine = me.start_matlab(option=self._start_options, background=False)
+
+ # Write connection info to Matlab workspace
+ info = datetime.datetime.now(tz=datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S") + f", PID {os.getpid()}"
+ self.eng.workspace["PharaohConnected"] = info
+
+
+
+[docs]
+ def show_gui(self):
+ """
+ Makes the Matlab GUI visible.
+ """
+ self.eng.desktop(nargout=0)
+
+
+
+[docs]
+ def execute_script(self, script_path: PathLike) -> tuple[str, str]:
+ """
+ Executes a Matlab script.
+
+ Returns a tuple of strings containing the stdout and stderr streams of the script execution.
+ """
+ script_path = Path(script_path).absolute()
+
+ # Call the script
+ fname = script_path.stem
+ workdir = str(script_path.parent) if script_path.exists() else None
+ _, out, err = self.execute_function(function_name=fname, args=None, nargout=0, workdir=workdir)
+ return out, err
+
+
+
+[docs]
+ def execute_function(
+ self,
+ function_name: str,
+ args: list[t.Any] | None = None,
+ nargout: int = 0,
+ workdir: PathLike | None = None,
+ ) -> tuple[t.Any, str, str]:
+ """
+ Executes a Matlab function
+
+ Returns a tuple of result, stdout stream and stderr stream of the function execution.
+
+ The shape/type of result depends on the argument 'nargout'. See below.
+
+ :param function_name:
+ The name of the function to execute.
+
+ The function must be in the Matlab path to execute it.
+
+ Alternatively the workdir argument can be set to the parent directory of the function.
+ :param args: A list of positional input arguments to the function
+ :param nargout:
+ The number of output arguments.
+
+ If 0, the result return value of this function will be None.
+
+ If 1, the result return value will be a single value.
+
+ If greater than 1, the result return value will be a tuple containing nargout values.
+ :param workdir: The Matlab working directory to be changed to during function execution. Skipped if None.
+ """
+ old_cwd = self.eng.cd(str(workdir)) if workdir is not None else None
+ try:
+ out = io.StringIO()
+ err = io.StringIO()
+ result = getattr(self.eng, function_name)(
+ *(args or []), background=False, nargout=max(0, int(nargout)), stdout=out, stderr=err
+ )
+ out = out.getvalue()
+ err = err.getvalue()
+ return result, out, err
+ except Exception as e:
+ log.error(f"Execution of Matlab function/script {function_name!r} failed: {e}", exc_info=True)
+ raise
+ finally:
+ if old_cwd:
+ self.eng.cd(old_cwd, background=False)
+
+
+
+from __future__ import annotations
+
+import abc
+import copy
+import glob
+from pathlib import Path
+from typing import ClassVar
+
+import attrs
+import natsort
+import omegaconf
+from attrs.validators import deep_iterable, in_, instance_of, min_len
+
+__all__ = ["Resource", "LocalResource", "TransformedResource", "FileResource", "CustomResource"]
+
+
+
+[docs]
+@attrs.define
+class Resource:
+ """
+ The base class for all resources. Provides serialization/deserialization methods.
+ """
+
+ _registry: ClassVar[dict] = {}
+
+ alias: str = attrs.field(validator=(instance_of(str), min_len(1)))
+ _cachedir: str = attrs.field(validator=instance_of(str), kw_only=True, default="")
+
+ @staticmethod
+ def from_dict(definition: dict | omegaconf.DictConfig) -> Resource:
+ if isinstance(definition, omegaconf.DictConfig):
+ definition = omegaconf.OmegaConf.to_container(definition, resolve=True)
+ else:
+ definition = copy.deepcopy(definition)
+ resource_class = definition.pop("__class__")
+ registry = collect_resources()
+ if resource_class not in registry:
+ msg = (
+ f"No such resource type {resource_class!r} from definition of resource {definition}. "
+ f"Available types are {','.join(Resource._registry.keys())!r}!"
+ )
+ raise LookupError(msg)
+ return registry[resource_class](**definition)
+
+ def to_dict(self):
+ dikt = dict(**attrs.asdict(self, filter=lambda attr, value: not attr.name.startswith("_")))
+ dikt["__class__"] = self.__class__.__name__
+ return dikt
+
+
+
+_collected_resources_cache = None
+
+
+def collect_resources() -> dict[str, type[Resource]]:
+ global _collected_resources_cache # noqa: PLW0603
+ if _collected_resources_cache is None:
+ from pharaoh.plugins.plugin_manager import PM
+
+ _collected_resources_cache = {type_.__name__: type_ for type_ in PM.pharaoh_collect_resource_types()}
+ return _collected_resources_cache
+
+
+
+[docs]
+@attrs.define
+class LocalResource(abc.ABC, Resource):
+ """
+ Represents a resource that is located on the user's hard-drive.
+ """
+
+
+[docs]
+ @abc.abstractmethod
+ def locate(self) -> Path:
+ """
+ Returns the path to the resource file.
+ """
+ raise NotImplementedError
+
+
+
+
+
+[docs]
+@attrs.define
+class TransformedResource(LocalResource, abc.ABC):
+ """
+ A resource that is depending on another resource and maybe transforms it dynamically into another resource.
+ """
+
+ sources: list[str] = attrs.field(
+ validator=(
+ deep_iterable(
+ member_validator=instance_of(str),
+ iterable_validator=instance_of(list),
+ ),
+ min_len(1),
+ )
+ )
+
+
+[docs]
+ @abc.abstractmethod
+ def transform(self, resources: dict[str, Resource]):
+ """
+ Executed by Pharaoh before asset generation.
+ Transforms the linked resource into a new one.
+ The resulting resource may be cached or recreated each time.
+ """
+ raise NotImplementedError
+
+
+
+
+
+[docs]
+@attrs.define
+class CustomResource(Resource):
+ """
+ A resource that can hold arbitrary information. It may be used as a temporary replacement for
+ more specific resources that are not yet implemented.
+ For example, it could hold database connection properties or other custom data.
+
+ Example::
+
+ CustomResource(alias="foo", traits=dict(a=1, b=[2, 3], c=dict(d=[4, 5])))
+
+
+ :ivar traits: A dict with arbitrary data.
+ """
+
+ traits: dict
+
+
+
+
+[docs]
+@attrs.define
+class FileResource(LocalResource):
+ """
+ A resource that matches files/directories using a wildcard pattern.
+
+ Example::
+
+ FileResource(alias="mycsvfile", pattern="C:/temp/*.csv", sort="ascending")
+
+ :ivar pattern: A pathname pattern.
+ The pattern may contain simple shell-style wildcards.
+ Filenames starting with a dot are special cases that are not matched by '*' and '?' patterns.
+ :ivar sort: descending (default) or ascending.
+ Sorting is done using the natsort library to sort filesystem paths naturally. Example::
+
+ 0.txt, 01.txt, 1.txt, 10.txt, 2.txt # default sorting (ascending)
+ 0.txt, 01.txt, 1.txt, 2.txt, 10.txt # natural sorting (ascending)
+
+ """
+
+ pattern: str | Path = attrs.field(converter=str)
+
+ sort: str = attrs.field(validator=in_(("ascending", "descending")), default="descending")
+
+
+[docs]
+ def locate(self) -> Path:
+ """
+ Returns the first match for the file pattern, depending on the chosen sort order.
+ """
+ return self.first_match()
+
+
+
+[docs]
+ def first_match(self) -> Path:
+ """
+ Returns the first match for the file pattern, depending on the chosen sort order.
+ """
+ return self.get_files()[0]
+
+
+
+[docs]
+ def last_match(self) -> Path:
+ """
+ Returns the last match for the file pattern, depending on the chosen sort order.
+ """
+ return self.get_files()[-1]
+
+
+
+[docs]
+ def get_match(self, index) -> Path:
+ """
+ Returns the n-th match for the file pattern, depending on the chosen sort order.
+ """
+ return self.get_files()[index]
+
+
+
+[docs]
+ def get_files(self, recursive=True) -> list[Path]:
+ """
+ Returns all matches for the file pattern, depending on the chosen sort order.
+
+ :param recursive: If recursive is true, the pattern '**' will match any files and
+ zero or more directories and subdirectories.
+ """
+ files = [Path(p).resolve() for p in glob.glob(self.pattern, recursive=recursive)]
+ sort = {"ascending": False, "descending": True}
+ return natsort.natsorted(files, reverse=sort[self.sort])
+
+
+
+from __future__ import annotations
+
+import copy
+import datetime
+import functools
+import logging
+import os
+import re
+import shutil
+import sys
+import traceback
+import uuid
+from pathlib import Path
+from typing import TYPE_CHECKING, Any, Union
+
+import attr
+import attrs
+import omegaconf
+import omegaconf.errors
+
+import pharaoh
+import pharaoh.log
+import pharaoh.util.oc_resolvers
+from pharaoh.assetlib import finder, resource
+from pharaoh.assetlib.context import context_stack
+from pharaoh.errors import AssetGenerationError, ProjectInconsistentError
+from pharaoh.plugins.plugin_manager import PM
+from pharaoh.util.contextlib_chdir import chdir
+
+if TYPE_CHECKING:
+ from collections.abc import Iterable, Iterator
+
+ import jinja2
+
+try:
+ PHARAOH_CLI_PATH = Path(shutil.which("pharaoh.exe")).as_posix()
+except Exception:
+ PHARAOH_CLI_PATH = "pharaoh.exe"
+
+PathLike = Union[str, Path]
+
+LRU_PHARAOH_PROJECT: PharaohProject = None
+
+DEFAULT_MISSING = object()
+
+log = pharaoh.log.log
+
+
+
+[docs]
+def get_project(lookup_path: str | Path | None = None) -> PharaohProject:
+ """
+ Returns an instance of PharaohProject.
+
+ :param lookup_path: If None, the function tries to return the Pharaoh singleton instance that is already loaded in
+ memory and raises and exception if there is none.
+ If given a string or Path instance, searches the given path and all its parent folders
+ for a ``pharaoh.yaml`` project file and returns a PharaohProject instance.
+ """
+ if lookup_path is None:
+ # Enforce having a singleton per process
+ if LRU_PHARAOH_PROJECT is None:
+ msg = "No PharaohProject instance created yet!"
+ raise RuntimeError(msg)
+ return LRU_PHARAOH_PROJECT
+
+ if isinstance(lookup_path, (str, Path)):
+ lookup_path = Path(lookup_path)
+ if lookup_path.is_file():
+ lookup_path = lookup_path.parent
+ else:
+ msg = "Unsupported value for argument 'lookup_path'."
+ raise ValueError(msg)
+
+ path = Path(lookup_path).absolute()
+ if path.is_file():
+ path = path.parent
+ while True:
+ if (path / PharaohProject.PROJECT_SETTINGS_YAML).exists():
+ return PharaohProject(project_root=path)
+
+ if path == path.parent:
+ # We reached the path's anchor level
+ msg = (
+ f"Cannot find a Pharaoh project file {PharaohProject.PROJECT_SETTINGS_YAML} in "
+ f"{lookup_path} or one of its parent directories!"
+ )
+ raise LookupError(msg)
+
+ path = path.parent
+
+
+
+
+[docs]
+class PharaohProject:
+ PROJECT_SETTINGS_YAML = "pharaoh.yaml"
+
+ def __new__(cls, *args, **kwargs):
+ # Cache the last recently used project instance in a global variable,
+ # so it can be accessed from anywhere after creation
+ global LRU_PHARAOH_PROJECT # noqa: PLW0603
+ LRU_PHARAOH_PROJECT = object.__new__(cls)
+ return LRU_PHARAOH_PROJECT
+
+ def __del__(self):
+ try: # noqa: SIM105
+ pharaoh.log.remove_filehandlers(self._project_root)
+ except Exception:
+ pass
+
+
+[docs]
+ def __init__(
+ self,
+ project_root: PathLike,
+ overwrite: bool = False,
+ templates: str | Iterable[str] = ("pharaoh.default_project",),
+ template_context: dict[str, Any] | None = None,
+ **kwargs,
+ ):
+ """
+ Instantiates a Pharaoh project instance using either an existing project root
+ (directory containing pharaoh.yaml) or creates a new project.
+
+ :param project_root: A directory containing a pharaoh.yaml file
+ :param overwrite: If the project directory already contains files, overwrite them.
+ :param templates: The project template(s) to use for creating the Sphinx project files.
+ Maybe be any number of template names or paths to directories.
+ :param template_context: The Jinja rendering context for the selected project templates.
+ :keyword custom_settings: A path to a YAML file containing settings to overwrite the default project settings.
+ """
+ self._settings_map: dict[str, omegaconf.DictConfig] = {}
+ self._project_root: Path = Path(project_root).absolute().resolve()
+ self._asset_finder: finder.AssetFinder = None
+
+ logging_add_filehandler = kwargs.pop("logging_add_filehandler", True)
+ custom_settings = kwargs.pop("custom_settings", None)
+ if kwargs:
+ msg = f"Unknown keyword arguments {tuple(kwargs.keys())}!"
+ raise ValueError(msg)
+
+ pharaoh.log.add_streamhandler()
+ if logging_add_filehandler:
+ pharaoh.log.add_filehandler(self._project_root)
+ pharaoh.log.add_warning_filehandler(self._project_root)
+
+ if isinstance(templates, str):
+ templates = [templates]
+ self._ensure_project(
+ templates=templates, template_context=template_context, recreate=overwrite, custom_settings=custom_settings
+ )
+
+ self.load_settings(namespace="all")
+ log.setLevel(self.get_setting("logging.level").upper())
+
+
+
+[docs]
+ def load_settings(self, namespace: str = "all"):
+ """
+ Loads settings from various namespaces.
+
+ default
+ The default setting in the Pharaoh library
+
+ project
+ The project settings that may be modified by the user
+
+ env
+ The settings defined by environment variables starting with PHARAO
+
+ all
+ Loads all of the above
+
+ :param namespace: The namespace to load settings from. all, default, project or env.
+ """
+ if namespace in ("all", "default"):
+ self._settings_map["default"] = self._load_default_settings()
+
+ if namespace in ("all", "project"):
+ project_settings = self._project_root / self.PROJECT_SETTINGS_YAML
+ with open(project_settings) as fp:
+ self._settings_map["project"] = omegaconf.OmegaConf.load(fp)
+
+ if namespace in ("all", "env"):
+ dotlist = []
+ env_pat = re.compile(r"^PHARAOH?((\.)|(__)).+$", re.IGNORECASE)
+ for k, v in os.environ.items():
+ if env_pat.fullmatch(k):
+ keys = re.split(r"\.|(?<!_)__(?!_)", k)
+ key = ".".join(keys[1:]).lower()
+ dotlist.append(f"{key}={v}")
+ if len(dotlist):
+ self._settings_map["env"] = omegaconf.OmegaConf.from_dotlist(dotlist)
+ else:
+ self._settings_map["env"] = omegaconf.OmegaConf.create({})
+ self._update_merged_settings()
+
+
+ def _update_merged_settings(self) -> omegaconf.DictConfig:
+ """
+ Merges settings from all namespaces.
+ """
+ self._merged_settings = omegaconf.OmegaConf.merge(
+ self._settings_map["default"],
+ self._settings_map["project"],
+ self._settings_map["env"],
+ )
+
+
+[docs]
+ def get_settings(self) -> omegaconf.DictConfig:
+ """
+ Returns merged settings from all namespaces.
+ """
+ if self._merged_settings is None:
+ self._update_merged_settings()
+ return self._merged_settings
+
+
+
+[docs]
+ def put_setting(self, key: str, value: Any):
+ """
+ Sets a setting by its dot-separated name, e.g. "core.debug".
+
+ This has no effect, if there is an environment variable defining the same setting!
+ """
+ # todo: Let put_setting be able to overwrite settings defined in env vars.
+ # Currently env vars have priority.
+ paths = key.lower().split(".")
+ node = self._project_settings
+ for path in paths[:-1]:
+ node = getattr(node, path)
+ setattr(node, paths[-1], value)
+ self._update_merged_settings()
+
+
+
+[docs]
+ def get_setting(self, key: str, default=DEFAULT_MISSING, to_container: bool = False, resolve: bool = True):
+ """
+ Gets a setting by its dot-separated name, e.g. "core.debug".
+
+ The settings are preferably taken from environment variables (e.g. PHARAOH.CORE.DEBUG), whereas the values
+ must be TOML compatible values (e.g. "true" for boolean).
+
+ If no environment variable is found, the user defined settings in the pharaoh.yaml file in the
+ project root will be used. If the setting is not specified in there or the file is not existing,
+ the Pharaoh default settings are queried.
+
+ If the setting is not found, a LookupError is returned unless "default" is set.
+
+ Examples::
+
+ proj.get_setting("report.title")
+ proj.get_setting("cloud.metadata", to_container=True, resolve=False) == {
+ "author": "${report.author}",
+ "title": "${report.title}",
+ }
+ proj.get_setting("cloud.metadata", to_container=True, resolve=True) == {
+ "author": "loibljoh",
+ "title": "Pharaoh Report",
+ }
+ proj.get_setting("cloud.metadata.title") == "Pharaoh Report"
+ proj.get_setting("cloud.metadata") == omegaconf.dictconfig.DictConfig(...)
+
+ :param key: Dot-separated name of the setting value to return
+ :param default: The returned default value if the key does not exist.
+ :param to_container: If set, recursively converts an OmegaConf config to a primitive Python container type
+ (dict or list).
+ :param resolve: True to resolve all values if "to_container" is set to True
+ """
+ if not key:
+ msg = "Must be a string with a minimum length of 1!"
+ raise ValueError(msg)
+ merged_settings = self.get_settings()
+ key = key.lower()
+ try:
+ paths = key.split(".")
+ ret = merged_settings
+ for path in paths:
+ ret = ret[path]
+
+ if omegaconf.OmegaConf.is_config(ret) and to_container:
+ ret = omegaconf.OmegaConf.to_container(ret, resolve=resolve)
+ return ret
+ except Exception:
+ if default is DEFAULT_MISSING:
+ msg = f"No setting key called {key!r}!"
+ raise LookupError(msg) from None
+ return default
+
+
+
+[docs]
+ def save_settings(self, include_env: bool = False):
+ """
+ Saves back the project settings to the project YAML file.
+
+ :param include_env: If True, Pharaoh settings that are set via environment variables will be persisted
+ to the project settings YAML file.
+ """
+ project_settings = self._project_root / self.PROJECT_SETTINGS_YAML
+ if include_env:
+ settings = omegaconf.OmegaConf.merge(
+ self._settings_map["project"],
+ self._settings_map["env"],
+ )
+ else:
+ settings = self._project_settings
+ with open(project_settings, "w") as fp:
+ omegaconf.OmegaConf.save(config=settings, f=fp)
+
+
+ @property
+ def _project_settings(self):
+ return self._settings_map["project"]
+
+ @property
+ def settings_file(self):
+ return self._project_root / self.PROJECT_SETTINGS_YAML
+
+ @property
+ def project_root(self):
+ return self._project_root
+
+ @property
+ def sphinx_report_project(self):
+ return self.project_root / "report-project"
+
+ @property
+ def sphinx_report_project_components(self):
+ return self.sphinx_report_project / "components"
+
+ @property
+ def sphinx_report_build(self):
+ return self.project_root / "report-build"
+
+ @property
+ def asset_build_dir(self):
+ return self.sphinx_report_project / ".asset_build"
+
+ @property
+ def asset_finder(self) -> finder.AssetFinder:
+ if self._asset_finder is None:
+ self._asset_finder = finder.AssetFinder(self.asset_build_dir)
+ return self._asset_finder
+
+
+[docs]
+ def add_component(
+ self,
+ component_name: str,
+ templates: str | (Path | (Iterable[str] | Iterable[Path])) = ("pharaoh.empty",),
+ render_context: dict | None = None,
+ resources: list[resource.Resource] | None = None,
+ metadata: dict[str, Any] | None = None,
+ index: int = -1,
+ overwrite: bool = False,
+ ):
+ """
+ Adds a new component to the Pharaoh project
+
+ Example::
+
+ from pharaoh.api import FileResource, PharaohProject
+
+ proj = PharaohProject(".")
+ proj.add_component(
+ component_name="component_ABC",
+ templates=[
+ "plugin_abc.template_xyz", # plugin template
+ "path/to/template/directory", # template directory
+ "path/to/template/file/tmpl.pharaoh.py" # template file
+ ],
+ render_context={"foo": "bar"},
+ resources=[FileResource(alias="dlh5_result", pattern="C:/temp/**/*.dlh5")],
+ metadata={"some tag": "some value"},
+ index=-1, # append
+ overwrite=False
+ )
+
+ :param component_name: The name of the component. Must be a valid Python identifier.
+ :param templates: A list of component templates to use for creating the components project files.
+ Those may be the template identifier (e.g. ``plugin_abc.template_xyz``) of a registered plugin template,
+ a path to a template directory or a path to a template file (single-file template).
+
+ Since multiple template may be specified, their order matters in cases where different templates
+ create equally-named files, thus templates might overwrite files of the previous templates.
+
+ This enables *template-composition*, where template designers can chunk their bigger templates into
+ smaller re-usable building blocks.
+
+ If omitted, an empty default template is used.
+
+ :param render_context: The Jinja rendering context for the selected template.
+ The actual template rendering context will have ``component_name``, ``resources`` and
+ ``metadata`` available under the respective keys.
+ :param resources: A list of Resource instances, defining the component's resources used in asset scripts
+ :param metadata: A dictionary of metadata that may be used to find the component via
+ :func:`PharaohProject.find_component()
+ <pharaoh.project.PharaohProject.find_component>` method.
+ :param overwrite: If True, an already existing component will be overwritten
+ """
+ from pharaoh.templating.first_level.find import find_template
+ from pharaoh.templating.first_level.render import render_template
+
+ # Check if component with this name already added
+ components = []
+ for comp in self.iter_components():
+ if comp["name"] == component_name:
+ if overwrite:
+ pass
+ else:
+ msg = f"Component {component_name!r} already exists!"
+ raise KeyError(msg)
+ else:
+ components.append(comp)
+
+ render_context = render_context or {}
+ render_context["pharaoh_cli_path"] = PHARAOH_CLI_PATH
+
+ if isinstance(templates, (str, Path)):
+ templates = [templates]
+ templates = [str(t) for t in templates]
+
+ if not templates:
+ msg = "No templates specified!"
+ raise ValueError(msg)
+
+ comp = Component(
+ name=component_name,
+ templates=templates,
+ render_context=render_context,
+ resources=resources or [],
+ metadata=metadata or {},
+ )
+
+ # Support negative indexing where -1 is the last item. Normal insert with -1 will insert at second last index.
+ if index < 0:
+ index = max(0, len(components) + 1 + index)
+
+ components.insert(
+ index,
+ attrs.asdict(comp),
+ )
+
+ for templ in templates:
+ template_path = find_template(templ)
+ if (template_path / "[[ component_name ]]").exists(): # pragma: no cover
+ msg = (
+ "The usage of [[ component_name ]] as top level directory name of component "
+ "templates is not required anymore. "
+ )
+ raise RuntimeError(msg)
+
+ render_template(
+ template_path=template_path,
+ outputdir=self.sphinx_report_project_components / component_name,
+ context=comp.get_render_context(),
+ )
+
+ self._project_settings.components = components
+ self.save_settings()
+ log.info(f"Added component {component_name!r}. Saved project.")
+
+
+
+[docs]
+ def add_template_to_component(
+ self, component_name: str, templates: str | (Path | Iterable[str]), render_context: dict | None = None
+ ):
+ """
+ Adds additional templates to an existing component, that may overwrite existing files during rendering.
+
+ :param component_name: The name of the component. Must be a valid Python identifier.
+ :param templates: The component template(s) to use for creating the components project files.
+ Maybe be the a single or multiple template names or paths to a directory.
+ :param render_context: The Jinja rendering context for the selected template.
+ The actual template rendering context will have component_name, resources and metadata
+ available under the respective keys.
+ """
+ from pharaoh.templating.first_level.find import find_template
+ from pharaoh.templating.first_level.render import render_template
+
+ # Check if component with this name already added
+ for component_index, comp in enumerate(self.iter_components()): # noqa: B007
+ if comp["name"] == component_name:
+ component = Component.from_dictconfig(comp)
+ break
+ else:
+ msg = f"Component {component_name!r} does not exist!"
+ raise KeyError(msg)
+
+ component.render_context.update(render_context or {})
+
+ if isinstance(templates, (str, Path)):
+ templates = [templates]
+ templates = [str(t) for t in templates]
+
+ if not templates:
+ msg = "No templates specified!"
+ raise ValueError(msg)
+
+ component.templates.extend(templates)
+
+ for templ in templates:
+ template_path = find_template(templ)
+ render_template(
+ template_path=template_path,
+ outputdir=self.sphinx_report_project_components / component_name,
+ context=component.get_render_context(),
+ )
+
+ self._project_settings.components[component_index] = attrs.asdict(component)
+
+ self._update_merged_settings()
+ self.save_settings()
+ log.info(f"Updated component {component_name!r}. Saved project.")
+
+
+
+[docs]
+ def remove_component(
+ self,
+ filter: str,
+ regex: bool = False,
+ ) -> list[str]:
+ """
+ Removes one or multiple existing components.
+
+ :param filter: A case-insensitive component filter. Either a full-match or regular expression, depending on
+ regex argument.
+ :param regex: If True, the filter argument will be treated as regular expression. Components
+ that partially match the regular expression are removed.
+ :returns: A list of component names that got removed
+ """
+ # Check if component with this name already added
+ if regex:
+ rex = re.compile(filter, flags=re.IGNORECASE)
+ else:
+ filter = filter.lower()
+ removed = []
+ components = []
+ for comp in self.iter_components():
+ if regex and rex.match(comp["name"]) is not None or comp["name"].lower() == filter:
+ comp_files = self.sphinx_report_project_components / comp["name"]
+ if comp_files.exists() and comp_files.is_dir(): # pragma: no cover
+ shutil.rmtree(comp_files)
+ removed.append(comp["name"])
+ else:
+ components.append(comp)
+
+ if len(removed):
+ self._project_settings.components = components
+ self.save_settings()
+ log.info(f"Removed components {','.join(removed)}. Saved project.")
+ return removed
+
+
+
+[docs]
+ def iter_components(self) -> Iterator[omegaconf.DictConfig]:
+ """
+ Returns an iterator over all components from a project.
+ """
+ yield from self._project_settings.get("components", []) or []
+
+
+
+[docs]
+ def find_components(self, expression: str = "") -> list[omegaconf.DictConfig]:
+ """
+ Find components by their metadata using an evaluated expression.
+
+ The expression must be a valid Python expression and following local variables may be used:
+
+ - name: The name of the component
+ - templates: A list of templates used to render the component
+ - metadata: A dict of metadata specified
+ - render_context: The Jinja rendering context specified
+ - resources: A list of resource definitions
+
+ :param expression: A Python expression that will be evaluated. If it evaluates to a truthy result,
+ the component name is included in the returned list.
+
+ Example: ``name == "dummy" and metadata.foo in (1,2,3)``.
+
+ A failing evaluation will be treated as False. An empty expression will always match.
+ """
+ found = []
+ for comp in self.iter_components():
+ if not expression:
+ found.append(comp)
+ continue
+ try:
+ result = eval(expression, {}, copy.deepcopy(comp))
+ except Exception:
+ result = False
+ if result:
+ found.append(comp)
+ return found
+
+
+
+[docs]
+ def get_resource(self, alias: str, component: str) -> resource.Resource:
+ """
+ Finds a Resource from a project component by its alias.
+
+ :param component: The component's name
+ :param alias: The resource's alias
+ """
+ for r in self.iter_resources(component):
+ if r["alias"] == alias:
+ obj = resource.Resource.from_dict(r)
+ obj._cachedir = str(self.sphinx_report_project / ".resource_cache" / component)
+ return obj
+
+ msg = f"Cannot find resource {alias!r} in component {component!r}!"
+ raise LookupError(msg)
+
+
+
+[docs]
+ def iter_resources(self, component: str) -> Iterator[omegaconf.DictConfig]:
+ """
+ Returns an iterator over all resources from a project component.
+
+ :param component: The component's name
+ """
+ for comp in self.iter_components():
+ if comp["name"] == component:
+ yield from comp["resources"]
+ return
+
+ msg = f"Component {component} does not exist!"
+ raise LookupError(msg)
+
+
+
+[docs]
+ def update_resource(self, alias: str, component: str, resource: resource.Resource):
+ """
+ Updates a Resource from a project component by its alias.
+
+ :param component: The component's name
+ :param alias: The resource's alias
+ :param resource: The resource's alias
+ """
+ for comp in self.iter_components():
+ if comp["name"] == component:
+ for res in comp["resources"]:
+ if res["alias"] == alias:
+ res.update(resource.to_dict())
+ self.save_settings()
+ return
+ comp["resources"].append(resource.to_dict())
+ self.save_settings()
+ return
+
+ msg = f"Component {component} does not exist!"
+ raise LookupError(msg)
+
+
+
+[docs]
+ def get_default_sphinx_configuration(self, confdir: PathLike):
+ """
+ Provides Sphinx project configurations, that will be dynamically included by the generated conf.py.
+ """
+ confdir = Path(confdir)
+
+ config = {
+ "pharaoh_jinja_templates": [],
+ "pharaoh_jinja_context": {},
+ "extensions": [
+ "sphinx_design", # https://github.com/executablebooks/sphinx-design
+ "pharaoh.templating.second_level.sphinx_ext.jinja_ext",
+ "pharaoh.templating.second_level.sphinx_ext.asset_ext",
+ ],
+ "project": self.get_setting("report.title"),
+ "author": self.get_setting("report.author"),
+ "copyright": f"{datetime.datetime.now(tz=datetime.timezone.utc).date()}",
+ # The suffix(es) of source filenames.
+ # You can specify multiple suffix as a list of string:
+ "source_suffix": [".rst", ".txt"],
+ "master_doc": "index",
+ "templates_path": ["_templates"],
+ # List of patterns, relative to source directory, that match files and
+ # directories to ignore when looking for source files.
+ # This pattern also affects html_static_path and html_extra_path.
+ "exclude_patterns": [".asset_build", "**/asset_scripts"],
+ # Make sure the target is unique
+ "autosectionlabel_prefix_document": True,
+ # Latex Builder (PDF)
+ }
+ # HTML Builder
+ static_path = confdir / "_static"
+ assert static_path.exists()
+ css_files = list((static_path / "css").glob("*.css"))
+ js_files = list((static_path / "js").glob("*.js"))
+ config.update(
+ {
+ "html_title": self.get_setting("report.title"),
+ "html_use_smartypants": True,
+ "html_last_updated_fmt": "%b %d, %Y",
+ "html_split_index": False,
+ "html_sidebars": {
+ "**": ["searchbox.html", "globaltoc.html", "sourcelink.html"],
+ },
+ # Custom index page
+ # https://ofosos.org/2018/12/28/landing-page-template/
+ # html_additional_pages={"index": "index.html"},
+ # Add any paths that contain custom static files (such as style sheets) here,
+ # relative to this directory. They are copied after the builtin static files,
+ # so a file named "pharaoh.css" will overwrite the builtin "pharaoh.css".
+ "html_static_path": ["_static"],
+ "html_css_files": [f"css/{cssfile.name}" for cssfile in css_files],
+ "html_js_files": [f"js/{jsfile.name}" for jsfile in js_files],
+ "html_show_sphinx": False,
+ "html_show_copyright": True,
+ "html_context": {
+ "pharaoh_version": pharaoh.__version__,
+ "pharaoh_logo": "_static/html_logo.png",
+ "pharaoh_show_logo": self.get_setting("report.html.show_pharaoh_logo"),
+ "pharaoh_homepage": "", # todo: change to RTD
+ },
+ "html_style": "sphinx_rtd_theme_overrides.css",
+ "html_theme": "sphinx_rtd_theme",
+ "html_theme_options": {
+ "display_version": False,
+ "prev_next_buttons_location": "both",
+ "style_external_links": False,
+ "collapse_navigation": False,
+ "sticky_navigation": True,
+ "navigation_depth": 5,
+ "includehidden": False,
+ },
+ }
+ )
+
+ PM.pharaoh_configure_sphinx(self, config, confdir)
+
+ return config
+
+
+
+[docs]
+ def generate_assets(self, component_filters: Iterable[str] = (".*",)) -> list[Path]:
+ """
+ Generate all assets by executing the asset scripts of a selected or all components.
+
+ All asset scripts are executed in separate parallel child processes
+ (number of workers determined by ``asset_gen.worker_processes`` setting;
+ setting 0 executes all asset scripts sequentially in the current process).
+
+ Setting ``asset_gen.script_ignore_pattern`` determines if a script is ignored.
+
+ Putting the comment ``# pharaoh: ignore`` at the start of a script will also ignore the file.
+
+ :param component_filters: A list of regular expressions that are matched against each component name.
+ If a component name matches any of the regular expressions, the component's
+ assets are regenerated (containing directory will be cleared)
+ """
+ from pharaoh.assetlib.generation import generate_assets, generate_assets_parallel
+
+ pharaoh.log.log_version_info()
+ log.info("Generating assets...")
+
+ PM.pharaoh_asset_gen_started(self)
+ sources = []
+ for comp in self.iter_components():
+ comp_name = comp["name"]
+ comp_asset_build_dir = self.asset_build_dir / comp_name
+ for cfilter in component_filters:
+ if re.match(cfilter, comp_name, re.IGNORECASE) is not None:
+ if comp_asset_build_dir.exists():
+ shutil.rmtree(comp_asset_build_dir, ignore_errors=False)
+
+ cachedir = self.sphinx_report_project / ".resource_cache" / comp_name
+ log.info(f"Preparing resources of component {comp_name!r} for asset generation...")
+ cachedir.mkdir(parents=True, exist_ok=True)
+ resources: dict[str, resource.Resource] = {
+ resource_dict["alias"]: resource.Resource.from_dict(resource_dict)
+ for resource_dict in comp["resources"]
+ }
+ for r in resources.values():
+ r._cachedir = str(cachedir)
+
+ PM.pharaoh_asset_gen_prepare_resources(project=self, resources=resources)
+
+ # TransformedResource may depend on others, so process them first.
+ for r in resources.values():
+ r._cachedir = str(cachedir)
+ if isinstance(r, resource.TransformedResource):
+ r.transform(resources=resources)
+
+ for script in (self.sphinx_report_project_components / comp_name / "asset_scripts").rglob("*"):
+ sources.append((comp_name, script))
+ break
+
+ workers = self.get_setting("asset_gen.worker_processes", 0)
+ if workers == 0: # Run in same process - used for easier debugging
+ results = []
+ for component_name, asset_source in sources:
+ try:
+ generate_assets(self.project_root, asset_src=asset_source, component_name=component_name)
+ results.append((asset_source, None))
+ except Exception:
+ results.append((asset_source, traceback.format_exc()))
+ else:
+ results = generate_assets_parallel(self.project_root, asset_sources=sources, workers=workers)
+
+ msg = "At least one error occurred while asset script execution:\n"
+ i = 1
+ processed_asset_scripts = []
+ for script, ex in results:
+ if ex:
+ msg += f"\n\nError #{i}: {ex}\n"
+ i += 1
+ else:
+ processed_asset_scripts.append(script)
+ if i > 1:
+ pharaoh.log.log_debug_info()
+ raise AssetGenerationError(msg)
+
+ return processed_asset_scripts
+
+
+
+[docs]
+ def build_report(self, catch_errors=True) -> int:
+ """
+ Builds the Sphinx project and returns the status code.
+
+ :param catch_errors: If True, Sphinx build errors will not raise an exception but return a -1 instead.
+ """
+ import multiprocessing
+
+ from sphinx.util.docutils import docutils_namespace, patch_docutils
+
+ from pharaoh.sphinx_app import PharaohSphinx
+
+ builder = self.get_setting("report.builder")
+ PM.pharaoh_build_started(self, builder)
+ self._check_template_dependencies()
+
+ with chdir(self.sphinx_report_project):
+ pharaoh.log.log_version_info()
+ log.info(f"Building Sphinx project using {builder.upper()} builder...")
+
+ resolved_settings = omegaconf.OmegaConf.merge(
+ self._settings_map["default"],
+ self._settings_map["project"],
+ self._settings_map["env"],
+ )
+ omegaconf.OmegaConf.resolve(resolved_settings)
+ self.sphinx_report_build.mkdir(parents=True, exist_ok=True)
+ with open(self.sphinx_report_build / "pharaoh.resolved.yaml", "w") as fp:
+ omegaconf.OmegaConf.save(config=resolved_settings, f=fp)
+
+ # Sphinx is used via its API instead of the CLI because we want to modify the info/warning stream
+ try:
+ sourcedir = "."
+ confdir = sourcedir
+ outputdir = str(self.sphinx_report_build)
+ doctreedir = f"{outputdir}\\.doctrees"
+ confoverrides = {}
+ status = pharaoh.log.SphinxLogRedirector(logging.DEBUG)
+ warning = pharaoh.log.SphinxLogRedirector(logging.WARNING)
+ freshenv = True
+ warningiserror = True
+ tags = None
+ verbosity = int(self.get_setting("report.verbosity", 0))
+ jobs = multiprocessing.cpu_count()
+ keep_going = True
+ pdb = False
+
+ with patch_docutils(confdir), docutils_namespace():
+ app = PharaohSphinx(
+ sourcedir,
+ confdir,
+ outputdir,
+ doctreedir,
+ builder,
+ confoverrides,
+ status,
+ warning,
+ freshenv,
+ warningiserror,
+ tags,
+ verbosity,
+ jobs,
+ keep_going,
+ pdb,
+ )
+ app.build(force_all=False, filenames=None)
+ if app.statuscode:
+ log.error(
+ f"Sphinx build finished with non-zero exit code {app.statuscode}. "
+ f"See warnings/errors in log above!"
+ )
+ else:
+ log.info(f"Sphinx build finished successfully! Report at:\n{self.sphinx_report_build}")
+
+ PM.pharaoh_build_finished(self, None)
+
+ return app.statuscode
+ except Exception:
+ log.error("Errors occurred during Sphinx build:", exc_info=True)
+ pharaoh.log.log_debug_info()
+
+ PM.pharaoh_build_finished(self, sys.exc_info())
+
+ if not catch_errors:
+ raise
+ return -1
+
+
+
+[docs]
+ def open_report(self): # pragma: no cover
+ """
+ Opens the generated report (if possible, e.g. for local HTML reports).
+ """
+ builder = self.get_setting("report.builder").lower()
+ if builder == "html" and os.name == "nt":
+ report = self.sphinx_report_build / "index.html"
+ if not report.is_file():
+ log.warning(f"Could not find HTML report {report}!")
+ return
+ import webbrowser
+
+ webbrowser.WindowsDefault().open_new_tab(f"file://{report}")
+ else:
+ log.warning(f"The open_report method is not implemented for builder {builder}!")
+
+
+
+[docs]
+ def archive_report(self, dest: PathLike | None = None) -> Path:
+ """
+ Create an archive from the build folder.
+
+ :param dest: A destination path to create the archive. Relative paths are relative to the project root.
+ If omitted, the filename will be taken from the ``report.archive_name`` setting.
+ :return: The path to the archive
+ """
+ if not self.sphinx_report_build.exists():
+ msg = "Pharaoh report not built yet!"
+ raise Exception(msg)
+ dest = Path(self.get_setting("report.archive_name")) if dest is None else Path(dest)
+
+ if not dest.is_absolute():
+ dest = (self.project_root / dest).absolute()
+
+ if not dest.suffix:
+ dest /= self.get_setting("report.archive_name")
+
+ if dest.exists():
+ os.remove(dest)
+ dest.parent.mkdir(parents=True, exist_ok=True)
+
+ base_name = dest.parent / dest.stem
+ fmt = dest.suffix.replace(".", "")
+ shutil.make_archive(str(base_name), fmt, self.sphinx_report_build)
+ log.info(f"Created archive at {dest}")
+
+ return dest
+
+
+ def _check_template_dependencies(self):
+ """
+ Checks if the used templates of all components are have dependencies to templates that are not used in any
+ component.
+
+ E.g. "plugin_abc.template_A" requires an additional component that renders the "plugin_abc.template_B" template,
+ in order for all glossary references to work properly.
+ """
+ l1_templates = PM.pharaoh_collect_l1_templates()
+ used_templates = set()
+ dependent_templates = set()
+ for comp in self._project_settings.get("components", []):
+ for template in comp.templates:
+ if template in l1_templates:
+ used_templates.add(template)
+ [dependent_templates.add(need) for need in l1_templates[template].needs]
+ if not dependent_templates.issubset(used_templates):
+ msg = (
+ f"The used project templates requires the existence of components that render "
+ f"following templates: {','.join(dependent_templates - used_templates)}"
+ )
+ raise ProjectInconsistentError(msg)
+
+ def _ensure_project(
+ self,
+ templates: list[str],
+ template_context: dict[str, Any] | None = None,
+ recreate=False,
+ custom_settings: str | Path | None = None,
+ ):
+ """
+ Creates a new project by rendering the Sphinx default project at the desired project location.
+ """
+ self.project_root.mkdir(exist_ok=True, parents=True)
+ existing_files = tuple(self.project_root.glob("*"))
+ if len(existing_files):
+ if recreate:
+ shutil.rmtree(
+ self.project_root, onerror=lambda func, path, exc_info: log.warning(f"Could not delete file {path}")
+ )
+ else:
+ # There are existing files, check if those are the required pharaoh project files/folders
+ for path in (
+ self.settings_file,
+ self.project_root,
+ self.sphinx_report_project,
+ ):
+ if not path.exists():
+ msg = f"Project structure inconsistent. Missing {path}!"
+ raise ProjectInconsistentError(msg)
+ self.asset_build_dir.mkdir(exist_ok=True, parents=True)
+ return
+
+ # Create a new project
+ self.project_root.mkdir(exist_ok=True, parents=True)
+ self.asset_build_dir.mkdir(exist_ok=True, parents=True)
+
+ default_settings = self._load_default_settings()
+ if isinstance(custom_settings, (str, Path)):
+ custom_settings = omegaconf.OmegaConf.load(custom_settings)
+ default_settings = omegaconf.OmegaConf.unsafe_merge(default_settings, custom_settings)
+
+ self.settings_file.write_text(omegaconf.OmegaConf.to_yaml(default_settings, resolve=False))
+
+ gitignore = self.project_root / ".gitignore"
+ gitignore_lines = [
+ "# Auto-generated by Pharaoh",
+ "/log*.txt",
+ "/report-build",
+ "/report-project/.asset_build",
+ "/report-project/.resource_cache",
+ "/*.zip",
+ "/*.idea",
+ ]
+ PM.pharaoh_modify_gitignore(gitignore_lines)
+ gitignore.write_text("\n".join(gitignore_lines), encoding="utf-8")
+
+ from .templating.first_level.render import render_sphinx_base_project
+
+ render_sphinx_base_project(
+ outputdir=self.sphinx_report_project,
+ templates=templates,
+ context={**(template_context or {}), **{"pharaoh_cli_path": PHARAOH_CLI_PATH}}, # noqa: PIE800
+ )
+
+ PM.pharaoh_project_created(self)
+ return
+
+ def _load_default_settings(self) -> omegaconf.DictConfig:
+ default_settings_collection = PM.pharaoh_collect_default_settings()
+ loaded = []
+ for default_settings in default_settings_collection:
+ if isinstance(default_settings, (str, Path)):
+ if not Path(default_settings).exists():
+ msg = f"Custom setting YAMl file {default_settings} does not exist!"
+ raise FileNotFoundError(msg)
+ loaded.append(omegaconf.OmegaConf.load(default_settings))
+ elif isinstance(default_settings, dict):
+ loaded.append(omegaconf.OmegaConf.create(default_settings))
+ else:
+ msg = (
+ f"Unsupported settings type {type(default_settings)} collected from plugin hook "
+ f"pharaoh_collect_default_settings!"
+ )
+ raise TypeError(msg)
+
+ return functools.reduce(omegaconf.OmegaConf.unsafe_merge, loaded)
+
+ def _update_template_env(self, env: jinja2.Environment):
+ """
+ Called by PharaohTemplateEnv.sphinx_config_inited_hook to update the template env by
+ project specific Jinja globals or filters
+
+ :param env: the Jinja env to update
+ """
+ env.globals["get_setting"] = self.get_setting
+ env.globals["search_assets_global"] = self.asset_finder.search_assets
+
+ def _build_asset_filepath(self, file: PathLike, component_name: str | None = None) -> Path:
+ """
+ Returns a new file name inside the asset build directory with the same filename as the input file,
+ except its file stem is suffixed with a unique uuid4 hash (first 8 chars).
+
+ E.g. `foo/bar/iris_scatter_plot.html` --> `<asset-build-dir>/<component_name>/iris_scatter_plot_ab8b4081.html`
+ """
+ file = Path(file)
+ try:
+ component = component_name or context_stack.get_parent_context("generate_assets")["asset"]["component_name"]
+ except Exception:
+ component = "unknown_component"
+ (self.asset_build_dir / component).mkdir(parents=True, exist_ok=True)
+ return self.asset_build_dir / component / f"{file.stem}_{str(uuid.uuid4())[:8]}{file.suffix}"
+
+
+
+@attrs.define(frozen=True, slots=False)
+class Component:
+ name: str = attrs.field(validator=(attr.validators.instance_of(str), attr.validators.matches_re(r"\w+")))
+ templates: list[str] = attrs.field(
+ validator=attrs.validators.deep_iterable(
+ member_validator=attrs.validators.instance_of(str),
+ iterable_validator=attrs.validators.instance_of(list),
+ )
+ )
+ render_context: dict = attrs.field(factory=dict)
+ resources: list[dict] = attrs.field(factory=list, converter=lambda resources: [r.to_dict() for r in resources])
+ metadata: dict = attrs.field(factory=dict)
+
+ def get_render_context(self) -> dict:
+ for reserved_key in ("component_name", "resources", "metadata"):
+ if reserved_key in self.render_context:
+ msg = f"{reserved_key!r} is a reserved context key!"
+ raise ValueError(msg)
+
+ return dict(**self.render_context, component_name=self.name, resources=self.resources, metadata=self.metadata)
+
+ @staticmethod
+ def from_dictconfig(cfg: omegaconf.DictConfig):
+ return Component(**omegaconf.OmegaConf.to_container(cfg, resolve=False))
+
+
+if __name__ == "__main__":
+ pass
+
+from pathlib import Path
+
+import omegaconf
+from jinja2.exceptions import UndefinedError
+from jinja2.utils import is_undefined
+
+DEFAULT = object()
+
+
+
+[docs]
+def required(value):
+ """
+ Raises an UndefinedError if the context variable is undefined.
+ The actual name of the Jinja global function is ``req``.
+
+ Example::
+
+ {{ h1(ctx.local.test.test_name|req) }}
+ """
+ if is_undefined(value):
+ raise UndefinedError
+ return value
+
+
+
+
+[docs]
+def rep(value) -> str:
+ """
+ Returns ``repr(value)``. The actual name of the Jinja global function is ``repr``.
+ """
+ return repr(value)
+
+
+
+
+[docs]
+def or_default(value, default):
+ """
+ Returns the result of ``value or default``.
+
+ Alias: ``or_d``
+ """
+ return value or default
+
+
+
+
+[docs]
+def oc_resolve(value: omegaconf.DictConfig):
+ """
+ Recursively converts an OmegaConf config to a primitive container (dict or list) and returns it.
+ """
+ conf = value.copy()
+ return omegaconf.OmegaConf.to_container(conf, resolve=True)
+
+
+
+
+[docs]
+def oc_get(cfg: omegaconf.DictConfig, key, default=DEFAULT):
+ """
+ Returns an object inside an omegaconf.DictConfig container.
+ If the object does not exist, the default is returned.
+
+ Example::
+
+ {% set plot_title = asset.context|oc_get("plot.title", "") %}
+
+ :param cfg: The omegaconf.DictConfig container to search
+ :param key: The name of the object to extract, e.g. ``plot.title``.
+ :param default: The default to return if key is not in cfg
+ """
+ try:
+ obj = cfg
+ for k in key.split("."):
+ ret = getattr(obj, k)
+ obj = ret
+ return ret
+ except Exception:
+ if default is DEFAULT:
+ msg = f"Not attribute {key} in {cfg}!"
+ raise AttributeError(msg) from None
+ return default
+
+
+
+
+[docs]
+def exists(path: str) -> bool:
+ """
+ Returns if path exists.
+ """
+ return Path(path).exists()
+
+
+
+
+[docs]
+def to_path(path: str) -> Path:
+ """
+ Returns path as a pathlib.Path instance.
+ """
+ return Path(path)
+
+
+
+
+[docs]
+def hasattr_(obj, name):
+ _hasattr = hasattr(obj, name)
+ _haskey = False
+ if isinstance(obj, (dict, omegaconf.DictConfig)):
+ _haskey = name in obj
+ return _hasattr or _haskey
+
+
+
+
+[docs]
+def md2html(text):
+ """
+ Converts the Markdown text (CommonMark syntax) to HTML.
+ """
+ import mistletoe
+
+ return mistletoe.markdown(text)
+
+
+
+env_filters = {
+ "hasattr": hasattr_,
+ "req": required,
+ "repr": rep,
+ "or_d": or_default,
+ "or_default": or_default,
+ "oc_resolve": oc_resolve,
+ "oc_get": oc_get,
+ "bool": bool,
+ "exists": exists,
+ "to_path": to_path,
+ "md2html": md2html,
+}
+
+# Only document the functions defined in here
+to_document = {
+ k: func for k, func in env_filters.items() if hasattr(func, "__module__") and func.__module__ == __name__
+}
+
+from __future__ import annotations
+
+import uuid
+from functools import partial
+from pathlib import Path
+
+from pharaoh.assetlib.finder import asset_groupby
+
+
+
+[docs]
+def raise_helper(msg):
+ """
+ Raises an Exception with a certain message. The actual name of the Jinja global function is ``raise``.
+ """
+ raise Exception(msg)
+
+
+
+
+[docs]
+def heading(text: str, level: int) -> str:
+ """
+ Renders a heading for a certain level.
+
+ Shortcuts for this function are automatically generated with names h1...h7.
+
+ Examples::
+
+ {{ heading("Page Title", 1) }}
+ {{ h2("Sub-Title") }}
+
+ :param text: The heading
+ :param level: The heading level. From 1 (top-level) to 7.
+ """
+ default = {1: "#", 2: "*", 3: "=", 4: "-", 5: "^", 6: '"', 7: "~"}
+ if isinstance(level, (float, int)):
+ if int(level) not in default:
+ msg = "Default heading characters only available from index 1 to 7!"
+ raise LookupError(msg)
+ character = default[level]
+ else:
+ character = str(level)
+ if character not in default.values():
+ raise ValueError("character must be an integer from 1-7 or one of " + ",".join(default.values()))
+
+ text = text.strip().replace("\r", "").replace("\n", "")
+ if len(text) < 2:
+ msg = "Heading text must be at least 2 characters"
+ raise ValueError(msg)
+ return f"{text}\n{character*len(text)}"
+
+
+
+
+[docs]
+def rand_id(chars: int | None = None) -> str:
+ """
+ Returns a unique id for usage in HTML as it is made sure it starts with no number
+ """
+ id = "i" + uuid.uuid4().hex
+ return id[:chars]
+
+
+
+
+[docs]
+def read_text(file) -> str:
+ """
+ Reads a file content using utf-8 encoding.
+ """
+ file = Path(file)
+ return file.read_text("utf-8")
+
+
+
+
+[docs]
+def hrule():
+ """
+ Renders a 1px gray horizontal rule using a raw html directive.
+ """
+ return '.. raw:: html\n\n <hr style="height:1px;border-width:0;color:gray;background-color:gray">\n\n'
+
+
+
+
+[docs]
+def fglob(pattern: str, root: str = ".") -> list[Path]:
+ """
+ Returns a list of relative paths of all files relative to a root directory that match a certain pattern.
+
+ Examples::
+
+ {% set index_files1 = fglob("index_*.rst") %}
+ {% set index_files2 = fglob("subdir/index_*.rst") %}
+
+ :param pattern: A glob pattern to match files.
+ :param root: The root directory for discovering files and anchor for relative paths.
+ Default is the current working directory (the parent directory of the currently rendered file).
+ """
+ rootpath = Path(root).absolute()
+ matches = rootpath.glob(pattern)
+ return [file.relative_to(rootpath) for file in matches]
+
+
+
+
+[docs]
+def assert_true(statement: bool, message: str = ""):
+ """
+ Wrapper for Python's assert builtin.
+
+ :param statement: A boolean statement
+ :param message: The message for the AssertionError, if statement is False.
+ """
+ if message:
+ assert statement, message
+ else:
+ assert statement
+
+
+
+env_globals = {
+ "assert_true": assert_true,
+ "fglob": fglob,
+ "hrule": hrule,
+ "raise": raise_helper,
+ "heading": heading,
+ "rand_id": rand_id,
+ "read_text": read_text,
+ "agroupby": asset_groupby,
+ "asset_groupby": asset_groupby,
+}
+# Shortcuts for headings
+for i in range(1, 8):
+ env_globals[f"h{i}"] = partial(heading, level=i)
+
+
+# Only document the functions defined in here
+to_document = {
+ k: func for k, func in env_globals.items() if hasattr(func, "__module__") and func.__module__ == __name__
+}
+
This HTML text was generated by an asset script!
Short
+ */ + .o-tooltip--left { + position: relative; + } + + .o-tooltip--left:after { + opacity: 0; + visibility: hidden; + position: absolute; + content: attr(data-tooltip); + padding: .2em; + font-size: .8em; + left: -.2em; + background: grey; + color: white; + white-space: nowrap; + z-index: 2; + border-radius: 2px; + transform: translateX(-102%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); +} + +.o-tooltip--left:hover:after { + display: block; + opacity: 1; + visibility: visible; + transform: translateX(-100%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); + transition-delay: .5s; +} + +/* By default the copy button shouldn't show up when printing a page */ +@media print { + button.copybtn { + display: none; + } +} diff --git a/_static/copybutton.js b/_static/copybutton.js new file mode 100644 index 0000000..2ea7ff3 --- /dev/null +++ b/_static/copybutton.js @@ -0,0 +1,248 @@ +// Localization support +const messages = { + 'en': { + 'copy': 'Copy', + 'copy_to_clipboard': 'Copy to clipboard', + 'copy_success': 'Copied!', + 'copy_failure': 'Failed to copy', + }, + 'es' : { + 'copy': 'Copiar', + 'copy_to_clipboard': 'Copiar al portapapeles', + 'copy_success': '¡Copiado!', + 'copy_failure': 'Error al copiar', + }, + 'de' : { + 'copy': 'Kopieren', + 'copy_to_clipboard': 'In die Zwischenablage kopieren', + 'copy_success': 'Kopiert!', + 'copy_failure': 'Fehler beim Kopieren', + }, + 'fr' : { + 'copy': 'Copier', + 'copy_to_clipboard': 'Copier dans le presse-papier', + 'copy_success': 'Copié !', + 'copy_failure': 'Échec de la copie', + }, + 'ru': { + 'copy': 'Скопировать', + 'copy_to_clipboard': 'Скопировать в буфер', + 'copy_success': 'Скопировано!', + 'copy_failure': 'Не удалось скопировать', + }, + 'zh-CN': { + 'copy': '复制', + 'copy_to_clipboard': '复制到剪贴板', + 'copy_success': '复制成功!', + 'copy_failure': '复制失败', + }, + 'it' : { + 'copy': 'Copiare', + 'copy_to_clipboard': 'Copiato negli appunti', + 'copy_success': 'Copiato!', + 'copy_failure': 'Errore durante la copia', + } +} + +let locale = 'en' +if( document.documentElement.lang !== undefined + && messages[document.documentElement.lang] !== undefined ) { + locale = document.documentElement.lang +} + +let doc_url_root = DOCUMENTATION_OPTIONS.URL_ROOT; +if (doc_url_root == '#') { + doc_url_root = ''; +} + +/** + * SVG files for our copy buttons + */ +let iconCheck = `` + +// If the user specified their own SVG use that, otherwise use the default +let iconCopy = ``; +if (!iconCopy) { + iconCopy = `` +} + +/** + * Set up copy/paste for code blocks + */ + +const runWhenDOMLoaded = cb => { + if (document.readyState != 'loading') { + cb() + } else if (document.addEventListener) { + document.addEventListener('DOMContentLoaded', cb) + } else { + document.attachEvent('onreadystatechange', function() { + if (document.readyState == 'complete') cb() + }) + } +} + +const codeCellId = index => `codecell${index}` + +// Clears selected text since ClipboardJS will select the text when copying +const clearSelection = () => { + if (window.getSelection) { + window.getSelection().removeAllRanges() + } else if (document.selection) { + document.selection.empty() + } +} + +// Changes tooltip text for a moment, then changes it back +// We want the timeout of our `success` class to be a bit shorter than the +// tooltip and icon change, so that we can hide the icon before changing back. +var timeoutIcon = 2000; +var timeoutSuccessClass = 1500; + +const temporarilyChangeTooltip = (el, oldText, newText) => { + el.setAttribute('data-tooltip', newText) + el.classList.add('success') + // Remove success a little bit sooner than we change the tooltip + // So that we can use CSS to hide the copybutton first + setTimeout(() => el.classList.remove('success'), timeoutSuccessClass) + setTimeout(() => el.setAttribute('data-tooltip', oldText), timeoutIcon) +} + +// Changes the copy button icon for two seconds, then changes it back +const temporarilyChangeIcon = (el) => { + el.innerHTML = iconCheck; + setTimeout(() => {el.innerHTML = iconCopy}, timeoutIcon) +} + +const addCopyButtonToCodeCells = () => { + // If ClipboardJS hasn't loaded, wait a bit and try again. This + // happens because we load ClipboardJS asynchronously. + if (window.ClipboardJS === undefined) { + setTimeout(addCopyButtonToCodeCells, 250) + return + } + + // Add copybuttons to all of our code cells + const COPYBUTTON_SELECTOR = 'div.highlight pre'; + const codeCells = document.querySelectorAll(COPYBUTTON_SELECTOR) + codeCells.forEach((codeCell, index) => { + const id = codeCellId(index) + codeCell.setAttribute('id', id) + + const clipboardButton = id => + `` + codeCell.insertAdjacentHTML('afterend', clipboardButton(id)) + }) + +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} + + +var copyTargetText = (trigger) => { + var target = document.querySelector(trigger.attributes['data-clipboard-target'].value); + + // get filtered text + let exclude = '.linenos'; + + let text = filterText(target, exclude); + return formatCopyText(text, '', false, true, true, true, '', '') +} + + // Initialize with a callback so we can modify the text before copy + const clipboard = new ClipboardJS('.copybtn', {text: copyTargetText}) + + // Update UI with error/success messages + clipboard.on('success', event => { + clearSelection() + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_success']) + temporarilyChangeIcon(event.trigger) + }) + + clipboard.on('error', event => { + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_failure']) + }) +} + +runWhenDOMLoaded(addCopyButtonToCodeCells) \ No newline at end of file diff --git a/_static/copybutton_funcs.js b/_static/copybutton_funcs.js new file mode 100644 index 0000000..dbe1aaa --- /dev/null +++ b/_static/copybutton_funcs.js @@ -0,0 +1,73 @@ +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +export function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +export function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} diff --git a/_static/css/badge_only.css b/_static/css/badge_only.css new file mode 100644 index 0000000..c718cee --- /dev/null +++ b/_static/css/badge_only.css @@ -0,0 +1 @@ +.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}} \ No newline at end of file diff --git a/_static/css/fonts/Roboto-Slab-Bold.woff b/_static/css/fonts/Roboto-Slab-Bold.woff new file mode 100644 index 0000000..6cb6000 Binary files /dev/null and b/_static/css/fonts/Roboto-Slab-Bold.woff differ diff --git a/_static/css/fonts/Roboto-Slab-Bold.woff2 b/_static/css/fonts/Roboto-Slab-Bold.woff2 new file mode 100644 index 0000000..7059e23 Binary files /dev/null and b/_static/css/fonts/Roboto-Slab-Bold.woff2 differ diff --git a/_static/css/fonts/Roboto-Slab-Regular.woff b/_static/css/fonts/Roboto-Slab-Regular.woff new file mode 100644 index 0000000..f815f63 Binary files /dev/null and b/_static/css/fonts/Roboto-Slab-Regular.woff differ diff --git a/_static/css/fonts/Roboto-Slab-Regular.woff2 b/_static/css/fonts/Roboto-Slab-Regular.woff2 new file mode 100644 index 0000000..f2c76e5 Binary files /dev/null and b/_static/css/fonts/Roboto-Slab-Regular.woff2 differ diff --git a/_static/css/fonts/fontawesome-webfont.eot b/_static/css/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000..e9f60ca Binary files /dev/null and b/_static/css/fonts/fontawesome-webfont.eot differ diff --git a/_static/css/fonts/fontawesome-webfont.svg b/_static/css/fonts/fontawesome-webfont.svg new file mode 100644 index 0000000..855c845 --- /dev/null +++ b/_static/css/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + diff --git a/_static/css/fonts/fontawesome-webfont.ttf b/_static/css/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000..35acda2 Binary files /dev/null and b/_static/css/fonts/fontawesome-webfont.ttf differ diff --git a/_static/css/fonts/fontawesome-webfont.woff b/_static/css/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000..400014a Binary files /dev/null and b/_static/css/fonts/fontawesome-webfont.woff differ diff --git a/_static/css/fonts/fontawesome-webfont.woff2 b/_static/css/fonts/fontawesome-webfont.woff2 new file mode 100644 index 0000000..4d13fc6 Binary files /dev/null and b/_static/css/fonts/fontawesome-webfont.woff2 differ diff --git a/_static/css/fonts/lato-bold-italic.woff b/_static/css/fonts/lato-bold-italic.woff new file mode 100644 index 0000000..88ad05b Binary files /dev/null and b/_static/css/fonts/lato-bold-italic.woff differ diff --git a/_static/css/fonts/lato-bold-italic.woff2 b/_static/css/fonts/lato-bold-italic.woff2 new file mode 100644 index 0000000..c4e3d80 Binary files /dev/null and b/_static/css/fonts/lato-bold-italic.woff2 differ diff --git a/_static/css/fonts/lato-bold.woff b/_static/css/fonts/lato-bold.woff new file mode 100644 index 0000000..c6dff51 Binary files /dev/null and b/_static/css/fonts/lato-bold.woff differ diff --git a/_static/css/fonts/lato-bold.woff2 b/_static/css/fonts/lato-bold.woff2 new file mode 100644 index 0000000..bb19504 Binary files /dev/null and b/_static/css/fonts/lato-bold.woff2 differ diff --git a/_static/css/fonts/lato-normal-italic.woff b/_static/css/fonts/lato-normal-italic.woff new file mode 100644 index 0000000..76114bc Binary files /dev/null and b/_static/css/fonts/lato-normal-italic.woff differ diff --git a/_static/css/fonts/lato-normal-italic.woff2 b/_static/css/fonts/lato-normal-italic.woff2 new file mode 100644 index 0000000..3404f37 Binary files /dev/null and b/_static/css/fonts/lato-normal-italic.woff2 differ diff --git a/_static/css/fonts/lato-normal.woff b/_static/css/fonts/lato-normal.woff new file mode 100644 index 0000000..ae1307f Binary files /dev/null and b/_static/css/fonts/lato-normal.woff differ diff --git a/_static/css/fonts/lato-normal.woff2 b/_static/css/fonts/lato-normal.woff2 new file mode 100644 index 0000000..3bf9843 Binary files /dev/null and b/_static/css/fonts/lato-normal.woff2 differ diff --git a/_static/css/theme.css b/_static/css/theme.css new file mode 100644 index 0000000..19a446a --- /dev/null +++ b/_static/css/theme.css @@ -0,0 +1,4 @@ +html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .eqno .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,.wy-menu-vertical li button.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .eqno .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,.wy-menu-vertical li button.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .eqno .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a button.pull-left.toctree-expand,.wy-menu-vertical li.on a button.pull-left.toctree-expand,.wy-menu-vertical li button.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .eqno .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a button.pull-right.toctree-expand,.wy-menu-vertical li.on a button.pull-right.toctree-expand,.wy-menu-vertical li button.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li button.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content .eqno .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content .eqno a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content p a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li a button.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content .eqno .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content p .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li button.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content .eqno .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a button.toctree-expand,.btn .wy-menu-vertical li.on a button.toctree-expand,.btn .wy-menu-vertical li button.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content .eqno .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a button.toctree-expand,.nav .wy-menu-vertical li.on a button.toctree-expand,.nav .wy-menu-vertical li button.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .eqno .btn .headerlink,.rst-content .eqno .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p .btn .headerlink,.rst-content p .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn button.toctree-expand,.wy-menu-vertical li.current>a .btn button.toctree-expand,.wy-menu-vertical li.current>a .nav button.toctree-expand,.wy-menu-vertical li .nav button.toctree-expand,.wy-menu-vertical li.on a .btn button.toctree-expand,.wy-menu-vertical li.on a .nav button.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .eqno .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li button.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .eqno .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li button.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .eqno .btn .fa-large.headerlink,.rst-content .eqno .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p .btn .fa-large.headerlink,.rst-content p .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn button.fa-large.toctree-expand,.wy-menu-vertical li .nav button.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .eqno .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li button.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .eqno .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li button.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .eqno .btn .fa-spin.headerlink,.rst-content .eqno .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p .btn .fa-spin.headerlink,.rst-content p .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn button.fa-spin.toctree-expand,.wy-menu-vertical li .nav button.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content .eqno .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li button.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content .eqno .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li button.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content .eqno .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li button.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content .eqno .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini button.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.rst-content section ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.rst-content section ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.rst-content section ul li p:last-child,.rst-content section ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.rst-content section ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.rst-content section ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.rst-content section ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content .section ol.arabic,.rst-content .toctree-wrapper ol,.rst-content .toctree-wrapper ol.arabic,.rst-content section ol,.rst-content section ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol.arabic li,.rst-content .section ol li,.rst-content .toctree-wrapper ol.arabic li,.rst-content .toctree-wrapper ol li,.rst-content section ol.arabic li,.rst-content section ol li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol.arabic li ul,.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content .toctree-wrapper ol.arabic li ul,.rst-content .toctree-wrapper ol li p:last-child,.rst-content .toctree-wrapper ol li ul,.rst-content section ol.arabic li ul,.rst-content section ol li p:last-child,.rst-content section ol li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol.arabic li ul li,.rst-content .section ol li ul li,.rst-content .toctree-wrapper ol.arabic li ul li,.rst-content .toctree-wrapper ol li ul li,.rst-content section ol.arabic li ul li,.rst-content section ol li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs>li{display:inline-block;padding-top:5px}.wy-breadcrumbs>li.wy-breadcrumbs-aside{float:right}.rst-content .wy-breadcrumbs>li code,.rst-content .wy-breadcrumbs>li tt,.wy-breadcrumbs>li .rst-content tt,.wy-breadcrumbs>li code{all:inherit;color:inherit}.breadcrumb-item:before{content:"/";color:#bbb;font-size:13px;padding:0 6px 0 3px}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li button.toctree-expand{display:block;float:left;margin-left:-1.2em;line-height:18px;color:#4d4d4d;border:none;background:none;padding:0}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover button.toctree-expand,.wy-menu-vertical li.on a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand{display:block;line-height:18px;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{padding:.4045em 1.618em .4045em 4.045em}.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{padding:.4045em 1.618em .4045em 5.663em}.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a{padding:.4045em 1.618em .4045em 7.281em}.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a{padding:.4045em 1.618em .4045em 8.899em}.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a{padding:.4045em 1.618em .4045em 10.517em}.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a{padding:.4045em 1.618em .4045em 12.135em}.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a{padding:.4045em 1.618em .4045em 13.753em}.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a{padding:.4045em 1.618em .4045em 15.371em}.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 1.618em .4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 button.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 button.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover button.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active button.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em;max-width:100%}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search>a:hover{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.version{margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .eqno .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content .eqno .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}.rst-content .toctree-wrapper>p.caption,.rst-content h1,.rst-content h2,.rst-content h3,.rst-content h4,.rst-content h5,.rst-content h6{margin-bottom:24px}.rst-content img{max-width:100%;height:auto}.rst-content div.figure,.rst-content figure{margin-bottom:24px}.rst-content div.figure .caption-text,.rst-content figure .caption-text{font-style:italic}.rst-content div.figure p:last-child.caption,.rst-content figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center,.rst-content figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img,.rst-content section>a>img,.rst-content section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp,.rst-content div.highlight span.linenos{user-select:none;pointer-events:none}.rst-content div.highlight span.linenos{display:inline-block;padding-left:0;padding-right:12px;margin-right:12px;border-right:1px solid #e6e9ea}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li,.rst-content .toctree-wrapper ol.loweralpha,.rst-content .toctree-wrapper ol.loweralpha>li,.rst-content section ol.loweralpha,.rst-content section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li,.rst-content .toctree-wrapper ol.upperalpha,.rst-content .toctree-wrapper ol.upperalpha>li,.rst-content section ol.upperalpha,.rst-content section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*,.rst-content .toctree-wrapper ol li>*,.rst-content .toctree-wrapper ul li>*,.rst-content section ol li>*,.rst-content section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child,.rst-content .toctree-wrapper ol li>:first-child,.rst-content .toctree-wrapper ul li>:first-child,.rst-content section ol li>:first-child,.rst-content section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child,.rst-content .toctree-wrapper ol li>p,.rst-content .toctree-wrapper ol li>p:last-child,.rst-content .toctree-wrapper ul li>p,.rst-content .toctree-wrapper ul li>p:last-child,.rst-content section ol li>p,.rst-content section ol li>p:last-child,.rst-content section ul li>p,.rst-content section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child,.rst-content .toctree-wrapper ol li>p:only-child,.rst-content .toctree-wrapper ol li>p:only-child:last-child,.rst-content .toctree-wrapper ul li>p:only-child,.rst-content .toctree-wrapper ul li>p:only-child:last-child,.rst-content section ol li>p:only-child,.rst-content section ol li>p:only-child:last-child,.rst-content section ul li>p:only-child,.rst-content section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul,.rst-content .toctree-wrapper ol li>ol,.rst-content .toctree-wrapper ol li>ul,.rst-content .toctree-wrapper ul li>ol,.rst-content .toctree-wrapper ul li>ul,.rst-content section ol li>ol,.rst-content section ol li>ul,.rst-content section ul li>ol,.rst-content section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul,.rst-content .toctree-wrapper ol.simple li>*,.rst-content .toctree-wrapper ol.simple li ol,.rst-content .toctree-wrapper ol.simple li ul,.rst-content .toctree-wrapper ul.simple li>*,.rst-content .toctree-wrapper ul.simple li ol,.rst-content .toctree-wrapper ul.simple li ul,.rst-content section ol.simple li>*,.rst-content section ol.simple li ol,.rst-content section ol.simple li ul,.rst-content section ul.simple li>*,.rst-content section ul.simple li ol,.rst-content section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink{opacity:0;font-size:14px;font-family:FontAwesome;margin-left:.5em}.rst-content .code-block-caption .headerlink:focus,.rst-content .code-block-caption:hover .headerlink,.rst-content .eqno .headerlink:focus,.rst-content .eqno:hover .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink:focus,.rst-content .toctree-wrapper>p.caption:hover .headerlink,.rst-content dl dt .headerlink:focus,.rst-content dl dt:hover .headerlink,.rst-content h1 .headerlink:focus,.rst-content h1:hover .headerlink,.rst-content h2 .headerlink:focus,.rst-content h2:hover .headerlink,.rst-content h3 .headerlink:focus,.rst-content h3:hover .headerlink,.rst-content h4 .headerlink:focus,.rst-content h4:hover .headerlink,.rst-content h5 .headerlink:focus,.rst-content h5:hover .headerlink,.rst-content h6 .headerlink:focus,.rst-content h6:hover .headerlink,.rst-content p.caption .headerlink:focus,.rst-content p.caption:hover .headerlink,.rst-content p .headerlink:focus,.rst-content p:hover .headerlink,.rst-content table>caption .headerlink:focus,.rst-content table>caption:hover .headerlink{opacity:1}.rst-content p a{overflow-wrap:anywhere}.rst-content .wy-table td p,.rst-content .wy-table td ul,.rst-content .wy-table th p,.rst-content .wy-table th ul,.rst-content table.docutils td p,.rst-content table.docutils td ul,.rst-content table.docutils th p,.rst-content table.docutils th ul,.rst-content table.field-list td p,.rst-content table.field-list td ul,.rst-content table.field-list th p,.rst-content table.field-list th ul{font-size:inherit}.rst-content .btn:focus{outline:2px solid}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .citation-reference>span.fn-bracket,.rst-content .footnote-reference>span.fn-bracket{display:none}.rst-content .hlist{width:100%}.rst-content dl dt span.classifier:before{content:" : "}.rst-content dl dt span.classifier-delimiter{display:none!important}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:auto minmax(80%,95%)}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{display:inline-grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{display:grid;grid-template-columns:auto auto minmax(.65rem,auto) minmax(40%,95%)}html.writer-html5 .rst-content aside.citation>span.label,html.writer-html5 .rst-content aside.footnote>span.label,html.writer-html5 .rst-content div.citation>span.label{grid-column-start:1;grid-column-end:2}html.writer-html5 .rst-content aside.citation>span.backrefs,html.writer-html5 .rst-content aside.footnote>span.backrefs,html.writer-html5 .rst-content div.citation>span.backrefs{grid-column-start:2;grid-column-end:3;grid-row-start:1;grid-row-end:3}html.writer-html5 .rst-content aside.citation>p,html.writer-html5 .rst-content aside.footnote>p,html.writer-html5 .rst-content div.citation>p{grid-column-start:4;grid-column-end:5}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{margin-bottom:24px}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.citation>dt>span.brackets:before,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.citation>dt>span.brackets:after,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a{word-break:keep-all}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a:not(:first-child):before,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.citation>dd p,html.writer-html5 .rst-content dl.footnote>dd p{font-size:.9rem}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{padding-left:1rem;padding-right:1rem;font-size:.9rem;line-height:1.2rem}html.writer-html5 .rst-content aside.citation p,html.writer-html5 .rst-content aside.footnote p,html.writer-html5 .rst-content div.citation p{font-size:.9rem;line-height:1.2rem;margin-bottom:12px}html.writer-html5 .rst-content aside.citation span.backrefs,html.writer-html5 .rst-content aside.footnote span.backrefs,html.writer-html5 .rst-content div.citation span.backrefs{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content aside.citation span.backrefs>a,html.writer-html5 .rst-content aside.footnote span.backrefs>a,html.writer-html5 .rst-content div.citation span.backrefs>a{word-break:keep-all}html.writer-html5 .rst-content aside.citation span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content aside.footnote span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content div.citation span.backrefs>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content aside.citation span.label,html.writer-html5 .rst-content aside.footnote span.label,html.writer-html5 .rst-content div.citation span.label{line-height:1.2rem}html.writer-html5 .rst-content aside.citation-list,html.writer-html5 .rst-content aside.footnote-list,html.writer-html5 .rst-content div.citation-list{margin-bottom:24px}html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content aside.footnote-list aside.footnote,html.writer-html5 .rst-content div.citation-list>div.citation,html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content aside.footnote-list aside.footnote code,html.writer-html5 .rst-content aside.footnote-list aside.footnote tt,html.writer-html5 .rst-content aside.footnote code,html.writer-html5 .rst-content aside.footnote tt,html.writer-html5 .rst-content div.citation-list>div.citation code,html.writer-html5 .rst-content div.citation-list>div.citation tt,html.writer-html5 .rst-content dl.citation code,html.writer-html5 .rst-content dl.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c;white-space:normal}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040;overflow-wrap:normal}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}.rst-content dl dd>ol:last-child,.rst-content dl dd>p:last-child,.rst-content dl dd>table:last-child,.rst-content dl dd>ul:last-child{margin-bottom:0}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px;max-width:100%}html.writer-html4 .rst-content dl:not(.docutils) .k,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .k{font-style:italic}html.writer-html4 .rst-content dl:not(.docutils) .descclassname,html.writer-html4 .rst-content dl:not(.docutils) .descname,html.writer-html4 .rst-content dl:not(.docutils) .sig-name,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .sig-name{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#000}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel,.rst-content .menuselection{font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .guilabel,.rst-content .menuselection{border:1px solid #7fbbe3;background:#e7f2fa}.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>.kbd,.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>kbd{color:inherit;font-size:80%;background-color:#fff;border:1px solid #a6a6a6;border-radius:4px;box-shadow:0 2px grey;padding:2.4px 6px;margin:auto 0}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block} \ No newline at end of file diff --git a/_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css b/_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css new file mode 100644 index 0000000..eb19f69 --- /dev/null +++ b/_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css @@ -0,0 +1 @@ +.sd-bg-primary{background-color:var(--sd-color-primary) !important}.sd-bg-text-primary{color:var(--sd-color-primary-text) !important}button.sd-bg-primary:focus,button.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}a.sd-bg-primary:focus,a.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}.sd-bg-secondary{background-color:var(--sd-color-secondary) !important}.sd-bg-text-secondary{color:var(--sd-color-secondary-text) !important}button.sd-bg-secondary:focus,button.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}a.sd-bg-secondary:focus,a.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}.sd-bg-success{background-color:var(--sd-color-success) !important}.sd-bg-text-success{color:var(--sd-color-success-text) !important}button.sd-bg-success:focus,button.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}a.sd-bg-success:focus,a.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}.sd-bg-info{background-color:var(--sd-color-info) !important}.sd-bg-text-info{color:var(--sd-color-info-text) !important}button.sd-bg-info:focus,button.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}a.sd-bg-info:focus,a.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}.sd-bg-warning{background-color:var(--sd-color-warning) !important}.sd-bg-text-warning{color:var(--sd-color-warning-text) !important}button.sd-bg-warning:focus,button.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}a.sd-bg-warning:focus,a.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}.sd-bg-danger{background-color:var(--sd-color-danger) !important}.sd-bg-text-danger{color:var(--sd-color-danger-text) !important}button.sd-bg-danger:focus,button.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}a.sd-bg-danger:focus,a.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}.sd-bg-light{background-color:var(--sd-color-light) !important}.sd-bg-text-light{color:var(--sd-color-light-text) !important}button.sd-bg-light:focus,button.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}a.sd-bg-light:focus,a.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}.sd-bg-muted{background-color:var(--sd-color-muted) !important}.sd-bg-text-muted{color:var(--sd-color-muted-text) !important}button.sd-bg-muted:focus,button.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}a.sd-bg-muted:focus,a.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}.sd-bg-dark{background-color:var(--sd-color-dark) !important}.sd-bg-text-dark{color:var(--sd-color-dark-text) !important}button.sd-bg-dark:focus,button.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}a.sd-bg-dark:focus,a.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}.sd-bg-black{background-color:var(--sd-color-black) !important}.sd-bg-text-black{color:var(--sd-color-black-text) !important}button.sd-bg-black:focus,button.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}a.sd-bg-black:focus,a.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}.sd-bg-white{background-color:var(--sd-color-white) !important}.sd-bg-text-white{color:var(--sd-color-white-text) !important}button.sd-bg-white:focus,button.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}a.sd-bg-white:focus,a.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}.sd-text-primary,.sd-text-primary>p{color:var(--sd-color-primary) !important}a.sd-text-primary:focus,a.sd-text-primary:hover{color:var(--sd-color-primary-highlight) !important}.sd-text-secondary,.sd-text-secondary>p{color:var(--sd-color-secondary) !important}a.sd-text-secondary:focus,a.sd-text-secondary:hover{color:var(--sd-color-secondary-highlight) !important}.sd-text-success,.sd-text-success>p{color:var(--sd-color-success) !important}a.sd-text-success:focus,a.sd-text-success:hover{color:var(--sd-color-success-highlight) !important}.sd-text-info,.sd-text-info>p{color:var(--sd-color-info) !important}a.sd-text-info:focus,a.sd-text-info:hover{color:var(--sd-color-info-highlight) !important}.sd-text-warning,.sd-text-warning>p{color:var(--sd-color-warning) !important}a.sd-text-warning:focus,a.sd-text-warning:hover{color:var(--sd-color-warning-highlight) !important}.sd-text-danger,.sd-text-danger>p{color:var(--sd-color-danger) !important}a.sd-text-danger:focus,a.sd-text-danger:hover{color:var(--sd-color-danger-highlight) !important}.sd-text-light,.sd-text-light>p{color:var(--sd-color-light) !important}a.sd-text-light:focus,a.sd-text-light:hover{color:var(--sd-color-light-highlight) !important}.sd-text-muted,.sd-text-muted>p{color:var(--sd-color-muted) !important}a.sd-text-muted:focus,a.sd-text-muted:hover{color:var(--sd-color-muted-highlight) !important}.sd-text-dark,.sd-text-dark>p{color:var(--sd-color-dark) !important}a.sd-text-dark:focus,a.sd-text-dark:hover{color:var(--sd-color-dark-highlight) !important}.sd-text-black,.sd-text-black>p{color:var(--sd-color-black) !important}a.sd-text-black:focus,a.sd-text-black:hover{color:var(--sd-color-black-highlight) !important}.sd-text-white,.sd-text-white>p{color:var(--sd-color-white) !important}a.sd-text-white:focus,a.sd-text-white:hover{color:var(--sd-color-white-highlight) !important}.sd-outline-primary{border-color:var(--sd-color-primary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-primary:focus,a.sd-outline-primary:hover{border-color:var(--sd-color-primary-highlight) !important}.sd-outline-secondary{border-color:var(--sd-color-secondary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-secondary:focus,a.sd-outline-secondary:hover{border-color:var(--sd-color-secondary-highlight) !important}.sd-outline-success{border-color:var(--sd-color-success) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-success:focus,a.sd-outline-success:hover{border-color:var(--sd-color-success-highlight) !important}.sd-outline-info{border-color:var(--sd-color-info) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-info:focus,a.sd-outline-info:hover{border-color:var(--sd-color-info-highlight) !important}.sd-outline-warning{border-color:var(--sd-color-warning) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-warning:focus,a.sd-outline-warning:hover{border-color:var(--sd-color-warning-highlight) !important}.sd-outline-danger{border-color:var(--sd-color-danger) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-danger:focus,a.sd-outline-danger:hover{border-color:var(--sd-color-danger-highlight) !important}.sd-outline-light{border-color:var(--sd-color-light) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-light:focus,a.sd-outline-light:hover{border-color:var(--sd-color-light-highlight) !important}.sd-outline-muted{border-color:var(--sd-color-muted) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-muted:focus,a.sd-outline-muted:hover{border-color:var(--sd-color-muted-highlight) !important}.sd-outline-dark{border-color:var(--sd-color-dark) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-dark:focus,a.sd-outline-dark:hover{border-color:var(--sd-color-dark-highlight) !important}.sd-outline-black{border-color:var(--sd-color-black) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-black:focus,a.sd-outline-black:hover{border-color:var(--sd-color-black-highlight) !important}.sd-outline-white{border-color:var(--sd-color-white) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-white:focus,a.sd-outline-white:hover{border-color:var(--sd-color-white-highlight) !important}.sd-bg-transparent{background-color:transparent !important}.sd-outline-transparent{border-color:transparent !important}.sd-text-transparent{color:transparent !important}.sd-p-0{padding:0 !important}.sd-pt-0,.sd-py-0{padding-top:0 !important}.sd-pr-0,.sd-px-0{padding-right:0 !important}.sd-pb-0,.sd-py-0{padding-bottom:0 !important}.sd-pl-0,.sd-px-0{padding-left:0 !important}.sd-p-1{padding:.25rem !important}.sd-pt-1,.sd-py-1{padding-top:.25rem !important}.sd-pr-1,.sd-px-1{padding-right:.25rem !important}.sd-pb-1,.sd-py-1{padding-bottom:.25rem !important}.sd-pl-1,.sd-px-1{padding-left:.25rem !important}.sd-p-2{padding:.5rem !important}.sd-pt-2,.sd-py-2{padding-top:.5rem !important}.sd-pr-2,.sd-px-2{padding-right:.5rem !important}.sd-pb-2,.sd-py-2{padding-bottom:.5rem !important}.sd-pl-2,.sd-px-2{padding-left:.5rem !important}.sd-p-3{padding:1rem !important}.sd-pt-3,.sd-py-3{padding-top:1rem !important}.sd-pr-3,.sd-px-3{padding-right:1rem !important}.sd-pb-3,.sd-py-3{padding-bottom:1rem !important}.sd-pl-3,.sd-px-3{padding-left:1rem !important}.sd-p-4{padding:1.5rem !important}.sd-pt-4,.sd-py-4{padding-top:1.5rem !important}.sd-pr-4,.sd-px-4{padding-right:1.5rem !important}.sd-pb-4,.sd-py-4{padding-bottom:1.5rem !important}.sd-pl-4,.sd-px-4{padding-left:1.5rem !important}.sd-p-5{padding:3rem !important}.sd-pt-5,.sd-py-5{padding-top:3rem !important}.sd-pr-5,.sd-px-5{padding-right:3rem !important}.sd-pb-5,.sd-py-5{padding-bottom:3rem !important}.sd-pl-5,.sd-px-5{padding-left:3rem !important}.sd-m-auto{margin:auto !important}.sd-mt-auto,.sd-my-auto{margin-top:auto !important}.sd-mr-auto,.sd-mx-auto{margin-right:auto !important}.sd-mb-auto,.sd-my-auto{margin-bottom:auto !important}.sd-ml-auto,.sd-mx-auto{margin-left:auto !important}.sd-m-0{margin:0 !important}.sd-mt-0,.sd-my-0{margin-top:0 !important}.sd-mr-0,.sd-mx-0{margin-right:0 !important}.sd-mb-0,.sd-my-0{margin-bottom:0 !important}.sd-ml-0,.sd-mx-0{margin-left:0 !important}.sd-m-1{margin:.25rem !important}.sd-mt-1,.sd-my-1{margin-top:.25rem !important}.sd-mr-1,.sd-mx-1{margin-right:.25rem !important}.sd-mb-1,.sd-my-1{margin-bottom:.25rem !important}.sd-ml-1,.sd-mx-1{margin-left:.25rem !important}.sd-m-2{margin:.5rem !important}.sd-mt-2,.sd-my-2{margin-top:.5rem !important}.sd-mr-2,.sd-mx-2{margin-right:.5rem !important}.sd-mb-2,.sd-my-2{margin-bottom:.5rem !important}.sd-ml-2,.sd-mx-2{margin-left:.5rem !important}.sd-m-3{margin:1rem !important}.sd-mt-3,.sd-my-3{margin-top:1rem !important}.sd-mr-3,.sd-mx-3{margin-right:1rem !important}.sd-mb-3,.sd-my-3{margin-bottom:1rem !important}.sd-ml-3,.sd-mx-3{margin-left:1rem !important}.sd-m-4{margin:1.5rem !important}.sd-mt-4,.sd-my-4{margin-top:1.5rem !important}.sd-mr-4,.sd-mx-4{margin-right:1.5rem !important}.sd-mb-4,.sd-my-4{margin-bottom:1.5rem !important}.sd-ml-4,.sd-mx-4{margin-left:1.5rem !important}.sd-m-5{margin:3rem !important}.sd-mt-5,.sd-my-5{margin-top:3rem !important}.sd-mr-5,.sd-mx-5{margin-right:3rem !important}.sd-mb-5,.sd-my-5{margin-bottom:3rem !important}.sd-ml-5,.sd-mx-5{margin-left:3rem !important}.sd-w-25{width:25% !important}.sd-w-50{width:50% !important}.sd-w-75{width:75% !important}.sd-w-100{width:100% !important}.sd-w-auto{width:auto !important}.sd-h-25{height:25% !important}.sd-h-50{height:50% !important}.sd-h-75{height:75% !important}.sd-h-100{height:100% !important}.sd-h-auto{height:auto !important}.sd-d-none{display:none !important}.sd-d-inline{display:inline !important}.sd-d-inline-block{display:inline-block !important}.sd-d-block{display:block !important}.sd-d-grid{display:grid !important}.sd-d-flex-row{display:-ms-flexbox !important;display:flex !important;flex-direction:row !important}.sd-d-flex-column{display:-ms-flexbox !important;display:flex !important;flex-direction:column !important}.sd-d-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}@media(min-width: 576px){.sd-d-sm-none{display:none !important}.sd-d-sm-inline{display:inline !important}.sd-d-sm-inline-block{display:inline-block !important}.sd-d-sm-block{display:block !important}.sd-d-sm-grid{display:grid !important}.sd-d-sm-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-sm-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 768px){.sd-d-md-none{display:none !important}.sd-d-md-inline{display:inline !important}.sd-d-md-inline-block{display:inline-block !important}.sd-d-md-block{display:block !important}.sd-d-md-grid{display:grid !important}.sd-d-md-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-md-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 992px){.sd-d-lg-none{display:none !important}.sd-d-lg-inline{display:inline !important}.sd-d-lg-inline-block{display:inline-block !important}.sd-d-lg-block{display:block !important}.sd-d-lg-grid{display:grid !important}.sd-d-lg-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-lg-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 1200px){.sd-d-xl-none{display:none !important}.sd-d-xl-inline{display:inline !important}.sd-d-xl-inline-block{display:inline-block !important}.sd-d-xl-block{display:block !important}.sd-d-xl-grid{display:grid !important}.sd-d-xl-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-xl-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}.sd-align-major-start{justify-content:flex-start !important}.sd-align-major-end{justify-content:flex-end !important}.sd-align-major-center{justify-content:center !important}.sd-align-major-justify{justify-content:space-between !important}.sd-align-major-spaced{justify-content:space-evenly !important}.sd-align-minor-start{align-items:flex-start !important}.sd-align-minor-end{align-items:flex-end !important}.sd-align-minor-center{align-items:center !important}.sd-align-minor-stretch{align-items:stretch !important}.sd-text-justify{text-align:justify !important}.sd-text-left{text-align:left !important}.sd-text-right{text-align:right !important}.sd-text-center{text-align:center !important}.sd-font-weight-light{font-weight:300 !important}.sd-font-weight-lighter{font-weight:lighter !important}.sd-font-weight-normal{font-weight:400 !important}.sd-font-weight-bold{font-weight:700 !important}.sd-font-weight-bolder{font-weight:bolder !important}.sd-font-italic{font-style:italic !important}.sd-text-decoration-none{text-decoration:none !important}.sd-text-lowercase{text-transform:lowercase !important}.sd-text-uppercase{text-transform:uppercase !important}.sd-text-capitalize{text-transform:capitalize !important}.sd-text-wrap{white-space:normal !important}.sd-text-nowrap{white-space:nowrap !important}.sd-text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sd-fs-1,.sd-fs-1>p{font-size:calc(1.375rem + 1.5vw) !important;line-height:unset !important}.sd-fs-2,.sd-fs-2>p{font-size:calc(1.325rem + 0.9vw) !important;line-height:unset !important}.sd-fs-3,.sd-fs-3>p{font-size:calc(1.3rem + 0.6vw) !important;line-height:unset !important}.sd-fs-4,.sd-fs-4>p{font-size:calc(1.275rem + 0.3vw) !important;line-height:unset !important}.sd-fs-5,.sd-fs-5>p{font-size:1.25rem !important;line-height:unset !important}.sd-fs-6,.sd-fs-6>p{font-size:1rem !important;line-height:unset !important}.sd-border-0{border:0 solid !important}.sd-border-top-0{border-top:0 solid !important}.sd-border-bottom-0{border-bottom:0 solid !important}.sd-border-right-0{border-right:0 solid !important}.sd-border-left-0{border-left:0 solid !important}.sd-border-1{border:1px solid !important}.sd-border-top-1{border-top:1px solid !important}.sd-border-bottom-1{border-bottom:1px solid !important}.sd-border-right-1{border-right:1px solid !important}.sd-border-left-1{border-left:1px solid !important}.sd-border-2{border:2px solid !important}.sd-border-top-2{border-top:2px solid !important}.sd-border-bottom-2{border-bottom:2px solid !important}.sd-border-right-2{border-right:2px solid !important}.sd-border-left-2{border-left:2px solid !important}.sd-border-3{border:3px solid !important}.sd-border-top-3{border-top:3px solid !important}.sd-border-bottom-3{border-bottom:3px solid !important}.sd-border-right-3{border-right:3px solid !important}.sd-border-left-3{border-left:3px solid !important}.sd-border-4{border:4px solid !important}.sd-border-top-4{border-top:4px solid !important}.sd-border-bottom-4{border-bottom:4px solid !important}.sd-border-right-4{border-right:4px solid !important}.sd-border-left-4{border-left:4px solid !important}.sd-border-5{border:5px solid !important}.sd-border-top-5{border-top:5px solid !important}.sd-border-bottom-5{border-bottom:5px solid !important}.sd-border-right-5{border-right:5px solid !important}.sd-border-left-5{border-left:5px solid !important}.sd-rounded-0{border-radius:0 !important}.sd-rounded-1{border-radius:.2rem !important}.sd-rounded-2{border-radius:.3rem !important}.sd-rounded-3{border-radius:.5rem !important}.sd-rounded-pill{border-radius:50rem !important}.sd-rounded-circle{border-radius:50% !important}.shadow-none{box-shadow:none !important}.sd-shadow-sm{box-shadow:0 .125rem .25rem var(--sd-color-shadow) !important}.sd-shadow-md{box-shadow:0 .5rem 1rem var(--sd-color-shadow) !important}.sd-shadow-lg{box-shadow:0 1rem 3rem var(--sd-color-shadow) !important}@keyframes sd-slide-from-left{0%{transform:translateX(-100%)}100%{transform:translateX(0)}}@keyframes sd-slide-from-right{0%{transform:translateX(200%)}100%{transform:translateX(0)}}@keyframes sd-grow100{0%{transform:scale(0);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50{0%{transform:scale(0.5);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50-rot20{0%{transform:scale(0.5) rotateZ(-20deg);opacity:.5}75%{transform:scale(1) rotateZ(5deg);opacity:1}95%{transform:scale(1) rotateZ(-1deg);opacity:1}100%{transform:scale(1) rotateZ(0);opacity:1}}.sd-animate-slide-from-left{animation:1s ease-out 0s 1 normal none running sd-slide-from-left}.sd-animate-slide-from-right{animation:1s ease-out 0s 1 normal none running sd-slide-from-right}.sd-animate-grow100{animation:1s ease-out 0s 1 normal none running sd-grow100}.sd-animate-grow50{animation:1s ease-out 0s 1 normal none running sd-grow50}.sd-animate-grow50-rot20{animation:1s ease-out 0s 1 normal none running sd-grow50-rot20}.sd-badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.sd-badge:empty{display:none}a.sd-badge{text-decoration:none}.sd-btn .sd-badge{position:relative;top:-1px}.sd-btn{background-color:transparent;border:1px solid transparent;border-radius:.25rem;cursor:pointer;display:inline-block;font-weight:400;font-size:1rem;line-height:1.5;padding:.375rem .75rem;text-align:center;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;vertical-align:middle;user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none}.sd-btn:hover{text-decoration:none}@media(prefers-reduced-motion: reduce){.sd-btn{transition:none}}.sd-btn-primary,.sd-btn-outline-primary:hover,.sd-btn-outline-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-primary:hover,.sd-btn-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary-highlight) !important;border-color:var(--sd-color-primary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-primary{color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary,.sd-btn-outline-secondary:hover,.sd-btn-outline-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary:hover,.sd-btn-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary-highlight) !important;border-color:var(--sd-color-secondary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-secondary{color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success,.sd-btn-outline-success:hover,.sd-btn-outline-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success:hover,.sd-btn-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success-highlight) !important;border-color:var(--sd-color-success-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-success{color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info,.sd-btn-outline-info:hover,.sd-btn-outline-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info:hover,.sd-btn-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info-highlight) !important;border-color:var(--sd-color-info-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-info{color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning,.sd-btn-outline-warning:hover,.sd-btn-outline-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning:hover,.sd-btn-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning-highlight) !important;border-color:var(--sd-color-warning-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-warning{color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger,.sd-btn-outline-danger:hover,.sd-btn-outline-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger:hover,.sd-btn-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger-highlight) !important;border-color:var(--sd-color-danger-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-danger{color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light,.sd-btn-outline-light:hover,.sd-btn-outline-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light:hover,.sd-btn-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light-highlight) !important;border-color:var(--sd-color-light-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-light{color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted,.sd-btn-outline-muted:hover,.sd-btn-outline-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted:hover,.sd-btn-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted-highlight) !important;border-color:var(--sd-color-muted-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-muted{color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark,.sd-btn-outline-dark:hover,.sd-btn-outline-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark:hover,.sd-btn-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark-highlight) !important;border-color:var(--sd-color-dark-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-dark{color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black,.sd-btn-outline-black:hover,.sd-btn-outline-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black:hover,.sd-btn-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black-highlight) !important;border-color:var(--sd-color-black-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-black{color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white,.sd-btn-outline-white:hover,.sd-btn-outline-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white:hover,.sd-btn-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white-highlight) !important;border-color:var(--sd-color-white-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-white{color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.sd-hide-link-text{font-size:0}.sd-octicon,.sd-material-icon{display:inline-block;fill:currentColor;vertical-align:middle}.sd-avatar-xs{border-radius:50%;object-fit:cover;object-position:center;width:1rem;height:1rem}.sd-avatar-sm{border-radius:50%;object-fit:cover;object-position:center;width:3rem;height:3rem}.sd-avatar-md{border-radius:50%;object-fit:cover;object-position:center;width:5rem;height:5rem}.sd-avatar-lg{border-radius:50%;object-fit:cover;object-position:center;width:7rem;height:7rem}.sd-avatar-xl{border-radius:50%;object-fit:cover;object-position:center;width:10rem;height:10rem}.sd-avatar-inherit{border-radius:50%;object-fit:cover;object-position:center;width:inherit;height:inherit}.sd-avatar-initial{border-radius:50%;object-fit:cover;object-position:center;width:initial;height:initial}.sd-card{background-clip:border-box;background-color:var(--sd-color-card-background);border:1px solid var(--sd-color-card-border);border-radius:.25rem;color:var(--sd-color-card-text);display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;position:relative;word-wrap:break-word}.sd-card>hr{margin-left:0;margin-right:0}.sd-card-hover:hover{border-color:var(--sd-color-card-border-hover);transform:scale(1.01)}.sd-card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem 1rem}.sd-card-title{margin-bottom:.5rem}.sd-card-subtitle{margin-top:-0.25rem;margin-bottom:0}.sd-card-text:last-child{margin-bottom:0}.sd-card-link:hover{text-decoration:none}.sd-card-link+.card-link{margin-left:1rem}.sd-card-header{padding:.5rem 1rem;margin-bottom:0;background-color:var(--sd-color-card-header);border-bottom:1px solid var(--sd-color-card-border)}.sd-card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.sd-card-footer{padding:.5rem 1rem;background-color:var(--sd-color-card-footer);border-top:1px solid var(--sd-color-card-border)}.sd-card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.sd-card-header-tabs{margin-right:-0.5rem;margin-bottom:-0.5rem;margin-left:-0.5rem;border-bottom:0}.sd-card-header-pills{margin-right:-0.5rem;margin-left:-0.5rem}.sd-card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom,.sd-card-img-top{width:100%}.sd-card-img,.sd-card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom{border-bottom-left-radius:calc(0.25rem - 1px);border-bottom-right-radius:calc(0.25rem - 1px)}.sd-cards-carousel{width:100%;display:flex;flex-wrap:nowrap;-ms-flex-direction:row;flex-direction:row;overflow-x:hidden;scroll-snap-type:x mandatory}.sd-cards-carousel.sd-show-scrollbar{overflow-x:auto}.sd-cards-carousel:hover,.sd-cards-carousel:focus{overflow-x:auto}.sd-cards-carousel>.sd-card{flex-shrink:0;scroll-snap-align:start}.sd-cards-carousel>.sd-card:not(:last-child){margin-right:3px}.sd-card-cols-1>.sd-card{width:90%}.sd-card-cols-2>.sd-card{width:45%}.sd-card-cols-3>.sd-card{width:30%}.sd-card-cols-4>.sd-card{width:22.5%}.sd-card-cols-5>.sd-card{width:18%}.sd-card-cols-6>.sd-card{width:15%}.sd-card-cols-7>.sd-card{width:12.8571428571%}.sd-card-cols-8>.sd-card{width:11.25%}.sd-card-cols-9>.sd-card{width:10%}.sd-card-cols-10>.sd-card{width:9%}.sd-card-cols-11>.sd-card{width:8.1818181818%}.sd-card-cols-12>.sd-card{width:7.5%}.sd-container,.sd-container-fluid,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container-xl{margin-left:auto;margin-right:auto;padding-left:var(--sd-gutter-x, 0.75rem);padding-right:var(--sd-gutter-x, 0.75rem);width:100%}@media(min-width: 576px){.sd-container-sm,.sd-container{max-width:540px}}@media(min-width: 768px){.sd-container-md,.sd-container-sm,.sd-container{max-width:720px}}@media(min-width: 992px){.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:960px}}@media(min-width: 1200px){.sd-container-xl,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:1140px}}.sd-row{--sd-gutter-x: 1.5rem;--sd-gutter-y: 0;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:calc(var(--sd-gutter-y) * -1);margin-right:calc(var(--sd-gutter-x) * -0.5);margin-left:calc(var(--sd-gutter-x) * -0.5)}.sd-row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--sd-gutter-x) * 0.5);padding-left:calc(var(--sd-gutter-x) * 0.5);margin-top:var(--sd-gutter-y)}.sd-col{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-auto>*{flex:0 0 auto;width:auto}.sd-row-cols-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}@media(min-width: 576px){.sd-col-sm{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-sm-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-sm-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-sm-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-sm-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-sm-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-sm-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-sm-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-sm-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-sm-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-sm-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-sm-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-sm-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-sm-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 768px){.sd-col-md{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-md-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-md-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-md-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-md-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-md-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-md-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-md-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-md-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-md-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-md-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-md-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-md-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-md-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 992px){.sd-col-lg{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-lg-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-lg-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-lg-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-lg-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-lg-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-lg-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-lg-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-lg-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-lg-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-lg-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-lg-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-lg-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-lg-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 1200px){.sd-col-xl{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-xl-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-xl-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-xl-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-xl-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-xl-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-xl-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-xl-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-xl-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-xl-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-xl-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-xl-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-xl-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-xl-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}.sd-col-auto{flex:0 0 auto;-ms-flex:0 0 auto;width:auto}.sd-col-1{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}.sd-col-2{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-col-3{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-col-4{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-col-5{flex:0 0 auto;-ms-flex:0 0 auto;width:41.6666666667%}.sd-col-6{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-col-7{flex:0 0 auto;-ms-flex:0 0 auto;width:58.3333333333%}.sd-col-8{flex:0 0 auto;-ms-flex:0 0 auto;width:66.6666666667%}.sd-col-9{flex:0 0 auto;-ms-flex:0 0 auto;width:75%}.sd-col-10{flex:0 0 auto;-ms-flex:0 0 auto;width:83.3333333333%}.sd-col-11{flex:0 0 auto;-ms-flex:0 0 auto;width:91.6666666667%}.sd-col-12{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-g-0,.sd-gy-0{--sd-gutter-y: 0}.sd-g-0,.sd-gx-0{--sd-gutter-x: 0}.sd-g-1,.sd-gy-1{--sd-gutter-y: 0.25rem}.sd-g-1,.sd-gx-1{--sd-gutter-x: 0.25rem}.sd-g-2,.sd-gy-2{--sd-gutter-y: 0.5rem}.sd-g-2,.sd-gx-2{--sd-gutter-x: 0.5rem}.sd-g-3,.sd-gy-3{--sd-gutter-y: 1rem}.sd-g-3,.sd-gx-3{--sd-gutter-x: 1rem}.sd-g-4,.sd-gy-4{--sd-gutter-y: 1.5rem}.sd-g-4,.sd-gx-4{--sd-gutter-x: 1.5rem}.sd-g-5,.sd-gy-5{--sd-gutter-y: 3rem}.sd-g-5,.sd-gx-5{--sd-gutter-x: 3rem}@media(min-width: 576px){.sd-col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-sm-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-sm-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-sm-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-sm-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-sm-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-sm-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-sm-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-sm-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-sm-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-sm-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-sm-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-sm-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-sm-0,.sd-gy-sm-0{--sd-gutter-y: 0}.sd-g-sm-0,.sd-gx-sm-0{--sd-gutter-x: 0}.sd-g-sm-1,.sd-gy-sm-1{--sd-gutter-y: 0.25rem}.sd-g-sm-1,.sd-gx-sm-1{--sd-gutter-x: 0.25rem}.sd-g-sm-2,.sd-gy-sm-2{--sd-gutter-y: 0.5rem}.sd-g-sm-2,.sd-gx-sm-2{--sd-gutter-x: 0.5rem}.sd-g-sm-3,.sd-gy-sm-3{--sd-gutter-y: 1rem}.sd-g-sm-3,.sd-gx-sm-3{--sd-gutter-x: 1rem}.sd-g-sm-4,.sd-gy-sm-4{--sd-gutter-y: 1.5rem}.sd-g-sm-4,.sd-gx-sm-4{--sd-gutter-x: 1.5rem}.sd-g-sm-5,.sd-gy-sm-5{--sd-gutter-y: 3rem}.sd-g-sm-5,.sd-gx-sm-5{--sd-gutter-x: 3rem}}@media(min-width: 768px){.sd-col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-md-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-md-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-md-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-md-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-md-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-md-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-md-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-md-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-md-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-md-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-md-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-md-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-md-0,.sd-gy-md-0{--sd-gutter-y: 0}.sd-g-md-0,.sd-gx-md-0{--sd-gutter-x: 0}.sd-g-md-1,.sd-gy-md-1{--sd-gutter-y: 0.25rem}.sd-g-md-1,.sd-gx-md-1{--sd-gutter-x: 0.25rem}.sd-g-md-2,.sd-gy-md-2{--sd-gutter-y: 0.5rem}.sd-g-md-2,.sd-gx-md-2{--sd-gutter-x: 0.5rem}.sd-g-md-3,.sd-gy-md-3{--sd-gutter-y: 1rem}.sd-g-md-3,.sd-gx-md-3{--sd-gutter-x: 1rem}.sd-g-md-4,.sd-gy-md-4{--sd-gutter-y: 1.5rem}.sd-g-md-4,.sd-gx-md-4{--sd-gutter-x: 1.5rem}.sd-g-md-5,.sd-gy-md-5{--sd-gutter-y: 3rem}.sd-g-md-5,.sd-gx-md-5{--sd-gutter-x: 3rem}}@media(min-width: 992px){.sd-col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-lg-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-lg-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-lg-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-lg-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-lg-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-lg-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-lg-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-lg-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-lg-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-lg-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-lg-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-lg-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-lg-0,.sd-gy-lg-0{--sd-gutter-y: 0}.sd-g-lg-0,.sd-gx-lg-0{--sd-gutter-x: 0}.sd-g-lg-1,.sd-gy-lg-1{--sd-gutter-y: 0.25rem}.sd-g-lg-1,.sd-gx-lg-1{--sd-gutter-x: 0.25rem}.sd-g-lg-2,.sd-gy-lg-2{--sd-gutter-y: 0.5rem}.sd-g-lg-2,.sd-gx-lg-2{--sd-gutter-x: 0.5rem}.sd-g-lg-3,.sd-gy-lg-3{--sd-gutter-y: 1rem}.sd-g-lg-3,.sd-gx-lg-3{--sd-gutter-x: 1rem}.sd-g-lg-4,.sd-gy-lg-4{--sd-gutter-y: 1.5rem}.sd-g-lg-4,.sd-gx-lg-4{--sd-gutter-x: 1.5rem}.sd-g-lg-5,.sd-gy-lg-5{--sd-gutter-y: 3rem}.sd-g-lg-5,.sd-gx-lg-5{--sd-gutter-x: 3rem}}@media(min-width: 1200px){.sd-col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-xl-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-xl-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-xl-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-xl-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-xl-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-xl-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-xl-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-xl-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-xl-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-xl-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-xl-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-xl-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-xl-0,.sd-gy-xl-0{--sd-gutter-y: 0}.sd-g-xl-0,.sd-gx-xl-0{--sd-gutter-x: 0}.sd-g-xl-1,.sd-gy-xl-1{--sd-gutter-y: 0.25rem}.sd-g-xl-1,.sd-gx-xl-1{--sd-gutter-x: 0.25rem}.sd-g-xl-2,.sd-gy-xl-2{--sd-gutter-y: 0.5rem}.sd-g-xl-2,.sd-gx-xl-2{--sd-gutter-x: 0.5rem}.sd-g-xl-3,.sd-gy-xl-3{--sd-gutter-y: 1rem}.sd-g-xl-3,.sd-gx-xl-3{--sd-gutter-x: 1rem}.sd-g-xl-4,.sd-gy-xl-4{--sd-gutter-y: 1.5rem}.sd-g-xl-4,.sd-gx-xl-4{--sd-gutter-x: 1.5rem}.sd-g-xl-5,.sd-gy-xl-5{--sd-gutter-y: 3rem}.sd-g-xl-5,.sd-gx-xl-5{--sd-gutter-x: 3rem}}.sd-flex-row-reverse{flex-direction:row-reverse !important}details.sd-dropdown{position:relative}details.sd-dropdown .sd-summary-title{font-weight:700;padding-right:3em !important;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}details.sd-dropdown:hover{cursor:pointer}details.sd-dropdown .sd-summary-content{cursor:default}details.sd-dropdown summary{list-style:none;padding:1em}details.sd-dropdown summary .sd-octicon.no-title{vertical-align:middle}details.sd-dropdown[open] summary .sd-octicon.no-title{visibility:hidden}details.sd-dropdown summary::-webkit-details-marker{display:none}details.sd-dropdown summary:focus{outline:none}details.sd-dropdown .sd-summary-icon{margin-right:.5em}details.sd-dropdown .sd-summary-icon svg{opacity:.8}details.sd-dropdown summary:hover .sd-summary-up svg,details.sd-dropdown summary:hover .sd-summary-down svg{opacity:1;transform:scale(1.1)}details.sd-dropdown .sd-summary-up svg,details.sd-dropdown .sd-summary-down svg{display:block;opacity:.6}details.sd-dropdown .sd-summary-up,details.sd-dropdown .sd-summary-down{pointer-events:none;position:absolute;right:1em;top:1em}details.sd-dropdown[open]>.sd-summary-title .sd-summary-down{visibility:hidden}details.sd-dropdown:not([open])>.sd-summary-title .sd-summary-up{visibility:hidden}details.sd-dropdown:not([open]).sd-card{border:none}details.sd-dropdown:not([open])>.sd-card-header{border:1px solid var(--sd-color-card-border);border-radius:.25rem}details.sd-dropdown.sd-fade-in[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out;animation:sd-fade-in .5s ease-in-out}details.sd-dropdown.sd-fade-in-slide-down[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out}.sd-col>.sd-dropdown{width:100%}.sd-summary-content>.sd-tab-set:first-child{margin-top:0}@keyframes sd-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes sd-slide-down{0%{transform:translate(0, -10px)}100%{transform:translate(0, 0)}}.sd-tab-set{border-radius:.125rem;display:flex;flex-wrap:wrap;margin:1em 0;position:relative}.sd-tab-set>input{opacity:0;position:absolute}.sd-tab-set>input:checked+label{border-color:var(--sd-color-tabs-underline-active);color:var(--sd-color-tabs-label-active)}.sd-tab-set>input:checked+label+.sd-tab-content{display:block}.sd-tab-set>input:not(:checked)+label:hover{color:var(--sd-color-tabs-label-hover);border-color:var(--sd-color-tabs-underline-hover)}.sd-tab-set>input:focus+label{outline-style:auto}.sd-tab-set>input:not(.focus-visible)+label{outline:none;-webkit-tap-highlight-color:transparent}.sd-tab-set>label{border-bottom:.125rem solid transparent;margin-bottom:0;color:var(--sd-color-tabs-label-inactive);border-color:var(--sd-color-tabs-underline-inactive);cursor:pointer;font-size:var(--sd-fontsize-tabs-label);font-weight:700;padding:1em 1.25em .5em;transition:color 250ms;width:auto;z-index:1}html .sd-tab-set>label:hover{color:var(--sd-color-tabs-label-active)}.sd-col>.sd-tab-set{width:100%}.sd-tab-content{box-shadow:0 -0.0625rem var(--sd-color-tabs-overline),0 .0625rem var(--sd-color-tabs-underline);display:none;order:99;padding-bottom:.75rem;padding-top:.75rem;width:100%}.sd-tab-content>:first-child{margin-top:0 !important}.sd-tab-content>:last-child{margin-bottom:0 !important}.sd-tab-content>.sd-tab-set{margin:0}.sd-sphinx-override,.sd-sphinx-override *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.sd-sphinx-override p{margin-top:0}:root{--sd-color-primary: #0071bc;--sd-color-secondary: #6c757d;--sd-color-success: #28a745;--sd-color-info: #17a2b8;--sd-color-warning: #f0b37e;--sd-color-danger: #dc3545;--sd-color-light: #f8f9fa;--sd-color-muted: #6c757d;--sd-color-dark: #212529;--sd-color-black: black;--sd-color-white: white;--sd-color-primary-highlight: #0060a0;--sd-color-secondary-highlight: #5c636a;--sd-color-success-highlight: #228e3b;--sd-color-info-highlight: #148a9c;--sd-color-warning-highlight: #cc986b;--sd-color-danger-highlight: #bb2d3b;--sd-color-light-highlight: #d3d4d5;--sd-color-muted-highlight: #5c636a;--sd-color-dark-highlight: #1c1f23;--sd-color-black-highlight: black;--sd-color-white-highlight: #d9d9d9;--sd-color-primary-text: #fff;--sd-color-secondary-text: #fff;--sd-color-success-text: #fff;--sd-color-info-text: #fff;--sd-color-warning-text: #212529;--sd-color-danger-text: #fff;--sd-color-light-text: #212529;--sd-color-muted-text: #fff;--sd-color-dark-text: #fff;--sd-color-black-text: #fff;--sd-color-white-text: #212529;--sd-color-shadow: rgba(0, 0, 0, 0.15);--sd-color-card-border: rgba(0, 0, 0, 0.125);--sd-color-card-border-hover: hsla(231, 99%, 66%, 1);--sd-color-card-background: transparent;--sd-color-card-text: inherit;--sd-color-card-header: transparent;--sd-color-card-footer: transparent;--sd-color-tabs-label-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-hover: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-inactive: hsl(0, 0%, 66%);--sd-color-tabs-underline-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-underline-hover: rgba(178, 206, 245, 0.62);--sd-color-tabs-underline-inactive: transparent;--sd-color-tabs-overline: rgb(222, 222, 222);--sd-color-tabs-underline: rgb(222, 222, 222);--sd-fontsize-tabs-label: 1rem} diff --git a/_static/design-tabs.js b/_static/design-tabs.js new file mode 100644 index 0000000..36b38cf --- /dev/null +++ b/_static/design-tabs.js @@ -0,0 +1,27 @@ +var sd_labels_by_text = {}; + +function ready() { + const li = document.getElementsByClassName("sd-tab-label"); + for (const label of li) { + syncId = label.getAttribute("data-sync-id"); + if (syncId) { + label.onclick = onLabelClick; + if (!sd_labels_by_text[syncId]) { + sd_labels_by_text[syncId] = []; + } + sd_labels_by_text[syncId].push(label); + } + } +} + +function onLabelClick() { + // Activate other inputs with the same sync id. + syncId = this.getAttribute("data-sync-id"); + for (label of sd_labels_by_text[syncId]) { + if (label === this) continue; + label.previousElementSibling.checked = true; + } + window.localStorage.setItem("sphinx-design-last-tab", syncId); +} + +document.addEventListener("DOMContentLoaded", ready, false); diff --git a/_static/doctools.js b/_static/doctools.js new file mode 100644 index 0000000..d06a71d --- /dev/null +++ b/_static/doctools.js @@ -0,0 +1,156 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Base JavaScript utilities for all Sphinx HTML documentation. + * + * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/_static/documentation_options.js b/_static/documentation_options.js new file mode 100644 index 0000000..f45177f --- /dev/null +++ b/_static/documentation_options.js @@ -0,0 +1,13 @@ +const DOCUMENTATION_OPTIONS = { + VERSION: '0.4.1.dev15+g326e192', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/_static/file.png b/_static/file.png new file mode 100644 index 0000000..a858a41 Binary files /dev/null and b/_static/file.png differ diff --git a/_static/generation_time_templating.svg b/_static/generation_time_templating.svg new file mode 100644 index 0000000..250435a --- /dev/null +++ b/_static/generation_time_templating.svg @@ -0,0 +1,16 @@ + diff --git a/_static/html_logo.png b/_static/html_logo.png new file mode 100644 index 0000000..6744b5e Binary files /dev/null and b/_static/html_logo.png differ diff --git a/_static/icon.pdn b/_static/icon.pdn new file mode 100644 index 0000000..b71e0ff Binary files /dev/null and b/_static/icon.pdn differ diff --git a/_static/icon.png b/_static/icon.png new file mode 100644 index 0000000..4f1f3ef Binary files /dev/null and b/_static/icon.png differ diff --git a/_static/jquery.js b/_static/jquery.js new file mode 100644 index 0000000..c4c6022 --- /dev/null +++ b/_static/jquery.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0' + + '' + + _("Hide Search Matches") + + "
" + ) + ); + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + localStorage.removeItem("sphinx_highlight_terms") + }, + + initEscapeListener: () => { + // only install a listener if it is really needed + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; + if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { + SphinxHighlight.hideSearchWords(); + event.preventDefault(); + } + }); + }, +}; + +_ready(() => { + /* Do not call highlightSearchWords() when we are on the search page. + * It will highlight words from the *previous* search query. + */ + if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); + SphinxHighlight.initEscapeListener(); +}); diff --git a/_static/sphinx_rtd_theme_overrides.css b/_static/sphinx_rtd_theme_overrides.css new file mode 100644 index 0000000..34f124f --- /dev/null +++ b/_static/sphinx_rtd_theme_overrides.css @@ -0,0 +1,18 @@ +/* This line is theme specific - it includes the base theme CSS */ +@import 'css/theme.css'; /* for the Read the Docs theme */ + +/* Disable page width limit */ +.wy-nav-content { + max-width: None +} + +/* Make the version text darker */ +.wy-side-nav-search>div.version { + color:hsla(0, 0%, 0%, 0.7) +} + +/*Fix for Sphinx 5.0+ prints double colons for roles: https://github.com/sphinx-doc/sphinx/issues/10594*/ +/*See also: https://stackoverflow.com/questions/70426337/sphinx-documentation-showing-double-colon-in-fields*/ +html.writer-html5 .rst-content dl.field-list>dt:after { + content: "" +} diff --git a/changelog.html b/changelog.html new file mode 100644 index 0000000..ca30c0d --- /dev/null +++ b/changelog.html @@ -0,0 +1,381 @@ + + + + + + +todo
+If you think you found a bug, please provide following information when contacting us:
++++
+- +
Your operating system name and version
- +
Infos about your Pharaoh installation:
+Open a terminal with your Pharaoh env loaded (e.g. in PyCharm) and type +
+pharaoh info
orpython -m pharaoh info
.That should return something like
+Pharaoh vX.Y.Z [Python 3.9.13, Windows-10-10.0.19045-SP0]
.- +
Any details about your local setup that might be helpful in troubleshooting
- +
The complete error message and traceback (not just a screenshot of parts of the error)
- +
Log files
- +
Detailed steps to reproduce the bug
- +
Maybe also a list of packages in your Python env:
pip freeze > pharaoh_env_packages.txt
todo
++ |
|
+
+ | + |
+ |
+ | + |
+ |
+ | + |
+ | + |
+ | + |
+ | + |
+ | + |
+ |
|
+
+ | + |
+ | + |
+ | + |
+ | + |
+ |
Pharaoh is a Sphinx-based Python framework for +generating reports in various format (HTML, Confluence, PDF) by combining the power of configurable +Jinja templates and Python scripts for asset (tables, plots, pictures, etc…) generation.
+Any of the following Python versions are compatible. +If you don’t have any installed, please download and execute a version by clicking the links below.
++++
+- +
Python 3.11 (recommended) +[Download Python 3.11.4]
- +
Python 3.10 +[Download Python 3.10.11]
- +
Python 3.9 (min. 3.9.2) +[Download Python 3.9.13]
A virtual environment has its own Python binary (which matches the version of the binary that was +used to create this environment) and can have its own independent set of installed Python packages +in its site directories.
+The easiest way to create a virtual environment, is to let the IDE PyCharm (recommended) create one for you. +Configure a virtual environment shows how to do that.
+The other option is to create it via command line like follows and then configure PyCharm to use this as +project interpreter.
+See also
+ +Open a shell (e.g. an Inicio shell or PowerShell)
Change into your workspace: cd C:\temp
Create a virtual environment: C:\Python311\python.exe -m venv pharaoh_venv
Activate it:
+Powershell .\pharaoh_venv\Scripts\Activate.ps1
Bash: source ./pharaoh_venv/Scripts/activate
Cmd/Bat: .\pharaoh_venv\Scripts\activate.bat
You should be getting a prefix to your command line like this: (pharaoh_venv) PS C:\temp>
Ensure you use binary packages where possible by installing wheel first pip install wheel
You can find more information about virtual environments here.
+After the virtual environment has been created, activate it and install ifx-pharaoh via pip:
+pip install pharaoh-report[<extras>]
+
Note
+Refer to section Extras to install additional dependencies.
+Note
+For updating Pharaoh to the latest version, append the -U
flag to the pip command
Then you would be able to run:
+pip install pharaoh-report[<extras>]
+
The extras
field is a comma-separated list of additional dependency groups.
Following extra dependencies may be installed, depending on your use case:
+bokeh
: Installs the Bokeh plotting framework and exporters
holoview
: Installs the Holoviews plotting framework
plotly
: Installs the Plotly plotting framework and exporters
matplotlib
: Installs the Matplotlib plotting framework
jupyter
: Installs Jupyter for asset script implementation
pandas
: Installs Pandas Data Analysis Library
pdf
: Installs an SVG to PDF converter for the LaTeX builder
all-plotting
: Installs all supported plotting frameworks
Pharaoh is a Sphinx-based Python framework for +generating reports in various formats (HTML, Confluence, PDF) by combining the power of configurable +Jinja templates and Python scripts for asset (tables, plots, pictures, etc…) generation.
+Click the PowerPoint icon to download the latest presentation slides:
+++
See also
+Demo Reports (might not reflect latest development version)
+Pharaoh mainly serves two user groups:
+They are creating pre-defined configurable templates & data post-processing methods and for your project and +therefore provide a simplification layer for the end user.
+They will build reports based on the templates the template designers created. +They provide the final content (assets like plots, table, etc…) and configuration +(context to fill template variables) for the templates.
+The standard Pharaoh workflow consists of following major steps:
+Project Generation
+Generates a Pharaoh project with one or multiple components based on predefined templates. +Its core is a fully-preconfigured Sphinx documentation project.
+This generation is called first-level templating or generation-time templating +further on in the documentation.
+It may be checked into GIT for incremental modifications or generated each time, depending on your project setup.
+Now the templates and asset scripts in the generated components as well as the project settings +can be modified by the user.
+Asset Generation
+During asset generation, the asset scripts of each component are executed.
+By using the resources files of its own and/or other components, +asset scripts are producing dynamic content assets for the later report, +like plots, images, tables and additional data (e.g. json) used for rendering the templates.
+Those assets are then registered with metadata, so that they can be searched/included by the templates.
+Each asset script may produce any amount of assets.
+Project Build
+Translates an existing Pharaoh project to a configured target format (e.g. HTML, Confluence, LaTeX), +while performing an additional templating step via Jinja.
+This is called second-level templating or build-time templating further on in the documentation.
+Templates may inherit and extend existing or user-defined base-templates (for re-use purposes).
+Templates may dynamically include other templates.
+Templates are rendered using a rendering context (variables that can be used in template), +that contains following data:
+Project Settings
Static/manually entered context data
Dynamic context data, that is created by executing a Python script (potentially accessing generated assets)
See also
+ ++ p | ||
+ |
+ pharaoh | + |
+ |
+ pharaoh.api | + |
+ |
+ pharaoh.assetlib.api | + |
Lists all items that can be imported from pharaoh.api
.
This module contains Pharaoh project-related API functions.
+Returns an instance of PharaohProject.
+lookup_path – If None, the function tries to return the Pharaoh singleton instance that is already loaded in
+memory and raises and exception if there is none.
+If given a string or Path instance, searches the given path and all its parent folders
+for a pharaoh.yaml
project file and returns a PharaohProject instance.
Instantiates a Pharaoh project instance using either an existing project root +(directory containing pharaoh.yaml) or creates a new project.
+project_root – A directory containing a pharaoh.yaml file
overwrite – If the project directory already contains files, overwrite them.
templates – The project template(s) to use for creating the Sphinx project files. +Maybe be any number of template names or paths to directories.
template_context – The Jinja rendering context for the selected project templates.
custom_settings – A path to a YAML file containing settings to overwrite the default project settings.
Adds a new component to the Pharaoh project
+Example:
+from pharaoh.api import FileResource, PharaohProject
+
+proj = PharaohProject(".")
+proj.add_component(
+ component_name="component_ABC",
+ templates=[
+ "plugin_abc.template_xyz", # plugin template
+ "path/to/template/directory", # template directory
+ "path/to/template/file/tmpl.pharaoh.py" # template file
+ ],
+ render_context={"foo": "bar"},
+ resources=[FileResource(alias="dlh5_result", pattern="C:/temp/**/*.dlh5")],
+ metadata={"some tag": "some value"},
+ index=-1, # append
+ overwrite=False
+)
+
component_name – The name of the component. Must be a valid Python identifier.
templates –
A list of component templates to use for creating the components project files.
+Those may be the template identifier (e.g. plugin_abc.template_xyz
) of a registered plugin template,
+a path to a template directory or a path to a template file (single-file template).
Since multiple template may be specified, their order matters in cases where different templates +create equally-named files, thus templates might overwrite files of the previous templates.
+This enables template-composition, where template designers can chunk their bigger templates into +smaller re-usable building blocks.
+If omitted, an empty default template is used.
+render_context – The Jinja rendering context for the selected template.
+The actual template rendering context will have component_name
, resources
and
+metadata
available under the respective keys.
resources – A list of Resource instances, defining the component’s resources used in asset scripts
metadata – A dictionary of metadata that may be used to find the component via
+PharaohProject.find_component()
method.
overwrite – If True, an already existing component will be overwritten
Adds additional templates to an existing component, that may overwrite existing files during rendering.
+component_name – The name of the component. Must be a valid Python identifier.
templates – The component template(s) to use for creating the components project files. +Maybe be the a single or multiple template names or paths to a directory.
render_context – The Jinja rendering context for the selected template. +The actual template rendering context will have component_name, resources and metadata +available under the respective keys.
Create an archive from the build folder.
+dest – A destination path to create the archive. Relative paths are relative to the project root.
+If omitted, the filename will be taken from the report.archive_name
setting.
The path to the archive
+Builds the Sphinx project and returns the status code.
+catch_errors – If True, Sphinx build errors will not raise an exception but return a -1 instead.
+Find components by their metadata using an evaluated expression.
+The expression must be a valid Python expression and following local variables may be used:
+name: The name of the component
templates: A list of templates used to render the component
metadata: A dict of metadata specified
render_context: The Jinja rendering context specified
resources: A list of resource definitions
expression –
A Python expression that will be evaluated. If it evaluates to a truthy result, +the component name is included in the returned list.
+Example: name == "dummy" and metadata.foo in (1,2,3)
.
A failing evaluation will be treated as False. An empty expression will always match.
+ +Generate all assets by executing the asset scripts of a selected or all components.
+All asset scripts are executed in separate parallel child processes
+(number of workers determined by asset_gen.worker_processes
setting;
+setting 0 executes all asset scripts sequentially in the current process).
Setting asset_gen.script_ignore_pattern
determines if a script is ignored.
Putting the comment # pharaoh: ignore
at the start of a script will also ignore the file.
component_filters – A list of regular expressions that are matched against each component name. +If a component name matches any of the regular expressions, the component’s +assets are regenerated (containing directory will be cleared)
+Provides Sphinx project configurations, that will be dynamically included by the generated conf.py.
+Finds a Resource from a project component by its alias.
+component – The component’s name
alias – The resource’s alias
Gets a setting by its dot-separated name, e.g. “core.debug”.
+The settings are preferably taken from environment variables (e.g. PHARAOH.CORE.DEBUG), whereas the values +must be TOML compatible values (e.g. “true” for boolean).
+If no environment variable is found, the user defined settings in the pharaoh.yaml file in the +project root will be used. If the setting is not specified in there or the file is not existing, +the Pharaoh default settings are queried.
+If the setting is not found, a LookupError is returned unless “default” is set.
+Examples:
+proj.get_setting("report.title")
+proj.get_setting("cloud.metadata", to_container=True, resolve=False) == {
+ "author": "${report.author}",
+ "title": "${report.title}",
+}
+proj.get_setting("cloud.metadata", to_container=True, resolve=True) == {
+ "author": "loibljoh",
+ "title": "Pharaoh Report",
+}
+proj.get_setting("cloud.metadata.title") == "Pharaoh Report"
+proj.get_setting("cloud.metadata") == omegaconf.dictconfig.DictConfig(...)
+
key – Dot-separated name of the setting value to return
default – The returned default value if the key does not exist.
to_container – If set, recursively converts an OmegaConf config to a primitive Python container type +(dict or list).
resolve – True to resolve all values if “to_container” is set to True
Returns an iterator over all components from a project.
+Returns an iterator over all resources from a project component.
+component – The component’s name
+Loads settings from various namespaces.
+The default setting in the Pharaoh library
+The project settings that may be modified by the user
+The settings defined by environment variables starting with PHARAO
+Loads all of the above
+namespace – The namespace to load settings from. all, default, project or env.
+Sets a setting by its dot-separated name, e.g. “core.debug”.
+This has no effect, if there is an environment variable defining the same setting!
+Removes one or multiple existing components.
+filter – A case-insensitive component filter. Either a full-match or regular expression, depending on +regex argument.
regex – If True, the filter argument will be treated as regular expression. Components +that partially match the regular expression are removed.
A list of component names that got removed
+Saves back the project settings to the project YAML file.
+include_env – If True, Pharaoh settings that are set via environment variables will be persisted +to the project settings YAML file.
+Lists all items that can be imported from pharaoh.assetlib.api
.
This module contains API functions related to asset generation. +When asset scripts are accessing those API functions, the functions can access the project information by themselves.
+Find components by their metadata using an evaluated expression.
+The expression must be a valid Python expression and following local variables may be used:
+name: The name of the component
templates: A list of templates used to render the component
metadata: A dict of metadata specified
render_context: The Jinja rendering context specified
resources: A list of resource definitions
expression –
A Python expression that will be evaluated. If it evaluates to a truthy result, +the component name is included in the returned list.
+Example: name == "dummy" and metadata.foo in (1,2,3)
.
A failing evaluation will be treated as False. An empty expression will always match.
+ +Returns the AssetFinder
instance for the current project
If executed from within a script that is placed inside a Pharaoh component, the function returns the components +name by analyzing the call stack.
+Get a Resource instance by its component name and resource alias.
+Returns a context manager that adds a new stack entry with custom metadata, +effectively updating the metadata context assets are exported with.
+When the context manager is left, the stack entry is removed again.
+See Metadata Stack docs.
+context_name – An optional name for the context in case it must be looked up.
context – Keywords arguments to specify custom metadata.
Register an asset manually. The file will be copied (if data is None and ‘file’ is a real file) or +written (if data is given) to the asset build folder of the current Pharaoh project.
+file – The filename. Must exist even if data is set, to have a filename to store the asset and to +automatically determine the template (if not set via template argument).
metadata – Additional metadata to store on the asset.
template –
The template used to render the asset. If omitted, it is inferred by the file extension.
+See also
+ +data – An io.BytesIO instance. Used if the asset is generated in memory and should be stored to disk by +this function.
copy2build –
If True, the asset will be copied to the asset build directory, +even if not referenced in the template.
+Background: Pharaoh stores all assets in the project directory and copies them to the build directory only if +copy2build is set to True or on-demand by Pharaoh. For example if an HTML file is rendered using an iframe, +the HTML file has to be copied to the build folder where the iframe can later include it.
+The file path where the asset will be actually stored
+Register a data context for the build-time templating stage. +The data may be given directly as dict/list or via a json or yaml file.
+This function is designed to be used within asset scripts, to easily register data you extract from resources +for the templating process.
+Example:
+from pharaoh.assetlib.api import register_templating_context
+
+register_templating_context(name="foo", context={"bar": "baz"})
+# will be accessed like this: {{ ctx.local.foo.bar.baz }}
+
name –
The name under which the data context is available inside Jinja templates. +Access like this (name: mycontext):
+{% set mycontext = ctx.local.mycontext %}
+
context – Either a str or Path
instance pointing to a json or yaml
+file, or a dict or list. All data must contain only json-compatible types, otherwise the data cannot be stored.
metadata – The given context will be internally registered as an asset with following metadata:
+dict(pharaoh_templating_context=name, **metadata)
kwargs – Keyword arguments that are mostly (except component
) passed to json.dumps(...)
, in case
+context
is a dict or list.
A class for discovering and searching generated assets.
+An instance of this class will be created by the Pharaoh project, where lookup_path
will be set to
+report_project/.asset_build
.
lookup_path – The root directory to look for assets. It will be searched recursively for assets.
+Discovers all assets by recursively searching for *.assetinfo
files and stores
+the collection as instance variable (_assets).
components – A list of components to search for assets. +If None (the default), all components will be searched.
+A dictionary that maps component names to a list of Asset
instances.
Returns the corresponding Asset
instance for a certain ID.
id – The ID of the asset to return
+An Asset
instance if found, None otherwise.
Iterates over all discovered assets.
+components – A list of component names to search. If None (the default), all components will be searched.
+An iterator over all discovered assets.
+Searches already discovered assets (see discover_assets()
) that match a condition.
condition –
A Python expression that is evaluated using the content of the *.assetinfo
JSON file
+as namespace. If the evaluation returns a truthy result, the asset is returned.
Refer to this example assetinfo file to see the available default namespace.
+Example:
+# All HTML file where the "label" metadata ends with "_plot"
+finder.search_assets('asset.suffix == ".html" and label.endswith("_plot")')
+
components – A list of component names to search. If None (the default), all components will be searched.
A list of assets whose metadata match the condition.
+Holds information about a generated asset.
+id – An MD5 hash of the asset’s filename, prefixed with “__ID__”. +Since a unique suffix is included in the filename, this ID hash is also unique. +Can be used to quickly find this Asset instance.
infofile (Path) – Absolute path to the *.assetinfo
file
assetfile (Path) – Absolute path to the actual asset file
context (omegaconf.DictConfig) – The content of infofile parsed into a OmegaConf dict.
Groups an iterable of Assets by a certain metadata key.
+During build-time rendering this function will be available as Jinja global function
+asset_groupby
and alias agroupby
.
Example:
+We have following 4 assets (simplified notation of specified metadata):
+Asset[a="1", b="3"]
+Asset[a="1", c="4"]
+Asset[a="2", b="3"]
+Asset[a="2", c="4"]
+
+Grouping by "a":
+ asset_groupby(assets, "a")
+will yield
+ {
+ "1": [Asset[a="1", b="3"], Asset[a="1", c="4"]],
+ "2": [Asset[a="2", b="3"], Asset[a="2", c="4"]],
+ }
+
+
+Grouping by "b" and default "default":
+ asset_groupby(assets, "b", default="default")
+will yield
+ {
+ "3": [Asset[a="1", b="3"], Asset[a="2", b="3"]],
+ "default": [Asset[a="1", c="4"], Asset[a="2", c="4"]],
+ }
+
seq – The iterable of assets to group
key – The nested attribute to use for grouping, e.g. “A.B.C”
sort_reverse – Reverse-sort the keys in the returned dictionary
default – Sort each item, where “key” is not an existing attribute, into this default group
A dictionary that maps the group names (values of A.B.C) to a list of items out of the input iterable
+Bases: LocalResource
A resource that matches files/directories using a wildcard pattern.
+Example:
+FileResource(alias="mycsvfile", pattern="C:/temp/*.csv", sort="ascending")
+
pattern – A pathname pattern. +The pattern may contain simple shell-style wildcards. +Filenames starting with a dot are special cases that are not matched by ‘*’ and ‘?’ patterns.
sort –
descending (default) or ascending. +Sorting is done using the natsort library to sort filesystem paths naturally. Example:
+0.txt, 01.txt, 1.txt, 10.txt, 2.txt # default sorting (ascending)
+0.txt, 01.txt, 1.txt, 2.txt, 10.txt # natural sorting (ascending)
+
Returns the first match for the file pattern, depending on the chosen sort order.
+Returns all matches for the file pattern, depending on the chosen sort order.
+recursive – If recursive is true, the pattern ‘**’ will match any files and +zero or more directories and subdirectories.
+Returns the n-th match for the file pattern, depending on the chosen sort order.
+Bases: Resource
A resource that can hold arbitrary information. It may be used as a temporary replacement for +more specific resources that are not yet implemented. +For example, it could hold database connection properties or other custom data.
+Example:
+CustomResource(alias="foo", traits=dict(a=1, b=[2, 3], c=dict(d=[4, 5])))
+
traits – A dict with arbitrary data.
+Bases:
++++
+ +- +class Resource(alias: str, *, cachedir: str = '')[source]
+Bases:
+object
The base class for all resources. Provides serialization/deserialization methods.
++
+ + + +- +class LocalResource(alias: str, *, cachedir: str = '')[source]
+- +
Represents a resource that is located on the user’s hard-drive.
+ + +
Matlab engine for Pharaoh asset generation.
+Usage:
+from pharaoh.assetlib.api import Matlab
+
+eng = Matlab()
+with eng:
+ out, err = eng.execute_script("myscript.m")
+ result, out, err = eng.execute_function("myfunc", [800.0], nargout=1)
+
Matlab engine for Pharaoh asset generation.
+start_options – See options at https://de.mathworks.com/help/matlab/ref/matlabwindows.html
+Connects to a Matlab engine. If an engine is running it will be connected, otherwise a new Matlab instance +will be started and connected.
+Returns the matlab engine instance
+Executes a Matlab function
+Returns a tuple of result, stdout stream and stderr stream of the function execution.
+The shape/type of result depends on the argument ‘nargout’. See below.
+function_name –
The name of the function to execute.
+The function must be in the Matlab path to execute it.
+Alternatively the workdir argument can be set to the parent directory of the function.
+args – A list of positional input arguments to the function
nargout –
The number of output arguments.
+If 0, the result return value of this function will be None.
+If 1, the result return value will be a single value.
+If greater than 1, the result return value will be a tuple containing nargout values.
+workdir – The Matlab working directory to be changed to during function execution. Skipped if None.
Following globals, filters and tests are available during build-time templating.
+All Jinja2 builtins are available of course:
+ +Pharaoh extends the template environment by additional globals and filters, which are described in following chapters.
+Following functions are available in the global templating scope, and can be used like variable, but all callable, e.g.
+{{ h1("Title") }}
.
Wrapper for Python’s assert builtin.
+statement – A boolean statement
message – The message for the AssertionError, if statement is False.
Renders a heading for a certain level.
+Shortcuts for this function are automatically generated with names h1…h7.
+Examples:
+{{ heading("Page Title", 1) }}
+{{ h2("Sub-Title") }}
+
text – The heading
level – The heading level. From 1 (top-level) to 7.
Raises an Exception with a certain message. The actual name of the Jinja global function is raise
.
Returns a list of relative paths of all files relative to a root directory that match a certain pattern.
+Examples:
+{% set index_files1 = fglob("index_*.rst") %}
+{% set index_files2 = fglob("subdir/index_*.rst") %}
+
pattern – A glob pattern to match files.
root – The root directory for discovering files and anchor for relative paths. +Default is the current working directory (the parent directory of the currently rendered file).
Variables can be modified by filters. Filters are separated from the variable by a pipe symbol (|
)
+and may have optional arguments in parentheses.
+Multiple filters can be chained. The output of one filter is applied to the next.
For example, {{ name|trim|title }}
will trim whitespace from the variable name and title-case the output.
Filters that accept arguments have parentheses around the arguments, just like a function call.
+For example: {{ listx|join(', ') }}
will join a list with commas.
Recursively converts an OmegaConf config to a primitive container (dict or list) and returns it.
+Raises an UndefinedError if the context variable is undefined.
+The actual name of the Jinja global function is req
.
Example:
+{{ h1(ctx.local.test.test_name|req) }}
+
Returns an object inside an omegaconf.DictConfig container. +If the object does not exist, the default is returned.
+Example:
+{% set plot_title = asset.context|oc_get("plot.title", "") %}
+
cfg – The omegaconf.DictConfig container to search
key – The name of the object to extract, e.g. plot.title
.
default – The default to return if key is not in cfg
Jinja2 Ansible Filters is a port of the ansible filters provided by Ansible’s templating engine.
+See pypi project page for a listing of all filters.
+The filters are useful, but unfortunately not documented. You may refer to the +docstrings in the source code.
+Each Pharaoh components has a directory called asset_scripts
, that contain scripts Python scripts or
+Jupyter Notebooks, that extract information from resources (local result files like HDF5, databases or other sources)
+and transform this information into plot, tables and any other kind of types.
The generated files are called Assets and may be directly referenced/embedded by a template +(like a picture, plot or table) or indirectly referenced by a template to provide context values for template rendering.
+The component’s templates then consume these assets while Sphinx renders the template to HTML. +Consume means that some of the assets are directly included in the template via the +Pharaoh asset directive and others are read by +local context scripts to use their content during templating.
+Asset generation is executed before the actual Sphinx build via
+generate_assets()
.
Generate all assets by executing the asset scripts of a selected or all components.
+All asset scripts are executed in separate parallel child processes
+(number of workers determined by asset_gen.worker_processes
setting;
+setting 0 executes all asset scripts sequentially in the current process).
Setting asset_gen.script_ignore_pattern
determines if a script is ignored.
Putting the comment # pharaoh: ignore
at the start of a script will also ignore the file.
component_filters – A list of regular expressions that are matched against each component name. +If a component name matches any of the regular expressions, the component’s +assets are regenerated (containing directory will be cleared)
+Besides calling the API function, also the CLI can be used:
+From within PyCharm or wherever you have the venv activated that has Pharaoh installed, you can use the +Pharaoh CLI:
+cd <your Pharaoh project directory>
+pharaoh generate # for generating assets of all components
+pharaoh generate -f "dummy_.*" # for generating assets of all components starting with "dummy_"
+pharaoh generate -f dummy_1 -f dummy_2 # only for dummy_1 and dummy_2
+
Via a terminal (PS or CMD) from within the project directory, where CMD scripts are generated for your convenience:
+.\pharaoh-generate-assets.cmd # for generating assets of all components
+.\pharaoh.cmd generate -f dummy_1
+
Asset scripts can be debugged in two different ways:
+Via Pharaoh Asset Generation
+Generated Pharaoh project ship a debug script debug.py
.
+Just open your generated project in an IDE with an interpreter/venv that has Pharaoh installed and start a debug
+session on debug.py
.
The script should be modified to your current needs, e.g. if you just want to debug asset generation,
+comment out the proj.build_report()
line. If you like to debug asset scripts of a single component,
+pass the component name like this proj.generate_assets(component_filters=("dummy_1",))
.
Important
+If the setting asset_gen.worker_processes
is set to a non-zero integer or "auto"
,
+then asset scripts are executed as child processes.
Make sure you have the PyCharm debugger option
+File | Settings | Build, Execution, Deployment | Python Debugger ->
+Attach to subprocess automatically while debugging
enabled.
Directly debug your script
+You can execute your asset scripts directly via your IDE of choice. Any functions you import from +pharaoh.api or +pharaoh.assetlib.api are built in a way to support +being executed outside of a Pharaoh asset generation.
+The main difference is, that Pharaoh is not monkey-patching any toolkit (Plotly, Bokeh, Pandas) function,
+so the functions are behaving according to their official documentation.
+So statements like `fig.write_image("myplot.png")
are storing the plot in the current working directory
+(this is the same directory your asset script is in), instead of report-project\.asset_build
.
See also
+ +This section shows how asset scripts are developed.
+For simplicity we will show first how to generate assets without the use of resources. +An example on how to access resources can be found in the Resources Reference.
+Refresher: A components asset scripts are always located in the component’s subdirectory asset_scripts
.
The script names do not matter but certain names may be ignored
+(see generate_assets()
).
Let’s assume we want to make a plot as our first asset in a script called my_plot.py
:
import plotly.express as px
+
+from pharaoh.assetlib.api import metadata_context
+
+fig = px.scatter(
+ data_frame=px.data.iris(), # Famous example IRIS data set
+ x="sepal_width",
+ y="sepal_length",
+ color="species",
+ symbol="species",
+ title="IRIS Example",
+)
+
+with metadata_context(label="my_plot"):
+ fig.write_html(file="iris_scatter.html")
+
Now let’s analyse what we got:
+import plotly.express as px
Import a plotting framework of choice. Some frameworks are better supported by Pharaoh than others, we’ll see this +later also in the Patched Frameworks section.
+from pharaoh.assetlib.api import metadata_context
Any API function you will need can be imported from the pharaoh.assetlib.api
module.
+The purpose of metadata_context
is explained later.
fig = px.scatter(...)
Create a Plotly figure with some example data. +Your own scripts would actually read the resources specified with the component and create plots, tables etc.
+with metadata_context(label="my_plot"):
This context manager determines the metadata the asset will be stored with. +This metadata is then used to include assets in a template, in this case this would look like this:
+.. pharaoh-asset::
+ :filter: label == "my_plot"
+ :template: iframe
+ :iframe-width: 500px
+ :iframe-height: 500px
+
This so-called Sphinx directive +is a custom Sphinx directive (click here for docs) +Pharaoh adds via the plugin system of Sphinx.
+metadata_context
is explained in more detail in the next section.
fig.write_html(file="iris_scatter.html")
This is where Pharaoh is doing it’s magic.
+plotly.graph_objects.Figure.write_html
is one of many function which is
+monkey-patched
+by Pharaoh. More info about the other patched frameworks you can find here.
If the asset script is executed via Pharaoh, the patched write_html
function will not store the plot to
+"iris_scatter.html"
in the current working directory, but rather in a predefined location for assets inside
+your Pharaoh project report_project/.asset_build/<component-name>
with a unique suffix, for example
+iris_scatter_9c30799b.html
.
Next to the actual asset, an asset-info file iris_scatter_9c30799b.assetinfo
+is stored that holds the metadata for the asset.
+Here is a (shortened) content example:
{
+ "asset": {
+ "script_name": "my_plot.py",
+ "script_path": "C:\\...\\project\\report-project\\components\\dummy_1\\asset_scripts\\my_plot.py",
+ "index": 1,
+ "component_name": "dummy_1",
+ "user_filepath": "iris_scatter.html",
+ "file": "C:\\...\\project\\report-project\\.asset_build\\dummy_1\\iris_scatter_9c30799b.html",
+ "name": "iris_scatter_9c30799b.html",
+ "stem": "iris_scatter_9c30799b",
+ "suffix": ".html",
+ "template": "raw_html"
+ },
+ "label": "my_plot"
+}
+
This has following advantages:
+Finding names for assets is completely unnecessary, since they get a unique name assigned and are included in
+templating through their metadata rather than their file name.
+You could basically name all your assets asset.<suffix>
.
So for example if you would create a single plot of a signal for 100 operating conditions like Vdd,
+you would not add the value of Vdd to the file name, but rather put it in the metadata
+(see here) and name the file always "<signal-name>.png"
.
Note
+Though the assets name does not matter, its suffix does. +It is used internally so make sure you add it.
+If the script is directly executed by the user, write_html
is not patched and saves the plot to
+"iris_scatter.html"
in the current working directory.
See also
+ +The metadata stack is a construct that helps assigning metadata to generated assets.
+Use the context manager returned by
+metadata_context()
in your asset scripts
+to dynamically add/remove/update metadata that is added to your assets.
Let’s explained based on an example:
+from pharaoh.assetlib.api import metadata_context, register_asset
+
+# Will have no metadata (except the default ones added by Pharaoh)
+register_asset("<some-file>")
+
+# Will have metadata: {"foo": "bar"} only
+register_asset("<some-file>", dict(foo="bar"))
+
+with metadata_context(a=1):
+ register_asset("<some-file>") # Will have metadata: {"a": 1} only
+
+ with metadata_context(a=2, b=3):
+ register_asset("<some-file>") # Will have metadata: {"a": 2, "b": 3}
+
+ with metadata_context(c=4):
+ register_asset("<some-file>") # Will have metadata: {"a": 1, "c": 4}
+
+ for i in range(10):
+ with metadata_context(e=i):
+ register_asset("<some-file>") # Will have metadata: {"a": 1, "e": i}
+
Note
+If you don’t want to use the context manager, because the metadata should be set globally in the script anyway
+or you’re asset script is a Jupyter Notebook (no context manager over multiple cells possible),
+you can also use the activate()
and deactivate()
method:
metadata_context(...).activate()
+register_asset("<some-file>")
+
Or if you want to deactivate the context again:
+mc = metadata_context(...).activate()
+register_asset("<some-file>")
+mc.deactivate()
+
The function register_asset()
is explained in the next section.
The function register_asset()
may be used to manually register
+assets of any type.
This is mainly used if you like to create assets that are not generated via +patched framework functions, like txt, json, rst files or +generated via frameworks not yet supported, like seaborn.
+Here some examples:
+register_asset("some-path.html", dict(label="my_html_snippet"), template="raw_html")
+
+data = b"""<div><p>This HTML text was generated by an asset script!</p></div>"""
+register_asset("raw.html", dict(label="my_html_snippet"), template="raw_html", data=io.BytesIO(data))
+
+data = b""".. important:: This reStructuredText text was generated by an asset script!"""
+register_asset("raw.rst", dict(label="my_rst_snippet"), template="raw_rst", data=io.BytesIO(data))
+
+data = b'This text was generated by an asset script and will be included via "literalinclude"'
+register_asset("raw.txt", dict(label="my_txt_snippet"), template="raw_txt", data=io.BytesIO(data))
+
Pharaoh monkey-patches +plot/table export functions of popular tools like Pandas, Plotly etc.
+This has the advantage, that users can work with the official plotting APIs of the respective frameworks +without having to take care about Manually Registering Assets.
+The following APIs are patched by Pharaoh:
+Automatically sets metadata template="datatable"
(see Asset Templates) .
but NOT pandas.io.formats.style.Styler.to_html()
+Styler instances have to be exported manually:
+html_file_name = "styled_table.html"
+styler.to_html(buf=html_file_name)
+register_asset(html_file_name, template="datatable")
+
Examples:
+import pandas as pd
+
+df = pd.DataFrame(
+ {
+ "blabla": ["a", "b", "c"],
+ "ints": [1, 2, 3],
+ "float": [1.5, 2.5, 3.5],
+ "hex": ["0x11", "0x12", "0x13"],
+ }
+)
+df.to_html(buf="table.html")
+
When running in Pharaoh asset generation, this function is patched to prevent showing the plot in the browser.
+Supports Force Static Exports.
+Examples:
+import plotly.express as px
+
+fig = px.scatter(
+ data_frame=px.data.iris(), x="sepal_width", y="sepal_length",
+ color="species", symbol="species", title=r"A title",
+)
+
+fig.write_image(file="iris_scatter1.svg")
+fig.write_html(file="iris_scatter2.html")
+fig.write_image(file="iris_scatter3.png", width=500, height=500)
+
Bokeh
++++
+- +
+When running in Pharaoh asset generation, this function is patched to prevent showing the plot in the browser.
+- +
+Supports Force Static Exports.
+- +
- +
- +
- +
Examples:
+++from bokeh.io import save +from bokeh.plotting import figure +from bokeh.sampledata.iris import flowers + +colormap = {"setosa": "red", "versicolor": "green", "virginica": "blue"} +colors = [colormap[x] for x in flowers["species"]] + +p = figure(title=f"Iris Morphology", width=400, height=400) +p.xaxis.axis_label = "Petal Length" +p.yaxis.axis_label = "Petal Width" +p.scatter(flowers["petal_length"], flowers["petal_width"], color=colors, fill_alpha=0.2, size=10) + +save(p, filename="iris_scatter.html") +
Matplotlib
++++
+- +
+When running in Pharaoh asset generation, this function is patched to prevent showing the plot in the browser.
+- +
matplotlib.figure.Figure.show()
+When running in Pharaoh asset generation, this function is patched to prevent showing the plot in the browser.
+- +
Examples:
+++import matplotlib.pyplot as plt +import numpy as np + +fig, ax = plt.subplots() +ax.plot(np.arange(0.0, 2.0, 0.01), 1 + np.sin(2 * np.pi * t)) +ax.set(xlabel='time (s)', ylabel='voltage (mV)', title='About as simple as it gets, folks') +ax.grid() + +fig.savefig("test.png") +plt.show() +
holoviews.util.save() +with backends Bokeh, Plotly and Matplotlib.
+Supports Force Static Exports.
+Examples:
+import holoviews as hv
+data = [(i, chr(97 + j), i * j) for i in range(5) for j in range(5) if i != j]
+
+with metadata_context(ext="plotly"):
+ hv.extension("plotly")
+ model = hv.HeatMap(data).opts(cmap="RdBu_r", width=400, height=400)
+ hv.save(model, "heatmap_holo_plotly.html")
+ hv.save(model, "heatmap_holo_plotly.svg")
+ hv.save(model, "heatmap_holo_plotly.png")
+
+with metadata_context(ext="bokeh"):
+ hv.extension("bokeh")
+ model = hv.HeatMap(data).opts(cmap="RdBu_r", width=400, height=400)
+ hv.save(model, "heatmap_holo_bokeh.html")
+ hv.save(model, "heatmap_holo_bokeh.png")
+
+with metadata_context(ext="matplotlib"):
+ hv.extension("matplotlib")
+ model = hv.HeatMap(data).opts(cmap="RdBu_r")
+ hv.save(model, "heatmap_holo_mpl.svg")
+ hv.save(model, "heatmap_holo_mpl.png")
+
plotly.graph_objects.Figure.write_html
is one of many function which is
by Pharaoh. More info about the other patched frameworks you can find here.
+If the asset script is executed via Pharaoh, the patched write_html
function will not store the plot to
+"iris_scatter.html"
in the current working directory, but rather in a predefined location for assets inside
+your Pharaoh project report_project/.asset_build/<component-name>
with a unique suffix, for example
+iris_scatter_9c30799b.html
.
While for debugging and interactive review it’s nice to have all plots rendered as dynamic elements in HTML, +other build targets might be used for export purposes like Confluence or LaTeX.
+Since those targets don’t support embedding dynamic HTML elements you would have to tweak your asset generation scripts +to export different formats for different report output formats, which is suboptimal.
+Pharaoh therefore provides a setting asset_gen.force_static
which can be used to signal asset scripts
+to export static assets instead of HTML.
For all patched plotting APIs, if asset_gen.force_static
is set to true
,
+Pharaoh takes care and exports static images instead of HTML when plotting APIs like
+plotly.io.write_html() is used.
Since not all frameworks are patched, you might have to add support by yourself inside asset scripts like this:
+import altair as alt # altair not supported at the moment
+
+from pharaoh.api import get_project
+from pharaoh.assetlib.api import register_asset
+
+pharaoh_project = get_project(__file__)
+force_static = project.get_setting("asset_gen.force_static")
+
+chart = alt.Chart(...)
+
+if force_static:
+ filename = "chart.png"
+else:
+ filename = "chart.html"
+
+chart.save(filename)
+register_asset(filename, dict(plotting_framework="altair"))
+
See also
+ +Pharaoh supports generating assets via Matlab scripts/function through the
+Matlab API pharaoh.assetlib.api.Matlab
.
The asset scripts still have to be Python scripts and any asset the Matlab scripts generate, have to be
+manually registered using register_asset()
.
Here an example:
+from pharaoh.assetlib.api import Matlab, register_asset, FileResource, get_resource
+
+resource: FileResource = get_resource(alias="<resource_alias>")
+some_resource_path: Path = resource.locate()
+
+with Matlab() as matlab:
+ plot_path, out, err = matlab.execute_function("generate_plot", [str(some_resource_path)], nargout=1)
+
+register_asset(plot_path, dict(from_matlab=True, foo="bar"))
+
To use the Matlab API you have to install an additional dependency, depending on your Matlab version:
++++
+- +
R2020B:
pip install matlabengine==9.9.*
- +
R2021A:
pip install matlabengine==9.10.*
- +
R2021B:
pip install matlabengine==9.11.*
- +
R2022A:
pip install matlabengine==9.12.*
- +
R2022B:
pip install matlabengine==9.13.*
- +
R2023A:
pip install matlabengine==9.14.*
- +
R2023B:
pip install matlabengine==9.15.*
This section deals with how to access assets in local context scripts and build-time templates.
+The AssetFinder
is responsible for discovering and searching assets
+based on filters using AssetFinder.search_assets()
.
This function is available in various places:
+The following script searches all JSON files that have "context_name"
set,
+loads them and exports their content as context for build-time templating:
import json
+from pharaoh.assetlib.api import get_asset_finder, get_current_component
+
+finder = get_asset_finder()
+component_name = get_current_component()
+
+context = {
+ asset.context.context_name: json.loads(asset.read_text())
+ for asset in finder.search_assets(
+ 'asset.suffix == ".json" and "context_name" in asset.context',
+ [component_name]
+ )
+}
+context["component_name"] = component_name
+return context
+
In some use cases this is used to make measurement information available during templating +to render the information in a table.
+In templates inside Jinja statements.
+The following snippet searches all image assets of the current component +(the component the template is rendered in) and stores the list of assets in a variable:
+{% set image_assets = search_assets("asset.suffix in ['.png', '.svg']") %}
+
The following snippet searches all image assets of the all components in the project. +This can be used to create components that summarize information from other components.
+{% set all_image_assets = search_assets_global("asset.suffix in ['.png', '.svg']") %}
+
The used functions search_assets
and search_assets_global
are
+partial functions where the component
argument is preset.
If you like to include some of your assets manually, the template environment provides +two functions for your convenience, that return the (relative) path of an asset to the including template:
+asset_rel_path_from_project(asset)
Returns the relative path from the Sphinx source directory to the passed asset.
+This path is needed for Sphinx directives that themselves take care to copy the asset into the build directory,
+like directives literalinclude
, image
and figure
.
Here’s an example:
+{% set matches = search_assets("<some condition>") %}
+
+.. figure:: {{ asset_rel_path_from_project(matches[0]) }}
+ :scale: 50 %
+
+ This is the caption of the figure (a simple paragraph).
+
where {{ asset_rel_path_from_project(matches[0]) }}
would render something like this:
+/.asset_build/dummy/mytxt_f65223c4.txt
asset_rel_path_from_build(asset)
Returns the relative path from the Sphinx build directory to the passed asset.
+This path is needed for Sphinx directives that themselves do NOT take care to copy the asset into the
+build directory, like when using the raw_html
directive and including a picture or HTMl file using an iframe.
In this case calling asset_rel_path_from_build
will automatically copy the asset from the asset build folder
+to the build directory and return it’s relative path from the including template.
Here’s an example:
+{% set matches = search_assets("<some condition>") %}
+
+.. raw:: html
+
+ <iframe src="{{ asset_rel_path_from_build(matches[0]) }}"
+ loading="lazy"
+ ></iframe><br>
+
where {{ asset_rel_path_from_build(matches[0]) }}
would render something like this:
+../../pharaoh_assets/iris_scatter_3e7d7ab7.html
Note
+Another way to include assets with a custom template is to use the template
+option of the
+Pharaoh asset directive with a path to your custom template.
Imaging in your asset scripts you are creating a plot for a measured signal for many different operating conditions:
+from pharaoh.assetlib.api import metadata_context
+
+for vdd in (8.0, 10.0, 12.0):
+ for iout in (1.0, 2.0, 5.0):
+ with metadata_context(vdd=vdd, iout=iout, signal_name="idd"):
+ fig = ...
+ fig.write_image(file="idd_plot.png")
+
Now in your report you may like to have all those plots added separately each with it’s own title containing the
+values of either vdd
or iout
.
Defining the template like this is not really viable, since in the template you usually have no idea about what values +were iterated over in the asset script. But let’s assume you know, then this would be the template:
+{% for vdd in (8.0, 10.0, 12.0) %}
+{{ heading("Plots for Vdd:%.1fV, Iout:%.1fA" % vout, 2 }}
+
+{% for iout in (1.0, 2.0, 5.0) %}
+{{ heading("Plots for Iout:%.1fA" % iout, 3 }}
+
+.. pharaoh-asset:: vdd == {{ vdd }} and iout == {{ iout }} and signal_name == 'idd'
+
+{% endfor %}
+{% endfor %}
+
But luckily there is a better option using asset_groupby
to
+group assets first by vdd
and then by iout
:
{% set idd_plots = search_assets("signal_name == 'idd'") %}
+{% for vdd, assets_grby_vdd in agroupby(idd_plots, key="vdd").items() %}
+{{ heading("Plots for Vdd:%.1fV" % vout, 2 }}
+
+ {% for iout, assets_grby_iout in agroupby(assets_grby_vdd, key="iout").items() %}
+{{ heading("Plots for Iout:%.1fA" % iout, 3 }}
+
+ {% for asset in assets_grby_iout %}
+.. pharaoh-asset:: {{ asset.id }}
+
+ {% endfor %}
+ {% endfor %}
+{% endfor %}
+
Note
+agroupby
is an alias for asset_groupby
. Both are available as global function during templating.
Following steps have to be done for configuring a Sphinx build:
+Choose a Sphinx build target via the report.target
setting.
+Currently supported are:
html and all other Sphinx default builders like latex.
Note
+For other builders than html
and *confluence
, you might need to install additional dependencies.
Configure the selected builder
+Via settings sections like report.html
and report.confluence
Via the Sphinx configuration
+report_project/conf.py
.
This file loads a default Sphinx configuration variables (also partially from settings) and assigns them to the local namespace.
+To modify just overwrite or alter them:
+# The list "html_css_files" is already defined from the default config,
+# but this is not the case for all variables.
+# To be sure inspect the local namespace via the debugger or print it
+# like this ``print(", ".join(locals().keys()))``
+html_css_files.append("css/mystyle.css")
+
Note
+The HTML builder already contains a reasonable default config, so no change needed in most cases.
+A Pharaoh report may be built using the method PharaohProject.build_report()
.
Important
+If your templates are accessing assets, make sure you call PharaohProject.generate_assets()
everytime your asset scripts or resources changed.
You can call the API within a Python script, as for example in the included report-project/debug.py
script.
++++from pharaoh.api import PharaohProject + +if __name__ == "__main__": # This guard is needed because Pharaoh is using multiprocessing + proj = PharaohProject(project_root="..") + proj.generate_assets() + proj.build_report() +
Or you just double-click the CLI scripts report-project/pharaoh-generate-assets.cmd
and
+report-project/pharaoh-build.cmd
.
Or with the Pharaoh venv activated, inside the project directory use the CLI directly: pharaoh build
Log output should be displayed in the console, but are also written to log.txt
and log_warnings.txt
+(only warnings or errors).
After building a report, it may be archived using PharaohProject.archive_report()
.
Create an archive from the build folder.
+dest – A destination path to create the archive. Relative paths are relative to the project root.
+If omitted, the filename will be taken from the report.archive_name
setting.
The path to the archive
+Alternatively just double-click the CLI script report-project/pharaoh-archive.cmd
.
++++Usage: pharaoh [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]... + + Pharaoh Commandline Interface. + + Usage examples: + + pharaoh new -t pharaoh_testing.simple -c "{'a': 1}" --settings ../my_settings.yaml + pharaoh add -n dummy1 -t pharaoh_testing.simple -c "{'test_name':'dummy'}" + pharaoh generate + pharaoh build + pharaoh archive + + Multi-command chaining is also possible (all on one line): + + pharaoh new add --name dummy1 -t pharaoh_testing.simple -c "{'test_name':'dummy1'}" + add --name dummy2 -t pharaoh_testing.simple -c "{'test_name':'dummy2'}" + generate build archive -d "archives/myarchive.zip" + +Options: + -p, --path PATH The path to the Pharaoh report directory (contains + pharaoh.yaml) or the path to pharaoh.yaml itself. If omitted, + the current working directory is used. + --help Show this message and exit. + +Commands: + add Adds a new component to the Pharaoh project. + add-template Adds additional templates to an existing component, that... + archive Archives the report to a ZIP file. + build Executes report generation for the current project. + env Updates the projects settings. + generate Generates assets. + info Prints Pharaoh version information, e.g. + new Creates a new Pharaoh report skeleton. + print-plugins Prints all available plugins and their installation paths + remove Removes one or multiple existing components. + show-report Opens the generated report (if possible, e.g. + update-resource Updates a Resource from a project component by its alias. +
The CLI may be used in different ways:
+Through the PIP entrypoint pharaoh
When you install Pharaoh via PIP, it will install a pharaoh.exe
+into your environment (<installdir>/Scripts
).
Once your environment is activated you can use the CLI via the pharaoh
command.
+Some examples using CMD:
cd "C:/projects/my_pharaoh_report"
+
+set PHARAOH.REPORT.TITLE="Some Title"
+
+pharaoh new
+pharaoh add --name dummy1 -t pharaoh_testing.simple --context "{'test_name':'dummy1'}"
+pharaoh add --name dummy2 -t pharaoh_testing.simple --context "{'test_name':'dummy2'}"
+pharaoh generate
+pharaoh build
+
If you current working directory is not inside the Pharaoh project, add the absolute or relative (to CWD) path +to the project as first argument like this:
+cd "C:/projects"
+
+pharaoh -p "my_pharaoh_report" new
+pharaoh -p "my_pharaoh_report" generate
+cd "my_pharaoh_report"
+pharaoh build
+
Through the generated CMD scripts report-project/*.cmd
The advantage of those scripts is, that they are working regardless of your Pharaoh Python environment being +activated or not, since they contain the absolute path to the Python interpreter used to generate them as a +fallback .
+Thus they are checking if pharaoh.exe
is on the system path (e.g. when executing inside an
+activated virtual env - like the terminal in PyCharm), but falling back to the absolute path.
Important
+If you rename or move the Python environment that was used to generate the Pharaoh project,
+those CMD script won’t work anymore (unless pharaoh.exe
is on the system path).
So you can just double click pharaoh-*.cmd
to perform the corresponding action with default arguments.
Note
+This way is just used to managed an already existing Pharaoh project, so commands like
+pharaoh.cmd new
does not really make sense.
If you like to use them via the terminal, the syntax is similar to the above examples:
++++
+- +
pharaoh.cmd generate
orpharaoh-generate-assets.cmd
- +
pharaoh.cmd build
orpharaoh-build.cmd
- +
pharaoh.cmd archive
orpharaoh-archive.cmd
++++Usage: pharaoh new [OPTIONS] + + Creates a new Pharaoh report skeleton. + + Example: + + pharaoh new -t pharaoh_testing.simple -c "{'a': 1}" --settings ../my_settings.yaml + +Options: + -f, --force Force project overwrite if already exists + -t, --template TEXT The project templates to use for creating the project + files. + -c, --context TEXT The Jinja rendering context for the selected template. + -s, --settings FILE A path to a YAML file containing settings to overwrite + the default project settings. + --help Show this message and exit. +
++++Usage: pharaoh add [OPTIONS] + + Adds a new component to the Pharaoh project. + + Example: + + pharaoh add -n dummy1 -t pharaoh_testing.simple -c "{'test_name':'dummy'}" + pharaoh add -n dummy1 -t pharaoh_testing.simple -r "FileResource(alias='foo', pattern='.*')" + +Options: + -n, --name TEXT The name of the component. Must be a valid Python + identifier. [required] + -t, --template TEXT The component templates to use for creating the + components project files. + -c, --context TEXT The Jinja rendering context for the selected templates. + -m, --metadata TEXT A dictionary of metadata that may be used to find the + components.Enter a valid json string + -r, --resource TEXT A Python expression that evaluates to a valid object of + type Resource. + -i, --index INTEGER The index to add the component. Default is -1 with means + at the end. + --help Show this message and exit. +
++++Usage: pharaoh update-resource [OPTIONS] + + Updates a Resource from a project component by its alias. + + Examples: + + pharaoh update-resource -n dummy1 -a foo -r "FileResource(alias='baz', pattern='*')" + +Options: + -n, --name TEXT The name of the component. Must be a valid Python + identifier. [required] + -a, --alias TEXT The resource alias. [required] + -r, --resource TEXT A Python expression that evaluates to a valid object of + type Resource. + --help Show this message and exit. +
++++Usage: pharaoh add-template [OPTIONS] + + Adds additional templates to an existing component, that may overwrite + existing files during rendering. + + Example: + + pharaoh add-template -n dummy1 -t pharaoh_testing.simple -c "{'test_name':'dummy'}" + +Options: + -n, --name TEXT The name of the component. Must be a valid Python + identifier. [required] + -t, --template TEXT The component templates to use for creating the + components project files. + -c, --context TEXT The Jinja rendering context for the selected templates. + --help Show this message and exit. +
++++Usage: pharaoh remove [OPTIONS] + + Removes one or multiple existing components. + + If the regular expression (case-insensitive) partially matches the component + name. To do a full match surround the pattern by ^ and &, e.g. ^my_component&. + + Example: + + pharaoh remove -f dummy.* + +Options: + -f, --filter TEXT A case-insensitive component filter. Either a full- or + regular expression match, depending on regex option. + [required] + -r, --regex If True, the filter option will be treated as regular + expression. Components that partially match the regular + expression are removed. + --help Show this message and exit. +
++++Usage: pharaoh env [OPTIONS] KEY [VALUE] + + Updates the projects settings. + + Example: + + pharaoh env foo_A 123 -> foo_a: 123 + pharaoh env foo_B bar -> foo_b: "bar" + pharaoh env foo_C "{'baz': 123}" -> foo_c: {'baz': 123} + pharaoh env foo_D class -> foo_d: "class" + pharaoh env foo_E -> foo_e: None + +Options: + --help Show this message and exit. +
++++Usage: pharaoh generate [OPTIONS] + + Generates assets. Either of the entire project or just a selected subset of + components. + + Examples: + + pharaoh generate + pharaoh generate -f dummy[12] + pharaoh generate -f dummy1 -f dummy2 + +Options: + -f, --filter TEXT A list of regular expressions that are matched against each + component name. If a component name matches any of the + regular expressions, the component's assets are regenerated + (containing directory will be cleared) + --help Show this message and exit. +
++++Usage: pharaoh build [OPTIONS] + + Executes report generation for the current project. + + Examples: + + pharaoh build + pharaoh -p "path/to/my/project" build + +Options: + --help Show this message and exit. +
++++Usage: pharaoh show-report [OPTIONS] + + Opens the generated report (if possible, e.g. for local HTML reports). + + Examples: + + pharaoh show-report + pharaoh -p "path/to/my/project" show-report + +Options: + --help Show this message and exit. +
++++Usage: pharaoh archive [OPTIONS] + + Archives the report to a ZIP file. + +Options: + -d, --dest PATH Path to the archive. Either a directory or filename with + extension zip. + --help Show this message and exit. +
++++Usage: pharaoh info [OPTIONS] + + Prints Pharaoh version information, e.g. ``Pharaoh v1.0.0 [Python 3.9.13, + Windows-10-10.0.19045-SP0]`` + +Options: + --help Show this message and exit. +
++++Usage: pharaoh print-plugins [OPTIONS] + + Prints all available plugins and their installation paths + +Options: + --help Show this message and exit. +
A Pharaoh project consists of any amount of components.
+The file report_project/index.rst
determines how the components are included in the report.
+Per default all components are added in sequence on the same level but this could be changed in the template.
A component has following traits:
++++
+- Name
- +
The name of the component; used for identification purposes.
+- Templates
- +
A list of templates used to generate the component’s files.
+- Render Context
- +
An arbitrarily-nested dictionary that is available during generation-time templating. +This context enables the re-use of the templates for different purposes.
+- Resources
- +
A list of resource definitions that let asset scripts easily access external data by using an alias instead of +absolute resource specifiers (like a file path).
+- Metadata
- +
This metadata dictionary can be used to lookup components +by their metadata.
+A potential use case is grouping components by their metadata and create a customized report layout +using the
+report_project/index.rst
template.
Resources let asset scripts easily access external data by using an alias instead of absolute resource specifiers +(like a file path or an RDDL artifact id).
+The currently available resource types:
++++
Accessing resources in asset scripts can be done like this:
+from pathlib import Path
+from pharaoh.assetlib.api import get_resource, FileResource
+
+resource: FileResource = get_resource(alias="<resource_alias>")
+first_matching_file_path: Path = resource.locate()
+...
+
To get a resource from a component or update a component’s resource, use theses functions:
+++
Component templates are used to create the main content for components +(see Adding Components) and can come in different forms:
+Template Directory
+++The simplest form of a template. A directory with some files.
+If assets shall be generated by the component, +the directory must contain an
+asset_scripts
subdirectory containing Python scripts.
Registered Templates
+++Pharaoh’s Plugin Architecture allows the development of plugins that register templates.
+That means plugins may come with their own template directories and register their paths in Pharaoh, +so these templates are then selectable using a template identifier, +like
+my_awesome_plugin.template1
.An advantage of plugin templates is, that for those templates, dependencies to other templates can be specified. +For example the template
+my_awesome_plugin.template1
may requires that themy_awesome_plugin.template2
+template is used at least once in any component, otherwise cross-references won’t work.
Template Files/Single-file templates
+++The smallest and most compact form of a template.
+Single-file templates are Python files with suffix
+.pharaoh.py
whose Python code creates assets and +whose module-level docstring represents the rST content.This file will be internally converted to a template directory:
+++my_template.pharaoh.py -> index_my_template.rst + asset_scripts/my_template.py +
Adding a component to a Pharaoh project can be done via the API function
+PharaohProject.add_component()
or the CLI
+command Add:
Adds a new component to the Pharaoh project
+Example:
+from pharaoh.api import FileResource, PharaohProject
+
+proj = PharaohProject(".")
+proj.add_component(
+ component_name="component_ABC",
+ templates=[
+ "plugin_abc.template_xyz", # plugin template
+ "path/to/template/directory", # template directory
+ "path/to/template/file/tmpl.pharaoh.py" # template file
+ ],
+ render_context={"foo": "bar"},
+ resources=[FileResource(alias="dlh5_result", pattern="C:/temp/**/*.dlh5")],
+ metadata={"some tag": "some value"},
+ index=-1, # append
+ overwrite=False
+)
+
component_name – The name of the component. Must be a valid Python identifier.
templates –
A list of component templates to use for creating the components project files.
+Those may be the template identifier (e.g. plugin_abc.template_xyz
) of a registered plugin template,
+a path to a template directory or a path to a template file (single-file template).
Since multiple template may be specified, their order matters in cases where different templates +create equally-named files, thus templates might overwrite files of the previous templates.
+This enables template-composition, where template designers can chunk their bigger templates into +smaller re-usable building blocks.
+If omitted, an empty default template is used.
+render_context – The Jinja rendering context for the selected template.
+The actual template rendering context will have component_name
, resources
and
+metadata
available under the respective keys.
resources – A list of Resource instances, defining the component’s resources used in asset scripts
metadata – A dictionary of metadata that may be used to find the component via
+PharaohProject.find_component()
method.
overwrite – If True, an already existing component will be overwritten
Once a component is generated, the options to modify it are limited to:
+++
A generated component can be removed via
+PharaohProject.remove_component()
,
+which results in deletion of report-project/components/<component-name>
and its entry in pharaoh.yaml
:
Components can be looked up via their metadata using the function
+PharaohProject.get_component_names_by_metadata()
.
A Sphinx Directive +is a generic block of explicit markup. +Along with roles, directives are one of the extension mechanism, a way of adding support for new constructs +without adding new primary syntax (directives may support additional syntax locally). +And Sphinx makes heavy use of it.
+All standard directives are always available, +whereas any other directives are domain-specific +(see Writing Sphinx Extensions) +and may require special action to make them available when processing the document, +like the Pharaoh Asset directive.
+The Pharaoh Asset directive pharaoh-asset
is added via the Pharaoh Sphinx extension and allows users
+to include assets in the document, based on one or multiple filter conditions.
The matched assets are then rendered using predefined templates +that can be specified in the asset metadata.
+The pharaoh-asset
directive consists of a name, arguments, options and content, e.g:
++++.. pharaoh-asset:: key1 == "A" or + key2 == "B" + :filter: key3 == "C" + :index: 0 + + key4 == "D"; + key5 == "E" ++
+- +
pharaoh-asset
is the name- +
key1 == "A" or key2 == "B"
is an argument- +
:filter: key3 == "C"
and:index: 0
are options- +
key4 == "D";key5 == "E"
is the content++Note
+The directive supports 3 ways to define an asset filter, all of them can be used in parallel and are AND-ed: +the argument, the
+:filter:
option and the content.The filters are treated as multiline statements and are split only on unescaped
+;
characters. +This meanskey4 == "D";key5 == "E"
is effectively the same askey4 == "D" and key5 == "E"
The directive internally uses the
+AssetFinder.search_assets()
function, so the same rules apply to the filter strings.
If your filter matches multiple assets, all matches will be rendered one after another.
+This can look quite messy very fast, so there is another option to add some more information to each rendered asset
+by the combined use with the AssetFinder.search_assets()
function,
+where the directive simply gets the ID of the asset to include.
++Following template example will search for all images. If at least one exists a new section will be created and the +images are added, each under its own rubric:
+++{% set image_assets = search_assets("asset.suffix in ['.png', '.svg']") %} + +{% if image_assets|length %} +{{ h2("Images") }} + + {% for asset in image_assets %} +.. rubric:: {{ asset.context.caption }} + +.. pharaoh-asset:: {{ asset.id }} + + {% endfor %} +{% endif %} +++Important
+In this case the asset’s ID must be the one-and-only filter that is given!
+
Following options may be used for the pharaoh-asset
directive:
:filter:
The filter (multiline string, ;
-separated) to select assets by metadata. See example above.
:optional:
If false (default), the asset filter MUST match any asset, otherwise an exception is raised.
+Valid values: true/yes/1
, false/no/0
:index:
If the filters match multiple assets, only render the assets with specified index (0-based).
+Following style is possible: 1,2,4-8,9
(like selecting pages to print). Whitespace is ignored.
:components:
A comma-separated string of component names to look for assets (see AssetFinder.search_assets()
).
Special values:
+_this_
(default): Searches only the current component the template is rendered in
_all_
: Searches all components
:template:
The template to use to embed the asset into the document. +It may be a template name as described in Asset Templates or +a file path to your own template. Please note that relative paths are resolved with the Pharaoh project directory +as root directory).
+If not specified as option to the directive, the asset metadata asset.template
is looked up
+(which maybe gets a default template automatically determined by the asset’s file suffix).
If the template is set neither via option nor asset metadata, an error will be raised.
+Note
+Another way to include assets manually is described here.
+:image-(alt,height,width,scale,align):
Passed to the image
directive, if the asset is rendered as image.
:iframe-width:
The width of the HTML iframe.
+The default value is determined by setting asset_gen.default_iframe_width
.
:iframe-height:
The height of the HTML iframe.
+The default value is determined by setting asset_gen.default_iframe_height
.
:ignore-title:
If asset metadata asset.title
is set, the asset will be rendered under a unique
+rubric.
+The rubric title is a unique cross-reference.
If this option is true, this behavior is skipped.
+Valid values: true/yes/1
, false/no/0
:ignore-description:
If asset metadata asset.description
is set, the description will be inserted directly before the asset
+as raw string (so you could add reST as well).
If this option is true, this behavior is skipped.
+:datatable-extended-search:
If true, assets rendered as interactive datatable will include an +interactive search builder
+The default value is determined by setting asset_gen.default_datatable_extended_search
.
Valid values: false/no/0
, true/yes/1
This section describes the templates used to include assets into your documents.
+There are 2 options how to specify the used template:
++++
+- +
Via the
+register_asset()
function:++data = b'This text was generated by an asset script and will be included via "literalinclude"' +register_asset("raw.txt", dict(label="my_txt_snippet"), template="raw_txt", data=io.BytesIO(data)) +As fallback, if
+template
is not specified, a default template is chosen depending on the asset’s file suffix:+
+- +
".html"
: iframe- +
".rst"
: raw_rst- +
".txt"
: raw_txt- +
".svg"
: image- +
".png"
: image- +
".jpg"
: image- +
".jpeg"
: image- +
".gif"
: image- +
".md"
: markdown- +
Via the
:template:
option of the Pharaoh asset directive (has priority over setting in metadata).
Following asset templates are currently supported:
++++
+- image
- +
The asset file will be included via the image directive. Options on the
+pharaoh-asset
directive starting withimage-
are +passed to the image directive.- raw_html
- +
The asset file will be included via the raw:: html directive. +The only modification is that the HTML content is pasted into a separate
+div
-tag with a unique, +autogenerated ID and a linebreak at the end.- markdown
- +
The asset file will be included via the raw:: html directive after converting the Markdown text to HTML.
+- raw_rst
- +
The content of the asset file will be pasted into the document without modification and no extra indentation.
+- raw_txt
- +
The asset file will be included via the literalinclude directive. Language highlighting is disabled.
+- iframe
- +
The asset file will be included via the raw directive, wrapped in an
+iframe
-tag. +Options on thepharaoh-asset
directive starting withiframe-
are added to the iframe options. +The iframe will be lazy-loaded by the browser.- datatable
- +
This template can only be used in combination with HTML tables produced by +pandas.DataFrame.to_html() or +pandas.io.formats.style.Styler.to_html().
+It transforms those tables into interactive JS-powered tables (see DataTables).
+
A generated and built Pharaoh project has following directory structure:
+📁 <project-name>
+├── 📄 pharaoh.yaml
+│ Contains all settings and component definitions.
+├── 📄 log.txt
+│ Build logs for all logging levels.
+├── 📄 log_warnings.txt
+│ Build logs for logging levels WARNING and higher.
+├── 📄 pharaoh_report_<timestamp>.zip
+│ A ZIP archive of the built report.
+├── 📄 .gitignore
+│ A file for GIT to ignore all transient files.
+├── 📁 report-build
+│ Contains the build output, e.g. HTML pages or LaTeX input files.
+│ For HTML output the main page is called index.html.
+└── 📁 report-project
+ ├── 📁 .resource_cache
+ │ Contains temporary/cached resources
+ ├── 📁 _static
+ │ Contains the CSS overrides and the HTML logo for the report.
+ ├── 📁 _templates
+ │ Contains HTML templates that override the defaults of the selected Sphinx theme.
+ │ In the case layout.html and footer.html. Only change if you really need to.
+ ├── 📁 .asset_build
+ │ All registered assets from asset generation will be stored here for each component separately.
+ ├── 📁 components
+ │ This is where components get created when added via the project API.
+ ├── 📁 user_templates
+ │ Stores user-defined templates for the build-time templating step.
+ ├── 📄 conf.py
+ │ The Sphinx configuration file (https://www.sphinx-doc.org/en/master/usage/configuration.html)
+ │ It loads a default configuration from the Pharaoh project into local variables.
+ │ Used to overwrite or add additional configuration.
+ ├── 📄 debug.py
+ │ For debugging the asset/report generation. Modify for your needs.
+ │ Per default it executes asset generation and report build.
+ ├── 📄 index.rst
+ │ The default template to control how the title page looks like.
+ │ It includes all components into the TOC (table of content) tree.
+ ├── 📄 index.rst.rendered
+ │ *.rendered files are generated during build-time templating step and allow to see the
+ │ actual rST content Sphinx is using for generating the report.
+ │ This helps for debugging your templates since you see the result after the templating step.
+ └── 📄 \*.cmd
+ Scripts to execute asset generation, report build or upload to R&D Datalake with a single double-click.
+
The Pharaoh settings file pharaoh.yaml
is used for three main purposes:
Configuration of Pharaoh functionality like asset generation, report build, archiving, RDDL upload, notifications…
Hold info about all added components
User-defined settings (accessible from templates)
The settings file is loaded using the OmegaConf library.
+++OmegaConf is a YAML based hierarchical configuration system, with support for merging configurations +from multiple sources (files, CLI argument, environment variables) providing a consistent API regardless +of how the configuration was created. OmegaConf also offers runtime type safety via Structured Configs.
+The library offers lots of features like +Variable interpolation and +Resolvers. +So if you want to get the best out of your configuration file, have a look in the OmegaConf documentation on the +mentioned topics above.
+
A Pharaoh project is generated with following default settings, if not specified otherwise:
+logging:
+ # The logging level for the Pharaoh logger, one of DEBUG, INFO, WARNING, ERROR
+ level: "INFO"
+
+report:
+ # The report output format. Currently supported are:
+ # - html and all other Sphinx default builders; see https://www.sphinx-doc.org/en/master/usage/builders/index.html
+ # - confluence, singleconfluence; see https://sphinxcontrib-confluencebuilder.readthedocs.io/en/stable/builders/
+ builder: "html"
+ # Verbosity of the Sphinx build. 0: INFO, 1: VERBOSE, 2: DEBUG
+ # VERBOSE: Will enable debug output of .. pharaoh-asset:: directive
+ verbosity: 0
+ # The report title displayed on the index page
+ title: "Pharaoh Report"
+ # The report author
+ author: "${user:}"
+ # Name of the ZIP archive. For available resolvers consult documentation
+ archive_name: "pharaoh_report_${utcnow.strf:%Y%m%d_%H%M%S}.zip"
+ # Options for HTML builder
+ html:
+ # Show Pharaoh logo on HTML report. Kindly let this enabled for branding, unless it's for external customers ;)
+ show_pharaoh_logo: true
+
+asset_gen:
+ # Create static (png, svg) assets instead of dynamic (html) ones where possible.
+ # This only has effect on the patched functions of frameworks like pandas, bokeh, plotly, ... .
+ # For more info refer to the documentation: todo
+ force_static: false
+ # How many worker processes (CPUs) to use for parallel asset generation. "auto" or some integer value.
+ # Use 0 to execute all asset scripts sequentially in the current process.
+ # Using multiprocessing only outperforms single-process if there are a large amount of asset scripts.
+ # The proof is in the pudding ;)
+ worker_processes: 0
+ # Regular expression (case-insensitive). If matches on file name with extension, the script is not executed during
+ # asset generation
+ script_ignore_pattern: "_.*" # ignore scripts that start with underscore
+ # The default width of embedded iframes. (html attribute)
+ default_iframe_width: "100%"
+ # The default height of embedded iframes. (html attribute)
+ default_iframe_height: "500px"
+ # Show all datatables per default with an interactive search function
+ default_datatable_extended_search: false
+
+# Options for toolkit patches
+toolkits:
+ bokeh:
+ export_png:
+ width: 720
+ height: 480
+ matplotlib:
+ savefig:
+ dpi: 150
+ facecolor: "auto"
+ edgecolor: "auto"
+ plotly:
+ write_image:
+ width: 1000
+ height: 600
+ scale: 1
+ validate: true
+ write_html:
+ default_width: "100%"
+ default_height: "100%"
+ holoviews:
+ save:
+ dpi: 300
+ pandas:
+ to_html:
+ na_rep: ""
+
Since default settings are very likely to not fit everyone’s needs, they can be modified by the user in several ways:
+Important
+Make sure all keys in your settings are lowercase, otherwise putting/getting settings might +not work as expected since settings are all treated lowercase internally!
+Pass a custom settings file on project creation via the keyword argument custom_settings
.
+The file is merged with the default settings, so it’s enough to overwrite only the divergent settings:
from pharaoh.api import PharaohProject
+
+proj = PharaohProject(project_root="some-path", custom_settings="mysettings.yaml")
+
Note
+If custom_settings
is a relative path, then the current working directory is used as anchor
Put settings via the settings API
+put_setting(key: str, value: Any)
:
from pharaoh.api import PharaohProject
+
+proj = PharaohProject(project_root="some-path")
+proj.put_setting("report.title", "My own title")
+proj.put_setting("toolkits.bokeh.export_png", dict(width=720, height=480))
+proj.save_settings() # Optional
+
Important
+Defining settings like this only changes the settings for the project instance in memory but does
+not persist them to pharaoh.yaml
.
To do so call save_settings()
manually if
+you don’t plan to modify components/resources right after (those functions do an auto-safe).
Put settings via environment variables
+Same example as with settings API, but with environment variables:
+import os
+from pharaoh.api import PharaohProject
+
+# Env variables have to be prefixed with "pharaoh." and are always treated lowercase
+os.environ["pharaoh.report.title"] = "My own title"
+os.environ["pharaoh.toolkits.bokeh.export_png"] = "{'width': 720, 'height': 480}"
+
+proj = PharaohProject(project_root="some-path")
+proj.save_settings(include_env=True)
+
Note
+Double-underscore __
also acts as a valid separator for keys, since some shell environments don’t
+allow dots inside variable names. E.g. PHARAOH__a.B__c___d
will resolve to pharaoh.a.b.c___d
.
Important
+Environment variables are only persisted to pharaoh.yaml
when saving like this:
+save_settings(include_env=True)
Important
+If Pharaoh environment variables are changed while the project is already instantiated,
+you can reload settings using
+load_settings(namespace="env")
:
import os
+from pharaoh.api import PharaohProject
+
+proj = PharaohProject(project_root="some-path")
+os.environ["pharaoh.report.title"] = "My Title"
+proj.load_settings(namespace="env")
+proj.save_settings(include_env=True)
+
Setting values can be accessed in various places throughout the Pharaoh project using
+PharaohProject.get_setting()
:
in Python scripts, e.g. asset- or local context-scripts:
+from pharaoh.api import get_project
+pharaoh_project = get_project(__file__) # Get a project instance location of this file in project directory
+title = project.get_setting("report.title")
+
in templates:
+{{ heading(get_setting("report.title"), 1) }}
+{% set static_export = get_setting("asset_gen.force_static", False) -%}
+
OmegaConf offers already some
+builtin resolvers
+like oc.env
: author: "${oc.env:USERNAME,unknown}"
+but Pharaoh adds additional ones for your convenience:
utcnow.strf
: Formats UTC time.
++Usage:
+archive_name: "pharaoh_report_${utcnow.strf:%Y%m%d_%H%M%S}.zip"
now.strf
: Same as above, but with timestamp of local timezone.
pharaoh.project_dir
: Returns the Pharaoh project directory with /
as path separator.
++Usage:
+key: "${pharaoh.project_dir:}/somepath"
(note the trailing:
, without, it would be a reference).
If you need additional resolvers or wish to implement your own, please get in contact with us.
+As you may have noticed, templating is a dominating core functionality of Pharaoh.
+The foundation for all templating in Pharaoh is reStructuredText and Jinja. +You might make yourself familiar once you encounter related questions in the rest of the section.
+++reStructuredText
+++reStructuredText is an easy-to-read, what-you-see-is-what-you-get plaintext markup syntax and parser system. +It is useful for in-line program documentation (such as Python docstrings), for quickly creating simple web pages, +and for standalone documents.
+reStructuredText is designed for extensibility for specific application domains.
+The primary goal of reStructuredText is to define and implement a markup syntax for use in Python docstrings +and other documentation domains, that is readable and simple, yet powerful enough for non-trivial use.
+Refer to the reStructuredText Reference +for learning the basics.
+Jinja
+++Jinja is a fast, expressive and extensible templating engine.
+A Jinja template is simply a text file. +Jinja can generate any text-based format (HTML, XML, CSV, LaTeX, etc.), in Pharaoh’s case mostly rST files. +A Jinja template does not need to have a specific extension: .html, .xml, or any other extension is just fine.
+A template contains variables and/or expressions, which get replaced with values when a +template is rendered; and tags, which control the logic of the template. +The template syntax is heavily inspired by Python.
+Refer to the Jinja Template Reference +for learning the basics.
+See Templating Builtins for a complete list of globals, filters and +tests you can use in your templates.
+
Like described in the introduction, Pharaoh’s workflow distinguishes +two major stages where template rendering is performed. +Please refer to the corresponding sections by following the links:
+++
Alias: first-level templating
+Generation-time templating is used while generating a new Pharaoh project or adding new components to an +existing project, and is inspired by copier, +a Jinja-based library for rendering project templates.
+In Pharaoh, we need first-level templates for two different purposes:
+++Project Templates
+++These are the templates that may be specified when creating a new Pharaoh project. +The composition of all templates in this case must always generate a Sphinx project that contains at +least a
+conf.py
and an index source file, likeindex.rst
.If you’re interested in maintaining your own project template please contact us for support, +but for most use cases our default template
+pharaoh.default_project
should be flexible enough.It is also possible to extend or overwrite parts of the default project, so please also get in touch +if you try to do so.
+Component Templates
+++Component templates are used to render new components into a Pharaoh project.
+The minimal requirement is basically an
+index.rst
file that can be included by the project +(at least what our default Pharaoh template concerns) or a +Template File
Like described here, templates can come in different styles.
+While for Project Templates only registered templates and template directories are allowed/useful, +Component Templates can, in addition to it, be generated using +Template Files/Single-file templates.
+Let’s have a look on how a component template directory may look like:
+++++📁 my_template +├── 📄 index.rst.jinja # reST file with templated content +├── 📄 test_context.py.jinja # Python file with templated content +├── 📁 asset_scripts # folder copied as-is +│ └── 📄 default_plots.py # file copied as-is +└── 📁 [[foo]] # folder with a templated name + └── 📄 [[ bar ]]_script.py # file with a templated name ++
+- +
+
📁 my_template
The top-level directory. The name is arbitrary, it will be replaced by the name of the component during coyping.
+- +
📁 asset_scripts
and📄 default_plots.py
are copied as-is without modifications.- +
+
📄 *.jinja
The content of all files with suffix
+.jinja
are rendered using Jinja.- +
+
📁 [[foo]]
and📄 [[ bar ]]_script.py
File or directory names using
+[[ <context-variable> ]]
are rendered using Jinja while copying.
After rendering like this proj.add_component("dummy", [".../my_template"], render_context={"foo": "a", "bar": "b"}
+a file structure like this is created:
++++📁 dummy +├── 📄 index.rst +├── 📄 test_context.py +├── 📁 asset_scripts +│ └── 📄 default_plots.py +└── 📁 a + └── 📄 b_script.py +
You see the render_context
passed to
+PharaohProject.add_component()
+or template_context
passed to PharaohProject()
are used to
+render file- or folder name as well as file content.
Important
+For Generation-time Templating, Jinja is configured to use brackets for blocks [% %]
and statements [[ ]]
+to not interfere with Build-time Templating, where Jinja will render the same
+files (only reST) again before passing it to
+Sphinx using curly-braces for blocks {% %}
and statements {{ }}
.
So imagine an extreme case of a file index.rst.jinja
with following content:
{{ h1("[[heading_prefix]]%s"|format(ctx.project.component_name)) }}
+
After Generation-time Templating
+(component named Test_1
with render context heading_prefix="PREFIX - "
)
+it results in a file index.rst
with content:
{{ h1("PREFIX - %s"|format(ctx.project.component_name)) }}
+
After Build-time Templating it results in content:
+PREFIX - Test_1
+###############
+
Single-file templates, or also called template files, are smallest and most compact form of a template, but also +limited.
+They are mainly designed to deliver template code and asset script in a single file and contribute content to +an existing component.
+Template files are Python files with suffix .pharaoh.py
those Python code creates assets and
+those module level docstring represents the reST content.
Here an example:
+"""
+{{ heading("My Plots", 2) }}
+
+.. pharaoh-asset:: label == "my_plot"
+"""
+from pharaoh.assetlib.api import metadata_context
+
+import plotly.express as px
+
+
+df = px.data.iris()
+fig = px.scatter(
+ df,
+ x="sepal_width",
+ y="sepal_length",
+ color="species",
+ symbol="species",
+ title=r"A title",
+)
+
+with metadata_context(label="my_plot"):
+ fig.write_html(file="iris_scatter.html")
+
This file will be internally converted to a template directory:
+my_template.pharaoh.py -> index_my_template.rst
+ asset_scripts/my_template.py
+
In order to automatically include all index_*.rst
files in your components index file index.rst
, you
+must add following code:
++++{% for index_rst in fglob("index_*.rst") %} +.. include:: {{ index_rst }} + +{% endfor %} +
Build-time templating is the step where Pharaoh hooks into Sphinx’s build process and renders each documentation +source file before it gets consumed by Sphinx.
+For example the source file index.rst
will be read in, rendered, and finally passed to Sphinx for
+further processing. Additionally for debugging purposes the output from rendering will be stored in the same
+directory as the source file with a .rendered
suffix (e.g. index.rst.rendered
),
+in case the Sphinx build raises errors.
Like mentionen in the Introduction, the main user groups of Pharaoh are +Template Designers and End-Users.
+Template Designers are responsible for creating templates for the End-Users of Pharaoh. +In order to make report generation as easy as possible for the end users, following template design guidelines +have to be considered:
+Tradeoff between flexibility and complexity for end-users
+If the designer hides much of the template code (e.g. through Template Inheritance) +and leaves the end user with just template extensions and configurations, +the reports will gain a lot of maintainability (report can be just re-build with updated base templates).
+If the designer just provides component templates with less abstraction, a lot of template code will +reside in the user’s report projects. This template code can only be updated by re-generating the +report project with updated component templates.
+So the general rule-of-thumb is to put all static template content or content that just needs configuration +in a base template and let the user just overwrite certain sections that are meant for it.
+Provide an abstraction library
+Provide a small Python library to further standardize and reduce the amount of code users have to write in +their asset scripts.
+Build smaller modular templates that can be composed together
Pharaoh templates support Template Inheritance through Jinja, +which is one of the most powerful and useful features of any template engine. +It means one template can inherit from another template.
+Generally, many report pages require the same or a similar layout and content for different pages, +so we use template inheritance to not repeat the same code in each template.
+A base template contains the basic layout which is common to all the other templates, +and it is from this base template we extend or derive the layout for other pages.
+In order to use inherit from base templates, those base templates must be discoverable via lookup paths. +Those lookup paths can be declared through:
++++
+- +
- +
a Sphinx configuration variable
+pharaoh_jinja_templates
inreport-project/conf.py
.This is a list of absolute or relative (to conf.py parent directory) lookup paths for base templates.
+Per default this is set to
+["user_templates"]
, which is an emtpy directory created by the default +Pharaoh Sphinx project template.Base template inside those lookup paths can be referenced via their relative path to the lookup directory, +so if we take this example:
+++++... +📁 user_templates +├── 📄 baseA.rst +└── 📁 others + └── 📄 baseB.rst +Then your templates could inherit from those templates like this:
+++++{% extends "baseA.rst" %} +{% block xyz %} +{# Insert block xyz content from baseA.rst #} +{{ super() }} +Some additional content +{% endblock %} +or
+++{% extends "others/baseB.rst" %} +{% block xyz %} +{# Overwrites block xyz content from others/baseB.rst #} +Some additional content +{% endblock %} +
The following base template defines an immutable page title and three sections with +immutable titles and a block declaration:
+{{ h1("Standardized Report Title") }}
+
+{{ h2("Prologue") }}
+{% block prologue %}
+This is a default content that may be overwritten by the child template
+{% endblock %}
+
+{{ h2("Test Description") }}
+{% block test_description %}
+{% endblock %}
+
+{{ h2("Plots") }}
+{% block plots %}
+{% endblock %}
+
The following child template inherits from base.rst
and overwrites two of the the declared blocks with
+custom content, and extends block prologue
:
{% extends "base.rst" %}
+
+{% block prologue %}
+{{ super() }}
+
+Additional content...
+{% endblock %}
+
+{% block test_description %}
+Some descriptive text...
+{% endblock %}
+
+{% block plots %}
+.. pharaoh-asset:: plot_name == "bla"
+
+{% endblock %}
+
During build-time templating you have access to a variety of context variables via Jinja variable ctx
.
+ctx
is a nested dictionary that allows dotted-access ({{ ctx.project.component_name }}
or
+{{ ctx["project"]["component_name"] }}
) to all static or dynamic rendering context defined by Pharaoh or the user.
ctx # The root variable for accessing rendering context
+ .project # Project related context - set by Pharaoh
+ .instance # The Pharaoh project instance itself
+ .component_name # The name of the current component the template resides in
+ .config # The content of Sphinx's `conf.py`, e.g. `ctx.config.copyright`
+ .user # User defined context of dict variable `pharaoh_jinja_context` in `conf.py`
+ .local # Component's local static & dynamically generated context - see below
+
ctx.local
is a special local context that may be different for each component or even each source file
+(but that’s rarely a use-case) of a component.
+It is composed by reading in so-called “local context files”, that reside next to the file that is
+currently rendered.
+Those files can be:
+++
+- +
YAML files with the naming scheme
+<contextname>_context.yaml
. +So the content of a YAML file calleddefault_context.yaml
would be available viactx.local.default
.++Note
+YAML files are loaded using the OmegaConf library.
+- +
Python files with the naming scheme
+<contextname>_context.py
.Those Python scripts are executed and must create a dict variable called
+context
.Since asset generation has already been executed, these scripts can also access the +
+AssetFinder
instance to find and read assets +to extend the render context.- +
Data context that is registered via the
pharaoh.assetlib.api.register_templating_context()
function.
See Templating Builtins for a complete list of builtin globals, filters and +tests you can use in your templates.
+If you like to add you own, Pharaoh provides some entrypoints in report-project/conf.py
:
pharaoh_jinja_filters
A dict that maps names to filter functions:
+pharaoh_jinja_filters = {
+ "angry": lambda text: text + " 😠" # Usage: {{ "error"|angry }}
+}
+
pharaoh_jinja_globals
A dict that maps names to global functions:
+pharaoh_jinja_globals = {
+ "angry": lambda text: text + " 😠" # Usage: {{ angry("error") }}
+}
+
pharaoh_jinja_tests
A dict that maps names to tests:
+pharaoh_jinja_tests = {
+ "angry_text": lambda text: "😠" in text # Usage: {% if "😠" is angry %}Someone is angry!{% endif %}
+}
+
This user guide targets end-users.
+If you’re about to create templates for end users, +please refer to our Template Designer Guide.
+End-users usually will:
+Generate Pharaoh projects based on the templates the template designers provided
Update project settings
Manage project components (update, add, remove)
Modify/add asset scripts
Overwrite certain pre-defined blocks inside provided templates
Important
+The following guide only shows the API provided by Pharaoh. Keep in mind that the template designers +of your project might provide an additional or different API to create Pharaoh projects.
+The first thing to do is generating a Pharaoh project.
+Note
+This step may be skipped if you already created a project and manage it via GIT for example.
+Create a Python script with following example content to create a new Pharaoh project:
+from pharaoh.api import PharaohProject
+
+# Create a project with a default template optimized for HTML output inside current working directory
+proj = PharaohProject(".")
+
+# For overwriting an existing project
+proj = PharaohProject(project_root="path-to-existing-project", overwrite=True)
+
+# Passing a custom template to extend the default project template "pharaoh.default_project"
+proj = PharaohProject(
+ ".",
+ templates=("pharaoh.default_project", r"C:\...\my-extension-template"),
+ template_context=dict(some_variable_in_extension_template="abc")
+)
+
+# Passing a custom settings YAML file
+proj = PharaohProject(".", custom_settings=r"C:\...\my_custom_settings.yaml")
+
In a terminal with activated virtual environment that has pharaoh installed, type following commands:
+# Create a project with a default template optimized for HTML output inside current working directory
+pharaoh new
+
+# For overwriting an existing project
+pharaoh -p "path/to/existing/project" new --force
+
+# Passing a custom template to extend the default project template "pharaoh.default_project"
+pharaoh new -t pharaoh.default_project -t "C:\...\my-extension-template"
+ -c "{'some_variable_in_extension_template': 'abc'}"
+
+# Passing a custom settings YAML file
+pharaoh -p . new --settings "C:\...\my_custom_settings.yaml"
+
See also
+ +Single settings may be updated during project generation (API & CLI) or afterwards (API or manually).
+This is done via the settings API function
+put_setting(key: str, value: Any)
+or via environment variables. Refer to the Custom Settings section
+for examples.
Besides passing an entire settings YAML file to the project generation, +settings can be set using environment variables during project generation. +Those will be automatically persisted in the settings file.
+# CMD syntax:
+set PHARAOH.LOGGING.LEVEL=INFO
+set PHARAOH.FOO=bar
+set PHARAOH.bla="{'blubb': 123}"
+
+# PowerShell syntax:
+Set-Variable -Name PHARAOH.LOGGING.LEVEL -Value INFO
+Set-Variable -Name PHARAOH.FOO -Value bar
+Set-Variable -Name PHARAOH.bla -Value "{'blubb': 123}"
+
+# Bash syntax:
+env "PHARAOH.LOGGING.LEVEL=INFO" bash
+env "PHARAOH.FOO=bar" bash
+env "PHARAOH.bla={'blubb': 123}" bash
+
+pharaoh new
+
This is done via the settings API function
+put_setting(key: str, value: Any)
+or via environment variables. Refer to the Custom Settings section
+for examples.
from pharaoh.api import PharaohProject
+
+proj = PharaohProject(project_root="some-path")
+proj.put_setting("report.title", "My own title")
+proj.put_setting("toolkits.bokeh.export_png", dict(width=720, height=480))
+
Updating settings via CLI may be done via the env command:
+cd "path/to/project"
+pharaoh env report.title "My own title"
+
+# or
+pharaoh -p "path/to/project" env toolkits.bokeh.export_png "{'width':720, 'height':480}"
+
Just open the file pharaoh.yaml
in your project root and modify its content according to
+YAML Coding Guidelines
+and the OmegaConf library specification.
The main content of a Pharaoh report is determined by components.
+See also
+ +Components may be added or updated using following API/CLI:
+This is done via the component API functions for managing components +documented here.
+Please refer to the component API functions for managing components +here. Relevant sections are:
+ +Please refer to the CLI reference here. Relevant commands are:
+Here some examples:
+pharaoh add -n dummy1 -t pharaoh_testing.simple -c "{'test_name':'dummy'}"
+pharaoh add -n dummy1 -t pharaoh_testing.simple -r "FileResource(alias='foo', pattern='.*')"
+
+pharaoh update-resource -n dummy1 -a foo -r "FileResource(alias='baz', pattern='.*')"
+
+pharaoh add-template -n dummy1 -t pharaoh_testing.simple -c "{'test_name':'dummy'}"
+
+pharaoh remove -f dummy.*
+
Once components are added, users may add or modify existing asset scripts.
+See also
+What’s an Asset? in the +Assets Reference and +Example Asset Scripts.
+If assets scripts are added or modified the templates might need to be updated to include potentially +modified or added assets.
+See also
+Pharaoh Directive on how to include generated assets into your templates +and the Build-time Templating section.
+